凹みTips

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

Unity でボリュームレンダリングをしてみる - vol.5 遮蔽とループの最適化

はじめに

前回はノイズ関数で作る雲についての話をしました。

tips.hecomi.com

おわりに書いたように、今回はポリゴンオブジェクトによる遮蔽およびループの最適化についての話をしようと思います。

サンプル

github.com

8. AABB Intersection 2 および 9. Occlusion が今回の範囲になります。

ループの最適化

前回の記事では、キューブポリゴンの中にレイがいるかどうかの判定を以下のコードで行っていました。

if (!all(max(0.5 - abs(localPos), 0.0))) break;

これについてはこの記事で解説を行っています。XYZ 各軸に対して辺の長さを調べて範囲内(-0.5 ~ +0.5)に各軸入っているかを見ている形ですね。

tips.hecomi.com

一方、vol.2 では AABB に対するレイの内外判定についての話を書きました。

tips.hecomi.com

これを使うと、レイの開始点(キューブポリゴン表面)から終了点(キューブポリゴンを抜ける裏面)までの距離をループの前に算出できます。以前の記事ではループを固定してステップ長を変更していたので重くなってしまっていましたが...(なぜそうしてしまったかは謎...)、ステップ長を固定して必要なループ数を事前に算出すればループ内の if 文が不要になります。

一応このキューブを通過する距離が正しく算出できているか以下のコードで確認してみましょう。

float4 frag(v2f i) : SV_Target
{
    // ワールド空間でのポリゴン表面座標とそこへのカメラからの向き
    float3 worldPos = i.worldPos;
    float3 worldDir = normalize(worldPos - _WorldSpaceCameraPos);

    // オブジェクト空間に変換
    float3 localPos = mul(unity_WorldToObject, float4(worldPos, 1.0));
    float3 localDir = UnityWorldToObjectDir(worldDir);

    // レイが突き抜けるまでの長さ
    float3 invLocalDir = 1.0 / localDir;
    float3 t1 = (-0.5 - localPos) * invLocalDir;
    float3 t2 = (+0.5 - localPos) * invLocalDir;
    float3 tmax3 = max(t1, t2);
    float2 tmax2 = min(tmax3.xx, tmax3.yz);
    float traverseDist = min(tmax2.x, tmax2.y);

    // 0 ~ 1 で赤、1 ~ で黄色になるように調整
    float f = frac(traverseDist);
    float r = min(traverseDist, 1.0);
    float g = max(traverseDist - r, 0.0);
    return float4(r, g, 0.0, 1.0);
}

f:id:hecomi:20200525021626g:plain

斜めにトラバースするときは 1 を超えて黄色に近づいて行くのがわかります。あってそうですね。これにより、ループ内での内外判定を除外することができ、より効率的にループを回せるようになりました。ループのコードも含めて書くと以下のようになります。

Shader "Intersection"
{

Properties
{
    ...
    [PowerSlider(10.0)] _Step("Step", Range(0.001, 0.1)) = 0.03
    ...
}

CGINCLUDE

...
float _Step;
...

float4 frag(v2f i) : SV_Target
{
    ...
    float3 invLocalDir = 1.0 / localDir;
    float3 t1 = (-0.5 - localPos) * invLocalDir;
    float3 t2 = (+0.5 - localPos) * invLocalDir;
    float3 tmax3 = max(t1, t2);
    float2 tmax2 = min(tmax3.xx, tmax3.yz);
    float traverseDist = min(tmax2.x, tmax2.y);
    int loop = floor(traverseDist / _Step);
    ...
    for (int i = 0; i < loop; ++i)
    {
        ...
    }
    ...
}

...
ENDCG
...

}

おまけ

完全に vol.2 で書いた内容を忘れていて、自分で最初に考えて書いたコードはこれでした。

// レイを十分に長い距離(2)伸ばして貫通させ、遠い側のスラブの交点を xyz それぞれで求める
float3 farSlabPos = clamp(localPos + localDir * 2, -0.5, 0.5);

// 開始点から xyz それぞれのスラブまでの距離
float3 toSlabPos = farSlabPos - localPos;

// xyz それぞれのスラブ位置までレイを伸ばした際のレイの長さ
float3 invLocalDir = 1.0 / localDir;
float3 distToSlab = abs(toSlabPos * invLocalDir);

// 一番短いものが貫通するスラブ
float traverseDist = min(min(distToSlab.x, distToSlab.y), distToSlab.z);

// 貫通に必要なステップ数
int loop = floor(traverseDist / _Step);

ステップ数が少し多くなるのでこれは結局ボツにしましたが別解ということで。。

ポリゴンオブジェクトによる遮蔽

さて、次はポリゴンによる遮蔽を行いたいと思います。現状だと次のような見え方になってしまっています。これを解決するには、遮蔽判定を行う必要があります。これは以前レイマーチングの記事で行ったデプスバッファとレイの深度の比較がそのまま使えそうです。

tips.hecomi.com

具体的にはこんなコードです。

...
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
...
float4 frag(v2f i) : SV_Target
{
    ...
    float depth = LinearEyeDepth(
        SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
    float3 cameraForward = -UNITY_MATRIX_V[2].xyz;
    float cameraToDepth = depth / dot(worldDir, cameraForward);
    float cameraToStart = length(worldPos - _WorldSpaceCameraPos);
    float maxLen = cameraToDepth - cameraToStart;
    float len = 0.f;
    ...
    for (int i = 0; i < loop; ++i)
    {
        ...
        len += _Step;
        if (len > maxLen) break;
        ...
    }
    ...
}
...

まずデプスバッファまでの距離 cameraToDepth を求めます。次にレイの開始点(ポリゴン表面位置)までのカメラからの距離 cameraToStart を求めてあげれば、この差分がレイの最大長 maxLen となるわけです。では結果を見てみましょう。

f:id:hecomi:20200525020709g:plain

遮蔽はうまく行っているようですがステップのアーティファクトが見えてしまっています(GIF の階調問題ではないです)。これを解決するには以下の記事でも言及されているように最後に 1 回だけデプス表面までレイを伸ばしてサンプリングを行います。

shaderbits.com

float4 frag(v2f i) : SV_Target
{
    ...
    for (int i = 0; i < loop; ++i)
    {
        ...
    }

    if (len > maxLen)
    {
        float step = maxLen - (len - _Step);
        localPos += step * localDir;
        float density = densityFunction(localPos);

        if (density > 0.0)
        {
            float d = density * step;
            transmittance *= 1.0 - d * _Absorption;

            float transmittanceLight = 1.0;
            float3 lightPos = localPos;

            for (int j = 0; j < _LoopLight; ++j)
            {
                float densityLight = densityFunction(lightPos);

                if (densityLight > 0.0)
                {
                    float dl = densityLight * lightStep;
                    transmittanceLight *= 1.0 - dl * _AbsorptionLight;
                    if (transmittanceLight < 0.01) 
                    {
                        transmittanceLight = 0.0;
                        break;
                    }
                }

                lightPos += localLightStep;
            }

            color.a += _Color.a * (_Opacity * d * transmittance);
            color.rgb += _LightColor0 * (_OpacityLight * d * transmittance * transmittanceLight);
            color = clamp(color, 0.0, 1.0);
        }
    }
    ...
}
...

これで次のようにアーティファクトが解消されました。

f:id:hecomi:20200527004807g:plain

ループの中のコードとだいたい同じなので関数に切り出して整理すれば完成です。完成コードはサンプルプロジェクトをご参照ください。

おわりに

あとはライティングをもう少し良くする(アンビエント対応やプローブ対応)のと影の対応を行いたいです。