ささみ雑記帳

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

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を調整。