ささみ雑記帳

少し長くなりそうな色々を書き留めておくためのノート。

ScriptableObjectにステージ配置データを保存、復元する仕組みの話

ブログ名を少し短くしました。(自分で読みにくかった…)

ステージ読込周りで、ステージ構成データみたいなScriptableObjectを割とよくつくるので、今回はその話です。

ステージの配置パーツをつくる

まず、ステージ内に配置する対象の「キャラ」「アイテム」といった個々のパーツは、

  • このコンポーネントが予め設定されている
  • こういうデータ初期化が必ずされている

みたいにある程度決まった形に制限されたパーツであって欲しいので、それぞれprefabとして作ることがほとんどです。
ここまでは多くの人の共通見解だと思います。

ステージの配置パーツをどう持たせるか問題

次に、これらの配置パーツを複数組み合わせて固有のステージデータを作ることを考えます。
まず思いつくのは、

  • キャラやアイテムを多数配置した、1ステージにつき1つのprefabをつくり、それをInstantiateする
  • キャラやアイテムを多数配置した、1ステージにつき1つのシーンをつくり、それをシーン読込する

辺りの統合型…100%自由に配置しちゃうぜ!方式だと思います。

このアプローチで進める場合、キャラやアイテムという単位で一度固めたprefabを複製しまくって配置する、といったワークフローになるのかな…と思うのですが、これをやると数が増えたり、パーツprefabに後で変更が加わったりした場合、問題が起こりそうです。

  • 複製したキャラ1000体に、実はこのコンポーネントが共通で必要だったので、手動で付け直さないといけない…
  • 複製したキャラ1000体のうち1体だけ、誤って必須コンポーネント外しちゃった。ごめんねてへぺろ

みたいな。後日発覚する拡張問題、ヒューマンエラーなどは普通に起こりうる話で。

パーツprefab+配置のみの情報でロードするアプローチの検討

「キャラ」「アイテム」といった配置パーツは、後日更新があり得るものなので、ある時点で完成品としてコピーするのではなく、通常通りのprefab(複製元)として扱い、自動生成する仕組みが欲しくなってきます。 実行時ではなく、ステージデータの作成中(非実行時)にです。

そこで、エディタ拡張でボタンを押すとprefabとして生成する風なアプローチを考えてみました。
こんな感じのです。

  • キャラやアイテムの配置情報だけをステージ配置データ(マスターデータ)として持つ
  • ステージ配置データの持たせ方はScriptableObject
  • インスペクタをエディタ拡張して、"Save"を押したらHierarchyからステージを読み取って位置を抽出、保存
  • インスペクタをエディタ拡張して、"Load"を押したら配置データにprefabをInstantiateして復元

下準備:ステージ配置パーツprefabの用意

これを実際やってみます。 ステージ配置周りの仕様は、ザックリ以下のように決めました。

  • 複数種類ある前提で試すため、CharacterとItemの2つのprefabをつくる
  • Resourcesフォルダ内にprefabを入れてそこから読む
  • Hierarchyウィンドウ側(実際に配置した際)は、"Stage"という空のGameObjectをつくってその下に配置
  • prefab化したGameObjectには、種類を識別するためStageCharacter, StageItemスクリプトを貼り付ける

Resourcesの下にこう入れておいて
f:id:sasanon:20181122025304p:plain

こんな風に生成されるイメージじゃ。
f:id:sasanon:20181122025309p:plain

配置データのオブジェクト名は、キャラ名やモンスター名、アイテム名が設定されると考えられるので、貼り付けた固有スクリプトで識別します。

StageCharacter, StageItemスクリプトについても、実際にはガッツリ固有処理が入る想定ですが、今回はそこは本筋ではないので、

using UnityEngine; public class StageCharacter : MonoBehaviour {}

using UnityEngine; public class StageItem: MonoBehaviour {}

これで。

ステージ配置データを定義

ScriptableObjectをステージ配置データ型DataStageをつくります。
gist.github.com クラスというよりも構造体のような状態。
DataCharacterとDataItemという子クラスの配列を持たせています。

4行目に書かれている通り、"Stage Data"という名前でアセットとして生成できるようになります。

f:id:sasanon:20181122031331g:plain

ステージ配置データの拡張エディタ処理を定義

Editorフォルダをつくり、その下にDataStage型の拡張エディタ用クラスをつくって、

f:id:sasanon:20181122031620p:plain

インスペクタの拡張処理を書き書き。

gist.github.com

Hierarchy上から”Stage”という名前になっているオブジェクトを探し出し、
その子階層にある"StageCharacter"や"StageItem"コンポーネント一覧を取得。

インスペクタには、まずSave/Loadボタンを表示します。

  • Saveボタンで、配置パーツ種別毎にTransformパラメータとオブジェクト名を保存。
  • Loadボタンで、配置パーツ種別毎にStage配下の当該種別をクリア、再生成して位置を設定。

してます。(ボタン処理は確認ダイアログを出したほうが良いです。その辺簡易版です…)
最後にDrawDefaultInspector();でデフォルトのインスペクタを表示しています。

実際に動かしてみます。
f:id:sasanon:20181122033115g:plain

Saveで配置情報が配列に保存され、Hierarchyから消しても、Loadでprefab再生成されます。

ステージ配置データできた!

そんな感じで、ステージ配置データを保存、復元する仕組みができました。

  • 複製したキャラ1000体に、実はこのコンポーネントが共通で必要だったので、手動で付け直さないといけない…

 →普通にprefabにコンポーネント付けて再Loadしたら直るよ

 →スクリプトで自動生成なので、こういうヒューマンエラーは防止できるよ

ゲームフローから実際に読み込む場合は、Load部分をそのままメソッド化して、ステージ配置データのアセットを参照、Loadメソッドを呼び出せば良さげですね。

今回やらなかったけどできそうなステージ設定とか

  • 固定扱いのステージオブジェクト
    今回のつくりでは、"StageCharacter"や"StageItem"コンポーネントが設定されたもののみを追加削除対象とするため、何も設定しなければ固定オブジェクトとして残り続けます。マップ地形は固定で…みたいなハイブリッドも問題なくできそうです。

  • 複数から選択扱いのステージオブジェクト
    StageData型は普通にクラスなので、当然ながら配置情報以外も設定できます。例えばenumでマップ種別を定義して、予め用意したマップ種別の中からひとつだけをGameObject.SetActive(true);で有効化、みたいなこともできそうです。

ノイズで背景を歪ませるシェーダーをつくる

ノイズシェーダーを歪みマップに応用する話です。
元記事
sasanon.hatenablog.jp

背景も歪めたい

計算で生成したノイズを応用するシリーズ、前回はノイズでアルファ加算画像を歪めるシェーダーをつくったので、次は背景、つまり「それまでの描画結果」を歪めるのもやってみたくなるのが人情ですね。
描画結果を取得するには、Unityシェーダーに用意されている特殊パスGrabPassを利用するのが手っ取り早いです。

docs.unity3d.com

全部書いてあってすごい。これはもう恩恵に預かりましょう。
ちなみにGrabは「つかむ」という意味。

公式リファレンスに書いてある通りですが、GrabPassの中に掴んだテクスチャ名を指定する使い方と、指定しない使い方があります。

この特性はいくつか留意点を示唆しています。

実行負荷としては、

  • テクスチャ名を指定しない場合、GrabPassシェーダー適用オブジェクト描画毎に、毎回つかむ(オブジェクト数によって処理負荷が増大する。重くなる)
  • テクスチャ名を指定する場合、フレーム内で1回だけ実行されるため、処理負荷は固定となる

一方、つかむ背景テクスチャ内容は、

  • テクスチャ名を指定しない場合、GrabPassシェーダー適用オブジェクト描画毎に、常に直前までの描画結果をつかむ。意図した描画結果が得やすい。
  • テクスチャ名を指定する場合、フレーム内で最も速く描画されるGrabPassシェーダー適用オブジェクト描画時に、それまでの描画結果をつかむ。以降は同じ描画結果を使う。(GrabPassシェーダー適用オブジェクトを複数配置する場合、カメラからの距離、それによる描画順、など気を遣う部分が増える)

とそれぞれメリット、デメリットがあります。これは使いたいシーンに応じてケースバイケースだと思います。
(別のテクスチャ名を指定した異なるGrabPassシェーダーを使えば、背景テクスチャ内容も別々に管理できるかな…? その辺り未検証)

GrabPassで描画結果をつかむ

ノイズ歪みシェーダー(アルファ加算画像用)を元に、歪ませる対象を書き換えることを考えます。
まず、普段書いているPassブロック文の前にGrabPassを足します。
今回はテクスチャ名を指定する書き方で書いてみます。

        GrabPass {
            "_BackgroundTexture"
        }

        Pass
        {
            ...

これでGrabPass 、(普段書き直している)Pass、の順に実行されるはず。

続いて、Pass内のパラメータを書き換え。
_MainTex, _BaseColor, _AddAlphaColorの使わないので消して、代わりにGrabPassで指定した_BackgroundTextureを追加します。

            sampler2D _BackgroundTexture;

            fixed4 _NoiseTilingOffset;
            fixed4 _NoiseSizeScroll;
            fixed _DistortionPower;

vertexシェーダーからfragmentシェーダーへの受け渡し部分ですが、GrabTextureのUV値はxyzwフルに使用するようなので、ここは素直に増やします。
シンプルにするため頂点カラーも削除しました。

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float4 vertex : SV_POSITION;
                fixed4 uvgrab : TEXCOORD0;
                fixed2 uvdist : TEXCOORD1;
            };

vertexシェーダーのメインテクスチャのUV変換、fragmentシェーダーのテクスチャ取得処理の部分は、公式のGrabTextureを使う処理をそのまま持ってきて差し替えます。ノイズで法線マップをつくって歪める周りの処理は、ノイズでアルファ加算画像を歪めるシェーダーから全く変えずにそのまま使っています。

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uvgrab = ComputeGrabScreenPos(o.vertex);
                o.uvdist = TRANSFORM_NOISE_TEX(v.uv, _NoiseTilingOffset, _NoiseSizeScroll);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 dist = normalNoise(i.uvdist, _NoiseSizeScroll.xy);   // perlinノイズで算出した法線を得る
                dist = dist * 2 - 1;                                        // 範囲を0.0~1.0から-1.0~1.0へ変換
                dist *= _DistortionPower;                                   // 歪み強度を乗算(歪み強度をシェーダーパラメータとして調整可能にする)

                i.uvgrab.xy += dist.xy;                                     // 歪み量だけ、メインテクスチャのUVをずらす

                fixed4 color = tex2Dproj( _BackgroundTexture,UNITY_PROJ_COORD(i.uvgrab) );
                return color;
            }

これで、算出したノイズ画像で背景を歪ませるシェーダーができました。
f:id:sasanon:20181024000630g:plain

ノイズ歪みシェーダーで水を描く

ノイズを拡張した際のUVアニメーションでそれっぽい動作確認をしましたが、この背景版ノイズ歪みシェーダーを使うと、流水表現ができます。
ノイズでアルファ加算画像を歪めるシェーダーで描いた炎と同様、TilingやDistortionPowerのパラメータ調節で様々な水になります。
f:id:sasanon:20181024000651g:plain

ノイズを法線マップとして、水にSpecularライティングを足してみる

水が表現できるなら、キラキラ光るようにもしたくなってきます。
歪みマップと法線マップは非常に似ているので、歪み成分を法線マップとして扱えば、ノイズでBumpマッピング的なこともできます。

…が、ライティングまで解説すると大変なので(というか正直ノイズ~歪みマップでかなり力尽きたので)…ここでは解説を諦めつつ、Specularライティングと、その前提としてのBumpマッピング的な処理を足すに留めておきます。

シェーダーパラメータとして、_NormalScale(法線マップ強度)、_SpecularPower(スペキュラ強度)、_Shininess(スペキュラの反射の鋭さ)を追加します。

    Properties
    {
        _NoiseTilingOffset ("NoiseTex Tiling(x,y)/Offset(z,w)", Vector) = (0.1,0.1,0,0)
        _NoiseSizeScroll   ("NoiseTex Size(x,y)/Scroll(z,w)"  , Vector) = (16,16,0,0)
        _DistortionPower   ("Distortion Power", Float ) = 0
        _NormalScale       ("Normal Scale", Float ) = 0
        _SpecularPower     ("Specular Power", Range(0.0, 1.0) ) = 0
        _Shininess         ("Shininess", Range(0.001, 1.0) ) = 0
    }
    ...
        Pass
        {
            ...
            sampler2D _BackgroundTexture;

            fixed4 _NoiseTilingOffset;
            fixed4 _NoiseSizeScroll;
            fixed _DistortionPower;

            fixed _SpecularPower;
            fixed _Shininess;
            fixed _NormalScale;
            ...
  • vertexシェーダーで受け取る情報としてnormalとtangentを追加
  • vertexシェーダーからfragmentシェーダーに渡す情報として、bumpマッピング用の接空間からワールド空間への変換ベクトル(tangent, binormal, normal)を追加
  • vertexシェーダーからfragmentシェーダーに渡す情報として、ライティング用にワールド空間視線ベクトルとワールド空間ライトベクトルを追加

します。

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                fixed4 uvgrab : TEXCOORD0;
                fixed2 uvdist : TEXCOORD1;
                fixed3 tangentToWorld[3] : TEXCOORD2;
                fixed3 viewWorld : TEXCOORD5;
                fixed3 lightWorld : TEXCOORD6;
            };

vertexシェーダーで、接空間からワールド空間へ変換するためのtangent/binormal/normalと、ライティングのためのワールド空間でのview, lightベクトルを設定します。

            v2f vert (appdata v)
            {
                v2f o;
                ...
                // tangent to world
                fixed3 normalWorld   = UnityObjectToWorldNormal(v.normal);
                fixed4 tangentWorld  = fixed4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);
                fixed3 binormalWorld = cross(normalWorld, tangentWorld.xyz) * (tangentWorld.w * unity_WorldTransformParams.w);
                o.tangentToWorld[0].xyz = tangentWorld;
                o.tangentToWorld[1].xyz = binormalWorld;
                o.tangentToWorld[2].xyz = normalWorld;

                // world-space view and light vector
                fixed3 posWorld = mul(unity_ObjectToWorld, v.vertex);
                o.viewWorld  = normalize(UnityWorldSpaceViewDir(posWorld));
                o.lightWorld = normalize(UnityWorldSpaceLightDir(posWorld));
                return o;
            }

fragmentシェーダーで、normalNoise関数で取得したdist値を(本来通り)法線マップとして扱い、ワールド空間での法線を得ます。その法線に基づいてSpecularライティング(Phongモデル)を計算、ライティング結果として加算合成します。

            fixed4 frag (v2f i) : SV_Target
            {
                ...
                fixed4 color = tex2Dproj( _BackgroundTexture,UNITY_PROJ_COORD(i.uvgrab) );

                // world-space normal
                fixed3 normalXY = i.tangentToWorld[0].xyz * dist.x + i.tangentToWorld[1].xyz * dist.y, normalZ = i.tangentToWorld[2].xyz * dist.z;
                fixed3 normalWorld = normalize(normalXY * _NormalScale + normalZ);

                // phong specular
                half NdotL = max(0, dot (normalWorld, i.lightWorld));
                float3 R = normalize( -i.lightWorld + 2.0 * normalWorld * NdotL );
                float3 spec = pow(max(0, dot(R, i.viewWorld)), _Shininess * 10) * _SpecularPower * 10;
                color.rgb += spec;

                return color;
            }

左がSpecularライティングあり、右がライティングなしです f:id:sasanon:20181024012117g:plain

ノイズ歪みシェーダー(GrabTexture版)のソースコード

例によって、同じ階層に元記事のSasamiNoise.cgincと一緒に置くことで動くはずです。
gist.github.com

ノイズでアルファ加算画像を歪ませるシェーダーをつくる

ノイズシェーダーを歪みマップに応用する話です。
元記事

テクスチャと色指定でアルファ加算する

アルファ加算は、炎などを表現するときによく用いられる加算合成方法で、
元画像のアルファ値(不透明度)が高い部分ほど、色を強く加算します。
f:id:sasanon:20181023010052p:plain 左から、Default-Particleテクスチャをそのまま表示、ベースカラーで赤を乗算、
ベースカラーを付けた上に黄色のアルファ加算カラーを加算、の状態です。
アルファ加算画像を表示するシェーダーを書いてみます。

    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _BaseColor         ("Base Color"     , Color ) = (1,1,1,1)
        _AddAlphaColor    ("Add Alpha Color", Color ) = (0,0,0,1)
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" }
        LOD 100
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            ・・・
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _BaseColor;
            fixed4 _AddAlphaColor;
            ・・・

メインテクスチャ(_MainTex)、ベースカラー(_BaseColor)、アルファ加算カラー(_AddAlphaColor) をパラメータとして追加します。TagsやBlendは透過色を使える設定にしておきます。

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 color = tex2D(_MainTex, i.uv.xy);
                color *= _BaseColor;
                color.rgb += _AddAlphaColor * color.a;
                return color;
            }

fragmentシェーダーでは、メインテクスチャ色にベースカラーを乗算して乗せた後、アルファ加算カラーを足し込みます。
アルファ加算カラーはメインテクスチャのアルファ値と乗算することで、
アルファ値が大きい部分ほど強く色が足されます。
PhotoShopなどの色範囲0-255は、シェーダー内では0.0~1.0範囲に対応するので、  乗算されると暗く、加算されると明るくなります)

歪みマップの考え方

こうしてできたアルファ加算なDefault-Particleテクスチャを、ノイズを利用して歪ませます。
よくある歪みマップは、以下のように値を扱います。
PhotoShopなどの色範囲0-255は、シェーダー内では0.0~1.0範囲になります)
・Rチャンネル(RGB=XYZの対応ならX)が0.5より大きいほど、メイン画像のUV値をU(X)+方向にずらす
・Rチャンネル(RGB=XYZの対応ならX)が0.5より小さいほど、メイン画像のUV値をU(X)-方向にずらす
・Gチャンネル(RGB=XYZの対応ならY)が0.5より大きいほど、メイン画像のUV値をV(Y)+方向にずらす
・Gチャンネル(RGB=XYZの対応ならY)が0.5より小さいほど、メイン画像のUV値をV(Y)-方向にずらす
f:id:sasanon:20181023012242p:plain
これは、法線(=面の傾きを表す方向)マップと非常によく似た扱い方で、
法線のRG値を歪みマップ情報として扱うと都合が良いです。

※法線マップの場合、RGチャンネルに加えてBチャンネルが
 貼り付けた面のモデル上の法線(面の傾き方向)=Z+方向を表す1.0となります。
 法線マップを画像として見ると青っぽいのはこのためで、全く傾きのない面は全ピクセルの色が(0.5, 0.5, 1.0)になります。
f:id:sasanon:20181023012629p:plain

ノイズで生成した法線を歪みマップとして扱う

ノイズシェーダーをつくった際、位相をずらしてXYそれぞれ異なる乱数を生成して、
法線をつくっていました。これを歪みマップとして扱うことを考えてみます。

ノイズテクスチャのTiling/Offset(_NoiseTilingOffset)とサイズ、UVスクロール速度(_NoiseSizeScroll)、さらに歪み強度(_DistortionPower)をパラメータとして追加します。

    {
        _MainTex ("Texture", 2D) = "white" {}
        _BaseColor         ("Base Color"     , Color ) = (1,1,1,1)
        _AddAlphaColor    ("Add Alpha Color", Color ) = (0,0,0,1)

        _NoiseTilingOffset ("NoiseTex Tiling(x,y)/Offset(z,w)", Vector) = (0.1,0.1,0,0)
        _NoiseSizeScroll   ("NoiseTex Size(x,y)/Scroll(z,w)"  , Vector) = (16,16,0,0)
        _DistortionPower   ("Distortion Power", Float ) = 0
    }
    SubShader
    {
            ・・・

        Pass
        {
            ・・・
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _BaseColor;
            fixed4 _AddAlphaColor;

            fixed4 _NoiseTilingOffset;
            fixed4 _NoiseSizeScroll;
            fixed _DistortionPower;
            ・・・

メインテクスチャとノイズテクスチャでUV値が2つになるので、vertexシェーダーから受け渡すUVを拡張します。

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 uv : TEXCOORD0;
            };
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
                o.uv.zw = TRANSFORM_NOISE_TEX(v.uv, _NoiseTilingOffset, _NoiseSizeScroll);
                return o;
            }

fragmentシェーダーでは、まずノイズテクスチャのUV値から法線マップ(≒歪みマップ)を計算します。
値の範囲が0.0~1.0(中央値0.5)なので、これを-1.0~1.0(中央値0.0)に変換し、歪み強度を掛けたものが歪み量(dist)になります。
こうして算出した歪み量の分だけ、メインテクスチャを参照するためのUVをずらすことで、歪んだ画像を得ます。

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 dist = normalNoise(i.uv.zw, _NoiseSizeScroll.xy);    // perlinノイズで算出した法線を得る
                dist = dist * 2 - 1;                                        // 範囲を0.0~1.0から-1.0~1.0へ変換
                dist *= _DistortionPower;                                   // 歪み強度を乗算(歪み強度をシェーダーパラメータとして調整可能にする)

                i.uv.xy += dist.xy;                                         // 歪み量だけ、メインテクスチャのUVをずらす

                fixed4 color = tex2D(_MainTex, i.uv.xy);
                color *= _BaseColor;
                color.rgb += _AddAlphaColor * color.a;
                return color;
            }

ノイズ歪みシェーダーで炎を描く

このノイズ歪みシェーダーで、先ほどのアルファ加算したDefault Particleテクスチャを歪ませると、炎ができます。
f:id:sasanon:20181023015928p:plain Tiling値を大きくすると急で細かい歪み方となり、0に近づける(絶対値を小さくする)と広く緩やかな歪みになります。
また、Distortion Powerの強さによって炎の歪み具合も変わります。
ノイズ=歪みマップは上方向にUVスクロールさせることもできるので、
f:id:sasanon:20181023020154g:plain
燃えるようなアニメーションも可能です。

ノイズ歪みシェーダーで煙を描く

このノイズ歪みシェーダーですが、Particle Systemに適用することもできます。
パーティクル設定カラーは、どうやら頂点カラーとして入力されるらしいので、これを乗算するように直してみます。

            struct appdata
            {
                ...
                float4 color : COLOR;
            };

            struct v2f
            {
                ...
                float4 color : TEXCOORD1;
            };

            v2f vert (appdata v)
            {
                ...

                o.color = _BaseColor * v.color;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                ...

                color *= i.color;
                color.rgb += _AddAlphaColor * color.a;
                return color;
            }

Particle SystemのRendererにMaterialをセットすれば準備完了。
Particle Systemの設定値は多すぎるので割愛しますが、Color over Lifetimeで透明、不透明、透明と遷移させることで煙のような表現になります。
f:id:sasanon:20181023022703g:plain
煙量マシマシ化。
f:id:sasanon:20181023022732g:plain

ノイズ歪みシェーダー(アルファ加算画像)のソースコード

同じ階層に、元記事のSasamiNoise.cgincと一緒に置くことで動くはずです。
gist.github.com

ノイズでグラデーション着色するシェーダーをつくる

ノイズを勉強した話の応用で、ノイズで着色して色々つくってみます。
前回の記事 sasanon.hatenablog.jp

ノイズの白黒に別の2色を割り当ててグラデーションする

このノイズを表示するだけシェーダーを元に、白黒に別の色を割り当てられるようにします。
変更点はあまりなくて、_BaseColor と _GradationColor パラメータを加えて、

    Properties
    {
        _NoiseTilingOffset ("NoiseTex Tiling(x,y)/Offset(z,w)", Vector) = (0.1,0.1,0,0)
        _NoiseSizeScroll   ("NoiseTex Size(x,y)/Scroll(z,w)"  , Vector) = (16,16,0,0)
        _BaseColor         ("Base Color"     , Color ) = (1,1,1,1)
        _GradationColor    ("Gradation Color", Color ) = (0,0,0,1)
    }

色をlerp(線形補間)します。

            fixed4 frag (v2f i) : SV_Target
            {
                fixed p = perlinNoise(i.uv, _NoiseSizeScroll.xy);
                //return fixed4 ( p, p, p, 1 );   // 白黒のノイズ画像を生成
                return lerp( _GradationColor, _BaseColor, p );   // 任意の2色のノイズ画像を生成
            }

白黒の入力値を別の色に置き換える考え方は、肉シェーダーのときもやっていましたね。
sasanon.hatenablog.jp

ノイズグラデーションで空を表現する

PhotoShop的にはパーリンノイズは雲模様フィルタなので、もちろん雲のある空が表現できます。
そしてノイズ周りでUVスクロールできるようにしたので、動きます。
f:id:sasanon:20181022034820g:plain

ノイズグラデーションのTilingを調節して板や髪のテクスチャを表現する

Tilingは1未満の範囲では、拡大縮小のような使い方ができます。
この拡大率をうまく変えると、色々と表現の幅が広がります。
例えば、(0.1, 0.02)くらい(UV比5:1)で色をうまく設定すると、板のようなテクスチャ感になりますが、
f:id:sasanon:20181022035633p:plain
(1.0, 0.05)くらい(UV比20:1)で色も変えると、キャラの髪に使えそうなテクスチャになります。
f:id:sasanon:20181022035657p:plain
(調整する手腕が問われる…ッ)

ノイズグラデーションで風を表現する

シェーダーをもう少し拡張すると、風のような筋も表現できます。
まず、透過色が使えるように設定を変更。

        Tags { "RenderType"="Transparent" }
        LOD 100
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

この時点で、透過色が使えるようになります。
f:id:sasanon:20181022050859g:plain
…これだと風というより吹雪っぽい(逆に吹雪表現には使えるかもしれません)。
なので、テクスチャを指定して表示範囲を絞れるようにしてみます。
Unlitシェーダーにある_MainTex周りを復活させて、vertexシェーダーから受け渡すUVを拡張します。
今回は(別名の変数に分けるのが面倒なので)xyにメインテクスチャ、zwにノイズテクスチャのUVを格納してみました。

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 uv : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
                o.uv.zw = TRANSFORM_NOISE_TEX(v.uv, _NoiseTilingOffset, _NoiseSizeScroll);
                return o;
            }

fragmentシェーダーでは、メインテクスチャとノイズテクスチャの色を乗算でブレンドします。

            fixed4 frag (v2f i) : SV_Target
            {
                fixed p = perlinNoise(i.uv.zw, _NoiseSizeScroll.xy);
                return lerp( _GradationColor, _BaseColor, p ) * tex2D(_MainTex, i.uv.xy);
            }

これでテクスチャを指定できるようになりました。
ここに、Unityですぐに使えるDefault-Particleテクスチャを指定してみると、
f:id:sasanon:20181022051238g:plain 表示範囲が絞れて、風エフェクトっぽい表現ができるようになりました。

2色着色する程度だとPhotoShopで実現できる範囲と被るのですが、
UVアニメーションしたり、他テクスチャと合成したり、
単純なものでもシェーダー的な処理が入ると応用範囲が広がります。

ノイズグラデーションのソースコード

同じ階層に、前回記事のSasamiNoise.cgincと一緒に置くことで動くはずです。
gist.github.com

ノイズシェーダーを勉強して拡張した話

最近は初級シェーダー書きとしてシェーダーを勉強してます。
知識おいしい。もぐもぐ。

ノイズシェーダーの基礎勉強

については、主にこちらの記事から学びました。
nn-hokuson.hatenablog.com postd.cc

他色々なサイトも巡っていて、情報が錯綜していて少し混乱していましたが、
パーリンノイズはよくバリューノイズ(Bilinear補完したブロックノイズ)を多オクターブ化したものと
勘違いされている(ので誤情報が多い)らしいですね。これを理解するまで時間がかかりました。

下の画像は左からバリューノイズ(1枚)、バリューノイズ(粒度=オクターブを変えて5枚加算)、
パーリンノイズ(1枚)、パーリンノイズ(5オクターブ)の計算結果です。
f:id:sasanon:20181021235816p:plain
バリューノイズは補完元のブロックノイズ感が見えるのに対し、
パーリンノイズは1枚の時点でランダムな方向に混ざっているのがわかるでしょうか?
それぞれ粒度=オクターブを変えて5枚加算すると、どちらも何となくPhotoShopの雲模様感が出てきますが、
ブロックノイズ感=縦横方向へのラインが見えるか、そうでないかの違いは残っています。

Tilingへの対応

おもちゃラボさんのコードを元に、Unityでテクスチャを扱う際によくあるTiling/Offsetと、UVスクロールへ対応させる拡張をしてみました。
(どれでも良いのですが、以降は5枚加算のパーリンノイズで解説します)

Tilingの拡張を行うためには、仮想的なテクスチャサイズが必要かなと考えました。
まず、vertexシェーダー側でサイズとTiling値とテクスチャサイズを乗算しておき、

// TRANSFORM_TEX相当の処理(vertexシェーダーでの変換)
fixed2 TRANSFORM_NOISE_TEX(fixed2 uv, fixed2 tiling, fixed2 size) {
    // 仮想ノイズテクスチャサイズ, Tilingの適用
    uv = uv * tiling * size;
    return uv;
}

fragmentシェーダー側の乱数算出(rand)内でテクスチャサイズで除算、 frac関数でループする小数値を得てみました。
(計算結果が浮動小数計算の誤差? で1超になることがあったので、調整で0.999...を掛けています)

fixed rand (fixed2 uv, fixed2 size) {
    // UV値をテクスチャサイズ(周期)で割り、Tiling値で繰り返すUV値にする
    uv = frac(uv/size);
    return frac(sin(dot(frac(uv/size), fixed2(12.9898,78.233))) * 43758.5453) * 0.99999;
}

効果がわかりやすいよう、仮想テクスチャサイズをものすごく絞って4*4サイズでお試し。
Tiling値を整数で増やすと、ノイズテクスチャがループするような(タイルを敷き詰めたような)見た目になっていきます。
f:id:sasanon:20181022003920g:plain:h200
逆に1未満の値にすると、引き伸ばされます。拡大縮小どちらもUV片軸ずつ行うことができるのも重要な点ですね。
f:id:sasanon:20181022004046g:plain:h200

Tiling/Offset、さらにUVスクロールへの対応

vertexシェーダー側に更に直して、OffsetずらしとUVスクロールができるようにもしておきます。
Tiling, Offset, 仮想テクスチャサイズ、スクロール量がそれぞれUV方向で、パラメータがちょっと多い…

// TRANSFORM_TEX相当の処理(vertexシェーダーでの変換)
fixed2 TRANSFORM_NOISE_TEX(fixed2 uv, fixed4 tilingOffset, fixed4 sizeScroll) {
    // 仮想ノイズテクスチャサイズ, Tiling, Offsetの適用
    uv = uv * tilingOffset.xy * sizeScroll.xy + tilingOffset.zw;

    // Scrollの適用。Tilingに合わせた相対速度でUVスクロールを行う
    uv += fixed2(sizeScroll.z * tilingOffset.x, -sizeScroll.w * tilingOffset.y) * _Time.y;

    return uv;
}

UVスクロール効果をお試し。
sizeScrollはxyが仮想テクスチャサイズ、zwがスクロール値として使っているので、
試しにw=-10で上から下にスクロールしてみるとこんな感じ。
f:id:sasanon:20181022005405g:plain:h200

Tilingによる引き伸ばし+UVスクロールで、流れ落ちる水の感じが出てきました。

ノイズ画像的な法線マップを生成してみる

ノイズっぽい法線マップもつくってみました。

これらのノイズはUV値を元に疑似乱数で生成しているのですが、
法線マップ的にはXYで異なる乱数を得ないと、常に斜め右上か左下方向の
面の傾きベクトルになってしまいます。

そこで、適当に位相をずらしてあげます。
パーリンノイズやバリューノイズでは、格子点(vertexシェーダーで計算した時点でUV値が1.0や2.0となる点)が重要なので、
位相ずらしもとりあえずジャスト1.0ずつずらしてみました。

// 法線マップ用ノイズ(x:0.0 ~ 1.0, x:0.0 ~ 1.0, z:1.0)
fixed3 normalNoise( fixed2 uv, fixed2 size )
{
    fixed3 result = fixed3( perlinNoise(uv.xy, size),
                            perlinNoise(uv.xy+fixed2(1,1), size),
                            1.0 );
    return result;
}

これで、こんな感じに法線マップっぽいノイズ画像が得られます。
Tiling/OffsetとUVアニメーションに対応する拡張と合わせると、流れる水が表現できそうな法線が出来てきます。
f:id:sasanon:20181022020743p:plain:h200f:id:sasanon:20181022020748g:plain:h200

ソースコード

ノイズ算出部分はそれなりに行数が長い処理になるので、
SasamiNoise.cgincファイルとしてシェーダーから分離し、使い回せるようにしておきました。
gist.github.com

そして、Unlitシェーダーから少しだけ改変したシェーダー側(.shader)では、
UnityでUnlitシェーダー生成時に記述されている#include "UnityCG.cginc"を代わりに#include "SasamiNoise.cginc" 、
vertexシェーダーでTRANSFORM_TEXの代わりにTRANSFORM_NOISE_TEX
fragmentシェーダーでperlinNoiseやbumpNoise関数を呼び出して使います。 gist.github.com

Unityで片面ずつ焼ける肉をつくった話

一週間ゲームジャムでトゥーン寄りな(トゥーンシェーディングはしてない)焼ける肉モデルを自作したのですが、
そのときの工程を踏まえながらメイキングを書いてみました。
なお完成ゲームはこちらです。

自分の備忘録としても書いておかないとね!

ゲーム仕様的な要求

当初は肉っぽいテクスチャをググったりもしたのですが、リアリスティックな
生肉写真テクスチャ(ほとんどは有料)しか出てこず、割と絶望でした。

考えたのが肉を焼くゲームなので、何も変化しないアイテムちっくな扱いの肉ではなく、
もう少し多くの仕様を満たす必要がありました。

  • 肉形状のモデルであること(ここはもっとAssetを探せば普通にあると思います)
  • 同じ形状(モデル)で、生肉、焼いた肉、炭化した肉の表現差分できること
  • 肉の色状態が焼け具合によって変化すること
  • できれば、焼肉にしたいので片面ずつ焼けることが望ましい

…この時点で(えっ、これ結構無理ゲーなのでは…?)と感じてました。
でもまぁゲームジャム初日だし、ということでやってみました。
男は度胸。何でも試してみるもんさ。

工程をざっくりまとめ

一応メイキングなので、工程全体をわかりやすくまとめてみます。
実際には行ったり来たりしながら調整することが多いかと思います。

  1. モデリング段階
    • テクスチャつくる
      大体こんなテクスチャをつくります
      f:id:sasanon:20180914012714p:plain:w200
    • モデリングする
      大体こんなモデルをつくります
      f:id:sasanon:20180920040906p:plain:w200
  2. Unity取り込み段階
    • 取り込み設定
      スケーリングと軸設定(上方向をY軸とするかZ軸とするか、左手座標か右手座標か)を合わせます
    • Unity上でマテリアル、シェーダーの割り当て
      Unity側ではマテリアル上でシェーダーを選択、シェーダーパラメータとしてテクスチャ設定となるので、
      その辺りを調整します
      Unity用シェーダーをAssetとして買ったり自作したものを使う場合がある(今回も自作シェーダーである)ため、
      最終見た目調整はエンジン上で行う必要があります
      f:id:sasanon:20180922054551g:plain:h200

肉テクスチャづくり

背景を塗り、線を描く

まずはPhotoShopで512px四方のテクスチャを新規作成し、背景レイヤーを赤で塗り潰します。
線を描く用の新規レイヤーを作成し、太さ36のブラシを選択してマウスでギュッギュと。
f:id:sasanon:20180914011600p:plain:h200
…子供の頃ペイントで書いた風の途中経過ですが、気にせず突き進みます。
引いた線を同じく太さ36の消しゴムで、先を尖らせたり歪ませたりします。
f:id:sasanon:20180914011946p:plain:h200
この手書きを繰り返して、線レイヤーを複数つくっておきます。

線を増やす

複数つくった線レイヤーを、更に複数にコピーします。
増やした線レイヤーをランダムサイズで拡大縮小、更に30%,50%,70%からランダムに透過率を変え、
テクスチャ全体に散らします。
今回は手書きで3パターンつくって、2回ずつコピー、計9レイヤーでやってみました。 f:id:sasanon:20180914012724p:plain:h200

肉モデルづくり

基本図形の選択、マテリアルとテクスチャ割当

Metasequoiaで肉の成形をします。 まずは基本プリミティブを作成。
このとき円柱ではなく直方体を選択することで、今回はUVマップ調整をしなくて済むように楽します。
実際にテクスチャを割り当てて、表面や裏面にテクスチャがそのまま出ることを確認。
側面はあまり厚くない想定なので、側面部分のUVは気にしないことにします。
f:id:sasanon:20180920033622p:plain:w200f:id:sasanon:20180920033629p:plain:w200

成形1:角を取って丸くする

真上から視点に切り替え(メタセコではF2キー)、範囲選択(Rキー)で4隅の裏表両方の頂点を選択します。
同じく真上から視点で、拡大縮小(Qキー)を使い、選んだ頂点のX軸、Z軸方向に縮めます。
4隅を選択していれば原点中心に縮小できるため、角を取って丸くする操作になります。
f:id:sasanon:20180920034657p:plain:w200f:id:sasanon:20180920034700p:plain:w200f:id:sasanon:20180920034702p:plain:w200
選択する頂点の範囲を広げて、さらに丸めます。そして、拡大縮小機能で押し潰して楕円形に。
f:id:sasanon:20180920035718p:plain:w200f:id:sasanon:20180920035721p:plain:w200f:id:sasanon:20180920035909p:plain:w200

成形2:適度に歪ませる

ハンバーグならこの形で完成でも良いのですが、焼肉とかステーキといった切り落としの肉にしたいので、
最後に敢えて不均一になるように歪ませます。半分くらいの範囲を適度に縮小してみたり。
f:id:sasanon:20180920040901p:plain:w200f:id:sasanon:20180920040906p:plain:w200
肉の形ができました。

肉シェーダーづくり

肉の形になったモデルを、Unityさんが認識できそうな形式(.3dsとか.maxとか)に出力して取り込みます。
f:id:sasanon:20180922033408p:plain:h300f:id:sasanon:20180922034239p:plain:h300
マテリアルを付け替え、シェーダーはテクスチャ+乗算カラーが乗せられそうなStandardを選択して、
乗算カラーで焼いた肉の色がキレイにできないか模索したのですが、中々うまくいきません。
f:id:sasanon:20180922034939p:plain:h300
赤身部分を焼けた色=茶色を狙って調整すると、脂肪まで暗い色になってしまいますが、
焼いた状態の脂肪の色はもっと明るい色のはずです。

シェーダー1:赤身肉と脂肪の2色をパターンテクスチャで線形補間してみる

焼いた肉の状態では、赤身肉と脂肪の2色が指定できればそれっぽくなりそうでした。
そんなわけで、肉色を白黒パターンテクスチャ+2色を指定できる専用シェーダーで出来ないか試します。
パターンテクスチャは元の肉テクスチャの背景レイヤーを黒で塗り直したものを出力。
f:id:sasanon:20180914041135p:plain:w200
Unlit Shaderを新規作成して、まずは赤身肉カラー(_MeatColor)、脂肪カラー(_FattyColor)を入力にします。

    Properties {
        _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}
        _MeatColor ("Meat Color",  Color) = (0,0,0,1)
        _FattyColor("Fatty Color", Color) = (1,1,1,1)
    }

フラグメントシェーダーでパターンテクスチャを参照し、
黒(0.0)に近いほど赤身肉の色(_MeatColor)、白(1.0)に近いほど脂肪の色(_FattyColor)となるlerp(線形補間)を掛けます。
lerpは第1引数と第2引数を第3引数(0.0~1.0範囲)の割合で混ぜる関数です。

    fixed4 frag (v2f i) : SV_Target
    {
        fixed f = tex2D(_MainTex, i.texcoord).r;

        // calc meat-fatty color
        return lerp(_MeatColor, _FattyColor, f);
    }

f:id:sasanon:20180922040742p:plain:h300f:id:sasanon:20180922040747p:plain:h300
生肉のときは赤~白、焼肉のときは茶~黄で、それぞれハッキリ色が出せました。

シェーダー2:生肉、焼肉、炭化の3状態を焼き加減パラメータで線形補間してみる

この専用シェーダーを拡張して、肉色を焼き加減パラメーターで色を変化させられるようにしていきます。
赤身肉カラー(_MeatColor)、脂肪カラー(_FattyColor)のペアについて、
生肉(Raw)、焼肉(Grill)、炭化(Carbon)の3状態を同時に設定できるように直します。

    Properties {
        _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}
        _RawMeatColor     ("Raw Meat Color",     Color) = (0,0,0,1)
        _RawFattyColor    ("Raw Fatty Color",    Color) = (1,1,1,1)
        _GrillMeatColor   ("Grill Meat Color",   Color) = (0,0,0,1)
        _GrillFattyColor  ("Grill Fatty Color",  Color) = (1,1,1,1)
        _CarbonMeatColor  ("Carbon Meat Color",  Color) = (0,0,0,1)
        _CarbonFattyColor ("Carbon Fatty Color", Color) = (1,1,1,1)
        _GrillRate        ("Grill Rate",         Range(0,2)) = 0.0
    }

そしてさらに、焼き加減(_GrillRate)パラメータを追加します。 この_GrillRateは0(生肉)~1(焼肉)~2(炭化)の値を取るfloat値を想定しています。
フラグメントシェーダーではそれぞれの状態での色をlerpした後、

        // calc meat-fatty color (raw, grill, and carbon state)
        fixed4 cRaw    = lerp(   _RawFattyColor,    _RawMeatColor, f);
        fixed4 cGrill  = lerp( _GrillFattyColor,  _GrillMeatColor, f);
        fixed4 cCarbon = lerp(_CarbonFattyColor, _CarbonMeatColor, f);

焼き加減0.0~1.0範囲と1.0~2.0範囲でそれぞれlerpします。

        // calc grill color
        fixed4 cRaw2Grill = lerp(cRaw, cGrill, saturate(_GrillRate));
        fixed4 cRaw2Carbon = lerp(cRaw2Grill , cCarbon, saturate(_GrillRate-1));
        return cRaw2Carbon;

saturate関数は中の値を0.0~1.0にclamp(0未満なら0に、1超なら1に)します。

  • 焼き加減0.0~1.0のとき
    最初のsaturate(_GrillRate)でsaturate関数の中が0.0~1.0の範囲となり、生肉色(cRaw)~焼肉色(cGrill)がlerpされます。
    次のsaturate(_GrillRate-1)でsaturate関数の中が0以下となるため、最初のlerp結果が適用されます。

  • 焼き加減1.0~2.0のとき
    最初のsaturate(_GrillRate)でsaturate関数の中が1以上のため、常に焼肉色(cGrill)となります。
    次のsaturate(_GrillRate-1)でsaturate関数の中が(0.0~1.0)の範囲となり、焼肉色(cGrill)~炭化色(cCarbon)がlerpされます。

エディタ上で焼き加減パラメータを動かしてみます。
f:id:sasanon:20180922051239g:plain

シェーダー3:表面、裏面の2面を法線方向の内積で線形補間してみる

さらにシェーダーを拡張して、表裏で異なる焼き加減を表現できるようにします。
焼き加減パラメータ(_GrillRate)を表面(Front)と裏面(Back)に分割します。

    Properties {
        _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}
        _RawMeatColor     ("Raw Meat Color",     Color) = (0,0,0,1)
        _RawFattyColor    ("Raw Fatty Color",    Color) = (1,1,1,1)
        _GrillMeatColor   ("Grill Meat Color",   Color) = (0,0,0,1)
        _GrillFattyColor  ("Grill Fatty Color",  Color) = (1,1,1,1)
        _CarbonMeatColor  ("Carbon Meat Color",  Color) = (0,0,0,1)
        _CarbonFattyColor ("Carbon Fatty Color", Color) = (1,1,1,1)
        _FrontGrillRate   ("Front Grill Rate",   Range(0,2)) = 0.0
        _BackGrillRate    ("Back Grill Rate",    Range(0,2)) = 0.0
    }

表裏の判定。これはモデル取り込み時の面の法線が上向き(0,0,1)か、下向き(0,0,-1)か
によって判断します(ポリゴン面の表裏と混同しないように気をつけます)。

シェーディングと同様の考え方で、オブジェクト空間同士で
法線と表面ベクトル(0,0,1)の内積を取ると-1(完全な裏面)~1(完全な表面)がわかります。
この値を0~1の範囲にclampして(2で割って0.5を足して)、表面率(surface_rate)とします。

    v2f vert (appdata_t v)
    {
        // calc front or back surface rate (in object space)
        o.surface_rate = dot(v.normal, half3(0,0,1))/2.0f + 0.5f;
    }

フラグメントシェーダーでは、表裏それぞれの焼き加減によって出した色を、 表面率(surface_rate)によってlerpします。

    fixed4 frag (v2f i) : SV_Target
    {
        fixed f = tex2D(_MainTex, i.texcoord).r;

        // calc meat-fatty color (raw, grill, and carbon state)
        fixed4 cRaw    = lerp(   _RawFattyColor,    _RawMeatColor, f);
        fixed4 cGrill  = lerp( _GrillFattyColor,  _GrillMeatColor, f);
        fixed4 cCarbon = lerp(_CarbonFattyColor, _CarbonMeatColor, f);

        // calc grill color (front and back)
        fixed4 cFront  = lerp(lerp(cRaw, cGrill, saturate(_FrontGrillRate)), cCarbon, saturate(_FrontGrillRate-1));
        fixed4 cBack   = lerp(lerp(cRaw, cGrill, saturate( _BackGrillRate)), cCarbon, saturate( _BackGrillRate-1));

        // calc surface color
        return lerp(cFront, cBack, i.surface_rate);
    }

くるくる回しながら表裏の焼き加減を変えてみて確認するとこんな感じ。
f:id:sasanon:20180922054551g:plain

上手に焼けましたー。

そんな訳で、「片面ずつ焼ける肉」ができました。完成です!

ゲームジャム中は、シェーダー1の段階まで1日目で、シェーダー2(焼け具合lerp)を2日目に拡張、
シェーダー3(裏表lerp)を4日目に拡張してました。
あとは、スクリプトで今どちらを向いてるかを(transform.upなどで)判定したり、焼け具合を更新(Update)したり、
鉄板に接する面の焼け(焦げ)具合によって煙の色を変えたり、音を鳴らしたり、を実装して、肉焼き行為が実現させています。

トゥーンなシェーディングとか、油量を反映したスペキュラとか、クオリティアップも色々考えてたけど、
割とゲームジャムに間に合わせるのに精一杯だったのと、いらすとやキャラには概ねマッチしたので、結果オーライかもしれない。

時間も限られていた中で、広く浅く色々やった感じのローポリ、ゲーム向け肉づくりとなったので、
誰かの何かの創作の参考になれば幸いです。

おまけ(鉄板のメイキング)

鉄板の作り方もサラッと。
f:id:sasanon:20180922110210p:plain:w200f:id:sasanon:20180922110214p:plain:w200
フォトショでグレースケールでないノイズテクスチャをつくった後、
f:id:sasanon:20180922110222p:plain:w200f:id:sasanon:20180922110230p:plain:w200f:id:sasanon:20180922110238p:plain:w200
Bチャンネルだけ白(1.0)で塗り潰して、即席でザラザラ法線マップをつくります。
f:id:sasanon:20180922110244p:plain:w200
それをStandardシェーダーに適用して、金属にしたいのでMetalicは1に近く、色は黒っぽく、
そしてなんか良い感じにスペキュラが入るようにライト角度やSmoothnessを調整。

一週間ゲームジャム(お題:「あつい」)に参加した話

生きてました。おはようございます。
この度は一週間ゲームジャムに参加したので、振り返りなどしたいと思って戻ってきました。

一週間ゲームジャムに参加しました

一週間ゲームジャムに久しぶりに参加しました。
今回はお題「あつい」ということで、肉を焼くゲーム「にくぶぎょう」をつくりました。

f:id:sasanon:20180912002052g:plain

前回参加はお題「フロー」のときなので、色々久しぶりな感じですね…
仕事が忙しかったりで完成しないターンが続いていたのですが、今回はちょうど時間の取れる時期だったこともあり、完成させられてよかった。

技術解説的な話も書こうと思ったのですが、長くなりそうなので、まずはどんな感じに進めていったのかをザクザク書いて記しておくことにしました。

自分で作業した工程

  • 企画、仕様策定(テキストにごりごり書くだけ…)
  • プログラム(DOTween以外は素Unityで。VisualStudio使用)
  • 作曲(BGM打ち込み。Cubase使用)
  • モデリング(肉。Metasequoia使用)
  • シェーダ(肉シェーダ、煙(加算+歪み)シェーダ。VisualStudioやサクラエディタ使用)
  • シェーダ用のテクスチャ、マップ作成(PhotoShop使用)
  • ステージ配置
  • (雑なスケジュール管理)

アセット等で外部に頼った部分

  • フォント
  • 効果音
  • ボイス
  • イラスト(人の絵)
  • 汎用UIアイコン

各日程でやったこと

1日目(月曜日)

企画立案
最近マイブームの加算歪みシェーダの表現をまだまだ試したい欲があるので、 炎や煙を表現できる暑さ、熱さの方針で焼肉を検討。
モデリングの試作
拾い物の肉テクスチャを貼ったりしつつ、最終的に肉シェーダを試作。 当初不可能と考えていた、肉の焼き色が表現できることを確認。

2日目(火曜日)

肉シェーダ拡張
肉の色問題が解決しそうなので、ゲーム化を視野に入れて肉シェーダを拡張。焼き加減でシームレスに色が変わるようにした。
ステージを簡単に配置。

3日目(水曜日)

アニメーション作成
焼き色表現ができそうなので、ゲーム中で一番行うことになる「肉を返す」操作アニメーションを検討。Animation系コンポーネントも考えたが、面倒になり、最終的に全部DOTweenで実装。

4日目(木曜日)

肉シェーダ再拡張
肉を返せるようになったので、面毎に焼き加減を個別に持たせる実装の必要性が浮上。肉シェーダを更に拡張して対応。
参照関係の構築
皿と肉、肉と鉄板の参照関係を作り始める。かなり難航。

5日目(木曜日)

肉の循環をつくる
肉を可変数オブジェクトとして、皿から肉を供給、肉を食べる処理を実装。皿の配置はいろいろ試作し、皿レーンをつくってそれを複数並べる形に決定。皿を送るアニメをDOTweenでつくり、皿送り時に肉を乗せるリセット処理を書く。
シーンの循環をつくる準備
タイトルUIを仮でざっくりつくる。フォントの素材を探し、焼肉屋なので和風傾向で、タイトルロゴ用と地の文用で仮決定。

6日目(木曜日)

シーンの循環をつくる
いわゆるゲームフローをつくる。昨日つくったタイトルUIに加えてリザルトUIを簡単につくり、シーン遷移を実装。
今回はシーンという状態単位ではなく、シーン間を遷移する変化をDOTweenで書いてそれをメソッドに閉じ込める風にしたところ、以前の参加時よりかなりあっさり書けた印象。タイトルもリザルトも基本Canvas内のUIしか触らないから、Updateをシーン毎に細かく割る必要はないんですね…勉強になった。
参照関係周りのバグ修正
いろいろ挙動が変なので、皿と肉、肉と鉄板をはじめ、参照関係を再整理。参照関係はもうちょっとスマートに書けるようになりたい…力不足感。

7日目(木曜日)
ゲームフローができて完成すること自体は確定したので、この日に入った時点で残作業は明確でした。

サウンド
ここへ来てやっとサウンドを入れる。サウンド入れ込み作業は今回はやや行き当たりばったりで、想定より時間が掛かってしまった。反省点。
フォント同様和風傾向でサウンド素材を探した結果、漢らしいボイスがあったので使わせて頂くことに。BGMは単純な和太鼓ループ音源を探してみたが、豪華すぎたりして少し合わない感じがしたので、今回は自分でつくった方が早いと判断し、Cubase打ち込みで自作。 スクリプトで入れ込み作業中、効果音も必要な部分が浮上したので、肉を焼く音や火が弾ける音を追加で探して入れる。

バランス調整的な作業
食べたときの焼き加減によってスコアを変える対応。バランス調整はだいぶ雑になったかも。ごめんなさい。

操作に対するレスポンスの充実
2日目に入れていた仮の人絵をいい加減ずっと仮すぎるので、いらすとやから探す。表情変化を念頭に探し、酔っぱらいというこの上なくドンピシャなイラストに差し替えることに。食べたとき表情を変化させて戻す処理を追加。
また、せっかく肉の表裏面ごとに焼き加減パラメータを持っているので、鉄板に接する面の焼き加減に応じて、煙パーティクルをモクモクさせる。白多め(水蒸気)~灰少なめ~黒多め(焦げ)という感じ。プレイヤーに危機感をもたせ操作誘導する。

アップロード作業 スクリーンショット撮影、サムネ用の動画撮影、非公開状態で仮アップロードしてWebGLでの動作確認。

振り返ってみて

2日目くらいまでは完成するなんて思ってなかったです。
最初に肉表現できない問題が立ちはだかって、次にアニメーション苦手すぎて肉がつかめない問題が道を阻んで、それから参照関係で長時間悩んで、…よく完成しましたねこれ。

私はプログラマ寄り知識セットなので、視覚的表現で高クオリティを狙うのが苦手なのですが、今回は最初からシェーダーでどこまで行けるかという切り口だったので、見た目クオリティもなかなか拘れたのは大きい成果でした。

今回は特に肉の色(とその変化)を表現するためにシェーダーを書いたので、その過程も記録しておきたい気分。シェーダー楽しいよ! 皆もシェーダー書こう。

おまけ(制作とあまり関係ない話)

今回使わせていただいたボイス、実は個人的に聞き覚えがあって、まさかみ自然保護区さんのスクールアイドルランナーで使われていたと記憶しています。…いやまさか、こんなタイミングでこのボイスに再開するなんて…と謎の感動を覚えていました。
…同人ゲームをよくやるクラスタか、コミケの同人ゲー戦利品配信よく見るクラスタにしか伝わらない話ですねこれ。伝わる人にだけ伝われ。