Metal Raytracing

Metal for Accelerating Ray Tracing を利用することで Apple 環境でもレイトレーシングを試すことが出来ます。Metal (Apple) の場合 RTX のような専用ハードウェアを持っている訳ではないので、コードも既存のパイプラインとほぼ同じでコンピュートシェーダで処理されます。

GameViewContoller.m の ViewDidLoad の中で Renderer.mm を初期化。InitWithMetalKitView 関数を呼び出し、Metal 初期化とレンダリングに必要な下記オブジェクトを生成。

  • Metal の MTLLibrary と MTLCommandQueue の初期化
  • Pipeline を作成 (Pipeline State Object と同じ)
    • メインとなるシェーダーは4つ
    • rayKernel
    • shadekernel
    • shadowKernel
    • accumulateKernel
  • Buffer の作成
  • Intersector の作成
    • MPSRayIntersector
      • Ray と Geometry の交差判定をテストする
    • MPSTriangleAccelerationStructure
      • Metal の Acceleration Structure

リソース生成

Renderer.mm の drawableSizeWillChange を呼び出し、レンダーターゲットやレイトレーシング結果を保持する MTLTexture と MTLBuffer を生成する。

Metal では rayBuffer, shadowRayBuffer, intersectionBuffer の 3つの Buffer を作っている。バッファサイズは rayStride * rayCount (width*height)。rayStride はユーザーが定義した、Ray 構造体 (origin, direction, mask, maxDistance, color) の大きさ。Ray と Intersection の結果は GPU 内で生成して使われる。

RenderTargets[2] と AccumulationTargets[2] と RandomTexture[1] を RGBA32Float で作っている。RandomTexture は、各 Pixel を rand() % 1024 * 1024 の値で初期化。

Metal は MTLTexture は Compute Encoder の setTexture に。MTLBuffer は Compute Encoder の setBuffer にバインドする。

Intersector

Ray が missed (何も交差しなかった) の場合は、IntersectionBuffer が返す値はマイナスになる。ray.maxDistance が >= 0 以上で IntersectionBuffer がマイナスの場合は、レイを放射したが、何にもヒットしなかった(Miss)と判定できる?

RayKernel

ただのコンピューターシェーダー。実際のレイトレーシング前の初期化処理など。放射する最初のレイの原点位置と方向とマスクの定義。描画結果を格納する RenderTargets[0] を (0,0,0) で初期化。

ShadeKernel

ShadeKernel と ShadowKernel は2次レイ、3次レイと複数回呼び出される。1次レイはスクリーンスペースから witdh * height 数を放射する。その時に、下記の3つの状態に別れる。なおレイトレースの前提としてライトソースも Acceleration Structure に含む。

  • Geometry に当たった場合はライティング計算
  • ライトに直接レイが当たれば、ライトの色をピクセル値として書き込む
  • 何にも当たらなければそのレイは終了する

ポリゴンと Intersection したレイの処理。シーンにあるオブジェクトはあらかじめ、Geometry か Light のどちらかの Mask を持っており、

二次レイを投げるための用意として、ShadeRay の origin と direction を更新している。

ShadowKernel

ShadowRay がライトまでの道の中で、他のオブジェクトに当たったかどうかを判定する。もし何のオブジェクトにも当たらない場合は ShadowRay の Origin Position は、影の中にはいなかったと判定できる。

ShadowRay は先に実行された ShadeRay で当たった位置から、ライト方向への Direction を持ったレイである。

ShadowRay が Light Source への道の途中で何かに当たった (intersectionDistance > 0) 時は、それは影の中にいると判定できるので、そのピクセルはそのままにしておく。逆に、Light Soruce への途中で何もなかった場合はライトが届いているということなので、該当ピクセルのシェーディングを行う。色情報は shadowRay.color の中で保持している。光は加算で強くなるので、該当ピクセルに shadowRay.color を加算して明るくする。

ShadowKernel に設定する SrcTex は ShaderKernel での DestTex (RenderTargets[0])。ShadowKernel の DestTex は RenderTargets[1]。ShadowKernel の終了時に、RenderTargets[1] の内容を [0] と入れ替えている。

AccumulateKernel

ノイズリダクションのため、複数フレームに渡る描画結果の平均を取っている。ShaderKernel, ShadowKernel の描画結果を RenderTargets[0] (SrcTex)として入力。それとは別に 1Frame 前の描画結果を AccumulationTargets[0] として残しておき、それを prevTex として使う。最終結果を AccumulationTargets[1] に書き出している。 

float3 prevColor = prevTex.read(tid).xyz;
prevColor *= uniforms.frameIndex;

prevTex は前回までのフレームの平均値が入っている。この前回までの描画結果と、現在のフレーム値を乗算することで、現在フレームまで加算し続けた結果と見なす値に一旦復元している。これを Accumulation と見なす。

color += prevColor;
color /= (uniforms.frameIndex + 1);

SrcTex の値に前回フレームまでの Accumulation を加算。最後に SrcTex を加算した分を + 1 した値で割れば、フレームの平均が求まる。

ただ、このやり方だと、frameIndex の値が大きくなりすぎると、prevColor の値が飽和してしまうので注意。そのため frameIndex の値が大きくなりすぎないように定期的に調整しないといけないが、frameIndex の値が小さいと色が収束しない。開始数フレームはノイズが多いのはそのため。雑に 200 フレームを超えたら 100 フレームに戻すみたいなコードを書いたら、以外と見栄えは気がつかない結果だった。

コメントを残す

メールアドレスが公開されることはありません。