"Er hat es wieder getan!" - das ist mir als erstes eingefallen, als ich auf die Rückseite des Pixar-Flyers
[1] schaute, der vollständig mit Code gefüllt war. Eine Gruppe von Konstruktionen und Ausdrücken wurde in der unteren rechten Ecke von niemand anderem als Andrew Kensler signiert. Für diejenigen, die ihn nicht kennen, sage ich: Andrew ist ein Programmierer, der 2009 einen 1337-Byte
-Raytracer in
Visitenkartengröße erfunden hat.
Diesmal hatte Andrew etwas umfangreicheres, aber ein viel interessanteres visuelles Ergebnis. Seit ich meine Game Engine Black Books über
Wolf3D und
DOOM fertig geschrieben habe, hatte ich Zeit, die Innenseiten des kryptischen Codes zu lernen. Und fast sofort war ich buchstäblich fasziniert von den Techniken, die in ihm entdeckt wurden. Sie unterschieden sich sehr von Andrews früheren Arbeiten, die auf einem "Standard" -Rochen-Tracer basierten. Ich war daran interessiert, etwas über Strahlenmarsch, Merkmale konstruktiver volumetrischer Geometrie, Monte-Carlo-Rendering / Pfadverfolgung sowie viele andere Tricks zu lernen, mit denen er Code in ein so kleines Stück Papier drückte.
Quellcode
Die Vorderseite des Flyers ist eine Anzeige für die Personalabteilung von Pixar. Auf der Rückseite sind 2.037 Bytes C ++ - Code gedruckt, der verschleiert ist, um die kleinstmögliche Oberfläche zu belegen.
#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
Arbeitet er überhaupt?
Mit dem Code gibt es eine Anweisung für den Start. Die Idee ist, die Standardausgabe in eine Datei umzuleiten. In der Erweiterung können wir annehmen, dass das Ausgabeformat ein Textbildformat namens NetPBM
[2] ist .
$ clang -o card2 -O3 raytracer.cpp
$ time ./card> pixar.ppm
echte 2m58.524s
Benutzer 2m57.567s
sys 0m0.415s
Nach zwei Minuten und achtundfünfzig Sekunden
[3] wird das folgende Bild erzeugt. Es ist erstaunlich, wie wenig Code dafür benötigt wird.
Sie können viel aus dem obigen Bild extrahieren. Grit ist ein offensichtliches Zeichen für einen "Pfad-Tracer". Diese Art von Renderer unterscheidet sich von Raytracing dadurch, dass die Strahlen nicht auf die Lichtquellen zurückgeführt werden. Bei diesem Verfahren werden Tausende von Strahlen pro Pixel von den Quellen emittiert und vom Programm überwacht, in der Hoffnung, dass sie die Lichtquelle finden. Dies ist eine interessante Technik, die viel besser als Raytracing das Rendern von Umgebungsokklusion, weichen Schatten, Ätzmitteln und Radiosität handhaben kann.
Wir werden den Code in Teile zerlegen
Durch die Übergabe der Eingabe an CLion wird der Code formatiert (siehe Ausgabe
hier ) und in kleinere Teile / Aufgaben unterteilt.
#include <stdlib.h> // card> pixar.ppm
#include <stdio.h>
#include <math.h>
#define R return
#define O Operator
typedef float F; typedef int I;
Struktur 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 (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 "; für (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); Va [] = {V (-11,6), V (11,6)}; für (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 & le; -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)))
; wenn (r <d) d = r, m = 2; F s = 19,9-py; wenn (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; für (; t <100; t + = c) wenn ((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, 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)); für (I b = 3; b -;) {I m = M (o, d, h, n); wenn (! M) brechen ; wenn (m == 1) {d = d + n * (
n% d * -2); o = h + d * .1; t = t * .2;} wenn (m == 2) {Fi = 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 * 0,1; t = t *. 2; wenn (i> 0 && M (h
+ n * 0,1, l, h, n) == 3) r = r + t * V (500.400,100) * i;} wenn (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); für (I y = h; y -;) für (I x = w; x -;) {V c; für (I p = s; p- -;) c = c + T (e
,! (g + 1 * (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
Jeder der Abschnitte wird im Rest des Artikels ausführlich beschrieben:
■ - gewöhnliche Tricks,
■ - Vektorklasse,
■ - Hilfscode,
■ - Datenbank,
■ - Ray Marching,
■ - Sampling,
■ - Hauptcode.
Allgemeine Tricks mit #define und typedef
Übliche Tricks sind die Verwendung von #define und typedef, um die Codemenge erheblich zu reduzieren. Hier bezeichnen wir F = float, I = int, R = return und O = operator. Reverse Engineering ist trivial.
Klasse v
Als nächstes kommt die Klasse V, die ich in Vec umbenannt habe (obwohl sie, wie wir weiter unten sehen werden, auch zum Speichern von RGB-Kanälen im Float-Format verwendet wird).
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); }
Beachten Sie, dass es keinen Subtraktionsoperator (-) gibt. Statt "X = A - B" zu schreiben, wird "X = A + B * -1" verwendet. Die inverse Quadratwurzel ist später nützlich, um die Vektoren zu normalisieren.
Hauptfunktion
main () ist das einzige Zeichen, das nicht verschleiert werden kann, da es von der _start-Funktion der libc-Bibliothek aufgerufen wird. Es lohnt sich normalerweise, damit zu beginnen, da es einfacher ist, auf diese Weise zu arbeiten. Es dauerte eine Weile, bis ich die Bedeutung der ersten Buchstaben herausgefunden hatte, aber es gelang mir trotzdem, etwas Lesbares zu schaffen.
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);
Beachten Sie, dass Float-Literale nicht den Buchstaben „f“ enthalten und der Bruchteil aus Platzgründen verworfen wird. Der gleiche Trick wird unten verwendet, wo der ganzzahlige Teil gelöscht wird (float x = .5). Ungewöhnlich ist auch das Konstrukt "for" mit einem Iterationsausdruck, der in die Unterbrechungsbedingung eingefügt wird.
Dies ist eine ziemlich standardmäßige Hauptfunktion für einen Ray / Path-Tracer. Hier werden Kameravektoren eingestellt und für jedes Pixel Strahlen ausgesendet. Der Unterschied zwischen dem Ray Tracer und dem Path Tracer besteht darin, dass pro TP im TP mehrere Strahlen emittiert werden, die leicht zufällig verschoben sind. Dann wird die für jeden Strahl in einem Pixel erhaltene Farbe in drei Gleitkanälen R, B, G akkumuliert. Am Ende wird eine tonale Korrektur des Ergebnisses der Reinhardt-Methode durchgeführt.
Der wichtigste Teil ist sampleCount, das theoretisch auf 1 gesetzt werden kann, um das Rendern und die Iteration zu beschleunigen. Hier sind Beispiel-Renderings mit Beispielwerten von 1 bis 2048.
Spoiler Überschrift
1

2

4

8

16

32

64

128

256

512

1024

2048
Hilfecode
Ein weiterer einfacher Code sind Hilfsfunktionen. In diesem Fall haben wir eine triviale Funktion min (), einen Zufallswertgenerator im Intervall [0,1] und einen viel interessanteren boxTest (), der Teil des CSG-Systems (Constructive Solid Geometry) ist, mit dem die Welt ausgeschnitten wird. CSG wird im nächsten Abschnitt behandelt.
float min(float l, float r) { return l < r ? l : r; } float randomVal() { return (float) rand() / RAND_MAX; }
Funktionen der konstruktiven Volumengeometrie
Der Code enthält keine Eckpunkte. Alles wird mit CSG-Funktionen erledigt. Wenn Sie mit ihnen nicht vertraut sind, sagen Sie einfach, dass dies Funktionen sind, die beschreiben, ob sich die Koordinate innerhalb oder außerhalb des Objekts befindet. Wenn die Funktion einen positiven Abstand zurückgibt, befindet sich der Punkt innerhalb des Objekts. Ein negativer Abstand zeigt an, dass sich der Punkt außerhalb des Objekts befindet. Es gibt viele Funktionen zum Beschreiben verschiedener Objekte. Zur Vereinfachung nehmen wir zum Beispiel eine Kugel und zwei Punkte, A und B.
Die Funktion testSphere () gibt -1 für Punkt A (dh außerhalb) und 1 für B (dh innen) zurück. Zeichen in Entfernungen sind nur ein Trick, mit dem Sie bei einem einzelnen Wert zwei Informationen anstelle von einer erhalten können. Ein ähnlicher Funktionstyp kann auch zur Beschreibung eines Parallelogramms geschrieben werden (genau dies wird in der Funktion BoxTest ausgeführt).
Nun wollen wir sehen, was passiert, wenn Sie das Vorzeichen des Rückgabewerts umdrehen.
Jetzt beschreiben wir kein festes Objekt, sondern erklären die ganze Welt für fest und schneiden darin leeren Raum aus. Funktionen können als Bausteine verwendet werden, die in Kombination komplexere Formen beschreiben können. Mit dem logischen Additionsoperator (min-Funktion) können wir ein Paar Rechtecke übereinander ausschneiden, und das Ergebnis sieht folgendermaßen aus.
Wenn Sie darüber nachdenken, sieht es aus wie der Raum, den wir studieren, weil der untere Raum genau so ausgedrückt wird - mit Hilfe von zwei geschnittenen Parallelogrammen.
Nachdem wir nun die leistungsstarken Kenntnisse von CSG beherrschen, können wir zum Code zurückkehren und die Datenbankfunktion betrachten, die am schwierigsten zu handhaben ist.
#define HIT_NONE 0 #define HIT_LETTER 1 #define HIT_WALL 2 #define HIT_SUN 3
Sie können hier die Funktion des "Ausschneidens" des Parallelogramms sehen, bei dem nur zwei Rechtecke verwendet werden, um den gesamten Raum zu konstruieren (unser Gehirn erledigt den Rest, es repräsentiert Wände). Die horizontale Leiter ist eine etwas komplexere CSG-Funktion unter Verwendung der Restteilung. Und schließlich bestehen die Buchstaben des Wortes PIXAR aus 15 Zeilen mit einem „Ursprung / Delta“ -Paar und zwei Sonderfällen für Kurven in den Buchstaben P und R.
Ray marschiert
Mit einer Datenbank von CSG-Funktionen, die die Welt beschreiben, reicht es aus, alle in der main () - Funktion emittierten Strahlen zu überspringen. Ray Marching verwendet die Distanzfunktion. Dies bedeutet, dass sich die Probenahmeposition um eine Strecke nach vorne zum nächsten Hindernis verschiebt.
Die Idee des Strahlmarschierens basierend auf der Entfernung besteht darin, eine Entfernung vorwärts zum nächsten Objekt zu bewegen. Am Ende nähert sich der Strahl der Oberfläche so sehr, dass er als Einfallspunkt angesehen werden kann.
Beachten Sie, dass Ray Marching keinen echten Schnittpunkt mit der Oberfläche zurückgibt, sondern eine Annäherung. Deshalb stoppt das Marschieren im Code, wenn d <0,01f ist.
Alles zusammen: Sampling
Die Untersuchung des Pfadverfolgers ist fast abgeschlossen. Es fehlt eine Brücke, die die main () - Funktion mit dem Ray Marcher verbindet. Dieser letzte Teil, den ich in „Spur“ umbenannt habe, ist das „Gehirn“, in dem die Strahlen je nach Begegnung abprallen oder aufhören.
Vec Trace(Vec origin, Vec direction) { Vec sampledPosition, normal, color, attenuation = 1; Vec lightDirection(!Vec(.6, .6, 1));
Ich habe ein wenig mit dieser Funktion experimentiert, um die maximal zulässige Anzahl von Strahlreflexionen zu ändern. Der Wert „2“ verleiht den Buchstaben eine überraschend schöne lackierte Vantablack-Farbe
[4] .
1234Vollständig bereinigter Quellcode
Um alles zusammenzusetzen, habe ich einen völlig sauberen
Quellcode erstellt .
Referenzen
[1] Quelle:
lexfrench Twitter-Beitrag am 8. Oktober 2018.[2] Quelle:
Wikipedia: NetPBM-Bildformat[3] Quelle:
Visualisierung auf dem leistungsstärksten MacBook Pro, 2017[4] Quelle:
Wikipedia: Vantablack