UnityでCommandBufferを利用して流体のレンダリングを行う
はじめに
前回はパーティクルで見せかけの球体を大量に描画してみました。 今回は前回のプログラムをベースに流体のレンダリングを行ってみます。
Unityのバージョン:2018.3.0b1
今回実装するもの
今回実装するのは次のスライドで紹介されているものです。
次の記事でも紹介されています。
- PS4世代の表現を先取り? PhysXを駆使して超リッチなエフェクトを実現したPC版「Borderlands 2」 - 4Gamer.net
- [SQEXOC 2012]Agni's Philosophyに見られるグラフィックス技術解説(後編)。ボリュームレンダリングやパーティクル処理の最新動向 - 4Gamer.net
球体のパーティクルのdepthに対してバイラテラルフィルタをかけることで 擬似的なメタボールを描画するものです。 バイラテラルフィルタについては過去に記事を書いたことがあります。
パーティクルの準備
シーンにパーティクルシステムを用意します。
パーティクルのLifetimeを10秒にして パーティクルの最大個数を1000個に設定しています。
パーティクルを毎秒200ほど出すようにします。
パーティクルに重力が働くようにします。
パーティクルにノイズを加えます。
物理の設定を行います。
レンダリングの設定は前回と同じです。 ビルボードでShadows Onlyにして影のマテリアルを設定しています。
プログラム
最初にソースコードの全文を最初に載せてしまいます。
スクリプトは次のとおり。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class FluidRendering : MonoBehaviour {
private Mesh quad;
private Mesh picturePlane;
private Material material;
private int gBuffer0ColorPropertyID;
private int gBuffer1ColorPropertyID;
private int gBuffer3ColorPropertyID;
private int frustumCornersID;
private int radiusID;
private int depth0RT;
private int depth1RT;
private Dictionary<Camera, CommandBuffer> dict = new Dictionary<Camera, CommandBuffer> ();
private ParticleSystem.Particle[] particles;
private Matrix4x4[] matrices;
[ColorUsage (showAlpha: false)]
public Color albedo;
[Range (0, 1)]
public float occlusion;
[ColorUsage (showAlpha: false)]
public Color specular;
[Range (0, 1)]
public float smoothness;
[ColorUsage (showAlpha: false, hdr: true)]
public Color emission;
public new ParticleSystem particleSystem;
public float particleSize = 1f;
private void Start () {
quad = new Mesh ();
var quadVertices = new List<Vector3> {
new Vector3 (0.5f, 0.5f, 0),
new Vector3 (-0.5f, 0.5f, 0),
new Vector3 (0.5f, -0.5f, 0),
new Vector3 (-0.5f, -0.5f, 0),
};
var quadTriangles = new List<int> {
1,
0,
2,
1,
2,
3
};
var quadUVs = new List<Vector2> {
new Vector2 (1, 1),
new Vector2 (0, 1),
new Vector2 (1, 0),
new Vector2 (0, 0),
};
quad.SetVertices (quadVertices);
quad.SetTriangles (quadTriangles, 0);
quad.SetUVs (0, quadUVs);
picturePlane = new Mesh ();
var ppVertices = new List<Vector3> {
new Vector3 (1.0f, 1.0f, 0.0f),
new Vector3 (-1.0f, 1.0f, 0.0f),
new Vector3 (-1.0f, -1.0f, 0.0f),
new Vector3 (1.0f, -1.0f, 0.0f),
};
var ppTriangles = new List<int> { 0, 1, 2, 2, 3, 0 };
var ppUVs = new List<Vector2> {
new Vector2 (1f, 0),
new Vector2 (0, 0),
new Vector2 (0, 1f),
new Vector2 (1f, 1f),
};
picturePlane.SetVertices (ppVertices);
picturePlane.SetTriangles (ppTriangles, 0);
picturePlane.SetUVs (0, ppUVs);
material = new Material (Shader.Find ("Fluid/FluidParticle"));
material.enableInstancing = true;
gBuffer0ColorPropertyID = Shader.PropertyToID ("_GBuffer0Color");
gBuffer1ColorPropertyID = Shader.PropertyToID ("_GBuffer1Color");
gBuffer3ColorPropertyID = Shader.PropertyToID ("_GBuffer3Color");
frustumCornersID = Shader.PropertyToID ("_FrustumCorner");
radiusID = Shader.PropertyToID ("_Radius");
depth0RT = Shader.PropertyToID ("_Depth0RT");
depth1RT = Shader.PropertyToID ("_Depth1RT");
particles = new ParticleSystem.Particle[particleSystem.main.maxParticles];
matrices = new Matrix4x4[particleSystem.main.maxParticles];
for (var i = 0; i < particleSystem.main.maxParticles; i++) {
matrices[i] = new Matrix4x4 ();
}
}
private void OnWillRenderObject () {
var camera = Camera.current;
if (camera == null) return;
CommandBuffer buf;
if (dict.ContainsKey (camera)) {
buf = dict[camera];
} else {
buf = new CommandBuffer ();
buf.name = "GBuffer Fluid";
camera.AddCommandBuffer (CameraEvent.AfterGBuffer, buf);
dict[camera] = buf;
}
buf.Clear ();
buf.GetTemporaryRT (
depth0RT,
width: -1,
height: -1,
depthBuffer : 0,
filter : FilterMode.Point,
format : RenderTextureFormat.RFloat
);
buf.SetRenderTarget (
new RenderTargetIdentifier (depth0RT),
new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget)
);
buf.ClearRenderTarget (false, true, new Color (1, 1, 1, 1));
material.SetFloat (radiusID, particleSize);
var numParticleAlive = particleSystem.GetParticles (particles);
for (var i = 0; i < numParticleAlive; i++) {
matrices[i].SetTRS (
particles[i].position,
camera.transform.rotation,
Vector3.one * particleSize
);
}
buf.DrawMeshInstanced (quad, 0, material, 0, matrices);
buf.GetTemporaryRT (
depth1RT,
width: -1,
height: -1,
depthBuffer : 0,
filter : FilterMode.Point,
format : RenderTextureFormat.RFloat
);
buf.SetRenderTarget (
new RenderTargetIdentifier (depth1RT),
new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget)
);
buf.ClearRenderTarget (false, true, new Color (1, 1, 1, 1));
buf.DrawMesh (
picturePlane,
Matrix4x4.identity,
material,
submeshIndex : 0,
shaderPass : 1
);
buf.SetRenderTarget (
new RenderTargetIdentifier (depth0RT),
new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget)
);
buf.ClearRenderTarget (false, true, new Color (1, 1, 1, 1));
buf.DrawMesh (
picturePlane,
Matrix4x4.identity,
material,
submeshIndex : 0,
shaderPass : 2
);
var gBufferTarget = new [] {
new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer0),
new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer1),
new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer2),
new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget),
};
buf.SetRenderTarget (gBufferTarget, BuiltinRenderTextureType.CameraTarget);
material.SetVector (
gBuffer0ColorPropertyID,
new Vector4 (albedo.r, albedo.g, albedo.b, occlusion)
);
material.SetVector (
gBuffer1ColorPropertyID,
new Vector4 (specular.r, specular.g, specular.b, smoothness)
);
material.SetVector (
gBuffer3ColorPropertyID,
new Vector4 (emission.r, emission.g, emission.b, 0)
);
var right = camera.farClipPlane * Mathf.Tan (camera.fieldOfView * 0.5f * Mathf.Deg2Rad) * camera.aspect;
var left = -camera.farClipPlane * Mathf.Tan (camera.fieldOfView * 0.5f * Mathf.Deg2Rad) * camera.aspect;
var top = camera.farClipPlane * Mathf.Tan (camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
var bottom = -camera.farClipPlane * Mathf.Tan (camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
var corner = new Vector4 (left, right, bottom, top);
material.SetVector (frustumCornersID, corner);
buf.DrawMesh (
picturePlane,
Matrix4x4.identity,
material,
submeshIndex : 0,
shaderPass : 3
);
}
}
OnWillRenderObject
を使っているので、
画面に常に映る地面などのGameObjectへアタッチする必要があります。
シェーダは次のとおり。
Shader "Fluid/FluidParticle"
{
Properties
{
_GBuffer0Color ("GBuffer0 Color", Color) = (0, 0, 0, 0)
_GBuffer1Color ("GBuffer1 Color", Color) = (0, 0, 0, 0)
_GBuffer3Color ("GBuffer3 Color", Color) = (0, 0, 0, 0)
}
SubShader
{
Pass
{
Name "Instancing"
Stencil {
Ref 129
WriteMask 129
Pass Replace
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
float3 eyeSpacePos : TEXCOORD1;
};
struct flagout
{
float depth: SV_Target;
};
uniform float _Radius;
void vert (in appdata v, out v2f o)
{
UNITY_SETUP_INSTANCE_ID (v);
o.position = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.eyeSpacePos = UnityObjectToViewPos(v.vertex);
}
void frag (in v2f i, out flagout o)
{
float3 normal;
normal.xy = i.uv *2 - 1;
float r2 = dot(normal.xy, normal.xy);
if (r2 > 1.0) discard;
normal.z = sqrt(1.0 - r2);
float4 pixelPos = float4(i.eyeSpacePos + normal * _Radius, 1);
float4 clipSpacePos = mul(UNITY_MATRIX_P, pixelPos);
o.depth = Linear01Depth(clipSpacePos.z / clipSpacePos.w);
}
ENDCG
}
CGINCLUDE
#include "UnityCG.cginc"
float bilateralBlur(float2 uv, sampler2D depthSampler, float2 blurDir) {
float depth = tex2D(depthSampler, uv).x;
float radius = min(1 / Linear01Depth(depth), 50);
float sum = 0;
float wsum = 0;
for (float x = -radius; x <= radius; x += 1) {
float sample = tex2Dlod(depthSampler, float4(uv + x * blurDir, 0, 0)).x;
float r = x * 0.2;
float w = exp(-r * r);
float r2 = (sample - depth) * 5;
float g = exp(-r2*r2);
sum += sample * w * g;
wsum += w * g;
}
if (wsum > 0) {
sum /= wsum;
}
return sum;
}
ENDCG
Pass {
Name "xBlur"
ZTest Always
Blend One Zero
Stencil {
Ref 1
ReadMask 1
Comp Equal
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
struct flagout
{
float depth : SV_TARGET;
};
uniform sampler2D _Depth0RT;
void vert (in appdata v, out v2f o)
{
o.position = v.vertex;
o.uv = v.uv;
}
void frag (in v2f i, out flagout o)
{
o.depth = bilateralBlur(
i.uv,
_Depth0RT,
float2(1 / _ScreenParams.x, 0)
);
}
ENDCG
}
Pass {
Name "yBlur"
ZTest Always
Blend One Zero
Stencil {
Ref 1
ReadMask 1
Comp Equal
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
struct flagout
{
float depth : SV_TARGET;
};
uniform sampler2D _Depth1RT;
void vert (in appdata v, out v2f o)
{
o.position = v.vertex;
o.uv = v.uv;
}
void frag (in v2f i, out flagout o)
{
o.depth = bilateralBlur(
i.uv,
_Depth1RT,
float2(0, 1 / _ScreenParams.y)
);
}
ENDCG
}
Pass {
Name "CalculateNormal"
Blend One Zero
Stencil {
Ref 1
ReadMask 1
Comp Equal
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
struct flagout
{
float4 gBuffer0 : SV_TARGET0;
float4 gBuffer1 : SV_TARGET1;
float4 gBuffer2 : SV_TARGET2;
float4 gBuffer3 : SV_TARGET3;
float depth: SV_DEPTH;
};
uniform float4 _GBuffer0Color;
uniform float4 _GBuffer1Color;
uniform float4 _GBuffer3Color;
uniform sampler2D _Depth0RT;
uniform float4 _FrustumCorner;
void vert (in appdata v, out v2f o)
{
o.position = v.vertex;
o.uv = v.uv;
}
float3 uvToEyeSpacePos(float2 uv, sampler2D depth)
{
float d = tex2D(depth, uv).x;
float3 frustumRay = float3(
lerp(_FrustumCorner.x, _FrustumCorner.y, uv.x),
lerp(_FrustumCorner.z, _FrustumCorner.w, uv.y),
_ProjectionParams.z
);
return frustumRay * d;
}
void frag (in v2f i, out flagout o)
{
float3 eyeSpacePos = uvToEyeSpacePos(i.uv, _Depth0RT);
o.depth = mul(UNITY_MATRIX_P, float4(eyeSpacePos, 1)).z;
float3 ddx = uvToEyeSpacePos(i.uv + float2(1 / _ScreenParams.x, 0), _Depth0RT) - eyeSpacePos;
float3 ddx2 = eyeSpacePos - uvToEyeSpacePos(i.uv - float2(1 / _ScreenParams.x, 0), _Depth0RT);
if (abs(ddx.z) > abs(ddx2.z)) {
ddx = ddx2;
}
float3 ddy = uvToEyeSpacePos(i.uv + float2(0, 1 / _ScreenParams.y), _Depth0RT) - eyeSpacePos;
float3 ddy2 = eyeSpacePos - uvToEyeSpacePos(i.uv - float2(0, 1 / _ScreenParams.y), _Depth0RT);
if (abs(ddy2.z) < abs(ddy.z)) {
ddy = ddy2;
}
float3 normal = cross(ddy, ddx);
normal = normalize(normal);
#if defined(UNITY_REVERSED_Z)
normal.z = -normal.z;
#endif
float4 worldSpacewNormal = mul(
transpose(UNITY_MATRIX_V),
float4(normal, 0)
);
o.gBuffer0 = _GBuffer0Color;
o.gBuffer1 = _GBuffer1Color;
o.gBuffer2 = float4(worldSpacewNormal * 0.5 + float3(0.5, 0.5, 0.5), 1);
o.gBuffer3 = _GBuffer3Color;
}
ENDCG
}
}
}
順番に解説していきます。
Meshの作成
Start
の最初で描画に使うMeshを作成しています。
public class FluidRendering : MonoBehaviour {
private Mesh quad;
...
private void Start () {
quad = new Mesh ();
var quadVertices = new List<Vector3> {
new Vector3 (0.5f, 0.5f, 0),
new Vector3 (-0.5f, 0.5f, 0),
new Vector3 (0.5f, -0.5f, 0),
new Vector3 (-0.5f, -0.5f, 0),
};
var quadTriangles = new List<int> {
1,
0,
2,
1,
2,
3
};
var quadUVs = new List<Vector2> {
new Vector2 (1, 1),
new Vector2 (0, 1),
new Vector2 (1, 0),
new Vector2 (0, 0),
};
quad.SetVertices (quadVertices);
quad.SetTriangles (quadTriangles, 0);
quad.SetUVs (0, quadUVs);
...
}
}
一辺の大きさが1の正方形を作っています。
次に画面全体を覆うメッシュも作成しています。
public class FluidRendering : MonoBehaviour {
...
private Mesh picturePlane;
...
private void Start () {
...
picturePlane = new Mesh ();
var ppVertices = new List<Vector3> {
new Vector3 (1.0f, 1.0f, 0.0f),
new Vector3 (-1.0f, 1.0f, 0.0f),
new Vector3 (-1.0f, -1.0f, 0.0f),
new Vector3 (1.0f, -1.0f, 0.0f),
};
var ppTriangles = new List<int> { 0, 1, 2, 2, 3, 0 };
var ppUVs = new List<Vector2> {
new Vector2 (1f, 0),
new Vector2 (0, 0),
new Vector2 (0, 1f),
new Vector2 (1f, 1f),
};
picturePlane.SetVertices (ppVertices);
picturePlane.SetTriangles (ppTriangles, 0);
picturePlane.SetUVs (0, ppUVs);
...
}
}
こちらは2パス目以降で使います。 一辺が-1~1の正方形です。
マテリアルの準備
private Material material;
private int gBuffer0ColorPropertyID;
private int gBuffer1ColorPropertyID;
private int gBuffer3ColorPropertyID;
private int frustumCornersID;
private int radiusID;
private int depth0RT;
private int depth1RT;
...
private ParticleSystem.Particle[] particles;
private Matrix4x4[] matrices;
...
private void Start () {
...
material = new Material (Shader.Find ("Fluid/FluidParticle"));
material.enableInstancing = true;
gBuffer0ColorPropertyID = Shader.PropertyToID ("_GBuffer0Color");
gBuffer1ColorPropertyID = Shader.PropertyToID ("_GBuffer1Color");
gBuffer3ColorPropertyID = Shader.PropertyToID ("_GBuffer3Color");
frustumCornersID = Shader.PropertyToID ("_FrustumCorner");
radiusID = Shader.PropertyToID ("_Radius");
depth0RT = Shader.PropertyToID ("_Depth0RT");
depth1RT = Shader.PropertyToID ("_Depth1RT");
particles = new ParticleSystem.Particle[particleSystem.main.maxParticles];
matrices = new Matrix4x4[particleSystem.main.maxParticles];
for (var i = 0; i < particleSystem.main.maxParticles; i++) {
matrices[i] = new Matrix4x4 ();
}
}
"Fluid/FluidParticle"
というシェーダからマテリアルを作成しています。
material.enableInstancing = true;
でGPUインスタンシングを有効にしています。
あとでシェーダに値を渡すときに利用するプロパティIDを取得しています。
パーティクル用の配列とパーティクルの行列用の配列を用意する部分は 前回と同じですね。
CommandBufferの初期化
...
private Dictionary<Camera, CommandBuffer> dict = new Dictionary<Camera, CommandBuffer> ();
...
private void OnWillRenderObject () {
var camera = Camera.current;
if (camera == null) return;
CommandBuffer buf;
if (dict.ContainsKey (camera)) {
buf = dict[camera];
} else {
buf = new CommandBuffer ();
buf.name = "GBuffer Fluid";
camera.AddCommandBuffer (CameraEvent.AfterGBuffer, buf);
dict[camera] = buf;
}
buf.Clear ();
...
}
カメラごとに呼ばれるOnWillRenderObject
でCommandBufferを作成しています。
作成したCommandBufferはCameraEvent.AfterGBuffer
のタイミングに差し込んでいます。
すでにカメラにCommandBufferが登録されている場合はそのCommandBufferを再利用します。
buf.Clear()
でCommandBufferをクリアします。
ここからコマンドを積んでいきます。
今回は全体で4つのパスを作っています。 1つ目でdepthを書き出し2つ目と3つ目で縦横のブラーをかけて4つ目でGBufferを書き出します。
1つ目から3つ目のパスはリニアのdepthを書き出すので、 チャンネル1つのRenderTextureを取得します。 3つ目では1つ目のRenderTextureを使い回すのでRenderTextureは2つ用意します。
1パス目
スクリプト
private void OnWillRenderObject () {
...
buf.GetTemporaryRT (
depth0RT,
width: -1,
height: -1,
depthBuffer : 0,
filter : FilterMode.Point,
format : RenderTextureFormat.RFloat
);
buf.SetRenderTarget (
new RenderTargetIdentifier (depth0RT),
new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget)
);
buf.ClearRenderTarget (false, true, new Color (1, 1, 1, 1));
material.SetFloat (radiusID, particleSize);
var numParticleAlive = particleSystem.GetParticles (particles);
for (var i = 0; i < numParticleAlive; i++) {
matrices[i].SetTRS (
particles[i].position,
camera.transform.rotation,
Vector3.one * particleSize
);
}
buf.DrawMeshInstanced (quad, 0, material, 0, matrices);
...
}
buf.GetTemporaryRT
でリニアなdepthを書き込むためのRenderTextureを取得します。
buf.GetTemporaryRT
の第一引数には前もって取得しておいたプロパティIDを設定します。
depth0RT = Shader.PropertyToID ("_Depth0RT");
としていたので、
このRenderTextureにはシェーダ内で_Depth0RT
をつかってアクセスできるようになります。
width
とheight
に-1
を指定することでカメラと同じサイズにできます。
デプスとステンシルにはカメラのものを使うためデプスバッファは必要ないので
depthBuffer
に0を指定します。
format
をRenderTextureFormat.RFloat
にして
floatの1チャンネルのRenderTextureを用意しています。
buf.SetRenderTarget
で描画先を作ったRenderTextureに設定します。
buf.ClearRenderTarget (false, true, new Color (1, 1, 1, 1));
で
描画先をクリアします。
material.SetFloat (radiusID, particleSize);
でパーティクルの半径を渡します。
今回はパーティクルの大きさは時間によって変化せず、すべてのインスタンスで同じなので、
インスタンスごとには値を設定しません。
パーティクルごとに行列を計算しCommandBuffer.DrawMeshInstanced
にわたすのは
前回と同じです。
particleSystem.GetParticles
でパーティクルを取得して行列の配列を作り、
buf.DrawMeshInstanced
で描画しています。
シェーダ
1パス目で使われているシェーダは次のとおりです。
Pass
{
Name "Instancing"
Stencil {
Ref 129
WriteMask 129
Pass Replace
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 position : SV_POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float3 eyeSpacePos : TEXCOORD1;
};
struct flagout
{
float depth: SV_Target;
};
uniform float _Radius;
void vert (in appdata v, out v2f o)
{
UNITY_SETUP_INSTANCE_ID (v);
o.position = UnityObjectToClipPos(v.vertex);
o.normal = UnityObjectToWorldNormal(v.normal);
o.uv = v.uv;
o.eyeSpacePos = UnityObjectToViewPos(v.vertex);
}
void frag (in v2f i, out flagout o)
{
float3 normal;
normal.xy = i.uv *2 - 1;
float r2 = dot(normal.xy, normal.xy);
if (r2 > 1.0) discard;
normal.z = sqrt(1.0 - r2);
float4 pixelPos = float4(i.eyeSpacePos + normal * _Radius, 1);
float4 clipSpacePos = mul(UNITY_MATRIX_P, pixelPos);
o.depth = Linear01Depth(clipSpacePos.z / clipSpacePos.w);
}
ENDCG
}
ほとんど前回のと同じです。 今回はパーティクルの大きさを変化させるつもりはないので、 その部分のコードは省略しています。 また、法線は後で計算するのでここではデプスだけを書き出しています。
ステンシル
Stencil {
Ref 129
WriteMask 129
Pass Replace
}
シェーディングを行うために必要な128の他に、このあとのパスで使う1を立てています。
頂点シェーダ
void vert (in appdata v, out v2f o)
{
UNITY_SETUP_INSTANCE_ID (v);
o.position = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.eyeSpacePos = UnityObjectToViewPos(v.vertex);
}
前回と同じですね。
フラグメントシェーダ
void frag (in v2f i, out flagout o)
{
float3 normal;
normal.xy = i.uv *2 - 1;
float r2 = dot(normal.xy, normal.xy);
if (r2 > 1.0) discard;
normal.z = sqrt(1.0 - r2);
float4 pixelPos = float4(i.eyeSpacePos + normal * _Radius, 1);
float4 clipSpacePos = mul(UNITY_MATRIX_P, pixelPos);
o.depth = Linear01Depth(clipSpacePos.z / clipSpacePos.w);
}
最後の書き出す値が0~1のリニアなdepthである点だけが前回と違います。
Linear01Depth(depth)
は環境の違いを吸収して
0から1までのリニアなdepthを作ってくれるものです。
2パス目
2パス目は画面全体を覆うメッシュを使って描画します。 1パス目の結果をテクスチャとして受け取ってもう一方のRenderTextureに 書き出しを行います。
2パス目ではx方向のバイラテラルブラーを行います。 2次元のバイラテラルブラーは本来xとyに分離できませんが、 ここでは大胆に分離して近似的に求めています。
スクリプト
private void OnWillRenderObject () {
...
buf.GetTemporaryRT (
depth1RT,
width: -1,
height: -1,
depthBuffer : 0,
filter : FilterMode.Point,
format : RenderTextureFormat.RFloat
);
buf.SetRenderTarget (
new RenderTargetIdentifier (depth1RT),
new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget)
);
buf.ClearRenderTarget (false, true, new Color (1, 1, 1, 1));
buf.DrawMesh (
picturePlane,
Matrix4x4.identity,
material,
submeshIndex : 0,
shaderPass : 1
);
...
}
先ほどと同じようにしてbuf.GetTemporaryRT
で
1チャンネルのRenderTextureを作成します。
このテクスチャにはシェーダ内から_Depth1RT
でアクセスできます。
buf.SetRenderTarget
でレンダーターゲットを指定した後に、
buf.ClearRenderTarget
でクリアしています。
その後、buf.DrawMesh
で画面全体を覆う四角形を描画しています。
shaderPass : 1
としてシェーダの2つ目のパスを使うようにしています。
シェーダ
2パス目のシェーダの全文は次のとおりです。
Pass {
Name "xBlur"
ZTest Always
Blend One Zero
Stencil {
Ref 1
ReadMask 1
Comp Equal
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
struct flagout
{
float depth : SV_TARGET;
};
uniform sampler2D _Depth0RT;
void vert (in appdata v, out v2f o)
{
o.position = v.vertex;
o.uv = v.uv;
}
void frag (in v2f i, out flagout o)
{
o.depth = bilateralBlur(
i.uv,
_Depth0RT,
float2(1 / _ScreenParams.x, 0)
);
}
ENDCG
}
ステンシル
Stencil {
Ref 1
ReadMask 1
Comp Equal
}
前のパスで書き出されたところだけ処理するためにステンシルを使っています。
レンダーテクスチャーの取得
uniform sampler2D _Depth0RT;
これで前のパスのレンダーターゲットをテクスチャとして受け取ります。 これで前のパスの描画結果を受け取れます。
フラグメントシェーダの出力
struct flagout
{
float depth : SV_TARGET;
};
今回の出力はfloatが1チャンネルだけです。
頂点シェーダ
void vert (in appdata v, out v2f o)
{
o.position = v.vertex;
o.uv = v.uv;
}
通常行うMVPをかけずに画面を覆うようにしています。
フラグメントシェーダ
フラグメントシェーダは次のとおりです。
void frag (in v2f i, out flagout o)
{
o.depth = bilateralBlur(
i.uv,
_Depth0RT,
float2(1 / _ScreenParams.x, 0)
);
}
1パスで書き出したdepthを_Depth0RT
で受け取り、バイラテラルフィルターをかけて
書き出しています。
float2(1 / _ScreenParams.x, 0)
でx方向の1ピクセルのサイズを渡しています。
_ScreenParams.x
はUnityの組み込みの変数でレンダーターゲットのwidth方向の
ピクセル数が入っています。
バイラテラルブラーを行うbilateralBlur
は3パス目でも使うので、CGINCLUDE
~ENDCG
に書いています。
CGINCLUDE
は次のとおり。
CGINCLUDE
#include "UnityCG.cginc"
float bilateralBlur(float2 uv, sampler2D depthSampler, float2 blurDir) {
float depth = tex2D(depthSampler, uv).x;
float radius = min(1 / Linear01Depth(depth), 50);
float sum = 0;
float wsum = 0;
for (float x = -radius; x <= radius; x += 1) {
float sample = tex2Dlod(depthSampler, float4(uv + x * blurDir, 0, 0)).x;
float r = x * 0.2;
float w = exp(-r * r);
float r2 = (sample - depth) * 5;
float g = exp(-r2*r2);
sum += sample * w * g;
wsum += w * g;
}
if (wsum > 0) {
sum /= wsum;
}
return sum;
}
ENDCG
スライドのとおりです。
ブラーの半径はデプスで適当に割った値と50との小さい方をとっています。
0.2
や5
というのはレンダリングの結果を見ながら適当に決めた値です。
3パス目
2パス目の方向を変えただけで同じです。
スクリプトは次のとおりです。
private void OnWillRenderObject () {
...
buf.SetRenderTarget (
new RenderTargetIdentifier (depth0RT),
new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget)
);
buf.ClearRenderTarget (false, true, new Color (1, 1, 1, 1));
buf.DrawMesh (
picturePlane,
Matrix4x4.identity,
material,
submeshIndex : 0,
shaderPass : 2
);
...
}
書き出すレンダーターゲットを_Depth0RT
にしています。
DrawMesh
でshaderPass
に2
を指定して3つ目のパスを使っています。
シェーダは次のとおりです。
Pass {
Name "yBlur"
ZTest Always
Blend One Zero
Stencil {
Ref 1
ReadMask 1
Comp Equal
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
struct flagout
{
float depth : SV_TARGET;
};
uniform sampler2D _Depth1RT;
void vert (in appdata v, out v2f o)
{
o.position = v.vertex;
o.uv = v.uv;
}
void frag (in v2f i, out flagout o)
{
o.depth = bilateralBlur(
i.uv,
_Depth1RT,
float2(0, 1 / _ScreenParams.y)
);
}
ENDCG
}
ブラーの方向だけが違っています。
float2(0, 1 / _ScreenParams.y)
でy方向の1ピクセルのベクトルを渡しています。
4パス目
depthから座標の復元
このパスではdepthからeyeスペースの座標を復元する必要があります。
視錐台の4つの頂点を与えることでdepthから座標を復元します。
まず最初に現在のピクセルがfar planeではどの位置にあたるかを計算します。
視錐台の左側と右側の縁へのベクトルを線形補間することで、 現在のピクセルをfar planeへ投影したときの点へのベクトルのx成分がわかります。
同様にして視錐台の上側と下側の縁へのベクトルを線形補間することで、 現在のピクセルをfarプレーンへ投影したときの点へのベクトルのy成分がわかります。
これで現在のピクセルがfarプレーン上ではどの位置に当たるのかというベクトルが手に入りました。 このfarプレーンへのベクトルに対して、カメラからfarまでを0から1にしたリニアなdepthを 掛け合わせることでeyeスペースでの座標が求まります。
スクリプト
private void OnWillRenderObject () {
...
var gBufferTarget = new [] {
new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer0),
new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer1),
new RenderTargetIdentifier (BuiltinRenderTextureType.GBuffer2),
new RenderTargetIdentifier (BuiltinRenderTextureType.CameraTarget),
};
buf.SetRenderTarget (gBufferTarget, BuiltinRenderTextureType.CameraTarget);
material.SetVector (
gBuffer0ColorPropertyID,
new Vector4 (albedo.r, albedo.g, albedo.b, occlusion)
);
material.SetVector (
gBuffer1ColorPropertyID,
new Vector4 (specular.r, specular.g, specular.b, smoothness)
);
material.SetVector (
gBuffer3ColorPropertyID,
new Vector4 (emission.r, emission.g, emission.b, 0)
);
var right = camera.farClipPlane * Mathf.Tan (camera.fieldOfView * 0.5f * Mathf.Deg2Rad) * camera.aspect;
var left = -camera.farClipPlane * Mathf.Tan (camera.fieldOfView * 0.5f * Mathf.Deg2Rad) * camera.aspect;
var top = camera.farClipPlane * Mathf.Tan (camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
var bottom = -camera.farClipPlane * Mathf.Tan (camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
var corner = new Vector4 (left, right, bottom, top);
material.SetVector (frustumCornersID, corner);
buf.DrawMesh (
picturePlane,
Matrix4x4.identity,
material,
submeshIndex : 0,
shaderPass : 3
);
}
buf.SetRenderTarget (gBufferTarget, BuiltinRenderTextureType.CameraTarget);
で書き出し先にGBufferを指定しています。
視錐台のfar planeでのleft、right、top、bottomの位置を計算してfloat4に詰め込んで渡します。
buf.DrawMesh
で画面いっぱいの長方形を描画します。
shaderPass
は3
を指定して4番目のパスを使います。
シェーダ
4パス目のシェーダは次のとおりです。
Pass {
Name "CalculateNormal"
Blend One Zero
Stencil {
Ref 1
ReadMask 1
Comp Equal
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
struct flagout
{
float4 gBuffer0 : SV_TARGET0;
float4 gBuffer1 : SV_TARGET1;
float4 gBuffer2 : SV_TARGET2;
float4 gBuffer3 : SV_TARGET3;
float depth: SV_DEPTH;
};
uniform float4 _GBuffer0Color;
uniform float4 _GBuffer1Color;
uniform float4 _GBuffer3Color;
uniform sampler2D _Depth0RT;
uniform float4 _FrustumCorner;
void vert (in appdata v, out v2f o)
{
o.position = v.vertex;
o.uv = v.uv;
}
float3 uvToEyeSpacePos(float2 uv, sampler2D depth)
{
float d = tex2D(depth, uv).x;
float3 frustumRay = float3(
lerp(_FrustumCorner.x, _FrustumCorner.y, uv.x),
lerp(_FrustumCorner.z, _FrustumCorner.w, uv.y),
_ProjectionParams.z
);
return frustumRay * d;
}
void frag (in v2f i, out flagout o)
{
float3 eyeSpacePos = uvToEyeSpacePos(i.uv, _Depth0RT);
o.depth = mul(UNITY_MATRIX_P, float4(eyeSpacePos, 1)).z;
float3 ddx = uvToEyeSpacePos(i.uv + float2(1 / _ScreenParams.x, 0), _Depth0RT) - eyeSpacePos;
float3 ddx2 = eyeSpacePos - uvToEyeSpacePos(i.uv - float2(1 / _ScreenParams.x, 0), _Depth0RT);
if (abs(ddx.z) > abs(ddx2.z)) {
ddx = ddx2;
}
float3 ddy = uvToEyeSpacePos(i.uv + float2(0, 1 / _ScreenParams.y), _Depth0RT) - eyeSpacePos;
float3 ddy2 = eyeSpacePos - uvToEyeSpacePos(i.uv - float2(0, 1 / _ScreenParams.y), _Depth0RT);
if (abs(ddy2.z) < abs(ddy.z)) {
ddy = ddy2;
}
float3 normal = cross(ddy, ddx);
normal = normalize(normal);
#if defined(UNITY_REVERSED_Z)
normal.z = -normal.z;
#endif
float4 worldSpacewNormal = mul(
transpose(UNITY_MATRIX_V),
float4(normal, 0)
);
o.gBuffer0 = _GBuffer0Color;
o.gBuffer1 = _GBuffer1Color;
o.gBuffer2 = float4(worldSpacewNormal * 0.5 + float3(0.5, 0.5, 0.5), 1);
o.gBuffer3 = _GBuffer3Color;
}
ENDCG
}
ステンシル
Stencil {
Ref 1
ReadMask 1
Comp Equal
}
これも最初のパスで書き出されたところだけ処理するためにステンシルを使っています。
プロパティの受け取り
uniform float4 _GBuffer0Color;
uniform float4 _GBuffer1Color;
uniform float4 _GBuffer3Color;
プロパティに設定した値をそれぞれ受け取っています。
フラグメントシェーダの書き出し先
struct flagout
{
float4 gBuffer0 : SV_TARGET0;
float4 gBuffer1 : SV_TARGET1;
float4 gBuffer2 : SV_TARGET2;
float4 gBuffer3 : SV_TARGET3;
float depth: SV_DEPTH;
};
スクリプトで設定したGBufferに書き出します。
頂点シェーダ
void vert (in appdata v, out v2f o)
{
o.position = v.vertex;
o.uv = v.uv;
}
画面いっぱいの長方形をそのまま渡しています。
フラグメントシェーダ
void frag (in v2f i, out flagout o)
{
float3 eyeSpacePos = uvToEyeSpacePos(i.uv, _Depth0RT);
o.depth = mul(UNITY_MATRIX_P, float4(eyeSpacePos, 1)).z;
float3 ddx = uvToEyeSpacePos(i.uv + float2(1 / _ScreenParams.x, 0), _Depth0RT) - eyeSpacePos;
float3 ddx2 = eyeSpacePos - uvToEyeSpacePos(i.uv - float2(1 / _ScreenParams.x, 0), _Depth0RT);
if (abs(ddx.z) > abs(ddx2.z)) {
ddx = ddx2;
}
float3 ddy = uvToEyeSpacePos(i.uv + float2(0, 1 / _ScreenParams.y), _Depth0RT) - eyeSpacePos;
float3 ddy2 = eyeSpacePos - uvToEyeSpacePos(i.uv - float2(0, 1 / _ScreenParams.y), _Depth0RT);
if (abs(ddy2.z) < abs(ddy.z)) {
ddy = ddy2;
}
float3 normal = cross(ddy, ddx);
normal = normalize(normal);
#if defined(UNITY_REVERSED_Z)
normal.z = -normal.z;
#endif
float4 worldSpacewNormal = mul(
transpose(UNITY_MATRIX_V),
float4(normal, 0)
);
o.gBuffer0 = _GBuffer0Color;
o.gBuffer1 = _GBuffer1Color;
o.gBuffer2 = float4(worldSpacewNormal * 0.5 + float3(0.5, 0.5, 0.5), 1);
o.gBuffer3 = _GBuffer3Color;
}
前のパスで計算したdepthに対してx方向とy方向の偏微分を求めてそれの外積で法線を計算しています。
DirectX 11やDirectX 12などではZバッファが逆向きで1がnearで0がfarとして扱われるので
normal.z = -normal.z;
としています。
実行結果
シーンには透明のMesh Colliderが配置されています。 パーティクルはこのColliderにぶつかっています。
シーンにReflection Probeを配置しました。
Post Processingも適当に設定してあります。 Post Processingのアンチエイリアスをかけるとバイラテラルフィルタをxとyに分離したことによる アーティファクトがほとんど気にならなくなりました。
流体のマテリアルは次のとおりです。
実行結果は次のようになりました。
fpsもリアルタイムで問題ないレベルです。
おわりに
今回はCommandBufferを利用して擬似的なメタボールによる流体のレンダリングを行いました。 今回は不透明な流体でしたが、裏面にも同様のことを行って屈折を考えることで 透明な水しぶきなどもレンダリングできるそうです。 興味のある方は挑戦してみるとよいのではないでしょうか。
今回のプロジェクトはGitHubのこちらのリポジトリにおいてあります。
なにか気づいた点などがあれば私のTwitterアカウントの方にお願いします。