Décryptage de Ray Tracer au format carte postale


"Il l'a encore fait!" - c'est ce que la première chose m'est venue quand j'ai regardé l'arrière du flyer Pixar [1] , qui était complètement rempli de code. Un groupe de constructions et d'expressions a été signé dans le coin inférieur droit par nul autre que Andrew Kensler. Pour ceux qui ne le connaissent pas, je dirai: Andrew est un programmeur qui a inventé un traceur de rayons de la taille d'une carte de visite de 1337 octets en 2009.

Cette fois, Andrew a proposé quelque chose de plus volumineux, mais avec un résultat visuel beaucoup plus intéressant. Depuis que j'ai fini d'écrire mes Black Books Game Engine sur Wolf3D et DOOM , j'ai eu le temps d'apprendre l'intérieur de son code cryptique. Et presque immédiatement, j'ai été littéralement fasciné par les techniques découvertes en lui. Ils étaient très différents des travaux précédents d'Andrew, basés sur un traceur de rayons "standard". J'étais intéressé à en apprendre davantage sur la marche des rayons, les caractéristiques de la géométrie volumétrique constructive, le rendu Monte Carlo / traçage de chemin, ainsi que de nombreuses autres astuces qu'il a utilisées pour compresser le code dans un si petit morceau de papier.



Code source




Le recto du dépliant est une publicité pour le service de recrutement Pixar. Au verso, 2 037 octets de code C ++ sont imprimés, obscurcis pour couvrir le moins de surface possible.

#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 

Travaille-t-il même?




Avec le code, il y a une instruction pour son lancement. L'idée est de rediriger la sortie standard vers un fichier. Par extension, nous pouvons supposer que le format de sortie est un format d'image texte appelé NetPBM [2] .

  $ clang -o card2 -O3 raytracer.cpp
 $ time ./card> pixar.ppm

 réel 2m58.524s
 utilisateur 2m57.567s
 sys 0m0.415s 

Après deux minutes et cinquante-huit secondes [3] , l'image suivante est générée. C'est incroyable le peu de code nécessaire pour cela.


Vous pouvez extraire beaucoup de l'image ci-dessus. Le grain est un signe évident d'un «traceur de chemin». Ce type de rendu diffère du lancer de rayons en ce que les rayons ne sont pas retracés jusqu'aux sources de lumière. Dans cette méthode, des milliers de rayons par pixel sont émis par les sources et le programme les surveille, en espérant qu'ils trouveront la source de lumière. Il s'agit d'une technique intéressante qui, bien mieux que le lancer de rayons, peut gérer le rendu de l'occlusion ambiante, des ombres douces, des caustiques et de la radiosité.

Nous allons diviser le code en plusieurs parties




Passer l'entrée à CLion formate le code (voir la sortie ici ) et le divise en parties / tâches plus petites.

  #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% * ceci) 
  );}}; 
  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 (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 "; pour (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)}; pour (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))) 
  ; si (r <d) d = r, m = 2; F s = 19,9-py; si (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; pour (; t <100; t + = c) si ((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)); pour (I b = 3; b -;) {I m = M (o, d, h, n); si (! M) casse ; si (m == 1) {d = d + n * ( 
  n% d * -2); o = h + d * .1; t = t * .2;} si (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; si (i> 0 && M (h 
  + n * .1, l, h, n) == 3) r = r + t * V (500,400,100) * i;} si (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); pour (I y = h; y -;) pour (I x = w; x -;) {V c; pour (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 

Chacune des sections est décrite en détail dans la suite de l'article:
- astuces ordinaires, - classe Vector, - code auxiliaire, - base de données, - marche de rayons, - échantillonnage, - code principal.

Astuces courantes avec #define et typedef




Les astuces courantes utilisent #define et typedef pour réduire considérablement la quantité de code. Ici, nous désignons F = float, I = int, R = return et O = operator. L'ingénierie inverse est triviale.

Classe v




Vient ensuite la classe V, que j'ai renommée Vec (même si, comme nous le verrons ci-dessous, elle est également utilisée pour stocker des canaux RVB au format flottant).

 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); } // dot product float operator%(Vec r) { return x * rx + y * ry + z * rz; } // inverse square root Vec operator!() {return *this * (1 / sqrtf(*this % *this) );} }; 

Notez qu'il n'y a pas d'opérateur de soustraction (-), donc au lieu d'écrire "X = A - B", "X = A + B * -1" est utilisé. La racine carrée inverse est utile plus tard pour normaliser les vecteurs.

Fonction principale




main () est le seul caractère qui ne peut pas être obscurci car il est appelé par la fonction _start de la bibliothèque libc. Cela vaut généralement la peine de commencer, car il sera plus facile de travailler de cette façon. Il m'a fallu un certain temps pour comprendre la signification des premières lettres, mais j'ai quand même réussi à créer quelque chose de lisible.

 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); // Cross-product to get the up vector Vec up(goal.y * left.z - goal.z * left.y, goal.z * left.x - goal.x * left.z, goal.x * left.y - goal.y * left.x); printf("P6 %d %d 255 ", w, h); for (int y = h; y--;) for (int x = w; x--;) { Vec color; for (int p = samplesCount; p--;) color = color + Trace(position, !(goal + left * (x - w / 2 + randomVal())+ up * (y - h / 2 + randomVal()))); // Reinhard tone mapping color = color * (1. / samplesCount) + 14. / 241; Vec o = color + 1; color = Vec(color.x / ox, color.y / oy, color.z / oz) * 255; printf("%c%c%c", (int) color.x, (int) color.y, (int) color.z); } } 

Notez que les littéraux flottants ne contiennent pas la lettre «f» et la partie fractionnaire est supprimée pour économiser de l'espace. La même astuce est utilisée ci-dessous, où la partie entière est supprimée (float x = .5). La construction "for" avec une expression d'itération insérée à l'intérieur de la condition de rupture est également inhabituelle.

Il s'agit d'une fonction principale assez standard pour un traceur de rayon / chemin. Les vecteurs de caméra sont définis ici et des rayons sont émis pour chaque pixel. La différence entre le traceur de rayons et le traceur de trajet est que plusieurs rayons sont émis par pixel dans le TP, qui sont légèrement décalés de manière aléatoire. Ensuite, la couleur obtenue pour chaque rayon d'un pixel est accumulée dans trois canaux flottants R, B, G. À la fin, une correction tonale du résultat de la méthode Reinhardt est effectuée.

La partie la plus importante est sampleCount, qui peut théoriquement être défini sur 1 pour accélérer le rendu et l'itération. Voici des exemples de rendus avec des exemples de valeurs de 1 à 2048.

En-tête de spoiler


1



2



4



8



16



32



64



128



256



512



1024



2048

Code d'assistance




Un autre morceau de code simple est les fonctions d'aide. Dans ce cas, nous avons une fonction triviale min (), un générateur de valeurs aléatoires dans l'intervalle [0,1] et un boxTest () beaucoup plus intéressant, qui fait partie du système Constructive Solid Geometry (CSG) utilisé pour découper le monde. Le CSG est discuté dans la section suivante.

 float min(float l, float r) { return l < r ? l : r; } float randomVal() { return (float) rand() / RAND_MAX; } // Rectangle CSG equation. Returns minimum signed distance from // space carved by lowerLeft vertex and opposite rectangle // vertex upperRight. float BoxTest(Vec position, Vec lowerLeft, Vec upperRight) { lowerLeft = position + lowerLeft * -1; upperRight = upperRight + position * -1; return -min( min( min(lowerLeft.x, upperRight.x), min(lowerLeft.y, upperRight.y) ), min(lowerLeft.z, upperRight.z)); } 

Fonctions de la géométrie volumétrique constructive




Il n'y a pas de sommets dans le code. Tout se fait à l'aide des fonctions CSG. Si vous ne les connaissez pas, dites simplement que ce sont des fonctions qui décrivent si la coordonnée est à l'intérieur ou à l'extérieur de l'objet. Si la fonction renvoie une distance positive, le point se trouve à l'intérieur de l'objet. Une distance négative indique que le point est en dehors de l'objet. Il existe de nombreuses fonctions pour décrire différents objets, mais par souci de simplification, prenons par exemple une sphère et deux points, A et B.

image

 // Signed distance point(p) to sphere(c,r) float testSphere(Vec p, Vec c, float r) { Vec delta = c - p; float distance = sqrtf(delta%delta); return radius - distance; } Vec A {4, 6}; Vec B {3, 2}; Vec C {4, 2}; float r = 2.; testSphere(A, C, r); // == -1 (outside) testSphere(B, C, r); // == 1 (inside) 

La fonction testSphere () renvoie -1 pour le point A (c'est-à-dire qu'il est à l'extérieur) et 1 pour B (c'est-à-dire qu'il est à l'intérieur). Les signes à distance ne sont qu'une astuce, vous permettant d'obtenir deux informations au lieu d'une dans le cas d'une valeur. Un type de fonction similaire peut également être écrit pour décrire un parallélogramme (c'est exactement ce qui est effectué dans la fonction BoxTest).


  // Signed distance point(p) to Box(c1,c2) float testRectangle(Vec p, Vec c1, Vec c2) { c1 = p + c1 * -1; c2 = c2 + position * -1; return min( min( min(c1.x, c2.x), min(c1.y, c2.y)), min(c1.z, c2.z)); } Vec A {3, 3}; Vec B {4, 6}; Vec C1 {2, 2}; Vec C2 {5, 4}; testRectangle(A, C1, C2); // 1.41 (inside) testRectangle(B, C1, C2); // -2.23 (outside) 

Voyons maintenant ce qui se passe si vous retournez le signe de la valeur de retour.


  // Signed distance point(p) to carved box(c1,c2) float testCarveBox(Vec p, Vec c1, Vec c2) { c1 = p + c1 * -1; c2 = c2 + position * -1; return -min( min( min(c1.x, c2.x), min(c1.y, c2.y)), min(c1.z, c2.z)); } Vec A {3, 3}; Vec B {4, 6}; Vec C1 {2, 2}; Vec C2 {5, 4}; testCarveBox(A, C1, C2); // == -1.41 (outside) testCarveBox(B, C1, C2); // == 2.23 (inside) 

Maintenant, nous ne décrivons pas un objet solide, mais déclarons le monde entier solide et y découpons un espace vide. Les fonctions peuvent être utilisées comme briques de construction qui, lorsqu'elles sont combinées, peuvent décrire des formes plus complexes. En utilisant l'opérateur d'addition logique (fonction min), nous pouvons découper une paire de rectangles l'un au-dessus de l'autre et le résultat ressemblera à ceci.


  // Signed distance point to room float testRoom(Vec p) { Vec C1 {2, 4}; Vec C2 {5, 2}; // Lower room Vec C3 {3, 5}; Vec C4 {4, 4}; // Upper room // min() is the union of the two carved volumes. return min(testCarvedBox(p, C1, C2), testCarvedBox(p, C3, C4)); } Vec A {3, 3}; Vec B {4, 6}; testRoom(A, C1, C2); // == -1.41 (outside) testRoom(B, C1, C2); // == 1.00 (inside) 

Si vous y réfléchissez, cela ressemble à la pièce que nous étudions, car la pièce inférieure s'exprime exactement de cette façon - à l'aide de deux parallélogrammes coupés.

Maintenant, après avoir maîtrisé la puissante connaissance de CSG, nous pouvons revenir au code et considérer la fonction de base de données, qui est la plus difficile à gérer.

 #define HIT_NONE 0 #define HIT_LETTER 1 #define HIT_WALL 2 #define HIT_SUN 3 // Sample the world using Signed Distance Fields. float QueryDatabase(Vec position, int &hitType) { float distance = 1e9; Vec f = position; // Flattened position (z=0) fz = 0; char letters[15*4+1] = // 15 two points lines "5O5_" "5W9W" "5_9_" // P (without curve) "AOEO" "COC_" "A_E_" // I "IOQ_" "I_QO" // X "UOY_" "Y_]O" "WW[W" // A "aOa_" "aWeW" "a_e_" "cWiO"; // R (without curve) for (int i = 0; i < sizeof(letters); i += 4) { Vec begin = Vec(letters[i] - 79, letters[i + 1] - 79) * .5; Vec e = Vec(letters[i + 2] - 79, letters[i + 3] - 79) * .5 + begin * -1; Vec o = f + (begin + e * min(-min((begin + f * -1) % e / (e % e), 0), 1) ) * -1; distance = min(distance, o % o); // compare squared distance. } distance = sqrtf(distance); // Get real distance, not square distance. // Two curves (for P and R in PixaR) with hard-coded locations. Vec curves[] = {Vec(-11, 6), Vec(11, 6)}; for (int i = 2; i--;) { Vec o = f + curves[i] * -1; distance = min(distance, ox > 0 ? fabsf(sqrtf(o % o) - 2) : (oy += oy > 0 ? -2 : 2, sqrtf(o % o)) ); } distance = powf(powf(distance, 8) + powf(position.z, 8), .125) - .5; hitType = HIT_LETTER; float roomDist ; roomDist = min(// min(A,B) = Union with Constructive solid geometry //-min carves an empty space -min(// Lower room BoxTest(position, Vec(-30, -.5, -30), Vec(30, 18, 30)), // Upper room BoxTest(position, Vec(-25, 17, -25), Vec(25, 20, 25)) ), BoxTest( // Ceiling "planks" spaced 8 units apart. Vec(fmodf(fabsf(position.x), 8), position.y, position.z), Vec(1.5, 18.5, -25), Vec(6.5, 20, 25) ) ); if (roomDist < distance) distance = roomDist, hitType = HIT_WALL; float sun = 19.9 - position.y ; // Everything above 19.9 is light source. if (sun < distance)distance = sun, hitType = HIT_SUN; return distance; } 

Vous pouvez voir ici la fonction de "découper" le parallélogramme, dans lequel seuls deux rectangles sont utilisés pour construire toute la pièce (notre cerveau fait le reste, il représente les murs). L'échelle horizontale est une fonction CSG légèrement plus complexe utilisant la division du reste. Et enfin, les lettres du mot PIXAR sont composées de 15 lignes avec une paire «origine / delta» et deux cas particuliers pour les courbes dans les lettres P et R.

Ray marchant




Ayant une base de données des fonctions CSG décrivant le monde, il nous suffit de sauter tous les rayons émis dans la fonction main (). La marche des rayons utilise la fonction de distance. Cela signifie que la position d'échantillonnage se déplace d'une distance vers l'obstacle le plus proche.

 // Perform signed sphere marching // Returns hitType 0, 1, 2, or 3 and update hit position/normal int RayMarching(Vec origin, Vec direction, Vec &hitPos, Vec &hitNorm) { int hitType = HIT_NONE; int noHitCount = 0; float d; // distance from closest object in world. // Signed distance marching for (float total_d=0; total_d < 100; total_d += d) if ((d = QueryDatabase(hitPos = origin + direction * total_d, hitType)) < .01 || ++noHitCount > 99) return hitNorm = !Vec(QueryDatabase(hitPos + Vec(.01, 0), noHitCount) - d, QueryDatabase(hitPos + Vec(0, .01), noHitCount) - d, QueryDatabase(hitPos + Vec(0, 0, .01), noHitCount) - d) , hitType; // Weird return statement where a variable is also updated. return 0; } 

L'idée de la marche des rayons basée sur la distance est d'avancer d'une distance vers l'objet le plus proche. En fin de compte, le faisceau s'approche tellement de la surface qu'il peut être considéré comme un point d'incidence.


Notez que la marche des rayons ne renvoie pas une véritable intersection avec la surface, mais une approximation. C'est pourquoi la marche s'arrête dans le code lorsque d <0,01f.

Tout mettre ensemble: échantillonnage




L'enquête sur le traceur est presque terminée. Il nous manque un pont qui relie la fonction main () au rayon marcher. Cette dernière partie, que j'ai renommée «Trace», est le «cerveau» dans lequel les rayons rebondissent ou s'arrêtent, selon ce qu'ils rencontrent.

 Vec Trace(Vec origin, Vec direction) { Vec sampledPosition, normal, color, attenuation = 1; Vec lightDirection(!Vec(.6, .6, 1)); // Directional light for (int bounceCount = 3; bounceCount--;) { int hitType = RayMarching(origin, direction, sampledPosition, normal); if (hitType == HIT_NONE) break; // No hit. This is over, return color. if (hitType == HIT_LETTER) { // Specular bounce on a letter. No color acc. direction = direction + normal * ( normal % direction * -2); origin = sampledPosition + direction * 0.1; attenuation = attenuation * 0.2; // Attenuation via distance traveled. } if (hitType == HIT_WALL) { // Wall hit uses color yellow? float incidence = normal % lightDirection; float p = 6.283185 * randomVal(); float c = randomVal(); float s = sqrtf(1 - c); float g = normal.z < 0 ? -1 : 1; float u = -1 / (g + normal.z); float v = normal.x * normal.y * u; direction = Vec(v, g + normal.y * normal.y * u, -normal.y) * (cosf(p) * s) + Vec(1 + g * normal.x * normal.x * u, g * v, -g * normal.x) * (sinf(p) * s) + normal * sqrtf(c); origin = sampledPosition + direction * .1; attenuation = attenuation * 0.2; if (incidence > 0 && RayMarching(sampledPosition + normal * .1, lightDirection, sampledPosition, normal) == HIT_SUN) color = color + attenuation * Vec(500, 400, 100) * incidence; } if (hitType == HIT_SUN) { // color = color + attenuation * Vec(50, 80, 100); break; // Sun Color } } return color; } 

J'ai expérimenté un peu avec cette fonction pour changer le nombre maximum de réflexions de faisceau autorisées. La valeur "2" donne aux lettres une couleur Vantablack laquée d'une beauté surprenante [4] .


1


2


3


4

Code source complètement nettoyé




Pour tout assembler, j'ai créé un code source complètement propre.

Les références




[1] Source: publication Twitter de lexfrench du 8 octobre 2018.

[2] Source: Wikipedia: format d'image NetPBM

[3] Source: Visualisation effectuée sur le MacBook Pro le plus puissant, 2017

[4] Source: Wikipedia: Vantablack

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


All Articles