はじめに
keijiro さんの以下のつぶやきを見て Unity 2017.1 から CustomRenderTexture
という新しいテクスチャが導入されたことを知りました。
Real time reaction-diffusion system with CustomRenderTexture (Unity 2017.01) https://t.co/NvZhkNTS1s pic.twitter.com/O1nJ09eHPu
— Keijiro Takahashi (@_kzr) 2017年5月14日
従来でも Graphics.Blit()
などで似たようなことが出来たと思いますが、これを使うと最低限のスクリプト、またはスクリプト無しで簡単にプロシージャルテクスチャを作ることが出来ます。
デモ
波動方程式をシミュレーションするシェーダと、シミュレーション結果をテッセレーションした Plane の頂点をいじりつつアルファで適当に抜いたサンプルです。
ダウンロード
解説
CustomRenderTexture
は RenderTexture
の拡張で、シェーダで直接画像を書き込んだりシミュレーションしたりできるようになるものです。シミュレーションした結果の RenderTexture
は通常のテクスチャとしてマテリアルに与えることが出来ます。ドキュメントの例を借りると、水のコースティクスやリップル、雨のエフェクトや、壁に当たった液体、みたいなシミュレーションに向いています。
最初の一回だけ更新や毎フレーム更新、部分的な更新や、マルチパスによる更新もできます。
コード / 設定
シミュレーション用のシェーダ
波動方程式を解くシェーダを書きます。二次元波動方程式は以下のエントリを参考に 2 フレーム前までの位置を残しておく方法で解きました。
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 でシミュレーション用のテクスチャを生成できます。ここに上記シェーダを適用したマテリアルをセットし、以下のように設定します。
RGFloat なテクスチャを用意して、R チャネルに 1 フレーム前、G チャネルに 2 フレーム前のシミュレーション結果の画像を格納することにします。マテリアルをセットすると使用できる Pass の名前(ここでは Update)が見えるので、Multi-Pass の場合はここで対象の Pass を選択します。自身のシミュレーション結果である _SelfTexture2D
を参照するためには Double Buffered にチェックを入れる必要があります。テクスチャの更新方法は色々と用意されているのですが、毎フレーム 1 回ずつであれば上記のように Initialization Mode を OnLoad、Update Mode を Realtime とすることでスクリプトなしに更新することが出来ます。なお、初期の水の位置を与えるために、Texture に以下のような画像を与えています。
結果
このシミュレーション結果のテクスチャを見てみるとこんな感じになっています。
頂点移動
このままだと微妙なので、テッセレーションしてディスプレースメントマップのように使って頂点を移動させてみます。テッセレーション部は以下のエントリをご参照ください。
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" }
スクリプトによる更新
Initialization Mode や Update Mode を OnDemandにするとスクリプトから更新ができるようになります。これにより必要な時に初期化したり、一度に数フレームシミュレーションするといったことができます。
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 からも指定できるのですが、スクリプトからもセットすることが出来ます。
これを使って、クリックした場所を波立たせるものを作ってみます。左クリックしたら沈んで、右クリックしたら盛り上がるようにします。
シェーダの変更
シミュレーション用のシェーダに次の 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 }); } } }
結果
水面に落ちる雨とかやると面白そうですね。
おわりに
気軽にプロシージャルテクスチャが作れるようになってとても便利ですね。他にも色々なシミュレーションを作れると思いますので、作ったら是非教えてください。