始めに
つい最近、部活のブログでComputeShaderを使用したGray-Scottモデルについて紹介しました。
このブログを書きながら、「これってCustomRenderTextureでも実装できるんじゃね?」と思ったのでやってみようと思います。
今回のプロジェクトはこちらになります。
CustomRenderTextureについて
カスタムレンダーテクスチャはレンダーテクスチャの拡張機能で、これを使うと簡単にシェーダー付きのテクスチャを作成できます。これは、コースティクス、雨の効果に使われるリップルシミュレーション、壁面へぶちまけられた液体など、あらゆる種類の複雑なシミュレーションを実装するのに便利です。また、カスタムレンダーテクスチャはスクリプトやシェーダーのフレームワークを提供し、部分的更新、または、マルチパスの更新、更新頻度の変更などのさらに複雑な設定をサポートします。
Unity公式から引用しました。( カスタムレンダーテクスチャ - Unity マニュアル )
私なりに要約すると、「機能を盛り込んだシェーダーをテクスチャに付加して動きを付けることが出来る」ということだと思います。
実装
では早速スクリプトを紹介したいと思います。
UpdateGrayScott.shader
Shader "Unlit/UpdateGrayScott"
{
Properties
{
_F("F", Float) = 0.04
_K("K", Float) = 0.06
_SimulateSpeed("SimulateSpeed", Float) = 1
_GridSize("Delta UV", Float) = 1
_Du("Du", Float) = 0.2
_Dv("Dv", Float) = 0.1
}
SubShader
{
Cull Off
ZWrite Off
ZTest Always
Pass
{
Name "Update"
CGPROGRAM
#pragma vertex CustomRenderTextureVertexShader
#pragma fragment frag
#include "UnityCustomRenderTexture.cginc"
float _F;
float _K;
float _SimulateSpeed;
float _GridSize;
float _Du;
float _Dv;
fixed4 frag (v2f_customrendertexture i) : SV_Target
{
float2 uv = i.globalTexcoord;
// 1pxあたりの単位を計算
float du = 1.0 / _CustomRenderTextureWidth;
float dv = 1.0 / _CustomRenderTextureHeight;
float3 duv = float3(du, dv, 0) * _GridSize;
// 現在のテクスチャの取得
float2 c = tex2D(_SelfTexture2D, uv);
float u = c.x;
float v = c.y;
// ラプラスの演算を行う
float2 laplacian =
tex2D(_SelfTexture2D, uv - duv.zy).xy +
tex2D(_SelfTexture2D, uv + duv.zy).xy +
tex2D(_SelfTexture2D, uv - duv.xz).xy +
tex2D(_SelfTexture2D, uv + duv.xz) - 4.0*float2(u, v);
float uvv = u*v*v;
// Gray-Scottモデルの反応拡散方程式
float dudt = _Du * laplacian.x - uvv + _F * (1.0 - u);
float dvdt = _Dv * laplacian.y + uvv - (_F + _K) * v;
return float4(saturate(u + dudt * _SimulateSpeed), saturate(v + dvdt * _SimulateSpeed), 0, 0);
}
ENDCG
}
}
}
InitializeGrayScott
Shader "Unlit/InitializeGrayScott"
{
Properties
{
_MainTex ("NoiseTexture", 2D) = "white" {}
_QuadWidth("QuadWidth", Range(0, 0.5)) = 0.2
}
SubShader
{
Cull Off ZWrite Off ZTest Always
Pass
{
Name "Initialize"
CGPROGRAM
#pragma vertex InitCustomRenderTextureVertexShader
#pragma fragment frag
#include "UnityCustomRenderTexture.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _QuadWidth;
fixed4 frag (v2f_init_customrendertexture i) : SV_Target
{
float2 uv = i.texcoord;
// ノイズテクスチャを取得
fixed4 col = tex2D(_MainTex, uv) * 0.1;
// テクスチャの中央の閾値を求める
fixed minThreadhold = 0.5 - _QuadWidth;
fixed maxThreadhold = 0.5 + _QuadWidth;
if(minThreadhold < uv.x && uv.x < maxThreadhold &&
minThreadhold < uv.y && uv.y < maxThreadhold)
col.xy += float2(0.5, 0.25);
else
col.xy += float2(1, 0);
return col;
}
ENDCG
}
}
}
RenderTextureUpdater
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RenderTextureUpdater : MonoBehaviour
{
[SerializeField]
private CustomRenderTexture m_texture;
[SerializeField]
private int m_updateStep = 1;
void Start()
{
m_texture.Initialize();
}
void Update()
{
m_texture.Update(m_updateStep);
}
}
解説
主にCustomRenderTextureについて解説します。
また、ここではGray-Scottについては解説しません。もし興味がある方は冒頭でも紹介した記事を参照してください。
CustomRenderTextureの作成
まずはCustomRenderTextureを作成します。
作成したCustomRenderTextureの設定は次のようになります。 Color Formatを"R16G16_UNORM"、DoubleBufferedにチェックを入れてください
Gray-Scottのシミュレーションでは2つの成分しか使用しないので2チャンネルのカラーフォーマットを使用します。
DoubleBufferedをオンにすることで、値を更新する際にテクスチャにアクセスすることが可能になります。
初期化処理
こちらも冒頭記事の内容と同様に以下の処理を行っております。
- テクスチャの中央に指定された幅で(u, v) = (0.5, 0.25)、それ以外は(u, v) = (1, 0)で初期化
- ランダム性を持たせるために、ノイズテクスチャを加算
// InitializeGrayScott.shaderの25行目から fixed4 frag (v2f_init_customrendertexture i) : SV_Target { float2 uv = i.texcoord; // ノイズテクスチャを取得 fixed4 col = tex2D(_MainTex, uv) * 0.1; // テクスチャの中央の閾値を求める fixed minThreadhold = 0.5 - _QuadWidth; fixed maxThreadhold = 0.5 + _QuadWidth; if(minThreadhold < uv.x && uv.x < maxThreadhold && minThreadhold < uv.y && uv.y < maxThreadhold) col.xy += float2(0.5, 0.25); else col.xy += float2(1, 0); return col; }
結果は以下のようになります。
更新処理
こちらはそのまま数式を実装した形になります。ほぼほぼComputeShaderの実装と同じです。
// UpdateGrayScott.shaderの40行目から fixed4 frag (v2f_customrendertexture i) : SV_Target { float2 uv = i.globalTexcoord; // 1pxあたりの単位を計算 float du = 1.0 / _CustomRenderTextureWidth; float dv = 1.0 / _CustomRenderTextureHeight; float3 duv = float3(du, dv, 0) * _GridSize; // 現在のテクスチャの取得 float2 c = tex2D(_SelfTexture2D, uv); float u = c.x; float v = c.y; // ラプラスの演算を行う float2 laplacian = tex2D(_SelfTexture2D, uv - duv.zy).xy + tex2D(_SelfTexture2D, uv + duv.zy).xy + tex2D(_SelfTexture2D, uv - duv.xz).xy + tex2D(_SelfTexture2D, uv + duv.xz) - 4.0*float2(u, v); float uvv = u*v*v; // Gray-Scottモデルの反応拡散方程式 float dudt = _Du * laplacian.x - uvv + _F * (1.0 - u); float dvdt = _Dv * laplacian.y + uvv - (_F + _K) * v; // return float4(saturate(u + dudt * _SimulateSpeed), saturate(v + dvdt * _SimulateSpeed), 0, 0); return float4(c, 0, 0); }
比較用としてComputeShaderでの実装も載せておきます。
GrayScottCalculator.compute
// GrayScottCalculator.computeの38行目から
void UpdateGrayScotte(uint3 id : SV_DispatchThreadID) {
float u = Texture[id.xy].x;
float v = Texture[id.xy].y;
float2 laplacian =
Texture[id.xy + uint2(-1, 0)] +
Texture[id.xy + uint2(1, 0)] +
Texture[id.xy + uint2(0, -1)] +
Texture[id.xy + uint2(0, 1)] - 4 * Texture[id.xy];
laplacian /= (dx*dx);
float dudt = Du * laplacian.x - u * v * v + f * (1.0 - u);
float dvdt = Dv * laplacian.y + u * v * v - (f + k) * v;
Texture[id.xy] = float2(u + dt * dudt, v + dt * dvdt);
}
C#プログラムから初期化、更新処理を呼び出す
CustomRenderTextureはC#からパス処理を呼び出すことが出来ます。 また、更新処理は指定された回数分行われます。
// RenderTextureUpdater.csの13行目から void Start() { m_texture.Initialize(); } void Update() { m_texture.Update(m_updateStep); }
マテリアルをCustomRenderTextureに設定する
2つシェーダーからマテリアルを作成して、CustomRenderTextureに設定してください。また、この時Initialize ModeとUpdate ModeはOnLoadに設定しましょう。
結果
ComputeShaderの時と似た形を生成することが出来ました。
あとは同じようにパラメータを変更したり、マテリアルを変更することで以下のような模様も作成ることが出来ます。
感想
前に波動方程式の実装を調べた際にComputeShaderとCustomRenderTextureの2種類の実装を見つけました。今回はそれと同じようにGrayScottを2種類の実装を提示できました。
正直どっちがいいのかと言われればケースバイケースだと思うので好きな方で実装するのがいいと思います。
次は、Gray-Scottで簡単に遊んだ記事でも書こうかと思います。
追記
遊んでみた記事書きました。