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.shader
の LitPassFragment
の中で基本的な処理を抜き出して、MyPathTracerHitShader.hlsl
の ClosestHitMain
に実装します。
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
を正しく設定できるようにします。
SurfaceData
は SurfaceData.hlsl
で定義されています。
PBR のライティング計算に使う各種データを保持していて、マテリアルに設定されたテクスチャなどのパラメータを InitializeStandardLitSurfaceData
関数の中でこの構造体に展開しています。
InitializeStandardLitSurfaceData
は LitInput.hlsl
で定義されていて、その中身を見ると、例えば以下のようにマテリアルに設定されているテクスチャや定数値からライティングに使うパラメータが計算されています。
half4 albedoAlpha = SampleAlbedoAlpha(uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap));
SampleAlbedoAlpha
をさらに辿っていくと SAMPLE_TEXTURE2D
、PLATFORM_SAMPLE_TEXTURE2D
、そして DirectX なら textureName.Sample(samplerName, coord2)
と、最終的にプラットフォーム毎のテクスチャサンプリングの関数にたどり着きます。
(com.unity.render-pipelines.core
パッケージにたどり着きます。)
今問題なのは SAMPLE_TEXTURE2D
がレイトレシェーダで使えないということです。
SAMPLE_TEXTURE2D
はテクスチャ LOD(ミップマップ)を自動で計算してくれるラスタライズ工程専用の機能です。
Compute シェーダと同様に、レイトレシェーダは微分オペレーション(ddx, ddy)が使えないため、ラスタライズと同じ方法でミップマップを計算することができません。
(新しい Shader Model の Compute シェーダでは微分オペレーションを使えるらしいのですが、反射等で隣接ピクセルの関係が薄くなるレイトレーシングにおいては使えたとしても有効に活用できるとはかぎらないでしょう)
今回は以下のようにして、単純に SAMPLE_TEXTURE2D
を SAMPLE_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.hlsl
は LitInput.hlsl
の中でも include されているのですが、今回 SAMPLE_TEXTURE2D
の定義の上書きをするために単独で先に読み込んでおきます。
そして #undef
で定義を消して、改めて SAMPLE_TEXTURE2D
を SAMPLE_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
を正しく設定できるようにします。
InputData
は Input.hlsl
で定義されています。
主に頂点に関連するデータを持っています。
ラスタライズでライティング計算を行う Fragment シェーダから見た Vertex シェーダからの入力という意味での InputData
なので、レイトレで同じ名前を扱うとちょっと意味がズレる気もしますがそのまま使っています。
InputData
は Input.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
で定義されている UniversalFragmentPBR
を MyPathTracerHitShader.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
関係を削除shadowMask
とaoFactor
を削除GetMainLight
とGetAdditionalLight
をshadowMask
とaoFactor
を扱わないものに変更- GI 関係の計算(
MixRealtimeAndBakedGI
,GlobalIllumination
)を削除して、giColor
を0
に設定 - 頂点ライティング関係を削除
この実装にはポイントライトとスポットライトの対応も含まれているのですが、動作は未検証です。
Forward+ のときレイトレではライトのカリングが正しく動作していないようです。
可能なら次回以降修正も行いたいです。
シェーダバリアントの対応
マテリアルのパラメータ等でシェーダの中身をマクロで切り替える仕組み、シェーダバリアントに対応しましょう。MyPathTracerLit.shader
の MyPathTracing
パスにバリアントの宣言を記述します。
バリアントは元の Lit.shader
の ForwardLit
パスからコピーして書き換えています。
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_fragment
と multi_compile_fragment
は fragment
を raytracing
に直しています。
結果
これで全てエラーなく動くのであれば以下のような絵が得られるはずです。
確かにライティングが得られていそうですが、まだまだイマイチな絵です。
単なる直接光のみのライティングだとこんなものでしょう。
![](https://blog.siliconstudio.co.jp/wp-content/uploads/2025/02/image-1024x576.png)
シーンの軽い修正
今のシーンにちょっと変なカクカクした影のようなものが出ていると思います。
![](https://blog.siliconstudio.co.jp/wp-content/uploads/2025/02/image-1-1-1024x576.png)
これは半透明オブジェクトをデカールとして置いているものと影専用オブジェクトです。
アート的な意図があってシーンに配置されているものを消すのは忍びないのですが、レイトレで対応するにはかなり冗長になりそうなのでデカールは無効化してしまいます。
影専用オブジェクトも消しても良いですが、そこまで気にならず存在に後から気がついたので残しています。
シーンを decal
で検索して MeshDecal
と名前の付いているオブジェクトを選択、インスペクタのチェックボックスを切って無効化しできます。
![](https://blog.siliconstudio.co.jp/wp-content/uploads/2025/02/image-1-1024x614.png)
![](https://blog.siliconstudio.co.jp/wp-content/uploads/2025/02/image-2-1024x576.png)
レイトレの結果がちょっと綺麗になりました。
影の対応
今は直接光しかないのでレイトレを使って影を描いてみましょう。
元の UniversalFragmentPBR
では Light
の shadowAttenuation
に影の遮蔽の値が入ります。
今回は 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
ファイルを選択するとインスペクタ上に情報が表示されます。
ここにあるインデックスは利用するの確認しておきましょう。
![](https://blog.siliconstudio.co.jp/wp-content/uploads/2025/02/image-3.png)
シャドウレイを飛ばす
影用のレイを飛ばす関数 TraceShadow
を MyPathTracerHitShader.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.hit
を true
で初期化して、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);
正しく動けば以下のようなぱっきりとした影のある絵が得られます。
ソフトシャドウの対応は確率的なサンプリングが必要になるので、今は扱いません。
![](https://blog.siliconstudio.co.jp/wp-content/uploads/2025/02/image-4-1024x576.png)
余談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.shader
、Payload.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;
}