koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

レイマーチングにおけるマーチングループのbreakについて

shaderlabでレイマーチングのシェーダーを書き,マーチングループの部分のアセンブリコードを確認して不満に思う点があった. 本記事ではマーチングその不満に思う点を解消する手法について述べる.

扱うシェーダーコード

この記事では下記のシェーダーコードを題材として取り上げる.

レイマーチングによる単純なスフィアトレーシングのshaderlabのコードであり,ForwardBaseパスのみサポートしている. ライティングは単純なLambert反射のみの計算であり,鏡面反射や環境光の計算は除外している. 出力アセンブリコードの増加を防ぐため,#pragma multi_compile_fog#pragma multi_compile_fwdbase は宣言していない.

Shader "koturn/RayMarching/LoopBreak"
{
    Properties
    {
        [IntRange]
        _MaxLoop ("Maximum loop count", Range(8, 1024)) = 128

        _MinRayLength ("Minimum length of the ray", Float) = 0.01
        _MaxRayLength ("Maximum length of the ray", Float) = 1000.0

        _MarchingFactor ("Marching Factor", Range(0.5, 1.0)) = 1.0

        _Color ("Color of the objects", Color) = (1.0, 1.0, 1.0, 1.0)

        [KeywordEnum(Post If Break, Post Flatten If Break, Pre If Break, Pre Flatten If Break, Use Loop Continuous, Use Index Update, Post Update Index)]
        _BreakMethod ("Break method of the marching loop", Int) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue" = "AlphaTest"
            "RenderType" = "Transparent"
            "DisableBatching" = "True"
            "IgnoreProjector" = "True"
            "VRCFallback" = "Hidden"
        }

        Cull Front

        CGINCLUDE
        #pragma target 3.0

        // keywords:
        //   FOG_LINEAR
        //   FOG_EXP
        //   FOG_EXP2
        // #pragma multi_compile_fog
        #pragma multi_compile_local_fragment _BREAKMETHOD_POST_IF_BREAK _BREAKMETHOD_POST_FLATTEN_IF_BREAK _BREAKMETHOD_PRE_IF_BREAK _BREAKMETHOD_PRE_FLATTEN_IF_BREAK _BREAKMETHOD_USE_LOOP_CONTINUOUS _BREAKMETHOD_USE_INDEX_UPDATE _BREAKMETHOD_POST_UPDATE_INDEX

        #include "UnityCG.cginc"
        #include "UnityStandardUtils.cginc"
        #include "AutoLight.cginc"


        /*!
         * @brief Input of the vertex shader, vert().
         */
        struct appdata
        {
            //! Local position of the vertex.
            float4 vertex : POSITION;
            //! Lightmap coordinate.
            float2 texcoord1 : TEXCOORD1;
        };

        /*!
         * @brief Output of the vertex shader, frag()
         * and input of fragment shader.
         */
        struct v2f
        {
            //! Clip space position of the vertex.
            float4 pos : SV_POSITION;
            //! Ray origin in object space (Camera position in object space).
            nointerpolation float3 localRayOrigin : TEXCOORD0;
            //! Unnormalized ray direction in object space.
            float3 localRayDirVector : TEXCOORD1;
            //! Lighting and shadowing parameters.
            UNITY_LIGHTING_COORDS(2, 3)
        };

        /*!
         * @brief Output of fragment shader.
         */
        struct fout
        {
            //! Output color of the pixel.
            half4 color : SV_Target;
            //! Depth of the pixel.
            float depth : SV_Depth;
        };


        float map(float3 p);
        float sdSphere(float3 p, float r);
        half4 calcLighting(half4 color, float3 worldPos, float3 worldNormal, half atten);
        float3 getNormal(float3 p);
        float getDepth(float4 projPos);


        #ifndef UNITY_LIGHTING_COMMON_INCLUDED
        fixed4 _LightColor0;
        #endif  // UNITY_LIGHTING_COMMON_INCLUDED

        //! Color of the objects.
        uniform half4 _Color;
        //! Maximum loop count.
        uniform int _MaxLoop;
        //! Minimum length of the ray.
        uniform float _MinRayLength;
        //! Maximum length of the ray.
        uniform float _MaxRayLength;
        //! Marching Factor.
        uniform float _MarchingFactor;


        /*!
         * @brief Vertex shader function for ForwardBase and ForwardAdd Pass.
         * @param [in] v  Input data
         * @return Output for fragment shader (v2f).
         */
        v2f vert(appdata v)
        {
            v2f o;
            UNITY_INITIALIZE_OUTPUT(v2f, o);

            o.pos = UnityObjectToClipPos(v.vertex);
            o.localRayOrigin = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1.0)).xyz;
            o.localRayDirVector = v.vertex - o.localRayOrigin;

            UNITY_TRANSFER_LIGHTING(o, v.texcoord1);

            return o;
        }

        /*!
         * @brief Fragment shader function.
         * @param [in] fi  Input data from vertex shader.
         * @return Output of each texels (fout).
         */
        fout frag(v2f fi)
        {
            const float3 ro = fi.localRayOrigin;
            const float3 rd = normalize(fi.localRayDirVector);

            float t = 0.0;

        #if defined(_BREAKMETHOD_POST_IF_BREAK)
            for (int i = 0; i < _MaxLoop; i++) {
                const float d = map(ro + rd * t);
                t += d * _MarchingFactor;

                if (d < _MinRayLength || t > _MaxRayLength) {
                    break;
                }
            }
        #elif defined(_BREAKMETHOD_POST_FLATTEN_IF_BREAK)
            for (int i = 0; i < _MaxLoop; i++) {
                const float d = map(ro + rd * t);
                t += d * _MarchingFactor;

                UNITY_FLATTEN
                if (d < _MinRayLength || t > _MaxRayLength) {
                    break;
                }
            }
        #elif defined(_BREAKMETHOD_PRE_IF_BREAK)
            float d = _MaxRayLength;
            for (int i = 0; i < _MaxLoop; i++) {
                if (d < _MinRayLength || t > _MaxRayLength) {
                    break;
                }

                d = map(ro + rd * t);
                t += d * _MarchingFactor;
            }
        #elif defined(_BREAKMETHOD_PRE_FLATTEN_IF_BREAK)
            float d = _MaxRayLength;
            for (int i = 0; i < _MaxLoop; i++) {
                UNITY_FLATTEN
                if (d < _MinRayLength || t > _MaxRayLength) {
                    break;
                }

                d = map(ro + rd * t);
                t += d * _MarchingFactor;
            }
        #elif defined(_BREAKMETHOD_USE_LOOP_CONTINUOUS)
            float d = _MaxRayLength;
            for (int i = 0; i < _MaxLoop && d >= _MinRayLength && t <= _MaxRayLength; i++) {
                d = map(ro + rd * t);
                t += d * _MarchingFactor;
            }
        #elif defined(_BREAKMETHOD_USE_INDEX_UPDATE)
            float d = _MaxRayLength;
            for (int i = 0; i < _MaxLoop; i = (d < _MinRayLength || t > _MaxRayLength) ? 0x7fffffff : i + 1) {
                d = map(ro + rd * t);
                t += d * _MarchingFactor;
            }
        #else  // defined(_BREAKMETHOD_POST_UPDATE_INDEX)
            for (int i = 0; i < _MaxLoop; i++) {
                const float d = map(ro + rd * t);
                t += d * _MarchingFactor;

                UNITY_FLATTEN
                if (d < _MinRayLength || t > _MaxRayLength) {
                    i = 0x7ffffffe;
                }
            }
        #endif
            clip(_MaxRayLength - t);

            const float3 localFinalPos = ro + rd * t;
            const float3 worldFinalPos = mul(unity_ObjectToWorld, float4(localFinalPos, 1.0).xyz);

            UNITY_LIGHT_ATTENUATION(atten, fi, worldFinalPos);

            half4 color = calcLighting(
                _Color,
                worldFinalPos,
                UnityObjectToWorldNormal(getNormal(localFinalPos)),
                atten);

            const float4 projPos = UnityWorldToClipPos(worldFinalPos);

            UNITY_APPLY_FOG(projPos.z, color);

            fout fo;
            UNITY_INITIALIZE_OUTPUT(fout, fo);
            fo.color = color;
            fo.depth = getDepth(projPos);

            return fo;
        }

        /*!
         * @brief SDF (Signed Distance Function) of objects.
         * @param [in] p  Position of the tip of the ray.
         * @return Signed Distance to the nearest object.
         */
        float map(float3 p)
        {
            return sdSphere(p, 0.5);
        }

        /*!
         * @brief SDF of Sphere.
         * @param [in] p  Position of the tip of the ray.
         * @param [in] r  Radius of sphere.
         * @return Signed Distance to the Sphere.
         */
        float sdSphere(float3 p, float r)
        {
            return length(p) - r;
        }

        /*!
         * Calculate lighting.
         * @param [in] color  Base color.
         * @param [in] worldPos  World coordinate.
         * @param [in] worldNormal  Normal in world space.
         * @param [in] atten  Light attenuation.
         * @return Color with lighting applied.
         */
        half4 calcLighting(half4 color, float3 worldPos, float3 worldNormal, half atten)
        {
            const float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
        #ifdef USING_DIRECTIONAL_LIGHT
            const float3 worldLightDir = UnityWorldSpaceLightDir(worldPos);
        #else
            const float3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
        #endif  // USING_DIRECTIONAL_LIGHT
            const fixed3 lightCol = _LightColor0.rgb * atten;

            // Lambertian reflectance.
            const float nDotL = dot(worldNormal, worldLightDir);
            const half3 diffuse = lightCol * pow(nDotL * 0.5 + 0.5, 2.0);

            const half4 outColor = half4(diffuse * _Color.rgb, _Color.a);

            return outColor;
        }

        /*!
         * @brief Calculate normal of the objects.
         *
         * @param [in] p  Position of the tip of the ray.
         * @return Normal of the objects.
         * @see https://iquilezles.org/articles/normalsSDF/
         */
        float3 getNormal(float3 p)
        {
            static const float h = 0.0001;
            static const float2 s = float2(1.0, -1.0);  // used only for generating k.
            static const float3 k[4] = {s.xyy, s.yxy, s.yyx, s.xxx};

            float3 normal = float3(0.0, 0.0, 0.0);

            UNITY_LOOP
            for (int i = 0; i < 4; i++) {
                normal += k[i] * map(p + h * k[i]);
            }

            return normalize(normal);
        }

        /*!
         * @brief Get depth from projected position.
         * @param [in] projPos  Projected position.
         * @return Depth value.
         */
        float getDepth(float4 projPos)
        {
            const float depth = projPos.z / projPos.w;
        #if defined(SHADER_API_GLCORE) \
            || defined(SHADER_API_OPENGL) \
            || defined(SHADER_API_GLES) \
            || defined(SHADER_API_GLES3)
            return depth * 0.5 + 0.5;
        #else
            return depth;
        #endif
        }
        ENDCG

        Pass
        {
            Name "FORWARD_BASE"
            Tags
            {
                "LightMode" = "ForwardBase"
            }

            Blend Off
            ZTest LEqual

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // keywords:
            //   DIRECTIONAL
            //   LIGHTMAP_ON
            //   DIRLIGHTMAP_COMBINED
            //   DYNAMICLIGHTMAP_ON
            //   LIGHTMAP_SHADOW_MIXING
            //   VERTEXLIGHT_ON
            //   LIGHTPROBE_SH
            // #pragma multi_compile_fwdbase
            ENDCG
        }  // ForwardBase
    }
}

目次

末尾if-break

まず最も典型的なマーチングループのシェーダーコードを示す. レイの衝突判定とレイの長さの上限の判定をループ末尾で行うコードと得られるDirect3D11のアセンブリコードである.

float t = 0.0;
for (int i = 0; i < _MaxLoop; i++) {
    const float d = map(ro + rd * t);
    t += d * _MarchingFactor;

    if (d < _MinRayLength || t > _MaxRayLength) {
        break;
    }
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_POST_IF_BREAK
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 6 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r0.w, l(0)
   4: mov r1.x, l(0)
   5: loop
   6:   ige r1.y, r1.x, cb0[4].x
   7:   breakc_nz r1.y
   8:   mad r1.yzw, r0.xxyz, r0.wwww, v1.xxyz
   9:   dp3 r1.y, r1.yzwy, r1.yzwy
  10:   sqrt r1.y, r1.y
  11:   add r1.y, r1.y, l(-0.500000)
  12:   mad r1.z, r1.y, cb0[4].w, r0.w
  13:   lt r1.y, r1.y, cb0[4].y
  14:   lt r1.w, cb0[4].z, r1.z
  15:   or r1.y, r1.w, r1.y
  16:   if_nz r1.y
  17:     mov r0.w, r1.z
  18:     break
  19:   endif
  20:   iadd r1.x, r1.x, l(1)
  21:   mov r0.w, r1.z
  22: endloop
  23: add r1.x, -r0.w, cb0[4].z
  24: lt r1.x, r1.x, l(0.000000)
  25: discard_nz r1.x

このアセンブリコードを見て思った問題点は下記の2つ.

  1. 16行目と21行目に同じ処理がある
    • 15行目と16行目の前で行ってもよいのでは?(元々のシェーダーコードがそうしているように)
  2. if ~ break ではなく,breakc_nz 命令を生成してほしい

末尾if-break (flatten)

前述の問題を解決できるかもしれないと思い,if文に [flatten] を指定した. 下記のコードでは UNITY_FLATTEN としているが,これは環境差吸収用のマクロで,HLSLコンパイラ向けなら [flatten] に置換される.

float t = 0.0;
for (int i = 0; i < _MaxLoop; i++) {
    const float d = map(ro + rd * t);
    t += d * _MarchingFactor;

    UNITY_FLATTEN
    if (d < _MinRayLength || t > _MaxRayLength) {
        break;
    }
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_POST_FLATTEN_IF_BREAK
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 5 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r0.w, l(0)
   4: mov r1.x, l(0)
   5: loop
   6:   ige r1.y, r1.x, cb0[4].x
   7:   breakc_nz r1.y
   8:   mad r1.yzw, r0.xxyz, r0.wwww, v1.xxyz
   9:   dp3 r1.y, r1.yzwy, r1.yzwy
  10:   sqrt r1.y, r1.y
  11:   add r1.y, r1.y, l(-0.500000)
  12:   mad r1.z, r1.y, cb0[4].w, r0.w
  13:   lt r1.y, r1.y, cb0[4].y
  14:   lt r1.w, cb0[4].z, r1.z
  15:   or r1.y, r1.w, r1.y
  16:   mov r0.w, r1.z
  17:   breakc_nz r1.y
  18:   iadd r1.x, r1.x, l(1)
  19:   mov r0.w, r1.z
  20: endloop
  21: add r1.x, -r0.w, cb0[4].z
  22: lt r1.x, r1.x, l(0.000000)
  23: discard_nz r1.x

breakc_nz 命令は生成されるようになったが,依然として冗長な mov 命令が生成される問題が残っている.

先頭if-break

そこでbreak判定を先頭に持っていくことにした. 初回は必ずfalseになるため,ループ末尾にif ~ breakを記述するのと意味は変わらない.

float d = _MaxRayLength;
for (int i = 0; i < _MaxLoop; i++) {
    if (d < _MinRayLength || t > _MaxRayLength) {
        break;
    }

    d = map(ro + rd * t);
    t += d * _MarchingFactor;
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_PRE_IF_BREAK
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 6 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r1.x, l(0)
   4: mov r1.y, cb0[4].z
   5: mov r0.w, l(0)
   6: loop
   7:   ige r1.z, r0.w, cb0[4].x
   8:   breakc_nz r1.z
   9:   lt r1.z, r1.y, cb0[4].y
  10:   lt r1.w, cb0[4].z, r1.x
  11:   or r1.z, r1.w, r1.z
  12:   if_nz r1.z
  13:     break
  14:   endif
  15:   mad r2.xyz, r0.xyzx, r1.xxxx, v1.xyzx
  16:   dp3 r1.z, r2.xyzx, r2.xyzx
  17:   sqrt r1.z, r1.z
  18:   add r1.y, r1.z, l(-0.500000)
  19:   mad r1.x, r1.y, cb0[4].w, r1.x
  20:   iadd r0.w, r0.w, l(1)
  21: endloop

if内の mov 命令が消えた! (これなら [flatten] 指定しなくても breakc_nz 命令を生成するぐらい気を利かせてもよいと思うが...)

先頭break (flatten)

[flatten] 指定をすることで breakc_nz 命令となった.

float d = _MaxRayLength;
for (int i = 0; i < _MaxLoop; i++) {
    UNITY_FLATTEN
    if (d < _MinRayLength || t > _MaxRayLength) {
        break;
    }

    d = map(ro + rd * t);
    t += d * _MarchingFactor;
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_PRE_FLATTEN_IF_BREAK
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 5 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r1.x, l(0)
   4: mov r1.y, cb0[4].z
   5: mov r0.w, l(0)
   6: loop
   7:   ige r1.z, r0.w, cb0[4].x
   8:   breakc_nz r1.z
   9:   lt r1.z, r1.y, cb0[4].y
  10:   lt r1.w, cb0[4].z, r1.x
  11:   or r1.z, r1.w, r1.z
  12:   breakc_nz r1.z
  13:   mad r2.xyz, r0.xyzx, r1.xxxx, v1.xyzx
  14:   dp3 r1.z, r2.xyzx, r2.xyzx
  15:   sqrt r1.z, r1.z
  16:   add r1.y, r1.z, l(-0.500000)
  17:   mad r1.x, r1.y, cb0[4].w, r1.x
  18:   iadd r0.w, r0.w, l(1)
  19: endloop
  20: add r0.w, -r1.x, cb0[4].z
  21: lt r0.w, r0.w, l(0.000000)
  22: discard_nz r0.w

かなり理想に近い形となったが,ループカウンタのための breakc_nz 命令と1つにまとめたい気持ちが出てくる.

forループ継続条件追加

for文の条件判定部分にbreak条件を折り込んだところ,breakc_nz 命令が1つにまとまった.

float d = _MaxLoop;
for (int i = 0; i < _MaxLoop && d >= _MinRayLength && t <= _MaxRayLength; i++) {
    d = map(ro + rd * t);
    t += d * _MarchingFactor;
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_USE_LOOP_CONTINUOUS
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 63 math, 4 temp registers, 1 textures, 4 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r1.x, l(0)
   4: mov r1.y, cb0[4].z
   5: mov r0.w, l(0)
   6: loop
   7:   ilt r1.z, r0.w, cb0[4].x
   8:   ge r1.w, r1.y, cb0[4].y
   9:   and r1.z, r1.w, r1.z
  10:   ge r1.w, cb0[4].z, r1.x
  11:   and r1.z, r1.w, r1.z
  12:   breakc_z r1.z
  13:   mad r2.xyz, r0.xyzx, r1.xxxx, v1.xyzx
  14:   dp3 r1.z, r2.xyzx, r2.xyzx
  15:   sqrt r1.z, r1.z
  16:   add r1.y, r1.z, l(-0.500000)
  17:   mad r1.x, r1.y, cb0[4].w, r1.x
  18:   iadd r0.w, r0.w, l(1)
  19: endloop
  20: add r0.w, -r1.x, cb0[4].z
  21: lt r0.w, r0.w, l(0.000000)
  22: discard_nz r0.w

欲を言うなら,先頭で判定するのは直感に反するので,末尾判断と同じようにしたいところである.

繰り返し時ループカウンタ操作

break条件を満たすとき,ループカウンタを大きな値にすることでbreakを実現するコードにしてみた. 0x7fffffffであればいかなる _MaxLoop の値に対しても i < _MaxLoop はfalseとなる. i = _MaxLoop としてもよかったのだが,後述の方法と足並を揃えるためと,即値命令の方が定数バッファへのアクセスよりよさそうだと根拠なく思ったためである.

float d = _MaxRayLength;
for (int i = 0; i < _MaxLoop; i = (d < _MinRayLength || t > _MaxRayLength) ? 0x7fffffff : i + 1) {
    d = map(ro + rd * t);
    t += d * _MarchingFactor;
}

// for (int i = 0; i < _MaxLoop; i = d < _MinRayLength ? 0x7fffffff : i++) {
//     const float d = map(ro + rd * t);
//     t += d * _MarchingFactor;
//     clip(_MaxRayLength - t);
// }
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_USE_INDEX_UPDATE
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 4 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r0.w, l(0)
   4: mov r1.x, l(0)
   5: loop
   6:   ige r1.y, r1.x, cb0[4].x
   7:   breakc_nz r1.y
   8:   mad r1.yzw, r0.xxyz, r0.wwww, v1.xxyz
   9:   dp3 r1.y, r1.yzwy, r1.yzwy
  10:   sqrt r1.y, r1.y
  11:   add r1.y, r1.y, l(-0.500000)
  12:   mad r0.w, r1.y, cb0[4].w, r0.w
  13:   lt r1.y, r1.y, cb0[4].y
  14:   lt r1.z, cb0[4].z, r0.w
  15:   or r1.y, r1.z, r1.y
  16:   iadd r1.z, r1.x, l(1)
  17:   movc r1.x, r1.y, l(0x7fffffff), r1.z
  18: endloop
  19: add r1.x, -r0.w, cb0[4].z
  20: lt r1.x, r1.x, l(0.000000)
  21: discard_nz r1.x

ループカウンタの条件判断レジスタと別レジスタmovc 命令の判定を行うコードのため,命令数が1つ少なくなった.

末尾ループカウンタ操作

for文の継続条件部分,更新部分にはループカウンタに関するもの以外書きたくない気持ちがある. そこで,下記のようにifの部分でループカウンタをいじることにしてみる. _MaxLoop - 1 の設定でよいのだが,1の減算命令が生成されてしまうかもしれないため,int型の最大値 - 1を設定することにしている. (実際には _MaxLoop - 1 でも減算命令は生成されなかった)

for (int i = 0; i < _MaxLoop; i++) {
    const float d = map(ro + rd * t);
    t += d * _MarchingFactor;

    UNITY_FLATTEN
    if (d < _MinRayLength || t > _MaxRayLength) {
        i = 0x7ffffffe;
    }
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_POST_UPDATE_INDEX
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 4 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r0.w, l(0)
   4: mov r1.x, l(0)
   5: loop
   6:   ige r1.y, r1.x, cb0[4].x
   7:   breakc_nz r1.y
   8:   mad r1.yzw, r0.xxyz, r0.wwww, v1.xxyz
   9:   dp3 r1.y, r1.yzwy, r1.yzwy
  10:   sqrt r1.y, r1.y
  11:   add r1.y, r1.y, l(-0.500000)
  12:   mad r0.w, r1.y, cb0[4].w, r0.w
  13:   lt r1.y, r1.y, cb0[4].y
  14:   lt r1.z, cb0[4].z, r0.w
  15:   or r1.y, r1.z, r1.y
  16:   iadd r1.z, r1.x, l(1)
  17:   movc r1.x, r1.y, l(0x7fffffff), r1.z
  18: endloop
  19: add r1.x, -r0.w, cb0[4].z
  20: lt r1.x, r1.x, l(0.000000)
  21: discard_nz r1.x

繰り返し時ループカウンタ操作と同じコードとなった. 可読性を重んじるならこのコードにすべきかもしれない. あと,d の宣言をループ内に持ってこれる点も大きい.

まとめ

HLSLコンパイラはループ内のbreakの扱いが下手であり,特に末尾にif ~ breakを書いた場合,かなり冗長なコードを生成することがわかった.

本記事で示した各手法について,アセンブリ中のmathとbranch数を示すと下記の通り. 基本的にこの値が小さいほどよいコードであると言えると思う.

手法 math branches
末尾if-break 62 6
末尾if-break (flatten) 62 5
先頭if-break 62 6
先頭break (flatten) 62 5
forループ継続条件追加 63 4
繰り返し時ループカウンタ操作 62 4
末尾ループカウンタ操作 62 4

branchは if ~ endif で1つ, breakc_nz 命令で1つ計上されているようだ. この値から繰り返し時ループカウンタ操作,または末尾ループカウンタ操作の出力アセンブリが良いアセンブリと言えるのではないだろうか?