“Ele fez de novo!” - foi o que me ocorreu quando olhei para o verso do folheto da Pixar
[1] , completamente preenchido com código. Um conjunto de construções e expressões foi assinado no canto inferior direito por ninguém menos que Andrew Kensler. Para quem não o conhece, direi: Andrew é um programador que inventou um
ray tracer de 1337 bytes em 2009.
Desta vez, Andrew apresentou algo mais volumoso, mas com um resultado visual muito mais interessante. Desde que terminei de escrever meus livros negros do Game Engine sobre
Wolf3D e
DOOM , tive tempo de aprender o interior de seu código enigmático. E quase imediatamente, fiquei literalmente fascinado pelas técnicas descobertas nele. Eles eram muito diferentes do trabalho anterior de Andrew, baseado em um traçador de raios "padrão". Eu estava interessado em aprender sobre marchar com raios, recursos de geometria volumétrica construtiva, renderização / rastreamento de caminhos de Monte Carlo, bem como muitos outros truques que ele usou para espremer código em um pedaço de papel tão pequeno.
Código fonte
A frente do folheto é um anúncio para o departamento de recrutamento da Pixar. No verso, são impressos 2.037 bytes de código C ++, ofuscados para ocupar a menor superfície possível.
#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
Ele trabalha mesmo?
Com o código, há uma instrução para seu lançamento. A idéia é redirecionar a saída padrão para um arquivo. Por extensão, podemos assumir que o formato de saída é um formato de imagem de texto chamado NetPBM
[2] .
$ clang -o card2 -O3 raytracer.cpp
$ time ./card> pixar.ppm
2m58.524s reais
usuário 2m57.567s
sys 0m0.415s
Após dois minutos e cinquenta e oito segundos
[3] , a seguinte imagem é gerada. É incrível como pouco código é necessário para isso.
Você pode extrair muito da imagem acima. Grit é um sinal óbvio de um "rastreador de caminho". Esse tipo de renderizador difere do rastreamento de raios, pois os raios não são rastreados de volta às fontes de luz. Nesse método, milhares de raios por pixel são emitidos das fontes e o programa os monitora, esperando que eles encontrem a fonte de luz. Essa é uma técnica interessante que, muito melhor do que o traçado de raios, pode lidar com a renderização de oclusão ambiental, sombras suaves, cáusticas e radiosidade.
Vamos dividir o código em partes
Passar entrada para o CLion formata o código (veja a saída
aqui ) e o divide em partes / tarefas menores.
#include <stdlib.h> // cartão> 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% * isto)
);
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 (lx, hx), L (ly, hy)), L (lz, hz));}
FS (Vp, 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 "; para (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)}; para (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 e h, V e n) {I m, s =
0; F t = 0, c; para (; t <100; t + = c) se ((c = S (h = o + d * t, m)) <. 01 || ++ s> 99) R n =! V (S (h + V (0,01,0
), s) -c, S (h + V (0, 01), s) -c, S (h + V (0,0, 0,01), s) -c), m; R 0;}
VT (V o, V d) {V h, n, r, t =
1, l (! V (.6, .6,1)); para (I b = 3; b -;) {I m = M (o, d, h, n); se (! M) quebrar ; se (m == 1) {d = d + n * (
n% d * -2); o = h + d * .1; t = t * .2;} se (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; se (i> 0 && M (h
+ n * .1, l, h, n) == 3) r = r + t * V (500,400,100) * i;} se (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); para (I y = h; y -;) para (I x = w; x -;) {V c; para (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
Cada uma das seções é descrita em detalhes no restante do artigo:
■ - truques comuns,
■ - classe vetorial,
■ - código auxiliar,
■ - banco de dados,
■ - marcha de raios,
■ - amostragem,
■ - código principal.
Truques comuns com #define e typedef
Truques comuns estão usando #define e typedef para reduzir significativamente a quantidade de código. Aqui denotamos F = float, I = int, R = return e O = operador. A engenharia reversa é trivial.
Classe v
Em seguida, vem a classe V, que renomeei para Vec (embora, como veremos abaixo, ela também seja usada para armazenar canais RGB no formato 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); }
Observe que não há operador de subtração (-); portanto, em vez de escrever "X = A - B", "X = A + B * -1" é usado. A raiz quadrada inversa é útil mais tarde para normalizar os vetores.
Função principal
main () é o único caractere que não pode ser ofuscado porque é chamado pela função _start da biblioteca libc. Geralmente, vale a pena começar com isso, porque será mais fácil trabalhar dessa maneira. Demorei um pouco para descobrir o significado das primeiras letras, mas ainda assim consegui criar algo legível.
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);
Observe que literais flutuantes não contêm a letra "f" e a parte fracionária é descartada para economizar espaço. O mesmo truque é usado abaixo, onde a parte inteira é descartada (float x = 0,5). Também incomum é a construção "for" com uma expressão de iteração inserida dentro da condição de interrupção.
Esta é uma função principal bastante padrão para um rastreador de raio / caminho. Os vetores da câmera são definidos aqui e os raios são emitidos para cada pixel. A diferença entre o traçador de raios e o traçador de caminhos é que vários raios são emitidos por pixel no TP, que são levemente deslocados aleatoriamente. Então a cor obtida para cada raio em um pixel é acumulada em três canais de flutuação R, B, G. No final, é realizada a correção tonal do resultado do método Reinhardt.
A parte mais importante é sampleCount, que teoricamente pode ser definido como 1 para acelerar a renderização e a iteração. Aqui estão representações de amostra com valores de amostra de 1 a 2048.
Título de spoiler
1

2

4

8

16

32.

64

128

256

512

1024

2048
Código auxiliar
Outro trecho simples de código são as funções auxiliares. Nesse caso, temos uma função trivial min (), um gerador de valor aleatório no intervalo [0,1] e um boxTest muito mais interessante, que faz parte do sistema Construct Solid Geometry (CSG) usado para cortar o mundo. O CSG é discutido na próxima seção.
float min(float l, float r) { return l < r ? l : r; } float randomVal() { return (float) rand() / RAND_MAX; }
Funções da geometria volumétrica construtiva
Não há vértices no código. Tudo é feito usando as funções CSG. Se você não estiver familiarizado com elas, basta dizer que essas são funções que descrevem se a coordenada está dentro ou fora do objeto. Se a função retornar uma distância positiva, o ponto estará dentro do objeto. Uma distância negativa indica que o ponto está fora do objeto. Existem muitas funções para descrever objetos diferentes, mas, para simplificar, tomemos, por exemplo, uma esfera e dois pontos, A e B.
A função testSphere () retorna -1 para o ponto A (ou seja, está fora) e 1 para B (ou seja, está dentro). Sinais a distâncias são apenas um truque, permitindo que você obtenha duas informações em vez de uma no caso de um único valor. Um tipo semelhante de função pode ser escrito para descrever um paralelogramo (é exatamente isso que é executado na função BoxTest).
Agora vamos ver o que acontece se você virar o sinal do valor de retorno.
Agora não descrevemos um objeto sólido, mas declaramos o mundo inteiro sólido e cortamos um espaço vazio nele. As funções podem ser usadas como tijolos de construção, que quando combinados podem descrever formas mais complexas. Usando o operador de adição lógica (função min), podemos cortar um par de retângulos um acima do outro e o resultado será semelhante a este.
Se você pensar bem, parece a sala que estamos estudando, porque a sala inferior é expressa exatamente dessa maneira - com a ajuda de dois paralelogramos cortados.
Agora, tendo dominado o poderoso conhecimento do CSG, podemos retornar ao código e considerar a função do banco de dados, que é a mais difícil de lidar.
#define HIT_NONE 0 #define HIT_LETTER 1 #define HIT_WALL 2 #define HIT_SUN 3
Você pode ver aqui a função de "cortar" o paralelogramo, no qual apenas dois retângulos são usados para construir toda a sala (nosso cérebro faz o resto, representa paredes). A escada horizontal é uma função CSG um pouco mais complexa usando a divisão restante. E, finalmente, as letras da palavra PIXAR são compostas por 15 linhas com um par "origem / delta" e dois casos especiais para curvas nas letras P e R.
Marcha de raio
Tendo um banco de dados de funções CSG descrevendo o mundo, é suficiente ignorarmos todos os raios emitidos na função main (). A marcha de raio usa a função de distância. Isso significa que a posição de amostragem se move para frente uma distância até o obstáculo mais próximo.
A idéia de marchar com raios com base na distância é avançar uma distância até o objeto mais próximo. No final, o feixe se aproximará tanto da superfície que pode ser considerado um ponto de incidência.
Observe que a marcha de raios não retorna uma interseção verdadeira com a superfície, mas uma aproximação. É por isso que a marcha para no código quando d <0,01f.
Juntando tudo: amostragem
A investigação do traçador de caminhos está quase completa. Está faltando uma ponte que conecte a função main () ao ray marcher. Esta última parte, que renomei de “Trace”, é o “cérebro” no qual os raios saltam ou param, dependendo do que encontram.
Vec Trace(Vec origin, Vec direction) { Vec sampledPosition, normal, color, attenuation = 1; Vec lightDirection(!Vec(.6, .6, 1));
Eu experimentei um pouco com essa função para alterar o número máximo de reflexões de feixe permitidas. O valor "2" confere às letras uma cor Vantablack lacada surpreendentemente bela
[4] .
1234Código fonte completamente limpo
Para juntar tudo, criei um
código fonte completamente limpo.
Referências
[1] Fonte:
lexfrench post no Twitter em 8 de outubro de 2018.[2] Fonte:
Wikipedia: formato de imagem NetPBM[3] Fonte:
visualização realizada no mais poderoso MacBook Pro, 2017[4] Fonte:
Wikipedia: Vantablack