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

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

Подобных проектов в интернете уже море, но практически все они показывают законченные программы, в которых разобраться крайне непросто. Вот, например, очень известная программа рендеринга, влезающая на визитку. Очень впечатляющий результат, однако разобраться в этом коде очень непросто. Моей целью является не показать как я могу, а детально рассказать, как подобное воспроизвести. Более того, мне кажется, что конкретно эта лекция полезна даже не столь как учебный материал по комьпютерной графике, но скорее как пособие по программированию. Я последовательно покажу, как прийти к конечному результату, начиная с самого нуля: как разложить сложную задачу на элементарно решаемые этапы.

Внимание: просто рассматривать мой код, равно как и просто читать эту статью с чашкой чая в руке, смысла не имеет. Эта статья рассчитана на то, что вы возьмётесь за клавиатуру и напишете ваш собственный движок. Он наверняка будет лучше моего. Ну или просто смените язык программирования!

Итак, сегодня я покажу, как отрисовывать подобные картинки:



Этап первый: сохранение картинки на диск


Я не хочу заморачиваться с оконными менеджерами, обработкой мыши/клавиатуры и тому подобным. Результатом работы нашей программы будет простая картинка, сохранённая на диск. Итак, первое, что нам нужно уметь, это сохранить картинку на диск. Вот здесь лежит код, который позволяет это сделать. Давайте я приведу его основной файл:

#include <limits>
#include <cmath>
#include <iostream>
#include <fstream>
#include <vector>
#include "geometry.h"

void render() {
    const int width    = 1024;
    const int height   = 768;
    std::vector<Vec3f> framebuffer(width*height);

    for (size_t j = 0; j<height; j++) {
        for (size_t i = 0; i<width; i++) {
            framebuffer[i+j*width] = Vec3f(j/float(height),i/float(width), 0);
        }
    }

    std::ofstream ofs; // save the framebuffer to file
    ofs.open("./out.ppm");
    ofs << "P6\n" << width << " " << height << "\n255\n";
    for (size_t i = 0; i < height*width; ++i) {
        for (size_t j = 0; j<3; j++) {
            ofs << (char)(255 * std::max(0.f, std::min(1.f, framebuffer[i][j])));
        }
    }
    ofs.close();
}

int main() {
    render();
    return 0;
}

В функции main вызывается только функция render(), больше ничего. Что же внутри функции render()? Перво-наперво я определяю картинку как одномерный массив framebuffer значений типа Vec3f, это простые трёхмерные векторы, которые дают нам цвет (r,g,b) для каждого пикселя.

Класс векторов живёт в файле geometry.h, описывать я его здесь не буду: во-первых, там всё тривиально, простое манипулирование двух и трёхмерными векторами (сложение, вычитание, присваивание, умножение на скаляр, скалярное произвдение), а во-вторых, gbg его уже подробно описал в рамках курса лекций по компьютерной графике.

Картинку я сохраняю в формате ppm; это самый простой способ сохранения изображений, хотя и не всегда самый удобный для дальнейшего просматривания. Если хотите сохранять в других форматах, то рекомендую всё же подключить стороннюю библиотеку, например, stb. Это прекрасная библиотека: достаточно в проект включить один заголовочный файл stb_image_write.h, и это позволит сохранять хоть в png, хоть в jpg.

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



Этап второй, самый сложный: непосредственно трассировка лучей


Это самый важный и сложный этап из всей цепочки. Я хочу определить в моём коде одну сферу и показать её на экране, не заморачиваясь ни материалами, ни освещением. Вот так должен выглядеть наш результат:



Для удобства в моём репозитории по одному коммиту на каждый этап; Github позволяет очень удобно просматривать внесённые изменения. Вот, например, что изменилось во втором коммите по сравнению с первым.

Для начала: что нам нужно, чтобы в памяти компьютера представить сферу? Нам достаточно четырёх чисел: трёхмерный вектор с центром сферы и скаляр, описывающий радиус:

struct Sphere {
    Vec3f center;
    float radius;

    Sphere(const Vec3f &c, const float &r) : center(c), radius(r) {}

    bool ray_intersect(const Vec3f &orig, const Vec3f &dir, float &t0) const {
        Vec3f L = center - orig;
        float tca = L*dir;
        float d2 = L*L - tca*tca;
        if (d2 > radius*radius) return false;
        float thc = sqrtf(radius*radius - d2);
        t0       = tca - thc;
        float t1 = tca + thc;
        if (t0 < 0) t0 = t1;
        if (t0 < 0) return false;
        return true;
    }
};

Единственная нетривиальная вещь в этом коде — это функция, которая позволяет проверить, пересекается ли заданный луч (исходящий из orig в направлении dir) с нашей сферой. Детальное описание алгоритма проверки пересечения луча и сферы можно прочитать тут, очень рекомендую это сделать и проверить мой код.

Как работает трассировка лучей? Очень просто. На первом этапе мы просто замели картинку градиентом:

    for (size_t j = 0; j<height; j++) {
        for (size_t i = 0; i<width; i++) {
            framebuffer[i+j*width] = Vec3f(j/float(height),i/float(width), 0);
        }
    }

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



Если пересечения со сферой нет, то мы поставим цвет1, иначе цвет2:

Vec3f cast_ray(const Vec3f &orig, const Vec3f &dir, const Sphere &sphere) {
    float sphere_dist = std::numeric_limits<float>::max();
    if (!sphere.ray_intersect(orig, dir, sphere_dist)) {
        return Vec3f(0.2, 0.7, 0.8); // background color
    }
    return Vec3f(0.4, 0.4, 0.3);
}

void render(const Sphere &sphere) {
	[...]
    for (size_t j = 0; j<height; j++) {
        for (size_t i = 0; i<width; i++) {
            float x =  (2*(i + 0.5)/(float)width  - 1)*tan(fov/2.)*width/(float)height;
            float y = -(2*(j + 0.5)/(float)height - 1)*tan(fov/2.);
            Vec3f dir = Vec3f(x, y, -1).normalize();
            framebuffer[i+j*width] = cast_ray(Vec3f(0,0,0), dir, sphere);
        }
    }
	[...]
}

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

  • ширина картинки, width
  • высота картинки, height
  • угол обзора, fov
  • расположение камеры, Vec3f(0,0,0)
  • направление взора, вдоль оси z, в направлении минус бесконечности

Этап третий: добавляем ещё сфер


Всё самое сложное уже позади, теперь наш путь безоблачен. Если мы умеем нарисовать одну сферу. то явно добавить ещё несколько труда не составит. Вот тут смотреть изменения в коде, а вот так выглядит результат:



Этап четвёртый: освещение


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

struct Light {
    Light(const Vec3f &p, const float &i) : position(p), intensity(i) {}
    Vec3f position;
    float intensity;
};

Считать настоящее освещение — это очень и очень непростая задача, поэтому, как и все, мы будем обманывать глаз, рисуя совершенно нефизичные, но максимально возможно правдоподобные результаты. Первое замечание: почему зимой холодно, а летом жарко? Потому что нагрев поверхности земли зависит от угла падения солнечных лучей. Чем выше солнце над горизонтом, тем ярче освещается поверхность. И наоборот, чем ниже над горизонтом, тем слабее. Ну а после того, как солнце сядет за горизонт, до нас и вовсе фотоны не долетают. Применительно к нашим сферам: вот наш луч, испущенный из камеры (никакого отношения к фотонам, обратите внимание!) пересёкся со сферой. Как нам понять, как освещена точка пересечения? Можно просто посмотреть на угол между нормальным вектором в этой точке и вектором, описывающим направление света. Чем меньше угол, тем лучше освещена поверхность. Чтобы считать было ещё удобнее, можно просто взять скалярное произвдение между вектором нормали и вектором освещения. Напоминаю, что скалярное произвдение между двумя векторами a и b равно произведению норм векторов на косинус угла между векторами: a*b = |a| |b| cos(alpha(a,b)). Если взять векторы единичной длины, то простейшее скалярное произведение даст нам интенсивность освещения поверхности.

Таким образом, в функции cast_ray вместо постоянного цвета будем возвращать цвет с учётом источников освещения:

Vec3f cast_ray(const Vec3f &orig, const Vec3f &dir, const Sphere &sphere) {
    [...]
    float diffuse_light_intensity = 0;
    for (size_t i=0; i<lights.size(); i++) {
        Vec3f light_dir      = (lights[i].position - point).normalize();
        diffuse_light_intensity  += lights[i].intensity * std::max(0.f, light_dir*N);
    }
    return material.diffuse_color * diffuse_light_intensity;
}

Изменения смотреть тут, а вот результат работы программы:



Этап пятый: блестящие поверхности


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



Посмотрите, насколько мало нужно было сделать изменений. Если вкратце, то отсветы на блестящих поверхностях тем ярче, чем меньше угол между направлением взгляда и направлением отражённого света. Ну а углы, понятно, мы будем считать через скалярные произведения, ровно как и раньше.

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


Этап шестой: тени


А почему это у нас есть свет, но нет теней? Непорядок! Хочу вот такую картинку:



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

Vec3f shadow_orig = light_dir*N < 0 ? point - N*1e-3 : point + N*1e-3;

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

Этап седьмой: отражения


Это невероятно, но чтобы добавить отражения в нашу сцену, нам достаточно добавить только три строчки кода:

    Vec3f reflect_dir = reflect(dir, N).normalize();
    Vec3f reflect_orig = reflect_dir*N < 0 ? point - N*1e-3 : point + N*1e-3; // offset the original point to avoid occlusion by the object itself
    Vec3f reflect_color = cast_ray(reflect_orig, reflect_dir, spheres, lights, depth + 1);

Убедитесь в этом сами: при пересечении с объектом мы просто считаем отражённый луч (функция из подсчёта отбесков пригодилась!) и рекурсивно вызываем функцию cast_ray в направлении отражённого луча. Обязательно поиграйте с глубиной рекурсии, я её поставил равной четырём, начните с нуля, что будет изменяться на картинке? Вот мой результат с работающим отражением и глубиной четыре:



Этап восьмой: преломление


Научившись считать отражения, преломления считаются ровно так же. Одна функция позволяющая посчитать направление преломившегося луча (по закону Снеллиуса), и три строчки кода в нашей рекурсивной функции cast_ray. Вот результат, в котором ближайший шарик стал «стеклянным», он и преломляет, и немного отражает:



Этап девятый: добавляем ещё объекты


А чего это мы всё без молока, да без молока. До этого момента мы рендерили только сферы, поскольку это один из простейших нетривиальных математических объектов. А давайте добавим кусок плоскости. Классикой жанра является шахматная доска. Для этого нам вполне достаточно десятка строчек в функции, которая считает пересечение луча со сценой.

Ну и вот результат:



Как я и обещал, ровно 256 строчек кода, посчитайте сами!

Этап десятый: домашнее задание


Мы прошли довольно долгий путь: научились добавлять объекты в сцену, считать довольно сложное освещение. Давайте я оставлю два задания в качестве домашки. Абсолютно вся подготовительная работа уже сделана в ветке homework_assignment. Каждое задание потребует максимум десять строчек кода.

Задание первое: Environment map


На данный момент, если луч не пересекает сцену, то мы ему просто ставим постоянный цвет. А почему, собственно, постоянный? Давайте возьмём сферическую фотографию (файл envmap.jpg) и используем её в качестве фона! Для облегчения жизни я слинковал наш проект с библиотекой stb для удобства работы со жпегами. Должен получиться вот такой рендер:



Задание второе: кря!


Мы умеем рендерить и сферы, и плоскости (см. шахматную доску). Так давайте добавим отрисовку триангулированных моделей! Я написал код, позволяющий читать сетку треугольников, и добавил туда функцию пересечения луч-треугольник. Теперь добавить утёнка нашу сцену должно быть совсем тривиально!



Заключение


Моя основная задача — показать проекты, которые интересно (и легко!) программировать, очень надеюсь, что у меня это получается. Это очень важно, так как я убеждён, что программист должен писать много и со вкусом. Не знаю как вам, но лично меня бухучёт и сапёр, при вполне сравнимой сложности кода, не привлекают совсем.

Двести пятьдесят строчек рейтрейсинга реально написать за несколько часов. Пятьсот строчек софтверного растеризатора можно осилить за несколько дней. В следующий раз разберём по полочкам рейкастинг, и заодно я покажу простейшие игры, которые пишут мои студенты-первокурсники в рамках обучения программированию на С++. Stay tuned!
Поделиться публикацией

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

    +9
    Оче круто! Спасибо!
      –8
      Было бы еще круче, если бы трассировало в compile time на шаблонах!
        +1

        Вот, пожалуйста – tcbrindle/raytracer.hpp.

          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

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

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

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

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

                    +4
                    Ого как. Обычный рейтрейсинг — это уже гении?
                    Лично я при изучении нового языка программирования практически всегда пишу рейтрейсер, т.к. только так я могу погрузиться в новый язык с интересом и пользой.
                    По-вашему, я гений в n-й степени, где n-количество языков, которые я изучил?
                    Спасибо.
                      +13
                      На самом деле, первые люди, которые проходили по этому пути, вполне себе гении. Да и сейчас написать эффективный рейтрейсер очень нетривиальная задача. А так, уже идя проторенной дорогой, не гонясь за скоростью (и робастностью!) вычислений — это да, школьный уровень.
                        +1
                        Верно
                    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

                            del

                              +1
                              проблема была в прекомпилированных заголовках
                              –2
                              Можно просто писать на другом языке же
                              +2
                              Красивая задача и лаконичное решение
                                0

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

                                  0
                                  Ну а потом bidirectional 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

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

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

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

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

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

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

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

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

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

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

                                                                  +1

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

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


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

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


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



                                                                              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
                                                                                О какая красота, спасибо!
                                                                                  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
                                                                                      Да, конечно. В истории коммитов я баги фиксить не хочу, поэтому имеет смысл смотреть на самый свежий коммит.
                                                                                        0
                                                                                        В начале третьего этапа как минимум одна из сфер отобразилась в эллипс, а не в круг. Так вот, в принципе всё правильно. Я подумал и понял, да, действительно, при центральной проекции сферы могут отображаться в эллипсы. Но всё равно картинка кажется неестественной. Не знаете, почему? Может быть, дело в том, что центральная проекция не самая лучшая? Может быть, потому что сетчатка глаза не плоская, а потому обычная центральная проекция на воображаемую плоскую поверхность — не самый лучший вариант?
                                                                                          +1
                                                                                          Центральная проекция хорошо описывает происходящее в глазу; единственное, что у меня камера довольно близко стоит к сферам, приводя к таким сильным искажениям. В реальной жизни немного не так обычно.
                                                                                          0
                                                                                          А подскажите кто-нибудь код для камеры, чтоб рендерила из сцены сразу равноугольную панораму. Я половину интернетов уже перерыл, осталось только китайский выучить.
                                                                                            0
                                                                                            А что такое равноугольная панорама?
                                                                                          0
                                                                                          Здравствуйте. Спасибо большое за статью!
                                                                                          Читая ее я задумался про specular часть в модели Фонга. Мне кажется вот этот узкий угловой разброс вдоль направления отраженного луча моделирует тот факт, что в основном все источники света имеют какой-то угловой размер. Даже вот у солнца он не такой уж и маленький: 0.01 радиан.
                                                                                          Мне интересно, все таки какой эффект тут играет главную роль в формировании размера отблеска, уголовой размер источника или все же лучи отражаются не математически строго вдоль зеркального направления, а в каком-то узком конусе?
                                                                                            +1
                                                                                            В том виде, как есть, specular компонента моделирует это отражение лучей, попадающее в некий конус вокруг идеального отражения. Впрочем, угловой размер источника тоже можно туда добавить.
                                                                                              +2
                                                                                              Нет, тут источники света точечные, степень в specular компоненте материала соответствует «шероховатости» поверхности — разбросу углов отражения (и соответственно размеру бликов). Отражение точечного источника в идеальном зеркале без шероховатости, будет выглядеть как один яркий пиксель, это если повезёт и отражение попадёт ровно в источник (точность флоата конечная, так что вероятность не совсем нулевая)

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

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