Seminggu yang lalu, saya
menerbitkan bab lain dari
kursus kuliah grafik komputer saya; Hari ini, kita kembali ke ray tracing, tapi kali ini kita akan melangkah sedikit lebih jauh daripada membuat bola-bola remeh. Saya tidak perlu fotorealisme, untuk tujuan kartun,
seperti ledakan , menurut saya, akan turun.
Seperti biasa, kami hanya memiliki kompiler kosong yang tersedia, tidak ada perpustakaan pihak ketiga yang dapat digunakan. Saya tidak ingin repot dengan manajer jendela, pemrosesan mouse / keyboard, dan sejenisnya. Hasil dari program kami adalah gambar sederhana yang disimpan ke disk. Saya tidak mengejar kecepatan / optimasi sama sekali, tujuan saya adalah untuk menunjukkan prinsip-prinsip dasar.
Secara total, bagaimana cara menggambar gambar seperti itu di 180 baris kode dalam kondisi seperti itu?

Izinkan saya menyisipkan animasi gif (enam meter):

Dan sekarang kita akan membagi seluruh tugas menjadi beberapa tahap:
Tahap satu: baca artikel sebelumnya
Ya persis. Hal pertama yang harus dilakukan adalah membaca
bab sebelumnya , yang berbicara tentang dasar-dasar penelusuran sinar. Ini sangat singkat, pada prinsipnya, semua refleksi-refraksi tidak dapat dibaca, tetapi setidaknya sampai pencahayaan yang tersebar saya sarankan membacanya. Kode ini cukup sederhana, orang bahkan menjalankannya pada mikrokontroler:

Tahap Dua: Draw One Sphere
Mari kita menggambar satu bola tanpa mengganggu bahan atau pencahayaan. Untuk kesederhanaan, bola ini akan hidup di pusat koordinat. Tentang gambar ini saya ingin mendapatkan:

Lihat kode di
sini , tetapi izinkan saya memberi Anda yang utama langsung dalam teks artikel:
#define _USE_MATH_DEFINES #include <cmath> #include <algorithm> #include <limits> #include <iostream> #include <fstream> #include <vector> #include "geometry.h" const float sphere_radius = 1.5; float signed_distance(const Vec3f &p) { return p.norm() - sphere_radius; } bool sphere_trace(const Vec3f &orig, const Vec3f &dir, Vec3f &pos) { pos = orig; for (size_t i=0; i<128; i++) { float d = signed_distance(pos); if (d < 0) return true; pos = pos + dir*std::max(d*0.1f, .01f); } return false; } int main() { const int width = 640; const int height = 480; const float fov = M_PI/3.; std::vector<Vec3f> framebuffer(width*height); #pragma omp parallel for for (size_t j = 0; j<height; j++) { // actual rendering loop for (size_t i = 0; i<width; i++) { float dir_x = (i + 0.5) - width/2.; float dir_y = -(j + 0.5) + height/2.; // this flips the image at the same time float dir_z = -height/(2.*tan(fov/2.)); Vec3f hit; if (sphere_trace(Vec3f(0, 0, 3), Vec3f(dir_x, dir_y, dir_z).normalize(), hit)) { // the camera is placed to (0,0,3) and it looks along the -z axis framebuffer[i+j*width] = Vec3f(1, 1, 1); } else { framebuffer[i+j*width] = Vec3f(0.2, 0.7, 0.8); // background color } } } std::ofstream ofs("./out.ppm", std::ios::binary); // save the framebuffer to file ofs << "P6\n" << width << " " << height << "\n255\n"; for (size_t i = 0; i < height*width; ++i) { for (size_t j = 0; j<3; j++) { ofs << (char)(std::max(0, std::min(255, static_cast<int>(255*framebuffer[i][j])))); } } ofs.close(); return 0; }
Kelas vektor tinggal di file geometry.h, saya tidak akan menjelaskannya di sini: pertama, semuanya sepele di sana, manipulasi sederhana vektor dua dan tiga dimensi (penambahan, pengurangan, penugasan, perkalian dengan skalar, produk skalar), dan kedua,
gbg sudah
menggambarkannya secara
rinci sebagai bagian dari kursus kuliah tentang grafik komputer.
Saya menyimpan gambar dalam
format ppm ; Ini adalah cara termudah untuk menyimpan gambar, meskipun tidak selalu yang paling nyaman untuk dilihat lebih lanjut.
Jadi, dalam fungsi main (), saya memiliki dua siklus: siklus kedua hanya menyimpan gambar ke disk, dan siklus pertama melewati semua piksel gambar, memancarkan sinar dari kamera melalui piksel ini, dan melihat apakah sinar ini berpotongan dengan bola kita.
Perhatian, ide utama artikel: jika dalam artikel terakhir kita secara analitik mempertimbangkan persimpangan sinar dan bola, sekarang saya menghitungnya secara numerik. Idenya sederhana: bola memiliki persamaan bentuk x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 = 0; tetapi secara umum fungsi f (x, y, z) = x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 didefinisikan di seluruh ruang. Di dalam bola, fungsi f (x, y, z) akan memiliki nilai negatif, dan di luar bola, itu akan menjadi positif. Yaitu, fungsi f (x, y, z) mengatur jarak (dengan tanda!) Untuk bola kita sebagai titik (x, y, z). Oleh karena itu, kita hanya meluncur sepanjang balok sampai kita bosan atau fungsi f (x, y, z) menjadi negatif. Fungsi sphere_trace () melakukan hal itu.
Tahap Tiga: Pencahayaan Primitif
Mari kita kodekan pencahayaan difus yang paling sederhana, saya ingin mendapatkan gambar seperti itu di output:

Seperti pada artikel sebelumnya, untuk kemudahan membaca, saya melakukan satu langkah = satu komit. Perubahan bisa
dilihat di sini .
Untuk pencahayaan difus, tidak cukup bagi kita untuk menghitung titik persimpangan balok dengan permukaan, kita perlu mengetahui vektor normal ke permukaan pada titik ini. Saya menerima vektor normal ini dengan
perbedaan hingga fungsi kami dari jarak ke permukaan:
Vec3f distance_field_normal(const Vec3f &pos) { const float eps = 0.1; float d = signed_distance(pos); float nx = signed_distance(pos + Vec3f(eps, 0, 0)) - d; float ny = signed_distance(pos + Vec3f(0, eps, 0)) - d; float nz = signed_distance(pos + Vec3f(0, 0, eps)) - d; return Vec3f(nx, ny, nz).normalize(); }
Pada prinsipnya, tentu saja, karena kita menggambar bola, yang normal dapat diperoleh lebih mudah, tetapi saya melakukannya dengan cadangan untuk masa depan.
Tahap Empat: mari kita menggambar pola di bola kita
Dan mari kita menggambar beberapa pola di daerah kita, misalnya, seperti ini:

Untuk melakukan ini, dalam kode sebelumnya, saya
hanya mengubah
dua baris!Bagaimana saya melakukan ini? Tentu saja, saya tidak punya tekstur. Saya baru saja mengambil fungsi g (x, y, z) = sin (x) * sin (y) * sin (z); sekali lagi didefinisikan di seluruh ruang. Ketika sinar saya melintasi bola di beberapa titik, maka nilai fungsi g (x, y, z) pada titik ini menetapkan warna piksel untuk saya.
Ngomong-ngomong, perhatikan lingkaran konsentris di sekitar bola - ini adalah artefak dari perhitungan numerik persimpangan saya.
Langkah Kelima: pemetaan perpindahan
Mengapa saya ingin menggambar pola ini? Dan dia akan membantu saya menggambar landak:

Di mana pola saya hitam, saya ingin mendorong lubang di bola kami, dan di mana warnanya putih, sebaliknya, regangkan punuknya.
Untuk melakukan ini, cukup
ubah tiga baris dalam kode kami:
float signed_distance(const Vec3f &p) { Vec3f s = Vec3f(p).normalize(sphere_radius); float displacement = sin(16*sx)*sin(16*sy)*sin(16*sz)*noise_amplitude; return p.norm() - (sphere_radius + displacement); }
Yaitu, saya mengubah perhitungan jarak ke permukaan kita, mendefinisikannya sebagai x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 - dosa (x) * dosa (y) * dosa (z). Bahkan, kami mendefinisikan
fungsi implisit .
Langkah Enam: Fungsi Tersirat Lain
Dan mengapa saya mengevaluasi produk sinus hanya untuk poin yang terletak di permukaan bola kita? Mari kita mendefinisikan kembali fungsi implisit kita seperti ini:
float signed_distance(const Vec3f &p) { float displacement = sin(16*px)*sin(16*py)*sin(16*pz)*noise_amplitude; return p.norm() - (sphere_radius + displacement); }
Perbedaannya dengan kode sebelumnya sangat kecil, lebih baik untuk
melihat diff . Inilah hasilnya:

Dengan demikian, kita dapat mendefinisikan komponen yang terputus di objek kita!
Langkah Tujuh: Pseudo Random Noise
Gambar sebelumnya sudah mulai menyerupai ledakan, tetapi produk dari sinus memiliki pola yang terlalu teratur. Kami membutuhkan lebih banyak fungsi "sobek", lebih "acak" ...
Kebisingan Perlin akan membantu kami. Berikut ini adalah sesuatu yang cocok untuk kita lebih baik daripada produk dari sinus:

Cara menghasilkan suara seperti itu agak offtopic, tetapi di sini adalah gagasan utama: Anda perlu menghasilkan gambar acak dengan resolusi yang berbeda, menghaluskannya untuk mendapatkan sesuatu seperti ini:

Dan kemudian jumlahkan semuanya:

Baca lebih lanjut di
sini dan di
sini .
Mari kita
tambahkan beberapa kode yang menghasilkan noise ini dan dapatkan gambar ini:

Harap perhatikan bahwa dalam kode rendering saya tidak mengubah apa pun, hanya fungsi yang "mengerutkan" bola kami yang telah berubah.
Tahap Delapan, Final: Tambah Warna
Satu-satunya hal yang saya
ubah dalam komit ini adalah bahwa alih-alih warna putih yang seragam, saya menerapkan warna yang secara linear tergantung pada jumlah noise yang diterapkan:
Vec3f palette_fire(const float d) { const Vec3f yellow(1.7, 1.3, 1.0);
Ini adalah gradien linier sederhana antara lima warna utama. Nah, ini fotonya!

Kesimpulan
Teknik penelusuran sinar ini disebut ray marching. Pekerjaan rumahnya sederhana: lintasi pelacak sinar sebelumnya dengan blackjack dan pantulan dengan ledakan kita, sehingga ledakan itu juga menerangi segala sesuatu di sekitar! Omong-omong, ledakan ini kurang tembus cahaya.