Kami menggambar ledakan kartun untuk 180 garis telanjang C ++

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); // note that the color is "hot", ie has components >1 const Vec3f orange(1.0, 0.6, 0.0); const Vec3f red(1.0, 0.0, 0.0); const Vec3f darkgray(0.2, 0.2, 0.2); const Vec3f gray(0.4, 0.4, 0.4); float x = std::max(0.f, std::min(1.f, d)); if (x<.25f) return lerp(gray, darkgray, x*4.f); else if (x<.5f) return lerp(darkgray, red, x*4.f-1.f); else if (x<.75f) return lerp(red, orange, x*4.f-2.f); return lerp(orange, yellow, x*4.f-3.f); } 

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.

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


All Articles