Unity6 + URP で動くパストレーサーを実装してみよう Part 2

Unity6 + URP で動くパストレーサーを実装してみよう Part 2

はじめに

こんにちは。シリコンスタジオ 研究開発室の川口です。
前回 Unity6 + URP で動作するレイトレーシングのカスタムレンダーパスを実装しました。
今回はその続きとしてパストレーサーを実装するための準備を進めていきましょう。

今回はレイトレシェーダで行う URP 相当のライティングとレイトレを使った影を実装していきます。
まだパストレーシング本体の実装に入れないのですが、影は簡単なレイトレとして良い題材なので紹介していきます。

対象読者:Unity、URP に触ったことのある方・GPU レイトレーシングの実装に興味のある方

Unity6 + URP で動くパストレーサーを実装してみよう Part 2

さっそく実装していきます。
実装は前回の内容をそのまま引き継いでいます。

執筆時点の環境(2025年2月)

  • Unity 6 (6000.0.27f1)
  • URP 17.0.3

ライティングの実装

前回は Ray Generation シェーダからレイを飛ばして、 Closest Hit シェーダで当たったマテリアルのベースマップを表示するところまで実装しました。
次は URP と同等のライティングを、 URP にある既存の処理を活用して実装してみます。

ライティングの計算はパストレーシングで絵を作る上での一番の肝と言えますが、その中には PBR の理論など難しい内容をたくさん含んでいます。
その難しい理論のいくつかは URP 上に実装されています。
なので URP への理解も深めながら使えるものは流用してパストレーサーを作っていきましょう。

Lit シェーダの再現

前回 Closest Hit シェーダの実装を URP の Lit.shader から持ってきました(MyPathTracerLit.shader)。
ライティングの計算も Lit.shader を移植する形で実装していきましょう。
Lit.shader のライティング本体の実装は LitForwardPass.shader にあるのでそれを MyPathTracerHitShader.hlsl に実装します。

パス:MyPathTracer\Library\PackageCache\com.unity.render-pipelines.universal\Shaders\LitForwardPass.shader

Lit パスのラスタライズ工程で Vertex シェーダ(LitPassVertex)と Fragment シェーダ(LitPassFragment)に分かれている内容を、ここではでは Closest Hit シェーダ(ClosestHitMain)一つに実装していきます。
頂点回りは前回実装したものを使うだけなので基本的に LitPassFragment の内容を移植していく作業になります。
今回はステレオレンダリングやインスタンシングなどラスタライズ特有の特殊なものは除外して、基本的な PBR のライティング処理だけを扱います。

LitForwardPass.shaderLitPassFragment の中で基本的な処理を抜き出して、MyPathTracerHitShader.hlslClosestHitMain に実装します。
Vertex シェーダで行われていた UV の変形処理も入れています。

// テクスチャ座標の変形(元は LitPassVertex の処理)
v.texCoord0 = TRANSFORM_TEX(v.texCoord0, _BaseMap);

SurfaceData surfaceData;
InitializeStandardLitSurfaceData(v.texCoord0, surfaceData);

InputData inputData;
InitializeInputData(v, surfaceData.normalTS, inputData);

float4 directLighting = UniversalFragmentPBR(inputData, surfaceData);

payload.hit = true;
payload.radiance = directLighting.rgb;

v は前回用意した Vertex で、レイが当たった位置で補間された頂点属性情報を保持しています。
URP のライティング計算は、その表面上の情報 SurfaceData と頂点関連の情報 InputData を利用します。
なお InitializeStandardLitSurfaceData の引数を LitPassFragment では input.uv だったところから input.texCoord0 に変更しています。

LitPassFragment から色々と処理を省いていますが、最低限のライティング処理はこれだけです。

URP のライティング計算をレイトレで動かす

この directLighting.rgb を使えば、シーン中のマテリアルとライトが反映された絵が得られるはずで、そのまま動いたら嬉しいのですがそうはいきません。
しっかり動くように実装を追加していきましょう。

SurfaceData

まず SurfaceData を正しく設定できるようにします。

SurfaceDataSurfaceData.hlsl で定義されています。
PBR のライティング計算に使う各種データを保持していて、マテリアルに設定されたテクスチャなどのパラメータを InitializeStandardLitSurfaceData 関数の中でこの構造体に展開しています。

InitializeStandardLitSurfaceDataLitInput.hlsl で定義されていて、その中身を見ると、例えば以下のようにマテリアルに設定されているテクスチャや定数値からライティングに使うパラメータが計算されています。

half4 albedoAlpha = SampleAlbedoAlpha(uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap));

SampleAlbedoAlpha をさらに辿っていくと SAMPLE_TEXTURE2DPLATFORM_SAMPLE_TEXTURE2D、そして DirectX なら textureName.Sample(samplerName, coord2) と、最終的にプラットフォーム毎のテクスチャサンプリングの関数にたどり着きます。
com.unity.render-pipelines.core パッケージにたどり着きます。)
今問題なのは SAMPLE_TEXTURE2D がレイトレシェーダで使えないということです。

SAMPLE_TEXTURE2D はテクスチャ LOD(ミップマップ)を自動で計算してくれるラスタライズ工程専用の機能です。
Compute シェーダと同様に、レイトレシェーダは微分オペレーション(ddx, ddy)が使えないため、ラスタライズと同じ方法でミップマップを計算することができません。
(新しい Shader Model の Compute シェーダでは微分オペレーションを使えるらしいのですが、反射等で隣接ピクセルの関係が薄くなるレイトレーシングにおいては使えたとしても有効に活用できるとはかぎらないでしょう)

今回は以下のようにして、単純に SAMPLE_TEXTURE2DSAMPLE_TEXTURE2D_LOD に置き換えて動作するようにしました。

// 先に SAMPLE_TEXTURE2D を定義するインクルードをしておく
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

// 現状は RayTracing シェーダでミップマップは使えないので、
// SAMPLE_TEXTURE2D を使ったときに Mip 0 に強制するように定義を置き換える
#ifdef SAMPLE_TEXTURE2D
    #undef SAMPLE_TEXTURE2D
    #define SAMPLE_TEXTURE2D(textureName, samplerName, coord) \
        SAMPLE_TEXTURE2D_LOD(textureName, samplerName, coord, 0)
#endif

#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

MyPathTracerHitShader.hlsl の先頭で include している箇所を変更します。

まず SAMPLE_TEXTURE2D を定義している Core.hlsl を include します。
Core.hlslLitInput.hlsl の中でも include されているのですが、今回 SAMPLE_TEXTURE2D の定義の上書きをするために単独で先に読み込んでおきます。
そして #undef で定義を消して、改めて SAMPLE_TEXTURE2DSAMPLE_TEXTURE2D_LOD で定義し直します。
このとき SAMPLE_TEXTURE2D_LOD の第4引数は 0 固定としました。

これにより以降 LitInput.hlsl の中で行われるテクスチャのサンプリングは全て Mip Level 0 に強制されて、レイトレシェーダでも動作するようになります。

余談

ところでミップマップはエイリアシングを避けたり処理負荷を減らしたりするための大切な技術で、何も考えずに Mip Level 0 に強制することが良いこととは言えません。
レイトレーシングでミップマップも扱うための理論ももちろんあります。
Rey Tracing Gems には Ray Differentials という考えを使った方法が紹介されています。
(PDF注意) Ray Tracing Gems, PART 5: DENOISING AND FILTERING, 20. Texture Level of Detail Strategies for Real-Time Ray Tracing

これを実装するのであれば、InitializeStandardLitSurfaceData の中身を全て自前で再実装したり Payload を再設計したり色々と追加の作業が必要になるでしょう。

InputData

次に InputData を正しく設定できるようにします。

InputDataInput.hlsl で定義されています。
主に頂点に関連するデータを持っています。

ラスタライズでライティング計算を行う Fragment シェーダから見た Vertex シェーダからの入力という意味での InputData なので、レイトレで同じ名前を扱うとちょっと意味がズレる気もしますがそのまま使っています。

InputDataInput.hlsl で定義されていますが、それを設定する InitializeInputData 関数はパス毎に実装が異なります。
LitForwardPass.hlsl, PBRGBufferPass.hlsl などそれぞれ Fragment シェーダのエントリポイントと同じファイルで定義されています)

なのでレイトレに必要な InputData を設定する InitializeInputData 関数を MyPathTracerHitShader.hlsl の中に定義しましょう。

関数内部で使う BuildTangentToWorld のために ShaderGraphFunctions.hlsl を追加でインクルードしています。
Tangent と World の相互変換をレイトレはよく使うので、ラスタライズではノーマルマップの計算だけに使っていた inputData.tangentToWorld に関する部分をマクロの分岐の外に出しています。

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl"

// 中略

// from : LitForwardPass.hlsl
void InitializeInputData(Vertex input, half3 normalTS, out InputData inputData)
{
    inputData.positionWS = WorldRayOrigin() + RayTCurrent() * WorldRayDirection();
    inputData.positionCS = 0;
    inputData.viewDirectionWS = WorldRayDirection();
    inputData.shadowCoord = 0;
    inputData.fogCoord = 0;
    inputData.vertexLighting = 0;
    inputData.bakedGI = 0;
    inputData.shadowMask = 0;
    inputData.normalizedScreenSpaceUV = DispatchRaysIndex().xy / DispatchRaysDimensions().xy;

    // レイトレ用の変換行列を使ってワールド法線と接線を解決する
    float3 normalWS = normalize(mul(input.normal, (float3x3)WorldToObject3x4()));

    // Tangent はあるものとして計算(レイトレではノーマルマップの計算以外にも必須)
    float4 tangentWS = float4(normalize(mul(input.tangent.xyz, (float3x3)WorldToObject3x4())), input.tangent.w);
    inputData.tangentToWorld = BuildTangentToWorld(tangentWS, normalWS); // ShaderGraphFuction.hlsl

    #if defined(_NORMALMAP) || defined(_DETAIL)
        inputData.normalWS = TransformTangentToWorld(normalTS, inputData.tangentToWorld); // SpaceTransforms.hlsl
    #else
        inputData.normalWS = normalWS;
    #endif
}

基本的には LitForwardPass.hlsl の実装と変わりません。
ラスタライズでは座標変換で得られる情報をレイトレシェーダで使えるシステム命令を使って書き換えています。
システム命令について詳しくは DXR の仕様を確認してみてください。

ノーマルマップについてのシェーダバリアントの記述も LitForwardPass.hlsl から持ってきています。
今は何も設定していないので正しく動きませんが後で対応します。

LitForwardPass.hlsl では inputData に影や GI のための情報も設定しています。
それらをレイトレシェーダ内で利用することもできますが、基本的にラスタライズ(リアルタイムレンダリング)用の情報で疑似的な表現です。
影も GI もパストレーシングではより正しく計算できるはずなのでここでは設定しません。

UniversalFragmentPBR

最後にライティングの計算を実装します。
UniversalFragmentPBR 関数が URP における PBR のライティング処理です。

さすがにラスタライズ用のライティング処理すべてをそのままレイトレで使うのは難しそうなので、専用の処理を実装しましょう。

Lighting.hlsl で定義されている UniversalFragmentPBRMyPathTracerHitShader.hlsl にコピーしてきて、MyUniversalFragmentPBR とします。
(全く Fragment の処理ではないですがどこから持ってきたのかわかりやすく残しています)。

// from : Lighting.hlsl
half4 MyUniversalFragmentPBR(InputData inputData, SurfaceData surfaceData)
{
    #if defined(_SPECULARHIGHLIGHTS_OFF)
    bool specularHighlightsOff = true;
    #else
    bool specularHighlightsOff = false;
    #endif
    BRDFData brdfData;

    // NOTE: can modify "surfaceData"...
    InitializeBRDFData(surfaceData, brdfData);

    // Clear-coat calculation...
    BRDFData brdfDataClearCoat = CreateClearCoatBRDFData(surfaceData, brdfData);
    uint meshRenderingLayers = GetMeshRenderingLayer();
    Light mainLight = GetMainLight();

    LightingData lightingData = CreateLightingData(inputData, surfaceData);
    lightingData.giColor = 0; // GI はパストレで解決する

#ifdef _LIGHT_LAYERS
    if (IsMatchingLightLayer(mainLight.layerMask, meshRenderingLayers))
#endif
    {
        lightingData.mainLightColor = LightingPhysicallyBased(brdfData, brdfDataClearCoat,
                                                              mainLight,
                                                              inputData.normalWS, inputData.viewDirectionWS,
                                                              surfaceData.clearCoatMask, specularHighlightsOff);
    }

    #if defined(_ADDITIONAL_LIGHTS)
    uint pixelLightCount = GetAdditionalLightsCount();

    #if USE_FORWARD_PLUS
    [loop] for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)
    {
        FORWARD_PLUS_SUBTRACTIVE_LIGHT_CHECK

        Light light = GetAdditionalLight(lightIndex, inputData.positionWS);

        #ifdef _LIGHT_LAYERS
            if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
        #endif
        {
            lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light,
                                                                          inputData.normalWS, inputData.viewDirectionWS,
                                                                          surfaceData.clearCoatMask, specularHighlightsOff);
        }
    }
    #endif // USE_FORWARD_PLUS

    LIGHT_LOOP_BEGIN(pixelLightCount)
        Light light = GetAdditionalLight(lightIndex, inputData.positionWS);

        #ifdef _LIGHT_LAYERS
            if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
        #endif
        {
            lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light,
                                                                          inputData.normalWS, inputData.viewDirectionWS,
                                                                          surfaceData.clearCoatMask, specularHighlightsOff);
        }
    LIGHT_LOOP_END
    #endif // _ADDITIONAL_LIGHTS

    #if REAL_IS_HALF
        // Clamp any half.inf+ to HALF_MAX
        return min(CalculateFinalColor(lightingData, surfaceData.alpha), HALF_MAX);
    #else
        return CalculateFinalColor(lightingData, surfaceData.alpha);
    #endif
}

ClosestHitMain 内で使う部分も書き換えましょう。

SurfaceData surfaceData;
InitializeStandardLitSurfaceData(v.texCoord0, surfaceData);

InputData inputData;
InitializeInputData(v, surfaceData.normalTS, inputData);

float4 directLighting = MyUniversalFragmentPBR(inputData, surfaceData);

payload.hit = true;
payload.radiance = directLighting.rgb;

MyUniversalFragmentPBR の実装はシェーダバリアント含めておおよその処理を UniversalFragmentPBR そのままを持ってきています。
主な変更点は以下になります。

  • DEBUG_DISPLAY 関係を削除
  • shadowMaskaoFactor を削除
  • GetMainLightGetAdditionalLightshadowMaskaoFactor を扱わないものに変更
  • GI 関係の計算(MixRealtimeAndBakedGI, GlobalIllumination)を削除して、giColor0 に設定
  • 頂点ライティング関係を削除

この実装にはポイントライトとスポットライトの対応も含まれているのですが、動作は未検証です。
Forward+ のときレイトレではライトのカリングが正しく動作していないようです。
可能なら次回以降修正も行いたいです。

シェーダバリアントの対応

マテリアルのパラメータ等でシェーダの中身をマクロで切り替える仕組み、シェーダバリアントに対応しましょう。
MyPathTracerLit.shaderMyPathTracing パスにバリアントの宣言を記述します。

バリアントは元の Lit.shaderForwardLit パスからコピーして書き換えています。

Pass
{
    Name "MyPathTracing"
    Tags{ "LightMode" = "MyPathTracing" }

    HLSLPROGRAM

    #pragma raytracing surface_shader

    // -------------------------------------
    // Material Keywords
    #pragma shader_feature_local _NORMALMAP
    #pragma shader_feature_local _PARALLAXMAP
    #pragma shader_feature_local _RECEIVE_SHADOWS_OFF
    #pragma shader_feature_local _ _DETAIL_MULX2 _DETAIL_SCALED
    // Any Hit で対応
    // #pragma shader_feature_local_raytracing _SURFACE_TYPE_TRANSPARENT
    // #pragma shader_feature_local_raytracing _ALPHATEST_ON
    // #pragma shader_feature_local_raytracing _ _ALPHAPREMULTIPLY_ON _ALPHAMODULATE_ON
    #pragma shader_feature_local_raytracing _EMISSION
    #pragma shader_feature_local_raytracing _METALLICSPECGLOSSMAP
    #pragma shader_feature_local_raytracing _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
    #pragma shader_feature_local_raytracing _OCCLUSIONMAP
    #pragma shader_feature_local_raytracing _SPECULARHIGHLIGHTS_OFF
    #pragma shader_feature_local_raytracing _ENVIRONMENTREFLECTIONS_OFF
    #pragma shader_feature_local_raytracing _SPECULAR_SETUP

    // -------------------------------------
    // Universal Pipeline keywords
    #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
    #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
    #pragma multi_compile _ EVALUATE_SH_MIXED EVALUATE_SH_VERTEX
    #pragma multi_compile_raytracing _ _ADDITIONAL_LIGHT_SHADOWS
    #pragma multi_compile_raytracing _ _REFLECTION_PROBE_BLENDING
    #pragma multi_compile_raytracing _ _REFLECTION_PROBE_BOX_PROJECTION
    #pragma multi_compile_raytracing _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
    #pragma multi_compile_raytracing _ _SCREEN_SPACE_OCCLUSION
    #pragma multi_compile_raytracing _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3
    #pragma multi_compile_raytracing _ _LIGHT_COOKIES
    #pragma multi_compile _ _LIGHT_LAYERS
    #pragma multi_compile _ _FORWARD_PLUS

    #include "MyPathTracerHitShader.hlsl"

    ENDHLSL
}

おそらく不要な設定や動かない機能もたくさん含んでいるのですが、一つ一つ必要かどうかを調べる時間がなかったので、基本的にそのまま持ってきています。
今回 Transparent や Alpha Test に関するもはコメントアウトしています(Any Hit シェーダに対応するときに考えましょう)。
shader_feature_local_fragmentmulti_compile_fragmentfragmentraytracing に直しています。

結果

これで全てエラーなく動くのであれば以下のような絵が得られるはずです。
確かにライティングが得られていそうですが、まだまだイマイチな絵です。
単なる直接光のみのライティングだとこんなものでしょう。

シーンの軽い修正

今のシーンにちょっと変なカクカクした影のようなものが出ていると思います。

これは半透明オブジェクトをデカールとして置いているものと影専用オブジェクトです。
アート的な意図があってシーンに配置されているものを消すのは忍びないのですが、レイトレで対応するにはかなり冗長になりそうなのでデカールは無効化してしまいます。
影専用オブジェクトも消しても良いですが、そこまで気にならず存在に後から気がついたので残しています。

シーンを decal で検索して MeshDecal と名前の付いているオブジェクトを選択、インスペクタのチェックボックスを切って無効化しできます。

レイトレの結果がちょっと綺麗になりました。

影の対応

今は直接光しかないのでレイトレを使って影を描いてみましょう。
元の UniversalFragmentPBR では LightshadowAttenuation に影の遮蔽の値が入ります。
今回は MyUniversalFragmentPBR の中で影用のレイを飛ばして shadowAttenuation に値を設定します。

Closest Hit シェーダから影のレイを飛ばす用意をします。

最大再帰深度

まず、MyPathTracingShader.raytrace で設定されているレイの最大再帰深度を変えます。

#pragma max_recursion_depth 2

ハードウェアレイトレーシングではレイの再帰深度(TraceRay で呼ばれたシェーダからさらに TraceRay を呼ぶ回数)に制限があります。
DXR では深度は0~31回に設定できます。
最初から最大の深度を設定しておけばいいように思うかもしれませんが、再帰深度はできる限り小さく設定した方がパフォーマンス的に優位とされています。
(事前に確保しておく再帰用のレジスタが節約できるようです。)

今回はレイが当たった面から光源方向にレイを飛ばすだけだと分かっているので最大深度は 2 になります。

シャドウレイ

影用のレイは当たったかどうかだけの判定でいいので専用のレイを用意します。
Payload.hlsl に影用のレイの Payload を用意します。

struct ShadowPayload
{
    bool hit;
};

Miss シェーダも影用のものを MyPathTracingShader.raytrace に用意します。
シェーダが呼ばれたら hit フラグを false にするだけの処理です。

[shader("miss")]
void ShadowMissShader(inout ShadowPayload payload : SV_RayPayload)
{
    payload.hit = false;
}

今 Miss シェーダは空用と影用があります。
これは Unity で .raytace ファイルを選択するとインスペクタ上に情報が表示されます。
ここにあるインデックスは利用するの確認しておきましょう。

シャドウレイを飛ばす

影用のレイを飛ばす関数 TraceShadowMyPathTracerHitShader.hlsl に用意します。

float TraceShadow(float3 lightDirection, float3 normalWS, float3 positionWS)
{
    RayDesc ray;
    ray.Origin = positionWS + 1e-6 * normalWS;
    ray.Direction = lightDirection;

    // シーンスケールに合わせて適切に設定すべき
    ray.TMin = 0.1;
    ray.TMax = 100;

    ShadowPayload payload = (ShadowPayload)0;
    payload.hit = true;
    TraceRay(_Scene, RAY_FLAG_SKIP_CLOSEST_HIT_SHADER | RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH,
             0xFF, 0, 1, 1 /* ShadowMissShader */, ray, payload);
    return (float)!payload.hit;
}

光源方向とヒットした位置と法線を使って影用のレイを飛ばします。
TraceRay の第6引数が先ほど確認した Miss シェーダのインデックスです。

法線は自己交差を避けるためにレイの発射位置を少しオフセットするために使います。
シーンスケールや形状によってはうまくいかないので調整が必要です。
ray.TMin も同様の役割で、ここでは適当に設定しています。

TraceRay をする際先ほど作った ShadowPayload を使います。

影は光源方向にレイを飛ばして遮蔽物があるかどうかで判定できます。
このとき、何に、どこで、レイがヒットしたのかは(ガラスなどの半透明影を扱わなければ)考える必要がありません。
なので payload.hittrue で初期化して、Miss シェーダが呼ばれたときだけ hit = false が設定されて帰ってくるようにします。

ここで TraceRay の第2引数は飛ばすレイの種類を指定する RAY_FLAG です。

今設定している RAY_FLAG_SKIP_CLOSEST_HIT_SHADER フラグは、レイが何かにヒットしても Closest Hit シェーダは呼ばないというものです。
影は Miss シェーダのヒット判定だけあれば良いのでこのフラグを使います。

RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH フラグは、レイが何かしらにヒットしたらそこで衝突の探索を終了するというものです。
レイトレの衝突判定は必ずしもレイの発射位置に近いものから行われるわけではありません。
ライティングなどの最近傍の衝突位置が必要な場合は、ぶつかる可能性のある全てのジオメトリの探索を行いますが、影は位置に関わらす遮蔽されているかどうかだけ分かれば良いのでこのフラグを使います。

影のレイトレはこれらのフラグを使うことで効率的に行うことができます。

この TraceShadow 関数を MyUniversalFragmentPBR の中で利用します。

Light mainLight = GetMainLight();
mainLight.shadowAttenuation = TraceShadow(mainLight.direction, inputData.normalWS, inputData.positionWS);

正しく動けば以下のようなぱっきりとした影のある絵が得られます。
ソフトシャドウの対応は確率的なサンプリングが必要になるので、今は扱いません。

余談1

自己交差の問題について、ローポリなサーフェスで発生しやすい、Shadow terminator problem という影のアーティファクトがあります。
Ray Tracing Gems II ではその一つの解決策が紹介されています。
(PDF注意) Ray Tracing Gems II, PART I: Ray Tracing Foundations, 4. Hacking the Shadow Terminator

簡単に実装出来て、シンプルな球で試すと効果が分かりやすいので取り入れてみても良いかもしれません。

余談2

本当はポイントライトやスポットライトの影にも対応したいのですが、GetAdditionalLight で得られる Light 構造体が持つ情報が影の処理には不十分で、対応するにはそれらを拡張する必要があり記事に載せるには冗長なので今回は省略します。

まとめ

これで Unity6 の URP と互換性のあるライティングと、レイトレ影を構築できました。
今は直接光だけなので元の URP のレンダリング結果と比べてもきれいな絵になっているとは言えません。
次回からパストレーシングの実装に移っていきます。

ソースコード

今回作ったソースコードの全文を載せます。
MyPathTracerLit.shaderPayload.hlsl、C# スクリプトは割愛します。

▼MyPathTracingShader.raytrace 全文

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityInput.hlsl"

#include "Payload.hlsl"

#pragma max_recursion_depth 2

RaytracingAccelerationStructure _Scene;
RWTexture2D<float4> _Result;

// https://github.com/boksajak/referencePT/blob/master/shaders/PathTracer.hlsl
RayDesc generatePinholeCameraRay(float2 pixel)
{
    float4x4 view = unity_WorldToCamera;
    float4x4 proj = unity_CameraProjection;

    // Setup the ray
    RayDesc ray;
    ray.Origin = _WorldSpaceCameraPos;
    ray.TMin = T_MIN;
    ray.TMax = T_MAX;

    // Extract the aspect ratio and field of view from the projection matrix
    float aspect = proj[1][1] / proj [0][0];
    float tanHalfFovY = 1.0f / proj [1][1];

    // Compute the ray direction for this pixel
    ray.Direction = normalize(
        (pixel.x * view[0].xyz * tanHalfFovY * aspect) -
        (pixel.y * view[1].xyz * tanHalfFovY) + view[2].xyz);

    return ray;
}

[shader("raygeneration")]
void MyRaygenShader()
{
    uint2 dispatchIdx = DispatchRaysIndex().xy;
    uint2 dispatchSize = DispatchRaysDimensions().xy;

    // Create the ray descriptor for this pixel
    float2 clipPixel = (dispatchIdx + 0.5) / (float2)dispatchSize;
    clipPixel.y = 1.0 - clipPixel.y;
    clipPixel = clipPixel * 2.0 - 1.0;
    RayDesc ray = generatePinholeCameraRay(clipPixel);

    RayPayload payload = (RayPayload)0;

    TraceRay(_Scene, 0, 0xFF, 0, 1, 0, ray, payload);

    _Result[dispatchIdx] = float4(payload.radiance, 1.0);
}

[shader("miss")]
void MyMissShader(inout RayPayload payload : SV_RayPayload)
{
    float t = WorldRayDirection().y * 0.9 + 0.1;
    t = saturate(pow(t, 0.2));
    payload.radiance = lerp(float3(0.5, 0.5, 0.7), float3(0.1, 0.3, 0.8), t);
    payload.hit = false;
}

[shader("miss")]
void ShadowMissShader(inout ShadowPayload payload : SV_RayPayload)
{
    payload.hit = false;
}

▼MyPathTracerHitShader.hlsl 全文

// 先に SAMPLE_TEXTURE2D を定義するインクルードをしておく
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

// 現状は RayTracing シェーダでミップマップは使えないので、
// SAMPLE_TEXTURE2D を使ったときに Mip0 に強制するように定義を置き換える
#ifdef SAMPLE_TEXTURE2D
    #undef SAMPLE_TEXTURE2D
    #define SAMPLE_TEXTURE2D(textureName, samplerName, coord) \
        SAMPLE_TEXTURE2D_LOD(textureName, samplerName, coord, 0)
#endif

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/ImageBasedLighting.hlsl"

#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl"

#include "UnityRaytracingMeshUtils.cginc"

#include "Payload.hlsl"

#define M_PI    3.14159265
#define EPSILON 1e-12

RaytracingAccelerationStructure _Scene;

struct AttributeData
{
    float2 barycentrics;
};

struct Vertex
{
    float3 position;
    float3 normal;
    float4 tangent;
    float2 texCoord0;
    float2 texCoord1;
    float2 texCoord2;
    float2 texCoord3;
    float4 color;
};

// Unity ビルトイン関数を使って頂点データを得る関数
Vertex FetchVertex(uint vertexIndex)
{
    Vertex v;
    v.position = UnityRayTracingFetchVertexAttribute3(vertexIndex, kVertexAttributePosition);
    v.normal = UnityRayTracingFetchVertexAttribute3(vertexIndex, kVertexAttributeNormal);
    v.tangent = UnityRayTracingFetchVertexAttribute4(vertexIndex, kVertexAttributeTangent);
    v.texCoord0 = UnityRayTracingFetchVertexAttribute2(vertexIndex, kVertexAttributeTexCoord0);
    v.texCoord1 = UnityRayTracingFetchVertexAttribute2(vertexIndex, kVertexAttributeTexCoord1);
    v.texCoord2 = UnityRayTracingFetchVertexAttribute2(vertexIndex, kVertexAttributeTexCoord2);
    v.texCoord3 = UnityRayTracingFetchVertexAttribute2(vertexIndex, kVertexAttributeTexCoord3);
    v.color = UnityRayTracingFetchVertexAttribute4(vertexIndex, kVertexAttributeColor);
    return v;
}

// レイトレのヒットした位置から頂点データを補間する関数
Vertex InterpolateVertices(Vertex v0, Vertex v1, Vertex v2, float3 barycentrics)
{
    Vertex v;
    #define INTERPOLATE_ATTRIBUTE(attr) v.attr = v0.attr * barycentrics.x + v1.attr * barycentrics.y + v2.attr * barycentrics.z
    INTERPOLATE_ATTRIBUTE(position);
    INTERPOLATE_ATTRIBUTE(normal);
    INTERPOLATE_ATTRIBUTE(tangent);
    INTERPOLATE_ATTRIBUTE(texCoord0);
    INTERPOLATE_ATTRIBUTE(texCoord1);
    INTERPOLATE_ATTRIBUTE(texCoord2);
    INTERPOLATE_ATTRIBUTE(texCoord3);
    INTERPOLATE_ATTRIBUTE(color);
    return v;
}

// from : LitForwardPass.hlsl
void InitializeInputData(Vertex input, half3 normalTS, out InputData inputData)
{
    inputData.positionWS = WorldRayOrigin() + RayTCurrent() * WorldRayDirection();
    inputData.positionCS = 0;
    inputData.viewDirectionWS = WorldRayDirection();
    inputData.shadowCoord = 0;
    inputData.fogCoord = 0;
    inputData.vertexLighting = 0;
    inputData.bakedGI = 0;
    inputData.shadowMask = 0;
    inputData.normalizedScreenSpaceUV = DispatchRaysIndex().xy / DispatchRaysDimensions().xy;

    // レイトレ用の変換行列を使ってワールド法線と接線を解決する
    float3 normalWS = normalize(mul(input.normal, (float3x3)WorldToObject3x4()));

    // Tangent はあるものとして計算(レイトレではノーマルマップの計算以外にも必須)
    float4 tangentWS = float4(normalize(mul(input.tangent.xyz, (float3x3)WorldToObject3x4())), input.tangent.w);
    inputData.tangentToWorld = BuildTangentToWorld(tangentWS, normalWS); // ShaderGraphFuction.hlsl

    #if defined(_NORMALMAP) || defined(_DETAIL)
        inputData.normalWS = TransformTangentToWorld(normalTS, inputData.tangentToWorld); // SpaceTransforms.hlsl
    #else
        inputData.normalWS = normalWS;
    #endif

    // inputData は Shadow/GI 関係のデータも扱うが、今回はそれらはレイトレで解決するので用意しない
}

float TraceShadow(float3 lightDirection, float3 normalWS, float3 positionWS)
{
    RayDesc ray;
    ray.Origin = positionWS + 1e-6 * normalWS;
    ray.Direction = lightDirection;

    // シーンスケールに合わせて適切に設定すべき
    ray.TMin = 0.1;
    ray.TMax = 100;

    ShadowPayload payload = (ShadowPayload)0;
    payload.hit = true;
    TraceRay(_Scene, RAY_FLAG_SKIP_CLOSEST_HIT_SHADER | RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH,
             0xFF, 0, 1, 1 /* ShadowMissShader */, ray, payload);
    return (float)!payload.hit;
}

// from : Lighting.hlsl
half4 MyUniversalFragmentPBR(InputData inputData, SurfaceData surfaceData)
{
    #if defined(_SPECULARHIGHLIGHTS_OFF)
    bool specularHighlightsOff = true;
    #else
    bool specularHighlightsOff = false;
    #endif
    BRDFData brdfData;

    // NOTE: can modify "surfaceData"...
    InitializeBRDFData(surfaceData, brdfData);

    // Clear-coat calculation...
    BRDFData brdfDataClearCoat = CreateClearCoatBRDFData(surfaceData, brdfData);
    uint meshRenderingLayers = GetMeshRenderingLayer();

    Light mainLight = GetMainLight();
    mainLight.shadowAttenuation = TraceShadow(mainLight.direction, inputData.normalWS, inputData.positionWS);

    LightingData lightingData = CreateLightingData(inputData, surfaceData);
    lightingData.giColor = 0; // GI はパストレで解決する


#ifdef _LIGHT_LAYERS
    if (IsMatchingLightLayer(mainLight.layerMask, meshRenderingLayers))
#endif
    {
        lightingData.mainLightColor = LightingPhysicallyBased(brdfData, brdfDataClearCoat,
                                                              mainLight,
                                                              inputData.normalWS, inputData.viewDirectionWS,
                                                              surfaceData.clearCoatMask, specularHighlightsOff);
    }

    #if defined(_ADDITIONAL_LIGHTS)
    uint pixelLightCount = GetAdditionalLightsCount();

    #if USE_FORWARD_PLUS
    [loop] for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)
    {
        FORWARD_PLUS_SUBTRACTIVE_LIGHT_CHECK

        Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
        //light.shadowAttenuation = TraceShadow(light.direction, inputData.normalWS, inputData.positionWS);

        #ifdef _LIGHT_LAYERS
            if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
        #endif
        {
            lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light,
                                                                          inputData.normalWS, inputData.viewDirectionWS,
                                                                          surfaceData.clearCoatMask, specularHighlightsOff);
        }
    }
    #endif // USE_FORWARD_PLUS

    LIGHT_LOOP_BEGIN(pixelLightCount)
        Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
        //light.shadowAttenuation = TraceShadow(light.direction, inputData.normalWS, inputData.positionWS);

        #ifdef _LIGHT_LAYERS
            if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
        #endif
        {
            lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light,
                                                                          inputData.normalWS, inputData.viewDirectionWS,
                                                                          surfaceData.clearCoatMask, specularHighlightsOff);
        }
    LIGHT_LOOP_END
    #endif // _ADDITIONAL_LIGHTS

    #if REAL_IS_HALF
        // Clamp any half.inf+ to HALF_MAX
        return min(CalculateFinalColor(lightingData, surfaceData.alpha), HALF_MAX);
    #else
        return CalculateFinalColor(lightingData, surfaceData.alpha);
    #endif
}

[shader("closesthit")]
void ClosestHitMain(inout RayPayload payload : SV_RayPayload, AttributeData attribs : SV_IntersectionAttributes)
{
    // レイトレのヒットした位置から頂点インデックスを取得
    uint3 triangleIndices = UnityRayTracingFetchTriangleIndices(PrimitiveIndex());

    // 頂点インデックスから三角形の3頂点を取得
    Vertex v0, v1, v2;
    v0 = FetchVertex(triangleIndices.x);
    v1 = FetchVertex(triangleIndices.y);
    v2 = FetchVertex(triangleIndices.z);

    // 3頂点からをヒット位置で補間
    float3 barycentricCoords = float3(1.0 - attribs.barycentrics.x - attribs.barycentrics.y, attribs.barycentrics.x, attribs.barycentrics.y);
    Vertex v = InterpolateVertices(v0, v1, v2, barycentricCoords);

    // テクスチャ座標の変形(元は LitPassVertex の処理)
    v.texCoord0 = TRANSFORM_TEX(v.texCoord0, _BaseMap);

    SurfaceData surfaceData;
    InitializeStandardLitSurfaceData(v.texCoord0, surfaceData);

    InputData inputData;
    InitializeInputData(v, surfaceData.normalTS, inputData);

    float4 directLighting = MyUniversalFragmentPBR(inputData, surfaceData);

    payload.hit = true;
    payload.radiance = directLighting.rgb;
}
この記事をシェアする

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です