凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

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

はじめに

keijiro さんの以下のつぶやきを見て Unity 2017.1 から CustomRenderTexture という新しいテクスチャが導入されたことを知りました。

従来でも Graphics.Blit() などで似たようなことが出来たと思いますが、これを使うと最低限のスクリプト、またはスクリプト無しで簡単にプロシージャルテクスチャを作ることが出来ます。

デモ

f:id:hecomi:20170516015118g:plain

波動方程式をシミュレーションするシェーダと、シミュレーション結果をテッセレーションした Plane の頂点をいじりつつアルファで適当に抜いたサンプルです。

ダウンロード

github.com

解説

CustomRenderTextureRenderTexture の拡張で、シェーダで直接画像を書き込んだりシミュレーションしたりできるようになるものです。シミュレーションした結果の RenderTexture は通常のテクスチャとしてマテリアルに与えることが出来ます。ドキュメントの例を借りると、水のコースティクスリップル、雨のエフェクトや、壁に当たった液体、みたいなシミュレーションに向いています。

docs.unity3d.com

最初の一回だけ更新や毎フレーム更新、部分的な更新や、マルチパスによる更新もできます。

コード / 設定

シミュレーション用のシェーダ

波動方程式を解くシェーダを書きます。二次元波動方程式は以下のエントリを参考に 2 フレーム前までの位置を残しておく方法で解きました。

qiita.com

Shader "Water/Simulation"
{

Properties
{
    _S2("PhaseVelocity^2", Range(0.0, 0.5)) = 0.2
    [PowerSlider(0.01)]
    _Atten("Attenuation", Range(0.0, 1.0)) = 0.999
    _DeltaUV("Delta UV", Float) = 3
}

CGINCLUDE

#include "UnityCustomRenderTexture.cginc"

half _S2;
half _Atten;
float _DeltaUV;

float4 frag(v2f_customrendertexture i) : SV_Target
{
    float2 uv = i.globalTexcoord;

    float du = 1.0 / _CustomRenderTextureWidth;
    float dv = 1.0 / _CustomRenderTextureHeight;
    float3 duv = float3(du, dv, 0) * _DeltaUV;

    float2 c = tex2D(_SelfTexture2D, uv);
    float p = (2 * c.r - c.g + _S2 * (
        tex2D(_SelfTexture2D, uv - duv.zy).r +
        tex2D(_SelfTexture2D, uv + duv.zy).r +
        tex2D(_SelfTexture2D, uv - duv.xz).r +
        tex2D(_SelfTexture2D, uv + duv.xz).r - 4 * c.r)) * _Atten;

    return float4(p, c.r, 0, 0);
}

ENDCG

SubShader
{
    Cull Off ZWrite Off ZTest Always
    Pass
    {
        Name "Update"
        CGPROGRAM
        #pragma vertex CustomRenderTextureVertexShader
        #pragma fragment frag
        ENDCG
    }
}

}

CustomTexture の作成

Create > CustomTexture でシミュレーション用のテクスチャを生成できます。ここに上記シェーダを適用したマテリアルをセットし、以下のように設定します。

f:id:hecomi:20170516235950p:plain

RGFloat なテクスチャを用意して、R チャネルに 1 フレーム前、G チャネルに 2 フレーム前のシミュレーション結果の画像を格納することにします。マテリアルをセットすると使用できる Pass の名前(ここでは Update)が見えるので、Multi-Pass の場合はここで対象の Pass を選択します。自身のシミュレーション結果である _SelfTexture2D を参照するためには Double Buffered にチェックを入れる必要があります。テクスチャの更新方法は色々と用意されているのですが、毎フレーム 1 回ずつであれば上記のように Initialization ModeOnLoadUpdate ModeRealtime とすることでスクリプトなしに更新することが出来ます。なお、初期の水の位置を与えるために、Texture に以下のような画像を与えています。f:id:hecomi:20170517001813p:plain

結果

このシミュレーション結果のテクスチャを見てみるとこんな感じになっています。

f:id:hecomi:20170517002751g:plain

頂点移動

このままだと微妙なので、テッセレーションしてディスプレースメントマップのように使って頂点を移動させてみます。テッセレーション部は以下のエントリをご参照ください。

tips.hecomi.com

Shader "Water/Surface" 
{

Properties
{
    _Color("Color", color) = (1, 1, 1, 0)
    _DispTex("Disp Texture", 2D) = "gray" {}
    _Glossiness ("Smoothness", Range(0,1)) = 0.5
    _Metallic ("Metallic", Range(0,1)) = 0.0
    _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 { "Queue" = "Transparent" "RenderType" = "Transparent" }

CGPROGRAM

#pragma surface surf Standard alpha addshadow fullforwardshadows vertex:disp tessellate:tessDistance
#pragma target 5.0
#include "Tessellation.cginc"

float _TessFactor;
float _Displacement;
float _MinDist;
float _MaxDist;
sampler2D _DispTex;
float4 _DispTex_TexelSize;
fixed4 _Color;
half _Glossiness;
half _Metallic;

struct appdata 
{
    float4 vertex   : POSITION;
    float4 tangent  : TANGENT;
    float3 normal   : NORMAL;
    float2 texcoord : TEXCOORD0;
};

struct Input 
{
    float2 uv_DispTex;
};

float4 tessDistance(appdata v0, appdata v1, appdata v2) 
{
    return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, _MinDist, _MaxDist, _TessFactor);
}

void disp(inout appdata v)
{
    float d = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r * _Displacement;
    v.vertex.xyz += v.normal * d;
}

void surf(Input IN, inout SurfaceOutputStandard o) 
{
    o.Albedo = _Color.rgb;
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = _Color.a * (0.5 + 0.5 * clamp(tex2D(_DispTex, IN.uv_DispTex).r, 0, 1));

    float3 duv = float3(_DispTex_TexelSize.xy, 0) * 10;
    half v1 = tex2D(_DispTex, IN.uv_DispTex - duv.xz).y;
    half v2 = tex2D(_DispTex, IN.uv_DispTex + duv.xz).y;
    half v3 = tex2D(_DispTex, IN.uv_DispTex - duv.zy).y;
    half v4 = tex2D(_DispTex, IN.uv_DispTex + duv.zy).y;
    o.Normal = normalize(float3(v1 - v2, v3 - v4, 0.3));
}

ENDCG

}

FallBack "Diffuse"

}

f:id:hecomi:20170517004236g:plain

スクリプトによる更新

Initialization ModeUpdate ModeOnDemandにするとスクリプトから更新ができるようになります。これにより必要な時に初期化したり、一度に数フレームシミュレーションするといったことができます。

using UnityEngine;

public class WaterSimulation : MonoBehaviour
{
    [SerializeField]
    CustomRenderTexture texture;

    void Start()
    {
        texture.Initialize();
    }

    void Update()
    {
        // 一度に 5 フレーム更新
        texture.Update(5);
    }
}

Update Zones を使ったインタラクション

Update Zones を使うと特定の領域のみ指定したパスでシミュレーションを行うことが出来ます。これは CustomTexture の UI からも指定できるのですが、スクリプトからもセットすることが出来ます。

docs.unity3d.com

これを使って、クリックした場所を波立たせるものを作ってみます。左クリックしたら沈んで、右クリックしたら盛り上がるようにします。

シェーダの変更

シミュレーション用のシェーダに次の 2 つのパスを足します。

Shader "Water/Simulation"
{

...

float4 frag_left_click(v2f_customrendertexture i) : SV_Target
{
    return float4(-1, 0, 0, 0);
}

float4 frag_right_click(v2f_customrendertexture i) : SV_Target
{
    return float4(1, 0, 0, 0);
}

...

SubShader
{
    ...

    Pass
    {
        Name "LeftClick"
        CGPROGRAM
        #pragma vertex CustomRenderTextureVertexShader
        #pragma fragment frag_left_click
        ENDCG
    }

    Pass
    {
        Name "LeftClick"
        CGPROGRAM
        #pragma vertex CustomRenderTextureVertexShader
        #pragma fragment frag_right_click
        ENDCG
    }
}

}

1 と -1 を書き込んでいるだけです。

コード

コード側では、マウスクリックがあった時に対象の UV を調べ、それを CustomRenderTextureUpdateZone に教えます。なお、一つでも UpdateZone が設定されると全体の更新がされなくなるので、ここでは defaultZone という全体を更新する領域を用意し、これとクリック位置の領域 2 つのパスを更新するようにセットします。

using UnityEngine;

public class WaterSimulation : MonoBehaviour
{
    ....

    void Update()
    {
        texture.ClearUpdateZones();
        UpdateZones();
        texture.Update(iterationPerFrame);
    }

    void UpdateZones()
    {
        bool leftClick = Input.GetMouseButton(0);
        bool rightClick = Input.GetMouseButton(1);
        if (!leftClick && !rightClick) return;

        RaycastHit hit;
        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out hit)) {
            var defaultZone = new CustomRenderTextureUpdateZone();
            defaultZone.needSwap = true;
            defaultZone.passIndex = 0; // 波動方程式のシミュレーションのパス
            defaultZone.rotation = 0f;
            defaultZone.updateZoneCenter = new Vector2(0.5f, 0.5f);
            defaultZone.updateZoneSize = new Vector2(1f, 1f);

            var clickZone = new CustomRenderTextureUpdateZone();
            clickZone.needSwap = true;
            clickZone.passIndex = leftClick ? 1 : 2; // 1 または -1 にバッファを塗るパス
            clickZone.rotation = 0f;
            clickZone.updateZoneCenter = new Vector2(hit.textureCoord.x, 1f - hit.textureCoord.y);
            clickZone.updateZoneSize = new Vector2(0.01f, 0.01f);

            texture.SetUpdateZones(new CustomRenderTextureUpdateZone[] { defaultZone, clickZone });
        }
    }
}

結果

水面に落ちる雨とかやると面白そうですね。

おわりに

気軽にプロシージャルテクスチャが作れるようになってとても便利ですね。他にも色々なシミュレーションを作れると思いますので、作ったら是非教えてください。