UnityでCommandBufferを利用して流体のレンダリングを行う

はじめに

前回はパーティクルで見せかけの球体を大量に描画してみました。 今回は前回のプログラムをベースに流体のレンダリングを行ってみます。

実行結果

Unityのバージョン:2018.3.0b1

今回実装するもの

今回実装するのは次のスライドで紹介されているものです。

次の記事でも紹介されています。

球体のパーティクルのdepthに対してバイラテラルフィルタをかけることで 擬似的なメタボールを描画するものです。 バイラテラルフィルタについては過去に記事を書いたことがあります。

パーティクルの準備

シーンにパーティクルシステムを用意します。

パーティクルの設定

パーティクルのLifetimeを10秒にして パーティクルの最大個数を1000個に設定しています。

Emissionの設定

パーティクルを毎秒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をつかってアクセスできるようになります。 widthheight-1を指定することでカメラと同じサイズにできます。 デプスとステンシルにはカメラのものを使うためデプスバッファは必要ないので depthBufferに0を指定します。 formatRenderTextureFormat.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パス目でも使うので、CGINCLUDEENDCGに書いています。 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.25というのはレンダリングの結果を見ながら適当に決めた値です。

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にしています。 DrawMeshshaderPass2を指定して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 planeへのベクトルのx成分

同様にして視錐台の上側と下側の縁へのベクトルを線形補間することで、 現在のピクセルをfarプレーンへ投影したときの点へのベクトルのy成分がわかります。

現在のピクセルのfar planeへのベクトルのy成分

これで現在のピクセルがfarプレーン上ではどの位置に当たるのかというベクトルが手に入りました。 このfarプレーンへのベクトルに対して、カメラからfarまでを0から1にしたリニアなdepthを 掛け合わせることでeyeスペースでの座標が求まります。

現在のピクセルのfar planeへのベクトルにdepthをかけることで座標が求まる

スクリプト

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で画面いっぱいの長方形を描画します。 shaderPass3を指定して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アカウントの方にお願いします。

  • Unity
  • Shader
新しい投稿
Blender自分用メモ