Pelacakan Ray GPU dalam Persatuan - Bagian 3

[ Bagian pertama dan kedua .]


Hari ini kita akan melakukan lompatan besar. Kami akan menjauh dari struktur bola yang eksklusif dan bidang tak terbatas yang kami lacak sebelumnya, dan menambahkan segitiga - seluruh esensi grafik komputer modern, sebuah elemen yang terdiri dari semua dunia virtual. Jika Anda ingin melanjutkan dengan apa yang kami selesaikan terakhir kali, maka gunakan kode dari bagian 2 . Kode yang sudah jadi untuk apa yang akan kita lakukan hari ini tersedia di sini . Ayo mulai!

Segitiga


Segitiga hanyalah daftar dari tiga simpul yang terhubung, masing-masing menyimpan posisinya sendiri, dan kadang-kadang normal. Urutan traversal dari sudut dari sudut pandang Anda menentukan apa yang kita lihat - bagian depan atau belakang segitiga. Secara tradisional, "depan" dianggap sebagai urutan traversal berlawanan arah jarum jam.

Pertama, kita harus dapat menentukan apakah sinar memotong sebuah segitiga, dan jika demikian, pada titik apa. Algoritma yang sangat populer (tapi jelas bukan satu-satunya ) untuk menentukan perpotongan sinar dengan segitiga diusulkan pada tahun 1997 oleh tuan-tuan Thomas Akenin-Meller dan Ben Trembor. Anda dapat membaca lebih lanjut tentang hal ini di artikel mereka “Fast, Minimum Storage Ray-Triangle Intersection” di sini .

Kode dari artikel dapat dengan mudah dipindahkan ke kode shader 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; } 

Untuk menggunakan fungsi ini, kita membutuhkan sinar dan tiga simpul segitiga. Nilai kembali memberitahu kita jika segitiga berpotongan. Dalam kasus persimpangan, tiga nilai tambahan dihitung: t menggambarkan jarak sepanjang balok ke titik persimpangan, dan u / v adalah dua dari tiga koordinat barycentric yang menentukan lokasi titik persimpangan pada segitiga (koordinat terakhir dapat dihitung sebagai w = 1 - u - v ). Jika Anda belum terbiasa dengan koordinat barycentric, bacalah penjelasannya yang bagus tentang Scratchapixel .

Tanpa terlalu banyak penundaan, mari kita lacak satu segitiga dengan simpul yang ditunjukkan dalam kode! Temukan fungsi Trace di shader dan tambahkan fragmen kode berikut ke dalamnya:

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

Seperti yang saya katakan, t menyimpan jarak sepanjang balok, dan kita bisa langsung menggunakan nilai ini untuk menghitung titik persimpangan. Normal, yang penting untuk menghitung refleksi yang benar, dapat dihitung menggunakan produk vektor dari setiap dua sisi segitiga. Luncurkan mode permainan dan kagumi segitiga yang Anda telusuri:


Latihan: Cobalah untuk menghitung posisi menggunakan koordinat barycentric daripada jarak. Jika Anda melakukan semuanya dengan benar, maka segitiga mengkilap akan terlihat persis seperti sebelumnya.

Jerat Segitiga


Kami mengatasi hambatan pertama, tetapi melacak seluruh jerat dari segitiga adalah cerita yang sama sekali berbeda. Pertama kita perlu mempelajari beberapa informasi dasar tentang jerat. Jika Anda mengenal mereka, Anda dapat dengan aman melewati paragraf berikutnya.

Dalam grafik komputer, mesh didefinisikan oleh beberapa buffer, yang paling penting adalah buffer vertex dan indeks . Buffer vertex adalah daftar vektor 3D yang menggambarkan posisi setiap verteks dalam ruang objek (ini berarti bahwa nilai-nilai tersebut tidak perlu diubah ketika memindahkan, memutar atau menskalakan objek - mereka dikonversi dari ruang objek ke ruang dunia dengan menggunakan penggandaan matriks) . Buffer indeks adalah daftar nilai integer yang merupakan indeks yang mengarah ke buffer verteks. Setiap tiga indeks membentuk segitiga. Misalnya, jika buffer indeks memiliki bentuk [0, 1, 2, 0, 2, 3], maka ia memiliki dua segitiga: segitiga pertama terdiri dari simpul pertama, kedua dan ketiga dalam buffer verteks, dan segitiga kedua terdiri dari yang pertama, ketiga dan puncak keempat. Oleh karena itu, buffer indeks juga menentukan urutan traversal yang disebutkan di atas. Selain buffer verteks dan indeks, mungkin ada buffer tambahan yang menambahkan informasi lain untuk setiap vertex. Buffer tambahan yang paling umum menyimpan normals , koordinat tekstur (disebut texcoords atau hanya UV ), serta warna vertex .

Menggunakan GameObjects


Pertama-tama, kita perlu mencari tahu GameObject mana yang harus menjadi bagian dari proses penelusuran ray. Solusi naif adalah dengan menggunakan FindObjectOfType<MeshRenderer>() , tetapi lakukan sesuatu yang lebih fleksibel dan lebih cepat. Mari kita tambahkan komponen RayTracingObject baru:

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

Komponen ini ditambahkan ke setiap objek yang ingin kita gunakan untuk pelacakan ray dan terlibat dalam pendaftaran mereka menggunakan RayTracingMaster . Tambahkan fungsi berikut ke wizard:

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

Semuanya berjalan dengan baik - sekarang kita tahu benda apa yang harus dilacak. Tetapi bagian yang sulit berlanjut: kita akan mengumpulkan semua data dari jerat Persatuan (matriks, buffer verteks dan indeks - ingat?), Tuliskan ke struktur data kita sendiri dan muatkan ke dalam GPU sehingga shader dapat menggunakannya. Mari kita mulai dengan mendefinisikan struktur data dan buffer di sisi C #, di panduan:

 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; 

... dan sekarang mari kita lakukan hal yang sama di shader. Apakah Anda terbiasa dengan hal itu?

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

Struktur data sudah siap, dan kita bisa mengisinya dengan data nyata. Kami mengumpulkan semua simpul semua jerat menjadi satu List<Vector3> besar List<Vector3> , dan semua indeks menjadi List<int> besar List<int> . Tidak ada masalah dengan simpul, tetapi indeks perlu diubah sehingga mereka terus menunjuk ke simpul yang benar di buffer besar kami. Bayangkan bahwa kita telah menambahkan objek dari 1000 simpul, dan sekarang kita menambahkan kubus jala sederhana. Segitiga pertama dapat terdiri dari indeks [0, 1, 2], tetapi karena kita sudah memiliki 1000 simpul dalam buffer, kita perlu menggeser indeks sebelum menambahkan simpul ke kubus. Artinya, mereka akan berubah menjadi [1000, 1001, 1002]. Begini tampilannya dalam kode:

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

Kami memanggil RebuildMeshObjectBuffers di fungsi OnRenderImage , dan jangan lupa untuk membebaskan buffer baru di OnDisable . Berikut adalah dua fungsi pembantu yang saya gunakan dalam kode di atas untuk menyederhanakan penanganan buffer sedikit:

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

Hebat, kami membuat buffer dan mereka diisi dengan data yang diperlukan! Sekarang kita hanya perlu melaporkan ini ke shader. Tambahkan kode berikut ke SetShaderParameters (dan berkat fungsi pembantu baru, kami dapat mengurangi kode buffer bola):

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

Jadi, pekerjaannya membosankan, tapi mari kita lihat apa yang baru saja kita lakukan: kami mengumpulkan semua data internal jerat (matriks, simpul dan indeks), menempatkannya dalam struktur yang mudah dan sederhana, dan kemudian mengirimnya ke GPU, yang sekarang menanti saat mereka bisa digunakan.

Jaring tracing


Jangan memaksanya menunggu. Dalam shader, kita sudah memiliki kode jejak dari sebuah segitiga individu, dan jaringnya, pada kenyataannya, hanyalah banyak segitiga. Satu-satunya aspek baru di sini adalah kita menggunakan matriks untuk mengubah simpul dari ruang objek ke ruang dunia menggunakan fungsi mul built-in (kependekan dari multiply). Matriks berisi terjemahan, rotasi, dan skala objek. Ini memiliki ukuran 4 × 4, jadi untuk perkalian kita membutuhkan vektor 4d. Tiga komponen pertama (x, y, z) diambil dari vertex buffer. Kami mengatur komponen keempat (w) menjadi 1 karena kami berhadapan dengan suatu poin. Jika ini adalah arahnya, maka kita akan menulis 0 di dalamnya untuk mengabaikan semua terjemahan dan skala dalam matriks. Apakah ini membingungkan bagi Anda? Kemudian baca tutorial ini setidaknya delapan kali. Berikut adalah kode shader:

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

Kami hanya satu langkah lagi dari melihat semuanya beraksi. Mari kita merestrukturisasi fungsi Trace sedikit dan menambahkan jejak objek mesh:

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

Hasil


Itu saja! Mari kita tambahkan beberapa jerat sederhana (Unity primitif baik-baik saja), berikan mereka komponen RayTracingObject dan amati keajaibannya. Jangan menggunakan jerat terinci (lebih dari beberapa ratus segitiga)! Shader kami tidak memiliki optimalisasi, dan jika Anda melakukannya secara berlebihan, maka diperlukan beberapa detik atau bahkan menit untuk melacak setidaknya satu sampel per piksel. Akibatnya, sistem akan menghentikan driver GPU, mesin Unity mungkin macet, dan komputer harus dihidupkan ulang.


Perhatikan bahwa jerat kami tidak memiliki bayangan yang halus, tetapi rata. Karena kita belum memuat normals dari simpul ke buffer, untuk mendapatkan normal dari simpul masing-masing segitiga, kita perlu melakukan produk vektor. Selain itu, kami tidak dapat menginterpolasi area segitiga. Kami akan mengatasi masalah ini di bagian tutorial selanjutnya.

Demi kepentingan, saya mengunduh Stanford Bunny dari arsip Morgan McGwire dan menggunakan pengubah decimate dari paket Blender, saya mengurangi jumlah simpul menjadi 431. Anda dapat bereksperimen dengan parameter pencahayaan dan materi kode keras dalam fungsi shader IntersectMeshObject . Inilah kelinci dielektrik dengan bayang-bayang lembut yang indah dan sedikit pencahayaan global yang tersebar di Grafitti Shelter :


... dan inilah kelinci logam di bawah cahaya arah yang kuat dari Cape Hill , melemparkan sorot disko ke lantai pesawat:


... dan di sini ada dua kelinci kecil yang bersembunyi di bawah batu besar Suzanne di bawah langit biru Kiara 9 Dusk (saya meresepkan bahan alternatif untuk objek kedua, memeriksa apakah indeks bergeser nol):


Apa selanjutnya


Sangat menyenangkan melihat jaring nyata di pelacak Anda sendiri untuk pertama kalinya, bukan? Hari ini kami memproses beberapa data, menemukan persimpangan dengan menggunakan algoritma Meller-Trambor, dan mengumpulkan semuanya sehingga kami bisa segera menggunakan mesin GameObjects mesin Unity. Selain itu, kami melihat salah satu keunggulan penelusuran sinar: segera setelah Anda menambahkan persimpangan baru ke kode, semua efek yang indah (bayangan lembut, pencahayaan global yang dipantulkan dan disebarkan, dan sebagainya) segera mulai bekerja.

Membawa kelinci yang mengkilap membutuhkan banyak waktu, dan saya masih harus menggunakan sedikit penyaringan untuk menghilangkan suara yang paling jelas. Untuk mengatasi masalah ini, adegan biasanya ditulis dalam struktur spasial, misalnya, dalam kotak, pohon dimensi-K atau hierarki volume pembatas, yang secara signifikan meningkatkan kecepatan rendering adegan besar.

Tetapi kita perlu bergerak secara berurutan: lebih jauh kita akan menghilangkan masalah dengan normal sehingga jerat kita (bahkan yang rendah poli) terlihat lebih halus dari sekarang. Akan lebih baik jika secara otomatis memperbarui matriks ketika memindahkan objek dan langsung merujuk ke materi Unity, dan tidak hanya menuliskannya dalam kode. Inilah yang akan kita lakukan di bagian selanjutnya dari seri tutorial ini. Terima kasih sudah membaca, dan sampai jumpa di bagian 4!

Source: https://habr.com/ru/post/id450308/


All Articles