UnityでのGPUレイトレーシング-パート3

[ 最初2番目の部分。]









今日は大きな飛躍を遂げます。 先ほどトレースした排他的な球面構造と無限平面から離れ、三角形を追加します。これは、すべての仮想世界を構成する要素である現代のコンピューターグラフィックスの本質です。 前回の作業を続けたい場合は、パート2のコードを使用してください。 今日行うことの準備ができたコードは、 ここから入手できます 。 さあ始めましょう!



三角形



三角形は、3つの接続された頂点の単なるリストであり、各頂点は位置を保持し、場合によっては正常です。 あなたの視点からの頂点の横断の順序は、私たちが見ているもの、つまり三角形の前面または背面を決定します。 伝統的に、「前」は反時計回りのトラバース順序と見なされます。



まず、光線が三角形と交差するかどうか、また交差する場合はどのポイントで交差するかを判断できる必要があります。 レイと三角形の交差を決定するための非常に人気のある(しかし確かに唯一ではない )アルゴリズムは、1997年に紳士Thomas Akenin-MellerとBen Tremborによって提案されました。 詳細については、こちらの記事「高速、最小ストレージレイと三角形の交差点」を参照してください



この記事のコードは、HLSLシェーダーコードに簡単に移植できます。



static const float EPSILON = 1e-8; bool IntersectTriangle_MT97(Ray ray, float3 vert0, float3 vert1, float3 vert2, inout float t, inout float u, inout float v) { // find vectors for two edges sharing vert0 float3 edge1 = vert1 - vert0; float3 edge2 = vert2 - vert0; // begin calculating determinant - also used to calculate U parameter float3 pvec = cross(ray.direction, edge2); // if determinant is near zero, ray lies in plane of triangle float det = dot(edge1, pvec); // use backface culling if (det < EPSILON) return false; float inv_det = 1.0f / det; // calculate distance from vert0 to ray origin float3 tvec = ray.origin - vert0; // calculate U parameter and test bounds u = dot(tvec, pvec) * inv_det; if (u < 0.0 || u > 1.0f) return false; // prepare to test V parameter float3 qvec = cross(tvec, edge1); // calculate V parameter and test bounds v = dot(ray.direction, qvec) * inv_det; if (v < 0.0 || u + v > 1.0f) return false; // calculate t, ray intersects triangle t = dot(edge2, qvec) * inv_det; return true; }
      
      





この関数を使用するには、光線と三角形の3つの頂点が必要です。 戻り値は、三角形が交差したかどうかを示しています。 交差の場合、3つの追加の値が計算されます: t



はビームに沿って交差点までの距離を表し、 u



/ v



は三角形上の交差点の位置を決定する3つの重心座標のうちの2つです(最後の座標はw = 1 - u - v



として計算できます)。 重心座標にまだ慣れていない場合は、 Scratchapixelの優れた説明をお読みください。



あまり遅延することなく、コードに示された頂点を持つ1つの三角形をトレースしましょう! シェーダーでTrace



関数を見つけて、次のコードフラグメントを追加します。



 // Trace single triangle float3 v0 = float3(-150, 0, -150); float3 v1 = float3(150, 0, -150); float3 v2 = float3(0, 150 * sqrt(2), -150); float t, u, v; if (IntersectTriangle_MT97(ray, v0, v1, v2, t, u, v)) { if (t > 0 && t < bestHit.distance) { bestHit.distance = t; bestHit.position = ray.origin + t * ray.direction; bestHit.normal = normalize(cross(v1 - v0, v2 - v0)); bestHit.albedo = 0.00f; bestHit.specular = 0.65f * float3(1, 0.4f, 0.2f); bestHit.smoothness = 0.9f; bestHit.emission = 0.0f; } }
      
      





前述したように、 t



はビームに沿った距離を保存し、この値を直接使用して交差点を計算できます。 正しい反射を計算するために重要な法線は、三角形の2つのエッジのベクトル積を使用して計算できます。 ゲームモードを起動し、最初にトレースされた三角形に感心します。









演習:距離ではなく重心座標を使用して位置を計算してください。 すべてを正しく行うと、光沢のある三角形は以前とまったく同じように見えます。



三角形メッシュ



最初の障害を克服しましたが、三角形からメッシュ全体をトレースすることはまったく別の話です。 最初に、メッシュに関するいくつかの基本情報を学ぶ必要があります。 それらを知っていれば、次の段落を安全にスキップできます。



コンピューターグラフィックスでは、メッシュはいくつかのバッファーで定義されますが、その中で最も重要なのは頂点バッファーとインデックスバッファーです。 頂点バッファー、オブジェクト空間内の各頂点の位置を記述する3Dベクトルのリストです(これは、オブジェクトを移動、回転、スケーリングするときにそのような値を変更する必要がないことを意味します-それらは、行列乗算を使用して、オブジェクトの空間からワールド空間に即座に変換されます ) 。 インデックスバッファは、頂点バッファを指すインデックスである整数値のリストです。 3つのインデックスごとに三角形が構成されます。 たとえば、インデックスバッファーの形式が[0、1、2、0、2、3]の場合、2つの三角形があります。最初の三角形は頂点バッファーの最初、2番目、3番目の頂点で構成され、2番目の三角形は1番目、3番目で構成されますおよび4番目のピーク。 したがって、インデックスバッファは前述の走査順序も定義します。 頂点バッファーとインデックスに加えて、各頂点に他の情報を追加する追加のバッファーが存在する場合があります。 最も一般的な追加のバッファーには、 法線テクスチャ座標texcoordsまたは単にUVと呼ばれる)、および頂点の色が格納されます



GameObjectsを使用する



まず、どのGameObjectがレイトレーシングプロセスの一部になるべきかを調べる必要があります。 素朴な解決策は、単にFindObjectOfType<MeshRenderer>()



使用することですが、より柔軟で高速な処理を行います。 新しいRayTracingObject



コンポーネントを追加しましょう:



 using UnityEngine; [RequireComponent(typeof(MeshRenderer))] [RequireComponent(typeof(MeshFilter))] public class RayTracingObject : MonoBehaviour { private void OnEnable() { RayTracingMaster.RegisterObject(this); } private void OnDisable() { RayTracingMaster.UnregisterObject(this); } }
      
      





このコンポーネントは、レイトレーシングに使用する各オブジェクトに追加され、それらをRayTracingMaster



登録します。 ウィザードに次の機能を追加します。



 private static bool _meshObjectsNeedRebuilding = false; private static List<RayTracingObject> _rayTracingObjects = new List<RayTracingObject>(); public static void RegisterObject(RayTracingObject obj) { _rayTracingObjects.Add(obj); _meshObjectsNeedRebuilding = true; } public static void UnregisterObject(RayTracingObject obj) { _rayTracingObjects.Remove(obj); _meshObjectsNeedRebuilding = true; }
      
      





すべてが順調に進んでいます-これで、トレースする必要のあるオブジェクトがわかりました。 しかし、次の部分は難しい部分です:Unityメッシュ(マトリックス、頂点バッファー、インデックス-すべて覚えていますか?)からすべてのデータを収集し、独自のデータ構造に書き込み、シェーダーがそれらを使用できるようにGPUにロードします。 ウィザードで、C#側のデータ構造とバッファを定義することから始めましょう。



 struct MeshObject { public Matrix4x4 localToWorldMatrix; public int indices_offset; public int indices_count; } private static List<MeshObject> _meshObjects = new List<MeshObject>(); private static List<Vector3> _vertices = new List<Vector3>(); private static List<int> _indices = new List<int>(); private ComputeBuffer _meshObjectBuffer; private ComputeBuffer _vertexBuffer; private ComputeBuffer _indexBuffer;
      
      





...そして、シェーダーでも同じことをしましょう。 慣れていますか?



 struct MeshObject { float4x4 localToWorldMatrix; int indices_offset; int indices_count; }; StructuredBuffer<MeshObject> _MeshObjects; StructuredBuffer<float3> _Vertices; StructuredBuffer<int> _Indices;
      
      





データ構造の準備ができており、実際のデータでそれらを埋めることができます。 すべてのメッシュのすべての頂点を1つの大きなList<Vector3>



に収集し、すべてのインデックスを大きなList<int>



収集します。 頂点に問題はありませんが、インデックスを変更して、大きなバッファ内の正しい頂点を指し続ける必要があります。 すでに1000個の頂点からオブジェクトを追加し、今度は単純なメッシュキューブを追加したことを想像してください。 最初の三角形はインデックス[0、1、2]で構成されますが、バッファにすでに1000個の頂点があるため、キューブに頂点を追加する前にインデックスをシフトする必要があります。 つまり、[1000、1001、1002]になります。 コードでは次のようになります。



 private void RebuildMeshObjectBuffers() { if (!_meshObjectsNeedRebuilding) { return; } _meshObjectsNeedRebuilding = false; _currentSample = 0; // Clear all lists _meshObjects.Clear(); _vertices.Clear(); _indices.Clear(); // Loop over all objects and gather their data foreach (RayTracingObject obj in _rayTracingObjects) { Mesh mesh = obj.GetComponent<MeshFilter>().sharedMesh; // Add vertex data int firstVertex = _vertices.Count; _vertices.AddRange(mesh.vertices); // Add index data - if the vertex buffer wasn't empty before, the // indices need to be offset int firstIndex = _indices.Count; var indices = mesh.GetIndices(0); _indices.AddRange(indices.Select(index => index + firstVertex)); // Add the object itself _meshObjects.Add(new MeshObject() { localToWorldMatrix = obj.transform.localToWorldMatrix, indices_offset = firstIndex, indices_count = indices.Length }); } CreateComputeBuffer(ref _meshObjectBuffer, _meshObjects, 72); CreateComputeBuffer(ref _vertexBuffer, _vertices, 12); CreateComputeBuffer(ref _indexBuffer, _indices, 4); }
      
      





OnRenderImage



関数でRebuildMeshObjectBuffers



を呼び出し、 RebuildMeshObjectBuffers



で新しいバッファーを解放することを忘れないでください。 バッファの処理を少し簡略化するために上記のコードで使用した2つのヘルパー関数を次に示します。



 private static void CreateComputeBuffer<T>(ref ComputeBuffer buffer, List<T> data, int stride) where T : struct { // Do we already have a compute buffer? if (buffer != null) { // If no data or buffer doesn't match the given criteria, release it if (data.Count == 0 || buffer.count != data.Count || buffer.stride != stride) { buffer.Release(); buffer = null; } } if (data.Count != 0) { // If the buffer has been released or wasn't there to // begin with, create it if (buffer == null) { buffer = new ComputeBuffer(data.Count, stride); } // Set data on the buffer buffer.SetData(data); } } private void SetComputeBuffer(string name, ComputeBuffer buffer) { if (buffer != null) { RayTracingShader.SetBuffer(0, name, buffer); } }
      
      





素晴らしい、バッファを作成し、必要なデータで満たされています! これをシェーダーに報告するだけです。 SetShaderParameters



次のコードを追加します(新しいヘルパー関数のおかげで、スフィアバッファーのコードを削減できます)。



 SetComputeBuffer("_Spheres", _sphereBuffer); SetComputeBuffer("_MeshObjects", _meshObjectBuffer); SetComputeBuffer("_Vertices", _vertexBuffer); SetComputeBuffer("_Indices", _indexBuffer);
      
      





だから、作業は退屈ですが、今やったことを見てみましょう:メッシュのすべての内部データ(マトリックス、頂点、インデックス)を収集し、それらを便利でシンプルな構造に配置し、GPUに送信します。使用できます。



メッシュトレース



彼を待たせないようにしましょう。 シェーダーには、単一の三角形のトレースコードが既にあり、メッシュは実際には多くの三角形です。 ここでの唯一の新しい側面は、マトリックスを使用して、組み込みmul



関数(乗算の略)を使用して頂点をオブジェクト空間からワールド空間に変換することです。 マトリックスには、オブジェクトの平行移動、回転、スケールが含まれます。 サイズは4×4なので、乗算には4dベクトルが必要です。 最初の3つのコンポーネント(x、y、z)は、頂点バッファーから取得されます。 ポイントを扱っているため、4番目のコンポーネント(w)を1に設定します。 これが方向である場合は、0を書き込んで、マトリックス内のすべての変換とスケールを無視します。 これは混乱していますか? 次に、 このチュートリアルを少なくとも8回読んでください 。 シェーダーコードは次のとおりです。



 void IntersectMeshObject(Ray ray, inout RayHit bestHit, MeshObject meshObject) { uint offset = meshObject.indices_offset; uint count = offset + meshObject.indices_count; for (uint i = offset; i < count; i += 3) { float3 v0 = (mul(meshObject.localToWorldMatrix, float4(_Vertices[_Indices[i]], 1))).xyz; float3 v1 = (mul(meshObject.localToWorldMatrix, float4(_Vertices[_Indices[i + 1]], 1))).xyz; float3 v2 = (mul(meshObject.localToWorldMatrix, float4(_Vertices[_Indices[i + 2]], 1))).xyz; float t, u, v; if (IntersectTriangle_MT97(ray, v0, v1, v2, t, u, v)) { if (t > 0 && t < bestHit.distance) { bestHit.distance = t; bestHit.position = ray.origin + t * ray.direction; bestHit.normal = normalize(cross(v1 - v0, v2 - v0)); bestHit.albedo = 0.0f; bestHit.specular = 0.65f; bestHit.smoothness = 0.99f; bestHit.emission = 0.0f; } } } }
      
      





すべてが実際に動作しているのを見るには、ほんの一歩です。 Trace



関数を少し再構築して、メッシュオブジェクトのトレースを追加しましょう。



 RayHit Trace(Ray ray) { RayHit bestHit = CreateRayHit(); uint count, stride, i; // Trace ground plane IntersectGroundPlane(ray, bestHit); // Trace spheres _Spheres.GetDimensions(count, stride); for (i = 0; i < count; i++) { IntersectSphere(ray, bestHit, _Spheres[i]); } // Trace mesh objects _MeshObjects.GetDimensions(count, stride); for (i = 0; i < count; i++) { IntersectMeshObject(ray, bestHit, _MeshObjects[i]); } return bestHit; }
      
      





結果



以上です! いくつかの単純なメッシュ(Unityプリミティブは問題ありません)を追加し、それらにRayTracingObject



コンポーネントを与えて、魔法を見てみましょう。 まだ詳細なメッシュ(数百以上の三角形)を使用しないでください ! 私たちのシェーダーには最適化が欠けており、やりすぎると、ピクセルあたり少なくとも1つのサンプルをトレースするのに数秒から数分かかることがあります。 その結果、システムはGPUドライバーを停止し、Unityエンジンがクラッシュする可能性があり、コンピューターを再起動する必要があります。









メッシュにはスムーズではなく、フラットシェーディングがあることに注意してください。 頂点の法線をバッファにまだロードしていないため、各三角形の頂点の法線を取得するには、ベクトル積を実行する必要があります。 さらに、三角形の領域を補間することはできません。 この問題は、チュートリアルの次の部分で対処します。



興味のために、 Morgan McGwireアーカイブからStanford Bunnyをダウンロードし、 Blenderパッケージのデシメート修飾子を使用して、頂点の数を431に減らしました。IntersectMeshObjectシェーダー関数で照明パラメーターとハードコーディングされたマテリアルを試すことができます。 Grafitti Shelterの美しいソフトシャドウと少し拡散したグローバルライティングを備えた誘電ウサギです。









...そして、ケープヒルの強い指向性の光の下で、床面にディスコのまぶしさを放つ金属のウサギがいます:









...そして、青い空の下で大きな石スザンヌの下に隠れている2匹の小さなウサギKiara 9 Dusk (インデックスシフトがゼロかどうかを確認しながら、2番目のオブジェクトの代替マテリアルを指定しました):









次は?



自分のトレーサーで実際のメッシュを初めて見るのは素晴らしいことですよね? 今日、データを処理し、Meller-Tramborアルゴリズムを使用して交差点を見つけ、UnityエンジンのGameObjectsをすぐに使用できるようにすべてを収集しました。 さらに、レイトレーシングの利点の1つを確認しました。新しい交差点をコードに追加するとすぐに、すべての美しい効果(ソフトシャドウ、反射および拡散グローバルライティングなど)がすぐに機能し始めます。



光沢のあるウサギのレンダリングにはかなりの時間がかかりましたが、最も明らかなノイズを取り除くには少しフィルタリングを使用する必要がありました。 この問題を解決するために、通常、シーンは空間構造、たとえばグリッド、K次元ツリー、または境界ボリュームの階層に書き込まれます。これにより、大きなシーンのレンダリング速度が大幅に向上します。



ただし、順番に移動する必要があります。さらに、法線の問題を排除して、メッシュ(低ポリメッシュであっても)が現在よりも滑らかに見えるようにします。 また、オブジェクトを移動するときにマトリックスを自動的に更新し、Unityマテリアルを直接参照して、コードで記述するだけでなく、それも便利です。 これが、チュートリアルシリーズの次のパートで行うことです。 読んでくれてありがとう、そしてパート4でお会いしましょう!



All Articles