凹みTips

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

Unity でボリュームレンダリングをしてみる - vol.2 レイの衝突判定

はじめに

前回、Unity でのボリュームレンダリングの導入を行いました。

tips.hecomi.com

今回はレイの通過判定の改善を行います。

プロジェクト

github.com

4 が今回の範囲になります。

環境

レイと AABB の衝突

前回は all(max(0.5 - abs(lpos), 0.0)) というレイが立方体の内部にいるかどうかの判定をループ回数分行っていましたが、分岐はシェーダ内では高コストのため避けたいところです。そこで、ボリュームを覆う立方体ポリゴンを AABB としてみなし(ローカル座標系なら Axis-Aligned になる)、レイと AABB の高速な衝突計算を分岐や除算を極力排除して行えば高速化が望めます。高速な計算は以下に解説が載っています。

日本語では以下の記事がとてもわかり易いです:

まずは衝突計算は置いておいて、前回のレイのループ部を以下のように書き直して衝突計算ができた場合にどういう計算になるかのイメージを確認しておきましょう。

struct Ray
{
    float3 from;
    float3 dir;
    float tmin;
    float tmax;
};

fixed4 frag(v2f i) : SV_Target
{
    float3 worldDir = i.worldPos - _WorldSpaceCameraPos;
    float3 localDir = normalize(mul(unity_WorldToObject, worldDir));

    Ray ray;
    ray.from = i.localPos;
    ray.dir = localDir;
    intersection(ray);

    float3 localStep = localDir * ray.tmax / _Iteration;
    float3 localPos = i.localPos;
    fixed4 output = 0.0;

    [loop]
    for (int i = 0; i < _Iteration; ++i)
    {
        output += (1.0 - output.a) * sample(localPos + 0.5) * _Intensity;
        localPos += localStep;
    }

    return _Color * output;
}

前回と同じようにローカル座標系に変換した後、ポリゴン表面(i.localPos)から微小ステップ(localStep)毎進めた位置のボリュームのデータ(sample())をアルファブレンディングしていく形です。

f:id:hecomi:20180104172014p:plain

前回と異なるのは、微小ステップを前回は 1.0 / _Iteration と適当にしていたところを、レイの AABB の通過距離(localDir * ray.tmax)が AABB とレイの衝突計算により事前にわかることから、内外判定がループ内で必要なくなっている点です。ではこの衝突判定をどうやるか見ていきましょう。

bool intersection(inout Ray ray)
{
    ray.tmin = -10000.0;
    ray.tmax = +10000.0;

    if (ray.dir.x != 0.0) 
    {
        float t1 = (-0.5 - ray.from.x) / ray.dir.x;
        float t2 = (+0.5 - ray.from.x) / ray.dir.x;
        ray.tmin = max(ray.tmin, min(t1, t2));
        ray.tmax = min(ray.tmax, max(t1, t2));
    }
 
    if (ray.dir.y != 0.0) 
    {
        float t1 = (-0.5 - ray.from.y) / ray.dir.y;
        float t2 = (+0.5 - ray.from.y) / ray.dir.y;
        ray.tmin = max(ray.tmin, min(t1, t2));
        ray.tmax = min(ray.tmax, max(t1, t2));
    }
 
    if (ray.dir.z != 0.0) 
    {
        float t1 = (-0.5 - ray.from.z) / ray.dir.z;
        float t2 = (+0.5 - ray.from.z) / ray.dir.z;
        ray.tmin = max(ray.tmin, min(t1, t2));
        ray.tmax = min(ray.tmax, max(t1, t2));
    }

    return ray.tmax >= ray.tmin;
}

XYZ それぞれの方向のエッジケース(それぞれが軸に平行な場合)を除いて、それぞれの軸の入出時間を調べ、その交差点を探索しています(詳細は前述の記事をご参照下さい)。AABB は固定で、(-0.5, -0.5, -0.5) - (0.5, 0.5, 0.5) としています。しかしながら見てお分かりのようにエッジケース判定の分岐も除算も計算ステップも多く、とても非効率です。

そこで、これを記事に従って最適化します。エッジケース部は float の仕様から暗黙的に除外できるので外し、レイの逆数は予め計算しておきます。更に、それぞれの軸に分割して計算しているところは HLSL の関数でまとめて扱えるのでまとめてしまいます。これで次のような計算に簡略化できます。

bool intersection(inout Ray ray)
{
    float3 invDir = 1.0 / ray.dir;
    float3 t1 = (-0.5 - ray.from) * invDir;
    float3 t2 = (+0.5 - ray.from) * invDir;

    float3 tmin3 = min(t1, t2);
    float2 tmin2 = max(tmin3.xx, tmin3.yz);
    ray.tmin = max(tmin2.x, tmin2.y);

    float3 tmax3 = max(t1, t2);
    float2 tmax2 = min(tmax3.xx, tmax3.yz);
    ray.tmax = min(tmax2.x, tmax2.y);

    return ray.tmax >= ray.tmin;
}

tmin3tmax3 はそれぞれ最初・最後のスラブにあたるまでの時間(X、Y、Z別)で、この中から最後に入るスラブの時間(tmin3 の成分で最も大きい時間)と最初にスラブから出る時間(tmax3 の成分で最も小さい時間)を求め、入る → 出るの順に時間がなっていれば AABB と衝突している、となります。

しかしもう少し考えてみると、オブジェクトスペースで計算していて、このフラグメントシェーダが起動されているということは AABB すなわち立方体ポリゴンと衝突しているので衝突判定は必要ありません...。更に最初に衝突する面はポリゴン表面とわかっているので(ray.fromi.localPos).、必要なのはポリゴンからいつ抜けるのかだけです。つまり tmax だけあればよく、これからレイの通過距離が求められれば他の情報は必要ありません。そこで要らない計算を削ります。

void intersection(inout Ray ray)
{
    float3 invDir = 1.0 / ray.dir;
    float3 t1 = (-0.5 - ray.from) * invDir;
    float3 t2 = (+0.5 - ray.from) * invDir;

    float3 tmax3 = max(t1, t2);
    float2 tmax2 = min(tmax3.xx, tmax3.yz);
    ray.tmax = min(tmax2.x, tmax2.y);
}

これで必要なレイの通過時間を少ない計算で求められました。

あとはループで _Iteration を変数として与えているところを固定値にしてやり、[loop] しているところを [unroll] して展開すればループ数によってはもう少し効率良くなるかもしれません。

結果

前回

f:id:hecomi:20180108205708p:plain

今回

f:id:hecomi:20180108205741p:plain

今回の方がレイが確実に到達するため全体的に明るくなっています。しかしながらパフォーマンスを見てみると前回の方が 10 msec 以上速いです。。これはレイを固定長にすることで端などは早めにレイが抜けるためループ数が少なく押さえられているのと、判定するための計算がそこまで重くないためループ数増加のペナルティの方が大きい、といったことが考えられます。

なのでプロパティで切り替えられるようレイを固定長にする版も書きました。

Shader "VolumeRendering/VolumeRendering2"
{

Properties
{
    ...
    [Header(Variable)]
    [KeywordEnum(VARIABLE_LENGTH, FIXED_LENGTH)] 
    _RAY("Ray Method", Float) = 0
}

...
fixed4 frag(v2f i) : SV_Target
{
    ...
#ifdef _RAY_FIXED_LENGTH
    float dt = 1.0 / _Iteration;
    float time = 0.0;
    float3 localStep = localDir * dt;
#else
    float3 localStep = localDir * ray.tmax / _Iteration;
#endif
    float3 localPos = i.localPos;
    fixed4 output = 0.0;

    [loop]
    for (int i = 0; i < _Iteration; ++i)
    {
        ...
#ifdef _RAY_FIXED_LENGTH
        time += dt;
        if (time > ray.tmax) break;
#endif
    }
    ...
}
...

SubShader
{

...
Pass
{
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile _RAY_VARIABLE_LENGTH _RAY_FIXED_LENGTH
    ENDCG
}

}

}

固定長にしたところパフォーマンスは...、ほぼ前回と同じです orz

おわりに

あまり芳しい成果は得られませんでしたが、気を取り直して次は伝達関数による色付けやシェーディングを行います。

追記(2018/01/28)

続きを書きました。

tips.hecomi.com