UnityのGPUレイトレーシング

レイトレーシングには驚くべき時代が来ました。 NVIDIAはAI加速ノイズリダクションを実装し、Microsoft はDirectX 12のネイティブサポートを発表し、ピーターシャーリーは本を無料で販売して いますお好きなものをお支払いください )。 光線追跡はついに法廷で受け入れられる機会を得たようです。 革命の始まりについて話すのは時期尚早かもしれませんが、この分野の知識を学び、蓄積し始めることは間違いなく価値があります。



この記事では、Unityでゼロから計算シェーダーを使用した非常にシンプルなレイトレーサーを作成します。 C#でスクリプトを作成し、HLSLでシェーダーを作成します。 すべてのコードはBitbucketにアップロードされます。



その結果、次のようにレンダリングできます。









光線追跡理論



まず、レイトレーシング理論の基本の簡単な概要から始めたいと思います。 よく知っている場合は、このセクションを安全にスキップできます。



現実世界で写真がどのように表示されるか想像してみましょう-非常に単純化されていますが、これはレンダリングを説明するのに十分です。 すべては、光子を放出する光源から始まります。 光子は、表面に衝突するまで直線的に飛行し、その後反射または屈折し、さらに移動を続け、表面に吸収されたエネルギーの一部を失います。 遅かれ早かれ、フォトンの一部がカメラのセンサーに落ちて、完成した画像が作成されます。 基本的に、レイトレーシング手順はこれらの手順を模倣して、写実的な画像を作成します。



実際には、光源から放出される光子のごく一部のみがカメラに到達します。 したがって、 可逆ハイメンコルツ原理を使用して計算は逆の順序で実行されます。光源から光子を放出する代わりに、光線はカメラからシーンに放出され、反射または屈折され、最終的に光源に到達します。



作成するレイトレーサーは、Turner Whitedによる1980年の記事に基づいてます。 シャープな影をシミュレートし、反射を完全に修正できます。 さらに、トレーサーは、屈折、拡散グローバルイルミネーション、鮮やかな反射、ソフトシャドウなどのより複雑な効果の実装の基礎として機能します。



基本



新しいUnityプロジェクトを作成することから始めましょう。 C# RayTracingMaster.cs



を作成し、シェーダーRayTracingShader.compute



を計算します。 次の基本コードをC#スクリプトに貼り付けます。



 using UnityEngine; public class RayTracingMaster : MonoBehaviour { public ComputeShader RayTracingShader; private RenderTexture _target; private void OnRenderImage(RenderTexture source, RenderTexture destination) { Render(destination); } private void Render(RenderTexture destination) { // Make sure we have a current render target InitRenderTexture(); // Set the target and dispatch the compute shader RayTracingShader.SetTexture(0, "Result", _target); int threadGroupsX = Mathf.CeilToInt(Screen.width / 8.0f); int threadGroupsY = Mathf.CeilToInt(Screen.height / 8.0f); RayTracingShader.Dispatch(0, threadGroupsX, threadGroupsY, 1); // Blit the result texture to the screen Graphics.Blit(_target, destination); } private void InitRenderTexture() { if (_target == null || _target.width != Screen.width || _target.height != Screen.height) { // Release render texture if we already have one if (_target != null) _target.Release(); // Get a render target for Ray Tracing _target = new RenderTexture(Screen.width, Screen.height, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear); _target.enableRandomWrite = true; _target.Create(); } } }
      
      





カメラがレンダリングを完了すると、 OnRenderImage



関数OnRenderImage



Unityによって自動的に呼び出されます。 レンダーするには、まず適切なサイズでレンダーターゲットを作成し、これについてコンピューティングシェーダーに通知する必要があります。 0は、計算シェーダーカーネル関数のインデックスです。1つしかありません。



次にシェーダーを渡します。 これは、シェーダーコードを実行するスレッドのグループを処理するようGPUに要求することを意味します。 スレッドの各グループはいくつかのスレッドで構成され、その数はシェーダー自体に設定されます。 スレッドグループのサイズと数は3次元で示すことができるため、任意の次元のタスクに計算シェーダーを簡単に適用できます。 この場合、ターゲットレンダーのピクセルごとに1つのストリームを作成する必要があります。 コンピューティングシェーダーUnityテンプレートで指定されているデフォルトのスレッドグループサイズは[numthreads(8,8,1)]



であるため、それに固執し、8×8ピクセルごとに1つのスレッドグループを作成します。 最後に、 Graphics.Blit



を使用して結果を画面に書き込みます。



プログラムを確認しましょう。 RayTracingMaster



コンポーネントをシーンRayTracingMaster



追加し(これはOnRenderImage



呼び出すときに重要です)、計算シェーダーを割り当て、再生モードを開始します。 計算シェーダーUnityテンプレートの出力は、美しい三角形のフラクタルとして表示されるはずです。



カメラ



画面に画像を表示できるようになったので、カメラ光線を生成しましょう。 Unityは完全に機能するカメラを提供するため、計算されたマトリックスを使用できます。 シェーダーでマトリックスを設定することから始めましょう。 RayTracingMaster.cs



スクリプトに次の行を追加します。



 private Camera _camera; private void Awake() { _camera = GetComponent<Camera>(); } private void SetShaderParameters() { RayTracingShader.SetMatrix("_CameraToWorld", _camera.cameraToWorldMatrix); RayTracingShader.SetMatrix("_CameraInverseProjection", _camera.projectionMatrix.inverse); }
      
      





レンダリングする前に、 OnRenderImage



からSetShaderParameters



を呼び出します。



シェーダーでは、マトリックス、 Ray



構造、および構築関数を定義します。 HLSLでは、C#とは異なり、関数または変数を使用するに宣言する必要があることに注意してください。 各画面ピクセルの中心について、ビームのソースと方向を計算し、後者を色として表示します。 シェーダー全体は次のようになります。



 #pragma kernel CSMain RWTexture2D<float4> Result; float4x4 _CameraToWorld; float4x4 _CameraInverseProjection; struct Ray { float3 origin; float3 direction; }; Ray CreateRay(float3 origin, float3 direction) { Ray ray; ray.origin = origin; ray.direction = direction; return ray; } Ray CreateCameraRay(float2 uv) { // Transform the camera origin to world space float3 origin = mul(_CameraToWorld, float4(0.0f, 0.0f, 0.0f, 1.0f)).xyz; // Invert the perspective projection of the view-space position float3 direction = mul(_CameraInverseProjection, float4(uv, 0.0f, 1.0f)).xyz; // Transform the direction from camera to world space and normalize direction = mul(_CameraToWorld, float4(direction, 0.0f)).xyz; direction = normalize(direction); return CreateRay(origin, direction); } [numthreads(8,8,1)] void CSMain (uint3 id : SV_DispatchThreadID) { // Get the dimensions of the RenderTexture uint width, height; Result.GetDimensions(width, height); // Transform pixel to [-1,1] range float2 uv = float2((id.xy + float2(0.5f, 0.5f)) / float2(width, height) * 2.0f - 1.0f); // Get a ray for the UVs Ray ray = CreateCameraRay(uv); // Write some colors Result[id.xy] = float4(ray.direction * 0.5f + 0.5f, 1.0f); }
      
      





インスペクターでカメラを回転させてみてください。 「色付きの空」がそれに応じて動作することがわかります。



次に、色を実際のスカイボックスに置き換えましょう。 私の例では、HDRI Haven WebサイトのCape Hillを使用しますが、もちろん、他のものを選択することもできます。 ダウンロードしてUnityにドラッグします。 インポートパラメータで、ダウンロードしたファイルの解像度が2048を超える場合は、最大解像度を増やすことを忘れないでくださいpublic Texture SkyboxTexture



テクスチャスクリプトを追加し、インスペクタでテクスチャを割り当て、この行をSetShaderParameters



関数に追加してシェーダに設定します。



 RayTracingShader.SetTexture(0, "_SkyboxTexture", SkyboxTexture);
      
      





シェーダーで、テクスチャと対応するサンプラー、およびすぐに使用する定数を定義します。



 Texture2D<float4> _SkyboxTexture; SamplerState sampler_SkyboxTexture; static const float PI = 3.14159265f;
      
      





ここで、方向の色を記録する代わりに、スカイボックスをサンプリングします。 これを行うには、 デカルト方向ベクトルを球面座標に変換し、テクスチャ座標に関連付けます。 CSMain



最後の部分を次のものにCSMain



ます。



 // Sample the skybox and write it float theta = acos(ray.direction.y) / -PI; float phi = atan2(ray.direction.x, -ray.direction.z) / -PI * 0.5f; Result[id.xy] = _SkyboxTexture.SampleLevel(sampler_SkyboxTexture, float2(phi, theta), 0);
      
      





トレース



これまでのところ良い。 次に、レイトレーシング自体を開始します。 数学的には、ビームとシーンのジオメトリ間の交差を計算し、衝突パラメータ(ビームに沿った位置、法線、距離)を保存できます。 ビームが複数のオブジェクトと衝突する場合、最も近いものを選択します。 シェーダーでstruct RayHit



を定義しましょう:



 struct RayHit { float3 position; float distance; float3 normal; }; RayHit CreateRayHit() { RayHit hit; hit.position = float3(0.0f, 0.0f, 0.0f); hit.distance = 1.#INF; hit.normal = float3(0.0f, 0.0f, 0.0f); return hit; }
      
      





通常、シーンは多くの三角形で構成されていますが、単純なものから始めます。地球の無限平面と複数の球体の交差点からです!



グランドプレーン



線の無限平面との交点の計算 y=0 -かなり簡単なタスク。 ただし、ビームの正方向の衝突のみを考慮し、潜在的な以前の衝突よりも近くないすべての衝突を破棄します。



デフォルトでは、HLSLのパラメーターは参照ではなく値で渡されるため、コピーのみを処理し、呼び出し元の関数に変更を渡すことはできません。 元の構造体を変更できるように、 inout



修飾子を付けてRayHit bestHit



を渡します。 シェーダーコードは次のようになります。



 void IntersectGroundPlane(Ray ray, inout RayHit bestHit) { // Calculate distance along the ray where the ground plane is intersected float t = -ray.origin.y / ray.direction.y; if (t > 0 && t < bestHit.distance) { bestHit.distance = t; bestHit.position = ray.origin + t * ray.direction; bestHit.normal = float3(0.0f, 1.0f, 0.0f); } }
      
      





それを使用するには、 Trace



wireframe関数を追加しましょう(どれだけ拡張するか):



 RayHit Trace(Ray ray) { RayHit bestHit = CreateRayHit(); IntersectGroundPlane(ray, bestHit); return bestHit; }
      
      





さらに、基本的なシェーダー関数が必要です。 ここで、 inout



を使用してRay



再度渡します。後で反射について話すときに変更します。 デバッグの目的で、ジオメトリとの衝突では法線を返し、それ以外の場合はスカイボックスサンプリングコードに戻ります。



 float3 Shade(inout Ray ray, RayHit hit) { if (hit.distance < 1.#INF) { // Return the normal return hit.normal * 0.5f + 0.5f; } else { // Sample the skybox and write it float theta = acos(ray.direction.y) / -PI; float phi = atan2(ray.direction.x, -ray.direction.z) / -PI * 0.5f; return _SkyboxTexture.SampleLevel(sampler_SkyboxTexture, float2(phi, theta), 0).xyz; } }
      
      





CSMain



両方の機能を使用しCSMain



。 スカイボックスのサンプルコードをまだ削除していない場合は削除し、次の行を追加してビームを追跡し、衝突を不明瞭にします。



 // Trace and shade RayHit hit = Trace(ray); float3 result = Shade(ray, hit); Result[id.xy] = float4(result, 1);
      
      





球体



飛行機は世界で最も興味深いオブジェクトではないので、すぐに球体を追加しましょう。 線と球の交点の数学的計算は、 ウィキペディアで見つけることができます。 今回は、ビーム衝突の2つのオプション、入力ポイントp1 - p2



と出力ポイントp1 + p2



ます。 最初にエントリポイントをチェックし、もう一方が適合しない場合は出口ポイントを使用します。 この場合、球体は、位置(xyz)と半径(w)で構成されるfloat4



の値として定義されます。 コードは次のようになります。



 void IntersectSphere(Ray ray, inout RayHit bestHit, float4 sphere) { // Calculate distance along the ray where the sphere is intersected float3 d = ray.origin - sphere.xyz; float p1 = -dot(ray.direction, d); float p2sqr = p1 * p1 - dot(d, d) + sphere.w * sphere.w; if (p2sqr < 0) return; float p2 = sqrt(p2sqr); float t = p1 - p2 > 0 ? p1 - p2 : p1 + p2; if (t > 0 && t < bestHit.distance) { bestHit.distance = t; bestHit.position = ray.origin + t * ray.direction; bestHit.normal = normalize(bestHit.position - sphere.xyz); } }
      
      





球体を追加するには、 Trace



からこの関数を呼び出します。たとえば、次のようになります。



 // Add a floating unit sphere IntersectSphere(ray, bestHit, float4(0, 3.0f, 0, 1.0f));
      
      





スムージング



使用されるアプローチには1つの問題があります。各ピクセルの中心のみをチェックするため、結果として歪み(ugいラダー)が顕著になります。 この問題を回避するために、1つではなく、ピクセルごとに複数の光線をトレースします。 各光線は、ピクセル領域内でランダムなオフセットを受け取ります。 許容レベルのフレームレートを維持するために、プログレッシブサンプリングを実行します。つまり、フレームごとにピクセルごとに1つのビームをトレースし、カメラが動かない場合は時間をかけて平均します。 カメラを移動するたびに(または、可視性、ステージジオメトリ、照明などの他のパラメータを変更するたびに)、最初からやり直す必要があります。



いくつかの結果を追加するために使用する、非常にシンプルなイメージエフェクトシェーダーを作成しましょう。 このシェーダーAddShader



を呼び出し、最初の行にShader "Hidden/AddShader"



があることを確認します。 Cull Off ZWrite Off ZTest Always



Cull Off ZWrite Off ZTest Always



Blend SrcAlpha OneMinusSrcAlpha



Cull Off ZWrite Off ZTest Always



追加して、アルファブレンディングを有効にします。 次に、 frag



関数を次の行に置き換えます。



 float _Sample; float4 frag (v2f i) : SV_Target { return float4(tex2D(_MainTex, i.uv).rgb, 1.0f / (_Sample + 1.0f)); }
      
      





これで、このシェーダーは単純に最初のサンプルを不透明度でレンダリングします 1 次に不透明度あり  frac12 それから  frac13 等、すべてのサンプルを同じ重みで平均化します。



スクリプトでは、サンプルを読み取り、イメージエフェクトシェーダーを適用する必要があります。



 private uint _currentSample = 0; private Material _addMaterial;
      
      





また、 InitRenderTexture



ターゲットレンダーを再構築する場合、 _currentSamples = 0



をリセットし、カメラ変換の変更を認識するUpdate



関数を追加する必要がありUpdate







 private void Update() { if (transform.hasChanged) { _currentSample = 0; transform.hasChanged = false; } }
      
      





シェーダーを使用するには、マテリアルを初期化し、現在のサンプルについて伝え、それを使用してRender



関数の画面に挿入する必要があります。



 // Blit the result texture to the screen if (_addMaterial == null) _addMaterial = new Material(Shader.Find("Hidden/AddShader")); _addMaterial.SetFloat("_Sample", _currentSample); Graphics.Blit(_target, destination, _addMaterial); _currentSample++;
      
      





そのため、すでにプログレッシブサンプリングを行っていますが、それでもピクセルの中心を使用しています。 計算シェーダーで、 float2 _PixelOffset



を設定し、ハードコーディングされたオフセットfloat2(0.5f, 0.5f)



代わりにfloat2 _PixelOffset



で使用します。 スクリプトに戻り、次の行をSetShaderParameters



追加してランダムオフセットを作成します。



 RayTracingShader.SetVector("_PixelOffset", new Vector2(Random.value, Random.value));
      
      





カメラを動かせば、画像はまだ目に見える歪みであることがわかりますが、2、3フレーム静止するとすぐに消えてしまいます。 以下は、私たちが行ったことの比較です。















リフレクション



レイトレーサーの基盤は整っているので、レイトレースを他のレンダリングテクニックと実際に区別するトリッキーなことに取りかかることができます。 このリストの最初は完全な反射です。 アイデアは簡単です:表面と衝突すると、学校から覚えている反射法則に従って光線を反射します(入射角=反射角)。エネルギーを減らし、光線が空と衝突するまでプロセスを繰り返します。与えられた回数の反射の後、彼はエネルギーを使い果たしません。



シェーダーで変数float3 energy



をビームに追加し、 CreateRay



関数でray.energy = float3(1.0f, 1.0f, 1.0f)



として初期化します。 最初は、ビームはすべてのカラーチャンネルで最大値を持ち、反射ごとに減少します。



最大8つのトレース(元のレイと7つの反射)を実行し、 Shade



関数の呼び出しに結果を追加しますが、レイのエネルギーを乗算します。 たとえば、光線が一度反射されて失われたとします  frac34 あなたのエネルギー。 その後、移動を続けて空と衝突するため、ピクセルに転送するだけです  frac14 空のエネルギー。 次のようにCSMain



を変更し、以前のTrace



およびShade



呼び出しを置き換えます。



 // Trace and shade float3 result = float3(0, 0, 0); for (int i = 0; i < 8; i++) { RayHit hit = Trace(ray); result += ray.energy * Shade(ray, hit); if (!any(ray.energy)) break; }
      
      





Shade



機能は現在、エネルギーの更新と反射ビームの生成も実行しているため、ここで入力が重要になります。 エネルギーを更新するために、表面の反射色による要素ごとの乗算を実行します。 たとえば、金の場合、鏡面反射係数はfloat3(1.0f, 0.78f, 0.34f)



にほぼ等しくfloat3(1.0f, 0.78f, 0.34f)



100%赤、78%緑、および34%青のみを反射し、反射に特徴的な金色の色合いを与えます。 これらの値はいずれも1を超えないように注意してください。そうしないと、私たちからのエネルギーがどこからでも生成されます。 さらに、反射率は多くの場合、予想よりも低くなります。 たとえば、Naty HoffmanのPhysics and Math of Shadingのスライド64の値の一部を参照してください。



HLSLには、指定された法線でビームを反射するための組み込み関数があり、これは便利です。 浮動小数点数が不正確であるため、反射ビームは、反射元の表面によってブロックされることがあります。 これを避けるために、法線方向に沿って位置をわずかにシフトします。 新しいShade



機能は次のようになります。



 float3 Shade(inout Ray ray, RayHit hit) { if (hit.distance < 1.#INF) { float3 specular = float3(0.6f, 0.6f, 0.6f); // Reflect the ray and multiply energy with specular reflection ray.origin = hit.position + hit.normal * 0.001f; ray.direction = reflect(ray.direction, hit.normal); ray.energy *= specular; // Return nothing return float3(0.0f, 0.0f, 0.0f); } else { // Erase the ray's energy - the sky doesn't reflect anything ray.energy = 0.0f; // Sample the skybox and write it float theta = acos(ray.direction.y) / -PI; float phi = atan2(ray.direction.x, -ray.direction.z) / -PI * 0.5f; return _SkyboxTexture.SampleLevel(sampler_SkyboxTexture, float2(phi, theta), 0).xyz; } }
      
      





スカイボックスに1より大きい係数を掛けることで、スカイボックスの明るさをわずかに増加させることができます。次に、 Trace



機能を試してみてください。 いくつかの球体をループに入れると、結果は次のようになります。









指向性光源



そのため、鏡面反射をトレースできます。これにより、滑らかな金属表面をレンダリングできますが、非金属表面の場合は、拡散反射というもう1つのプロパティが必要です。 要するに、金属は反射色の色相を持つ入射光のみを反射しますが、非金属は光を表面で屈折させ、散乱させ、アルベドの色で塗ってランダムな方向に残します。 通常使用される理想的なランバートサーフェスの場合、確率は上記の方向とサーフェス法線の間の角度のコサインに比例します。 このトピックについては、 ここで詳しく説明します



拡散照明を開始するには、 public Light DirectionalLight



RayTracingMaster



追加し、シーンに指向性照明ソースを設定しましょう。 カメラ変換で行ったように、 Update



機能で光源変換の変更を認識する必要もあります。 SetShaderParameters



関数に次の行を追加します。



 Vector3 l = DirectionalLight.transform.forward; RayTracingShader.SetVector("_DirectionalLight", new Vector4(lx, ly, lz, DirectionalLight.intensity));
      
      





シェーダーで、 float4 _DirectionalLight



定義します。 Shade



関数で、鏡面反射色の直後にアルベド色を定義します。



 float3 albedo = float3(0.8f, 0.8f, 0.8f);
      
      





返される黒の値を単純な拡散シェーディングに置き換えます。



 // Return a diffuse-shaded color return saturate(dot(hit.normal, _DirectionalLight.xyz) * -1) * _DirectionalLight.w * albedo;
      
      





スカラー積が次のように定義されることを忘れないでください a cdotb=||a|| ||b|| cos theta 。 両方のベクトル(法線と光の方向)には単位長があるため、スカラー積、つまり角度の余弦が正確に必要です。 ビームとライトの方向は反対であるため、直接照明では、スカラー積は1ではなく-1を返します。 これを考慮するには、記号を変更する必要があります。 最後に、この値を飽和させます(たとえば、間隔に制限します) [0,1] )負のエネルギーを避けるため。



指向性光源が影を落とすためには、影のビームをトレースする必要があります。 考慮中のサーフェスの位置から開始し(自己シャドーイングを回避するために非常に小さな変位でも)、光が来た方向を示します。 何かが彼の無限への道を遮る場合、拡散照明は使用しません。 拡散反射色の上に次の行を追加します。



 // Shadow test ray bool shadow = false; Ray shadowRay = CreateRay(hit.position + hit.normal * 0.001f, -1 * _DirectionalLight.xyz); RayHit shadowHit = Trace(shadowRay); if (shadowHit.distance != 1.#INF) { return float3(0.0f, 0.0f, 0.0f); }
      
      





これで、鋭い影のある光沢のあるプラスチック球をトレースできます! 鏡面反射光に0.04、アルベドに0.8を設定すると、次の結果が得られます。









シーンと素材



より複雑でカラフルなシーンの作成を始めましょう! シェーダーですべてをハードに処理する代わりに、より汎用性を高めるためにシーンをC#に設定します。



まず、シェーダーでRayHit



構造を展開します。 Shade



関数でマテリアルプロパティをグローバルに設定する代わりに、オブジェクトごとにそれらを定義してRayHit



保存します。 float3 albedo



およびfloat3 specular



をstruct float3 albedo



追加し、 float3 specular



float3(0.0f, 0.0f, 0.0f)



で初期化します。 また、ハードコードされた値の代わりにhit



からこれらの値を使用するようにShade



関数を変更します。



一般的に球体がCPUとGPUにあるものを理解するために、シェーダーとC#のスクリプトでSphere



構造体を定義します。 シェーダー側からは、次のようになります。



 struct Sphere { float3 position; float radius; float3 albedo; float3 specular; };
      
      





この構造をC#スクリプトにコピーします。



シェーダーでは、 IntersectSphere



関数をfloat4



ではなく構造体で動作させる必要があります。 これは簡単です。



 void IntersectSphere(Ray ray, inout RayHit bestHit, Sphere sphere) { // Calculate distance along the ray where the sphere is intersected float3 d = ray.origin - sphere.position; float p1 = -dot(ray.direction, d); float p2sqr = p1 * p1 - dot(d, d) + sphere.radius * sphere.radius; if (p2sqr < 0) return; float p2 = sqrt(p2sqr); float t = p1 - p2 > 0 ? p1 - p2 : p1 + p2; if (t > 0 && t < bestHit.distance) { bestHit.distance = t; bestHit.position = ray.origin + t * ray.direction; bestHit.normal = normalize(bestHit.position - sphere.position); bestHit.albedo = sphere.albedo; bestHit.specular = sphere.specular; } }
      
      





また、 IntersectGroundPlane



関数でbestHit.albedo



bestHit.specular



を設定して、素材をカスタマイズします。



次に、 StructuredBuffer<Sphere> _Spheres



定義します。 この場所に、CPUはシーンを構成するすべての領域を保存します。 Trace



関数からすべてのハードコーディングされた球体を削除し、次の行を追加します。



 // Trace spheres uint numSpheres, stride; _Spheres.GetDimensions(numSpheres, stride); for (uint i = 0; i < numSpheres; i++) IntersectSphere(ray, bestHit, _Spheres[i]);
      
      





ここで、シーンに少し命を吹き込みます。 C#スクリプトに一般的なパラメーターを追加して、球体と計算バッファーの位置を制御しましょう。



 public Vector2 SphereRadius = new Vector2(3.0f, 8.0f); public uint SpheresMax = 100; public float SpherePlacementRadius = 100.0f; private ComputeBuffer _sphereBuffer;
      
      





OnEnable



でシーンを構成し、 OnEnable



でバッファーをOnDisable



ます。 したがって、コンポーネントがオンになるたびに、ランダムなシーンが生成されます。この関数SetUpScene



は、球体を特定の半径に配置し、既存の球体と交差する球体を破棄しようとします。球体の半分は金属(黒アルベド、鏡面反射)、残りの半分は非金属(色アルベド、鏡面反射4%)です。



 private void OnEnable() { _currentSample = 0; SetUpScene(); } private void OnDisable() { if (_sphereBuffer != null) _sphereBuffer.Release(); } private void SetUpScene() { List<Sphere> spheres = new List<Sphere>(); // Add a number of random spheres for (int i = 0; i < SpheresMax; i++) { Sphere sphere = new Sphere(); // Radius and radius sphere.radius = SphereRadius.x + Random.value * (SphereRadius.y - SphereRadius.x); Vector2 randomPos = Random.insideUnitCircle * SpherePlacementRadius; sphere.position = new Vector3(randomPos.x, sphere.radius, randomPos.y); // Reject spheres that are intersecting others foreach (Sphere other in spheres) { float minDist = sphere.radius + other.radius; if (Vector3.SqrMagnitude(sphere.position - other.position) < minDist * minDist) goto SkipSphere; } // Albedo and specular color Color color = Random.ColorHSV(); bool metal = Random.value < 0.5f; sphere.albedo = metal ? Vector3.zero : new Vector3(color.r, color.g, color.b); sphere.specular = metal ? new Vector3(color.r, color.g, color.b) : Vector3.one * 0.04f; // Add the sphere to the list spheres.Add(sphere); SkipSphere: continue; } // Assign to compute buffer _sphereBuffer = new ComputeBuffer(spheres.Count, 40); _sphereBuffer.SetData(spheres); }
      
      





マジックナンバー40 V new ComputeBuffer(spheres.Count, 40)



は、バッファーのステップです。メモリ内の1つの球体のサイズ(バイト単位)。計算するには、構造体の浮動小数点数を計算Sphere



し、浮動小数点バイトサイズ(4バイト)を乗算します。最後に、関数にシェーダーバッファーを設定しますSetShaderParameters







 RayTracingShader.SetBuffer(0, "_Spheres", _sphereBuffer);
      
      





結果



おめでとう、私たちはそれをやった!GPUに既製のホワイトレイトレーサーがあり、ミラー反射、単純な拡散照明、シャープな影のある多くの球体をレンダリングできます。完全なソースコードがBitbucketにアップロードされます。球の配置パラメーターを試して、美しい景色を観察します。















次は?



今日、私たちは多くのことを達成しましたが、もっと多くのことを実現することができます:拡散したグローバルライティング、ソフトシャドウ、屈折を伴う部分的に透明なマテリアル、そして球体の代わりに三角形の使用。次の記事では、ホワイトレイトレーサーをパストレーサーに拡張して、上記のいくつかを学習します。



All Articles