C++
Mathematics
Programming
Working with 3D-graphics
Game development
Comments 124
-8
Было бы еще круче, если бы трассировало в compile time на шаблонах!
0
Generating the above 512x512 image at compile time took around 45 minutes with Clang 4.0 on my Macbook Pro.


Огонь! Интересно что он дальше пишет что в рантайме эта же задача выполняется за пол-секунды, т.е. компилятор не очень хорошо трассирует лучи. Вариант по ссылке в основном основан на constexpr (т.е. код вполне понимаемый), но еще дальше он пишет что вариант трассировки лучей написанный чисто на шаблонах работает еще медленней. Интересно, никогда не думал о таком аспекте constexpr как ускорение компиляции.
0
Моя основная задача — показать проекты, которые интересно (и легко!) программировать

Да что бы такое програмировать это нужно быть гением.
+22
Совсем нет, или студенты нашего провинциального университета поголовно гении. Достаточно немного усидчивости и последовательности. Конечно, хорошие методички лишними не бывают.
+2
Полагаю, имелось в виду, «чтобы программировать такое ЛЕГКО — надо или хорошо набить руку на этом, или быть гением, чтобы делать такое сходу с нуля.»
+3
студенты нашего провинциального университета

Судя по почте на github, это Université de Lorraine. Ну, не такой он провинциальный. 201-300 место в ARWU2018. Тот же МФТИ – 410-500 место. Студенты, я подозреваю, тоже не самые простые.

0
Рекомендую посмотреть, как именно составляются рейтинги вузов. Тот факт, что университет Лотарингии впереди, например, СПбГУ по рейтингу, совершенно не обозначает того, что там лучше студенты. Будучи причастным к обоим, могу сравнивать.

Ещё один простой пример: École Polytechnique, школа с сильнейшими французским студентами, находится на 401-500 в этом же самом рейтинге. Неужто она хуже провинциального университета (я по-прежнему про Université de Lorraine), который на 201-300? Нет, она меньше. Бюджет важен для рейтинга. Кстати, следите за руками. Университет Лотарингии был создан в 2012м году путём слияния шести (!) различных вузов, близких друг к другу географически. Для чего это было сделано? Подсказка: см. слова «бюджет», «количество студентов».
+1
Про слияние мелких вузов в крупный, чтобы был понятен масштаб, я просто оставлю вот здесь картинку расположения университетских кампусов:
Скрытый текст


С юга на север больше двухсот километров между кампусами.
+2

Я совсем не об этом. Не знаю, как у других, но у меня слова «провинциальный вуз» ассоциируются с вузом, который вообще ни в какие рейтинги не входит, и в котором студентам-программистам на паре по компьютерной графике показывают пиратский Corel Draw, а сами студенты если и разбираются в программировании, то не благодаря вузу, а вопреки. К сожалению, такие вузы существуют. Я просто как-то смотрел учебные планы и рабочие программы небольших региональных вузов, и там всё не так хорошо, как хотелось бы. Может, я ошибаюсь, и таких вузов почти и не осталось, в которых ООП на TurboPascal на третьем курсе учат (условно).
Извините за оффтопик, но раз уж заговорили. Вообще, я заметил по другим публикациям, что для вас характерна «чрезмерная» скромность. Вы пишете, что не специалист и сами плохо в чём-то разбираетесь, а на самом деле разбираетесь достаточно хорошо. Что это всё легко понять студентам обычного провинциального вуза, но вуз не такой уж и обычный, а вполне себе хороший. Что какую-то тему вы прошли ещё в школе, но при этом оказывается, что это специализированная физмат-школа (извините, если ошибаюсь, я мог и перепутать).
Наверняка у вас благие намерения. Вы хотите мотивировать других людей, показать им, что не боги горшки обжигают. Но если материал для кого-то сложный, то слова о том, что он простой и школьного уровня, могут лишь заставить человека подумать: «Ну вот, школьники понимают, а я не понимаю. Видать, я недотёпа». Самооценка падает, ничего делать не хочется.
Так получилось, что я довольно долго проработал в вузе и параллельно вёл кружок в местном лицее. Вуз маленький, конкурсы небольшие, разброс в подготовке студентов очень большой. А в лицее на физмате много талантливых ребят. И иногда так получалось, что я одну и ту же тему рассказывал восьмиклассникам в кружке и студентам (например, линейные корректирующие коды). И бывало, что восьмиклассники схватывали быстрее. Но если бы я сказал студентам, что это школьная тема – что недалеко от истины, в общем-то, – вряд ли бы это подняло их мотивацию. Это просто звучало бы обидно.
Это всё вовсе не критика, у вас замечательные статьи, с удовольствием их читаю. Просто наблюдение. Вы, безусловно, возразите мне, что это статьи, которые может понять школьник, а не обязан их понять, и что не стоит «читать их за чашкой чая». И будете правы. Но я не о формальной стороне, а о коннотации. Просто учитывайте, что аудитория на хабре с более широким разбросом уровня подготовки.

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

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

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

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


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

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

0

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

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

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

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

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

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

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

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

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

0
Я тут проходил мимо, но решил попробовать, с плюсами знаком плохо и вопрос закономерный:
без #include algorithm ругается на std::max, std::min.
С подключенными алгоритмами ругается на все остальное (vector, vec3f, framebuffer, ofs).
Using namespace std эффекта не возымела.
MSVS2017Community, подскажите как настроить или в чем искать проблему?
0

Следующий этап — path tracing :)
По-идее, не сильно сложнее, нужно только рандомно отскок луча делать и цикл по сэмплам.

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

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

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

иллюстрация

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

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

0
Не могли бы вы подсказать исходники, которые могли бы отображать пересечение поверхностей? Желательно также с нуля. Давно ищу что-то несложное, чтобы добавить компонент рисования 3D графики в один математический пакет.

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

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

У меня есть отдельные списки прямоугольников для каждого уравнения поверхности. Минимальная задача пока — нарисовать пересекающиеся поверхности. Желательно ещё бы что-то почитать про перспективу и вращение сцены, т.к. я пока толком не пойму откуда взяты конкретные матричные операции.
+1
Открывал статью с опаской встретить чего ни будь сложно воспринимаемое после рабочего дня, наполненного тестами и отладкой. Однако, тема оказалась изложена крайне просто и понятно. Спасибо за материал!
0

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

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

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

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

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

+1

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

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


Полторы секунды на кадр 64 x 128.
0

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


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



+3
Самый простой и дурацкий способ, добавить к порогу случайный шум.
Получится примерно такое:
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
0
Самый простой и дурацкий способ

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

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

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

image

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

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

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

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

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

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

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

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



или так:



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


Скрытый текст
Патч к вот этому коммиту
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() {

0
А материал не подскажете для утёнка(в смысле что бы было как на образце)? С материалом сфер получается не совсем то и я не уверен проблема в логике или материал не тот просто.
PS кстати забавно что треугольники модели сразу в мировом пространстве.
0
Конечно в мировом, это самая вводная лекция в компьютерную графику, тут вообще пространство одно. Преобразования идут гораздо позже.

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

Вы о equirectangular projection? Только она не равноугольная, а равнопромежуточная. А что именно не получается сделать? Это вроде бы простая проекция. Надо переделать рендер, чтобы испускал лучи во все стороны, меняя в цикле два угла («прицел» и «доворот»). Потом откладываем эти углы по вертикали и горизонтали на изображении и пишем в соответствующую точку значение. Как-то так.

0
Ну, на самом деле всё наоборот. Для кадой точки изображения нужно найти эти углы «прицел» и «поворот», потом испустить лучи. Потом в этой точке записываем значение. И от этого «наоборот» у меня ум за разум заходит. Мне бы готового кода кусок.
0

Да, наоборот. Картинка-то первична. :)


Ну, тут тоже не особо сложно. Если размер W⨉H, то для точки (j, i) будут углы 2πj/W (горизонталь) и π(i — H/2)/H (вертикаль). Переводим из полярных координат в декартовы и получаем единичный вектор направления луча dir в функции render. Остальное всё то же самое.


Единственное, надо обработать отдельно случаи, когда вертикальный угол близок к π/2, чтоб не было бесконечностей.


Я бы код показал, но с телефона не очень удобно писать его.

0
Гран мерси, попробую закодить. Но интересно глянуть и на ваш код. Погромист из меня тот ещё.
0

Если не забуду, попробую написать, как буду дома.


Да, и я ошибся. Отдельно вертикальный случай обрабатывать не надо. Так как мы вектор задаём, никаких неоднозначностей не будет.

0

В 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, чтоб был заметен эффект, получается такое изображение:


0

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

0
Да, всё отлично. Спасибо. Стало гораздо понятнее, как сделать остальные типы камер — цилиндрическую, кубическую и рыбий глаз.
+1
Сделать рендер в кубическую панораму оказалось неожиданно просто. Просто поменял порядок осей в описании камеры и она повернулась.
Код
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

+1
Осталась сущая ерунда — в добавление к софтварному плееру равнопромежуточных панорам накодить софтварный же плеер кубических панорам.
0
Здравствуйте. Спасибо большое за статью!
Читая ее я задумался про specular часть в модели Фонга. Мне кажется вот этот узкий угловой разброс вдоль направления отраженного луча моделирует тот факт, что в основном все источники света имеют какой-то угловой размер. Даже вот у солнца он не такой уж и маленький: 0.01 радиан.
Мне интересно, все таки какой эффект тут играет главную роль в формировании размера отблеска, уголовой размер источника или все же лучи отражаются не математически строго вдоль зеркального направления, а в каком-то узком конусе?
+1
В том виде, как есть, specular компонента моделирует это отражение лучей, попадающее в некий конус вокруг идеального отражения. Впрочем, угловой размер источника тоже можно туда добавить.
+2
Нет, тут источники света точечные, степень в specular компоненте материала соответствует «шероховатости» поверхности — разбросу углов отражения (и соответственно размеру бликов). Отражение точечного источника в идеальном зеркале без шероховатости, будет выглядеть как один яркий пиксель, это если повезёт и отражение попадёт ровно в источник (точность флоата конечная, так что вероятность не совсем нулевая)
+1
Сделал на досуге реализацию на rust. Только цели делать без зависимостей не ставил: image для работы с картинками, rayon для распараллеливания, nalgebra — вектора. Сначала сделал свои вектора, потом решил не тащить веловипед, благо зависимости подключаются тривиально.

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

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

P.S. код — github.com/splav/rust-tinyraytracer
Only those users with full accounts are able to leave comments. , please.