Comments 124
Вот, пожалуйста – tcbrindle/raytracer.hpp.
Generating the above 512x512 image at compile time took around 45 minutes with Clang 4.0 on my Macbook Pro.
Огонь! Интересно что он дальше пишет что в рантайме эта же задача выполняется за пол-секунды, т.е. компилятор не очень хорошо трассирует лучи. Вариант по ссылке в основном основан на constexpr (т.е. код вполне понимаемый), но еще дальше он пишет что вариант трассировки лучей написанный чисто на шаблонах работает еще медленней. Интересно, никогда не думал о таком аспекте constexpr как ускорение компиляции.
Моя основная задача — показать проекты, которые интересно (и легко!) программировать
Да что бы такое програмировать это нужно быть гением.
студенты нашего провинциального университета
Судя по почте на github, это Université de Lorraine. Ну, не такой он провинциальный. 201-300 место в ARWU2018. Тот же МФТИ – 410-500 место. Студенты, я подозреваю, тоже не самые простые.
Ещё один простой пример: École Polytechnique, школа с сильнейшими французским студентами, находится на 401-500 в этом же самом рейтинге. Неужто она хуже провинциального университета (я по-прежнему про Université de Lorraine), который на 201-300? Нет, она меньше. Бюджет важен для рейтинга. Кстати, следите за руками. Университет Лотарингии был создан в 2012м году путём слияния шести (!) различных вузов, близких друг к другу географически. Для чего это было сделано? Подсказка: см. слова «бюджет», «количество студентов».
С юга на север больше двухсот километров между кампусами.
Я совсем не об этом. Не знаю, как у других, но у меня слова «провинциальный вуз» ассоциируются с вузом, который вообще ни в какие рейтинги не входит, и в котором студентам-программистам на паре по компьютерной графике показывают пиратский Corel Draw, а сами студенты если и разбираются в программировании, то не благодаря вузу, а вопреки. К сожалению, такие вузы существуют. Я просто как-то смотрел учебные планы и рабочие программы небольших региональных вузов, и там всё не так хорошо, как хотелось бы. Может, я ошибаюсь, и таких вузов почти и не осталось, в которых ООП на TurboPascal на третьем курсе учат (условно).
Извините за оффтопик, но раз уж заговорили. Вообще, я заметил по другим публикациям, что для вас характерна «чрезмерная» скромность. Вы пишете, что не специалист и сами плохо в чём-то разбираетесь, а на самом деле разбираетесь достаточно хорошо. Что это всё легко понять студентам обычного провинциального вуза, но вуз не такой уж и обычный, а вполне себе хороший. Что какую-то тему вы прошли ещё в школе, но при этом оказывается, что это специализированная физмат-школа (извините, если ошибаюсь, я мог и перепутать).
Наверняка у вас благие намерения. Вы хотите мотивировать других людей, показать им, что не боги горшки обжигают. Но если материал для кого-то сложный, то слова о том, что он простой и школьного уровня, могут лишь заставить человека подумать: «Ну вот, школьники понимают, а я не понимаю. Видать, я недотёпа». Самооценка падает, ничего делать не хочется.
Так получилось, что я довольно долго проработал в вузе и параллельно вёл кружок в местном лицее. Вуз маленький, конкурсы небольшие, разброс в подготовке студентов очень большой. А в лицее на физмате много талантливых ребят. И иногда так получалось, что я одну и ту же тему рассказывал восьмиклассникам в кружке и студентам (например, линейные корректирующие коды). И бывало, что восьмиклассники схватывали быстрее. Но если бы я сказал студентам, что это школьная тема – что недалеко от истины, в общем-то, – вряд ли бы это подняло их мотивацию. Это просто звучало бы обидно.
Это всё вовсе не критика, у вас замечательные статьи, с удовольствием их читаю. Просто наблюдение. Вы, безусловно, возразите мне, что это статьи, которые может понять школьник, а не обязан их понять, и что не стоит «читать их за чашкой чая». И будете правы. Но я не о формальной стороне, а о коннотации. Просто учитывайте, что аудитория на хабре с более широким разбросом уровня подготовки.
Что касается хабра, то, конечно, я вынужден признать, что в силу критического недостатка времени я тут не всегда пишу самодостаточные тексты. Обычно я пишу либо для себя самого, когда разбираюсь в новой теме, либо (как конкретно эта статья, обратите внимание, что я уже опубликовал её перевод) в качестве записей для своих студентов, но, в отличие от хабра, они дополнительно ещё имеют цирк со мной у доски.
А что абсолютно всё, о чём я пишу, это просто — так ведь это правда. Если я не сумел написать доступно — ну я так себе лектор. В комментариях ведь можно задавать вопросы, правда? Достаточно иметь огонь в глазах :) Вопреки почему-то распространённому мнению, на хабре совсем не только дерьмом поливают, но и вдумчиво отвечают на вопросы.
Для примера напишу про свой — Волгоградский Государственный Технический Университет, опорный ВУЗ, между прочим :)
На первом курсе был матан, линейная алгебра, дискретка и прочие годные вещи и Кумир. Программистская часть программы была составлена с учетом абсолютно нулевых начальных знаний в этой области у студентов.
Думаю, если поискать, и его удастся найти. А какая цель? Посмотреть на возможную ошибку в расчете отражений? На С#, кстати.
Кода намного больше, а делает он намного меньше.
Единственное, чем я горжусь, то что все фигуры строятся из одного «Лепестка», который потом поворачивается, отражается и переносится. Немного посидел с листочком с расчетами, и вместо задания кучи точек смог обойтись четыремя.
Лично я при изучении нового языка программирования практически всегда пишу рейтрейсер, т.к. только так я могу погрузиться в новый язык с интересом и пользой.
По-вашему, я гений в n-й степени, где n-количество языков, которые я изучил?
Спасибо.
int save_tiff_rgb(const char* filename,void* data,int w,int h,int bpl) {
enum { dpi=96, o_bps=8, o_attr=14, n_attr=15, hdr_size=o_attr+6+12*n_attr };
FILE *f; int y,res=1,dbpl=w*3,sz=h*dbpl;
const char hdr[hdr_size]={
0x49,0x49,0x2A,0, o_attr,0,0,0,
8,0, 8,0, 8,0, // o_bps=@8 bit per sample
n_attr,0, // o_attr=@14 AttrCount=15
0xFE,0, 4,0, 1,0,0,0, 0,0,0,0, // NewSubfileType=0
0,1, 3,0, 1,0,0,0, w,w>>8,0,0, // ImageWidth=w
1,1, 3,0, 1,0,0,0, h,h>>8,0,0, // ImageLength=h
2,1, 3,0, 3,0,0,0, o_bps,0,0,0, // BitPerSample={8,8,8}
3,1, 3,0, 1,0,0,0, 1,0,0,0, // Compression=none
6,1, 3,0, 1,0,0,0, 2,0,0,0, // PhotometricInterpretation=2
0x11,1, 4,0, 1,0,0,0, hdr_size,0,0,0,// StripOffset=@200
0x12,1, 3,0, 1,0,0,0, 1,0,0,0, // Orientation=1 from top-left
0x15,1, 3,0, 1,0,0,0, 3,0,0,0, // SamplesPerPixel=3
0x16,1, 3,0, 1,0,0,0, h,h>>8,0,0, // RowsPerStrip=h
0x17,1, 4,0, 1,0,0,0, sz,sz>>8,sz>>16,sz>>24, // StripBytesCounts
0x1A,1, 3,0, 1,0,0,0, dpi,dpi>>8,0,0,// XResolution
0x1B,1, 3,0, 1,0,0,0, dpi,dpi>>8,0,0,// YResolution
0x1C,1, 3,0, 1,0,0,0, 1,0,0,0, // PlanarConfiguration=1 RGBRGB...
0x28,1, 3,0, 1,0,0,0, 2,0,0,0, // ResulutionUnit=inch
0,0,0,0 // Next IFD offset
};
f=fopen(filename,"wb+"); if (!f) return 1;
fwrite(hdr,1,hdr_size,f);
for(y=0;y<h;y++) fwrite((char*)data+y*bpl,1,dbpl,f);
fclose(f);
return 0;
}
Его так же можно использовать для 10битного цвета, для пущей красочности.
typedef unsigned short uint16;
typedef unsigned int uint32;
int save_tiff_rgb10a2(const char* filename,void* data,int w,int h,int bpl) {
enum { dpi=96, o_bps=8, o_attr=14, n_attr=15, hdr_size=o_attr+6+12*n_attr };
uint32 c,v,*s; uint16* line; FILE *f; int x,y,res=1,dbpl=w*6,sz=h*dbpl;
const char hdr[hdr_size]={
0x49,0x49,0x2A,0, o_attr,0,0,0,
16,0, 16,0, 16,0, // o_bps=@8 bit per sample
n_attr,0, // o_attr=@14 AttrCount=15
0xFE,0, 4,0, 1,0,0,0, 0,0,0,0, // NewSubfileType=0
0,1, 3,0, 1,0,0,0, w,w>>8,0,0, // ImageWidth=w
1,1, 3,0, 1,0,0,0, h,h>>8,0,0, // ImageLength=h
2,1, 3,0, 3,0,0,0, o_bps,0,0,0, // BitPerSample={16,16,16}
3,1, 3,0, 1,0,0,0, 1,0,0,0, // Compression=none
6,1, 3,0, 1,0,0,0, 2,0,0,0, // PhotometricInterpretation=2
0x11,1, 4,0, 1,0,0,0, hdr_size,0,0,0,// StripOffset=@200
0x12,1, 3,0, 1,0,0,0, 1,0,0,0, // Orientation=1 from top-left
0x15,1, 3,0, 1,0,0,0, 3,0,0,0, // SamplesPerPixel=3
0x16,1, 3,0, 1,0,0,0, h,h>>8,0,0, // RowsPerStrip=h
0x17,1, 4,0, 1,0,0,0, sz,sz>>8,sz>>16,sz>>24, // StripBytesCounts
0x1A,1, 3,0, 1,0,0,0, dpi,dpi>>8,0,0,// XResolution
0x1B,1, 3,0, 1,0,0,0, dpi,dpi>>8,0,0,// YResolution
0x1C,1, 3,0, 1,0,0,0, 1,0,0,0, // PlanarConfiguration=1 RGBRGB...
0x28,1, 3,0, 1,0,0,0, 2,0,0,0, // ResulutionUnit=inch
0,0,0,0 // Next IFD offset
};
f=fopen(filename,"wb+"); line=(uint16*)malloc(dbpl);
if (f && line) {
fwrite(hdr,1,hdr_size,f);
for(y=0;y<h;y++) {
s=(uint32*)( (char*)data + bpl*y );
for(x=0;x<w;x++) {
c=s[x];
v=c&1023; line[3*x ]=(v<<6)|(v>>4); c>>=10; // R
v=c&1023; line[3*x+1]=(v<<6)|(v>>4); c>>=10; // G
v=c&1023; line[3*x+2]=(v<<6)|(v>>4); // B
}
fwrite(line,1,dbpl,f);
}
res=0;
}
if (line) free(line);
if (f) fclose(f);
return res;
}
w — width, h — height, bpl — bytes per line
Для отладочного вывода в учебной программе самое то. :) Да и не только в учебной. Недавно писал обработку видеопотока, надо было найти место, где кадры бьются — отладочный вывод в ppm-ки быстро решил задачу!
А tiff да, красота, и для законченных систем наверняка лучше, но он и куда сложнее, как и все форматы-контейнеры
Автору спасибо за статью, прочитал с удовольствием.
Я тоже на волне интереса к rtx решил разобраться с этим делом. В моем варианте был ещё вывод на экран с аккумуляцией, опциональный рендеринг на видеокарте который давай на порядок большую скорость, глубина резкости, антиалиасинг, ааbb tree, kd tree, optix для rtx рендеринга. С материалами долго возился. Делать не сложно если делать всё по шагам. Знание и рендерер строится как пирамидка.
1) gamedev.ru/code/articles/vulkan_raytracing
2) gamedev.ru/code/articles/vulkan_raytracing_part2
без #include algorithm ругается на std::max, std::min.
С подключенными алгоритмами ругается на все остальное (vector, vec3f, framebuffer, ofs).
Using namespace std эффекта не возымела.
MSVS2017Community, подскажите как настроить или в чем искать проблему?
Следующий этап — path tracing :)
По-идее, не сильно сложнее, нужно только рандомно отскок луча делать и цикл по сэмплам.
А автор привел обратную трассировку(т.н. ray casting) когда считаем лучи от камеры к источнику света
Обратная трассировка лучей (она же рэйкастинг, raycasting) — простой, хотя и довольно медленный, метод получения высокореалистичных изображений. Этот метод часто путают с прямой трассировкой лучей (рэйтрэйсинг, raytracing), которая, на самом деле, практически никогда и никем не используется из-за своей редкостной неэффективности. Впрочем, эти два термина уже практически и не различают.
Вики тогда не было и мне почему-то показалось что прямая так называется т.к. моделирует свет как он идет на самом деле — от источника к глазу.Впрочем насколько я понял из современного описания рэйкастинг используется скорее для определения видимости.Из Вики:
Эта особенность делает невозможным точный рендеринг отражений, преломлений и естественной проекции теней с помощью рейкастинга.
Тогда не ясно что имели в виду авторы demo design faq.Буду благодарен если просветите.
Сейчас это выглядит так:
Здесь не используется полное вычисление картинки, т.к. это вероятно долго будет вычисляться на c#, а просто рисуется линиями и полигонами. Я так понимаю, что этим способом пересечения не нарисовать?
У меня есть отдельные списки прямоугольников для каждого уравнения поверхности. Минимальная задача пока — нарисовать пересекающиеся поверхности. Желательно ещё бы что-то почитать про перспективу и вращение сцены, т.к. я пока толком не пойму откуда взяты конкретные матричные операции.
en.smath.com/forum/resource.ashx?a=29878&b=2
«Please do not link directly to this resource. You must have a session in the forum.»
Не стоит открывать демку на слабой машине)
Если вдруг кто-то заинтересовался упомянутой библиотекой stb, вот репозиторий – nothings/stb. Обратите внимание на список подобных однофайловых библиотечек.
Я за повсеместное распространение однофайловых библиотек.
При всем уважении, это очень скользкая дорожка. Подключать то их может и удобно, но ведь иногда надо и их код читать.
Хинт насчет векторов: вместо реализации add/sub/… лучше реализовывать одноименные трейты, и получить бесплатно удобные арифметические операторы.
Спасибо за работу, Вам удалось пробудить то ощущение от магии цифр, которое было в студенчестве!
Тем временем народ запускает трассировщик на микроконтроллерах:
Извините, ссылка на гитхаб, искать по имени ESP32-Raytracer.
Сделайте, например, постобработку алгоритмом Флойда-Стейнберга. Он очень простой и при этом неплохо работает. Можно даже не постобработку, а сразу с рендерингом объединить.
Для реализации достаточно описания из Википедии, нужно лишь убедиться, что нет выхода за границы.
Получится примерно такое:
На 128*64 конечно не фонтан, но всё же:
noise можно генерить умножением A[n+1]=A[n]*B, noise=highbits(A[n]), A[0]=1, B mod 8=5
Самый простой и дурацкий способ
Не дурацкий, а вполне себе распространённый random dithering. :) Единственный недостаток, по сути, – менее точная передача контуров по сравнению с методами на диффузии ошибки. Но, при таких разрешениях, наверное, разницы и нет.
char *src_data=(char*)src->data, *dst_data=(char*)dst->data;
unsigned *s,*d,ng,ns=012345,ny=1016;
int x,y,r,g,b,c,cc,de0,de1,xsi,e=-16384;
for(y=0;y<h;y++) {
s=(unsigned*)src_data;
d=(unsigned*)dst_data;
ng=1+y*ny;
for(x=0;x<w;x++) {
r=s[x]&255; g=(s[x]>>8)&255; b=(s[x]>>16)&255;
//c=(5*r+9*g+2*b)>>4;
c=(3*r+12*g+b)>>4;
cc=c*c; de0=cc; de1=cc-255*255;
ng*=ns; xsi=(ng>>16)&32767;
if (abs(e+de0+xsi)<=abs(e+de1+xsi)) { e+=de0; d[x]=0; }
else { e+=de1; d[x]=0xFFFFFF; }
}
src_data+=src->bpl;
dst_data+=dst->bpl;
}
Просто случайный порог даёт более зашумлённое изображение.
Мало чего понял пока и хотел бы уточнить:
1. Можно ли подобным способом рисовать научную 3D графику, имея в виду не просто поверхности, но и: оси, сетку на поверхностях, кривые, 3D рамку вокруг? Хотелось бы, чтобы кривые (сетки и пр.) имели одну толщину на разном масштабе.
2. Где находится код, который управляет перспективой и как бы попроще двигать камеру, масштабировать?
3. В принципе, если картинка небольшая, то меня скорость устраивает для c#. Особая красота не нужна (отражения, преломления). Как ещё можно оптимизировать код? Кроме нескольких потоков, как у товарища выше.
Вопросы эти возникают из желания создать компонент для рисования сцены из множества поверхностей, кривых и прочей научной тематики (как в Mathcad, к примеру).
Уточку тоже попробую добавить. Она как раз в тему отображения поверхностей, заданных неявными уравнениями (марширующие кубы).
2. Про камеру у меня чуть подробнее расписано вот тут: github.com/ssloy/tinyraytracer/wiki
Вкратце, камера сидит в начале координат и смотрит в направлении -z. Возьмите любую другую точку и любое другое направление, будет работать. Вот, например, плавающая камера, выполненная для shadertoy:
www.shadertoy.com/view/tsjGRW
3. Думаю, что для ваших целей лучше брать треугольники и рисовать их напрямую без рейтрейсинга. Всё необходимое расписано вот тут:
github.com/ssloy/tinyrenderer/wiki
В самом начале статьи была ссылка на русскую версию.
Либо я рисую кривую, а она пересекает треугольник (выходит из него), как это в общем случае рисовать?
Поверхности могут быть любые и комбинации их друг с другом — любые. Например, бутылка Клейна.
или так:
Поверхность построена методом «марширующих кубов», т.е. разбита на треугольники.
diff --git a/tinyraytracer.cpp b/tinyraytracer.cpp
index b581274..eea15aa 100644
--- a/tinyraytracer.cpp
+++ b/tinyraytracer.cpp
@@ -97,7 +97,9 @@ Vec3f cast_ray(const Vec3f &orig, const Vec3f &dir, const std::vector<Sphere> &s
Material material;
if (depth>4 || !scene_intersect(orig, dir, spheres, point, N, material)) {
- return Vec3f(0.2, 0.7, 0.8); // background color
+ int a = std::max(0, std::min(envmap_width -1, static_cast<int>((atan2(dir.z, dir.x)/(2*M_PI) + .5)*envmap_width)));
+ int b = std::max(0, std::min(envmap_height-1, static_cast<int>(acos(dir.y)/M_PI*envmap_height)));
+ return envmap[a+b*envmap_width]; // background color
}
Vec3f reflect_dir = reflect(dir, N).normalize();
@@ -125,10 +127,13 @@ Vec3f cast_ray(const Vec3f &orig, const Vec3f &dir, const std::vector<Sphere> &s
}
void render(const std::vector<Sphere> &spheres, const std::vector<Light> &lights) {
- const int width = 1024;
+ const float eyesep = 0.2;
+ const int delta = 60; // focal distance 3
+ const int width = 1024+delta;
const int height = 768;
const float fov = M_PI/3.;
- std::vector<Vec3f> framebuffer(width*height);
+ std::vector<Vec3f> framebuffer1(width*height);
+ std::vector<Vec3f> framebuffer2(width*height);
#pragma omp parallel for
for (size_t j = 0; j<height; j++) { // actual rendering loop
@@ -136,20 +141,30 @@ void render(const std::vector<Sphere> &spheres, const std::vector<Light> &lights
float dir_x = (i + 0.5) - width/2.;
float dir_y = -(j + 0.5) + height/2.; // this flips the image at the same time
float dir_z = -height/(2.*tan(fov/2.));
- framebuffer[i+j*width] = cast_ray(Vec3f(0,0,0), Vec3f(dir_x, dir_y, dir_z).normalize(), spheres, lights);
+ framebuffer1[i+j*width] = cast_ray(Vec3f(-eyesep/2,0,0), Vec3f(dir_x, dir_y, dir_z).normalize(), spheres, lights);
+ framebuffer2[i+j*width] = cast_ray(Vec3f(+eyesep/2,0,0), Vec3f(dir_x, dir_y, dir_z).normalize(), spheres, lights);
}
}
- std::vector<unsigned char> pixmap(width*height*3);
- for (size_t i = 0; i < height*width; ++i) {
- Vec3f &c = framebuffer[i];
- float max = std::max(c[0], std::max(c[1], c[2]));
- if (max>1) c = c*(1./max);
- for (size_t j = 0; j<3; j++) {
- pixmap[i*3+j] = (unsigned char)(255 * std::max(0.f, std::min(1.f, framebuffer[i][j])));
+ std::vector<unsigned char> pixmap((width-delta)*height*3);
+ for (size_t j = 0; j<height; j++) {
+ for (size_t i = 0; i<width-delta; i++) {
+ Vec3f c1 = framebuffer1[i+delta+j*width];
+ Vec3f c2 = framebuffer2[i+ j*width];
+
+ float max1 = std::max(c1[0], std::max(c1[1], c1[2]));
+ if (max1>1) c1 = c1*(1./max1);
+ float max2 = std::max(c2[0], std::max(c2[1], c2[2]));
+ if (max2>1) c2 = c2*(1./max2);
+ float avg1 = (c1.x+c1.y+c1.z)/3.;
+ float avg2 = (c2.x+c2.y+c2.z)/3.;
+
+ pixmap[(j*(width-delta) + i)*3 ] = 255*avg1;
+ pixmap[(j*(width-delta) + i)*3+1] = 0;
+ pixmap[(j*(width-delta) + i)*3+2] = 255*avg2;
}
}
- stbi_write_jpg("out.jpg", width, height, 3, pixmap.data(), 100);
+ stbi_write_jpg("out.jpg", width-delta, height, 3, pixmap.data(), 100);
}
int main() {
PS кстати забавно что треугольники модели сразу в мировом пространстве.
Вот полное решение домашки (патч к вот этому коммиту):
80a81,94
> float duck_dist = std::numeric_limits<float>::max();
> for (int t=0; t<duck.nfaces(); t++) {
> float dist;
> if (duck.ray_triangle_intersect(t, orig, dir, dist) && dist<duck_dist && dist<spheres_dist) {
> duck_dist = dist;
> hit = orig + dir*dist;
> Vec3f v0 = duck.point(duck.vert(t, 0));
> Vec3f v1 = duck.point(duck.vert(t, 1));
> Vec3f v2 = duck.point(duck.vert(t, 2));
> N = cross(v1-v0, v2-v0).normalize();
> material = Material(1.5, Vec4f(0.3, 1.5, 0.2, 0.5), Vec3f(.24, .21, .09), 125.);
> }
> }
>
85c99
< if (d>0 && fabs(pt.x)<10 && pt.z<-10 && pt.z>-30 && d<spheres_dist) {
---
> if (d>0 && fabs(pt.x)<10 && pt.z<-10 && pt.z>-30 && d<spheres_dist && d<duck_dist) {
92c106
< return std::min(spheres_dist, checkerboard_dist)<1000;
---
> return std::min(duck_dist, std::min(spheres_dist, checkerboard_dist))<1000;
100c114,120
< return Vec3f(0.2, 0.7, 0.8); // background color
---
> Sphere env(Vec3f(0,0,0), 100, Material());
> float dist = 0;
> env.ray_intersect(orig, dir, dist);
> Vec3f p = orig+dir*dist;
> int a = (atan2(p.z, p.x)/(2*M_PI) + .5)*envmap_width;
> int b = acos(p.y/100)/M_PI*envmap_height;
> return envmap[a+b*envmap_width];//Vec3f(0.2, 0.7, 0.8); // background color
Ещё её называют Skymap.
Вот здесь показано соотношение сферической равноугольной проекции и Меркатора:
paulbourke.net/geometry/transformationprojection
Вы о equirectangular projection? Только она не равноугольная, а равнопромежуточная. А что именно не получается сделать? Это вроде бы простая проекция. Надо переделать рендер, чтобы испускал лучи во все стороны, меняя в цикле два угла («прицел» и «доворот»). Потом откладываем эти углы по вертикали и горизонтали на изображении и пишем в соответствующую точку значение. Как-то так.
Да, наоборот. Картинка-то первична. :)
Ну, тут тоже не особо сложно. Если размер W⨉H, то для точки (j, i) будут углы 2πj/W (горизонталь) и π(i — H/2)/H (вертикаль). Переводим из полярных координат в декартовы и получаем единичный вектор направления луча dir в функции render. Остальное всё то же самое.
Единственное, надо обработать отдельно случаи, когда вертикальный угол близок к π/2, чтоб не было бесконечностей.
Я бы код показал, но с телефона не очень удобно писать его.
Если не забуду, попробую написать, как буду дома.
Да, и я ошибся. Отдельно вертикальный случай обрабатывать не надо. Так как мы вектор задаём, никаких неоднозначностей не будет.
В render
нужно использовать такой цикл:
#pragma omp parallel for
for (size_t j = 0; j<height; j++) { // actual rendering loop
for (size_t i = 0; i<width; i++) {
float a = 2.0 * M_PI * i / (float)width;
float b = M_PI * (j - height / 2.0) / (float)height;
float dir_x = cos(b) * cos(a);
float dir_y = -sin(b);
float dir_z = cos(b) * sin(a);
framebuffer[i+j*width] = cast_ray(Vec3f(0,0,0), Vec3f(dir_x, dir_y, dir_z), spheres, lights);
}
}
Если подвинуть сферы ближе по оси Z, чтоб был заметен эффект, получается такое изображение:
Всё работает как надо? А то не очень понятно по картинке. :)
local function getRayDirection(x, y) return norm(x / 256 - 1, (512 - y) / 256 - 1, 90 / fov) end
local function getTestPoint(t, rx, ry, rz) return ex + rx * t, ey + ry * t, ez + rz * t end
local function trace(x,y)
local rx, ry, rz = getRayDirection(x, y) -- normal cam pos
-- rz = -rz -- turn camera at back
-- rx = -rx -- flip camera to current back view
-- local ry, rx, rz = getRayDirection(x, y) -- rotate cam clockwise 90 deg
-- local rx, rz, ry = getRayDirection(x, y) -- look at up
-- turn camera to top/buttom
-- local rx, rz, ry = getRayDirection(x, y) -- look at up
-- rz = -rz -- flip camera to correct look up
-- ry = -ry -- invert look at up - look down
-- turn camera to right/left
-- local rz, ry, rx = getRayDirection(x, y) -- look at right/left
-- rz = -rz -- flip to correct view at left
-- rx = -rx -- flip to correct view at left
-- turn camera to right/left
local tx, ty, tz = 0, 0, 0
local t, distance, color = 0, 0, 0
for i = 1, maxSteps do
tx, ty, tz = getTestPoint(t, rx, ry, rz)
distance, color = scene(tx, ty, tz)
--the test point is close enough, render
if distance < .05 then return render(x, y, t, tx, ty, tz, rx, ry, rz, color) end
--the test point is too far, give up, draw the sky
if distance >= tmax then break end
--move forward by some fraction
t = t + distance * .7
end
return sky(x, y, rx, ry, rz)
end
Читая ее я задумался про specular часть в модели Фонга. Мне кажется вот этот узкий угловой разброс вдоль направления отраженного луча моделирует тот факт, что в основном все источники света имеют какой-то угловой размер. Даже вот у солнца он не такой уж и маленький: 0.01 радиан.
Мне интересно, все таки какой эффект тут играет главную роль в формировании размера отблеска, уголовой размер источника или все же лучи отражаются не математически строго вдоль зеркального направления, а в каком-то узком конусе?
Без утки всё идентично. Утка почему-то чуть подругому рендерится. Может что с нормалями накосячил…
Кстати пересечение с треугольниками в вашей версии одностороннее и лучи только на входе преломляются.
Если взять двустороннее пересечение, то у меня так получается:
Хочу, как руки дойдут, прикрутить зачитывание и интерполяцию нормалей — интересно, на сколько реалистично будет утка выглядеть.
P.S. код — github.com/splav/rust-tinyraytracer
256 строчек голого C++: пишем трассировщик лучей с нуля за несколько часов