🔄

VRChatのシェーダーで状態を持つ

に公開

TL;DR

https://phi16.hatenablog.com/entry/2023/09/25/235421
を参考にやってみた記事なので特に新規性はありません。
ただ、Unity初心者(≠プログラミング初心者)でも分かりやすいかなと思います。
完成するものは以下のようになります。
https://x.com/Toru31415926535/status/1941510078705828296

準備

ワールド自体の作り方はここでは説明しないので以下などを参考にして下さい。
https://note.com/shimenin/n/na902379cf717
また、普通のカスタムシェーダーの作り方もここでは説明しないので以下などを参考にして下さい。
https://docs.unity3d.com/ja/2022.3/Manual/SL-VertexFragmentShaderExamples.html

使用しているツール

  • VRChat Creator Companion: 2.4.2
  • Unity: 2022.3.22f1
  • VRChat SDK: 3.8.2

本題

必要なオブジェクトやファイルの作成等

  1. ProjectタブのAssetsフォルダーにComputeフォルダーを作る
    • 右クリック->Create->Folder
    • 必須ではないがいくつかファイルを作るのでまとめるのに作っておくのを推奨
  2. HierarchyにVisualizerという名前のQuadを追加
    • 右クリック->3D Object->Quad
    • 今回はここに出力結果を描画する
  3. ProjectタブのAssets/ComputeにVisualizerという名前のMaterialを追加
    • 右クリック->Create->Material
  4. ProjectタブのAssets/ComputeにVisualizerという名前のShaderを追加
    • 右クリック->Create->Shader->Unlit Shader
    • このShaderが出力結果を描画する
  5. Visualizer(Material)のShaderとしてVisualizer(Shader)を登録
    • ProjectタブでShaderをMaterialにドラック&ドロップ
  6. Visualizer(Quad)のMaterialとしてVisualizer(Material)を登録
    • ProjectタブのMaterialをHierarchyタブのQuadにドラック&ドロップ
  7. HierarchyにInternalという名前のQuadを追加
    • デバッグ用などに内部状態をそのまま表示する場所
  8. ProjectタブのAssets/Computeにupdateという名前のMaterialを追加
  9. ProjectタブのAssets/Computeにupdateという名前のShaderを追加
    • このShaderで内部状態の更新を行う
  10. update(Material)のShaderとしてupdate(Shader)を登録
  11. Internal(Quad)のMaterialとしてupdate(Material)を登録
  12. ProjectタブのAssets/ComputeにComputeUpdateという名前のU# Scriptを追加
    • 右クリック->Create->U# Scriptを選ぶとSave UdonSharp Fileというウインドウが開くのでファイル名にComputeUpdateと入力して保存
    • ComputeUpdate.csとComputeUpdate.assetの2つのファイルが出来れば成功
  13. Internal(Quad)のスクリプトとしてComputeUpdate.csを登録
    • ProjectタブのComputeUpdate.csをHierarchyタブのQuadにドラック&ドロップ

ShaderとU# Scriptの編集

それぞれ以下のように編集

長いので折り畳み
ComputeUpdate.cs
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class ComputeUpdate : UdonSharpBehaviour
{
    // 外部マテリアルの参照
    public Material update; 
    public Material visualizer;

    // メンバ変数
    private RenderTexture[] rt;
    private uint output = 0;

    void Start()
    {
        const int N = 256;
        const int EW = 1; // 1要素の幅
        const int EH = 1; // 1要素の高さ
        const int FW = N * EW; // 全体の幅
        const int FH = N * EH; // 全体の高さ
        rt = new RenderTexture[2]; // ダブルバッファリング
        for (uint i = 0; i < 2; i++)
        {
            // それぞれ初期化
            rt[i] = new RenderTexture(FW, FH, 0, RenderTextureFormat.ARGBFloat);
            rt[i].filterMode = FilterMode.Point;
            rt[i].Create();
        }
        update.SetFloat("_Init", 1.0f); // 初期化直後であることを伝える
    }

    void FixedUpdate()
    {
        update.SetTexture("_Store", rt[output]); // 前のフレームの状態を update にコピー
        VRCGraphics.Blit(null, rt[output ^ 1], update); // update のシェーダーを実行して状態を更新
        visualizer.SetTexture("_Store", rt[output ^ 1]); // 更新した状態を visualizer にコピー
        output = output ^ 1;
        update.SetFloat("_Init", 0.0f); // 初期化直後でないことを伝える
    }
}
Visualizer.shader
Shader "Unlit/Visualize"
{
    Properties
    {
        _Store ("Store", 2D) = "red" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

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

            sampler2D _Store;
            float4 _Store_ST;

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

            fixed4 frag (v2f i) : SV_Target
            {
                // 内部状態(_Store)を使って描画
                // ここでは色を反転させてるだけ
                fixed4 col = float4(1, 1, 1, 0) - tex2D(_Store, i.uv);
                return col;
            }
            ENDCG
        }
    }
}
update.shader
Shader "Unlit/update"
{
    Properties
    {
        _Store ("Store", 2D) = "green" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

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

            sampler2D _Store;
            float _Init;

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

            fixed4 frag (v2f i) : SV_Target
            {
                #define N 256
                uint2 iuv = i.uv * N;
                float4 d;
                if (_Init > 0.5) {
                    // 状態の初期化
                    float t0 = 2.0f * i.uv.x - 1.0f;
                    float t1 = frac(i.uv.x + i.uv.y);
                    d = float4(t0 * t0, i.uv.y, t1, 0);
                } else {
                    // 前の状態を読み込み
                    d = tex2Dlod(_Store, float4((iuv + 0.5) / N, 0, 0));
                    // 新しい状態の生成
                    d = frac(d + float4(1.0, 1.0, 1.0, 0.0) * (1.0 / N));
                }
                return d;
            }
            ENDCG
        }
    }
}

外部参照の解決

HierarchyタブのInternalを選択し、InspectorタブのCompute Update(Script)のUpdateの横が「None (Material)」になっているはずなので、これを「update」に変更する。Visualizerも同様に「None (Material)」から「Visualizer」に変更する。

動作確認

ここまで全部出来ていれば最初に貼った動画のようになるはず。(左がVisualizer, 右がInternal)

Discussion