Расшифровка трассировщика лучей размером с открытку

http://fabiensanglard.net/postcard_pathtracer/
  • Перевод

«Он снова это сделал!», — вот, что первое пришло мне в голову, когда я посмотрел на оборотную сторону флаера Pixar [1], полностью заполненную кодом. Скопление конструкций и выражений была подписана в правом нижнем углу не кем иным, как Эндрю Кенслером. Для тех, кто его не знает, скажу: Эндрю — это программист, придумавший в 2009 году 1337-байтный трассировщик лучей размером с визитку.

На этот раз Эндрю придумал нечто более объёмное, но с гораздо более интересным визуальным результатом. Так как я закончил писать свои Game Engine Black Books про Wolf3D и DOOM, у меня появилось время на изучение внутренностей его загадочного кода. И почти сразу меня буквально очаровали обнаруженные в нём техники. Они сильно отличались от предыдущей работы Эндрю, основанной на «стандартном» трассировщике лучей. Мне было интересно узнать о ray marching, функциях конструктивной объемной геометрии, рендеринге Монте-Карло/трассировкой пути, а также множестве других трюков, которые он использовал, чтобы ужать код в такой небольшой кусок бумаги.



Исходный код




Передняя часть флаера — это реклама отдела найма персонала компании Pixar. На обратной стороне напечатано 2 037 байта кода на C++, обфусцированного, чтобы занимать как можно меньшую поверхность.

#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){R V(x+r.x,y+r.y,z+r.z);}V O*(V r){R V(x*r.x,y*r.
y,z*r.z);}F O%(V r){R x*r.x+y*r.y+z*r.z;}V O!(){R*this*(1/sqrtf(*this%*this)
);}};F L(F l,F r){R l<r?l:r;}F U(){R(F)rand()/RAND_MAX;}F B(V p,V l,V h){l=p
+l*-1;h=h+p*-1;R-L(L(L(l.x,h.x),L(l.y,h.y)),L(l.z,h.z));}F S(V p,I&m){F d=1\
e9;V f=p;f.z=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,o.x>0?f\
absf(sqrtf(o%o)-2):(o.y+=o.y>0?-2:2,sqrtf(o%o)));}d=powf(powf(d,8)+powf(p.z,
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(p.x),8),p.y,p.z),V(1.5,18.5,-25),V(6.5,20,25)))
;if(r<d)d=r,m=2;F s=19.9-p.y;if(s<d)d=s,m=3;R d;}I M(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;}V T(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=n.z<0?-1:1,u=-1/(g+n.z),v=n.x*n.y*u;d=V(v,g+n.y*n.y*u,-n.y)*(cosf(p)*s)+V(
1+g*n.x*n.x*u,g*v,-g*n.x)*(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(g.z,
0,-g.x)*(1./w),u(g.y*l.z-g.z*l.y,g.z*l.x-g.x*l.z,g.x*l.y-g.y*l.x);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*(x-w/2+U())+u*(y-h/2+U())));c=c*(1./s)+14./241;V o=c+1;c=V(c.x/o.x,c.
y/o.y,c.z/o.z)*255;printf("%c%c%c",(I)c.x,(I)c.y,(I)c.z);}}// Andrew Kensler

Он вообще работает?




С кодом есть инструкция по его запуску. Идея заключается в том, чтобы перенаправить стандартный вывод в файл. По расширению можно предположить, что формат вывода — это текстовый формат изображений под названием NetPBM[2].

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

real	2m58.524s
user	2m57.567s
sys	0m0.415s

Спустя две минуты и пятьдесят восемь секунд[3] генерируется следующее изображение. Потрясающе, насколько мало кода для него требуется.


Из показанного выше изображения можно извлечь очень многое. Зернистость — очевидный признак «трассировщика пути». Этот тип рендерера отличается от трассировки лучей (raytracing) тем, что лучи не трассируются обратно к источникам освещения. В этом способе из источников испускаются тысячи лучей на пиксель и программа следит за ними, надеясь, что он найдут источник освещения. Это интересная техника, которая гораздо лучше, чем трассировка лучей, справляется с рендерингом ambient occlusion, мягких теней, каустики и radiosity.

Разобьём код на части




Передача ввода в CLion форматирует код (вывод см. здесь) и разбивает его на меньшие части/задачи.

#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){R V(x+r.x,y+r.y,z+r.z);}V O*(V r){R V(x*r.x,y*r.
y,z*r.z);}F O%(V r){R x*r.x+y*r.y+z*r.z;}V O!(){R*this*(1/sqrtf(*this%*this)
);}};
F L(F l,F r){R l<r?l:r;}F U(){R(F)rand()/RAND_MAX;}F B(V p,V l,V h){l=p
+l*-1;h=h+p*-1;R-L(L(L(l.x,h.x),L(l.y,h.y)),L(l.z,h.z));}
F S(V p,I&m){F d=1\
e9;V f=p;f.z=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,o.x>0?f\
absf(sqrtf(o%o)-2):(o.y+=o.y>0?-2:2,sqrtf(o%o)));}d=powf(powf(d,8)+powf(p.z,
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(p.x),8),p.y,p.z),V(1.5,18.5,-25),V(6.5,20,25)))
;if(r<d)d=r,m=2;F s=19.9-p.y;if(s<d)d=s,m=3;R d;}
I M(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;}
V T(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=n.z<0?-1:1,u=-1/(g+n.z),v=n.x*n.y*u;d=V(v,g+n.y*n.y*u,-n.y)*(cosf(p)*s)+V(
1+g*n.x*n.x*u,g*v,-g*n.x)*(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(g.z,
0,-g.x)*(1./w),u(g.y*l.z-g.z*l.y,g.z*l.x-g.x*l.z,g.x*l.y-g.y*l.x);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*(x-w/2+U())+u*(y-h/2+U())));c=c*(1./s)+14./241;V o=c+1;c=V(c.x/o.x,c.
y/o.y,c.z/o.z)*255;printf("%c%c%c",(I)c.x,(I)c.y,(I)c.z);}}
// Andrew Kensler

Каждый из разделов подробно описан в оставшейся части статьи:
— обычные трюки, — класс Vector, — вспомогательный код, — база данных, — Ray marching, — сэмплирование, — основной код.

Обычные трюки с #define и typedef




Обычные трюки — это использование #define и typedef для значительного уменьшения объёма кода. Здесь мы обозначаем F=float, I=int, R=return и O=operator. Реверс-инжиниринг выполняется тривиально.

Класс V




Далее идёт класс V, который я переименовал в Vec (даже несмотря на то, что, как мы увидим ниже, он также используется для хранения RGB-каналов в формате 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 + r.x, y + r.y, z + r.z); }
    Vec operator*(Vec r) { return Vec(x * r.x, y * r.y, z * r.z); }
    // dot product
    float operator%(Vec r) { return x * r.x + y * r.y + z * r.z; }
    // inverse square root
    Vec operator!() {return *this * (1 / sqrtf(*this % *this) );}
};

Заметьте, что здесь отсутствует оператор вычитания (-), поэтому вместо записи «X = A — B» используется «X = A + B * -1». Обратный квадратный корень пригождается в дальнейшем для нормализации векторов.

Функция Main




main() — это единственный символ, который нельзя обфусцировать, потому что он вызывается функцией _start библиотеки libc. Обычно стоит начинать с него, потому что так работать будет легче. Мне потребовалось какое-то время, чтобы догадаться о значениях первых букв, но всё-таки удалось создать нечто читаемое.

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 / o.x, color.y / o.y, color.z / o.z) * 255;
      printf("%c%c%c", (int) color.x, (int) color.y, (int) color.z);
    }
}

Заметьте, что литералы типа float не содержат буквы «f», а дробная часть отбрасывается для экономии места. Тот же трюк используется ниже, где отбрасывается целочисленная часть (float x = .5). Также необычна конструкция «for» с выражением итерации, вставленное внутрь условия останова.

Это довольно стандартная функция main для трассировщика лучей/пути. Здесь задаются векторы камеры и для каждого пикселя испускаются лучи. Разница между трассировщиком лучей и трассировщиком пути в том, что в ТП на пиксель испускается несколько лучей, которые слегка сдвинуты случайным образом. Затем цвет, полученный для каждого луча в пикселе накапливается в трёх float-каналах R,B,G. В конце выполняется тональная коррекция результата метододм Рейнхарда.

Самая важная часть — это sampleCount, которому теоретически можно присвоить значение 1 для ускорения рендеринга и итераций. Вот примеры визуализаций со значениями сэмплов от 1 до 2048.

Заголовок спойлера


1



2



4



8



16



32



64



128



256



512



1024



2048

Вспомогательный код




Ещё один простой фрагмент кода — вспомогательные функции. В данном случае у нас есть тривиальная функция min(), генератор случайных значений в интервале [0,1] и гораздо более интересная boxTest(), которая является частью системы конструктивной объемной геометрии (Constructive Solid Geometry, CSG), используемой для вырезания мира. Про CSG рассказывается в следующем разделе.

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));
}

Функции конструктивной объёмной геометрии




В коде нет вершин. Всё выполняется с помощью функций CSG. Если вы незнакомы с ними, то достаточно просто сказать, что это функции, описывающие, находится ли координата внутри или снаружи объекта. Если функция возвращает положительное расстояние, то точка находится внутри объекта. Отрицательное расстояние говорит о том, что точка снаружи объекта. Существует множество функции для описания разных объектов, но ради упрощения давайте для примера возьмём сферу и две точки, A и 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)

Функция testSphere() возвращает -1 для точки A (то есть она снаружи) и 1 для B (то есть она внутри). Знаки у расстояний — это просто трюк, позволяющий получить два элемента информации вместо одного в случае одного значения. Подобный тип функции можно написать и для описания параллелограмма (именно это и выполняется в функции function 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)

Теперь давайте посмотрим, что получится, если перевернуть знак возвращаемого значения.


  // 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)

Теперь мы не описываем твёрдый объект, а объявили твёрдым весь мир и вырезаем в нём пустое пространство. Функции можно использовать как строительные кирпичики, которые при комбинировании могу сочетании описывают более сложные формы. С помощью оператора логического сложения (функция min) мы можем вырезать пару прямоугольников один над другим и результат будет выглядеть следующим образом.


  // 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)

Если задуматься, то это выглядит как исследуемая нами комната, потому что именно так выражена нижняя комната — с помощью двух вырезанных параллелограммов.

Теперь, овладев могучими познаниями CSG, мы можем вернуться к коду и рассмотреть функцию базы данных, с которой разобраться сложнее всего.

#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)
  f.z = 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,
                   o.x > 0 ? fabsf(sqrtf(o % o) - 2)
                           : (o.y += o.y > 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;
}

Можно увидеть здесь функцию «вырезания» параллелограмма, в которой для построения целой комнаты используется всего два прямоугольника (всё остальное делает наш мозг, он представляет, что это стены). Горизонтальная лестница — это чуть более сложная CSG-функция, использующая деление с остатком. И, наконец, буквы слова PIXAR составлены из 15 линий с парой «начало координат/дельта» и двумя особыми случаями для кривых в буквах P и R.

Ray Marching




Имея базу данных CSG-функций, описывающих мир, нам достаточно пропустить все лучи, испущенные в функции main(). В ray marching используется функция расстояния. Это значит, что позиция сэмплирования сдвигается вперёд на расстояние до ближайшего препятствия.

// 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;
}

Идея ray marching с учётом расстояний заключается в том, чтобы двигаться вперёд на расстояние до ближайшего объекта. В конце концов луч приблизится к поверхности настолько, что точку можно будет считать точкой падения.


Заметьте, что ray marching возвращает не истинное пересечение с поверхностью, а приближение. Именно поэтому в коде marching останавливается, когда d < 0.01f.

Собираем всё вместе: сэмплирование




Исследование трассировщика пути почти завершено. Нам не хватает моста, который соединил бы функцию main() с ray marcher. Эта последняя часть, которую я переименовал в «Trace», является «мозгом», в котором лучи отражаются или останавливаются, в зависимости от того, с чем они столкнулись.

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;
}

Я немного поэкспериментировал с этой функцией, чтобы изменить максимальное количество допустимых отражений луча. Значение «2» придаёт буквам на удивление красивую лакированную окраску Vantablack[4].


1


2


3


4

Полностью очищенный исходный код




Чтобы собрать всё вместе, я создал полностью очищенный исходный код.

Справочные материалы




[1] Источник: пост в Twitter lexfrench 8 октября 2018 года

[2] Источник: Википедия: формат изображений NetPBM

[3] Источник: Визуализация выполнена на максимально мощном MacBook Pro, 2017

[4] Источник: Википедия: Vantablack
Поделиться публикацией

Похожие публикации

Комментарии 52

    +14

    Отличная программа. Мы с группой энтузиастов с SQL.RU (много флуда) перевели ее на десяток разных языков программирования, чтобы сравнить вычислительную скорость. Есть проект на гитхабе с результатами.


    Один из промежуточных более-менее полных результатов



    В результате победила с небольшим перевесом версия на С со вставками SSE4 инструкций.

      –8
      где питон, где VB?
        0
        По ссылкам не умеем переходить?
        github.com/Mark-Kovalyov/CardRaytracerBenchmark
          –45
          По незнакомым ссылкам не перехожу.
            +11

            Но с незнакомцами в интернете всё же разговариваете! :)

              +17
              Вы знаете питон, vb, но не знаете гитхаб? Оо
                0
                Вы их, как Столман — заказывайте по email.
                –7
                Ох уж эта советская дебильная манера говорить «не умеем переходить?», «проходим», «ноги вытираем»
                В ней даже нет и тени попытки быть вежливым с собеседником.
                0
                Питон в последней строке, версии на VB еще не было.
                0
                Запустил версию на ПХП, работала час, вывела другую картинку, там не «PIXAR» написано.
                  0
                  у нас этот же алгоритм, но были другие исходные данные матрицы.

                  пых, как и родной питон работали примерно с такой вот «скоростью»
                    0
                    Любопытно будет ли ускорение, если применить typehints на входные и выходные параметры, а ещё pow заменить на две звёздочки. Попробую позже, если не забуду.
                      0
                      А разве Python использует typehints для ускорения? Я что-то такое слышал применительно к Cython, мол они будут использовать стандартные typehints вместо своих типов, но когда-то в будущем. Может быть я ошибаюсь. Напишите, что получилось. Спасибо!
                        0
                        Я про PHP говорил.
                +3
                Жаль что переводчик к русскому языку относится намного небрежнее, чем автор к языку программы.
                  +20


                  Оригинальный троллинг, ящитаю.
                    +1
                    Картинки все в webp, не во всех браузерах отобразятся.
                    caniuse.com/#feat=webp
                      0

                      На ипхоне тоже не отображается

                        0
                        Между тем в оригинале используется HTML-элемент PICTURE с fallback-изображением в формате PNG.
                          0
                          Разве WebP входит в стандарт?
                            +2
                            В стандарт чего?
                              0
                              W3C
                              Не может же быть так, что все изображения работают «просто потому что»
                              Должен же быть какой-то минимальный список необходимых к поддержке форматов аудио и видео.
                                +1
                                В стандарте HTML не определён список обязательных для поддержки форматов изображений, и каждая реализация поддерживает собственный набор форматов.
                                MDN
                                  0

                                  Сейчас Firefox Beta показывает WebP, в этом месяце выйдет стабильная версия с его поддержкой.

                        0
                        В этом способе из источников испускаются тысячи лучей на пиксель и программа следит за ними, надеясь, что он найдут источник освещения.

                        Поясните плз. Из источника в источник?

                          0

                          Из камеры (точки, из которой смотрит зритель).

                          –1
                          Он снова сделал это = ему снова удалось.
                            0
                            А vantablack разве отражает свет?
                              +1

                              Это лакированный vantablack :)

                                0
                                Vantablack лакировать? Серьезно? Вы, наверное перепутали с Varnish black.
                              0
                              Титанический труд по обрезанию кода.
                              Чисто теоретически, его можно запустить на мк с внешней памятью?
                                0

                                А можно покрутить сцену? Интересно, что наверху и с другой стороны :)

                                  +1
                                    +2

                                    Там лишь стены и решетка, камера в углу — так что смотреть особо не на что.


                                    Гифки под спойлером




                                    (Извиняюсь за размеры и fps. Первую гифку habrastorage не пропускает, пришлось ужимать. А вторую и третью надоело ждать пока отрендерятся.)

                                    0
                                    Отправил автору оригинального текста письмо с косяками в его тексте.
                                    Посмотрим как он ответит.
                                    Косяки следующие:
                                    Функция testSphere() возвращает -1 для точки A (то есть она снаружи) и 1 для B (то есть она внутри). Знаки у расстояний — это просто трюк, позволяющий получить два элемента информации вместо одного в случае одного значения. Подобный тип функции можно написать и для описания параллелограмма (именно это и выполняется в функции function BoxTest).

                                    Судя по коду и комменту к нему, это всё же расстояние до сферы, если быть точнее, до её поверхности.
                                    // 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;
                                      }  

                                    Ну и наконец о том, что тут коммент лучше названия:
                                    // Sample the world using Signed Distance Fields.
                                    float QueryDatabase(Vec position, int &hitType) {

                                    Ибо это действительно функция которая считает Distance Field.
                                    А ещё, есть сильные подозрения что…
                                    В коде нет вершин. Всё выполняется с помощью функций CSG.

                                    Что CSG тут не причём. А всё выполняется с помощью SDF функций.
                                      0
                                      По поводу testSphere() каюсь что затупил. Причиной тому стало: -1, 1, слова про две информации, и наконец что знак это просто трюк. Почему-то понял это как два возможных значения. Просто точки A и B в примере неудачные, что значения такие получились.
                                      Знак при этом вовсе не трюк, а побочный эффект простой реализации функции. Кроме того он играет роль при комбинировании моделей.
                                      +1

                                      Просто картинка при 4096 сэмплах, не зря же ноут 10 часов мне воздух грел :)
                                      Хотя я от картинки с 2048 сэмплами из статьи разницы не увидел


                                      Тыц

                                      image

                                        +1
                                        Отличается. Причем в лучшую сторону. Но 10 часов. У меня сейчас 2048, FullHD делается.
                                        +1
                                        Я не настоящий сварщик, но может кто-нибудь про этот момент рассказать или ссылку дать:
                                        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)
                                        

                                        Сами знаете какое слово хочется написать.
                                          +2

                                          Коды символов используются как координаты. Можно заменить на массив целых. x0 y0 — начало линии и x1 y1 — конец.


                                          int letters[15*4+1] = {
                                            // (x0, y0), (x1, y1)
                                            // P (without curve)
                                            53, 79, 53, 95,
                                            53, 87, 57, 87,
                                            53, 95, 57, 95,
                                            // I
                                            65, 79, 69, 79,
                                            67, 79, 67, 95,
                                            65, 95, 69, 95,
                                            // X
                                            73, 79, 81, 95,
                                            73, 95, 81, 79,
                                            // A
                                            85, 79, 89, 95,
                                            89, 95, 93, 79,
                                            87, 87, 91, 87,
                                            // R (without curve)
                                            97, 79, 97, 95,
                                            97, 87, 101, 87,
                                            97, 95, 101, 95,
                                            99, 87, 105, 79
                                          };

                                          Вот здесь можете экспериментировать:
                                          https://codepen.io/anon/pen/bOYjNJ

                                            0
                                            Что-то не так:
                                            1drv.ms/u/s!Ai7NYx46sGsugdUAJBfOJMljf7BKJQ
                                            Копипастим, onedrive спотыкается на восклицательном знаке.
                                              +1

                                              Там же в комментариях написано without curve. Дугу из линий не построить, они дорисовываются вот тут:


                                              // 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,
                                                                 o.x > 0 ? fabsf(sqrtf(o % o) - 2)
                                                                         : (o.y += o.y > 0 ? -2 : 2, sqrtf(o % o))
                                                             );
                                                }
                                                0
                                                Это понятно, но где ноль? Какие-бы координаты не вводил, получается фигня. Я даже думал об относительных координатах, но и они не работают.
                                                  +1

                                                  x от 53 до 105 (слева направо)
                                                  y от 79 до 95 (снизу вверх)


                                                  53, 79, 105, 79, // линия у пола
                                                  53, 95, 105, 95, // верх надписи
                                                  +1
                                                  Всё, разобрался.
                                            +1
                                            Отличная статья про Raymarching Distance Fields: Трехмерный движок на формулах Excel для чайников. Автор этого алгоритма рендеринга — Inigo Quilez, у него на сайте самая подробная информация по теме, вот например 3D Distance Functions.
                                              +2
                                              Получилось! Тыц.
                                                +1

                                                Ну да, чем ещё заниматься 1го января русскому программисту ^_^

                                                  +2

                                                  А я наивно ждал другое слово:
                                                  habr

                                                    +2
                                                    Портировать это на 1С? =)
                                                      +3

                                                      Держите на Rust:


                                                      Cargo.toml
                                                      name = "pixar"
                                                      version = "0.1.0"
                                                      authors = ["AndrewKensler <xxx@xxx.xx>"]
                                                      edition = "2018"
                                                      
                                                      [dependencies]
                                                      rand = "*"
                                                      png = "*"
                                                      indicatif = "*"
                                                      num_cpus = "*"
                                                      
                                                      [profile.dev]
                                                      opt-level = 3

                                                      main.rs
                                                      const IS_HABR: bool = false;
                                                      const WIDTH: u32 = 960;
                                                      const HEIGHT: u32 = 540;
                                                      const SAMPLES_POW: u32 = 4;
                                                      const BOUNCE_COUNT: u32 = 3;
                                                      
                                                      const HIT_NONE: u8 = 0;
                                                      const HIT_LETTER: u8 = 1;
                                                      const HIT_WALL: u8 = 2;
                                                      const HIT_SUN: u8 = 3;
                                                      
                                                      #[derive(Copy, Clone)]
                                                      struct Vec3 {
                                                          x: f64,
                                                          y: f64,
                                                          z: f64,
                                                      }
                                                      
                                                      impl Vec3 {
                                                          fn new(x: f64, y: f64, z: f64) -> Vec3 {
                                                              Vec3 { x, y, z }
                                                          }
                                                          fn repeat(v: f64) -> Vec3 {
                                                              Vec3::new(v, v, v)
                                                          }
                                                          fn zeros() -> Vec3 {
                                                              Vec3::repeat(0.)
                                                          }
                                                          #[allow(dead_code)]
                                                          fn rotate_x(self, c: f64) -> Vec3 {
                                                              Vec3::new(
                                                                  self.x,
                                                                  self.y * c.cos() - self.z * c.sin(),
                                                                  self.y * c.sin() + self.z * c.cos(),
                                                              )
                                                          }
                                                          #[allow(dead_code)]
                                                          fn rotate_y(self, c: f64) -> Vec3 {
                                                              Vec3::new(
                                                                  self.z * c.sin() + self.x * c.cos(),
                                                                  self.y,
                                                                  self.z * c.cos() - self.x * c.sin(),
                                                              )
                                                          }
                                                          #[allow(dead_code)]
                                                          fn rotate_z(self, c: f64) -> Vec3 {
                                                              Vec3::new(
                                                                  self.x * c.cos() - self.y * c.sin(),
                                                                  self.x * c.sin() + self.y * c.cos(),
                                                                  self.z,
                                                              )
                                                          }
                                                      }
                                                      
                                                      use ::std::ops::{Add, Mul, Not, Rem};
                                                      
                                                      impl Add for Vec3 {
                                                          type Output = Vec3;
                                                          fn add(self, other: Vec3) -> Vec3 {
                                                              Vec3 {
                                                                  x: self.x + other.x,
                                                                  y: self.y + other.y,
                                                                  z: self.z + other.z,
                                                              }
                                                          }
                                                      }
                                                      impl Add<f64> for Vec3 {
                                                          type Output = Vec3;
                                                          fn add(self, other: f64) -> Vec3 {
                                                              Vec3 {
                                                                  x: self.x + other,
                                                                  y: self.y + other,
                                                                  z: self.z + other,
                                                              }
                                                          }
                                                      }
                                                      impl Mul for Vec3 {
                                                          type Output = Vec3;
                                                          fn mul(self, other: Vec3) -> Vec3 {
                                                              Vec3 {
                                                                  x: self.x * other.x,
                                                                  y: self.y * other.y,
                                                                  z: self.z * other.z,
                                                              }
                                                          }
                                                      }
                                                      impl Mul<f64> for Vec3 {
                                                          type Output = Vec3;
                                                          fn mul(self, other: f64) -> Vec3 {
                                                              Vec3 {
                                                                  x: self.x * other,
                                                                  y: self.y * other,
                                                                  z: self.z * other,
                                                              }
                                                          }
                                                      }
                                                      impl Rem for Vec3 {
                                                          type Output = f64;
                                                          fn rem(self, other: Vec3) -> f64 {
                                                              self.x * other.x + self.y * other.y + self.z * other.z
                                                          }
                                                      }
                                                      impl Not for Vec3 {
                                                          type Output = Vec3;
                                                          fn not(self) -> Vec3 {
                                                              self * (1. / (self % self).sqrt())
                                                          }
                                                      }
                                                      
                                                      // Rectangle CSG equation. Returns minimum signed distance from
                                                      // space carved by
                                                      // lowerLeft vertex and opposite rectangle vertex upperRight.
                                                      fn box_test(position: Vec3, lower_left: Vec3, upper_right: Vec3) -> f64 {
                                                          let lower_left = position + lower_left * -1.;
                                                          let upper_right = upper_right + position * -1.;
                                                          -lower_left
                                                              .x
                                                              .min(lower_left.y)
                                                              .min(lower_left.z)
                                                              .min(upper_right.x)
                                                              .min(upper_right.y)
                                                              .min(upper_right.z)
                                                      }
                                                      
                                                      fn min(a: f64, b: f64) -> f64 {
                                                          a.min(b)
                                                      }
                                                      
                                                      // Sample the world using Signed Distance Fields.
                                                      fn query_database(position: Vec3, hit_type: &mut u8) -> f64 {
                                                          let mut distance = 1_000_000_000.0_f64;
                                                          let mut f = position; // Flattened position (z=0)
                                                          f.z = 0.;
                                                          if IS_HABR {
                                                              let letters = [
                                                                  // X
                                                                  (57, 79, 65, 95),
                                                                  (57, 95, 65, 79),
                                                                  // A
                                                                  (69, 79, 73, 95),
                                                                  (73, 95, 77, 79),
                                                                  (71, 87, 75, 87),
                                                                  // B (without curves)
                                                                  (81, 79, 81, 95),
                                                                  (81, 79, 85, 79),
                                                                  (81, 87, 85, 87),
                                                                  (81, 95, 85, 95),
                                                                  // R (without curve)
                                                                  (93, 79, 93, 95),
                                                                  (93, 87, 97, 87),
                                                                  (93, 95, 97, 95),
                                                                  (95, 87, 101, 79),
                                                              ];
                                                      
                                                              for letter in letters.iter() {
                                                                  let begin = Vec3::new(letter.0 as f64 - 79., letter.1 as f64 - 79., 0.) * 0.5;
                                                                  let e = Vec3::new(letter.2 as f64 - 79., letter.3 as f64 - 79., 0.) * 0.5 + begin * -1.;
                                                                  let o = f + (begin + e * (-((begin + f * -1.) % e / (e % e)).min(0.)).min(1.)) * -1.;
                                                                  distance = distance.min(o % o); // compare squared distance.
                                                              }
                                                          } else {
                                                              let letters = [
                                                                  // 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 letter in letters.iter() {
                                                                  let points = letter.as_bytes();
                                                                  let begin = Vec3::new(points[0] as f64 - 79., points[1] as f64 - 79., 0.) * 0.5;
                                                                  let e = Vec3::new(points[2] as f64 - 79., points[3] as f64 - 79., 0.) * 0.5 + begin * -1.;
                                                                  let o = f + (begin + e * (-((begin + f * -1.) % e / (e % e)).min(0.)).min(1.)) * -1.;
                                                                  distance = distance.min(o % o); // compare squared distance.
                                                              }
                                                          }
                                                          distance = distance.sqrt(); // Get real distance, not square distance.
                                                      
                                                          // Two curves (for P and R in PixaR) with hard-coded locations.
                                                          let curves = if IS_HABR {
                                                              vec![Vec3::new(3., 6., 0.), Vec3::new(3., 2., 0.), Vec3::new(9., 6., 0.)]
                                                          } else {
                                                              vec![Vec3::new(-11., 6., 0.), Vec3::new(11., 6., 0.)]
                                                          };
                                                          for curve in curves.iter() {
                                                              let mut o = f + *curve * -1.;
                                                              distance = distance.min(if o.x > 0. {
                                                                  ((o % o).sqrt() - 2.).abs()
                                                              } else {
                                                                  o.y += if o.y > 0. { -2. } else { 2. };
                                                                  (o % o).sqrt()
                                                              });
                                                          }
                                                          distance = (distance.powf(8.) + position.z.powf(8.)).powf(0.125) - 0.5;
                                                          *hit_type = HIT_LETTER;
                                                      
                                                          let room_dist = min(
                                                              // min(A,B) = Union with Constructive solid geometry
                                                              //-min carves an empty space
                                                              -min(
                                                                  // Lower room
                                                                  box_test(
                                                                      position,
                                                                      Vec3::new(-30., -0.5, -30.),
                                                                      Vec3::new(30., 18., 30.),
                                                                  ),
                                                                  // Upper room
                                                                  box_test(
                                                                      position,
                                                                      Vec3::new(-25., 17., -25.),
                                                                      Vec3::new(25., 20., 25.),
                                                                  ),
                                                              ),
                                                              box_test(
                                                                  // Ceiling "planks" spaced 8 units apart.
                                                                  Vec3::new(position.x.abs() % 8., position.y, position.z),
                                                                  Vec3::new(1.5, 18.5, -25.),
                                                                  Vec3::new(6.5, 20., 25.),
                                                              ),
                                                          );
                                                          if room_dist < distance {
                                                              distance = room_dist;
                                                              *hit_type = HIT_WALL;
                                                          };
                                                      
                                                          let sun = 19.9 - position.y; // Everything above 19.9 is light source.
                                                          if sun < distance {
                                                              distance = sun;
                                                              *hit_type = HIT_SUN;
                                                          }
                                                      
                                                          return distance;
                                                      }
                                                      
                                                      fn ray_marching(origin: Vec3, direction: Vec3, hit_pos: &mut Vec3, hit_norm: &mut Vec3) -> u8 {
                                                          let mut hit_type = HIT_NONE;
                                                          let mut no_hit_count = 0;
                                                      
                                                          // Signed distance marching
                                                          let mut total_d = 0.;
                                                          while total_d < 100. {
                                                              *hit_pos = origin + direction * total_d;
                                                              let d = query_database(*hit_pos, &mut hit_type); // distance from closest object in world.
                                                              if d < 0.01 || no_hit_count > 99 {
                                                                  *hit_norm = !Vec3::new(
                                                                      query_database(*hit_pos + Vec3::new(0.01, 0., 0.), &mut no_hit_count) - d,
                                                                      query_database(*hit_pos + Vec3::new(0., 0.01, 0.), &mut no_hit_count) - d,
                                                                      query_database(*hit_pos + Vec3::new(0., 0., 0.01), &mut no_hit_count) - d,
                                                                  );
                                                                  return hit_type;
                                                              }
                                                              no_hit_count += 1;
                                                              total_d += d;
                                                          }
                                                          return 0;
                                                      }
                                                      
                                                      fn trace(origin: Vec3, direction: Vec3) -> Vec3 {
                                                          let mut origin = origin;
                                                          let mut direction = direction;
                                                          let mut sampled_position = Vec3::zeros();
                                                          let mut normal = Vec3::zeros();
                                                          let mut color = Vec3::zeros();
                                                          let mut attenuation = Vec3::repeat(1.);
                                                      
                                                          let light_direction = !Vec3::new(0.6, 0.6, 1.0);
                                                      
                                                          let bounce_count = BOUNCE_COUNT;
                                                          for _ in 0..bounce_count {
                                                              let hit_type = ray_marching(origin, direction, &mut sampled_position, &mut normal);
                                                              if hit_type == HIT_NONE {
                                                                  break;
                                                              } // No hit. This is over, return color.
                                                              if hit_type == HIT_LETTER {
                                                                  // Specular bounce on a letter. No color acc.
                                                                  direction = direction + normal * (normal % direction * -2.);
                                                                  origin = sampled_position + direction * 0.1;
                                                                  attenuation = attenuation * 0.2; // Attenuation via distance traveled.
                                                              }
                                                              if hit_type == HIT_WALL {
                                                                  // Wall hit uses color yellow?
                                                                  let incidence = normal % light_direction;
                                                                  let p = 6.283185 * random_val();
                                                                  let c = random_val();
                                                                  let s = (1. - c).sqrt();
                                                                  let g = if normal.z < 0. { -1. } else { 1. };
                                                                  let u = -1. / (g + normal.z);
                                                                  let v = normal.x * normal.y * u;
                                                                  direction = Vec3::new(v, g + normal.y * normal.y * u, -normal.y) * p.cos() * s
                                                                      + Vec3::new(1. + g * normal.x * normal.x * u, g * v, -g * normal.x) * p.sin() * s
                                                                      + normal * c.sqrt();
                                                                  origin = sampled_position + direction * 0.1;
                                                                  attenuation = attenuation * 0.2;
                                                                  if incidence > 0.
                                                                      && ray_marching(
                                                                          sampled_position + normal * 0.1,
                                                                          light_direction,
                                                                          &mut sampled_position,
                                                                          &mut normal,
                                                                      ) == HIT_SUN
                                                                  {
                                                                      color = color + attenuation * Vec3::new(500., 400., 100.) * incidence;
                                                                  }
                                                              }
                                                              if hit_type == HIT_SUN {
                                                                  //
                                                                  color = color + attenuation * Vec3::new(50., 80., 100.);
                                                                  break; // Sun Color
                                                              }
                                                          }
                                                          return color;
                                                      }
                                                      
                                                      fn random_val() -> f64 {
                                                          rand::random()
                                                      }
                                                      
                                                      fn get_sampling_points(pow: u32) -> Vec<(f64, f64)> {
                                                          let mut points = vec![];
                                                          let max = 1 << pow;
                                                          for a in 0..max {
                                                              let mut b = 0;
                                                              for i in 0..pow {
                                                                  let mask_a = 1 << i;
                                                                  let bit = (mask_a & a) != 0;
                                                                  if bit {
                                                                      let mask_b = 1 << (pow - i - 1);
                                                                      b = b | mask_b;
                                                                  }
                                                              }
                                                              points.push(((a as f64 + 0.5) / max as f64, (b as f64 + 0.5) / max as f64));
                                                          }
                                                          points
                                                      }
                                                      use std::time::Instant;
                                                      use std::thread;
                                                      
                                                      extern crate indicatif;
                                                      use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
                                                      
                                                      fn main() {
                                                          let w = WIDTH as i32;
                                                          let h = HEIGHT as i32;
                                                          let samples_power = SAMPLES_POW;
                                                          let samples_count = 1 << samples_power;
                                                      
                                                          let now = Instant::now();
                                                          let m = MultiProgress::new();
                                                          let sty = ProgressStyle::default_bar()
                                                              .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {eta_precise}")
                                                              .progress_chars("=>-");
                                                      
                                                          let get_promise = move |y0, y1, pb: ProgressBar| -> Vec<u8> {
                                                              let sampling_points = &get_sampling_points(samples_power);
                                                              let position = Vec3::new(-22., 5., 25.);
                                                              let goal = !(Vec3::new(-3., 4., 0.) + position * -1.);
                                                      
                                                              let left = Vec3::new(goal.z, 0., -goal.x) * (1. / w as f64);
                                                              let up = Vec3::new(
                                                                  goal.y * left.z - goal.z * left.y,
                                                                  goal.z * left.x - goal.x * left.z,
                                                                  goal.x * left.y - goal.y * left.x,
                                                              );
                                                      
                                                              let mut image_data: Vec<u8> = Vec::with_capacity((w * h * 3) as usize);
                                                      
                                                              for y in y0..y1 {
                                                                  for x in 0..w {
                                                                      let x = (w / 2 - x) as f64;
                                                                      let y = (h / 2 - y) as f64;
                                                                      let mut color = Vec3::zeros();
                                                                      for p in sampling_points {
                                                                          let x = x + p.0;
                                                                          let y = y + p.1;
                                                                          color = color + trace(position, !(goal + left * x + up * y));
                                                                      }
                                                                      color = color * (1. / samples_count as f64) + 14. / 241.;
                                                                      let o = color + 1.;
                                                                      color = Vec3::new(color.x / o.x, color.y / o.y, color.z / o.z) * 255.;
                                                      
                                                                      image_data.push(color.x.max(0.).min(255.) as u8);
                                                                      image_data.push(color.y.max(0.).min(255.) as u8);
                                                                      image_data.push(color.z.max(0.).min(255.) as u8);
                                                                  }
                                                                  pb.inc(1);
                                                              }
                                                              pb.finish_with_message("done");
                                                              image_data
                                                          };
                                                      
                                                          println!("Rendering...");
                                                          let mut promices = vec![];
                                                          let num_logical_cores = num_cpus::get() as i32;
                                                          for n in 0..num_logical_cores {
                                                              let fragment_size = h / num_logical_cores;
                                                              let y0 = fragment_size * n;
                                                              let y1 = if n == num_logical_cores - 1 {
                                                                  h
                                                              } else {
                                                                  fragment_size * (n + 1)
                                                              };
                                                              let length = (y1 - y0) as u64;
                                                      
                                                              let pb = m.add(ProgressBar::new(length));
                                                              pb.set_style(sty.clone());
                                                      
                                                              let promice = thread::spawn(move || {
                                                                  get_promise(y0, y1, pb)
                                                              });
                                                      
                                                              promices.push(promice);
                                                          }
                                                      
                                                          m.join_and_clear().unwrap();
                                                          let mut image_data = Vec::new();
                                                          for promice in promices {
                                                              let mut thread_image_data = promice.join().unwrap();
                                                              image_data.append(&mut thread_image_data);
                                                          }
                                                      
                                                          println!("{} sec", now.elapsed().as_secs());
                                                      
                                                          let name = format!("{}_{:04}.png", if IS_HABR { "habr" } else { "pixar" }, samples_count);
                                                          let path = std::path::Path::new(&name);
                                                          let file = std::fs::File::create(path).unwrap();
                                                          let ref mut writer_1 = std::io::BufWriter::new(file);
                                                      
                                                          let mut encoder = png::Encoder::new(writer_1, w as u32, h as u32);
                                                          use png::HasParameters;
                                                          encoder.set(png::ColorType::RGB).set(png::BitDepth::Eight);
                                                          let mut writer = encoder.write_header().unwrap();
                                                          writer.write_image_data(&image_data).unwrap();
                                                      }
                                                      +1
                                                      Например учебник Русского языка почитать. Чтоб 1го не проскакивало. Хотя… да какая разница в новогодние каникулы.

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                  Самое читаемое