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の下にこう入れておいて
こんな風に生成されるイメージじゃ。
配置データのオブジェクト名は、キャラ名やモンスター名、アイテム名が設定されると考えられるので、貼り付けた固有スクリプトで識別します。
StageCharacter, StageItemスクリプトについても、実際にはガッツリ固有処理が入る想定ですが、今回はそこは本筋ではないので、
using UnityEngine;
public class StageCharacter : MonoBehaviour {}
using UnityEngine;
public class StageItem: MonoBehaviour {}
これで。
ステージ配置データを定義
ScriptableObjectをステージ配置データ型DataStageをつくります。
gist.github.com
クラスというよりも構造体のような状態。
DataCharacterとDataItemという子クラスの配列を持たせています。
4行目に書かれている通り、"Stage Data"という名前でアセットとして生成できるようになります。
ステージ配置データの拡張エディタ処理を定義
Editorフォルダをつくり、その下にDataStage型の拡張エディタ用クラスをつくって、
インスペクタの拡張処理を書き書き。
Hierarchy上から”Stage”という名前になっているオブジェクトを探し出し、
その子階層にある"StageCharacter"や"StageItem"コンポーネント一覧を取得。
インスペクタには、まずSave/Loadボタンを表示します。
- Saveボタンで、配置パーツ種別毎にTransformパラメータとオブジェクト名を保存。
- Loadボタンで、配置パーツ種別毎にStage配下の当該種別をクリア、再生成して位置を設定。
してます。(ボタン処理は確認ダイアログを出したほうが良いです。その辺簡易版です…)
最後にDrawDefaultInspector();でデフォルトのインスペクタを表示しています。
実際に動かしてみます。
Saveで配置情報が配列に保存され、Hierarchyから消しても、Loadでprefab再生成されます。
ステージ配置データできた!
そんな感じで、ステージ配置データを保存、復元する仕組みができました。
- 複製したキャラ1000体に、実はこのコンポーネントが共通で必要だったので、手動で付け直さないといけない…
→普通にprefabにコンポーネント付けて再Loadしたら直るよ
→スクリプトで自動生成なので、こういうヒューマンエラーは防止できるよ
ゲームフローから実際に読み込む場合は、Load部分をそのままメソッド化して、ステージ配置データのアセットを参照、Loadメソッドを呼び出せば良さげですね。
今回やらなかったけどできそうなステージ設定とか
ノイズで背景を歪ませるシェーダーをつくる
ノイズシェーダーを歪みマップに応用する話です。
元記事
sasanon.hatenablog.jp
背景も歪めたい
計算で生成したノイズを応用するシリーズ、前回はノイズでアルファ加算画像を歪めるシェーダーをつくったので、次は背景、つまり「それまでの描画結果」を歪めるのもやってみたくなるのが人情ですね。
描画結果を取得するには、Unityシェーダーに用意されている特殊パスGrabPassを利用するのが手っ取り早いです。
全部書いてあってすごい。これはもう恩恵に預かりましょう。
ちなみに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;
}
これで、算出したノイズ画像で背景を歪ませるシェーダーができました。
ノイズ歪みシェーダーで水を描く
ノイズを拡張した際のUVアニメーションでそれっぽい動作確認をしましたが、この背景版ノイズ歪みシェーダーを使うと、流水表現ができます。
ノイズでアルファ加算画像を歪めるシェーダーで描いた炎と同様、TilingやDistortionPowerのパラメータ調節で様々な水になります。
ノイズを法線マップとして、水に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ライティングあり、右がライティングなしです
ノイズ歪みシェーダー(GrabTexture版)のソースコード
例によって、同じ階層に元記事のSasamiNoise.cgincと一緒に置くことで動くはずです。
gist.github.com
ノイズでアルファ加算画像を歪ませるシェーダーをつくる
ノイズシェーダーを歪みマップに応用する話です。
元記事
テクスチャと色指定でアルファ加算する
アルファ加算は、炎などを表現するときによく用いられる加算合成方法で、
元画像のアルファ値(不透明度)が高い部分ほど、色を強く加算します。
左から、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)-方向にずらす
これは、法線(=面の傾きを表す方向)マップと非常によく似た扱い方で、
法線のRG値を歪みマップ情報として扱うと都合が良いです。
※法線マップの場合、RGチャンネルに加えてBチャンネルが
貼り付けた面のモデル上の法線(面の傾き方向)=Z+方向を表す1.0となります。
法線マップを画像として見ると青っぽいのはこのためで、全く傾きのない面は全ピクセルの色が(0.5, 0.5, 1.0)になります。
ノイズで生成した法線を歪みマップとして扱う
ノイズシェーダーをつくった際、位相をずらして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テクスチャを歪ませると、炎ができます。
Tiling値を大きくすると急で細かい歪み方となり、0に近づける(絶対値を小さくする)と広く緩やかな歪みになります。
また、Distortion Powerの強さによって炎の歪み具合も変わります。
ノイズ=歪みマップは上方向にUVスクロールさせることもできるので、
燃えるようなアニメーションも可能です。
ノイズ歪みシェーダーで煙を描く
このノイズ歪みシェーダーですが、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で透明、不透明、透明と遷移させることで煙のような表現になります。
煙量マシマシ化。
ノイズ歪みシェーダー(アルファ加算画像)のソースコード
同じ階層に、元記事の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スクロールできるようにしたので、動きます。
ノイズグラデーションのTilingを調節して板や髪のテクスチャを表現する
Tilingは1未満の範囲では、拡大縮小のような使い方ができます。
この拡大率をうまく変えると、色々と表現の幅が広がります。
例えば、(0.1, 0.02)くらい(UV比5:1)で色をうまく設定すると、板のようなテクスチャ感になりますが、
(1.0, 0.05)くらい(UV比20:1)で色も変えると、キャラの髪に使えそうなテクスチャになります。
(調整する手腕が問われる…ッ)
ノイズグラデーションで風を表現する
シェーダーをもう少し拡張すると、風のような筋も表現できます。
まず、透過色が使えるように設定を変更。
Tags { "RenderType"="Transparent" }
LOD 100
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
この時点で、透過色が使えるようになります。
…これだと風というより吹雪っぽい(逆に吹雪表現には使えるかもしれません)。
なので、テクスチャを指定して表示範囲を絞れるようにしてみます。
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テクスチャを指定してみると、
表示範囲が絞れて、風エフェクトっぽい表現ができるようになりました。
2色着色する程度だとPhotoShopで実現できる範囲と被るのですが、
UVアニメーションしたり、他テクスチャと合成したり、
単純なものでもシェーダー的な処理が入ると応用範囲が広がります。
ノイズグラデーションのソースコード
同じ階層に、前回記事のSasamiNoise.cgincと一緒に置くことで動くはずです。
gist.github.com
ノイズシェーダーを勉強して拡張した話
最近は初級シェーダー書きとしてシェーダーを勉強してます。
知識おいしい。もぐもぐ。
ノイズシェーダーの基礎勉強
については、主にこちらの記事から学びました。
nn-hokuson.hatenablog.com
postd.cc
他色々なサイトも巡っていて、情報が錯綜していて少し混乱していましたが、
パーリンノイズはよくバリューノイズ(Bilinear補完したブロックノイズ)を多オクターブ化したものと
勘違いされている(ので誤情報が多い)らしいですね。これを理解するまで時間がかかりました。
下の画像は左からバリューノイズ(1枚)、バリューノイズ(粒度=オクターブを変えて5枚加算)、
パーリンノイズ(1枚)、パーリンノイズ(5オクターブ)の計算結果です。
バリューノイズは補完元のブロックノイズ感が見えるのに対し、
パーリンノイズは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値を整数で増やすと、ノイズテクスチャがループするような(タイルを敷き詰めたような)見た目になっていきます。
逆に1未満の値にすると、引き伸ばされます。拡大縮小どちらもUV片軸ずつ行うことができるのも重要な点ですね。
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で上から下にスクロールしてみるとこんな感じ。
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アニメーションに対応する拡張と合わせると、流れる水が表現できそうな法線が出来てきます。
ソースコード
ノイズ算出部分はそれなりに行数が長い処理になるので、
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を探せば普通にあると思います)
- 同じ形状(モデル)で、生肉、焼いた肉、炭化した肉の表現差分できること
- 肉の色状態が焼け具合によって変化すること
- できれば、焼肉にしたいので片面ずつ焼けることが望ましい
…この時点で(えっ、これ結構無理ゲーなのでは…?)と感じてました。
でもまぁゲームジャム初日だし、ということでやってみました。
男は度胸。何でも試してみるもんさ。
工程をざっくりまとめ
一応メイキングなので、工程全体をわかりやすくまとめてみます。
実際には行ったり来たりしながら調整することが多いかと思います。
-
モデリング段階
-
テクスチャつくる
大体こんなテクスチャをつくります
-
モデリングする
大体こんなモデルをつくります
-
テクスチャつくる
-
Unity取り込み段階
-
取り込み設定
スケーリングと軸設定(上方向をY軸とするかZ軸とするか、左手座標か右手座標か)を合わせます
-
Unity上でマテリアル、シェーダーの割り当て
Unity側ではマテリアル上でシェーダーを選択、シェーダーパラメータとしてテクスチャ設定となるので、
その辺りを調整します
Unity用シェーダーをAssetとして買ったり自作したものを使う場合がある(今回も自作シェーダーである)ため、
最終見た目調整はエンジン上で行う必要があります
-
取り込み設定
肉テクスチャづくり
背景を塗り、線を描く
まずはPhotoShopで512px四方のテクスチャを新規作成し、背景レイヤーを赤で塗り潰します。
線を描く用の新規レイヤーを作成し、太さ36のブラシを選択してマウスでギュッギュと。
…子供の頃ペイントで書いた風の途中経過ですが、気にせず突き進みます。
引いた線を同じく太さ36の消しゴムで、先を尖らせたり歪ませたりします。
この手書きを繰り返して、線レイヤーを複数つくっておきます。
線を増やす
複数つくった線レイヤーを、更に複数にコピーします。
増やした線レイヤーをランダムサイズで拡大縮小、更に30%,50%,70%からランダムに透過率を変え、
テクスチャ全体に散らします。
今回は手書きで3パターンつくって、2回ずつコピー、計9レイヤーでやってみました。
肉モデルづくり
基本図形の選択、マテリアルとテクスチャ割当
Metasequoiaで肉の成形をします。 まずは基本プリミティブを作成。
このとき円柱ではなく直方体を選択することで、今回はUVマップ調整をしなくて済むように楽します。
実際にテクスチャを割り当てて、表面や裏面にテクスチャがそのまま出ることを確認。
側面はあまり厚くない想定なので、側面部分のUVは気にしないことにします。
成形1:角を取って丸くする
真上から視点に切り替え(メタセコではF2キー)、範囲選択(Rキー)で4隅の裏表両方の頂点を選択します。
同じく真上から視点で、拡大縮小(Qキー)を使い、選んだ頂点のX軸、Z軸方向に縮めます。
4隅を選択していれば原点中心に縮小できるため、角を取って丸くする操作になります。
選択する頂点の範囲を広げて、さらに丸めます。そして、拡大縮小機能で押し潰して楕円形に。
成形2:適度に歪ませる
ハンバーグならこの形で完成でも良いのですが、焼肉とかステーキといった切り落としの肉にしたいので、
最後に敢えて不均一になるように歪ませます。半分くらいの範囲を適度に縮小してみたり。
肉の形ができました。
肉シェーダーづくり
肉の形になったモデルを、Unityさんが認識できそうな形式(.3dsとか.maxとか)に出力して取り込みます。
マテリアルを付け替え、シェーダーはテクスチャ+乗算カラーが乗せられそうなStandardを選択して、
乗算カラーで焼いた肉の色がキレイにできないか模索したのですが、中々うまくいきません。
赤身部分を焼けた色=茶色を狙って調整すると、脂肪まで暗い色になってしまいますが、
焼いた状態の脂肪の色はもっと明るい色のはずです。
シェーダー1:赤身肉と脂肪の2色をパターンテクスチャで線形補間してみる
焼いた肉の状態では、赤身肉と脂肪の2色が指定できればそれっぽくなりそうでした。
そんなわけで、肉色を白黒パターンテクスチャ+2色を指定できる専用シェーダーで出来ないか試します。
パターンテクスチャは元の肉テクスチャの背景レイヤーを黒で塗り直したものを出力。
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);
}
生肉のときは赤~白、焼肉のときは茶~黄で、それぞれハッキリ色が出せました。
シェーダー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されます。
エディタ上で焼き加減パラメータを動かしてみます。
シェーダー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);
}
くるくる回しながら表裏の焼き加減を変えてみて確認するとこんな感じ。
上手に焼けましたー。
そんな訳で、「片面ずつ焼ける肉」ができました。完成です!
ゲームジャム中は、シェーダー1の段階まで1日目で、シェーダー2(焼け具合lerp)を2日目に拡張、
シェーダー3(裏表lerp)を4日目に拡張してました。
あとは、スクリプトで今どちらを向いてるかを(transform.upなどで)判定したり、焼け具合を更新(Update)したり、
鉄板に接する面の焼け(焦げ)具合によって煙の色を変えたり、音を鳴らしたり、を実装して、肉焼き行為が実現させています。
トゥーンなシェーディングとか、油量を反映したスペキュラとか、クオリティアップも色々考えてたけど、
割とゲームジャムに間に合わせるのに精一杯だったのと、いらすとやキャラには概ねマッチしたので、結果オーライかもしれない。
時間も限られていた中で、広く浅く色々やった感じのローポリ、ゲーム向け肉づくりとなったので、
誰かの何かの創作の参考になれば幸いです。
おまけ(鉄板のメイキング)
鉄板の作り方もサラッと。
フォトショでグレースケールでないノイズテクスチャをつくった後、
Bチャンネルだけ白(1.0)で塗り潰して、即席でザラザラ法線マップをつくります。
それをStandardシェーダーに適用して、金属にしたいのでMetalicは1に近く、色は黒っぽく、
そしてなんか良い感じにスペキュラが入るようにライト角度やSmoothnessを調整。
一週間ゲームジャム(お題:「あつい」)に参加した話
生きてました。おはようございます。
この度は一週間ゲームジャムに参加したので、振り返りなどしたいと思って戻ってきました。
一週間ゲームジャムに参加しました
一週間ゲームジャムに久しぶりに参加しました。
今回はお題「あつい」ということで、肉を焼くゲーム「にくぶぎょう」をつくりました。
前回参加はお題「フロー」のときなので、色々久しぶりな感じですね…
仕事が忙しかったりで完成しないターンが続いていたのですが、今回はちょうど時間の取れる時期だったこともあり、完成させられてよかった。
技術解説的な話も書こうと思ったのですが、長くなりそうなので、まずはどんな感じに進めていったのかをザクザク書いて記しておくことにしました。
自分で作業した工程
- 企画、仕様策定(テキストにごりごり書くだけ…)
- プログラム(DOTween以外は素Unityで。VisualStudio使用)
- 作曲(BGM打ち込み。Cubase使用)
- モデリング(肉。Metasequoia使用)
- シェーダ(肉シェーダ、煙(加算+歪み)シェーダ。VisualStudioやサクラエディタ使用)
- シェーダ用のテクスチャ、マップ作成(PhotoShop使用)
- ステージ配置
- (雑なスケジュール管理)
アセット等で外部に頼った部分
- フォント
- 効果音
- ボイス
- イラスト(人の絵)
- 汎用UIアイコン
各日程でやったこと
1日目(月曜日)
企画立案
肉モデリングの試作
2日目(火曜日)
肉シェーダ拡張
3日目(水曜日)
アニメーション作成
4日目(木曜日)
肉シェーダ再拡張
参照関係の構築
5日目(木曜日)
肉の循環をつくる
シーンの循環をつくる準備
6日目(木曜日)
シーンの循環をつくる
今回はシーンという状態単位ではなく、シーン間を遷移する変化をDOTweenで書いてそれをメソッドに閉じ込める風にしたところ、以前の参加時よりかなりあっさり書けた印象。タイトルもリザルトも基本Canvas内のUIしか触らないから、Updateをシーン毎に細かく割る必要はないんですね…勉強になった。
参照関係周りのバグ修正
7日目(木曜日)
ゲームフローができて完成すること自体は確定したので、この日に入った時点で残作業は明確でした。
バランス調整的な作業
操作に対するレスポンスの充実
また、せっかく肉の表裏面ごとに焼き加減パラメータを持っているので、鉄板に接する面の焼き加減に応じて、煙パーティクルをモクモクさせる。白多め(水蒸気)~灰少なめ~黒多め(焦げ)という感じ。プレイヤーに危機感をもたせ操作誘導する。
振り返ってみて
2日目くらいまでは完成するなんて思ってなかったです。
最初に肉表現できない問題が立ちはだかって、次にアニメーション苦手すぎて肉がつかめない問題が道を阻んで、それから参照関係で長時間悩んで、…よく完成しましたねこれ。
私はプログラマ寄り知識セットなので、視覚的表現で高クオリティを狙うのが苦手なのですが、今回は最初からシェーダーでどこまで行けるかという切り口だったので、見た目クオリティもなかなか拘れたのは大きい成果でした。
今回は特に肉の色(とその変化)を表現するためにシェーダーを書いたので、その過程も記録しておきたい気分。シェーダー楽しいよ! 皆もシェーダー書こう。
おまけ(制作とあまり関係ない話)
今回使わせていただいたボイス、実は個人的に聞き覚えがあって、まさかみ自然保護区さんのスクールアイドルランナーで使われていたと記憶しています。…いやまさか、こんなタイミングでこのボイスに再開するなんて…と謎の感動を覚えていました。
…同人ゲームをよくやるクラスタか、コミケの同人ゲー戦利品配信よく見るクラスタにしか伝わらない話ですねこれ。伝わる人にだけ伝われ。