🌈

そろそろShaderをやるパート29 マウスクリックした箇所に波紋を発生させる

2021/03/20に公開

そろそろShaderをやります

そろそろShaderをやります。そろそろShaderをやりたいからです。
パート100までダラダラ頑張ります。10年かかってもいいのでやります。
100記事分くらい学べば私レベルの初心者でもまあまあ理解できるかなと思っています。

という感じでやってます。

※初心者がメモレベルで記録するので
 技術記事としてはお力になれないかもしれません。

下準備

下記参考
そろそろShaderをやるパート1 Unite 2017の動画を見る(基礎知識~フラグメントシェーダーで色を変える)

デモ

クリックした箇所を起点に波紋を発生させています。

Shaderサンプル

まずは過去記事でメモを残した波紋方程式をCustomRenderTextureを利用して発生させるShaderを書き換えます。

Shader "Custom/RippleSimulation"
{
    Properties
    {
        _S2("PhaseVelocity^2", Range(0.0, 0.5)) = 0.2
        _Attenuation("Attenuation", Range(0.0, 1.0)) = 0.999
        _DeltaUV("Delta UV", Range(0.0, 0.5)) = 0.1
        _Displacement("Displacement", Range(1.0, 5.0)) = 3.0
    }

    CGINCLUDE
    #include "UnityCustomRenderTexture.cginc"

    half _S2;
    half _Attenuation;
    float _DeltaUV;
    float _Displacement;
    float _height;
    sampler2D _MainTex;

    //波動方程式を計算するフラグメントシェーダー
    float4 frag(v2f_customrendertexture i) : SV_Target
    {
        float2 uv = i.globalTexcoord;

        // 1pxあたりの単位を計算する
        float du = 1.0 / _CustomRenderTextureWidth;
        float dv = 1.0 / _CustomRenderTextureHeight;
        float2 duv = float2(du, dv) * _DeltaUV; //_DeltaUVは係数 大きくするほど広がりを見せる

        // 現在の位置のテクセルをフェッチ
        float2 c = tex2D(_SelfTexture2D, uv);

        //波動方程式
        //h(t + 1) = 2h + c(h(x + 1) + h(x - 1) + h(y + 1) + h(y - 1) - 4h) - h(t - 1)
        //今回、h(t + 1)は次のフレームでの波の高さを表す
        //R,Gをそれぞれ高さとして使用
        float k = (2.0 * c.r) - c.g; //2h - h(t - 1) を先に計算
        float p = (k + _S2 * ( //_S2は係数 位相の変化する速度
                tex2D(_SelfTexture2D, uv + duv.x).r +
                tex2D(_SelfTexture2D, uv - duv.x).r +
                tex2D(_SelfTexture2D, uv + duv.y).r +
                tex2D(_SelfTexture2D, uv - duv.y).r - 4.0 * c.r)
        ) * _Attenuation; //減衰係数

        // 現在の状態をテクスチャのR成分に、ひとつ前の(過去の)状態をG成分に書き込む。
        return float4(p, c.r, 0, 0);
    }

    //クリックしたときに利用されるフラグメントシェーダー
    float4 frag_left_click(v2f_customrendertexture i) : SV_Target
    {
        return float4(_Displacement, 0, 0, 0);
    }
    
    ENDCG

    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        //デフォルトで利用されるPass
        Pass
        {
            Name "Update"
            CGPROGRAM
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag
            ENDCG
        }

        //クリックしたときに利用されるPass
        Pass
        {
            Name "LeftClick"
            CGPROGRAM
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag_left_click
            ENDCG
        }
    }
}

SubShader内にPassを2つ定義しています。
"使用したいフラグメントシェーダーをGPUに教える部分(#pragma fragmentのことです)"がそれぞれのPassの中で異なります。

"クリックした"、"クリックしていない"に応じて、RippleSimulationに定義された2つのPassを外部から切り替えることを想定した記述です。

このShaderは適当なマテリアルに設定したのち、
CustomRenderTextureのMaterialに設定します。

RG成分のバッファを利用しているのでColor Formatはそれぞれ32bit分確保できるR32G32_SFLOATが望ましいです。(たぶん)

【参考リンク】:ゲームを動かす技術と発想 R


次にテッセレーションシェーダーです。
波紋は以前の記事にもまとめた通り、CustomRenderTextureのrだけ利用し、テッセレーションにより分割したポリゴンの高さに反映しています。

【参考リンク】:そろそろShaderをやるパート28 テッセレーションで波紋表現

Shader "Custom/Tessellation"
{

    Properties
    {
        _Color("Color", color) = (1, 1, 1, 0)
        _MainTex("Base (RGB)", 2D) = "white" {}
        _DispTex("Disp Texture", 2D) = "gray" {}
        _MinDist("Min Distance", Range(0.1, 50)) = 10
        _MaxDist("Max Distance", Range(0.1, 50)) = 25
        _TessFactor("Tessellation", Range(1, 50)) = 10 //分割レベル
        _Displacement("Displacement", Range(0, 1.0)) = 0.3 //変位
    }

    SubShader
    {

        Tags
        {
            "RenderType"="Opaque"
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert //vertが頂点シェーダーであることをGPUに伝える
            #pragma fragment frag //fragがフラグメントシェーダーであることをGPUに伝える
            #pragma hull hull //hullがハルシェーダーであることをGPUに伝える
            #pragma domain domain //domainがドメインシェーダーであることをGPUに伝える

            #include "Tessellation.cginc"
            #include "UnityCG.cginc"

            //定数を定義
            #define INPUT_PATCH_SIZE 3
            #define OUTPUT_PATCH_SIZE 3

            float _TessFactor;
            float _Displacement;
            float _MinDist;
            float _MaxDist;
            sampler2D _DispTex;
            sampler2D _MainTex;
            fixed4 _Color;

            //GPUから頂点シェーダーに渡す構造体
            struct appdata
            {
                float3 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            //頂点シェーダーからハルシェーダーに渡す構造体
            struct HsInput
            {
                float4 position : POS;
                float3 normal : NORMAL;
                float2 texCoord : TEXCOORD;
            };

            //ハルシェーダーからテッセレーター経由でドメインシェーダーに渡す構造体
            struct HsControlPointOutput
            {
                float3 position : POS;
                float3 normal : NORMAL;
                float2 texCoord : TEXCOORD;
            };

            //Patch-Constant-Functionからテッセレーター経由でドメインシェーダーに渡す構造体
            struct HsConstantOutput
            {
                float tessFactor[3] : SV_TessFactor;
                float insideTessFactor : SV_InsideTessFactor;
            };

            //ドメインシェーダーからフラグメントシェーダーに渡す構造体
            struct DsOutput
            {
                float4 position : SV_Position;
                float2 texCoord : TEXCOORD0;
            };

            //頂点シェーダー
            HsInput vert(appdata i)
            {
                HsInput o;
                o.position = float4(i.vertex, 1.0);
                o.normal = i.normal;
                o.texCoord = i.texcoord;
                return o;
            }

            //=======================【用語】==================================
            // コントロールポイント:頂点分割で使う制御点
            // パッチ:ポリゴン分割処理を行う際に使用するコントロールポイントの集合
            //================================================================
            
            //ハルシェーダー
            //パッチに対してコントロールポイントを割り当てて出力する
            //コントロールポイントごとに1回実行
            [domain("tri")] //分割に利用する形状を指定 "tri" "quad" "isoline"から選択
            [partitioning("integer")] //分割方法 "integer" "fractional_eve" "fractional_odd" "pow2"から選択
            [outputtopology("triangle_cw")] //出力された頂点が形成するトポロジー(形状) "point" "line" "triangle_cw" "triangle_ccw" から選択
            [patchconstantfunc("hullConst")] //Patch-Constant-Functionの指定
            [outputcontrolpoints(OUTPUT_PATCH_SIZE)] //出力されるコントロールポイントの集合の数
            HsControlPointOutput hull(InputPatch<HsInput, INPUT_PATCH_SIZE> i, uint id : SV_OutputControlPointID)
            {
                HsControlPointOutput o = (HsControlPointOutput)0;
                //頂点シェーダーに対してコントロールポイントを割り当て
                o.position = i[id].position.xyz;
                o.normal = i[id].normal;
                o.texCoord = i[id].texCoord;
                return o;
            }

            //Patch-Constant-Function
            //どの程度頂点を分割するかを決める係数を詰め込んでテッセレーターに渡す
            //パッチごとに一回実行される
            HsConstantOutput hullConst(InputPatch<HsInput, INPUT_PATCH_SIZE> i)
            {
                HsConstantOutput o = (HsConstantOutput)0;

                float4 p0 = i[0].position;
                float4 p1 = i[1].position;
                float4 p2 = i[2].position;
                //頂点からカメラまでの距離を計算しテッセレーション係数を距離に応じて計算しなおす LOD的な?
                float4 tessFactor = UnityDistanceBasedTess(p0, p1, p2, _MinDist, _MaxDist, _TessFactor);

                o.tessFactor[0] = tessFactor.x;
                o.tessFactor[1] = tessFactor.y;
                o.tessFactor[2] = tessFactor.z;
                o.insideTessFactor = tessFactor.w;

                return o;
            }

            //ドメインシェーダー
            //テッセレーターから出てきた分割位置で頂点を計算し出力するのが仕事
            [domain("tri")] //分割に利用する形状を指定 "tri" "quad" "isoline"から選択
            DsOutput domain(
                HsConstantOutput hsConst,
                const OutputPatch<HsControlPointOutput, INPUT_PATCH_SIZE> i,
                float3 bary : SV_DomainLocation)
            {
                DsOutput o = (DsOutput)0;

                //新しく出力する各頂点の座標を計算
                float3 f3Position =
                    bary.x * i[0].position +
                    bary.y * i[1].position +
                    bary.z * i[2].position;

                //新しく出力する各頂点の法線を計算
                float3 f3Normal = normalize(
                    bary.x * i[0].normal +
                    bary.y * i[1].normal +
                    bary.z * i[2].normal);

                //新しく出力する各頂点のUV座標を計算
                o.texCoord =
                    bary.x * i[0].texCoord +
                    bary.y * i[1].texCoord +
                    bary.z * i[2].texCoord;

                //tex2Dlodはフラグメントシェーダー以外の箇所でもテクスチャをサンプリングできる関数
                //ここでrだけ利用することで波紋の高さに応じて頂点の変位を操作できる!すごい!
                float disp = tex2Dlod(_DispTex, float4(o.texCoord, 0, 0)).r * _Displacement;
                f3Position.xyz += f3Normal * disp;

                o.position = UnityObjectToClipPos(float4(f3Position.xyz, 1.0));

                return o;
            }

            //フラグメントシェーダー
            fixed4 frag(DsOutput i) : SV_Target
            {
                return tex2D(_MainTex, i.texCoord) * _Color;
            }
            ENDCG
        }
    }

    Fallback "Unlit/Texture"

}

CustomRenderTextureをInspectorから"Disp Texture"に設定すれば準備完了です。

C#スクリプト

先ほど波動方程式の箇所で述べた下記文言の切り替えを担うのは
C#スクリプトです。

"クリックした"、"クリックしていない"に応じて、RippleSimulationに定義された2つのPassを外部から切り替えることを想定した記述です。

using UnityEngine;

/// <summary>
/// クリックした箇所に波紋を発生させる
/// </summary>
public class ClickRipple : MonoBehaviour
{
    [SerializeField] private CustomRenderTexture _customRenderTexture;
    [SerializeField, Range(0.01f, 0.05f)] private float _ripppleSize = 0.01f;
    [SerializeField] private int iterationPerFrame = 5;
    
    private CustomRenderTextureUpdateZone _defaultZone;

    private void Start()
    {
        //初期化
        _customRenderTexture.Initialize();

        //波動方程式のシミュレート用のUpdateZone
        //全体の更新用
        _defaultZone = new CustomRenderTextureUpdateZone
        {
            needSwap = true,
            passIndex = 0,
            rotation = 0f,
            updateZoneCenter = new Vector2(0.5f, 0.5f),
            updateZoneSize = new Vector2(1f, 1f)
        };
    }

    private void Update()
    {
        //クリック時のUpdateZoneがクリック後も適応された状態にならないように一度消去する
        _customRenderTexture.ClearUpdateZones();
        UpdateZonesClickArea();
        //更新したいフレーム数を指定して更新
        _customRenderTexture.Update(iterationPerFrame);
    }

    /// <summary>
    /// クリックした箇所を起点に特定の領域のみ指定したパスでシミュレートさせる
    /// </summary>
    private void UpdateZonesClickArea()
    {
        bool leftClick = Input.GetMouseButton(0);
        if (!leftClick) return;

        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out var hit))
        {
            //クリック時に使用するUpdateZone
            //クリックした箇所を更新の原点とする
            //使用するパスもクリック用に変更
            var clickZone = new CustomRenderTextureUpdateZone
            {
                needSwap = true,
                passIndex = 1,
                rotation = 0f,
                updateZoneCenter = new Vector2(hit.textureCoord.x, 1f - hit.textureCoord.y),
                updateZoneSize = new Vector2(_ripppleSize, _ripppleSize)
            };

            _customRenderTexture.SetUpdateZones(new CustomRenderTextureUpdateZone[] {_defaultZone, clickZone});
        }
    }
}

CustomRenderTextureに実装されているUpdateZoneを利用することで
任意の箇所のみ指定したPassでシミュレートするという魔法が使えるそうです。

ただし、指定した箇所以外のシミュレートが停止するとのことなので、
全体のシミュレート(_defaultZoneの箇所)も同時に行っています。

参考リンク

Unity 2017.1 の機能の CustomRenderTexture を使ってみた

Discussion