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

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

はじめに

こんにちは。研究開発室の川口です。
突然ですが皆さんはレイトレーシングを実装したいときにどんな環境を利用しますか?

C/C++ や Rust を使った CPU で動作するシンプルな実装はとても大切ですし、OptiX や最近登場した HIPRT を使った GPU で動作する実践的な実装も非常に興味深いです。
DXR や Vulkan Ray Tracing を使ってゲームに応用できるような実装を追求しても良いですし、お手軽に Shadertoy 上で動作する表現に挑戦するのも楽しいです。

そんな色々なレイトレの実装環境の中でもちょっと特殊といえる、 Unity 上で GPU レイトレを行う話を何回かにわけて書いていきたいと思います。
理論や設計などには深く言及せず、ざっくりとレイトレの実装例としてパストレーサーを作っていく過程を紹介していきます。
最終的には ReSTIR のような実践的なアルゴリズムを実装するところまで紹介できると良いなと考えています(が予定は未定です)。

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

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

今回はせっかくなので今年リリースされたばかりの Unity6 と、その新機能である RenderGraph を使って実装を進めてみたいと思います。
レンダリング環境には URP を使います。HDRP でも良いのですが、そちらには Unity 謹製のレイトレ・パストレがあると思うので、URP 上で動く新しいレイトレフィーチャーとして実装してみました。

執筆時点の環境(2024年12月)

  • Unity 6 (6000.0.27f1)
  • URP 17.0.3

Unity でレイトレを実装するメリット

しばらく前に社内でレイトレーシングの勉強会を行ったときに Unity + URP の環境を利用しました。
ちょっと特殊で応用が利きにくい面はあるのですがメリットも多かったと感じています。

まず Unity のレイトレーシングは内部が DXR なので、近年の GPU レイトレの設計と変わらない基本的な思想に手軽に触れられます。
そして Acceleration Structure や Binding Table といった、自分で実装すると理解や設定が面倒なもろもろが隠蔽されているので、レイトレのアルゴリズムそのものにいきなり着手できます。

GPU レイトレを勉強しようとしてその下回りの DirectX や Vulkan、CUDA の実装途中で挫折や妥協する人は多いと思うので、そのあたりの過程を飛ばして早々にレイトレシェーダの実装に取り組めるのは勉強やプロトタイピングをしたい人にはメリットがあると言えます。
もちろん隠蔽されている部分こそが大事でもあるので、これを手がかりとして、将来的には DXR などで一から GPU レイトレの実装にも挑戦してみてほしいとも思います。

さらに Unity はゲームエンジンですのでモデルやテクスチャなどのアセットのインポートを自前で組む必要がなく、いきなりゲームで使うような規模の大きいシーンでレイトレを試せる点も大きなメリットでしょう。

プロジェクトの作成

さて前置きが長くなりましたが実装に入ってみようと思います。
といっても今回はほとんど環境設定で、レイトレシェーダ自体には少ししか触れられないと思います。

Unity Hub から Unity6 の新規プロジェクト MyPathTracer を作成します。

このとき Universal 3D Sample のテンプレートを選びました。
空のシーンに適当なプリミティブを置いてレイを飛ばしても味気ないので、最初からゲームエンジンの豊富なサンプルを享受していきましょう。

もちろん重かったりシーン構造が複雑で設定箇所が多かったりと面倒な点も多く出てくると思います。

プロジェクト構成

Universal 3D Sample は4つのシーンを含んでいます。
今回は最初にプロジェクトを立ち上げたとき出てくる、The terminal のシーンで作業していきます。

最初に立ち上げると Tutorial が表示されますが適当に閉じます。

Assets 以下に作業用のフォルダ MyPathTracer を作成し、その中に Editor, Scripts, Shaders フォルダを置きます。

Editor はエディタ専用の処理を記述するスクリプトを置く場所で今回は使わないかもしれません。
私はブログ用にスクリーンショットが欲しいのでキャプチャスクリプトを置いています。

またバージョン管理のためこの時点で git リポジトリを作成して初期コミットしておくと良いでしょう。
Unity プロジェクト用の gitignore はこちらです。

DirectX12 の有効化

Unity のレイトレは DXR なので、DirectX12 でしか動作しません。
Unity のプロジェクト設定も DirectX12 にしましょう。

ツールバー > Edit > Project Settings > Player を開きます。
Auto Graphics API for Windows のチェックを外して、Graphics APIs for Windows のリストの + から Direct3D12 を追加します。
最初からある Direct3D11 を – で削除します。

エディタの再起動を要求されるのでそれに従います。

シェーダの追加

Shaders 以下にレイトレーシング用のシェーダを追加します。
アセットウインドウ > 右クリック > Create > Shader > Ray Tracing Shader

名前は MyPathTracingShader としました。ちなみに拡張子は .raytrace です。

レイトレシェーダの中身は後で実装します。

Renderer Feature と Render Pass の追加

レイトレシェーダを RenderGraph で実行するカスタムレンダーパスとして実装していきます。

Scripts 以下に スクリプト MyPathTracerFeature を追加します。
アセットウインドウ > 右クリック > Create > Rendering > URP Renderer Feature

MyPathTracerFeature.cs を開きます。
この時点でスクリプトの中には RenderGraph に対応したカスタムパスの雛形が入っています。

この雛形では ScriptableRenderPassScriptableRendererFeature の内部クラスとして実装されています。
今は一つのカスタムパスだけなのでこれでもいいですが、将来的に複数のレンダーパス(例えばデノイズパス)を追加することを考えると、ScriptableRenderPass ごとにスクリプトを分けるべきかもしれません。
必要になったときに対応しましょう。

名前の置換

CustomRenderPassMyPathTracerPass と置き変えます。
Visual Studio であればクラス名にカーソルを合わせて Crtl+R → Crtl+R のショートカットを使うと、安全に全ての名前を書き換えられます。

m_ScriptablePassm_MyPathTracerPass としておきましょう。

パスの実行タイミング

MyPathTracerFeatureCreate の中でレンダーパスの実行タイミングが設定されています。
今回は RenderPassEvent.AfterRenderingOpaquesRenderPassEvent.BeforeRenderingPostProcessing に置き換えます。

public override void Create()
{
    m_MyPathTracerPass = new MyPathTracerPass();

    // Configures where the render pass should be injected.
    m_MyPathTracerPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
}

ゲーム画面だけでレイトレ

Unity には Scene View と Game View がありますが、シーンを編集するための Scene View にレイトレはいらないので無効化します。
MyPathTracerFeatureAddRenderPasses の中に以下の処理を書いて、Scene View とプレビュー(多分マテリアルのサムネイルとかの表示)のときはレイトレパスを追加しないようにします。

if (renderingData.cameraData.isSceneViewCamera || renderingData.cameraData.isPreviewCamera) return;

Renderer Feature の設定

追加した Renderer Feature が動作するようにシーンに設定します。
Assets > Settings > PC > PC_High_Renderer
Add Renderer Feature > My Path Tracer Feature

これで MyPathTracerPass が実行されるようになりました。

Render Pass の実装

それでは MyPathTracerPass の中身を実装してレイトレを実行できるようにしていきましょう。

RenderGraph では RecordRenderGraph に描画に関するリソースの設定や、描画の実行など一連の処理を実装します。
実際に実行するコマンドなどは builder.SetRenderFunc の中に delegate として記述します(雛形だと ExecutePass が該当)。

RenderGraph の実装についてはサイバーエージェント SGE コアテクさんの一連の解説記事が非常に分かりやすく、勉強になりました。

レンダーパスの互換性について

ScriptableRenderPass の雛形に含まれる以下の関数はカスタムレンダーパスの古い実装方法で、RenderGraph を使わない際の互換性のために用意されています。
(2024年12月時点では存在)

- OnCameraSetup

- Execute

- OnCameraCleanup

いつなくなるかもわからないのでまるごと削除しました。

レイトレシェーダの読み込み

まず最初に作ったレイトレシェーダを読み込みます。

MyPathTracerPass にコンストラクタを追加して、RaytracingShader を読み込みます。
ついでにプロファイラにも対応しておきます。

private RayTracingShader rayTracingShader;

public MyPathTracerPass()
{
    base.profilingSampler = new ProfilingSampler("MyPathTracerPass");
    rayTracingShader = AssetDatabase.LoadAssetAtPath<RayTracingShader>("Assets/MyPathTracer/Shaders/MyPathTracingShader.raytrace");
}

描画パラメータの用意

RenderGraph では delegate 関数(ここでは ExecutePass)の形で描画処理を記述します。
ExecutePass の中から MyPathTracerPass のメンバに直接アクセスはせず、描画に使うリソースやパラメータは PassData (デフォルト名。任意のクラス名も可)クラスを介してやりとりすることになります。

今回はレイトレシェーダ本体、シェーダを実行した結果を一度描き出すためのフレームバッファ、結果を表示するためのカメラのフレームバッファ、そしてレイトレ用のシーン構造である Acceleration Structure 、そしてカメラ情報を受け渡します。
今後、例えばサンプル数や反射回数などのパラメータを追加したくなった際は、この PassData に記述していくことになります。

ユーザに露出するパラメータはポストプロセスのように Volume で管理するようになると思いますが、それはまた次回以降の記事で紹介します。

private class PassData
{
    public RayTracingShader rayTracingShader;
    public TextureHandle output_ColorTexture;
    public TextureHandle camera_ColorTarget;
    public RayTracingAccelerationStructure rayTracingAccelerationStructure;

    public Camera camera;
}

MyPathTracerPass のメンバに rayTracingShader があって、さらにその内部クラスである PassData にも rayTracingShader があるのはちょっと冗長な気もしますが、そういうものと考えておきましょう。(もっとスマートに記述するやり方があるかもしれません)

Acceleration Structure の用意

レイトレにおけるシーン構造である Acceleration Structure の細かい処理は Unity に任せますが、そのリソースは自前で管理します。
まず private RayTracingAccelerationStructure rayTracingAccelerationStructure;MyPathTracerPass のメンバに追加しておきます。PassData の中身とは別です。

そして MyPathTracerPass に解放処理を行う関数 Cleanup を用意してその中で Acceleration Structure を解放するようにします。

public void Cleanup()
{
    rayTracingAccelerationStructure?.Dispose();
}

Cleanup を Renderer Feature の解放処理で呼び出すように、MyPathTracerFeatureDispose をオーバーライドします。

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        m_MyPathTracerPass?.Cleanup();
    }
}

GPU リソースの作成

PassData に設定するリソースを用意します。
分かりやすく passName ~ using の間に書いていきます。
PrepareResources のような関数を作っても良いかもしれません。
URP が管理している各種リソースへアクセスする UniversalResourceData も雛形で生成されている箇所から移動しています。

ここではレイトレ結果を描き出すバッファをカメラのフレームバッファのフォーマットから少し変更して作成しています。

Acceleration Structure もここで初期化します。
Acceleration Structure の設定は色々ありますが、今回は深く考えずシーン全てのメッシュを含めるようにしています。
動的な更新に対応するともう少し変わってくると思うので、次回以降の記事で扱うかもしれません。

UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();

// 現在のカメラで描画されたカラーフレームバッファを取得
var colorTexture = resourceData.activeColorTexture;

// レイトレ結果を描き出すバッファを作成
RenderTextureDescriptor rtdesc = cameraData.cameraTargetDescriptor;
rtdesc.graphicsFormat = GraphicsFormat.R16G16B16A16_SFloat;
rtdesc.depthStencilFormat = GraphicsFormat.None;
rtdesc.depthBufferBits = 0;
rtdesc.enableRandomWrite = true;
var resultTex = UniversalRenderer.CreateRenderGraphTexture(renderGraph, rtdesc, "_RayTracedColor", false);

// Acceleration Structure を作成
if (rayTracingAccelerationStructure == null)
{
    var settings = new RayTracingAccelerationStructure.Settings();
    settings.rayTracingModeMask = RayTracingAccelerationStructure.RayTracingModeMask.Everything;
    settings.managementMode = RayTracingAccelerationStructure.ManagementMode.Automatic;
    settings.layerMask = 255;

    rayTracingAccelerationStructure = new RayTracingAccelerationStructure(settings);

    // AS の構築はここだけ。動的な更新には今は対応しない
    rayTracingAccelerationStructure.Build();
}

UnsafePass の設定

RenderGraph を使ってレンダーパスを追加します。
雛形では AddRasterRenderPass となっている箇所を、AddUnsafePass に書き換えます。

using (var builder = renderGraph.AddUnsafePass<PassData>(passName, out var passData, base.profilingSampler))

ついでにプロファイラの設定も行っておきましょう。

RenderGraph におけるこの AddRenderPass 系の関数には、オブジェクト描画を行う AddRasterRenderPass、コンピュートシェーダによる各種計算を行う AddComputePass、その他の自由な処理を記述できる AddUnsafePass があります。
レイトレは UnsafePass で行うことになります。

AddRasterRenderPass を置き換えたので、SetRenderFuncExecutePass の二箇所にある Context も RasterGraphContext から UnsafeGraphContext へ置き換えます。

static void ExecutePass(PassData data, UnsafeGraphContext context)
builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) => ExecutePass(data, context));

PassData の設定

using スコープの中で passData を設定します。
RenderGraph で利用するテクスチャは UseTexture で利用方法を宣言する必要があります。

今回 passData.output_ColorTexture はレイトレ結果を描き出す先なので Write のみの設定です。
passData.camera_ColorTarget はカメラの描画先で、最終的にレイトレ結果を反映するターゲットになります。
今回は読み込むは必要ないのですが、今後シーンカラーを使った処理が欲しくなることがあるかもしれないので ReadWrite にしました。

passData.rayTracingShader = rayTracingShader;
passData.output_ColorTexture = resultTex;
passData.camera_ColorTarget = colorTexture;
passData.rayTracingAccelerationStructure = rayTracingAccelerationStructure;
passData.camera = cameraData.camera;

builder.UseTexture(passData.output_ColorTexture, AccessFlags.Write);
builder.UseTexture(passData.camera_ColorTarget, AccessFlags.ReadWrite);

ExecutePass の実装

描画処理の本体である ExecutePass を実装します。
context からコマンドバッファが取得できるので、そこに描画命令を設定していきます。
UnsafeGraphContext に用意されていないコマンドは Native な CommandBuffer を介して呼んでいます。

cmd.SetRayTracing* でテクスチャや Acceleration Structure を設定します。
プロパティ名はシェーダ側と対応させる必要があります。

DispatchRays でレイトレシェーダを起動します。
native_cmdDispatchRays は引数に Camera が不要でそちらの方が DXR っぽくて良いかもしれません。

SetRayTracingShaderPass の役割は後ほど説明します。

static void ExecutePass(PassData data, UnsafeGraphContext context)
{
    var native_cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);

    native_cmd.SetRayTracingShaderPass(data.rayTracingShader, "MyPathTracing");

    context.cmd.SetRayTracingAccelerationStructure(data.rayTracingShader, Shader.PropertyToID("_Scene"), data.rayTracingAccelerationStructure);
    context.cmd.SetRayTracingTextureParam(data.rayTracingShader, Shader.PropertyToID("_Result"), data.output_ColorTexture);

    context.cmd.DispatchRays(data.rayTracingShader, "MyRaygenShader", (uint)data.camera.pixelWidth, (uint)data.camera.pixelHeight, 1, data.camera);

    // 結果をカメラに書き戻す
    native_cmd.Blit(data.output_ColorTexture, data.camera_ColorTarget);
}

レイトレシェーダの実装

ExecutePass の実装に合わせて MyPathTracingShader.raytrace を少し書き換えました。

#pragma max_recursion_depth 1

RaytracingAccelerationStructure _Scene;
RWTexture2D<float4> _Result;

[shader("raygeneration")]
void MyRaygenShader()
{
    uint2 dispatchIdx = DispatchRaysIndex().xy;
    _Result[dispatchIdx] = float4(dispatchIdx.x & dispatchIdx.y, (dispatchIdx.x & 15)/15.0, (dispatchIdx.y & 15)/15.0, 0.0);
}

SetRayTracingAccelerationStructure で設定する RaytracingAccelerationStructure_SceneSetRayTracingTextureParam で設定する結果を描き出すバッファは _Result という名前にしています。

DispatchRays で設定する Ray Generation シェーダのエントリポイントは MyRaygenShader という名前です。

この時点でエラーなく実行できているならば以下のような絵が得られます。

デフォルトのレイトレシェーダはピクセル位置に基づいてフラクタル図形を描くだけです。ポストエフェクトで白飛びもしています。

今は DXR におけるレイトレのエントリポイントである Ray Generation シェーダを実行しているだけで、Compute Shader でもできるようなことが適当に描かれています。
GPU レイトレでは、この Ray Generation シェーダからレイを飛ばして、レイが当たった箇所で Closest Hit シェーダ、当たらないとき Miss シェーダが起動します。
レイの衝突に応じてライティングなどの処理を記述することでレイトレを実現できます。

レイをつくる

DXR におけるレイは RayDesc 構造体で定義されて、Unity でも同じです。https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html#ray-description-structure

Ray Generation シェーダは DispatchRays で指定した解像度に基づいて、各ピクセル毎に起動します。
ピクセル位置とカメラの情報から飛ばすべきレイを計算します。

ここでは Unity のビルトイン変数にある変換行列を使って、Unity のカメラと一致するようにレイを定義します。
この関数は Ray Tracing Gems II, Chapter 14: The Reference Path Tracer の実装を参考にしています。

URP のヘッダも include しておきましょう。

#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"
#define FLT_MAX 1.#INF

RayDesc generatePinholeCameraRay(float2 pixel)
{
    float4x4 view = unity_WorldToCamera;
    float4x4 proj = unity_CameraProjection ;

    // Setup the ray
    RayDesc ray;
    ray.Origin = _WorldSpaceCameraPos;
    ray.TMin = 0.f;
    ray.TMax = FLT_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;
}

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);

レイを飛ばす

GPU レイトレはシェーダ(RayGen)からシェーダ(Closest Hit, Miss)を起動するという、従来のシェーダのパイプラインとは異なる挙動をします。
(CUDA におけるカーネルから別のカーネルを呼ぶような処理がイメージしやすいかもしれません)
このとき異なるシェーダ間では自由に変数などをやりとりできません。

DXR では Payload というユーザ定義の構造体を介してレイトレに必要な情報を異なるシェーダの間でやりとりします。

Payload は複数のシェーダで共有するので別ファイルで定義しておきましょう。
MyPathTracingShader.raytrace と同じフォルダに Payload.hlsl を作ります。

現状はヒットしたかどうかと放射輝度(色)を記録することにします。

struct RayPayload
{
    bool hit;
    float3 radiance;
};

MyPathTracingShader.raytrace 側で #include "Payload.hlsl" をするのも忘れないようにしましょう。

MyRaygenShader の中でレイを飛ばします。

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

TraceRay 関数が DXR におけるレイを飛ばす処理です。
https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html#traceray

この TraceRay 関数を呼ぶと引数に入れた Acceleration Structure をトラバースして、各種設定に応じてヒットシェーダなどが起動します。
TraceRay の引数は Unity に隠蔽されている情報に基づくもの含んでいたりしてちょっとややこしいので今回はこういうものとさせてください。
ちゃんと理解したいのであれば、DXR で一から実装していくのがなんだかんだで一番効率が良いかもしれません。

レイが当たらなかったときの処理

レイは飛ばしましたが、その後の処理が今はありません。

まずは何にも当たらなかった場合の処理を実装してみます。

DXR では TraceRay 関数で飛ばしたレイが何にも当たらなかったとき、Miss シェーダが呼ばれるようになっています。
MyPathTracingShader.raytrace に Miss シェーダ MyMissShader を追加してみましょう。

[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;
}

適当にレイの向きに応じて空っぽい色を設定しています。

そして MyRaygenShader の中で TraceRay の後に、結果の色を描き出すようにします。

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

この時点でゲーム画面は以下のようになるはずです。

カメラからレイを飛ばして何にも当たらない箇所だけに色が付いています。
(真ん中の白丸はシーンに最初から設定されている FPS 用の中心点です)
絵は地味ですが、シーン中のジオメトリを登録する処理などを自分で実装することなくこの結果が得られているのは結構驚きで、Unity を使っている大きな利点を享受できているといえるでしょう。

レイが当たったときの処理(準備)

レイがオブジェクトに当たったときに走る処理がないので今は真っ黒です。
DXR ではレイがメッシュにヒットしたとき Closest Hit シェーダが呼ばれます。
Any Hit というのもありますがそれは必要になったときに改めて触れます。

この Closest Hit シェーダは通常のオブジェクト描画においてマテリアルに対応するものです。
そのため MyPathTracingShader.raytrace ではなくオブジェクトにくっつけるマテリアルのシェーダとして実装していきます。

Unity のマテリアルは ShaderLab という形式で記述されます。

ShaderLab はパラメータを定義する Property や描画処理を記述する Pass などのブロックで構成されています。
この Pass は単一のものではなく、Forward、深度プレパス、シャドウ、GBuffer などオブジェクト描画を使う処理に応じて、一つのシェーダファイルに複数パスの定義が実装できます。
例えば MyPathTracer\Library\PackageCache\com.unity.render-pipelines.universal\Shaders\Lit.shader は URP の標準マテリアルの実装で、たくさんのパスを含んでいることが分かります。

レイトレ用の Closest Hit シェーダもひとつの Pass として実装することになります。
本当はシーン中のマテリアルへ自動的に Closest Hit シェーダパスを挿入するような仕組みを用意できるといいのですが、今回は全部のマテリアルを手で置き換えていこうと思います。

レイトレを使わない部分は標準マテリアルと全く同じにしたいので、Lit.shader をコピーして使うことにします。
MyPathTracingShader.raytrace と同じフォルダに MyPathTracer\Library\PackageCache\com.unity.render-pipelines.universal\Shaders\Lit.shader をコピーしてきて、MyPathTracerLit.shader とリネームしました。

Lit.shader は巨大なので Closest Hit シェーダ本体の実装は別ファイルにします。
同じフォルダに MyPathTracerHitShader.hlsl を作ります。

MyPathTracerLit.shader の先頭行にあるシェーダの名前を変えておきます。

Shader "Custom/MyPathTracerLit"

MyPathTracerLit.shader の一番最後(執筆時点では XRMotionVectors パスのうしろ)にレイトレ用の Pass を追加します。

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

    HLSLPROGRAM

    #pragma raytracing surface_shader

    #include "MyPathTracerHitShader.hlsl"

    ENDHLSL
}

ここで大事なのは “MyPathTracing” と #pragma raytracing です。

MyPathTracing” は ExecutePass の中で行っている SetRayTracingShaderPass で指定する名前と対応しています。
これは Ray Generation シェーダが TraceRay 関数を通じて呼び出すヒットシェーダを名前で紐付けしています。
#pragma raytracing はこのパスがレイトレ用であることを示しています。
surface_shader の部分は何でも良さそうなのですが HDRP で使われているものと同じにしています。

これらの設定によってこのシェーダを設定したマテリアルがレイトレに対応していると Unity が解釈してくれます。
Property ブロックに記述してあるパラメータやテクスチャなどもレイトレで使えるようになります。
DXR でマテリアルを細かく設定するのはとても手間で、Shader Binding Table などの細かい仕様の理解も必要なので、Unity がこれを全てやってくれるのは今回の試みを行う上での最大のメリットでしょう。

レイが当たったときの処理(本体)

Closest Hit シェーダの本体を MyPathTracerHitShader.hlsl に実装します。
細かく解説するとキリがないので全文を載せます。

ポイントは Unity ビルトインの機能を活用してレイトレにおける頂点データを解決している所です。
DXR でレイトレを実装するときは頂点バッファやインデックスバッファを正しく扱うための実装が必要です。
Unity には既にそのための機能が用意されていて、ビルトインの機能 UnityRaytracingMeshUtils.cginc を通じて簡単にアクセスできます。

マテリアルのプロパティも同様で、今回は UPR の Lit.shader をそのまま使っているので、LitInput.hlsl をインクルードすることで Lit 用のプロパティにアクセスできます。

とりあえず今回使わなくても将来的に使うかもしれない頂点属性もまとめて用意しています。

▼MyPathTracerHitShader.hlsl 全文

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

#include "UnityRaytracingMeshUtils.cginc"

#include "Payload.hlsl"

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;
}

[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);

    // ベースマップのテクスチャから色を取得
    // 元の Lit.shader で使っている _BaseMap などのプロパティは LitInput.hlsl を include することで使える
    float3 color = SAMPLE_TEXTURE2D_LOD(_BaseMap, sampler_BaseMap, v.texCoord0, 0).rgb;

    payload.hit = true;
    payload.radiance = color;
}

マテリアルの置換

シーン中で使われている Lit マテリアルを MyPathTracerLit に置き換えていきます。
Assets > Scenes > Terminal > Art 以下の Architechture, Props の中にある Material フォルダにシーンで使われているマテリアルがまとまっています。

マテリアルのインスペクタ上部にある Shader を見て Universal Render Pipeline/Lit になっているものをまとめて選択します。

マテリアルを選択したら、Shader のポップアップから Universal Render Pipeline/LitCustom/MyPathTracerLit に置き換えます。

元が同じ Lit なので、シェーダを差し替えても設定済みのマテリアルプロパティの値はそのまま維持されます。
これで TraceRay で飛ばしたレイが、対応するマテリアルを持つメッシュにヒットすると、そのベースマップが描画されるようになりました。

Lit 以外のマテリアルの対応をどうするのはは未定です。
今回、元のシーンに完全互換のパストレーサーを作ることは目的でないので、真っ黒のまま放置するかもしれません。

ちなみに現状は URP の通常の描画もそのまま動いています。
通常描画の結果も活用するのか無効化してしまうのかも今後考えていきます。

まとめ

これで Unity6 の URP 上で GPU レイトレーシングを使って絵を描くことができるようになりました。
よく見るとテクスチャも正しく反映されていないようにも見えますし、全くレイトレの効果が感じられるような絵にはなっていません。

次回はパストレーシングのアルゴリズムを実装して、カッコいい絵を出せるようにしていきたいと思います。

ソースコード

今回作ったソースコードの全文を載せます。
MyPathTracerHitShader.hlslPayload.hlsl の全文は、本文中にあります。
MyPathTracerLit.shader はあまりに長いので省略させてください。Lit.shader をコピペしてシェーダ名を変えて Pass を一つ追加しているだけです。

▼MyPathTracerFeature.cs 全文

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEditor.VersionControl;
using UnityEditor;
using UnityEngine.Experimental.Rendering;
using static Unity.Burst.Intrinsics.X86.Avx;

public class MyPathTracerFeature : ScriptableRendererFeature
{
    class MyPathTracerPass : ScriptableRenderPass
    {
        private RayTracingShader rayTracingShader;
        private RayTracingAccelerationStructure rayTracingAccelerationStructure;

        public MyPathTracerPass()
        {
            base.profilingSampler = new ProfilingSampler("MyPathTracerPass");
            rayTracingShader = AssetDatabase.LoadAssetAtPath<RayTracingShader>("Assets/MyPathTracer/Shaders/MyPathTracingShader.raytrace");
        }

        public void Cleanup()
        {
            rayTracingAccelerationStructure?.Dispose();
        }

        // This class stores the data needed by the RenderGraph pass.
        // It is passed as a parameter to the delegate function that executes the RenderGraph pass.
        private class PassData
        {
            public RayTracingShader rayTracingShader;
            public TextureHandle output_ColorTexture;
            public TextureHandle camera_ColorTarget;
            public RayTracingAccelerationStructure rayTracingAccelerationStructure;

            public Camera camera;
        }

        // This static method is passed as the RenderFunc delegate to the RenderGraph render pass.
        // It is used to execute draw commands.
        static void ExecutePass(PassData data, UnsafeGraphContext context)
        {
            var native_cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);

            native_cmd.SetRayTracingShaderPass(data.rayTracingShader, "MyPathTracing");

            context.cmd.SetRayTracingAccelerationStructure(data.rayTracingShader, Shader.PropertyToID("_Scene"), data.rayTracingAccelerationStructure);
            context.cmd.SetRayTracingTextureParam(data.rayTracingShader, Shader.PropertyToID("_Result"), data.output_ColorTexture);

            context.cmd.DispatchRays(data.rayTracingShader, "MyRaygenShader", (uint)data.camera.pixelWidth, (uint)data.camera.pixelHeight, 1, data.camera);

            // 結果をカメラに書き戻す
            native_cmd.Blit(data.output_ColorTexture, data.camera_ColorTarget);
        }

        // RecordRenderGraph is where the RenderGraph handle can be accessed, through which render passes can be added to the graph.
        // FrameData is a context container through which URP resources can be accessed and managed.
        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            const string passName = "My Path Tracer Pass";

            UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
            UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();

            // 現在のカメラで描画されたカラーフレームバッファを取得
            var colorTexture = resourceData.activeColorTexture;

            // レイトレ結果を描き出すバッファを作成
            RenderTextureDescriptor rtdesc = cameraData.cameraTargetDescriptor;
            rtdesc.graphicsFormat = GraphicsFormat.R16G16B16A16_SFloat;
            rtdesc.depthStencilFormat = GraphicsFormat.None;
            rtdesc.depthBufferBits = 0;
            rtdesc.enableRandomWrite = true;
            var resultTex = UniversalRenderer.CreateRenderGraphTexture(renderGraph, rtdesc, "_RayTracedColor", false);

            // Acceleration Structure を作成
            if (rayTracingAccelerationStructure == null)
            {
                var settings = new RayTracingAccelerationStructure.Settings();
                settings.rayTracingModeMask = RayTracingAccelerationStructure.RayTracingModeMask.Everything;
                settings.managementMode = RayTracingAccelerationStructure.ManagementMode.Automatic;
                settings.layerMask = 255;

                rayTracingAccelerationStructure = new RayTracingAccelerationStructure(settings);

                // AS の構築はここだけ。動的な更新には今は対応しない
                rayTracingAccelerationStructure.Build();
            }

            // This adds a raster render pass to the graph, specifying the name and the data type that will be passed to the ExecutePass function.
            using (var builder = renderGraph.AddUnsafePass<PassData>(passName, out var passData, base.profilingSampler))
            {
                // Use this scope to set the required inputs and outputs of the pass and to
                // setup the passData with the required properties needed at pass execution time.
                passData.rayTracingShader = rayTracingShader;
                passData.output_ColorTexture = resultTex;
                passData.camera_ColorTarget = colorTexture;
                passData.rayTracingAccelerationStructure = rayTracingAccelerationStructure;
                passData.camera = cameraData.camera;

                builder.UseTexture(passData.output_ColorTexture, AccessFlags.Write);
                builder.UseTexture(passData.camera_ColorTarget, AccessFlags.ReadWrite); // 今回は読み込みはしないが一応

                // Setup pass inputs and outputs through the builder interface.
                // Eg:
                // builder.UseTexture(sourceTexture);
                // TextureHandle destination = UniversalRenderer.CreateRenderGraphTexture(renderGraph, cameraData.cameraTargetDescriptor, "Destination Texture", false);

                // Assigns the ExecutePass function to the render pass delegate. This will be called by the render graph when executing the pass.
                builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) => ExecutePass(data, context));
            }
        }
    }

    MyPathTracerPass m_MyPathTracerPass;

    /// <inheritdoc/>
    public override void Create()
    {
        m_MyPathTracerPass = new MyPathTracerPass();

        // Configures where the render pass should be injected.
        m_MyPathTracerPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    }

    // Here you can inject one or multiple render passes in the renderer.
    // This method is called when setting up the renderer once per-camera.
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (renderingData.cameraData.isSceneViewCamera || renderingData.cameraData.isPreviewCamera) return;

        renderer.EnqueuePass(m_MyPathTracerPass);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            m_MyPathTracerPass?.Cleanup();
        }
    }
}

▼MyPathTracingShader.raytrace 全文

// Uncomment this pragma for debugging the HLSL code in PIX. GPU performance will be impacted.
//#pragma enable_ray_tracing_shader_debug_symbols

#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 1

RaytracingAccelerationStructure _Scene;
RWTexture2D<float4> _Result;

#define FLT_MAX 1.#INF

// 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 = 0.f;
    ray.TMax = FLT_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;
}
この記事をシェアする

コメントを残す

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