βDia melakukannya lagi!β - inilah yang pertama kali terjadi pada saya ketika saya melihat bagian belakang selebaran Pixar
[1] , yang benar-benar penuh dengan kode. Sekelompok konstruksi dan ekspresi ditandatangani di sudut kanan bawah oleh Andrew Kensler. Bagi mereka yang tidak mengenalnya, saya akan mengatakan: Andrew adalah seorang programmer yang menemukan
pelacak sinar seukuran kartu nama 1337-byte pada tahun 2009.
Kali ini, Andrew muncul dengan sesuatu yang lebih banyak, tetapi dengan hasil visual yang jauh lebih menarik. Sejak saya selesai menulis Game Engine Black Books saya tentang
Wolf3D dan
DOOM , saya punya waktu untuk mempelajari bagian dalam kode samarnya. Dan segera, saya benar-benar terpesona oleh teknik yang ditemukan dalam dirinya. Mereka sangat berbeda dari pekerjaan Andrew sebelumnya, berdasarkan pelacak sinar "standar". Saya tertarik untuk belajar tentang ray marching, fitur geometri volumetrik yang konstruktif, render / jejak jalur Monte Carlo, serta banyak trik lain yang ia gunakan untuk memasukkan kode ke selembar kertas kecil.
Kode sumber
Bagian depan selebaran adalah iklan untuk departemen perekrutan Pixar. Di sisi sebaliknya dicetak 2.037 byte kode C ++, dikaburkan untuk menempati permukaan sekecil mungkin.
#include <stdlib.h> // card > pixar.ppm #include <stdio.h> #include <math.h> #define R return #define O operator typedef float F;typedef int I;struct V{F x,y,z;V(F v=0){x=y=z=v;}V(F a,F b,F c=0){x=a;y=b;z=c;}V O+(V r){RV(x+rx,y+ry,z+rz);}VO*(V r){RV(x*rx,y*r. y,z*rz);}FO%(V r){R x*r.x+y*r.y+z*rz;}VO!(){R*this*(1/sqrtf(*this%*this) );}};FL(F l,F r){R l<r?l:r;}FU(){R(F)rand()/RAND_MAX;}FB(V p,V l,V h){l=p +l*-1;h=h+p*-1;RL(L(L(lx,hx),L(ly,hy)),L(lz,hz));}FS(V p,I&m){F d=1\ e9;V f=p;fz=0;char l[]="5O5_5W9W5_9_COC_AOEOA_E_IOQ_I_QOUOY_Y_]OWW[WaOa_aW\ eWa_e_cWiO";for(I i=0;i<60;i+=4){V b=V(l[i]-79,l[i+1]-79)*.5,e=V(l[i+2]-79,l [i+3]-79)*.5+b*-1,o=f+(b+e*L(-L((b+f*-1)%e/(e%e),0),1))*-1;d=L(d,o%o);}d=sq\ rtf(d);V a[]={V(-11,6),V(11,6)};for(I i=2;i--;){V o=f+a[i]*-1;d=L(d,ox>0?f\ absf(sqrtf(o%o)-2):(o.y+=oy>0?-2:2,sqrtf(o%o)));}d=powf(powf(d,8)+powf(pz, 8),.125)-.5;m=1;F r=L(-L(B(p,V(-30,-.5,-30),V(30,18,30)),B(p,V(-25,17,-25),V (25,20,25))),B(V(fmodf(fabsf(px),8),py,pz),V(1.5,18.5,-25),V(6.5,20,25))) ;if(r<d)d=r,m=2;F s=19.9-py;if(s<d)d=s,m=3;R d;}IM(V o,V d,V&h,V&n){I m,s= 0;F t=0,c;for(;t<100;t+=c)if((c=S(h=o+d*t,m))<.01||++s>99)R n=!V(S(h+V(.01,0 ),s)-c,S(h+V(0,.01),s)-c,S(h+V(0,0,.01),s)-c),m;R 0;}VT(V o,V d){V h,n,r,t= 1,l(!V(.6,.6,1));for(I b=3;b--;){I m=M(o,d,h,n);if(!m)break;if(m==1){d=d+n*( n%d*-2);o=h+d*.1;t=t*.2;}if(m==2){F i=n%l,p=6.283185*U(),c=U(),s=sqrtf(1-c), g=nz<0?-1:1,u=-1/(g+nz),v=nx*ny*u;d=V(v,g+ny*ny*u,-ny)*(cosf(p)*s)+V( 1+g*nx*nx*u,g*v,-g*nx)*(sinf(p)*s)+n*sqrtf(c);o=h+d*.1;t=t*.2;if(i>0&&M(h +n*.1,l,h,n)==3)r=r+t*V(500,400,100)*i;}if(m==3){r=r+t*V(50,80,100);break;}} R r;}I main(){I w=960,h=540,s=16;V e(-22,5,25),g=!(V(-3,4,0)+e*-1),l=!V(gz, 0,-gx)*(1./w),u(gy*lz-gz*ly,gz*lx-gx*lz,gx*ly-gy*lx);printf("P\ 6 %d %d 255 ",w,h);for(I y=h;y--;)for(I x=w;x--;){V c;for(I p=s;p--;)c=c+T(e ,!(g+l*(xw/2+U())+u*(yh/2+U())));c=c*(1./s)+14./241;V o=c+1;c=V(cx/ox,c. y/oy,cz/oz)*255;printf("%c%c%c",(I)cx,(I)cy,(I)cz);}}// Andrew Kensler
Apakah dia bekerja?
Dengan kode tersebut ada instruksi untuk peluncurannya. Idenya adalah untuk mengarahkan output standar ke file. Dengan ekstensi, kita dapat mengasumsikan bahwa format output adalah format gambar teks yang disebut NetPBM
[2] .
$ clang -o card2 -O3 raytracer.cpp
$ time ./card> pixar.ppm
2m58.524s nyata
pengguna 2m57.567s
sys 0m0.415s
Setelah dua menit lima puluh delapan detik
[3] , gambar berikut dihasilkan. Sungguh menakjubkan betapa sedikit kode yang diperlukan untuk itu.
Anda dapat mengekstraksi banyak dari gambar di atas. Grit adalah tanda yang jelas dari "path tracer." Jenis renderer berbeda dari raytracing karena sinar tidak ditelusuri kembali ke sumber cahaya. Dalam metode ini, ribuan sinar per piksel dipancarkan dari sumber dan program memantaunya, berharap mereka akan menemukan sumber cahaya. Ini adalah teknik menarik yang, jauh lebih baik daripada ray tracing, dapat menangani render oklusi ambien, bayangan lembut, kaustik, dan radiositas.
Kami akan memecah kode menjadi beberapa bagian
Melewati input ke CLion memformat kode (lihat output di
sini ) dan memecahnya menjadi bagian / tugas yang lebih kecil.
#termasuk <stdlib.h> // card> pixar.ppm
#termasuk <stdio.h>
#termasuk <math.h>
#define R kembali
#define O operator
typedef float F; typedef int I;
struct V {F x, y, z; V (F v = 0) {x = y = z = v;} V (F a, F b, F
c = 0) {x = a; y = b; z = c;} V O + (V r) {RV (x + rx, y + ry, z + rz);} VO * (V r) {RV ( x * rx, y * r.
y, z * rz);} FO% (V r) {R x * r.x + y * r.y + z * rz;} VO! () {R * ini * (1 / sqrtf (*% ini * ini)
);}};
FL (F l, F r) {R l <r? L: r;} FU () {R (F) rand () / RAND_MAX;} FB (Vp, Vl, V h) {l = p
+ l * -1; h = h + p * -1; RL (L (L (lx, hx), L (ly, hy)), L (lz, hz));}
FS (Vp, I & m) {F d = 1 \
e9; Vf = p; fz = 0; char l [] = "5O5_5W9W5_9_COC_AOEOA_E_IOQ_I_QOUOY_Y_]] OWW [WaOa_aW \
eWa_e_cWiO "; for (I i = 0; i <60; i + = 4) {V b = V (l [i] -79, l [i + 1] -79) *. 5, e = V (l [ i + 2] -79, l
[i + 3] -79) * .5 + b * -1, o = f + (b + e * L (-L ((b + f * -1)% e / (e% e), 0), 1)) * - 1; d = L (d, o% o);} d = sq \
rtf (d); V a [] = {V (-11.6), V (11.6)}; untuk (I i = 2; i -;) {V o = f + a [i] * -1; d = L (d, sapi> 0? F \
absf (sqrtf (o% o) -2) :( o.y + = oy> 0? -2: 2, sqrtf (o% o)));} d = powf (powf (d, 8) + powf (pz ,
8), 125) -. 5; m = 1; F r = L (-L (B (p, V (-30, -. 5, -30), V (30,18,30)), B (p, V (-25.17, -25), V
(25,20,25))), B (V (fmodf (fabsf (px), 8), py, pz), V (1.5,18.5, -25), V (6.5,20,25))))
; jika (r <d) d = r, m = 2; F s = 19.9-py; jika (s <d) d = s, m = 3; R d;}
IM (V o, V d, V & h, V & n) {I m, s =
0; F t = 0, c; untuk (; t <100; t + = c) jika ((c = S (h = o + d * t, m)) <. 01 || ++ s> 99) R n =! V (S (h + V (.01,0
), s) -c, S (h + V (0, .01), s) -c, S (h + V (0,0, .01), s) -c), m; R 0;}
VT (V o, V d) {V h, n, r, t =
1, l (! V (.6, .6,1)); untuk (I b = 3; b -;) {I m = M (o, d, h, n); jika (! M) putus ; if (m == 1) {d = d + n * (
n% d * -2); o = h + d * .1; t = t * .2;} jika (m == 2) {F i = n% l, p = 6.283185 * U (), c = U (), s = sqrtf (1-c),
g = nz <0? -1: 1, u = -1 / (g + nz), v = nx * ny * u; d = V (v, g + ny * ny * u, -ny) * (cosf (p) * s) + V (
1 + g * nx * nx * u, g * v, -g * nx) * (sinf (p) * s) + n * sqrtf (c); o = h + d * .1; t = t *. 2; jika (i> 0 && M (h
+ n * .1, l, h, n) == 3) r = r + t * V (500.400.100) * i;} jika (m == 3) {r = r + t * V (50,80.100) ; break;}}
R r;}
I main () {I w = 960, h = 540, s = 16; V e (-22,5,25), g =! (V (-3,4,0) + e * -1), l =! V (gz,
0, -gx) * (1./w), u (gy * lz-gz * ly, gz * lx-gx * lz, gx * ly-gy * lx); printf ("P \
6% d% d 255 ", w, h); for (I y = h; y -;) untuk (I x = w; x -;) {V c; untuk (I p = s; p- -;) c = c + T (e
,! (g + l * (xw / 2 + U ()) + u * (yh / 2 + U ()))); c = c * (1./s) + 14. / 241; V o = c + 1; c = V (cx / ox, c.
y / oy, cz / oz) * 255; printf ("% c% c% c", (I) cx, (I) cy, (I) cz);}}
// Andrew Kensler
Setiap bagian dijelaskan secara rinci dalam sisa artikel:
β - trik biasa,
β - Kelas vektor,
β - kode tambahan,
β - database,
β - Ray berbaris,
β - pengambilan sampel,
β - kode utama.
Trik umum dengan #define dan typedef
Trik umum menggunakan #define dan typedef untuk secara signifikan mengurangi jumlah kode. Di sini kita menunjukkan F = float, I = int, R = return, dan O = operator. Reverse engineering itu sepele.
Kelas v
Berikutnya adalah kelas V, yang saya beri nama baru menjadi Vec (meskipun, seperti yang akan kita lihat di bawah, ini juga digunakan untuk menyimpan saluran RGB dalam format float).
struct Vec { float x, y, z; Vec(float v = 0) { x = y = z = v; } Vec(float a, float b, float c = 0) { x = a; y = b; z = c;} Vec operator+(Vec r) { return Vec(x + rx, y + ry, z + rz); } Vec operator*(Vec r) { return Vec(x * rx, y * ry, z * rz); }
Perhatikan bahwa tidak ada operator pengurangan (-), jadi alih-alih menulis "X = A - B", "X = A + B * -1" digunakan. Root kuadrat terbalik berguna nanti untuk menormalkan vektor.
Fungsi Utama
main () adalah satu-satunya karakter yang tidak dapat dikaburkan karena dipanggil oleh fungsi _start perpustakaan libc. Biasanya layak dimulai dengan itu, karena akan lebih mudah untuk bekerja dengan cara ini. Butuh beberapa saat untuk mencari tahu arti dari huruf-huruf pertama, tetapi masih berhasil membuat sesuatu yang bisa dibaca.
int main() { int w = 960, h = 540, samplesCount = 16; Vec position(-22, 5, 25); Vec goal = !(Vec(-3, 4, 0) + position * -1); Vec left = !Vec(goal.z, 0, -goal.x) * (1. / w);
Perhatikan bahwa liter float tidak mengandung huruf "f", dan bagian pecahan dibuang untuk menghemat ruang. Trik yang sama digunakan di bawah ini, di mana bagian integer dijatuhkan (float x = .5). Yang juga tidak biasa adalah konstruk "for" dengan ekspresi iterasi yang dimasukkan di dalam kondisi break.
Ini adalah fungsi utama yang cukup standar untuk pelacak sinar / lintasan. Vektor kamera diatur di sini dan sinar dipancarkan untuk setiap piksel. Perbedaan antara pelacak sinar dan pelacak jalur adalah bahwa beberapa sinar dipancarkan per piksel dalam TP, yang sedikit bergeser secara acak. Kemudian warna yang diperoleh untuk setiap sinar dalam pixel diakumulasikan dalam tiga kanal float R, B, G. Pada akhirnya, koreksi nada hasil metode Reinhardt dilakukan.
Bagian terpenting adalah sampleCount, yang secara teoritis dapat diatur ke 1 untuk mempercepat rendering dan iterasi. Berikut adalah rendering sampel dengan nilai sampel dari 1 hingga 2048.
Judul spoiler
1

2

4

8

16

32

64

128

256

512

1024

2048
Kode Pembantu
Sepotong kode sederhana adalah fungsi pembantu. Dalam kasus ini, kami memiliki fungsi sepele min (), generator nilai acak dalam interval [0,1] dan boxTest (), yang jauh lebih menarik, yang merupakan bagian dari sistem Constructive Solid Geometry (CSG) yang digunakan untuk memotong dunia. CSG dibahas di bagian selanjutnya.
float min(float l, float r) { return l < r ? l : r; } float randomVal() { return (float) rand() / RAND_MAX; }
Fungsi geometri volumetrik konstruktif
Tidak ada simpul dalam kode. Semuanya dilakukan menggunakan fungsi CSG. Jika Anda tidak terbiasa dengan mereka, maka cukup katakan bahwa ini adalah fungsi yang menggambarkan apakah koordinat ada di dalam atau di luar objek. Jika fungsi mengembalikan jarak positif, maka titik ada di dalam objek. Jarak negatif menunjukkan bahwa titik berada di luar objek. Ada banyak fungsi untuk menggambarkan objek yang berbeda, tetapi demi penyederhanaan, mari kita ambil contoh bola dan dua titik, A dan B.
Fungsi testSphere () mengembalikan -1 untuk titik A (yaitu, di luar) dan 1 untuk B (yaitu, di dalam). Tanda pada jarak hanyalah tipuan, yang memungkinkan Anda untuk mendapatkan dua informasi, bukan satu dalam kasus nilai tunggal. Tipe fungsi yang serupa juga dapat ditulis untuk menggambarkan jajaran genjang (inilah yang dilakukan dalam fungsi BoxTest).
Sekarang mari kita lihat apa yang terjadi jika Anda membalik tanda nilai pengembalian.
Sekarang kita tidak menggambarkan objek yang solid, tetapi menyatakan seluruh dunia solid dan memotong ruang kosong di dalamnya. Fungsi dapat digunakan sebagai batu bata bangunan, yang bila digabungkan dapat menggambarkan bentuk yang lebih kompleks. Menggunakan operator tambahan logis (fungsi min) kita dapat memotong sepasang persegi panjang satu di atas yang lain dan hasilnya akan terlihat seperti ini.
Jika Anda memikirkannya, sepertinya ruangan itu sedang kita pelajari, karena ruangan bawah dinyatakan persis seperti ini - dengan bantuan dua jajaran genjang.
Sekarang, setelah menguasai pengetahuan CSG yang kuat, kita dapat kembali ke kode dan mempertimbangkan fungsi basis data, yang merupakan yang paling sulit untuk ditangani.
#define HIT_NONE 0 #define HIT_LETTER 1 #define HIT_WALL 2 #define HIT_SUN 3
Anda dapat melihat di sini fungsi "memotong" jajaran genjang, di mana hanya dua persegi panjang yang digunakan untuk membangun seluruh ruangan (otak kita melakukan sisanya, itu melambangkan dinding). Tangga horisontal adalah fungsi CSG sedikit lebih kompleks menggunakan pembagian sisa. Dan akhirnya, huruf-huruf dari kata PIXAR terdiri dari 15 baris dengan pasangan "asal / delta" dan dua case khusus untuk kurva dalam huruf P dan R.
Ray berjalan
Memiliki basis data fungsi CSG yang menggambarkan dunia, cukup bagi kita untuk melewati semua sinar yang dipancarkan dalam fungsi main (). Ray marching menggunakan fungsi jarak. Ini berarti bahwa posisi pengambilan sampel bergeser maju jarak ke rintangan terdekat.
Gagasan ray marching berdasarkan jarak adalah untuk bergerak maju jarak ke objek terdekat. Pada akhirnya, balok akan mendekati permukaan sedemikian rupa sehingga dapat dianggap sebagai titik kejadian.
Perhatikan bahwa marching ray tidak mengembalikan persimpangan sejati dengan permukaan, tetapi perkiraan. Itulah sebabnya marching berhenti di kode ketika d <0,01f.
Menyatukan Semuanya: Pengambilan sampel
Investigasi jalur pelacak hampir selesai. Kami kehilangan jembatan yang menghubungkan fungsi utama () dengan ray marcher. Bagian terakhir ini, yang saya beri nama "Jejak", adalah "otak" tempat sinar memantul atau berhenti, tergantung pada apa yang mereka hadapi.
Vec Trace(Vec origin, Vec direction) { Vec sampledPosition, normal, color, attenuation = 1; Vec lightDirection(!Vec(.6, .6, 1));
Saya bereksperimen sedikit dengan fungsi ini untuk mengubah jumlah maksimum pantulan sinar yang diizinkan. Nilai "2" memberi huruf-huruf warna Vantablack indah yang dipernis indah
[4] .
1234Kode sumber dibersihkan sepenuhnya
Untuk menyatukan semuanya, saya membuat
kode sumber yang benar-benar bersih.
Referensi
[1] Sumber:
posting Twitter lexfrench pada 8 Oktober 2018.[2] Sumber:
Wikipedia: Format gambar NetPBM[3] Sumber:
Visualisasi dilakukan pada MacBook Pro paling kuat, 2017[4] Sumber:
Wikipedia: Vantablack