UEのマテリアル上でレイトレーシングしてみた
に更新
はじめに
次の画像を御覧ください。
4 つの球がワールド上に配置されている用に見えます。しかし、これを Unlit 表示にすると次のようになります。
何か不自然ですね。ではワイヤフレーム表示にしてみます。
なんと、色がついた 3 つの球はポリゴンによるものではなく、板ポリに適用されたマテリアルによって描画されたものでした。本物のポリゴンによる球は白だけだったのです。
趣旨
UE のマテリアルだけを使って、簡易的な球のレイトレーシングを実装するサンプルを作ってみました。 また、UE のマテリアルで利用可能な機能を活用して、以下のようなことが実現されています。
- マテリアル上に描画された空間が連続して見えるように、使用中の CameraActor 位置に基づいた描画を行う
- SkyLight の位置に基づいた、周囲のオブジェクトから浮かない簡易的な陰影づけ
実際に動かしてみると、こんな感じです。
完全にやってみたくてやった遊びなので実用性はあまりないですが、マテリアルのいろんな機能を使うサンプルとしては楽しいと思います。そんなに難しいことも出てきません。
環境
- Engine Version: 5.0.0 Early Access 2
作成に用いる考えと準備
レイトレーシングに使う座標系
レイトレーシングを行うには座標空間が必要です。今回は、マテリアルが適用されたオブジェクトの中心位置を、レイトレーシングで描画する内部座標系の原点と決めました。 (今後、レイトレーシングに使うマテリアル内部の空間の座標系のことは内部座標系と呼びます。) これにより、マテリアルを適用したオブジェクトごとに異なる座標系上でレイトレーシングが行われることになります。
座標変換
今回の実装では、内部座標系から見たら外側の、CameraActor の位置情報などをレイの生成に利用します。当然ですが、CameraActor などから取得できるのはワールド座標系での位置情報が原則です。この情報を内部座標系での処理に利用するには、ワールド座標系 → 内部座標系の座標変換が必要となります。
ここで対象とするの 2 つの座標系は、どちらも同じ大きさ(?)の直交座標系であるため、線形代数で登場するような座標変換の手法は必要ありません。原点位置をオフセットするような演算を行うだけでよく、これは減算で実現できます。
変換のために、ToRTSpace
というマテリアル関数を作成しました。ここで、TargetPosition
はワールド座標系上の変換元の点、ObjectOrigin
は今回の内部座標系の基準とする、ワールド座標系上でのオブジェクトの原点です。
今後、ワールド座標系 → 内部座標系の値の受け渡しで変換が必要なときに利用します。
レイを飛ばす始点と方向
レイトレーシングを行うためには、レイを飛ばさなければなりません。今回は image-order なレイトレーシングを考えるため、カメラ位置から、描画されるピクセル一つ一つに向かうようなレイを生成します。
すると、レイの始点はカメラ位置(ワールド座標系から内部座標系に変換されたもの)となり、レイの方向は「カメラ位置 → マテリアルが適用されたオブジェクト表面上の点位置」とできます。 この方向ベクトルはまたしても減算で手に入れることができます。得られた方向ベクトルは後々のために正規化しておきます。
レイの方向を得る処理は、CreateRayDirection
というマテリアル関数に記述しました。ここで、AbsoluteWorldPosition
はマテリアルが適用されたオブジェクトの表面上の点、CameraPosition
はカメラ位置です。
レイの始点を得る処理は、カメラ位置がそのまま始点と対応するため記述の必要がありません。
描画に必要な環境のパラメータを得る方法
カメラ位置やライト情報に基づいた描画をするには、それらの位置を取得せねばなりません。また、座標系構築のためにはマテリアルを適用したオブジェクトの中心位置が必要ですし、レイの射出方向を決めるにはそのオブジェクトの描画された表面上の任意の点の位置も必要です。
これらはすべて、UE の Material に標準で用意されているノードから取得することができます。
詳細は検索すれば出てくるので割愛するとして、概要を示します。
Camera Position
現在の Viewport の視点となっているカメラのワールド座標系における位置を取得できるノード。
SkyAtmosphereLightDirection
レベル上の DirectionalLight の方向ベクトルを取得できるノード。DirectionalLight は無限遠からの平行光線を想定した光源なので、位置に意味がなく、方向のみが取得できる。 今回の描画はこのライト情報に基づく。取得ライトのインデックスは、DirectionalLight の Atmosphere Sun Light Index プロパティと対応する。
Object Position
レベルに配置されたオブジェクトのバウンディングボックスから、その中心位置をワールド座標系に基づいて取得できるノード。
Absolute World Position (World Position)
オブジェクトの表面上の任意の点の、ワールド座標系での位置を取得できるノード。
球とレイの衝突判定
球とレイの衝突判定について詳説するには数学的な解説が必要となりますが、すでに先行する多くの有用な資料がありますので、ここでは詳説せず、経過のみを述べます。参考になりそうな資料のリンクを貼っておきます。
球は、その中心点と半径の 2 つのパラメータによって位置と形状を決定できます。これは、を球面上の任意の点、を球の中心点、を半径としたとき、以下の式で記述できることを示します。
は球面上の任意の点なので、この点がレイの線上のどこかの点と共有できるならば、球とレイが衝突したことになります。 共有する点が存在するかどうかは、単純に球のベクトル方程式にレイの方程式を代入して、レイの方程式に登場したについて解けば良いです。すると二次方程式になるので、二次方程式の解の公式に従って変形すると、以下のようになります。
ここで、について解いたの式の、ルートの内部に注目します。この式はルートの中に入っているため、値が負になってしまうと実数に解を持たなくなります。複素数の位置など登場しませんので、レイ上と球面上に共有する点はないとみなしてよいことになります。よって、ルート内の式の値を見て、それが以上であるかを確認するだでレイが球と衝突したかどうかを判別できることになります。これを判別式とします。
この判別式が以上であった場合のみ、衝突位置の計算などを行います。衝突位置は、について解いた式に衝突時のパラメータを代入し、求まったを元にレイのベクトル方程式からを求めればよいです。このとき、を求めるのに使う式のをに変更していますが、これはレイの始点から見て近いほうの共有点さえわかれば良いためです。
実際に作成する
レイ
レイを作成します。準備していおいたものを組み合わせて、以下のように記述できます。
ToRTSpace
でCamera Position
から取得したカメラのグローバル座標系上の位置を内部座標系の位置に変換し、Ray の始点の位置ベクトルを作成します。
CreateRayDirection
で、カメラ位置とオブジェクトの表面上の点の位置から、Ray の方向ベクトルを作成します。この方向ベクトルはカメラとオブジェクト上の点の相対的な位置に基づいており、グローバル座標系と内部座標系は基底が同じスケールの直交座標系であることから、座標変換を行わなくても適切な方向ベクトルを作成できます。
これで、レイを記述する以下の式が利用できます。 がレイの線上で取り得る任意の点、がレイの始点、がレイの方向です。 がパラメータとして変化することで、レイの線上の任意の点を指し示すことができるということです。 また、レイは視線と逆方向に飛ばしても意味がないので、とします。
球の衝突判定 Custom ノード
衝突判定くらいの複雑度をもつ処理になってくると、ノードで書いた際の可読性の低下が著しくなってきます。ここでは Custom ノードを用いて HLSL コードとして記述していくことにします。
Custom ノードは以下のようなインターフェイスとしました。
入力
RayOrigin
判定を行うレイの始点を示す位置ベクトル
RayDirection
判定を行うレイの方向ベクトル
TMax
レイのベクトル方程式に登場した変数が取り得る最大の値。
が大きくなると、レイの式はより遠くの点を示すようになります。逆に、TMax
での最大値を制限すると、それよりも遠方にある衝突点には衝突しないようにすることができます。これは、一直線上に複数のオブジェクトが存在している際の、重なり処理を行うために導入しています。
Center
球の中心点
Radius
球の半径
出力
return
衝突判定を 0/1 で表す Custom ノードはデフォルト戻り値の名称を変更できないためわかりにくくなっています。
T
衝突した場合、衝突したポイントでのレイの方程式のの値を返す。衝突しなかった場合、入力のTMax
の値をそのまま出力する。
この出力を次回以降の同一レイの衝突判定ではTMax
として使うことで、同一レイの線上に異なる物体が発見されたとしても、より近い場所になければ判定を無視することが可能になります。
コード
HLSL によるコードはとても短いです。
float3 OC = RayOrigin - Center;
float A = dot(RayDirection, RayDirection);
float B = dot(RayDirection, OC);
float C = dot(OC, OC) - pow(Radius, 2);
float D = pow(B, 2) - A*C;
if (D >= 0) {
float curT = (-B - sqrt(D)) / A;
if (curT > 0 && curT < TMax) {
T = curT;
return 1;
}
}
T = TMax;
return 0;
衝突位置の関連値を計算
ひとつ上で作成した衝突判定を包む形で、関連する値を計算します。ここでは、内部座標系における衝突位置、衝突位置での球面の法線などを求めます。簡単な計算ですので、Sphere
というマテリアル関数にノードで実装しています。
以下で各値の求め方を記載します。ノードはこれに対応しているだけです。
Hit
衝突判定関数の戻り値をそのまま使っているだけです。
Position
衝突判定関数から得られた、T
のパラメータをレイの方程式に代入することで、衝突位置を得ることができます。
Normal
球面上のレイ衝突点の法線ベクトルです。 法線は面に対して垂直な直線ですから、球の場合には球の中心から球面上の点に向かうベクトルはすべて、その点での法線と同じ方向のベクトルとなります。 正規化もしておきます。
T
これも衝突判定関数の戻り値をそのまま使っているだけです。
簡易シェーディングマテリアルマテリアル関数
ここまで作成したもので、ある視点から見たときの球の形状は判定できるようになりました。ここでは球のシェーディングマテリアルを作成していきます。
シェーディングは二次的なレイのトレースによるものが望ましいですが、今回はライト方向と法線の内積を用いた簡易的なものにしました。
きちんとしたレイトレースによる色付けを考える場合にはノードだと(主に繰り返し処理が)厳しいので、もっと広範囲を HLSL で記述したほうが良いと思いますが、今回は趣旨と外れるのでやめておきました。
以下がシェーディング用のDotShading
マテリアル関数です。
Color
ここでは、をライト方向として導入します。また、はその点を塗るベースカラーです。
ただし、ノード上では陰影の濃度の調整のために少し処理を追加しています。Add
で乗算する内積値の下限を底上げしているだけですが、そのままでは内積値が負の値を持ってしまう特性で意味がなくなってしまうので、事前にsaturate
しています。
saturate
は現代の GPU ではノーコストで実行できるので、へのクランプではclamp
よりおすすめです。
複数オブジェクトの描画
先ほど作成した球の描画マテリアル関数を使えば、もう単一の球を描画することができます。しかし、せっかくなので複数のオブジェクトを描画できるようにしたいです。複数オブジェクト描画用のマテリアル関数を考えていきます。このマテリアル関数はRender
という名称とします。
この関数はここまでの作業の集大成となっています。処理の流れは以下のようなものです。
- レイ情報(
RayOrigin
,RayDirection
)、ライト方向(LightDirection
)、レイの値の初期値(TMax
)を入力として受け取る。 Sphere
関数で球の衝突判定をする。DotShading
関数でシェーディングを行い、色を決定する。- 以上の処理を 1 単位としてカスケード接続し、衝突判定結果に応じて色とノーマルを
Lerp
で合成、全体でのオブジェクトの衝突位置をAdd
で統合する。 - 描画全体の衝突判定結果を
Hit
、描画結果をColor
、ノーマルの描画結果をNormal
、この描画を通して得られたレイの最短衝突位置(を導ける変数)であるをT
として出力する。
実際に外部の環境情報を入力するマテリアル
ここまでの処理はみなマテリアル関数として記述してきましたが、すでにレイトレースに必要な処理は出揃ったので、実際にオブジェクトに適用するマテリアルを作成します。このマテリアルでは、実際のライト方向やカメラ位置などの環境情報を入力します。マテリアル名はRayTracing
としました。
このマテリアルでの描画はマテリアルによって実装されているので、エンジンが持っているライティングによってシェーディングを受ける必要はありません。
そのため、マテリアルの Shading Model はUnlit
にしてあります。
また、Blend Mode をMasked
とし、Render
マテリアル関数のHit
出力をOpacity Mask
に入力してみました。これによって、衝突が検出されなかった場所はマスクされることになり、あたかもレイトレースで描画したオブジェクトがそこに存在するかのように見せることができます。
その他、ノードの接続については、ここまでで解説したものを適切につなげているだけです。
以下はマテリアルエディタ上の View で板ポリに描画されたRaytracing
マテリアルです。
3 つの球があります!(板です)
まとめ
こうして作成されたマテリアルをそこらのメッシュに適用すれば、そのメッシュの輪郭の内側をスクリーンとして、実装したレイトレースによるオブジェクトを描画できます。 どのように見えるかは、記事冒頭の動画をご参照ください。 使いみちは不明ですが、UE のマテリアルの機能を色々使えて楽しいです。おわりです。