Как стать автором
Обновить

256 строчек голого C++: пишем трассировщик лучей с нуля за несколько часов

Время на прочтение 8 мин
Количество просмотров 142K
Всего голосов 241: ↑241 и ↓0 +241
Комментарии 124

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

Оче круто! Спасибо!
Было бы еще круче, если бы трассировало в compile time на шаблонах!
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 на третьем курсе учат (условно).
Извините за оффтопик, но раз уж заговорили. Вообще, я заметил по другим публикациям, что для вас характерна «чрезмерная» скромность. Вы пишете, что не специалист и сами плохо в чём-то разбираетесь, а на самом деле разбираетесь достаточно хорошо. Что это всё легко понять студентам обычного провинциального вуза, но вуз не такой уж и обычный, а вполне себе хороший. Что какую-то тему вы прошли ещё в школе, но при этом оказывается, что это специализированная физмат-школа (извините, если ошибаюсь, я мог и перепутать).
Наверняка у вас благие намерения. Вы хотите мотивировать других людей, показать им, что не боги горшки обжигают. Но если материал для кого-то сложный, то слова о том, что он простой и школьного уровня, могут лишь заставить человека подумать: «Ну вот, школьники понимают, а я не понимаю. Видать, я недотёпа». Самооценка падает, ничего делать не хочется.
Так получилось, что я довольно долго проработал в вузе и параллельно вёл кружок в местном лицее. Вуз маленький, конкурсы небольшие, разброс в подготовке студентов очень большой. А в лицее на физмате много талантливых ребят. И иногда так получалось, что я одну и ту же тему рассказывал восьмиклассникам в кружке и студентам (например, линейные корректирующие коды). И бывало, что восьмиклассники схватывали быстрее. Но если бы я сказал студентам, что это школьная тема – что недалеко от истины, в общем-то, – вряд ли бы это подняло их мотивацию. Это просто звучало бы обидно.
Это всё вовсе не критика, у вас замечательные статьи, с удовольствием их читаю. Просто наблюдение. Вы, безусловно, возразите мне, что это статьи, которые может понять школьник, а не обязан их понять, и что не стоит «читать их за чашкой чая». И будете правы. Но я не о формальной стороне, а о коннотации. Просто учитывайте, что аудитория на хабре с более широким разбросом уровня подготовки.

Спасибо за развёрнутый ответ. Лично мне, на самом деле, абсолютно всё равно, какой уровень подготовки у моего собеседника. Мне это неинтересно. Мне интересен огонь в глазах, и, конечно, я всячески пытаюсь его разжечь. С огнём в глазах любые пробелы в подготовке преодолеваются без особых трудностей.

Что касается хабра, то, конечно, я вынужден признать, что в силу критического недостатка времени я тут не всегда пишу самодостаточные тексты. Обычно я пишу либо для себя самого, когда разбираюсь в новой теме, либо (как конкретно эта статья, обратите внимание, что я уже опубликовал её перевод) в качестве записей для своих студентов, но, в отличие от хабра, они дополнительно ещё имеют цирк со мной у доски.

А что абсолютно всё, о чём я пишу, это просто — так ведь это правда. Если я не сумел написать доступно — ну я так себе лектор. В комментариях ведь можно задавать вопросы, правда? Достаточно иметь огонь в глазах :) Вопреки почему-то распространённому мнению, на хабре совсем не только дерьмом поливают, но и вдумчиво отвечают на вопросы.

Для примера напишу про свой — Волгоградский Государственный Технический Университет, опорный ВУЗ, между прочим :)


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

Пример интересный, но не могли бы вы сказать, какую именно мысль он иллюстрирует? Я запутался слегка.
Просто ответил masai к обсуждению провинциальности университетов. И соглашаюсь с ним, что Université de Lorraine несколько не совсем провинциальный.
У нас один из провинциальных ВУЗов вошёл в десятку лучших, и есть «крутые» столичные ВУЗы, которые туда не вошли.
На самом делье это вполне реально, как действительно заметил haqreu доступно даже студентам. Я откопал скрин своего студенческого (в рамках курса www.edx.org/course/computer-graphics от небезыизвестного Ravi Ramamoorthi) рейтрейсера. Тут нет отражений, так как они не очень симпатичные у меня получились почему-то, но общую картину о рейтресинге дает.
Картинка 640x480
image

А код остался где-нибудь?

Думаю, если поискать, и его удастся найти. А какая цель? Посмотреть на возможную ошибку в расчете отражений? На С#, кстати.

Профдеформация. Люблю смотреть на чужой код :)
Ну могу показать, что я писал на втором курсе, хотя сейчас конечно страшно стыдно за такой код. Но, специалист думаю сможет указать, где я накосячил. Судя по результатам, что я хотел фонга в качестве четвертого вида отрисовки, а получил какую-то дичь, это не так уж и просто. Линк.

Кода намного больше, а делает он намного меньше.

Единственное, чем я горжусь, то что все фигуры строятся из одного «Лепестка», который потом поворачивается, отражается и переносится. Немного посидел с листочком с расчетами, и вместо задания кучи точек смог обойтись четыремя.
Спасибо за ссылку! Положите, пожалуйста, картинку в репозиторий, а то без компилятора трудно понять, каков результат.
Картинку он не рисует (отображает сразу на форму), но выглядит это примерно так:
image
Ага, спасибо! Ну картинку или скриншот — всё равно, главное показать людям, что пришли в репозиторий, что их ждёт.
Мы в школе делали рт (обратный, правда), даже остался, вроде даже компилировался когда-то
картинка
image

Ого как. Обычный рейтрейсинг — это уже гении?
Лично я при изучении нового языка программирования практически всегда пишу рейтрейсер, т.к. только так я могу погрузиться в новый язык с интересом и пользой.
По-вашему, я гений в n-й степени, где n-количество языков, которые я изучил?
Спасибо.
На самом деле, первые люди, которые проходили по этому пути, вполне себе гении. Да и сейчас написать эффективный рейтрейсер очень нетривиальная задача. А так, уже идя проторенной дорогой, не гонясь за скоростью (и робастностью!) вычислений — это да, школьный уровень.
Верно
Я во время своей учебы в вузе (конец 90х) покупал книжку с таким же движком. Было красиво, но долго )
Мы на борланд-BGI такое рисовали в ВУЗе. Да, было медленно, но интересно.
Еще можно сохранять не в ppm, а в tiff. Он поддерживается почти всеми, в отличии от ppm. Минимальный код примерно такой:
tiff-rgb8
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битного цвета, для пущей красочности.
tiff-rgb10
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

По-моему, проще подключить stb_image_write.h и писать сразу .jpg. Я пишу ppm только потому, что это кратчайший способ сохранить картинку без привлечения стороннего кода.
Плюс ppm в том, что его ВООБЩЕ не надо поддерживать. Тупо в цикле вывод массива пикселей в файл, перед которым маленький текстовый заголовок. При этом его просмотр поддерживают вполне реальные популярные смотрелки, например, IrfanView.

Для отладочного вывода в учебной программе самое то. :) Да и не только в учебной. Недавно писал обработку видеопотока, надо было найти место, где кадры бьются — отладочный вывод в ppm-ки быстро решил задачу!

А tiff да, красота, и для законченных систем наверняка лучше, но он и куда сложнее, как и все форматы-контейнеры

Автору спасибо за статью, прочитал с удовольствием.

Я тоже на волне интереса к rtx решил разобраться с этим делом. В моем варианте был ещё вывод на экран с аккумуляцией, опциональный рендеринг на видеокарте который давай на порядок большую скорость, глубина резкости, антиалиасинг, ааbb tree, kd tree, optix для rtx рендеринга. С материалами долго возился. Делать не сложно если делать всё по шагам. Знание и рендерер строится как пирамидка.

Если захотите (ну а вдруг) разобраться с RTX напрямую, без посредников в лице OptiX, скромно порекомендую свои статьи:

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 :)
По-идее, не сильно сложнее, нужно только рандомно отскок луча делать и цикл по сэмплам.

Ну а потом bidirectional path tracing :)
Такие люди как Вы вызывают не только интерес к компьютерной графике, но и желание всё «покрутить» самому. Спасибо огромное за статью, читал одновременно с интересом и удивлением насколько всё просто (в плане реализации). Будет интересно сегодня вечером попробовать соорудить что-нибудь простенькое.
Тут есть некоторая путаница в терминологии.Прямая трассировка — считаем лучи от источника в надежде что он попадет в экран(почти не используется).
А автор привел обратную трассировку(т.н. ray casting) когда считаем лучи от камеры к источнику света
Мне кажется, что вы путаете ray casting и ray tracing. Оба работают от камеры к источнику, но в данном случае у меня именно ray tracing.
Да, похоже.В свое время писал на паскале рэйтрейсер под впечатлением этого мануала
Обратная трассировка лучей (она же рэйкастинг, raycasting) — простой, хотя и довольно медленный, метод получения высокореалистичных изображений. Этот метод часто путают с прямой трассировкой лучей (рэйтрэйсинг, raytracing), которая, на самом деле, практически никогда и никем не используется из-за своей редкостной неэффективности. Впрочем, эти два термина уже практически и не различают.

Вики тогда не было и мне почему-то показалось что прямая так называется т.к. моделирует свет как он идет на самом деле — от источника к глазу.Впрочем насколько я понял из современного описания рэйкастинг используется скорее для определения видимости.Из Вики:
Эта особенность делает невозможным точный рендеринг отражений, преломлений и естественной проекции теней с помощью рейкастинга.

Тогда не ясно что имели в виду авторы demo design faq.Буду благодарен если просветите.
На самом деле, разделение этих терминов произошло относительно недавно, раньше они были взаимозаменяемыми. Ray casting находит первое пересечение и сразу же определяет конечный цвет пикселя, в то время как ray tracing идёт дальше, позволяя рендерить отражения и тому подобное.

иллюстрация

25 лет назад с таким же восторгом читал четырехтомник Аммерала по графике.
Код в статье надо подправить чтобы на MSVC работал. Во-первых добавить #include , во вторых открывать выводной поток в режиме std::ios::binary
С удовольствием. Присылайте пулл-реквест, у меня нет под рукой msvc, чтобы проверить.

Или отключить в настройках проекта предкомпилированные заголовки.

А ray marching и фракталы будут?
А я не знаю, почему бы и нет. Времени только мало…
Мы подождем =) Главное пишите
Не могли бы вы подсказать исходники, которые могли бы отображать пересечение поверхностей? Желательно также с нуля. Давно ищу что-то несложное, чтобы добавить компонент рисования 3D графики в один математический пакет.

Сейчас это выглядит так:
image

Здесь не используется полное вычисление картинки, т.к. это вероятно долго будет вычисляться на c#, а просто рисуется линиями и полигонами. Я так понимаю, что этим способом пересечения не нарисовать?

У меня есть отдельные списки прямоугольников для каждого уравнения поверхности. Минимальная задача пока — нарисовать пересекающиеся поверхности. Желательно ещё бы что-то почитать про перспективу и вращение сцены, т.к. я пока толком не пойму откуда взяты конкретные матричные операции.
Вспомнилось как было похожее на JS в 2017 js1k.com/2017-magic/details/2648
Не стоит открывать демку на слабой машине)
Не стоит вскрывать эту тему...
Открывал статью с опаской встретить чего ни будь сложно воспринимаемое после рабочего дня, наполненного тестами и отладкой. Однако, тема оказалась изложена крайне просто и понятно. Спасибо за материал!

Если вдруг кто-то заинтересовался упомянутой библиотекой stb, вот репозиторий – nothings/stb. Обратите внимание на список подобных однофайловых библиотечек.

Да! Это stb — это прекрасная билиотека, всё отдано в public domain. И, помимо отличной лицензии, автор сделал очень сильный упор на простоту подключения модулей. Я за повсеместное распространение однофайловых библиотек.
Я за повсеместное распространение однофайловых библиотек.

При всем уважении, это очень скользкая дорожка. Подключать то их может и удобно, но ведь иногда надо и их код читать.
А если в два файла, то не надо? ;)
А если библиотека хорошо разбита на файлы, то как правило ее удобней читать.
Разве нормальная среда разработки не делает чтение одинаково удобным в обоих случаях?
Не думаю что есть IDE которая сделает чтение условного Eigen, слитого в один файл, таким же удобным, как чтение Eigen разбитого по файлам.
Ого, быстро у вас получилось. Очень рекомендую выкладывать скриншоты в репозиторий. У меня, например, компилятора rust под рукой нет :(
Ну так, раст это не такая сложная штука, как люди думают. В репозитории ровно: 0 лайфтаймов и 0 unsafe.
Ну зачем же вы форкались :) Из-за этого, например, нельзя делать поиск по репозиторию, пришлось клонировать, чтобы посчитать все unsafe'ы.

Хинт насчет векторов: вместо реализации add/sub/… лучше реализовывать одноименные трейты, и получить бесплатно удобные арифметические операторы.
Я сначала попробовал это сделать, но не понял, как быть в случае, когда структура передаётся по референсу. Получается нужно заимплементить трейты дважды для Vec3f и &Vec3f?
Тоже пытаюсь немного разбираться в Rust. Вот моя реализация, но почему-то цвета отражений получаются кислотными github.com/Fliskr/r-tinyraytracer. Может кто-нибудь подсказать в чём может быть проблема?
Я вообще не разбираюсь в расте, но по картинке вижу, что у вас произошло переполнение, цвет стал ярче 255.
Да, Вы правы. Забыл в рендере взять минимальное между 1 и значением вектора. Спасибо!
Прочитал материал с чашкой чая в руке :) Придется вернуться еще раз.
Спасибо за работу, Вам удалось пробудить то ощущение от магии цифр, которое было в студенчестве!
Жаль, что у меня ни на первом, ни на пятом курсе профильного ВУЗа ничего подобного не было :)

Тем временем народ запускает трассировщик на микроконтроллерах:

Извините, ссылка на гитхаб, искать по имени ESP32-Raytracer.

ikaktys запустил на ESP + OLED, изображение бинарное, просто сравнение цвета с >128.


Полторы секунды на кадр 64 x 128.
добавь dithering будет шикарно.
Да, я думаю, как это проще всего сделать. Есть идеи?

Сделайте, например, постобработку алгоритмом Флойда-Стейнберга. Он очень простой и при этом неплохо работает. Можно даже не постобработку, а сразу с рендерингом объединить.


Для реализации достаточно описания из Википедии, нужно лишь убедиться, что нет выхода за границы.



Самый простой и дурацкий способ, добавить к порогу случайный шум.
Получится примерно такое:
gray^2 > noise ? white:black
image
На 128*64 конечно не фонтан, но всё же:
image
noise можно генерить умножением A[n+1]=A[n]*B, noise=highbits(A[n]), A[0]=1, B mod 8=5
О какая красота, спасибо!
Самый простой и дурацкий способ

Не дурацкий, а вполне себе распространённый random dithering. :) Единственный недостаток, по сути, – менее точная передача контуров по сравнению с методами на диффузии ошибки. Но, при таких разрешениях, наверное, разницы и нет.

Я немного напутал. Тут вариант построчного распространения ошибки с шумным порогом:
fragment
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;
}

image

Просто случайный порог даёт более зашумлённое изображение.
В общем, то же, но для c#: tinyraytracer in c#

image

Мало чего понял пока и хотел бы уточнить:

1. Можно ли подобным способом рисовать научную 3D графику, имея в виду не просто поверхности, но и: оси, сетку на поверхностях, кривые, 3D рамку вокруг? Хотелось бы, чтобы кривые (сетки и пр.) имели одну толщину на разном масштабе.

2. Где находится код, который управляет перспективой и как бы попроще двигать камеру, масштабировать?

3. В принципе, если картинка небольшая, то меня скорость устраивает для c#. Особая красота не нужна (отражения, преломления). Как ещё можно оптимизировать код? Кроме нескольких потоков, как у товарища выше.

Вопросы эти возникают из желания создать компонент для рисования сцены из множества поверхностей, кривых и прочей научной тематики (как в Mathcad, к примеру).
Уточку тоже попробую добавить. Она как раз в тему отображения поверхностей, заданных неявными уравнениями (марширующие кубы).
1. Можно, но не уверен, что нужно. Вы поверхности каким образом задавать хотите? Ведь нам нужна функция пересечения луча с объектом. Если разбивать поверхность на треугольники, то проще их проецировать на экран напрямую, особенно если не нужны преломления и проч.

2. Про камеру у меня чуть подробнее расписано вот тут: github.com/ssloy/tinyraytracer/wiki
Вкратце, камера сидит в начале координат и смотрит в направлении -z. Возьмите любую другую точку и любое другое направление, будет работать. Вот, например, плавающая камера, выполненная для shadertoy:
www.shadertoy.com/view/tsjGRW

3. Думаю, что для ваших целей лучше брать треугольники и рисовать их напрямую без рейтрейсинга. Всё необходимое расписано вот тут:
github.com/ssloy/tinyrenderer/wiki

В самом начале статьи была ссылка на русскую версию.
Так вот проблема вроде бы с проецированием. Будем считать, что поверхность разбита на треугольники. Есть два треугольника, они пересекаются (или 10 штук и все пересекаются). Как отдельным проецированием каждого из них отобразить это вот пересечение? Я почитаю по ссылкам, может про это там и написано.
Либо я рисую кривую, а она пересекает треугольник (выходит из него), как это в общем случае рисовать?
Поверхности могут быть любые и комбинации их друг с другом — любые. Например, бутылка Клейна.
Например, вот так:



или так:



Поверхность построена методом «марширующих кубов», т.е. разбита на треугольники.
Никаких проблем, первых трёх статей моего курса по компьютерной графике вполне хватит для отрисовки таких картинок (ключевое слово z-buffer).
По поводу пункта 3: вынесите часть математики за цикл, думаю, чуть-чуть поможет.
Да, конечно же нужно добавлять стереопары! Сделайте себе красно-синие очки из подручных материалов:


Скрытый текст
Патч к вот этому коммиту
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
Стал разбираться с листочком, с первых коммитов, угол обзора наверное float все таки?
Да, конечно. В истории коммитов я баги фиксить не хочу, поэтому имеет смысл смотреть на самый свежий коммит.
В начале третьего этапа как минимум одна из сфер отобразилась в эллипс, а не в круг. Так вот, в принципе всё правильно. Я подумал и понял, да, действительно, при центральной проекции сферы могут отображаться в эллипсы. Но всё равно картинка кажется неестественной. Не знаете, почему? Может быть, дело в том, что центральная проекция не самая лучшая? Может быть, потому что сетчатка глаза не плоская, а потому обычная центральная проекция на воображаемую плоскую поверхность — не самый лучший вариант?
Центральная проекция хорошо описывает происходящее в глазу; единственное, что у меня камера довольно близко стоит к сферам, приводя к таким сильным искажениям. В реальной жизни немного не так обычно.
А подскажите кто-нибудь код для камеры, чтоб рендерила из сцены сразу равноугольную панораму. Я половину интернетов уже перерыл, осталось только китайский выучить.
А что такое равноугольная панорама?
Мне по картинке неясно, вам Меркатор нужен?
Нет, не Меркатор. Сферическая равноугольная панорама, которая используется для виртуальных туров, игр-мистоидов и прочего такого. dmswart.com/2016/06/28/drawing-a-panorama
image
Ещё её называют 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, чтоб был заметен эффект, получается такое изображение:


Ух, шикарно! Спасибо огромное!
Отрендерил сцену в панораму, сунул панораму в плеер:
Картинки
image
image

Всё работает как надо? А то не очень понятно по картинке. :)

Да, всё отлично. Спасибо. Стало гораздо понятнее, как сделать остальные типы камер — цилиндрическую, кубическую и рыбий глаз.
Сделать рендер в кубическую панораму оказалось неожиданно просто. Просто поменял порядок осей в описании камеры и она повернулась.
Код
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 радиан.
Мне интересно, все таки какой эффект тут играет главную роль в формировании размера отблеска, уголовой размер источника или все же лучи отражаются не математически строго вдоль зеркального направления, а в каком-то узком конусе?
В том виде, как есть, specular компонента моделирует это отражение лучей, попадающее в некий конус вокруг идеального отражения. Впрочем, угловой размер источника тоже можно туда добавить.
Нет, тут источники света точечные, степень в specular компоненте материала соответствует «шероховатости» поверхности — разбросу углов отражения (и соответственно размеру бликов). Отражение точечного источника в идеальном зеркале без шероховатости, будет выглядеть как один яркий пиксель, это если повезёт и отражение попадёт ровно в источник (точность флоата конечная, так что вероятность не совсем нулевая)
Сделал на досуге реализацию на rust. Только цели делать без зависимостей не ставил: image для работы с картинками, rayon для распараллеливания, nalgebra — вектора. Сначала сделал свои вектора, потом решил не тащить веловипед, благо зависимости подключаются тривиально.

Без утки всё идентично. Утка почему-то чуть подругому рендерится. Может что с нормалями накосячил…
image
Кстати пересечение с треугольниками в вашей версии одностороннее и лучи только на входе преломляются.
Если взять двустороннее пересечение, то у меня так получается:
image

Хочу, как руки дойдут, прикрутить зачитывание и интерполяцию нормалей — интересно, на сколько реалистично будет утка выглядеть.

P.S. код — github.com/splav/rust-tinyraytracer
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории