Подобных проектов в интернете уже море, но практически все они показывают законченные программы, в которых разобраться крайне непросто. Вот, например, очень известная программа рендеринга, влезающая на визитку. Очень впечатляющий результат, однако разобраться в этом коде очень непросто. Моей целью является не показать как я могу, а детально рассказать, как подобное воспроизвести. Более того, мне кажется, что конкретно эта лекция полезна даже не столь как учебный материал по комьпютерной графике, но скорее как пособие по программированию. Я последовательно покажу, как прийти к конечному результату, начиная с самого нуля: как разложить сложную задачу на элементарно решаемые этапы.
Внимание: просто рассматривать мой код, равно как и просто читать эту статью с чашкой чая в руке, смысла не имеет. Эта статья рассчитана на то, что вы возьмётесь за клавиатуру и напишете ваш собственный движок. Он наверняка будет лучше моего. Ну или просто смените язык программирования!
Итак, сегодня я покажу, как отрисовывать подобные картинки:
![](https://habrastorage.org/getpro/habr/post_images/064/50f/65d/06450f65da7c0503838bc008dcaba6a3.jpg)
Этап первый: сохранение картинки на диск
Я не хочу заморачиваться с оконными менеджерами, обработкой мыши/клавиатуры и тому подобным. Результатом работы нашей программы будет простая картинка, сохранённая на диск. Итак, первое, что нам нужно уметь, это сохранить картинку на диск. Вот здесь лежит код, который позволяет это сделать. Давайте я приведу его основной файл:
#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.
Итого, целью данного этапа является убедиться, что мы можем а) создать картинку в памяти и записывать туда разные значения цветов б) сохранить результат на диск, чтобы можно было его просмотреть в сторонней программе. Вот результат:
![](https://habrastorage.org/getpro/habr/post_images/ec1/8d8/1f8/ec18d81f825957255d786b50c05c8c4b.jpg)
Этап второй, самый сложный: непосредственно трассировка лучей
Это самый важный и сложный этап из всей цепочки. Я хочу определить в моём коде одну сферу и показать её на экране, не заморачиваясь ни материалами, ни освещением. Вот так должен выглядеть наш результат:
![](https://habrastorage.org/getpro/habr/post_images/b02/143/d02/b02143d02d333d683313d209ec14eb25.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, в направлении минус бесконечности
Этап третий: добавляем ещё сфер
Всё самое сложное уже позади, теперь наш путь безоблачен. Если мы умеем нарисовать одну сферу. то явно добавить ещё несколько труда не составит. Вот тут смотреть изменения в коде, а вот так выглядит результат:
![](https://habrastorage.org/getpro/habr/post_images/fc0/f37/6c9/fc0f376c9cb8971781f2a24065b47fa5.jpg)
Этап четвёртый: освещение
Всем хороша наша картинка, да вот только освещения не хватает. На протяжении всей оставшейся статьи мы об этом только и будем разговаривать. Добавим несколько точечных источников освещения:
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;
}
Изменения смотреть тут, а вот результат работы программы:
![](https://habrastorage.org/getpro/habr/post_images/207/766/76d/20776676d3aff568b466807ef548ea5a.jpg)
Этап пятый: блестящие поверхности
Трюк со скалярным произведением между нормальным вектором и вектором света неплохо приближает освещение матовых поверхностей, в литературе называется диффузным освещением. Что же делать, если мы хотим гладкие да блестящие? Я хочу получить вот такую картинку:
![](https://habrastorage.org/getpro/habr/post_images/2d6/b39/c52/2d6b39c52aaa6a8c8667fd2df19f2e93.jpg)
Посмотрите, насколько мало нужно было сделать изменений. Если вкратце, то отсветы на блестящих поверхностях тем ярче, чем меньше угол между направлением взгляда и направлением отражённого света. Ну а углы, понятно, мы будем считать через скалярные произведения, ровно как и раньше.
Эта гимнастика с освещением матовых и блестящих поверхностей известна как модель Фонга. В вики есть довольно детальное описание этой модели освещения, она хорошо читается при параллельном сравнении с моим кодом. Вот ключевая для понимания картинка:
![](https://habrastorage.org/getpro/habr/post_images/881/d77/0ca/881d770ca8779cc876808a22c0fd8cce.png)
Этап шестой: тени
А почему это у нас есть свет, но нет теней? Непорядок! Хочу вот такую картинку:
![](https://habrastorage.org/getpro/habr/post_images/e8f/fdd/04c/e8ffdd04c72dfc92f0845bb7b079faf7.jpg)
Всего шесть строчек кода позволяют этого добиться: при отрисовке каждой точки мы просто убеждаемся, не пересекает ли луч точка-источник света объекты нашей сцены, и если пересекает, то пропускам текущий источник света. Тут есть только маленькая тонкость: я самую малость сдвигаю точку в направлении нормали:
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 в направлении отражённого луча. Обязательно поиграйте с глубиной рекурсии, я её поставил равной четырём, начните с нуля, что будет изменяться на картинке? Вот мой результат с работающим отражением и глубиной четыре:
![](https://habrastorage.org/getpro/habr/post_images/a9d/1c2/e6d/a9d1c2e6d428aaba07953cae3fa007ea.jpg)
Этап восьмой: преломление
Научившись считать отражения, преломления считаются ровно так же. Одна функция позволяющая посчитать направление преломившегося луча (по закону Снеллиуса), и три строчки кода в нашей рекурсивной функции cast_ray. Вот результат, в котором ближайший шарик стал «стеклянным», он и преломляет, и немного отражает:
![](https://habrastorage.org/getpro/habr/post_images/8c8/14a/56a/8c814a56aafc88609ebeb8c5a6f9a494.jpg)
Этап девятый: добавляем ещё объекты
А чего это мы всё без молока, да без молока. До этого момента мы рендерили только сферы, поскольку это один из простейших нетривиальных математических объектов. А давайте добавим кусок плоскости. Классикой жанра является шахматная доска. Для этого нам вполне достаточно десятка строчек в функции, которая считает пересечение луча со сценой.
Ну и вот результат:
![](https://habrastorage.org/getpro/habr/post_images/d88/871/a50/d88871a5021eb69cc64c7b9a5915e855.jpg)
Как я и обещал, ровно 256 строчек кода, посчитайте сами!
Этап десятый: домашнее задание
Мы прошли довольно долгий путь: научились добавлять объекты в сцену, считать довольно сложное освещение. Давайте я оставлю два задания в качестве домашки. Абсолютно вся подготовительная работа уже сделана в ветке homework_assignment. Каждое задание потребует максимум десять строчек кода.
Задание первое: Environment map
На данный момент, если луч не пересекает сцену, то мы ему просто ставим постоянный цвет. А почему, собственно, постоянный? Давайте возьмём сферическую фотографию (файл envmap.jpg) и используем её в качестве фона! Для облегчения жизни я слинковал наш проект с библиотекой stb для удобства работы со жпегами. Должен получиться вот такой рендер:
![](https://habrastorage.org/getpro/habr/post_images/3e0/531/fa9/3e0531fa9ee361de7580029e818bc5f5.jpg)
Задание второе: кря!
Мы умеем рендерить и сферы, и плоскости (см. шахматную доску). Так давайте добавим отрисовку триангулированных моделей! Я написал код, позволяющий читать сетку треугольников, и добавил туда функцию пересечения луч-треугольник. Теперь добавить утёнка нашу сцену должно быть совсем тривиально!
![](https://habrastorage.org/getpro/habr/post_images/064/50f/65d/06450f65da7c0503838bc008dcaba6a3.jpg)
Заключение
Моя основная задача — показать проекты, которые интересно (и легко!) программировать, очень надеюсь, что у меня это получается. Это очень важно, так как я убеждён, что программист должен писать много и со вкусом. Не знаю как вам, но лично меня бухучёт и сапёр, при вполне сравнимой сложности кода, не привлекают совсем.
Двести пятьдесят строчек рейтрейсинга реально написать за несколько часов. Пятьсот строчек софтверного растеризатора можно осилить за несколько дней. В следующий раз разберём по полочкам рейкастинг, и заодно я покажу простейшие игры, которые пишут мои студенты-первокурсники в рамках обучения программированию на С++. Stay tuned!
Комментарии (66)
Zet_Roy
20.01.2019 22:08Моя основная задача — показать проекты, которые интересно (и легко!) программировать
Да что бы такое програмировать это нужно быть гением.haqreu Автор
20.01.2019 22:21+5Совсем нет, или студенты нашего провинциального университета поголовно гении. Достаточно немного усидчивости и последовательности. Конечно, хорошие методички лишними не бывают.
EvilBeaver
21.01.2019 11:32Полагаю, имелось в виду, «чтобы программировать такое ЛЕГКО — надо или хорошо набить руку на этом, или быть гением, чтобы делать такое сходу с нуля.»
masai
21.01.2019 20:03+2студенты нашего провинциального университета
Судя по почте на github, это Universite de Lorraine. Ну, не такой он провинциальный. 201-300 место в ARWU2018. Тот же МФТИ – 410-500 место. Студенты, я подозреваю, тоже не самые простые.
haqreu Автор
21.01.2019 20:47Рекомендую посмотреть, как именно составляются рейтинги вузов. Тот факт, что университет Лотарингии впереди, например, СПбГУ по рейтингу, совершенно не обозначает того, что там лучше студенты. Будучи причастным к обоим, могу сравнивать.
Ещё один простой пример: Ecole Polytechnique, школа с сильнейшими французским студентами, находится на 401-500 в этом же самом рейтинге. Неужто она хуже провинциального университета (я по-прежнему про Universite de Lorraine), который на 201-300? Нет, она меньше. Бюджет важен для рейтинга. Кстати, следите за руками. Университет Лотарингии был создан в 2012м году путём слияния шести (!) различных вузов, близких друг к другу географически. Для чего это было сделано? Подсказка: см. слова «бюджет», «количество студентов».haqreu Автор
21.01.2019 21:19Про слияние мелких вузов в крупный, чтобы был понятен масштаб, я просто оставлю вот здесь картинку расположения университетских кампусов:
Скрытый текстmasai
21.01.2019 21:29Я совсем не об этом. Не знаю, как у других, но у меня слова «провинциальный вуз» ассоциируются с вузом, который вообще ни в какие рейтинги не входит, и в котором студентам-программистам на паре по компьютерной графике показывают пиратский Corel Draw, а сами студенты если и разбираются в программировании, то не благодаря вузу, а вопреки. К сожалению, такие вузы существуют. Я просто как-то смотрел учебные планы и рабочие программы небольших региональных вузов, и там всё не так хорошо, как хотелось бы. Может, я ошибаюсь, и таких вузов почти и не осталось, в которых ООП на TurboPascal на третьем курсе учат (условно).
Извините за оффтопик, но раз уж заговорили. Вообще, я заметил по другим публикациям, что для вас характерна «чрезмерная» скромность. Вы пишете, что не специалист и сами плохо в чём-то разбираетесь, а на самом деле разбираетесь достаточно хорошо. Что это всё легко понять студентам обычного провинциального вуза, но вуз не такой уж и обычный, а вполне себе хороший. Что какую-то тему вы прошли ещё в школе, но при этом оказывается, что это специализированная физмат-школа (извините, если ошибаюсь, я мог и перепутать).
Наверняка у вас благие намерения. Вы хотите мотивировать других людей, показать им, что не боги горшки обжигают. Но если материал для кого-то сложный, то слова о том, что он простой и школьного уровня, могут лишь заставить человека подумать: «Ну вот, школьники понимают, а я не понимаю. Видать, я недотёпа». Самооценка падает, ничего делать не хочется.
Так получилось, что я довольно долго проработал в вузе и параллельно вёл кружок в местном лицее. Вуз маленький, конкурсы небольшие, разброс в подготовке студентов очень большой. А в лицее на физмате много талантливых ребят. И иногда так получалось, что я одну и ту же тему рассказывал восьмиклассникам в кружке и студентам (например, линейные корректирующие коды). И бывало, что восьмиклассники схватывали быстрее. Но если бы я сказал студентам, что это школьная тема – что недалеко от истины, в общем-то, – вряд ли бы это подняло их мотивацию. Это просто звучало бы обидно.
Это всё вовсе не критика, у вас замечательные статьи, с удовольствием их читаю. Просто наблюдение. Вы, безусловно, возразите мне, что это статьи, которые может понять школьник, а не обязан их понять, и что не стоит «читать их за чашкой чая». И будете правы. Но я не о формальной стороне, а о коннотации. Просто учитывайте, что аудитория на хабре с более широким разбросом уровня подготовки.haqreu Автор
21.01.2019 21:44Спасибо за развёрнутый ответ. Лично мне, на самом деле, абсолютно всё равно, какой уровень подготовки у моего собеседника. Мне это неинтересно. Мне интересен огонь в глазах, и, конечно, я всячески пытаюсь его разжечь. С огнём в глазах любые пробелы в подготовке преодолеваются без особых трудностей.
Что касается хабра, то, конечно, я вынужден признать, что в силу критического недостатка времени я тут не всегда пишу самодостаточные тексты. Обычно я пишу либо для себя самого, когда разбираюсь в новой теме, либо (как конкретно эта статья, обратите внимание, что я уже опубликовал её перевод) в качестве записей для своих студентов, но, в отличие от хабра, они дополнительно ещё имеют цирк со мной у доски.
А что абсолютно всё, о чём я пишу, это просто — так ведь это правда. Если я не сумел написать доступно — ну я так себе лектор. В комментариях ведь можно задавать вопросы, правда? Достаточно иметь огонь в глазах :)
Alex_ME
21.01.2019 22:54Для примера напишу про свой — Волгоградский Государственный Технический Университет, опорный ВУЗ, между прочим :)
На первом курсе был матан, линейная алгебра, дискретка и прочие годные вещи и Кумир. Программистская часть программы была составлена с учетом абсолютно нулевых начальных знаний в этой области у студентов.
haqreu Автор
21.01.2019 22:57Пример интересный, но не могли бы вы сказать, какую именно мысль он иллюстрирует? Я запутался слегка.
QtRoS
20.01.2019 23:30+1На самом делье это вполне реально, как действительно заметил haqreu доступно даже студентам. Я откопал скрин своего студенческого (в рамках курса www.edx.org/course/computer-graphics от небезыизвестного Ravi Ramamoorthi) рейтрейсера. Тут нет отражений, так как они не очень симпатичные у меня получились почему-то, но общую картину о рейтресинге дает.
Картинка 640x480Areredify
21.01.2019 00:40Мы в школе делали рт (обратный, правда), даже
samodum
21.01.2019 00:06+1Ого как. Обычный рейтрейсинг — это уже гении?
Лично я при изучении нового языка программирования практически всегда пишу рейтрейсер, т.к. только так я могу погрузиться в новый язык с интересом и пользой.
По-вашему, я гений в n-й степени, где n-количество языков, которые я изучил?
Спасибо.haqreu Автор
21.01.2019 00:08+2На самом деле, первые люди, которые проходили по этому пути, вполне себе гении. Да и сейчас написать эффективный рейтрейсер очень нетривиальная задача. А так, уже идя проторенной дорогой, не гонясь за скоростью (и робастностью!) вычислений — это да, школьный уровень.
Klenov_s
20.01.2019 23:59Я во время своей учебы в вузе (конец 90х) покупал книжку с таким же движком. Было красиво, но долго )
kovserg
21.01.2019 01:04Еще можно сохранять не в ppm, а в tiff. Он поддерживается почти всеми, в отличии от ppm. Минимальный код примерно такой:
tiff-rgb8int 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; }
haqreu Автор
21.01.2019 01:10По-моему, проще подключить stb_image_write.h и писать сразу .jpg. Я пишу ppm только потому, что это кратчайший способ сохранить картинку без привлечения стороннего кода.
LorHobbit
21.01.2019 21:57Плюс ppm в том, что его ВООБЩЕ не надо поддерживать. Тупо в цикле вывод массива пикселей в файл, перед которым маленький текстовый заголовок. При этом его просмотр поддерживают вполне реальные популярные смотрелки, например, IrfanView.
Для отладочного вывода в учебной программе самое то. :) Да и не только в учебной. Недавно писал обработку видеопотока, надо было найти место, где кадры бьются — отладочный вывод в ppm-ки быстро решил задачу!
А tiff да, красота, и для законченных систем наверняка лучше, но он и куда сложнее, как и все форматы-контейнеры
Автору спасибо за статью, прочитал с удовольствием.
ivanrt
21.01.2019 04:16Я тоже на волне интереса к rtx решил разобраться с этим делом. В моем варианте был ещё вывод на экран с аккумуляцией, опциональный рендеринг на видеокарте который давай на порядок большую скорость, глубина резкости, антиалиасинг, ааbb tree, kd tree, optix для rtx рендеринга. С материалами долго возился. Делать не сложно если делать всё по шагам. Знание и рендерер строится как пирамидка.
iOrange
21.01.2019 19:34+2Если захотите (ну а вдруг) разобраться с RTX напрямую, без посредников в лице OptiX, скромно порекомендую свои статьи:
1) gamedev.ru/code/articles/vulkan_raytracing
2) gamedev.ru/code/articles/vulkan_raytracing_part2
Scrayer
21.01.2019 08:12Я тут проходил мимо, но решил попробовать, с плюсами знаком плохо и вопрос закономерный:
без #include algorithm ругается на std::max, std::min.
С подключенными алгоритмами ругается на все остальное (vector, vec3f, framebuffer, ofs).
Using namespace std эффекта не возымела.
MSVS2017Community, подскажите как настроить или в чем искать проблему?
Alex_ME
21.01.2019 10:16Следующий этап — path tracing :)
По-идее, не сильно сложнее, нужно только рандомно отскок луча делать и цикл по сэмплам.
IGR2014
21.01.2019 11:55+1Такие люди как Вы вызывают не только интерес к компьютерной графике, но и желание всё «покрутить» самому. Спасибо огромное за статью, читал одновременно с интересом и удивлением насколько всё просто (в плане реализации). Будет интересно сегодня вечером попробовать соорудить что-нибудь простенькое.
fediatherobot
21.01.2019 12:02Тут есть некоторая путаница в терминологии.Прямая трассировка — считаем лучи от источника в надежде что он попадет в экран(почти не используется).
А автор привел обратную трассировку(т.н. ray casting) когда считаем лучи от камеры к источнику светаhaqreu Автор
21.01.2019 12:04Мне кажется, что вы путаете ray casting и ray tracing. Оба работают от камеры к источнику, но в данном случае у меня именно ray tracing.
fediatherobot
21.01.2019 16:38Да, похоже.В свое время писал на паскале рэйтрейсер под впечатлением этого мануала
Обратная трассировка лучей (она же рэйкастинг, raycasting) — простой, хотя и довольно медленный, метод получения высокореалистичных изображений. Этот метод часто путают с прямой трассировкой лучей (рэйтрэйсинг, raytracing), которая, на самом деле, практически никогда и никем не используется из-за своей редкостной неэффективности. Впрочем, эти два термина уже практически и не различают.
Вики тогда не было и мне почему-то показалось что прямая так называется т.к. моделирует свет как он идет на самом деле — от источника к глазу.Впрочем насколько я понял из современного описания рэйкастинг используется скорее для определения видимости.Из Вики:
Эта особенность делает невозможным точный рендеринг отражений, преломлений и естественной проекции теней с помощью рейкастинга.
Тогда не ясно что имели в виду авторы demo design faq.Буду благодарен если просветите.haqreu Автор
21.01.2019 19:16На самом деле, разделение этих терминов произошло относительно недавно, раньше они были взаимозаменяемыми. Ray casting находит первое пересечение и сразу же определяет конечный цвет пикселя, в то время как ray tracing идёт дальше, позволяя рендерить отражения и тому подобное.
иллюстрация
ViacheslavMezentsev
21.01.2019 17:11Не могли бы вы подсказать исходники, которые могли бы отображать пересечение поверхностей? Желательно также с нуля. Давно ищу что-то несложное, чтобы добавить компонент рисования 3D графики в один математический пакет.
Сейчас это выглядит так:
Здесь не используется полное вычисление картинки, т.к. это вероятно долго будет вычисляться на c#, а просто рисуется линиями и полигонами. Я так понимаю, что этим способом пересечения не нарисовать?
У меня есть отдельные списки прямоугольников для каждого уравнения поверхности. Минимальная задача пока — нарисовать пересекающиеся поверхности. Желательно ещё бы что-то почитать про перспективу и вращение сцены, т.к. я пока толком не пойму откуда взяты конкретные матричные операции.rogoz
21.01.2019 17:46Картинка не картинка
en.smath.com/forum/resource.ashx?a=29878&b=2
«Please do not link directly to this resource. You must have a session in the forum.»
Lexx918
21.01.2019 17:52Вспомнилось как было похожее на JS в 2017 js1k.com/2017-magic/details/2648
Не стоит открывать демку на слабой машине)
ktod
21.01.2019 18:02+1Открывал статью с опаской встретить чего ни будь сложно воспринимаемое после рабочего дня, наполненного тестами и отладкой. Однако, тема оказалась изложена крайне просто и понятно. Спасибо за материал!
masai
21.01.2019 21:50Если вдруг кто-то заинтересовался упомянутой библиотекой stb, вот репозиторий – nothings/stb. Обратите внимание на список подобных однофайловых библиотечек.
haqreu Автор
21.01.2019 21:54Да! Это stb — это прекрасная билиотека, всё отдано в public domain. И, помимо отличной лицензии, автор сделал очень сильный упор на простоту подключения модулей. Я за повсеместное распространение однофайловых библиотек.
Dark_Daiver
21.01.2019 22:28Я за повсеместное распространение однофайловых библиотек.
При всем уважении, это очень скользкая дорожка. Подключать то их может и удобно, но ведь иногда надо и их код читать.haqreu Автор
21.01.2019 22:34А если в два файла, то не надо? ;)
Dark_Daiver
21.01.2019 22:37А если библиотека хорошо разбита на файлы, то как правило ее удобней читать.
haqreu Автор
21.01.2019 22:39Разве нормальная среда разработки не делает чтение одинаково удобным в обоих случаях?
Dark_Daiver
21.01.2019 22:44Не думаю что есть IDE которая сделает чтение условного Eigen, слитого в один файл, таким же удобным, как чтение Eigen разбитого по файлам.
fey
22.01.2019 09:33+1Спасибо за интересный туториал. Сейчас изучаю Rust, так что сделал на нём: github.com/feymartynov/tinyraytracer.
haqreu Автор
22.01.2019 09:50Ого, быстро у вас получилось. Очень рекомендую выкладывать скриншоты в репозиторий. У меня, например, компилятора rust под рукой нет :(
badgerdash
22.01.2019 14:36Прочитал материал с чашкой чая в руке :) Придется вернуться еще раз.
Спасибо за работу, Вам удалось пробудить то ощущение от магии цифр, которое было в студенчестве!
VioletGiraffe
22.01.2019 16:19+1Жаль, что у меня ни на первом, ни на пятом курсе профильного ВУЗа ничего подобного не было :)
mdma_xtc
Оче круто! Спасибо!
orcy
Было бы еще круче, если бы трассировало в compile time на шаблонах!
masai
Вот, пожалуйста – tcbrindle/raytracer.hpp.
orcy
Огонь! Интересно что он дальше пишет что в рантайме эта же задача выполняется за пол-секунды, т.е. компилятор не очень хорошо трассирует лучи. Вариант по ссылке в основном основан на constexpr (т.е. код вполне понимаемый), но еще дальше он пишет что вариант трассировки лучей написанный чисто на шаблонах работает еще медленней. Интересно, никогда не думал о таком аспекте constexpr как ускорение компиляции.