20191230のUnityに関する記事は8件です。

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.jslib
mergeInto(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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】DOTS Visual Scriptingを試してみた

今年のUniteの「Unity開発ロードマップ最新情報」にて、DOTSベースのビジュアルスクリプティングツールが開発中であると言う旨が話されました。

unite.png
※上記講演のスライドより引用

このツール自体は引用しているスライド中にある通り、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と省略)

インストール & バージョン情報

インストール方法については↑のフォーラムリンクを参照。
ちなみに6thからはmanifest.jsonに指定のパッケージを追記する形で取得できます。
(※Editor上のPackageManagerからの取得はまだ出来ない)

学び始めるには

具体的な解説に入っていく前にオススメのチュートリアル動画を紹介します。
こちらの方も合わせて見ると理解が深まるかもしれません。

DOTS VSを学び始めるにあたっては以下の動画がおすすめです。
Unity公式のSpace Shooter tutorial」をベースにDOTS VSで実装を行う内容であり、全5回分の動画があります。

英語動画ではありますが、字幕対応しているので自動翻訳で日本語化することも可能です。
(精度は自動翻訳なのでそこだけはご了承を)

他にも3rd DropにてRoll a Ball Demoのデモが公開されているので、こちらをサンプルとして追ってみるのも有りかもしれません。

機能について

ここからは機能解説に入っていきます。
※ここらを把握済みであり「所感を聞きたい」と言う方は読み飛ばしてもokです。 → 触ってみた所感について

何が出来るのか?

現時点の機能で出来ることとしては、大まかに言うとツールで組んだロジックからECSのコードを生成する事が出来ます。 2, 3
※具体的にどういった物が生成できるのかについては後述

ビジュアルスクリプティングでECSのコードを実装できるということは、非プログラマーの方でもロジックに集中して実装を行いつつもECSベースのパフォーマンスの良いコードを生成できるという利点があるかと思います。

具体的な機能詳細について解説していきます。

ComponentDataの定義

機能の1つとして「ツール上からComponentDataを定義したコードを生成する機能」があります。
ComponentDataの定義はAssetsのメニューから「Create → Visual Script → New Component各種」で生成できます。

componentdata.png

生成すると以下の画面が開かれるので、こちらからComponentDataが持つフィールドなどを設定していきます。

ちなみにメニュー側にもComponentの他にShared ComponentBuffer Elementと言った物がありますが、これらを指定することでそれに応じたComponentDataを定義することも可能です。4

componentdata_.png

生成コードについて

定義が完了したら上記ウィンドウの右下にある【Save】ボタンを押下することでコードが生成されます。

生成されるコードとしては、指定した名称に対しProxyと言うサフィックスが付いたC#コードとなります。
Sampleと言う名称を指定したらSampleProxy.csが生成される。

サフィックスの理由について

明言されていたわけではないので自分の考えとなりますが、現時点でサフィックスが付く理由としては、生成されたコードを見た感じだと「オーサリングコンポーネントとしても機能出来るようにする」と言った意図がありそうです。
※生成されるコードはテキストエディタで直接開くか、上記ウィンドウと一緒に開かれるであろうCode Viewerと言うウィンドウから確認することが可能。

以下に生成したサンプルを引用します。(説明用に幾つかコメントを追記)

SampleProxy.cs(クリックで展開)
SampleProxy.cs
using 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上で機能するようになります。
変換のフローについては上記の生成後のコードを見ると分かりやすいかもです。

componentdata_2.png

ECS Graph(ComponentSystem)の実装

もう一つの機能であるECS Graphは実際にロジックを組む部分であり、最終的には組み込んだロジックを元にComponentSystemのコードが生成されます。

こちらはAssetsのメニューから「Create → Visual Script → ECS Graph」でグラフの生成できます。

graph.png

生成を行うと以下の初期画面(初期テンプレ)が表示されます。

Art002.png

ちなみにこの状態で生成されるコードとしては以下のものとなり、OnUpdate(グラフ上ではOn Update Entities)の中でmyQueryと言うEntityQueryを見て処理を行っているのが見えてきます。

SampleSystem.cs(クリックで展開)
SampleSystem.cs
using 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に変換することで動作確認可能です。

all_graph2_com2.png

各種ウィンドウについて

各種ウィンドウに対しては青い番号で振ってあります。

  • ①: Visual Script
    • メインとなるグラフビュー
  • ②: Blackboard
    • EntityQueryの定義やグラフ内(ComponentSystem内)での変数の定義が可能
  • ③: MiniMap
    • グラフ全体のミニマップを表示
  • ④: Code Viewer
    • 現在のグラフに対応するComponentSystemのコードを表示
  • ⑤: Inspector
    • グラフの詳細の設定
    • 「JobSytstemの有無」や「実行順」に関する設定はこちらから指定可能
      • Use Job System → JobSystemの有効/無効
      • UpdateBefore/UpdateAfter → グラフ間の実行順
        • ※ComponentSystem間の実行順とも言い換えられる
      • Execution Order → グラフ内の実行順
        • On Start EntitiesOn 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の定義
      • ※上の例ではTranslationRotationを持つ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.cs
using 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.cs
using 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]属性を付けたクラス名が項目に追加されているのでそちらから呼び出すことが出来ます。

Art003.png

参考

他にサポートされている機能

他にも以下の機能などがサポートされているみたいです。

Coroutine

面白い機能としては、ECSベースのCoroutineが実装されてました。

一例を挙げると以下のようにOn Update EntitiesにてWaitを呼び出すと、「待機中に呼び出す処理」「完了時に呼び出す処理」を指定することが出来ます。

以下の例では待機中には"Waiting"と言うログを、完了時には"Finish"と言うログを出力します。

coroutine.png

ただ、現状のCoroutine自体はComponentSystem.OnUpdateの中にて自前のステート管理を自動生成して制御しているっぽく、完了時にもう一度Coroutineが回り始めるので考えて制御する必要はありそうです。

参考までに上記グラフの出力コードを引用します。

SampleSystem.cs(クリックで展開)
SampleSystem.cs
using 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】ボタンを押下すると、ボタン群が並んでいるエリアの下にフレーム番号が記載されたエリアが表示されます。

この状態でグラフを実行すると「フレーム毎に処理をトレース」する事ができ、一時停止/実行停止中に以下のように操作してトレース結果を追うことが出来ます。

wait.gif

マクロ

グラフ内で計算式などを組むと、計算式のノード構成を使いまわしたくなるときが出てくるかと思います。この要件はマクロを使うことで対応可能です。

これを使うことで複雑な計算式と言った「使い回せる処理」に関しては、グラフ内で関数化するような感じで定義を纏めることが出来ます。

やり方

サンプルとして「引数から受け取った4つの値を加算して返す」処理をマクロ化してみます。

先ずは予め計算式と組まれている一連のノード郡を選択し、右クリックメニューから「RefactorExtract Macro」を選択します。

Art006.png

そうすると以下のようなマクロノードとマクロ用のECS Graphのアセットが生成されます。
今回は名前をSampleMacroと設定しました。

次にマクロノードをダブルクリックして開きます。

Art004.png

この中ではマクロの実装を定義できます。
以下の様に引数/戻り値を設定して計算式を組み上げます。

hoge.png

計算式を組み上げたら前に開いていたグラフに戻ります。
するとマクロノードが引数/戻り値に応じて入出力が変わっているので、後はこちらを使うだけです。

サンプルでは「1+2+3+4」の結果をログに出力してます。(シンプルすぎた感..)

Art003.png

キー/ボタン入力の取得

グラフ上から直接キー/ボタン入力を取得することが出来ます。

入力を実際に組み込むにあたっては、入力側の抽象化を考慮して「カスタムノードで抽象化して取るか?」「ECS側で入力を取らずにEntityとして入力を渡すか?」など色々と設計を考えられるかと思いますが、簡易的に確認する場合には有用かもしれません。

Art004.png

その他

幾つかの例を挙げましたが、他にも色々とあります。
記事中では全て解説しないので、他の機能についてはフォーラムにある各バージョンの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で実装を行うチュートリアル動画。

Forum

DOTS基礎

Other


  1. 後述しますが、現時点で出来ることとしては「ECSのコード生成」となるので呼び名的には後者が正しい感はある。 

  2. 故に名称的にはPackageManagerの表記である「Visual Scripting ECS」が機能的に正しい感があったりも。 

  3. 逆に言うとECSの知見があって自分でコードを書ける場合にはDOTS VSを使わなくても実装出来ると思われる。 

  4. Eventと言うのもありますが...詳細についてまでは追いきれておらず...パッと見た感じだとIBufferElementDataが生成される上でDotsEventAttributeと言う属性がつくっぽいが...? 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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と呼ぶことにします。

スクリーンショット 2019-12-30 午後0.54.16.png

これら2つのオブジェクトには親子関係があり、ParentCubeChildCubeの親オブジェクトである、とします。
スクリーンショット 2019-12-30 午後0.54.41.png

ここで、これらのオブジェクトをEntity化することを考え、ParentCubeにのみConvertToEntityをAddします。

実行してEntityDebuggerを確認してみると、
スクリーンショット 2019-12-30 午後1.07.36.png
確かにParentCubeChildCubeもEntityに変換されます。

ここからが本題ですが、ここでParentCubeを削除しようと思います。

まず、ParentCubeを識別するためのタグと、その「削除」を行うタイミングを知らせるためのタグを作成します。

ParentCube.cs
using Unity.Entities;

/// <summary>
/// ParentCubeを識別するためのタグ
/// </summary>
public struct ParentCube : IComponentData
{
}
Destroyable.cs
using Unity.Entities;

/// <summary>
/// 削除するタイミングを知らせるためのタグ
/// </summary>
public struct Destroyable : IComponentData
{
}

次のようなスクリプトを作成し、ParentCubeにアタッチします。

ParentCubeAuthoring.cs
using 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.cs
using 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;
    }
}

これで実行してみると、

スクリーンショット 2019-12-30 午後1.46.36.png
スクリーンショット 2019-12-30 午後1.46.48.png

確かにParentCubeは削除されたのですが、ChildCubeは削除されません。

さらに、(今回のケースではあまり関係ないですが)LocalToWorldのデータがLocalToParentのデータに置き換わってしまっているので、予期せぬ場所に子Entityが出現したりして、かなり困ることもあるかもしれません。

解決法

子Entityの参照が、親Entityに付いているChildというBufferElementDataに入っているので、それを使って子Entityを削除します。

DestroyCubesSystemのコードを見る
DestroyCubesSystem.cs
using 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;
    }
}


これで子オブジェクトも削除されるはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでSpriteから当たり判定(Polygon Collider 2D)を自動でつける

はじめに

attack2.gif

この記事では透過されたpng画像をSpriteにし、Polygon Collider 2Dを使って当たり判定を自動でつけるところまでやります

目次

  1. 背景透過画像を用意する
  2. Unityにimportする
  3. 画像をSpriteに変換する
  4. SpriteRendererとPolygonCollider2Dを使う

ターゲット

  • 2Dアクションゲーム作りたいけど、当たり判定の作り方がよくわからない
  • デザイナーじゃないし2DのAnimationが作れないから画像から当たり判定をつけたい

環境

Mac Mojave : 10.14.6
Unity : 2019.2.4f1
Rider : 2019.1
.Net : 4.x

1.背景透過画像を用意する

まず、元となるSprite画像を用意します。
今回はテストで作ったので、fire alpacaというソフトを使って背景透過のpng画像を描きアニメーションさせました。

image.pngimage.pngimage.pngimage.pngimage.pngimage.pngimage.png

今回はこの7枚の背景透過画像を元にします。
このとき注意しないといけないのが、攻撃するときに当たり判定をつけたい画像(武器など)と当たり判定をつけたくない画像(プレイヤー自身)などの出力を分ける必要があるということです

2.Unityにimportする

用意したpngファイルをそのままUnityのAssets内に置く
image.png

Assets/Resources/Sprites/Player/Attack3/Weapon

としました。

このままではUnityで使えないのでSpriteに変換していきます。

3.画像をSpriteに変換する

まず、importした画像を全部選択します
image.png

次に、Inspectorから、Texture TypeがDefaultになっているのをSprite(2D and UI)にします

image.png

image.png

Inspectorの下の方にApplyというボタンがあるので、そこをクリックします
image.png

すると、pngの画像をSpriteとして扱えるようになりました
image.png

4.SpriteRendererとPolygonCollider2Dを使う

ここまでで、透過画像のimportおよびSpriteへの変換が行えました。

最後に、自動でColliderを生成してくれるPolygonCollider2Dを使います。

image.png

  1. SpriteRendererをAddComponentする
  2. SpriteRendererのSpriteに自動でColliderをつけたい画像を設定する
  3. PolygonCollider2DをAddComponentする
  4. 自動で生成完了

流れはこれだけなのですが、順番が問題でこの順番じゃないと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の存在を知らなかったた

もっとこうした方がいいよとかここ間違ってる等ありましたらご指摘ください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MixamoをUnityで使おうぜ

Mixamo何するものぞ

Mixamoとは、3Dモデルやアニメーションを無料で商用、非商用問わず利用することができるWebサービスです。
Adobeが運営しているサービスで、利用にはAdobeアカウントが必要です。
Mixamo_top.png
3Dモデルもアニメーションも種類が豊富かつハイクオリティで、画面右側のパネルでアニメーションを微調整することもできます。

非常に便利なサービスなのですが、MixamoのアセットをそのままUnityにインポートするといくつか問題が発生します。この記事では自分が出くわした事案とその解決法をご紹介します。

事案1.テクスチャが貼られていない

今回使おうと思うのはこちらのDouglas君です。
douglas.png

さてDouglas君をUnityにインポートしました。すると...
douglas_white.png
驚きの白さ。キン消しみたいになっちゃってますね。

解決法

テクスチャが貼られていないのはテクスチャが存在していないからです。fbxファイルの中に埋め込んであるのでこれを引き出してあげる必要があります。
extract_texture.png
ImportSettingsのMaterialsタブにあるExtract Texturesボタンを押してください。テクスチャの展開先を適当に選んでやればモデルの中に埋め込まれていたテクスチャが生成されます。なおMaterialへの適用は自動で行われます。便利!
douglasWithTexture.png
...狂ってますね。事案2です。

事案2.透けている

お腹が出ちゃってますね。物理的に。袖も若干突き破ってますし眼球もシンプソンズみたいになっています。

解決法

これはMaterialのRendering ModeがTransparentになっているのが原因です。
material_setting.png
ですがこちらのMaterialもfbxの中に埋め込まれているため設定を変更することができません。テクスチャと同様にfbxから引き出してあげましょう。
extract_mateerial.png
ImportSettingsのMaterialsタブにあるExtract Materialsボタンを押してください。
Materialが個別に生成されると思うのでいい感じに設定を変更してあげましょう。
douglas_color.png
シンプソンズみたいだったDouglas君がウィル・スミスみたいになりました。地球の1つや2つは救ってそうな面構えです。

事案3.足首が折れている

3Dモデルは問題なくインポートすることができました。このモデルをゲームキャラクターとして使用するにはアニメーションが必要です。
douglas_run.gif
今回はこちらの走るアニメーションを使おうと思います。

ドラッグ&ドロップでインポートし、Animation TypeをHumanoidに変更します。
そのプレビューがこちらです。
unityModel_run.gif
左足首にご注目下さい。折れてます。Mixamoのアニメーションアセットに含まれているAvatarはUnityのMecanimに上手く対応できないようですね。

解決法

アニメーションのfbxのImport SettingsのRigタブのAvatar DefinitionをCopy From Other Avatarに変更し、Sourceに適当なHumanoidのAvatarを入れましょう。
running_setting.png
今回はDouglasのAvatarを入れています。事前にDouglasのAvatarをDouglasのImport SettingのRigタブからHumanoidに変更しておくようにしましょう。
変更した結果がこちらです。
unityModel_run_proper.gif
足首が治っていることはもちろん上半身も自然な感じになっています。

まとめ

Mixamoは便利なサービスですがUnityで使うにはひと手間必要です。そこら辺を勝手にやってくれるAssetPostProcessorを作りました。
https://github.com/Dumble009/MixAmoAssetImporter
こちらを使えば今回の記事で取り上げたような操作を自動で行ってくれます。お試しください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity のシェーダー開発方法のまとめと備忘録・頂点アニメーションテクスチャ(VAT)シェーダー

Unity のみで完結できる頂点アニメーションテクスチャー(VAT・Vertex Animation Texture)のエクスポーターとシェーダーを作りました。

--

こちらの記事はとりあえず動くシェーダーは作れる、だけど… という内容です。

拾い物やネットからのコピペ、アセットストア経由など、プロジェクトは雑多なアセットのチャンプルーが常で、たまーにシェーダーを作ったり、という方や、プロジェクトの規模に合うであろう内容になっています。

もくじ

はじめに

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.cgincShadeSH9 を使用します。このマクロは Pass のタグ設定で LightModeForwardBase に設定した状態で使用する必要があります。

#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.worldPoshalf3 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 等に名前を書き換えて自作シェーダーを実装します。
UnityMetaInputUnityMetaFragment を通して返します。
_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つで、以下の通り定義されています。

SpecularStrengthspecColor.rgb の3つのうち最大の値を返します。
UnityStandardCore.cgincUNITY_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 パスを自身で実装する方法は、こちらが非常に参考になります。

VertexInputappdata に、VertexOutputv2f にすると見慣れたものになります。

ステレオレンダリングへの対応

マルチパスのステレオレンダリング(単に複数のカメラで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 のテンプレートで既に対応がなされていますが、

  1. #pragma multi_compile_fog を追加
  2. struct v2fUNITY_FOG_COORDS(1);(1 は TEXCOORD# の # 部分)
  3. vert()UNITY_TRANSFER_FOG(o, o.vertex);
  4. frag()UNITY_APPLY_FOG(i.fogCoord, apply_to_this_fixed4_color);

で対応させることができます。こちらの Unity Answers の記事が詳しいです。

フォグ対応の注意点

フォグのマクロは LightModeForwardAdd に設定されている場合は、なにもしてくれません。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;
#endif
v2f 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"
}

構造体やシェーダープログラムのおススメと注意点

印象ベースですが、各要素の名前は下記のものがおススメです。

特に、v2fpos は、テンプレとして使う事も多い Unlit Shader NewUnlitShader.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) = 0

Enum がある場合はプロパティーに設定しておく

大抵のものは UnityEngine.Rendering に Enum が定義されているので、一覧から探して忘れずに設定しておきます。

プロパティーにシェーダーからアクセスする

シェーダーからプロパティーの値を参照するには、CGINCLUDECGPROGRAM 内で変数を定義します。詳細はこちらです。

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次元の座標ですが、TEXCOORDfloat4 or half4 の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)でのテスト動画です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

頂点アニメーションテクスチャ(VAT)& Unity シェーダーの開発まとめ

Unity のみで完結できる頂点アニメーションテクスチャー(VAT・Vertex Animation Texture)のエクスポーターとシェーダーを作りました。

--

こちらの記事はとりあえず動くシェーダーは作れる、だけど… という内容です。

拾い物やネットからのコピペ、アセットストア経由など、プロジェクトは雑多なアセットのチャンプルーが常で、たまーにシェーダーを作ったり、という方や、プロジェクトの規模に合うであろう内容になっています。

もくじ

はじめに

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.cgincShadeSH9 を使用します。このマクロは Pass のタグ設定で LightModeForwardBase に設定した状態で使用する必要があります。

#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.worldPoshalf3 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 等に名前を書き換えて自作シェーダーを実装します。
UnityMetaInputUnityMetaFragment を通して返します。
_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つで、以下の通り定義されています。

SpecularStrengthspecColor.rgb の3つのうち最大の値を返します。
UnityStandardCore.cgincUNITY_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 パスを自身で実装する方法は、こちらが非常に参考になります。

VertexInputappdata に、VertexOutputv2f にすると見慣れたものになります。

ステレオレンダリングへの対応

マルチパスのステレオレンダリング(単に複数のカメラで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 のテンプレートで既に対応がなされていますが、

  1. #pragma multi_compile_fog を追加
  2. struct v2fUNITY_FOG_COORDS(1);(1 は TEXCOORD# の # 部分)
  3. vert()UNITY_TRANSFER_FOG(o, o.vertex);
  4. frag()UNITY_APPLY_FOG(i.fogCoord, apply_to_this_fixed4_color);

で対応させることができます。こちらの Unity Answers の記事が詳しいです。

フォグ対応の注意点

フォグのマクロは LightModeForwardAdd に設定されている場合は、なにもしてくれません。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;
#endif
v2f 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"
}

構造体やシェーダープログラムのおススメと注意点

印象ベースですが、各要素の名前は下記のものがおススメです。

特に、v2fpos は、テンプレとして使う事も多い Unlit Shader NewUnlitShader.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) = 0

Enum がある場合はプロパティーに設定しておく

大抵のものは UnityEngine.Rendering に Enum が定義されているので、一覧から探して忘れずに設定しておきます。

プロパティーにシェーダーからアクセスする

シェーダーからプロパティーの値を参照するには、CGINCLUDECGPROGRAM 内で変数を定義します。詳細はこちらです。

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次元の座標ですが、TEXCOORDfloat4 or half4 の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)でのテスト動画です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む