- 投稿日:2019-12-30T22:38:52+09:00
UnityWebGLでスマホか否かを判別する
はじめに
UnityのWebGLにおいてアクセスしている端末がスマホかどうか知りたい。
そんなことするの自分だけかもしれないが。
そんなときApplication.platform
やプラットフォーム依存コンパイルでは使っている端末がパソコンなのかスマホなのかが取得できないのでブラウザースクリプトとの相互作用を利用して取得する。環境
- Unity 2019.2.17f1
UnityとJavaScriptで互いに関数/メソッドを呼ぶ
UnityWebGLではUnityのC#からJavaScriptを呼び出したりその逆が可能。
WebGL: ブラウザースクリプトとの相互作用 - Unity マニュアル
詳しくは公式マニュアルを参照。そこでこの記事ではC#からJavaScriptにユーザーエージェントを取得させ、それを元にUnityのメソッドを呼ぶことでスマホであるかをUnityに知らせる。
実際のコード
ファイル名や関数名などは各々のプロジェクトに合わせた形に書き換える。
Assets/Plugins
以下に拡張子がjslibのファイルを作る。CheckPlatform.jslibmergeInto(LibraryManager.library, { CheckPlatform: function () { //ユーザーエージェントを取得して全て小文字に変換する var ua = window.navigator.userAgent.toLowerCase(); //ユーザーエージェント文字列にandroidかiosが含まれているか if(ua.indexOf("android") !== -1 || ua.indexOf("ios") !== -1){ //今開いているシーンにあるGameManagerというオブジェクトにアタッチされているスクリプトのsetSmartPhoneModeというメソッドを呼ぶ unityInstance.SendMessage('GameManager', 'setSmartPhoneMode') } }, });GameManager.cs//スマホかどうかを保存しておく public static bool IsSmartPhone { get; private set; } = false; //JavaScriptから呼び出すメソッド public void setSmartPhoneMode() { IsSmartPhone = true; }そして、以下のようにすることでJavaScriptを呼びC#にスマホであることを知らせることができる。
[DllImport("__Internal")] private static extern void CheckPlatform(); //Startではなくてもいい void Start() { //こうしておかないとUnityEditorでCheckPlatformを呼ぼうとしてエラーになる #if (UNITY_WEBGL && !UNITY_EDITOR) CheckPlatform(); #endif }最後に
今回の記事では端末がAndroidかiOSであるとしか判別していないが、ユーザーエージェント文字列をそのままUnityに送るなどすればブラウザやOSの種類も取得できる
参考リンク
WebGL: ブラウザースクリプトとの相互作用 - Unity マニュアル
JavaScriptでブラウザとOSを判定する2つの方法 - Qiita
- 投稿日:2019-12-30T18:36:39+09:00
【Unity】DOTS Visual Scriptingを試してみた
今年のUniteの「Unity開発ロードマップ最新情報」にて、DOTSベースのビジュアルスクリプティングツールが開発中であると言う旨が話されました。
このツール自体は引用しているスライド中にある通り、Unity2020系統でのプレビューリリース(付け加えると2020年のGDCの前後)を目標としているものとなりますが、ツール自体はフィードバックを目的とした開発版の物がフォーラムにて公開されており、現時点で6thまでリリースされてます。
今回話す内容としては最新版である「DOTS Visual Scripting 6th experimental drop」を触った上での機能や所感について色々とメモしていければと思います。
注意点
フォーラムでも免責事項として言及されてますが、大雑把に纏めると以下の点についてはご了承ください。
- 実稼働用ではない
- まだ動作不安定なところがあったりも
- 破壊的変更の可能性大いに有り
後はDOTS(特にECS)に関する知識が多少必要となってきますが、記事中では基礎的な所や概念については触れません。
これらに関する初学者向けの資料としては今年のUniteであった以下の講演が比較的新しい上に分かりやすくてオススメです。(どちらか一方ではなく、合わせて見た方が良いかも)ツールの呼び名について
フォーラムの表題では
DOTS Visual Scripting
と記載されてますが、Package Manager側の方ではVisual Scripting ECS
と表記されていたりします。1最終的にどの名称が採択されるのかはわかりませんが、この記事中では「DOTS Visual Scripting」の名称で統一します。 (便宜上、
DOTS VS
と省略)インストール & バージョン情報
- Unity 2019.3.0f3
- DOTS VS
インストール方法については↑のフォーラムリンクを参照。
ちなみに6thからはmanifest.jsonに指定のパッケージを追記する形で取得できます。
(※Editor上のPackageManagerからの取得はまだ出来ない)学び始めるには
具体的な解説に入っていく前にオススメのチュートリアル動画を紹介します。
こちらの方も合わせて見ると理解が深まるかもしれません。
DOTS VS
を学び始めるにあたっては以下の動画がおすすめです。
「Unity公式のSpace Shooter tutorial」をベースにDOTS VS
で実装を行う内容であり、全5回分の動画があります。英語動画ではありますが、字幕対応しているので自動翻訳で日本語化することも可能です。
(精度は自動翻訳なのでそこだけはご了承を)
- Chapter 1: Create your first Entity and Component in ECS
- Chapter 2: Create ComponentSystem
- Chapter 3: Instantiate Entity and Create MacroNode in Unity ECS
- Chapter 4: Boundary Detection and Destroy Entity in Unity ECS with Visual Scripting Tool
- Chapter 5: Enemy Generation and Collision Detection
他にも3rd DropにてRoll a Ball Demoのデモが公開されているので、こちらをサンプルとして追ってみるのも有りかもしれません。
機能について
ここからは機能解説に入っていきます。
※ここらを把握済みであり「所感を聞きたい」と言う方は読み飛ばしてもokです。 → 触ってみた所感について何が出来るのか?
現時点の機能で出来ることとしては、大まかに言うとツールで組んだロジックからECSのコードを生成する事が出来ます。 2, 3
※具体的にどういった物が生成できるのかについては後述ビジュアルスクリプティングでECSのコードを実装できるということは、非プログラマーの方でもロジックに集中して実装を行いつつもECSベースのパフォーマンスの良いコードを生成できるという利点があるかと思います。
具体的な機能詳細について解説していきます。
ComponentDataの定義
機能の1つとして「ツール上から
ComponentData
を定義したコードを生成する機能」があります。
ComponentDataの定義はAssetsのメニューから「Create → Visual Script → New Component各種」で生成できます。生成すると以下の画面が開かれるので、こちらからComponentDataが持つフィールドなどを設定していきます。
ちなみにメニュー側にも
Component
の他にShared Component
やBuffer Element
と言った物がありますが、これらを指定することでそれに応じたComponentDataを定義することも可能です。4生成コードについて
定義が完了したら上記ウィンドウの右下にある【Save】ボタンを押下することでコードが生成されます。
生成されるコードとしては、指定した名称に対し
Proxy
と言うサフィックスが付いたC#コードとなります。
→Sample
と言う名称を指定したらSampleProxy.cs
が生成される。サフィックスの理由について
明言されていたわけではないので自分の考えとなりますが、現時点でサフィックスが付く理由としては、生成されたコードを見た感じだと「オーサリングコンポーネントとしても機能出来るようにする」と言った意図がありそうです。
※生成されるコードはテキストエディタで直接開くか、上記ウィンドウと一緒に開かれるであろうCode Viewer
と言うウィンドウから確認することが可能。以下に生成したサンプルを引用します。(説明用に幾つかコメントを追記)
SampleProxy.cs
(クリックで展開)SampleProxy.csusing System; using System.ComponentModel; using Unity.Entities; using UnityEngine; using VisualScripting.Entities.Runtime; using System.Collections.Generic; // Editor上から定義したデータ本体 [Serializable, ComponentEditor] public struct Sample : IComponentData { public int IntValue; public Unity.Entities.Entity Prefab; // GameObjectから変換されたPrefabEntity } // オーサリング用コンポーネント [AddComponentMenu("Visual Scripting Components/Sample")] class SampleProxy : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs { public int IntValue; // Editor上で「GameObject」として指定したデータはEntityに変換される。 // これによりPrefabEntityの運用が可能に。 public UnityEngine.GameObject Prefab; public void Convert(Unity.Entities.Entity entity, Unity.Entities.EntityManager dstManager, GameObjectConversionSystem conversionSystem) { dstManager.AddComponentData(entity, new Sample { IntValue = IntValue, Prefab = conversionSystem.GetPrimaryEntity(Prefab) }); } public void DeclareReferencedPrefabs(List<UnityEngine.GameObject> referencedPrefabs) { referencedPrefabs.Add(Prefab); } }補足: GameObjectについて
ComponentDataを生成する際にはフィールドに対し、直接
GameObject
を指定することが出来ます。この指定を行うとGameObjectは自動的に
PrefabEntity
に変換されてPureECS上で機能するようになります。
変換のフローについては上記の生成後のコードを見ると分かりやすいかもです。ECS Graph(ComponentSystem)の実装
もう一つの機能である
ECS Graph
は実際にロジックを組む部分であり、最終的には組み込んだロジックを元にComponentSystemのコードが生成されます。こちらはAssetsのメニューから「Create → Visual Script → ECS Graph」でグラフの生成できます。
生成を行うと以下の初期画面(初期テンプレ)が表示されます。
ちなみにこの状態で生成されるコードとしては以下のものとなり、
OnUpdate
(グラフ上ではOn Update Entities
)の中でmyQuery
と言うEntityQuery
を見て処理を行っているのが見えてきます。
SampleSystem.cs
(クリックで展開)SampleSystem.csusing Microsoft.CSharp; using System; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; public class SampleSystem : ComponentSystem { private Unity.Entities.EntityQuery myQuery; protected override void OnCreate() { myQuery = EntityManager.UniversalQuery; } protected override void OnUpdate() { { Entities.With(myQuery).ForEach((Unity.Entities.Entity myQueryEntity) => { } ); } } }画面構成
基本的な画面構成については初期版のForumである「DOTS Visual Scripting first experimental drop」に纏まってますが、一部補足を加えたものを記事中でも記載します。
※Forumの方は初期版故に一部画面が古いので注意。(それを言うとこの記事も今後古くなっていくと思われるが..。)サンプルとして用意してある以下のグラフの詳細についてはStickynoteのコメントを御覧ください。
このグラフ自体はシーン中にCubeと言ったGameObjectを置き、Convert to Entity
でEntityに変換することで動作確認可能です。各種ウィンドウについて
各種ウィンドウに対しては青い番号で振ってあります。
- ①: Visual Script
- メインとなるグラフビュー
- ②: Blackboard
EntityQuery
の定義やグラフ内(ComponentSystem内)での変数の定義が可能- ③: MiniMap
- グラフ全体のミニマップを表示
- ④: Code Viewer
- 現在のグラフに対応するComponentSystemのコードを表示
- ⑤: Inspector
- グラフの詳細の設定
- 「JobSytstemの有無」や「実行順」に関する設定はこちらから指定可能
Use Job System
→ JobSystemの有効/無効UpdateBefore
/UpdateAfter
→ グラフ間の実行順
- ※ComponentSystem間の実行順とも言い換えられる
Execution Order
→ グラフ内の実行順
- ※
On Start Entities
やOn Update Entities
などの実行順を指す- グラフ内で適当なノードを選択したり、ProjectViewからグラフのAssetを選択することで表示可能
各種ボタン/設定項目について
各種ボタンや設定項目については赤い番号で振ってあります。
- ①: Save All
- グラフの保存
- ②: Build All
- グラフのビルドを行い、結果をC#コードとして出力
- 出力先は
Assets/Runtime/VisualScripting/[GraphName].cs
- ※挙動が少し複雑なので後述
- ③: Code Viewer
- ※上述の「④: Code Viewer」を開く
- ④: Show MiniMap
- ※上述の「③: MiniMap」を開く
- ⑤: Show Blackboard
- ※上述の「②: Blackboard」を開く
- ⑥: Refresh UI
- UI全体の更新
- ⑦: Options
- コンパイルや
Show unused nodes
の設定切り替えが可能- ⑧: EntityQuery → Required Components
EntityQuery
に対する必要となるComponentDataの定義
- ※上の例では
Translation
とRotation
を持つEntityを対象に設定- ⑨: EntityQuery → Additional Criterias
EntityQuer
に対する条件式
- ※上の例ではBlackboardからは設定していないが、
On Update Entities
ノード側で「Tlanslate.y < 10」でフィルタリングするように設定している。- ⑩: Toggle Tracing for Current Instance
- インスタンスに対するフレームトレースを行う。詳細は後述
グラフの実行/ビルドについて
グラフを実行する際には「そのグラフを開いている時のみ」実行時に自動でコンパイルが走るように思われました。
※グラフ中にCompilation Pending
と表示されるはず。逆に言うとグラフを閉じている時には自動コンパイルが走らないために、そのままだと動作しません。
閉じた状態で実行を行うには【Build All】ボタンからグラフをビルドしてC#コードとして出力する必要があります。ここで一点注意点として、「グラフをビルドした状態」で「グラフを開いたまま」実行を行うと、実行中に動的コンパイルが走って処理が2回呼ばれる事がありました。
→ 例えばOn Start Entities
に初期化処理を書いているとした場合、実行時にビルドしたコード側からのOn Start Entities
の呼び出したあった後に動的コンパイルが走り、完了した時点でもう一度「コンパイル済みの方」のOn Start Entities
が呼ばれるので初期化処理が2回走ります。どこか設定を見落としているだけかもしれない + 将来的に改修されるところかもしれませんが、一応補足まで。
補足: グラフの出力結果について
参考までに上記のグラフの出力結果を載せておきます。(こちらも説明用に幾つかコメントを追記)
SampleSystem.cs
(クリックで展開)SampleSystem.csusing Microsoft.CSharp; using System; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; public class SampleSystem : ComponentSystem { private Unity.Entities.EntityQuery TransformQuery; private Unity.Entities.EntityQuery HogeQuery; // Blackboard上から定義した変数はSingletonなEntityとして保持される public struct GraphData : Unity.Entities.IComponentData { public float addAngle; } protected override void OnCreate() { // Blackboard上から定義したEntityQueryの初期化 TransformQuery = GetEntityQuery( ComponentType.ReadWrite<Unity.Transforms.Translation>(), ComponentType.ReadWrite<Unity.Transforms.Rotation>()); HogeQuery = GetEntityQuery(ComponentType.ReadOnly<Unity.Transforms.Translation>()); EntityManager.CreateEntity(typeof(GraphData)); SetSingleton(new GraphData { addAngle = 0.3F }); } protected override void OnUpdate() { // On Update Entitiesの処理内容 // ※グラフ中には説明用に「On Start Entities」と「On End Entities」のノードも置いてあるが、 // 処理が未定義なので未出力となっている。 GraphData graphData = GetSingleton<GraphData>(); { Entities.With(TransformQuery).ForEach(( Unity.Entities.Entity TransformQueryEntity, ref Unity.Transforms.Translation TransformQueryTranslation, ref Unity.Transforms.Rotation TransformQueryRotation) => { // 「Additional Criterias」に設定した条件式 var Criteria = TransformQueryTranslation.Value.y < 10F; if (!Criteria) { return; } // 「Translate by」の処理 TransformQueryTranslation.Value += new float3(0F, 0.01F, 0F); // 「Rotate by」の処理 TransformQueryRotation.Value = math.mul( TransformQueryRotation.Value, quaternion.AxisAngle(math.normalize(new float3(0F, 1F, 0F)), graphData.addAngle)); } ); } } }ECS Graphの拡張について
グラフのノードは自分で拡張することが出来ます。
拡張方法は簡単であり、
Graph Tools Foundation
と言うパッケージ内のNodeAttributes.cs
に定義されている[Node]属性を使うだけです。サンプル
サンプルとして以下の拡張ノードを定義してみました。
CustomNode.cs
(クリックで展開)CustomNode.csusing Unity.Mathematics; using UnityEngine.VisualScripting; namespace VisualScripting.CustomNode { // [Node]を付けたクラスのstatic methodがノードとして表示される [Node] public static class CustomNode { // 受け取った値を加算して返す public static int Sample(int x, int y, int z) => x + y + z; // こうすればテーブルの参照も出来たり // ※現状のDOTS VSでは配列が定義できないっぽいので..? static readonly int[] SampleTable = new int[] {0, 1, 2, 3}; public static int GetSampleTable(int index) => SampleTable[index]; // 行列演算用のノードも作成出来る // ※現状のDOTS VSでは行列に対する直接な操作は出来ないっぽいのが、拡張ノード経由で対応可能 public static float4x4 MatrixSample(float3 pos, quaternion rot) { return new float4x4(rot, pos); } } }拡張したノードはグラフビュー上で右クリック →
Create Node
を開くと、[Node]属性を付けたクラス名が項目に追加されているのでそちらから呼び出すことが出来ます。参考
他にサポートされている機能
他にも以下の機能などがサポートされているみたいです。
Coroutine
面白い機能としては、ECSベースのCoroutineが実装されてました。
一例を挙げると以下のように
On Update Entities
にてWait
を呼び出すと、「待機中に呼び出す処理」「完了時に呼び出す処理」を指定することが出来ます。以下の例では待機中には"Waiting"と言うログを、完了時には"Finish"と言うログを出力します。
ただ、現状のCoroutine自体は
ComponentSystem.OnUpdate
の中にて自前のステート管理を自動生成して制御しているっぽく、完了時にもう一度Coroutineが回り始めるので考えて制御する必要はありそうです。参考までに上記グラフの出力コードを引用します。
SampleSystem.cs
(クリックで展開)SampleSystem.csusing Microsoft.CSharp; using System; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; public class SampleSystem : ComponentSystem { private Unity.Entities.EntityQuery TransformQueryExcludeCoroutine; private Unity.Entities.EntityQuery TransformQuery; private Unity.Entities.EntityQuery HogeQuery; public struct TransformQueryCoroutine : Unity.Entities.ISystemStateComponentData { public int State; public VisualScripting.Entities.Runtime.Wait Wait; } private static bool UpdateTransformQueryCoroutine( float deltaTime, ref TransformQueryCoroutine transformQueryCoroutine) { switch (transformQueryCoroutine.State) { case 0: { transformQueryCoroutine.Wait.Time = 3F; transformQueryCoroutine.State = 1; return SampleSystem.UpdateTransformQueryCoroutine(deltaTime, ref transformQueryCoroutine); } case 1: { if (transformQueryCoroutine.Wait.MoveNext(deltaTime)) { Debug.Log("Waiting"); return true; } transformQueryCoroutine.State = 2; return SampleSystem.UpdateTransformQueryCoroutine(deltaTime, ref transformQueryCoroutine); } case 2: { Debug.Log("Finish"); return false; } } return false; } public struct GraphData : Unity.Entities.IComponentData { public float addAngle; } protected override void OnCreate() { TransformQueryExcludeCoroutine = GetEntityQuery( ComponentType.Exclude<TransformQueryCoroutine>(), ComponentType.ReadOnly<Unity.Transforms.Translation>(), ComponentType.ReadOnly<Unity.Transforms.Rotation>()); TransformQuery = GetEntityQuery( ComponentType.ReadWrite<TransformQueryCoroutine>(), ComponentType.ReadOnly<Unity.Transforms.Translation>(), ComponentType.ReadOnly<Unity.Transforms.Rotation>()); HogeQuery = GetEntityQuery(ComponentType.ReadOnly<Unity.Transforms.Translation>()); EntityManager.CreateEntity(typeof(GraphData)); SetSingleton(new GraphData { addAngle = 0.3F }); } protected override void OnUpdate() { EntityManager.AddComponent<TransformQueryCoroutine>(TransformQueryExcludeCoroutine); { Entities.With(TransformQuery).ForEach(( Unity.Entities.Entity TransformQueryEntity, ref TransformQueryCoroutine transformQueryCoroutine) => { { if (!SampleSystem.UpdateTransformQueryCoroutine(Time.deltaTime, ref transformQueryCoroutine)) { transformQueryCoroutine.State = 0; } } } ); } } }フレームトレース機能
画面構成の章で解説した【⑩: Toggle Tracing for Current Instance】ボタンを押下すると、ボタン群が並んでいるエリアの下にフレーム番号が記載されたエリアが表示されます。
この状態でグラフを実行すると「フレーム毎に処理をトレース」する事ができ、一時停止/実行停止中に以下のように操作してトレース結果を追うことが出来ます。
マクロ
グラフ内で計算式などを組むと、計算式のノード構成を使いまわしたくなるときが出てくるかと思います。この要件はマクロを使うことで対応可能です。
これを使うことで複雑な計算式と言った「使い回せる処理」に関しては、グラフ内で関数化するような感じで定義を纏めることが出来ます。
やり方
サンプルとして「引数から受け取った4つの値を加算して返す」処理をマクロ化してみます。
先ずは予め計算式と組まれている一連のノード郡を選択し、右クリックメニューから「
Refactor
→Extract Macro
」を選択します。そうすると以下のようなマクロノードとマクロ用の
ECS Graph
のアセットが生成されます。
今回は名前をSampleMacro
と設定しました。次にマクロノードをダブルクリックして開きます。
この中ではマクロの実装を定義できます。
以下の様に引数/戻り値を設定して計算式を組み上げます。計算式を組み上げたら前に開いていたグラフに戻ります。
するとマクロノードが引数/戻り値に応じて入出力が変わっているので、後はこちらを使うだけです。サンプルでは「1+2+3+4」の結果をログに出力してます。(シンプルすぎた感..)
キー/ボタン入力の取得
グラフ上から直接キー/ボタン入力を取得することが出来ます。
入力を実際に組み込むにあたっては、入力側の抽象化を考慮して「カスタムノードで抽象化して取るか?」「ECS側で入力を取らずにEntityとして入力を渡すか?」など色々と設計を考えられるかと思いますが、簡易的に確認する場合には有用かもしれません。
その他
幾つかの例を挙げましたが、他にも色々とあります。
記事中では全て解説しないので、他の機能についてはフォーラムにある各バージョンのfeatureを御覧ください。現時点ではサポートされていない機能?
主に出来ることについて纏めていきましたが、触っている感じだと出来ないことも幾つか有るように思われました。
ひょっとしたら見落としているだけかもしれませんが...調査した範囲で見えてきたことを纏めます。
- 配列が使えない?
- 一応Forumにてこういったやり取りがありましたが..詳細は不明 + 追いきれておらず...
- 名前空間の設定
- 現状だと全てグローバル名前空間に置かれる
- まだ未検証だが...C#コードの出力先は決まっているので、上手く
ADF
(Assembly Definition Files)を設定することが出来たら多少マシになるかも- ComponentDataにポインタを持たせられない?
- ComponentDataを生成するツール上からは見受けられなかった
- ※ポインタ自体はBlittable型なので持たせること自体は可能
- グラフ上から行列にアクセスできない
- 「行列の型(float4x4など)」自体はあるが、要素に対するアクセスなどが無い?
他にもあるかもしれませんが、自分の方で触った際には上記の項目が気になりました。
とは言えど、例えば行列については将来的に対応されることを期待して今はカスタムノードで対応したり、ポインタについては直接触らない方向で別手段を検討(例えばラップするとか)していくなどすれば代替出来るかもしれません。
何れにせよ今後の更新で改善される可能性があるので、引き続きForumなど追っていければと思います。
触ってみた所感について
触った際の所感について記載していきます。
ここからは自分の考えが主なので、考えの一環程度に捉えて頂けると幸いです。ECSの考え方は覚える必要がありそう
出来る事自体がECSそのままなので、予めECSの知見がある方にはとっつきやすい仕組みかと思いました。
逆に言うとECSと言うアーキテクチャである以上、どんな方でも触るにあたっては「ECSの考え方」自体は覚える必要があるように思われました。ここで言う「考え方」とは「EntityQueryで処理するEntityをフィルタリング → フィルタリングしたEntityに対し処理を適用」と言ったロジックを組むにあたって必要となる考え方であり、根底にあるメモリ管理/キャッシュ効率と言ったゴリラ感のある話題を覚える必要があるという訳ではありません。
(かと言って最終的にどこまでの知識が必要となってくるかは見えていない所...)ビジュアルスクリプティングとの相性は良さそう
私がECSを学び始め、少し理解できてきたタイミングで「これって入出力がハッキリしているから特殊な組み方とか許容しなければ、ビジュアルスクリプティングと相性良さそうだよな」と思う時が有りましたが、今回の
DOTS VS
はまさにそのイメージに近い物が来たかなー感が有りました。※特殊な組み方・・(書いておいて特殊かどうかわかりませんが...)例えばECS側からグローバルなシングルトンを覗きに行ったり、MonoBehaviourの参照を引っ張ってきて状態に依存させたりなど。
ただし、組み込むにあたっては上記の「ECSの考え方」自体は必要となりそうなので、ここらがどれくらい受け入れられうのかは少し気になるところです。
最終的にはどこまで対応されるのか?
現時点ではECSのコード生成という段階ではありますが、「
Unity2020
辺りから追加されていくDOTS周りの機能」や「DOTS寄りになっていくTinyUnity」と言った他の機能を踏まえると、まだまだ出来る事自体は増えていきそうな感じはします。初期のフォーラムでも関連する話題は上がっており、以下のようなコメントも投稿されていたりします。
DOTS Visual Scripting first experimental drop #20最後に
所々に記載してますが、
DOTS VS
自体はまだまだ開発中のステータスです。
将来的な変更によっては今回の記載内容と相違する点が出てくる可能性も十分にあります。それを踏まえつつ現時点のイメージ把握程度に留めておくのが良いかもしれません。
今回話せなかったテーマ
今回の記事では未検証項目などもあって書けなかったテーマがあります。
DOTS VS
を実際に組み込むにあたっての設計について今回は「機能と所感の話」が主であり、「実際にこれをどう組み込んでいくのか?」についてまでは話しきれませんでした。
将来的なバージョンアップを踏まえつつ、改めて時間が取れれば検証してみたいタイトルではあります。後は関連事項として、Uniteの「Unity開発ロードマップ最新情報」にて「GDCのタイミングで
DOTS VS
をガッツリ使ったゲームサンプルを公開予定?」である旨が話されていたので、そちらの方も実装の一例として気になるところではあります。※ちなみに...コードベースにはなりますが、以前↓の記事を書いたので参考までに。
【Unity】DOTS(ECS)を導入したゲームを作る際に設計してみた話関連リンク
チュートリアル動画
全5回に渡って「Unity公式のSpace Shooter tutorial」をベースにDOTS Visual Scriptingで実装を行うチュートリアル動画。
- Chapter 1: Create your first Entity and Component in ECS
- Chapter 2: Create ComponentSystem
- Chapter 3: Instantiate Entity and Create MacroNode in Unity ECS
- Chapter 4: Boundary Detection and Destroy Entity in Unity ECS with Visual Scripting Tool
- Chapter 5: Enemy Generation and Collision Detection
Forum
- DOTS Visual Scripting 6th experimental drop
- DOTS Visual Scripting 5th experimental drop
- DOTS Visual Scripting 4th experimental drop
- DOTS Visual Scripting 3rd experimental drop
- DOTS Visual Scripting 2nd experimental drop
- DOTS Visual Scripting first experimental drop
DOTS基礎
Other
- DOTS Visual Scriptingのお試しバージョンを触ってみた
- 【Unity】DOTS(ECS)を導入したゲームを作る際に設計してみた話
- ※
DOTS VS
から少しズレますが...ECSをゲームに組み込む上での参考程度に
- 投稿日:2019-12-30T15:18:26+09:00
【Unity DOTS】親Entityの削除時に子Entityも一緒に削除する方法
はじめに
親子関係のあるオブジェクトを削除する時、従来のUnityであれば親オブジェクトを削除すれば子オブジェクトも併せて一緒に削除されます。
しかし、ECSでは親Entityを削除しても子Entityは削除されず、データが書き変わって残り続けてしまいます。
そのような問題を解決する方法をこの記事に書きました。実行環境
- Unity 2019.3.0f3
- Entities 0.4.0 preview.10
- Hybrid Renderer 0.3.1 preview.10
注意
- 本記事ではpreview packageを多く使用しています。今の実装と本記事の実装が大幅に異なる可能性があるのでご注意下さい。
- 本記事ではGameObject/Component を使用したHybrid ECSを前提としています。
問題
下図のように2つのオブジェクトがある場合を考えます。
白くて大きい立方体のオブジェクトをParentCube
、
黒くて小さい立方体のオブジェクトをChildCube
と呼ぶことにします。これら2つのオブジェクトには親子関係があり、
ParentCube
はChildCube
の親オブジェクトである、とします。
ここで、これらのオブジェクトをEntity化することを考え、
ParentCube
にのみConvertToEntity
をAddします。実行してEntityDebuggerを確認してみると、
確かにParentCube
もChildCube
もEntityに変換されます。ここからが本題ですが、ここで
ParentCube
を削除しようと思います。まず、
ParentCube
を識別するためのタグと、その「削除」を行うタイミングを知らせるためのタグを作成します。ParentCube.csusing Unity.Entities; /// <summary> /// ParentCubeを識別するためのタグ /// </summary> public struct ParentCube : IComponentData { }Destroyable.csusing Unity.Entities; /// <summary> /// 削除するタイミングを知らせるためのタグ /// </summary> public struct Destroyable : IComponentData { }次のようなスクリプトを作成し、
ParentCube
にアタッチします。ParentCubeAuthoring.csusing UnityEngine; using Unity.Entities; public class ParentCubeAuthoring : MonoBehaviour, IConvertGameObjectToEntity { public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) { dstManager.AddComponentData(entity, new ParentCube()); } }そして、
ParentCube
を削除する(そしてそれによりChildCube
も削除されることを期待する)、DestroyCubesSystem
を作成します。
コードを見る
DestroyCubesSystem.csusing Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Transforms; public class DestroyCubesSystem : JobComponentSystem { private EntityCommandBufferSystem _bufferSystem; protected override void OnCreate() { _bufferSystem = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>(); } [BurstCompile] private struct DestroyCubesJob : IJobForEachWithEntity<ParentCube, Destroyable> { public EntityCommandBuffer.Concurrent CommandBuffer; public void Execute(Entity entity, int index, [ReadOnly] ref ParentCube c0, [ReadOnly] ref Destroyable c1) { CommandBuffer.DestroyEntity(index, entity); } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var jobHandle = new DestroyCubesJob { CommandBuffer = _bufferSystem.CreateCommandBuffer().ToConcurrent(), }.Schedule(this, inputDeps); return jobHandle; } }これで実行してみると、
確かに
ParentCube
は削除されたのですが、ChildCube
は削除されません。さらに、(今回のケースではあまり関係ないですが)
LocalToWorld
のデータがLocalToParent
のデータに置き換わってしまっているので、予期せぬ場所に子Entityが出現したりして、かなり困ることもあるかもしれません。解決法
子Entityの参照が、親Entityに付いている
Child
というBufferElementDataに入っているので、それを使って子Entityを削除します。
DestroyCubesSystem
のコードを見る
DestroyCubesSystem.csusing Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Transforms; public class DestroyCubesSystem : JobComponentSystem { private EntityCommandBufferSystem _bufferSystem; protected override void OnCreate() { _bufferSystem = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>(); } [BurstCompile] private struct DestroyCubesJob : IJobForEachWithEntity<ParentCube, Destroyable> { public EntityCommandBuffer.Concurrent CommandBuffer; // 追加 [ReadOnly] public BufferFromEntity<Child> Children; public void Execute(Entity entity, int index, [ReadOnly] ref ParentCube c0, [ReadOnly] ref Destroyable c1) { // 追加 var childEntityBuffer = Children[entity]; // 追加 for (var i = 0; i < childEntityBuffer.Length; i++) { CommandBuffer.DestroyEntity(index, childEntityBuffer[i].Value); } CommandBuffer.DestroyEntity(index, entity); } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var jobHandle = new DestroyCubesJob { CommandBuffer = _bufferSystem.CreateCommandBuffer().ToConcurrent(), // 追加 Children = GetBufferFromEntity<Child>() }.Schedule(this, inputDeps); return jobHandle; } }
これで子オブジェクトも削除されるはずです。
- 投稿日:2019-12-30T14:21:39+09:00
UnityでSpriteから当たり判定(Polygon Collider 2D)を自動でつける
はじめに
この記事では透過されたpng画像をSpriteにし、Polygon Collider 2Dを使って当たり判定を自動でつけるところまでやります
目次
- 背景透過画像を用意する
- Unityにimportする
- 画像をSpriteに変換する
- SpriteRendererとPolygonCollider2Dを使う
ターゲット
- 2Dアクションゲーム作りたいけど、当たり判定の作り方がよくわからない
- デザイナーじゃないし2DのAnimationが作れないから画像から当たり判定をつけたい
環境
Mac Mojave : 10.14.6
Unity : 2019.2.4f1
Rider : 2019.1
.Net : 4.x1.背景透過画像を用意する
まず、元となるSprite画像を用意します。
今回はテストで作ったので、fire alpacaというソフトを使って背景透過のpng画像を描きアニメーションさせました。今回はこの7枚の背景透過画像を元にします。
このとき注意しないといけないのが、攻撃するときに当たり判定をつけたい画像(武器など)と当たり判定をつけたくない画像(プレイヤー自身)などの出力を分ける必要があるということです2.Unityにimportする
用意したpngファイルをそのままUnityのAssets内に置く
Assets/Resources/Sprites/Player/Attack3/Weapon
としました。
このままではUnityで使えないのでSpriteに変換していきます。
3.画像をSpriteに変換する
次に、Inspectorから、Texture TypeがDefaultになっているのをSprite(2D and UI)にします
Inspectorの下の方にApplyというボタンがあるので、そこをクリックします
すると、pngの画像をSpriteとして扱えるようになりました
4.SpriteRendererとPolygonCollider2Dを使う
ここまでで、透過画像のimportおよびSpriteへの変換が行えました。
最後に、自動でColliderを生成してくれるPolygonCollider2Dを使います。
- SpriteRendererをAddComponentする
- SpriteRendererのSpriteに自動でColliderをつけたい画像を設定する
- PolygonCollider2DをAddComponentする
- 自動で生成完了
流れはこれだけなのですが、順番が問題でこの順番じゃないとPolygonCollider2Dは自動で生成してくれません。
また、アニメーションさせたくてScriptから連続でSpriteを変更し、自動的にそのSpriteから当たり判定を付与したいときは
private async void Renderer(){ _spriteRenderer.sprite = _attackSprites[i]; gameObject.AddComponent<PolygonCollider2D>(); await UniTask.Delay(50); Destroy(GetComponent<PolygonCollider2D>()); }このようにDestroyメソッドの中にGetComponent()を入れて削除し、またAddComponentで追加し自動生成を行うといっためんどくさい作業をしないといけないみたいです。
エディター上のPolygonCollider2DをResetすれば自動でColliderをつけてくれますが、Scriptからprivate void Reset(){ //hogehoge }このように書くと、Resetがoverrideされるみたいで、ここに買いた処理がResetボタンを押した時の処理になるみたいです。
なので、処理は重いですがこういう形にしました。
ちょっとハマったところ
- 画像の出力は全てUI.Imageでやっていたので、SpriteRendererの存在を知らなかったた
もっとこうした方がいいよとかここ間違ってる等ありましたらご指摘ください!
- 投稿日:2019-12-30T14:08:46+09:00
MixamoをUnityで使おうぜ
Mixamo何するものぞ
Mixamoとは、3Dモデルやアニメーションを無料で商用、非商用問わず利用することができるWebサービスです。
Adobeが運営しているサービスで、利用にはAdobeアカウントが必要です。
3Dモデルもアニメーションも種類が豊富かつハイクオリティで、画面右側のパネルでアニメーションを微調整することもできます。非常に便利なサービスなのですが、MixamoのアセットをそのままUnityにインポートするといくつか問題が発生します。この記事では自分が出くわした事案とその解決法をご紹介します。
事案1.テクスチャが貼られていない
さてDouglas君をUnityにインポートしました。すると...
驚きの白さ。キン消しみたいになっちゃってますね。解決法
テクスチャが貼られていないのはテクスチャが存在していないからです。fbxファイルの中に埋め込んであるのでこれを引き出してあげる必要があります。
ImportSettingsのMaterialsタブにあるExtract Texturesボタンを押してください。テクスチャの展開先を適当に選んでやればモデルの中に埋め込まれていたテクスチャが生成されます。なおMaterialへの適用は自動で行われます。便利!
...狂ってますね。事案2です。事案2.透けている
お腹が出ちゃってますね。物理的に。袖も若干突き破ってますし眼球もシンプソンズみたいになっています。
解決法
これはMaterialのRendering ModeがTransparentになっているのが原因です。
ですがこちらのMaterialもfbxの中に埋め込まれているため設定を変更することができません。テクスチャと同様にfbxから引き出してあげましょう。
ImportSettingsのMaterialsタブにあるExtract Materialsボタンを押してください。
Materialが個別に生成されると思うのでいい感じに設定を変更してあげましょう。
シンプソンズみたいだったDouglas君がウィル・スミスみたいになりました。地球の1つや2つは救ってそうな面構えです。事案3.足首が折れている
3Dモデルは問題なくインポートすることができました。このモデルをゲームキャラクターとして使用するにはアニメーションが必要です。
今回はこちらの走るアニメーションを使おうと思います。ドラッグ&ドロップでインポートし、Animation TypeをHumanoidに変更します。
そのプレビューがこちらです。
左足首にご注目下さい。折れてます。Mixamoのアニメーションアセットに含まれているAvatarはUnityのMecanimに上手く対応できないようですね。解決法
アニメーションのfbxのImport SettingsのRigタブのAvatar DefinitionをCopy From Other Avatarに変更し、Sourceに適当なHumanoidのAvatarを入れましょう。
今回はDouglasのAvatarを入れています。事前にDouglasのAvatarをDouglasのImport SettingのRigタブからHumanoidに変更しておくようにしましょう。
変更した結果がこちらです。
足首が治っていることはもちろん上半身も自然な感じになっています。まとめ
Mixamoは便利なサービスですがUnityで使うにはひと手間必要です。そこら辺を勝手にやってくれるAssetPostProcessorを作りました。
https://github.com/Dumble009/MixAmoAssetImporter
こちらを使えば今回の記事で取り上げたような操作を自動で行ってくれます。お試しください。
- 投稿日:2019-12-30T00:33:36+09:00
Unityの新規プロジェクト作成時にやるべきことメモ
自分の開発開発環境にバシッとあてはまる記事があったのでこれ通りにやればよさそう
https://qiita.com/cs1000/items/07368892a599b2b7b836
要するに以下のことをやってる
- Unityプロジェクトを新規作成
- Version ControlのModeをVisible Meta Filesに(メニュー>Edit>ProjectSettings>Editor)
- 外部バージョン管理システム(SVNやGit)のサポートを有効化する設定。
- UnityはAssetsディレクトリ内の各ディレクトリ、ファイル対する.metaファイルを自動的に作成しているが、これは各ディレクトリ、ファイルの参照情報が含まれている(metaファイル内に各ディレクトリ、ファイルのID情報が記録されている。)。Assets内のディレクトリ、ファイルの名前を変更したり、場所を移動させたりすると、.metaファイル内の情報も更新される。例えばスクリプトファイルに対するmetaファイルを削除した場合、そのスクリプトを割り当てられたゲームオブジェクトやプレハブは、スクリプトへの参照が切られてしまうため、機能を失ってしまう。これを修正するには、これらのオブジェクトに手動でスクリプトを再割り当てしなければならない。だから、metaファイルは絶対にバージョン管理する必要があるのだ。
- Asset SerializationのModeをForce Textに(メニュー>Edit>ProjectSettings>Editor)
- 効果は、AssetsにPrefab、sceneといったデフォルトでバイナリファイルのものがテキストデータとして保存される、というもの。これによりバージョン管理で差分が見やすくなる
- プロジェクトは以下の.gitignoreをcommitしてpush
- https://github.com/github/gitignore/blob/master/Unity.gitignore
- 最低限のものだけ登録する。最低限のものでUnityによって自動生成され、プロジェクトの状態が復元されるものは不要ってこと。
参考
- 投稿日:2019-12-30T00:29:42+09:00
Unity のシェーダー開発方法のまとめと備忘録・頂点アニメーションテクスチャ(VAT)シェーダー
Unity のみで完結できる頂点アニメーションテクスチャー(VAT・Vertex Animation Texture)のエクスポーターとシェーダーを作りました。
Vertex Animation Texture shader, #unity3d.
— サトー (@sator_imaging) December 24, 2019
202 frames@30fps animation, 4,472 tris each mesh.
--
GeForce GTX 1080 on Unity Editor
90fps=1,500mesh
60fps=3,000mesh#madewithunity #unity pic.twitter.com/ctxG7GT4tg--
こちらの記事はとりあえず動くシェーダーは作れる、だけど… という内容です。
拾い物やネットからのコピペ、アセットストア経由など、プロジェクトは雑多なアセットのチャンプルーが常で、たまーにシェーダーを作ったり、という方や、プロジェクトの規模に合うであろう内容になっています。
もくじ
- はじめに
- Unity の標準機能への対応方法
- プラットフォームごとの違いを吸収する方法
- ShaderLab の構造・文法の備忘録
- Properties
- SubShader と Pass
- CGPROGRAM ~ ENDCG
- おわりに
はじめに
Project ウインドウから Unlit Shader を作り、それをベースに自作シェーダーを作ると、影を落とさない&受けない、Global Illumination の各種プローブ、ライトマップの適用やベイクにも反応しないシェーダーが出来上がります。
加えて、Unity のバージョンアップによって古くなった情報がいまでも検索に引っかかる為、専業ではない人間ではなかなか新しい情報に巡り合えず、開発に無駄な時間がかかってしまうのが Unity シェーダー開発のあるあるかと思います。
この記事は、VAT シェーダーを開発する過程で調べた、Unity の各標準機能に対応したシェーダーを開発する為に必要な情報と ShaderLab の備忘録をまとめたものです。
- バージョン: Unity 2018 LTS
- グラス型AR・VR やモバイル等で Forward Rendering を使っている
- なんだかんだで Built-in Render Pipeline (Legacy) を使っている
目標
前述の通り、Unity の標準機能に一通り対応させることです。特定のプロジェクトだけではなく汎用的に使うことができ、アセットストアへの出品も可能なものを目指します。
対応させたい Unity 標準機能の一覧
- GI & ライトマップのベイクと反映が行われる。
- Light Probe がある環境では影響を受ける。
- Reflection Probe のレンダリングに映る。
- 天球・スカイボックスや Reflection Probe が映り込む。
- ライトマップが適用される。
- ライトマップのベイク時に周辺環境に影響を与える。
- 影を落とす&受ける。
- GPU Instancing と関連技術に対応している。
- グラス型AR・VR でのステレオレンダリングで、Single Pass が使える。
- Single Pass(Instanced) にも対応している。 ※ 全てのアセットに目が行き届く開発環境でないと使ったことが無い人が多いのでは・・・?
- Lighting 設定のフォグの影響を受ける。
Universal Render Pipeline(旧LWRP)について
先日ベータ版から抜けて正式リリースとなった Universal Render Pipeline。
ここではシェーダーの基本的なところは抑えていますが、Built-in 向けのシェーダーを URP に対応させるにはどうしたらいいか、については扱っていません。ステレオレンダリング&フォワードレンダリングやモバイルに関わることが多いという事もありますが、冒頭で述べたように、自身の管理外のアセットの対応状況に左右されることなどを考えると、中小規模のプロジェクトではまだ時期尚早かなと思っています。
以下の表を見て、うっ、となったというのもあります。
Shader Graph について
Maya のような DCC ツールもノードベースで見栄えをいじりますが、最後の最後で結局、欲しい情報を取れるノードが無くて望むものが作り切れない、なんて事も多いので、コーディングせずに Unity Shader Graph で、、、も扱っていません。
個人的には、ノードベースでは作りきれない、となった時に Shader Graph から ShaderLab のコードを書き出して自分で実装する、という選択もあると思うので、Unity でのシェーダー開発の方法は理解しておいて損はないと思います。
Unity の標準機能への対応方法
まずは Unity 標準機能への対応方法の紹介です。
ShaderLab の基本的なところは、備忘録として次の章にまとめています。
Global Illumination への対応
Light Probe(球面調和)の影響を受けるようにする
ライトプローブに対応するには、
UnityCG.cginc
のShadeSH9
を使用します。このマクロはPass
のタグ設定でLightMode
をForwardBase
に設定した状態で使用する必要があります。#include "UnityCG.cginc" ... Tags { "LightMode" = "ForwardBase" }球面調和は可能な限り頂点シェーダーで処理する
球面調和(Spherical Harmonics)はけして軽い処理ではないので、頂点シェーダーで十分な結果を得られる場合はピクセルシェーダーで処理をしない方が良いです。
以下のサンプル画像を見てわかるように、Light Probe と Lighting > Environment Lighting の影響を計算するだけなので、ノーマルマッピング等を行わない限りにおいてはクッキリはっきりとした変化のある絵は出ないからです。
※ ロボットの下手側の肩のライティング結果に、わずかながら違いが見受けられます。
例(頂点シェーダー):
o.color.rgb *= ShadeSH9( half4(UnityObjectToWorldNormal(v.normal), 1) );※ 頂点シェーダーで設定したカラーはフラグメントシェーダーに渡される際に、GPU で頂点間を線形補完された状態になります。
例(ピクセルシェーダー):
col.rgb *= ShadeSH9( half4(UnityObjectToWorldNormal(i.normal), 1) );Reflection Probe に映るようにする
カスタムシェーダー側で何もしなくても Reflection Probe に映り込み、頂点シェーダーの変形結果も反映されます。もしも映らない場合は Reflection Probe Static の設定を忘れていないかを確認します。
※ VAT Kit ではシェーダー内で
_Time
を使用してアニメーションさせていますが、ベイクする度に映り込む位置が1フレームずつ進んでしまいました。Unity & ShaderLab ではベイク用のパスなどは用意されていないので、必要に応じて対策を施す必要があります。Reflection Probe や天球・スカイボックスが映り込むようにする
Reflection Probe 及び Lighting > Environment で設定された天球・スカイボックスの情報の取得方法は、こちらの公式ドキュメントに載っています。
ただし、上記の公式ドキュメントの実装だと、なぜか複数の Reflection Probe を跨いだ際のブレンドには対応していません。
Reflection Probe のブレンドに対応する
ブレンディングに対応するにはフラグメントシェーダーで追加の処理が必要になります。
※ Reflection Probe のブレンドに対応した例
リフレクションの計算には、ワールド空間の法線と視線ベクトルが必要になります。
ノーマルマッピング等を行わない場合は、頂点シェーダーでリフレクションの法線を事前に計算しておき、映り込みのサンプリングのみフラグメントシェーダーで行います。
※ 視線ベクトルの計算に必要な頂点のワールド座標は、頂点シェーダーで計算した場合も GPU で線形補完されるので精度は変わらない(ハズ)なので、ノーマルマッピングをする時は頂点の座標のみ頂点シェーダーで計算しておくのも良いかもしれません。
Reflection Probe 対応のサンプルコード
各手法を切り替えられるようにコメントとして残したサンプルです。
struct v2f { ... half3 worldRefl : TEXCOORD2; half3 worldPos : TEXCOORD3; // not required when worldRefl calculated in vertex shader ... };※
worldRefl
を頂点シェーダーで計算した場合は、worldPos
をフラグメントシェーダーに渡す必要はなくなります。v2f
から消して、o.worldPos
をhalf3 worldPos
に変更します。頂点シェーダー
v2f vert(appdata v) { ... o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; float3 worldViewDir = normalize( UnityWorldSpaceViewDir(o.worldPos) ); float3 worldNormal = UnityObjectToWorldNormal(v.normal); o.worldRefl = reflect(-worldViewDir, worldNormal); ... }フラグメントシェーダー
※ 映り込みのボケはミップマップの LOD レベルを調整することで実現します。fixed4 frag(v2f i) : SV_Target { ... half3 worldRefl; ///////half3 worldPos; half mipmap_lod_level = 0; // blurry reflection ////////mipmap_lod_level = roughness * UNITY_SPECCUBE_LOD_STEPS; // worldspace data from vertex shader worldRefl = i.worldRefl; ///////worldPos = i.worldPos; // calculate worlspace data in fragment shader ////////worldPos = mul(unity_ObjectToWorld, i.pos).xyz; ////////worldRefl = reflect( -normalize(UnityWorldSpaceViewDir(worldPos)), UnityObjectToWorldNormal(i.normal) ); // reflection probe blending half4 skyData0 = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, worldRefl, mipmap_lod_level); half4 skyData1 = UNITY_SAMPLE_TEXCUBE_SAMPLER_LOD(unity_SpecCube1, unity_SpecCube0, worldRefl, mipmap_lod_level); half3 reflColor0 = DecodeHDR(skyData0, unity_SpecCube0_HDR); half3 reflColor1 = DecodeHDR(skyData1, unity_SpecCube1_HDR); // resulting reflection color half3 reflColor = lerp(reflColor1, reflColor0, unity_SpecCube0_BoxMin.w); ... }リフレクションプローブのブレンディングはこちらを参考にさせて頂きました。
(Unity 2018 LTS よりバージョンが古い Unity についての記事です)ライトマップのベイクに対応する
ライトマップのベイク時に周辺環境に影響を与えるようにする方法は、以下の通りです。
自作の自己発光マテリアルを使ってベイクした結果
まずは結果です。中央の黒い球体が、ライトマップのベイクの際に使われる Meta パスのみを実装したカスタムシェーダーで、周辺環境に対して光を放って影響を与えています。
自己発光させて周囲を照らしたい時の注意点
自己発光しているマテリアルで周囲を照らしたい場合、シェーダーの実装だけでは実現できません。
以下の通り
Material.globalIlluminationFlags
を適切に設定しておく必要があります。(これは分かりませんよ)
Yes. The thing is the color used by the baked lighting doesn't come from the usual vertex/fragment passes people are familiar with. Those come from a special "Meta" pass. See the Illumin-VertexLit.shader from the built in shader source for a basic example. However for realtime emission there's one more step that needs to be done. There's a flag that needs to be set on the material from script. Usually this is done with a custom material editor. It can be manually set via material.globalIlluminationFlags or automatically set with MaterialEditor.FixupEmissiveFlag().
https://forum.unity.com/threads/emmisive.492609/#post-3206559ビルトインのスタンダードシェーダーの
CustomEditor
として使われているStandardShaderGUI.cs
では、以下のような処理をしています。// A material's GI flag internally keeps track of whether emission is enabled at all, it's enabled but has no effect // or is enabled and may be modified at runtime. This state depends on the values of the current flag and emissive color. // The fixup routine makes sure that the material is in the correct state if/when changes are made to the mode or color. MaterialEditor.FixupEmissiveFlag(material); bool shouldEmissionBeEnabled = (material.globalIlluminationFlags & MaterialGlobalIlluminationFlags.EmissiveIsBlack) == 0; SetKeyword(material, "_EMISSION", shouldEmissionBeEnabled);MaterialGlobalIlluminationFlags を設定するスクリプト
エディターから設定する場合は、メッシュに対して以下のようなコンポーネントを適用します。設定が済めばアセット自体が更新されるので、剥がしても大丈夫(なハズ)です。
using UnityEngine; public class GlobalIlluminationFlags : MonoBehaviour { public MaterialGlobalIlluminationFlags flag = MaterialGlobalIlluminationFlags.AnyEmissive; void OnValidate() { var rend = GetComponent<Renderer>(); if (rend && rend.sharedMaterial) { rend.sharedMaterial.globalIlluminationFlags = flag; } } }ライトマップのベイク時に使用する "LightMode"="Meta" パスの実装
GI のベイクの際に使われる Meta パスは、
UnityMetaPass.cginc
で定義されている構造体に必要な値を設定して返してあげれば良いだけです。struct UnityMetaInput { half3 Albedo; half3 Emission; half3 SpecularColor; };Meta パス実装のサンプルコード
Unity built-in Shader の Standard Shader の Meta パスをベースに、フラグメントシェーダーのみカスタマイズして実装するのが最も簡単な手法です。
※ 強い光を設定する事が多い自己発光カラーのプロパティーは、
[HDR]
を付けておくと Inspector 上でスーパーホワイトの設定が出来るようになるので便利です。[HDR] _EmissionColor("Emission Color", Color) = (0, 0, 0, 1)※
#pragma fragment frag_meta_custom
等に名前を書き換えて自作シェーダーを実装します。
※UnityMetaInput
はUnityMetaFragment
を通して返します。
※_EmissionColor
など、UnityStandardInput.cginc
で定義済みのプロパティー変数の定義は不要です。(再定義するなとエラーが出ます)Pass { Name "META" Tags { "LightMode" = "Meta" } Cull Off CGPROGRAM #include "UnityStandardMeta.cginc" #pragma vertex vert_meta // change name and implement if customizing vertex shader #pragma fragment frag_meta_custom // changed to customized fragment shader name #pragma shader_feature _EMISSION #pragma shader_feature _METALLICGLOSSMAP #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A #pragma shader_feature ___ _DETAIL_MULX2 #pragma shader_feature EDITOR_VISUALIZATION // pre-defined in UnityStandardInput.cginc from UnityStandardMeta.cginc //half _Glossiness; //half4 _Color; //half4 _EmissionColor; half3 _SpecularColor; // customized fragment shader float4 frag_meta_custom(v2f_meta i) : SV_Target { UnityMetaInput o; UNITY_INITIALIZE_OUTPUT(UnityMetaInput, o); // required but no effect for EnergyConservationBetweenDiffuseAndSpecular half oneMinusReflectivity = 0; // do it half smoothness = _Glossiness; half3 albedo = _Color; half3 specularColor = _SpecularColor; half3 emissionColor = _EmissionColor; // assign result to output albedo = EnergyConservationBetweenDiffuseAndSpecular(albedo, specularColor, oneMinusReflectivity); #if defined(EDITOR_VISUALIZATION) o.Albedo = albedo; #else o.Albedo = UnityLightmappingAlbedo(albedo, specularColor, smoothness); #endif o.SpecularColor = specularColor; o.Emission = emissionColor; return UnityMetaFragment(o); } ENDCG }Diffuse と Specular と Smoothness の関係性
Unity の Standard シェーダーの Meta パスでは、Diffuse と Specular と Smoothness に基づいて重み付けを行う Unity のマクロが2つ使用されていて、その状態でベイクを行うと以下のような結果になります。
(当然ですが、カラーブリーディングに合わせて自身の見た目を実装する必要があります)※ 表面が滑らかだと Diffuse(赤)のみが拡散し、ザラついていると Specular(青)が混ざる。
表面が滑らかだとスペキュラー成分が拡散しないのではなく、本来であれば指向性の強い反射が起こるハズですが、2018 LTS ではライトマッパーがその現象に対応していません。(Enlighten、Progressive CPU / GPU ともに)
※ 表面が滑らかな曲面を使って、指向性の強い反射光を集めたレンダリング例
Standard シェーダーの Meta パスで使用されているマクロ関数
Standard シェーダーの Meta パスで使用されている Unity ビルトインのマクロ関数は、エネルギー保存の法則にしたがって Albedo の調整を行うものと、ライトマッパーに合わせてさらに Albedo の調整を行うものの2つで、以下の通り定義されています。
※
SpecularStrength
はspecColor
の.rgb
の3つのうち最大の値を返します。
※UnityStandardCore.cginc
のUNITY_SETUP_BRDF_INPUT
=SpecularSetup
内で一度 diffuse の調整を行い、焼きこむ際には次に示すUnityLightmappingAlbedo
マクロ関数でさらにライトマッパー向けの調整を行っているようです。// Diffuse/Spec Energy conservation inline half3 EnergyConservationBetweenDiffuseAndSpecular ( half3 albedo, half3 specColor, out half oneMinusReflectivity ) { oneMinusReflectivity = 1 - SpecularStrength(specColor); #if !UNITY_CONSERVE_ENERGY return albedo; #elif UNITY_CONSERVE_ENERGY_MONOCHROME return albedo * oneMinusReflectivity; #else return albedo * (half3(1, 1, 1) - specColor); #endif }※ Unity はエディタープレビュー時には上記の調整のみを行った Albedo を表示し、焼きこむ時のみ以下のマクロ関数で smoothness に基づいたライトマッパー向けの調整を加え、その結果を Albedo に設定しているようです。
※ プレビューか否かの処理はマクロ内ではなく Meta パス内に直接記述されているので、将来のアップデートに伴ってコード修正が必要になりそうな気がしています。half3 UnityLightmappingAlbedo (half3 diffuse, half3 specular, half smoothness) { half roughness = SmoothnessToRoughness(smoothness); half3 res = diffuse; res += specular * roughness * 0.5; return res; }公式ドキュメントの Global Illumination Meta パスの実装例
公式ドキュメントにも、自己発光に対応していない実装サンプルがあります。
Unity のライトマップをシェーダーに適用する
※ Unity でベイクしたライトマップをカスタムシェーダーに適用した状態。
Unity がベイクしたライトマップをシェーダーに適用する方法は、以下の通りです。
※ モデルインポーターの「Generate Lightmap UVs」の設定に関わらず、
TEXCOORD1
には UV が入っています。struct appdata { ... float2 texcoord1 : TEXCOORD1; ... };struct v2f { ... float2 texcoord1 : TEXCOORD1; ... };※ Unity のライトマップは複数のオブジェクトを一つのテクスチャアトラスにまとめた状態になっているので、ライトマップ内の指定の位置に UV をスケール&移動します。
v2f vert(appdata v) { ... o.texcoord1 = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw; ... }fixed4 frag(v2f i) : SV_Target { ... col.rgb *= DecodeLightmap( tex2D(unity_Lightmap, i.texcoord1.xy) ); ... }こちらの公式ドキュメントにも実装例があります。
影を受ける
影の情報をシェーダーで得る方法は、こちらの記事に非常に分かりやすくまとまっています。
影を落とす
カスタムシェーダーを割り当てたオブジェクトの影を落とす一番手っ取り早い方法は、後述の FallBack 法です。
頂点シェーダー等で変形させている場合の注意点
頂点シェーダーやジオメトリシェーダーによる変形がある場合は、変形処理を
ShadowCaster
パスでも同様に適用する必要があります。その為、以下の FallBack 法や UnityStandardShadow.cginc 法は使用できません。
※ 変形した場所ではなく、オレンジで囲われた本来の位置を元に影が落ちてしまっている。
ShadowCaster
パスでも同様の変形を行う必要があるので、VATKit_vertShader.cginc
等のように頂点シェーダーを外部ファイル化して#include
する等、重複した処理を書かずに済む方法も開発の早い段階から検討しておかないと、後々の切り分け作業で大変なことになりそうです。参考:
FallBack 法
上記のシェーダーテンプレートにも入っていますが、
FallBack "Mobile/VertexLit"
として処理を任せてしまうのが、将来的な保守も不要で一番です。
※
ShadowCaster
パスが存在するとフォールバックが起きないので、パスが存在しない状態にしておきます。UnityStandardShadow.cginc 法
こちらは Standard Shader の ShadowCaster パスをそのまま使う方法です。
将来的なアップデートで動かなくなる可能性も無くはないので、FallBack を本来の用途以外で使うのはためらわれる、という場合以外は
FallBack "Mobile/VertexLit"
を使う方が良いと思います。参考:
オフィシャルサイトのアーカイブから、「ダウンロード(Win or Mac)」→「ビルトインシェーダー」を選択して最新のシェーダーコードをダウンロードすることもできます。
全自前実装法
頂点シェーダーで変形している場合など、ShadowCaster パスを自身で実装する方法は、こちらが非常に参考になります。
※
VertexInput
をappdata
に、VertexOutput
をv2f
にすると見慣れたものになります。ステレオレンダリングへの対応
マルチパスのステレオレンダリング(単に複数のカメラで2回描画しているだけ)にはデフォルトで対応していますが、左右の目の画像を横に並べてレンダリングする、Single pass モードのステレオレンダリングに対応する為には、頂点シェーダー等を対応させる必要があります。
ざっくり言うとスクリーン座標など、画面が左右横並びで2倍幅になると影響を受ける処理は、Unity謹製のマクロを使おう、というお話です。
(例:mul(UNITY_MATRIX_MVP, v);
ではなくUnityObjectToClipPos()
を使う)加えて、Single-pass instanced(2018 LTS ではプレビュー版) モードへの対応もあります。そちらは以下に続きます。
インスタンス及び関連技術への対応(GPU Instancing)
シェーダーをインスタンス対応にすると、マテリアルのインスペクターに「Enable GPU Instancing」というチェックボックスが表示されるようになります。
逆を言えば、Enable GPU Instancing が無いものはインスタンスに未対応となり、インスタンスだけではなく以下の関連技術の全てが無効であるという事になります。
まず、最初にインスタンスへの対応方法はこちらです。
Material Property Block への対応
シェーダーをインスタンスに対応させることで、
MaterialPropertyBlock
への対応も可能になります。ただし、あくまでも似通ったマテリアルが大量に必要になる事態を防ぐための機能であって、インスタンスへの対応が必要だからと言って Batch 数や SetPass calls、Draw call が減ったりすることは無いようです。
※ Saved by batching: 0、Batches / SetPass calls ともに 5,404 で、減っている様子はない。
VAT シェーダーでは Time Offset が MaterialPropertyBlock に対応しているので、冒頭で紹介した動画のようにメッシュとマテリアルが一対でも、それぞれのアニメーションのタイミングを変えることが可能になっています。
※ 対応しても GPU Instancing のように Inspector の表示が変わるわけではないので、残念ながら外側からではどのプロパティーが対応しているのかは分かりません。
Material Property Block の使用方法
Material Property Block を使ったプロパティーの設定方法は、既存のものを取得 → 無ければ作成&セット → レンダラーへ適用の流れです。
Single pass instanced モードのステレオレンダリング
Single pass Instanced モードへの対応方法の詳細はこちらです。
フォグへの対応
Project ウインドウから作成できる、Unlit Shader のテンプレートで既に対応がなされていますが、
#pragma multi_compile_fog
を追加struct v2f
にUNITY_FOG_COORDS(1);
(1 は TEXCOORD# の # 部分)vert()
にUNITY_TRANSFER_FOG(o, o.vertex);
frag()
にUNITY_APPLY_FOG(i.fogCoord, apply_to_this_fixed4_color);
で対応させることができます。こちらの Unity Answers の記事が詳しいです。
フォグ対応の注意点
フォグのマクロは
LightMode
がForwardAdd
に設定されている場合は、なにもしてくれません。ForwardBase
もしくはLightMode
無しのパスでのみ動作します。
ForwardAdd
を単純に実装して複数のライトで照らした場合は、2つ目以降のライトがフォグを無視することになるので、少しおかしな結果になった気がします(うろ覚えですが…)この辺りはフォワードライティングという手法の限界かもしれませんね。※
i.fogCoord
の情報が ForwardAdd でも取得できるようなら、それをライティング側が配慮すれば対応は出来そうです。プラットフォームごとの違いを吸収する方法
OpenGL や DirectX など、プラットフォームごとに UV 座標やデプスバッファの扱いなどに違いがあります。
何もしなくても Unity が吸収してくれる場合や、そうではない場合があります。詳細や対応方法は以下のページから確認できます。
※ 抜粋:クリップ後の座標に違いがある。
- Direct3D-like: The clip space depth goes from +1.0 at the near plane to 0.0 at the far plane. This applies to Direct3D, Metal and consoles.
- OpenGL-like: The clip space depth goes from –1.0 at the near plane to +1.0 at the far plane. This applies to OpenGL and OpenGL ES.
※ 抜粋:デプスが黒→白ではなく、白→黒のプラットフォームがある。
- Use SystemInfo.usesReversedZBuffer to find out if you are on a platform using reversed depth (Z).
※ 抜粋:UV 座標系の違いに対応する方法。
// Flip sampling of the Texture: // The main Texture // texel size will have negative Y). #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0) uv.y = 1 - uv.y; #endifv2f vert(appdata v) { ... o.uv = v.uv; // This example is rendering with upside-down flipped projection, // so flip the vertical UV coordinate too if (_ProjectionParams.x < 0) { o.uv.y = 1 - o.uv.y; } ... }
UNITY_UV_STARTS_AT_TOP
や_ProjectionParams
については、こちらに詳細があります。ShaderLab の構造・文法の備忘録
ここからは、シェーダーの中身の構造・文法についてです。
自作シェーダー作成の際の注意点
基本的にサーフェイスシェーダーは使わない方が良いと思います。ある程度まではパッと作れますが、後々制限に悩まされます。
特に、サーフェイスシェーダーの情報を扱っている人が少ないという点がネックです。
※ 以下のようなコードが載っている場合はサーフェイスシェーダーの情報です。
- #pragma surface surf Lambert
- void surf(Input IN, inout SurfaceOutput o)
- SurfaceOutputStandard
- uv_MainTex
シェーダーのテンプレート
シェーダー開発のテンプレートとして、リファレンスの URL をコメントとして記載した物を作ってみました。
Shader "Path/To/The/Shader" { // https://docs.unity3d.com/Manual/SL-Properties.html // https://docs.unity3d.com/ScriptReference/MaterialPropertyDrawer.html Properties { } // https://docs.unity3d.com/Manual/SL-SubShader.html SubShader { // https://docs.unity3d.com/Manual/SL-SubShaderTags.html Tags { "Queue" = "Geometry" "RenderType" = "Opaque" } // https://docs.unity3d.com/Manual/SL-Pass.html // Cull, ZTest, etc... CGINCLUDE #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" #pragma target 3.0 ENDCG // https://docs.unity3d.com/Manual/SL-Pass.html Pass { // https://docs.unity3d.com/Manual/SL-PassTags.html Tags { } // https://docs.unity3d.com/Manual/SL-Pass.html // Cull, ZTest, etc... CGPROGRAM ENDCG } } FallBack "Mobile/VertexLit" // https://docs.unity3d.com/Manual/SL-CustomEditor.html //CustomEditor "ClassName" }構造体やシェーダープログラムのおススメと注意点
印象ベースですが、各要素の名前は下記のものがおススメです。
特に、
v2f
のpos
は、テンプレとして使う事も多い Unlit ShaderNewUnlitShader.shader
ではvertex
になっていますが、ライティング、影関連のマクロの影響でpos
に変更しておいた方が良いです。
(TRANSFER_VERTEX_TO_FRAGMENT(o)
というマクロでpos
がハードコードされています)※
struct
は最後にセミコロンstruct appdata { ... };struct v2f { float4 pos : SV_POSITION; ... };v2f vert(appdata v) { v2f o; ... return o; }fixed4 frag(v2f i) : SV_Target { ... }--
Properties
Properties ブロックでは、シェーダーに必要なテクスチャやその他のプロパティーを設定します。
※ シェーダープログラムからアクセスするには、後述の手続きが必要です。例:
_MyTexture ("My texture", 2D) = "white" {} _MyNormalMap ("My normal map", 2D) = "bump" {} //Grey _MyInt ("My integer", Int) = 2 _MyFloat ("My float", Float) = 1.5 _MyRange ("My range", Range(0.0, 1.0)) = 0.5 _MyColor ("My colour", Color) = (1, 0, 0, 1) // (R, G, B, A) _MyVector ("My Vector4", Vector) = (0, 0, 0, 0) // (x, y, z, w)プロパティーの命名に迷った場合
プロパティーの命名に迷ったら、
UnityStandardInput.cginc
を参考にすると良いかもしれません。抜粋:
//--------------------------------------- half4 _Color; half _Cutoff; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _DetailAlbedoMap; float4 _DetailAlbedoMap_ST; sampler2D _BumpMap; half _BumpScale; sampler2D _DetailMask; sampler2D _DetailNormalMap; half _DetailNormalMapScale; sampler2D _SpecGlossMap; sampler2D _MetallicGlossMap; half _Metallic; half _Glossiness; half _GlossMapScale; sampler2D _OcclusionMap; half _OcclusionStrength; sampler2D _ParallaxMap; half _Parallax; half _UVSec; half4 _EmissionColor; sampler2D _EmissionMap; //-------------------------------------------------------------------------------------マテリアルインスペクターのふるまいを調整
チェックボックスやスライダー付きの整数プロパティーなど、Unity のビルトインシェーダーのようなインスペクターを再現するには、以下のページにある Material Property Drawer アトリビュートを追加で設定する必要があります。
抜粋:
- 整数プロパティーにスライダーが欲しい場合は、
[IntRange]
- 単に Inspector でチェックボックスを表示したいだけなら、
[MaterialToggle]
- チェックボックスに応じて 0 or 1 がプロパティーに設定されます。
[Toggle]
はキーワードを設定したい時に使用する。
#pragma shader_feature
や#pragma multi_compile
で使用CustomEditor を指定して完全にカスタマイズする
上記のシェーダーテンプレートの、
FallBack
の並びにCustomEditor
を追加してクラス名を指定すると、指定のエディター拡張でインスペクターの描画を完全にコントロールできるようになります。CustomEditor "ShaderInspectorClassName"参考 ※指定のクラスを含むエディタースクリプトは
Editor
フォルダ以下に配置します。Cull や ZTest 等に Inspector で設定したプロパティーの値を使う
Cull 等にインスペクターで設定した値を渡したい場合は、
[プロパティ名]
と記述します。例:
Cull [_CullPropertyName]
ZTest [_ZTestPropertyName]
プロパティーに
Enum
を設定してドロップダウンメニューから選べるようにしておくと、間違った値を設定をしてしまうことを防ぐ事が出来ます。例:
[Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull", Float) = 0Enum がある場合はプロパティーに設定しておく
大抵のものは
UnityEngine.Rendering
に Enum が定義されているので、一覧から探して忘れずに設定しておきます。プロパティーにシェーダーからアクセスする
シェーダーからプロパティーの値を参照するには、
CGINCLUDE
かCGPROGRAM
内で変数を定義します。詳細はこちらです。※
uniform float4 _MyColor;
と書くことも出来ますが、uniform
は無くても良いです。テクスチャ解像度や時間などの特殊なプロパティー
テクスチャとして
_MainTex
を設定すると、Inspector から UV オフセットとスケールが設定できるようになります。シェーダーからそれらの値にアクセスするには、_ST
(Scale & Translate)を末尾に付けたfloat4 _MainTex_ST
という特殊なプロパティーを使います。同様に
float4 _MainTex_TexelSize
を定義した場合は、アサインされているテクスチャのサイズなどが取得できるようになります。その他にも、末尾
_HDR
や、VATシェーダーでも使っている_Time
等があります。詳細は以下のページが参考になります。SubShader と Pass
SubShader はプロパティの並びに必要なブロックで、タグを通して Render Queue の設定などが可能です。
Background (1000): 背景、スカイボックス(天球)
Geometry (2000): 不透明オブジェクト(描画順は手前→奥)
Transparent (3000): 透明オブジェクト(3,000 以上のキューから描画順が奥→手前になる)
Overlay (4000): レンズフレアや GUI、テキストなど※
"Geometry+5"
のように相対的に指定することも可能です。SubShader のその他の詳細はこちらから確認できます。
パフォーマンスに応じてシェーダーを切り替える LOD 処理
シェーダーの LOD については、こちらから確認できます。
SubShader の Tags
SubShader の
Tags
の詳細はこちらから確認できます。SubShader の Pass
SubShader には複数の
Pass
ブロックを持つ事ができ、影描画やライトマップのベイク時など、場合によって処理を変える事が出来ます。(パスが無い SubShader のみの実装でも動作します)Pass と SubShader は記述内容はほぼ同様ですが、Tags の内容が若干違います。
タグの後には SubShader と同様に
Cull
等の設定を必要に応じて記述します。CGPROGRAM ~ ENDCG
SubShader や Pass ブロック内に記述する、シェーダー本体の実装部分です。頂点シェーダーやピクセルシェーダーは CGPROGRAM ~ ENDCG の間に記述します。
Unity エンジンから取得できるデータの一覧など、良く参照する公式ドキュメントはこちらです。
チュートリアル
リファレンス
サンプル集
CGINCLUDE ~ ENDCG
上記の
CGPROGRAM
とほぼ同様ですが、SubShader 内などで、各 Pass 共通の処理を書くときに使用します。シェーダーモデルのターゲットレベル
ターゲットレベルは、基本的に Shader Model 3.0 にしておくと良いと思います。
#pragma target 3.0
各モデルでサポートされている機能の一覧はこちらで確認できます。
multi_compile vs. shader_feature
設定によって処理内容を変える方法として、
#pragma multi_compile...
と#pragma shader_feature...
の2つがあります。
multi_compile
- 使用しているかどうかに関わらず、すべての組み合わせがコンパイルされる。ランタイム中に
Material.EnableKeyword
で切り替えが可能。shader_feature
- プロジェクト内で使用している組み合わせだけがコンパイルされる。ランタイム中に切り替えは不可能。
とりあえず
multi_compile
を使っておこう、と思えますが、multi_compile
は組み合わせのすべてを shader variants としてコンパイルするので、数によっては非常に時間のかかる処理になってしまいます。
(例: オン・オフが4つ、2 ^ 4 = 16 種類のバリアント)シェーダーが初めて表示されるときにメインスレッドでコンパイルが走るわけですが、シェーダーバリアントが多い場合は、特にモバイル環境でコンパイルに時間がかかり、それが終わるまでアニメーション・パーティクル・UIの入力ほか全てが停まることになります。
そのため、
shader_feature
との使い分けが必要になります。参考:
TEXCOORD が不足した場合
UV は基本的には最大 4 枚(TEXCOORD0~3)で、
TEXCOORD1
はライトマップ用に使われる想定になっています。モデルのインポート設定で Generate Lightmap UVs をオンにした場合、TEXCOORD1
が更新されるという事ですね。この
TEXCOORD
は UV 以外にも Unity 標準のフォグの実装などでも使われていたり、その他様々な用途で消費され、足りなくなってきます。UVは2次元の座標ですが、
TEXCOORD
はfloat4
orhalf4
の4要素を持てるようになっています。なので、UV 1つ目をTEXCOORD#.xy
に、UV 2つ目をTEXCOORD#.zw
に、といったように2つを1つにまとめる事も可能です。
※ VAT シェーダーでもTEXCOORD0
の.zw
を使っています。Unity 上で UV を設定する場合、
Mesh.uv*
を使用すると.zw
の設定が出来ないので、代わりにMesh.GetUVs
&Mesh.SetUVs
を使います。
v2f
など、頂点シェーダーからピクセルシェーダーにデータを送る際には、TEXCOORD
を効率よく利用することを心がけておきたいです。頂点シェーダーの注意点
テクスチャの色情報を取得する
tex2D()
がありますが、これはフラグメントシェーダーの中でしか使えません。バーテックスシェーダーからテクスチャにアクセスしたい場合には、代わりに
tex2Dlod()
を使用します。フラグメントシェーダーの注意点
注意点、では無いですが、
fixed4 frag()
だと HDR レンダリングに対応できるのか、出来ないのか、調べ切れませんでした。溢れた色が思った通りにならなければ、
half4 frag()
やfloat4 frag()
にすると良いかもしれません。おわりに
冒頭で紹介した VAT シェーダーですが、仕事で使う必要が無くなった&他の事で手一杯なので、今後の開発についてはまっっったく予定が立たない状態ではありますが、積んでしまったパーティクル(Shuriken)のメッシュインスタンスへの対応なども、シェーダーの開発に合わせてこちらの備忘録まとめに追加していければ良いなと思っています。
今回は1体のポリゴン数が 4,472、アニメーションが 202 frames @ 30fps のアセットを録画しながら大量に動かす、という結構な重みでのテストでしたが、モバイルでも 100 体程度なら十分動いたのは予想外でした。
ループモーションで動くキャラをつまんで移動することもできるので、リアルタイムストラテジーのように引いた絵に大量のキャラがいるようなゲームであれば、VAT でのアニメーションでも十分なのかもしれませんね。
--
最後までお読み頂きありがとうございます。これは Unity のアドベントカレンダーに向けて書いていたのですが、当初想定した以上に長く&遅くなってしまいました。今後のシェーダー開発の助けになれば幸いです。
それでは以上になります。お疲れさまでした。
おまけ
モバイル端末(Android)でのテスト動画です。
モバイルで VAT シェーダーのテスト
— サトー (@sator_imaging) December 25, 2019
Vertex Animation Texture shader on mobile platform, #unity3d.
202 frames@30fps animation, 4,472 tris each.
Xperia 5 (Snapdragon 855 / Adreno 640)
60fps=180mesh
30fps=460mesh
1,000mesh=20fps pic.twitter.com/k1GpXStzQW
- 投稿日:2019-12-30T00:29:42+09:00
頂点アニメーションテクスチャ(VAT)& Unity シェーダーの開発まとめ
Unity のみで完結できる頂点アニメーションテクスチャー(VAT・Vertex Animation Texture)のエクスポーターとシェーダーを作りました。
Vertex Animation Texture shader, #unity3d.
— サトー (@sator_imaging) December 24, 2019
202 frames@30fps animation, 4,472 tris each mesh.
--
GeForce GTX 1080 on Unity Editor
90fps=1,500mesh
60fps=3,000mesh#madewithunity #unity pic.twitter.com/ctxG7GT4tg--
こちらの記事はとりあえず動くシェーダーは作れる、だけど… という内容です。
拾い物やネットからのコピペ、アセットストア経由など、プロジェクトは雑多なアセットのチャンプルーが常で、たまーにシェーダーを作ったり、という方や、プロジェクトの規模に合うであろう内容になっています。
もくじ
- はじめに
- Unity の標準機能への対応方法
- プラットフォームごとの違いを吸収する方法
- ShaderLab の構造・文法の備忘録
- Properties
- SubShader と Pass
- CGPROGRAM ~ ENDCG
- おわりに
はじめに
Project ウインドウから Unlit Shader を作り、それをベースに自作シェーダーを作ると、影を落とさない&受けない、Global Illumination の各種プローブ、ライトマップの適用やベイクにも反応しないシェーダーが出来上がります。
加えて、Unity のバージョンアップによって古くなった情報がいまでも検索に引っかかる為、専業ではない人間ではなかなか新しい情報に巡り合えず、開発に無駄な時間がかかってしまうのが Unity シェーダー開発のあるあるかと思います。
この記事は、VAT シェーダーを開発する過程で調べた、Unity の各標準機能に対応したシェーダーを開発する為に必要な情報と ShaderLab の備忘録をまとめたものです。
- バージョン: Unity 2018 LTS
- グラス型AR・VR やモバイル等で Forward Rendering を使っている
- なんだかんだで Built-in Render Pipeline (Legacy) を使っている
目標
前述の通り、Unity の標準機能に一通り対応させることです。特定のプロジェクトだけではなく汎用的に使うことができ、アセットストアへの出品も可能なものを目指します。
対応させたい Unity 標準機能の一覧
- GI & ライトマップのベイクと反映が行われる。
- Light Probe がある環境では影響を受ける。
- Reflection Probe のレンダリングに映る。
- 天球・スカイボックスや Reflection Probe が映り込む。
- ライトマップが適用される。
- ライトマップのベイク時に周辺環境に影響を与える。
- 影を落とす&受ける。
- GPU Instancing と関連技術に対応している。
- グラス型AR・VR でのステレオレンダリングで、Single Pass が使える。
- Single Pass(Instanced) にも対応している。 ※ 全てのアセットに目が行き届く開発環境でないと使ったことが無い人が多いのでは・・・?
- Lighting 設定のフォグの影響を受ける。
Universal Render Pipeline(旧LWRP)について
先日ベータ版から抜けて正式リリースとなった Universal Render Pipeline。
ここではシェーダーの基本的なところは抑えていますが、Built-in 向けのシェーダーを URP に対応させるにはどうしたらいいか、については扱っていません。ステレオレンダリング&フォワードレンダリングやモバイルに関わることが多いという事もありますが、冒頭で述べたように、自身の管理外のアセットの対応状況に左右されることなどを考えると、中小規模のプロジェクトではまだ時期尚早かなと思っています。
以下の表を見て、うっ、となったというのもあります。
Shader Graph について
Maya のような DCC ツールもノードベースで見栄えをいじりますが、最後の最後で結局、欲しい情報を取れるノードが無くて望むものが作り切れない、なんて事も多いので、コーディングせずに Unity Shader Graph で、、、も扱っていません。
個人的には、ノードベースでは作りきれない、となった時に Shader Graph から ShaderLab のコードを書き出して自分で実装する、という選択もあると思うので、Unity でのシェーダー開発の方法は理解しておいて損はないと思います。
Unity の標準機能への対応方法
まずは Unity 標準機能への対応方法の紹介です。
ShaderLab の基本的なところは、備忘録として次の章にまとめています。
Global Illumination への対応
Light Probe(球面調和)の影響を受けるようにする
ライトプローブに対応するには、
UnityCG.cginc
のShadeSH9
を使用します。このマクロはPass
のタグ設定でLightMode
をForwardBase
に設定した状態で使用する必要があります。#include "UnityCG.cginc" ... Tags { "LightMode" = "ForwardBase" }球面調和は可能な限り頂点シェーダーで処理する
球面調和(Spherical Harmonics)はけして軽い処理ではないので、頂点シェーダーで十分な結果を得られる場合はピクセルシェーダーで処理をしない方が良いです。
以下のサンプル画像を見てわかるように、Light Probe と Lighting > Environment Lighting の影響を計算するだけなので、ノーマルマッピング等を行わない限りにおいてはクッキリはっきりとした変化のある絵は出ないからです。
※ ロボットの下手側の肩のライティング結果に、わずかながら違いが見受けられます。
例(頂点シェーダー):
o.color.rgb *= ShadeSH9( half4(UnityObjectToWorldNormal(v.normal), 1) );※ 頂点シェーダーで設定したカラーはフラグメントシェーダーに渡される際に、GPU で頂点間を線形補完された状態になります。
例(ピクセルシェーダー):
col.rgb *= ShadeSH9( half4(UnityObjectToWorldNormal(i.normal), 1) );Reflection Probe に映るようにする
カスタムシェーダー側で何もしなくても Reflection Probe に映り込み、頂点シェーダーの変形結果も反映されます。もしも映らない場合は Reflection Probe Static の設定を忘れていないかを確認します。
※ VAT Kit ではシェーダー内で
_Time
を使用してアニメーションさせていますが、ベイクする度に映り込む位置が1フレームずつ進んでしまいました。Unity & ShaderLab ではベイク用のパスなどは用意されていないので、必要に応じて対策を施す必要があります。Reflection Probe や天球・スカイボックスが映り込むようにする
Reflection Probe 及び Lighting > Environment で設定された天球・スカイボックスの情報の取得方法は、こちらの公式ドキュメントに載っています。
ただし、上記の公式ドキュメントの実装だと、なぜか複数の Reflection Probe を跨いだ際のブレンドには対応していません。
Reflection Probe のブレンドに対応する
ブレンディングに対応するにはフラグメントシェーダーで追加の処理が必要になります。
※ Reflection Probe のブレンドに対応した例
リフレクションの計算には、ワールド空間の法線と視線ベクトルが必要になります。
ノーマルマッピング等を行わない場合は、頂点シェーダーでリフレクションの法線を事前に計算しておき、映り込みのサンプリングのみフラグメントシェーダーで行います。
※ 視線ベクトルの計算に必要な頂点のワールド座標は、頂点シェーダーで計算した場合も GPU で線形補完されるので精度は変わらない(ハズ)なので、ノーマルマッピングをする時は頂点の座標のみ頂点シェーダーで計算しておくのも良いかもしれません。
Reflection Probe 対応のサンプルコード
各手法を切り替えられるようにコメントとして残したサンプルです。
struct v2f { ... half3 worldRefl : TEXCOORD2; half3 worldPos : TEXCOORD3; // not required when worldRefl calculated in vertex shader ... };※
worldRefl
を頂点シェーダーで計算した場合は、worldPos
をフラグメントシェーダーに渡す必要はなくなります。v2f
から消して、o.worldPos
をhalf3 worldPos
に変更します。頂点シェーダー
v2f vert(appdata v) { ... o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; float3 worldViewDir = normalize( UnityWorldSpaceViewDir(o.worldPos) ); float3 worldNormal = UnityObjectToWorldNormal(v.normal); o.worldRefl = reflect(-worldViewDir, worldNormal); ... }フラグメントシェーダー
※ 映り込みのボケはミップマップの LOD レベルを調整することで実現します。fixed4 frag(v2f i) : SV_Target { ... half3 worldRefl; ///////half3 worldPos; half mipmap_lod_level = 0; // blurry reflection ////////mipmap_lod_level = roughness * UNITY_SPECCUBE_LOD_STEPS; // worldspace data from vertex shader worldRefl = i.worldRefl; ///////worldPos = i.worldPos; // calculate worlspace data in fragment shader ////////worldPos = mul(unity_ObjectToWorld, i.pos).xyz; ////////worldRefl = reflect( -normalize(UnityWorldSpaceViewDir(worldPos)), UnityObjectToWorldNormal(i.normal) ); // reflection probe blending half4 skyData0 = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, worldRefl, mipmap_lod_level); half4 skyData1 = UNITY_SAMPLE_TEXCUBE_SAMPLER_LOD(unity_SpecCube1, unity_SpecCube0, worldRefl, mipmap_lod_level); half3 reflColor0 = DecodeHDR(skyData0, unity_SpecCube0_HDR); half3 reflColor1 = DecodeHDR(skyData1, unity_SpecCube1_HDR); // resulting reflection color half3 reflColor = lerp(reflColor1, reflColor0, unity_SpecCube0_BoxMin.w); ... }リフレクションプローブのブレンディングはこちらを参考にさせて頂きました。
(Unity 2018 LTS よりバージョンが古い Unity についての記事です)ライトマップのベイクに対応する
ライトマップのベイク時に周辺環境に影響を与えるようにする方法は、以下の通りです。
自作の自己発光マテリアルを使ってベイクした結果
まずは結果です。中央の黒い球体が、ライトマップのベイクの際に使われる Meta パスのみを実装したカスタムシェーダーで、周辺環境に対して光を放って影響を与えています。
自己発光させて周囲を照らしたい時の注意点
自己発光しているマテリアルで周囲を照らしたい場合、シェーダーの実装だけでは実現できません。
以下の通り
Material.globalIlluminationFlags
を適切に設定しておく必要があります。(これは分かりませんよ)
Yes. The thing is the color used by the baked lighting doesn't come from the usual vertex/fragment passes people are familiar with. Those come from a special "Meta" pass. See the Illumin-VertexLit.shader from the built in shader source for a basic example. However for realtime emission there's one more step that needs to be done. There's a flag that needs to be set on the material from script. Usually this is done with a custom material editor. It can be manually set via material.globalIlluminationFlags or automatically set with MaterialEditor.FixupEmissiveFlag().
https://forum.unity.com/threads/emmisive.492609/#post-3206559ビルトインのスタンダードシェーダーの
CustomEditor
として使われているStandardShaderGUI.cs
では、以下のような処理をしています。// A material's GI flag internally keeps track of whether emission is enabled at all, it's enabled but has no effect // or is enabled and may be modified at runtime. This state depends on the values of the current flag and emissive color. // The fixup routine makes sure that the material is in the correct state if/when changes are made to the mode or color. MaterialEditor.FixupEmissiveFlag(material); bool shouldEmissionBeEnabled = (material.globalIlluminationFlags & MaterialGlobalIlluminationFlags.EmissiveIsBlack) == 0; SetKeyword(material, "_EMISSION", shouldEmissionBeEnabled);MaterialGlobalIlluminationFlags を設定するスクリプト
エディターから設定する場合は、メッシュに対して以下のようなコンポーネントを適用します。設定が済めばアセット自体が更新されるので、剥がしても大丈夫(なハズ)です。
using UnityEngine; public class GlobalIlluminationFlags : MonoBehaviour { public MaterialGlobalIlluminationFlags flag = MaterialGlobalIlluminationFlags.AnyEmissive; void OnValidate() { var rend = GetComponent<Renderer>(); if (rend && rend.sharedMaterial) { rend.sharedMaterial.globalIlluminationFlags = flag; } } }ライトマップのベイク時に使用する "LightMode"="Meta" パスの実装
GI のベイクの際に使われる Meta パスは、
UnityMetaPass.cginc
で定義されている構造体に必要な値を設定して返してあげれば良いだけです。struct UnityMetaInput { half3 Albedo; half3 Emission; half3 SpecularColor; };Meta パス実装のサンプルコード
Unity built-in Shader の Standard Shader の Meta パスをベースに、フラグメントシェーダーのみカスタマイズして実装するのが最も簡単な手法です。
※ 強い光を設定する事が多い自己発光カラーのプロパティーは、
[HDR]
を付けておくと Inspector 上でスーパーホワイトの設定が出来るようになるので便利です。[HDR] _EmissionColor("Emission Color", Color) = (0, 0, 0, 1)※
#pragma fragment frag_meta_custom
等に名前を書き換えて自作シェーダーを実装します。
※UnityMetaInput
はUnityMetaFragment
を通して返します。
※_EmissionColor
など、UnityStandardInput.cginc
で定義済みのプロパティー変数の定義は不要です。(再定義するなとエラーが出ます)Pass { Name "META" Tags { "LightMode" = "Meta" } Cull Off CGPROGRAM #include "UnityStandardMeta.cginc" #pragma vertex vert_meta // change name and implement if customizing vertex shader #pragma fragment frag_meta_custom // changed to customized fragment shader name #pragma shader_feature _EMISSION #pragma shader_feature _METALLICGLOSSMAP #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A #pragma shader_feature ___ _DETAIL_MULX2 #pragma shader_feature EDITOR_VISUALIZATION // pre-defined in UnityStandardInput.cginc from UnityStandardMeta.cginc //half _Glossiness; //half4 _Color; //half4 _EmissionColor; half3 _SpecularColor; // customized fragment shader float4 frag_meta_custom(v2f_meta i) : SV_Target { UnityMetaInput o; UNITY_INITIALIZE_OUTPUT(UnityMetaInput, o); // required but no effect for EnergyConservationBetweenDiffuseAndSpecular half oneMinusReflectivity = 0; // do it half smoothness = _Glossiness; half3 albedo = _Color; half3 specularColor = _SpecularColor; half3 emissionColor = _EmissionColor; // assign result to output albedo = EnergyConservationBetweenDiffuseAndSpecular(albedo, specularColor, oneMinusReflectivity); #if defined(EDITOR_VISUALIZATION) o.Albedo = albedo; #else o.Albedo = UnityLightmappingAlbedo(albedo, specularColor, smoothness); #endif o.SpecularColor = specularColor; o.Emission = emissionColor; return UnityMetaFragment(o); } ENDCG }Diffuse と Specular と Smoothness の関係性
Unity の Standard シェーダーの Meta パスでは、Diffuse と Specular と Smoothness に基づいて重み付けを行う Unity のマクロが2つ使用されていて、その状態でベイクを行うと以下のような結果になります。
(当然ですが、カラーブリーディングに合わせて自身の見た目を実装する必要があります)※ 表面が滑らかだと Diffuse(赤)のみが拡散し、ザラついていると Specular(青)が混ざる。
表面が滑らかだとスペキュラー成分が拡散しないのではなく、本来であれば指向性の強い反射が起こるハズですが、2018 LTS ではライトマッパーがその現象に対応していません。(Enlighten、Progressive CPU / GPU ともに)
※ 表面が滑らかな曲面を使って、指向性の強い反射光を集めたレンダリング例
Standard シェーダーの Meta パスで使用されているマクロ関数
Standard シェーダーの Meta パスで使用されている Unity ビルトインのマクロ関数は、エネルギー保存の法則にしたがって Albedo の調整を行うものと、ライトマッパーに合わせてさらに Albedo の調整を行うものの2つで、以下の通り定義されています。
※
SpecularStrength
はspecColor
の.rgb
の3つのうち最大の値を返します。
※UnityStandardCore.cginc
のUNITY_SETUP_BRDF_INPUT
=SpecularSetup
内で一度 diffuse の調整を行い、焼きこむ際には次に示すUnityLightmappingAlbedo
マクロ関数でさらにライトマッパー向けの調整を行っているようです。// Diffuse/Spec Energy conservation inline half3 EnergyConservationBetweenDiffuseAndSpecular ( half3 albedo, half3 specColor, out half oneMinusReflectivity ) { oneMinusReflectivity = 1 - SpecularStrength(specColor); #if !UNITY_CONSERVE_ENERGY return albedo; #elif UNITY_CONSERVE_ENERGY_MONOCHROME return albedo * oneMinusReflectivity; #else return albedo * (half3(1, 1, 1) - specColor); #endif }※ Unity はエディタープレビュー時には上記の調整のみを行った Albedo を表示し、焼きこむ時のみ以下のマクロ関数で smoothness に基づいたライトマッパー向けの調整を加え、その結果を Albedo に設定しているようです。
※ プレビューか否かの処理はマクロ内ではなく Meta パス内に直接記述されているので、将来のアップデートに伴ってコード修正が必要になりそうな気がしています。half3 UnityLightmappingAlbedo (half3 diffuse, half3 specular, half smoothness) { half roughness = SmoothnessToRoughness(smoothness); half3 res = diffuse; res += specular * roughness * 0.5; return res; }公式ドキュメントの Global Illumination Meta パスの実装例
公式ドキュメントにも、自己発光に対応していない実装サンプルがあります。
Unity のライトマップをシェーダーに適用する
※ Unity でベイクしたライトマップをカスタムシェーダーに適用した状態。
Unity がベイクしたライトマップをシェーダーに適用する方法は、以下の通りです。
※ モデルインポーターの「Generate Lightmap UVs」の設定に関わらず、
TEXCOORD1
には UV が入っています。struct appdata { ... float2 texcoord1 : TEXCOORD1; ... };struct v2f { ... float2 texcoord1 : TEXCOORD1; ... };※ Unity のライトマップは複数のオブジェクトを一つのテクスチャアトラスにまとめた状態になっているので、ライトマップ内の指定の位置に UV をスケール&移動します。
v2f vert(appdata v) { ... o.texcoord1 = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw; ... }fixed4 frag(v2f i) : SV_Target { ... col.rgb *= DecodeLightmap( tex2D(unity_Lightmap, i.texcoord1.xy) ); ... }こちらの公式ドキュメントにも実装例があります。
影を受ける
影の情報をシェーダーで得る方法は、こちらの記事に非常に分かりやすくまとまっています。
影を落とす
カスタムシェーダーを割り当てたオブジェクトの影を落とす一番手っ取り早い方法は、後述の FallBack 法です。
頂点シェーダー等で変形させている場合の注意点
頂点シェーダーやジオメトリシェーダーによる変形がある場合は、変形処理を
ShadowCaster
パスでも同様に適用する必要があります。その為、以下の FallBack 法や UnityStandardShadow.cginc 法は使用できません。
※ 変形した場所ではなく、オレンジで囲われた本来の位置を元に影が落ちてしまっている。
ShadowCaster
パスでも同様の変形を行う必要があるので、VATKit_vertShader.cginc
等のように頂点シェーダーを外部ファイル化して#include
する等、重複した処理を書かずに済む方法も開発の早い段階から検討しておかないと、後々の切り分け作業で大変なことになりそうです。参考:
FallBack 法
上記のシェーダーテンプレートにも入っていますが、
FallBack "Mobile/VertexLit"
として処理を任せてしまうのが、将来的な保守も不要で一番です。
※
ShadowCaster
パスが存在するとフォールバックが起きないので、パスが存在しない状態にしておきます。UnityStandardShadow.cginc 法
こちらは Standard Shader の ShadowCaster パスをそのまま使う方法です。
将来的なアップデートで動かなくなる可能性も無くはないので、FallBack を本来の用途以外で使うのはためらわれる、という場合以外は
FallBack "Mobile/VertexLit"
を使う方が良いと思います。参考:
オフィシャルサイトのアーカイブから、「ダウンロード(Win or Mac)」→「ビルトインシェーダー」を選択して最新のシェーダーコードをダウンロードすることもできます。
全自前実装法
頂点シェーダーで変形している場合など、ShadowCaster パスを自身で実装する方法は、こちらが非常に参考になります。
※
VertexInput
をappdata
に、VertexOutput
をv2f
にすると見慣れたものになります。ステレオレンダリングへの対応
マルチパスのステレオレンダリング(単に複数のカメラで2回描画しているだけ)にはデフォルトで対応していますが、左右の目の画像を横に並べてレンダリングする、Single pass モードのステレオレンダリングに対応する為には、頂点シェーダー等を対応させる必要があります。
ざっくり言うとスクリーン座標など、画面が左右横並びで2倍幅になると影響を受ける処理は、Unity謹製のマクロを使おう、というお話です。
(例:mul(UNITY_MATRIX_MVP, v);
ではなくUnityObjectToClipPos()
を使う)加えて、Single-pass instanced(2018 LTS ではプレビュー版) モードへの対応もあります。そちらは以下に続きます。
インスタンス及び関連技術への対応(GPU Instancing)
シェーダーをインスタンス対応にすると、マテリアルのインスペクターに「Enable GPU Instancing」というチェックボックスが表示されるようになります。
逆を言えば、Enable GPU Instancing が無いものはインスタンスに未対応となり、インスタンスだけではなく以下の関連技術の全てが無効であるという事になります。
まず、最初にインスタンスへの対応方法はこちらです。
Material Property Block への対応
シェーダーをインスタンスに対応させることで、
MaterialPropertyBlock
への対応も可能になります。ただし、あくまでも似通ったマテリアルが大量に必要になる事態を防ぐための機能であって、インスタンスへの対応が必要だからと言って Batch 数や SetPass calls、Draw call が減ったりすることは無いようです。
※ Saved by batching: 0、Batches / SetPass calls ともに 5,404 で、減っている様子はない。
VAT シェーダーでは Time Offset が MaterialPropertyBlock に対応しているので、冒頭で紹介した動画のようにメッシュとマテリアルが一対でも、それぞれのアニメーションのタイミングを変えることが可能になっています。
※ 対応しても GPU Instancing のように Inspector の表示が変わるわけではないので、残念ながら外側からではどのプロパティーが対応しているのかは分かりません。
Material Property Block の使用方法
Material Property Block を使ったプロパティーの設定方法は、既存のものを取得 → 無ければ作成&セット → レンダラーへ適用の流れです。
Single pass instanced モードのステレオレンダリング
Single pass Instanced モードへの対応方法の詳細はこちらです。
フォグへの対応
Project ウインドウから作成できる、Unlit Shader のテンプレートで既に対応がなされていますが、
#pragma multi_compile_fog
を追加struct v2f
にUNITY_FOG_COORDS(1);
(1 は TEXCOORD# の # 部分)vert()
にUNITY_TRANSFER_FOG(o, o.vertex);
frag()
にUNITY_APPLY_FOG(i.fogCoord, apply_to_this_fixed4_color);
で対応させることができます。こちらの Unity Answers の記事が詳しいです。
フォグ対応の注意点
フォグのマクロは
LightMode
がForwardAdd
に設定されている場合は、なにもしてくれません。ForwardBase
もしくはLightMode
無しのパスでのみ動作します。
ForwardAdd
を単純に実装して複数のライトで照らした場合は、2つ目以降のライトがフォグを無視することになるので、少しおかしな結果になった気がします(うろ覚えですが…)この辺りはフォワードライティングという手法の限界かもしれませんね。※
i.fogCoord
の情報が ForwardAdd でも取得できるようなら、それをライティング側が配慮すれば対応は出来そうです。プラットフォームごとの違いを吸収する方法
OpenGL や DirectX など、プラットフォームごとに UV 座標やデプスバッファの扱いなどに違いがあります。
何もしなくても Unity が吸収してくれる場合や、そうではない場合があります。詳細や対応方法は以下のページから確認できます。
※ 抜粋:クリップ後の座標に違いがある。
- Direct3D-like: The clip space depth goes from +1.0 at the near plane to 0.0 at the far plane. This applies to Direct3D, Metal and consoles.
- OpenGL-like: The clip space depth goes from –1.0 at the near plane to +1.0 at the far plane. This applies to OpenGL and OpenGL ES.
※ 抜粋:デプスが黒→白ではなく、白→黒のプラットフォームがある。
- Use SystemInfo.usesReversedZBuffer to find out if you are on a platform using reversed depth (Z).
※ 抜粋:UV 座標系の違いに対応する方法。
// Flip sampling of the Texture: // The main Texture // texel size will have negative Y). #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0) uv.y = 1 - uv.y; #endifv2f vert(appdata v) { ... o.uv = v.uv; // This example is rendering with upside-down flipped projection, // so flip the vertical UV coordinate too if (_ProjectionParams.x < 0) { o.uv.y = 1 - o.uv.y; } ... }
UNITY_UV_STARTS_AT_TOP
や_ProjectionParams
については、こちらに詳細があります。ShaderLab の構造・文法の備忘録
ここからは、シェーダーの中身の構造・文法についてです。
自作シェーダー作成の際の注意点
基本的にサーフェイスシェーダーは使わない方が良いと思います。ある程度まではパッと作れますが、後々制限に悩まされます。
特に、サーフェイスシェーダーの情報を扱っている人が少ないという点がネックです。
※ 以下のようなコードが載っている場合はサーフェイスシェーダーの情報です。
- #pragma surface surf Lambert
- void surf(Input IN, inout SurfaceOutput o)
- SurfaceOutputStandard
- uv_MainTex
シェーダーのテンプレート
シェーダー開発のテンプレートとして、リファレンスの URL をコメントとして記載した物を作ってみました。
Shader "Path/To/The/Shader" { // https://docs.unity3d.com/Manual/SL-Properties.html // https://docs.unity3d.com/ScriptReference/MaterialPropertyDrawer.html Properties { } // https://docs.unity3d.com/Manual/SL-SubShader.html SubShader { // https://docs.unity3d.com/Manual/SL-SubShaderTags.html Tags { "Queue" = "Geometry" "RenderType" = "Opaque" } // https://docs.unity3d.com/Manual/SL-Pass.html // Cull, ZTest, etc... CGINCLUDE #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" #pragma target 3.0 ENDCG // https://docs.unity3d.com/Manual/SL-Pass.html Pass { // https://docs.unity3d.com/Manual/SL-PassTags.html Tags { } // https://docs.unity3d.com/Manual/SL-Pass.html // Cull, ZTest, etc... CGPROGRAM ENDCG } } FallBack "Mobile/VertexLit" // https://docs.unity3d.com/Manual/SL-CustomEditor.html //CustomEditor "ClassName" }構造体やシェーダープログラムのおススメと注意点
印象ベースですが、各要素の名前は下記のものがおススメです。
特に、
v2f
のpos
は、テンプレとして使う事も多い Unlit ShaderNewUnlitShader.shader
ではvertex
になっていますが、ライティング、影関連のマクロの影響でpos
に変更しておいた方が良いです。
(TRANSFER_VERTEX_TO_FRAGMENT(o)
というマクロでpos
がハードコードされています)※
struct
は最後にセミコロンstruct appdata { ... };struct v2f { float4 pos : SV_POSITION; ... };v2f vert(appdata v) { v2f o; ... return o; }fixed4 frag(v2f i) : SV_Target { ... }--
Properties
Properties ブロックでは、シェーダーに必要なテクスチャやその他のプロパティーを設定します。
※ シェーダープログラムからアクセスするには、後述の手続きが必要です。例:
_MyTexture ("My texture", 2D) = "white" {} _MyNormalMap ("My normal map", 2D) = "bump" {} //Grey _MyInt ("My integer", Int) = 2 _MyFloat ("My float", Float) = 1.5 _MyRange ("My range", Range(0.0, 1.0)) = 0.5 _MyColor ("My colour", Color) = (1, 0, 0, 1) // (R, G, B, A) _MyVector ("My Vector4", Vector) = (0, 0, 0, 0) // (x, y, z, w)プロパティーの命名に迷った場合
プロパティーの命名に迷ったら、
UnityStandardInput.cginc
を参考にすると良いかもしれません。抜粋:
//--------------------------------------- half4 _Color; half _Cutoff; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _DetailAlbedoMap; float4 _DetailAlbedoMap_ST; sampler2D _BumpMap; half _BumpScale; sampler2D _DetailMask; sampler2D _DetailNormalMap; half _DetailNormalMapScale; sampler2D _SpecGlossMap; sampler2D _MetallicGlossMap; half _Metallic; half _Glossiness; half _GlossMapScale; sampler2D _OcclusionMap; half _OcclusionStrength; sampler2D _ParallaxMap; half _Parallax; half _UVSec; half4 _EmissionColor; sampler2D _EmissionMap; //-------------------------------------------------------------------------------------マテリアルインスペクターのふるまいを調整
チェックボックスやスライダー付きの整数プロパティーなど、Unity のビルトインシェーダーのようなインスペクターを再現するには、以下のページにある Material Property Drawer アトリビュートを追加で設定する必要があります。
抜粋:
- 整数プロパティーにスライダーが欲しい場合は、
[IntRange]
- 単に Inspector でチェックボックスを表示したいだけなら、
[MaterialToggle]
- チェックボックスに応じて 0 or 1 がプロパティーに設定されます。
[Toggle]
はキーワードを設定したい時に使用する。
#pragma shader_feature
や#pragma multi_compile
で使用CustomEditor を指定して完全にカスタマイズする
上記のシェーダーテンプレートの、
FallBack
の並びにCustomEditor
を追加してクラス名を指定すると、指定のエディター拡張でインスペクターの描画を完全にコントロールできるようになります。CustomEditor "ShaderInspectorClassName"参考 ※指定のクラスを含むエディタースクリプトは
Editor
フォルダ以下に配置します。Cull や ZTest 等に Inspector で設定したプロパティーの値を使う
Cull 等にインスペクターで設定した値を渡したい場合は、
[プロパティ名]
と記述します。例:
Cull [_CullPropertyName]
ZTest [_ZTestPropertyName]
プロパティーに
Enum
を設定してドロップダウンメニューから選べるようにしておくと、間違った値を設定をしてしまうことを防ぐ事が出来ます。例:
[Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull", Float) = 0Enum がある場合はプロパティーに設定しておく
大抵のものは
UnityEngine.Rendering
に Enum が定義されているので、一覧から探して忘れずに設定しておきます。プロパティーにシェーダーからアクセスする
シェーダーからプロパティーの値を参照するには、
CGINCLUDE
かCGPROGRAM
内で変数を定義します。詳細はこちらです。※
uniform float4 _MyColor;
と書くことも出来ますが、uniform
は無くても良いです。テクスチャ解像度や時間などの特殊なプロパティー
テクスチャとして
_MainTex
を設定すると、Inspector から UV オフセットとスケールが設定できるようになります。シェーダーからそれらの値にアクセスするには、_ST
(Scale & Translate)を末尾に付けたfloat4 _MainTex_ST
という特殊なプロパティーを使います。同様に
float4 _MainTex_TexelSize
を定義した場合は、アサインされているテクスチャのサイズなどが取得できるようになります。その他にも、末尾
_HDR
や、VATシェーダーでも使っている_Time
等があります。詳細は以下のページが参考になります。SubShader と Pass
SubShader はプロパティの並びに必要なブロックで、タグを通して Render Queue の設定などが可能です。
Background (1000): 背景、スカイボックス(天球)
Geometry (2000): 不透明オブジェクト(描画順は手前→奥)
Transparent (3000): 透明オブジェクト(3,000 以上のキューから描画順が奥→手前になる)
Overlay (4000): レンズフレアや GUI、テキストなど※
"Geometry+5"
のように相対的に指定することも可能です。SubShader のその他の詳細はこちらから確認できます。
パフォーマンスに応じてシェーダーを切り替える LOD 処理
シェーダーの LOD については、こちらから確認できます。
SubShader の Tags
SubShader の
Tags
の詳細はこちらから確認できます。SubShader の Pass
SubShader には複数の
Pass
ブロックを持つ事ができ、影描画やライトマップのベイク時など、場合によって処理を変える事が出来ます。(パスが無い SubShader のみの実装でも動作します)Pass と SubShader は記述内容はほぼ同様ですが、Tags の内容が若干違います。
タグの後には SubShader と同様に
Cull
等の設定を必要に応じて記述します。CGPROGRAM ~ ENDCG
SubShader や Pass ブロック内に記述する、シェーダー本体の実装部分です。頂点シェーダーやピクセルシェーダーは CGPROGRAM ~ ENDCG の間に記述します。
Unity エンジンから取得できるデータの一覧など、良く参照する公式ドキュメントはこちらです。
チュートリアル
リファレンス
サンプル集
CGINCLUDE ~ ENDCG
上記の
CGPROGRAM
とほぼ同様ですが、SubShader 内などで、各 Pass 共通の処理を書くときに使用します。シェーダーモデルのターゲットレベル
ターゲットレベルは、基本的に Shader Model 3.0 にしておくと良いと思います。
#pragma target 3.0
各モデルでサポートされている機能の一覧はこちらで確認できます。
multi_compile vs. shader_feature
設定によって処理内容を変える方法として、
#pragma multi_compile...
と#pragma shader_feature...
の2つがあります。
multi_compile
- 使用しているかどうかに関わらず、すべての組み合わせがコンパイルされる。ランタイム中に
Material.EnableKeyword
で切り替えが可能。shader_feature
- プロジェクト内で使用している組み合わせだけがコンパイルされる。ランタイム中に切り替えは不可能。
とりあえず
multi_compile
を使っておこう、と思えますが、multi_compile
は組み合わせのすべてを shader variants としてコンパイルするので、数によっては非常に時間のかかる処理になってしまいます。
(例: オン・オフが4つ、2 ^ 4 = 16 種類のバリアント)シェーダーが初めて表示されるときにメインスレッドでコンパイルが走るわけですが、シェーダーバリアントが多い場合は、特にモバイル環境でコンパイルに時間がかかり、それが終わるまでアニメーション・パーティクル・UIの入力ほか全てが停まることになります。
そのため、
shader_feature
との使い分けが必要になります。参考:
TEXCOORD が不足した場合
UV は基本的には最大 4 枚(TEXCOORD0~3)で、
TEXCOORD1
はライトマップ用に使われる想定になっています。モデルのインポート設定で Generate Lightmap UVs をオンにした場合、TEXCOORD1
が更新されるという事ですね。この
TEXCOORD
は UV 以外にも Unity 標準のフォグの実装などでも使われていたり、その他様々な用途で消費され、足りなくなってきます。UVは2次元の座標ですが、
TEXCOORD
はfloat4
orhalf4
の4要素を持てるようになっています。なので、UV 1つ目をTEXCOORD#.xy
に、UV 2つ目をTEXCOORD#.zw
に、といったように2つを1つにまとめる事も可能です。
※ VAT シェーダーでもTEXCOORD0
の.zw
を使っています。Unity 上で UV を設定する場合、
Mesh.uv*
を使用すると.zw
の設定が出来ないので、代わりにMesh.GetUVs
&Mesh.SetUVs
を使います。
v2f
など、頂点シェーダーからピクセルシェーダーにデータを送る際には、TEXCOORD
を効率よく利用することを心がけておきたいです。頂点シェーダーの注意点
テクスチャの色情報を取得する
tex2D()
がありますが、これはフラグメントシェーダーの中でしか使えません。バーテックスシェーダーからテクスチャにアクセスしたい場合には、代わりに
tex2Dlod()
を使用します。フラグメントシェーダーの注意点
注意点、では無いですが、
fixed4 frag()
だと HDR レンダリングに対応できるのか、出来ないのか、調べ切れませんでした。溢れた色が思った通りにならなければ、
half4 frag()
やfloat4 frag()
にすると良いかもしれません。おわりに
冒頭で紹介した VAT シェーダーですが、仕事で使う必要が無くなった&他の事で手一杯なので、今後の開発についてはまっっったく予定が立たない状態ではありますが、積んでしまったパーティクル(Shuriken)のメッシュインスタンスへの対応なども、シェーダーの開発に合わせてこちらの備忘録まとめに追加していければ良いなと思っています。
今回は1体のポリゴン数が 4,472、アニメーションが 202 frames @ 30fps のアセットを録画しながら大量に動かす、という結構な重みでのテストでしたが、モバイルでも 100 体程度なら十分動いたのは予想外でした。
ループモーションで動くキャラをつまんで移動することもできるので、リアルタイムストラテジーのように引いた絵に大量のキャラがいるようなゲームであれば、VAT でのアニメーションでも十分なのかもしれませんね。
--
最後までお読み頂きありがとうございます。これは Unity のアドベントカレンダーに向けて書いていたのですが、当初想定した以上に長く&遅くなってしまいました。今後のシェーダー開発の助けになれば幸いです。
それでは以上になります。お疲れさまでした。
おまけ
モバイル端末(Android)でのテスト動画です。
モバイルで VAT シェーダーのテスト
— サトー (@sator_imaging) December 25, 2019
Vertex Animation Texture shader on mobile platform, #unity3d.
202 frames@30fps animation, 4,472 tris each.
Xperia 5 (Snapdragon 855 / Adreno 640)
60fps=180mesh
30fps=460mesh
1,000mesh=20fps pic.twitter.com/k1GpXStzQW