Трассировщик лучей на D

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

Естественно, мне захотелось повторить или хотя бы попробовать выступить переводчиком с С++ (с единственного языка программирования, который я даже не пробовал учить), поэтому я рискнул потратить некоторое время на качественный перевод и небольшие исправления приведенного в статье «Трассировщик лучей на визитке» и вот что получилось…

Адаптированный код на D трассировщика лучей выглядит так:

 // https://tproger.ru/translations/business-card-raytracer/ // Адаптация для D by aquaratixc import std.stdio; // структура "вектор" struct Vector { private { float x; float y; float z; } this(float x, float y, float z) { this.x = x; this.y = y; this.z = z; } @property float getX() { return x; } @property float getY() { return y; } @property float getZ() { return z; } // сложение векторов Vector opBinary(string op)(Vector rhs) if (op == "+") { return Vector(this.x + rhs.getX, this.y + rhs.getY, this.z + rhs.getZ); } // масштабирование векторов Vector opBinary(string op)(float rhs) if (op == "*") { return Vector(this.x * rhs, this.y * rhs, this.z * rhs); } // скалярное произведение векторов float opBinary(string op)(Vector rhs) if (op == "%") { return x * rhs.getX + y * rhs.getY + z * rhs.getZ; } // векторное произведение векторов Vector opBinary(string op)(Vector rhs) if (op == "^") { return Vector( y * rhs.getZ - z * rhs.getY, z * rhs.getX - x * rhs.getZ, x * rhs.getY - y * rhs.getX ); } // нормализация вектора Vector opUnary(string op)() if (op == "~") { import std.math : sqrt; return this * (1.0 / sqrt(this % this)); } } enum int[] SPHERES_POSITIONS = [247570,280596,280600,249748,18578,18577,231184,16,16]; float randomize() { import std.random; auto gen = Random(unpredictableSeed); return uniform(0.0f, 1.0f, gen); } // сэмплируем мир и возвращаем цвет пикселя по лучу начинающемуся в точке origin // и имеющему направление direction Vector sampler(Vector origin, Vector direction) { import std.math; float traced = 0.0; Vector normal; // Проверяем, натыкается ли луч на что-нибудь int m = tracer(origin, direction, traced, normal); if (!m) { // Сфера не была найдена, и луч идет вверх: генерируем цвет неба return Vector(0.7, 0.6, 1) * pow(1.0 - direction.getZ, 4); } // Возможно, луч задевает сферу // cross - координата пересечения Vector cross = origin + direction * traced; // light - направление света (с небольшим искажением для эффекта мягких теней) Vector light = ~(Vector(9.0 + randomize, 9.0 + randomize, 16.0) + cross * (-1)); // halfVector - полувектор Vector halfVector = direction + normal * (normal % direction * (-2)); // Расчитываем коэффицент Ламберта float lambert = light % normal; // Рассчитываем фактор освещения (коэффицент Ламберта > 0 или находимся в тени)? if ((lambert < 0.0) || tracer(cross, light, traced, normal)) { lambert = 0.0; } // Рассчитываем цвет p (с учетом диффузии и отражения света) float p = pow(light % halfVector * (lambert > 0.0), 99); // m == 1 // Сфера не была задета, и луч уходит вниз, в пол: генерируем цвет пола if (m & 1) { // Сфера не была задета, и луч уходит вниз, в пол: генерируем цвет пола cross = cross * 0.2; return (cast(int)(ceil(cross.getX) + ceil(cross.getY)) & 1) ? Vector(3.0, 1.0, 1.0) : Vector(3.0, 3.0, 3.0) * (lambert * 0.2 + 0.1); } // m == 2 Была задета сфера: генерируем луч, отскакивающий от поверхности сферы // Ослабляем цвет на 50%, так как он отскакивает от поверхности (* .5) return Vector(p,p,p) + sampler(cross, halfVector) * 0.5; } // Тест на пересечение для линии [origin,vector] // Возвращаем 2, если была задета сфера (а также дистанцию пересечения traced и полу-вектор normal). // Возвращаем 0, если луч ничего не задевает и идет вверх, в небо // Возвращаем 1, если луч ничего не задевает и идет вниз, в пол int tracer ( Vector origin, Vector direction, ref float traced, ref Vector normal ) { traced = 1e9; int m = 0; float p = -origin.getZ / direction.getZ; if (0.01 < p) { traced = p; normal = Vector(0.0, 0.0, 0.0); m = 1; } // Мир зашифрован в G, в 9 линий и 19 столбцов // Для каждого столбца for(int k = 19; k--; ) // Для каждой строки for(int j = 9; j--; ) // Для этой линии j есть ли в столбце i cфера? if(SPHERES_POSITIONS[j] & 1 << k) { // Сфера есть, но задевает ли ее луч? Vector pm = origin + Vector(-k, 0, -j-4); float lambert = pm % direction; float c = pm % pm - 1; float q = lambert * lambert - c; // Задевает ли луч сферу? if (q > 0) { // Да. Считаем расстояние от камеры до сферы import std.math; float s = -lambert - sqrt(q); if ((s < traced) && (s > 0.01)) // Это минимальное расстояние, сохраняем его. А также // вычитаем вектор отскакивающего луча и записываем его в 'n' traced = s, normal = ~(pm + direction * traced), m = 2; } } return m; } void main() { // Заголовок PPM writef("P6 512 512 255 "); // Оператор "~" осуществляет нормализацию вектора // Направление камеры Vector g = ~Vector(-6, -16, 0); // Вектор, отвечающий за высоту камеры... Vector a = ~(Vector(0,0,1) ^ g) * 0.002; // Правый вектор, получаемый с помощью векторного произведения Vector b = ~(g ^ a) * 0.002; // WTF? Вот здесь https://news.ycombinator.com/item?id=6425965 написано про это подробнее.. Vector c = (a + b) * (-256) + g; // Для каждого столбца for (int y = 512; y--; ) { // Для каждого пикселя в строке for (int x = 512; x--; ) { // Используем структуру вектора, чтобы хранить цвет в RGB // Стандартный цвет пикселя — почти черный Vector p = Vector(13, 13, 13); // Бросаем по 64 луча из каждого пикселя for (int r = 64; r--; ) { // Немного меняем влево/вправо и вверх/вниз координаты начала луча (для эффекта глубины резкости) Vector t = a * (randomize() - 0.5) * 99 + b * (randomize() - 0.5) * 99; // Назначаем фокальной точкой камеры v(17,16,8) и бросаем луч // Аккумулируем цвет, возвращенный в переменной t p = sampler( Vector(17,16,8) + t, // Начало луча ~(t * (-1) + (a * (randomize() + x) + b * (y + randomize()) + c) * 16) // Направление луча с небольшим искажением ради эффекта стохастического сэмплирования ) * 3.5 + p; // + p для аккумуляции цвета } // Записываем байты PPM write(cast(char) p.getX,cast(char) p.getY, cast(char) p.getZ); } } }

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

Для того, чтобы насладиться результатом работы программы, необходимо выполнить ряд команд:

dmd card.d ./card > card.ppm

Выполняется программа достаточно долго (по крайней мере, на моем компьютере), но после окончания работы программы, в ее папке появляется файл card.ppm, который содержит красивое изображение с различными эффектами.

Вот так выглядит изображение после конвертации в JPG:

А исходный файл PPM — тут.

Довольно неплохо получилось, правда?

P.S: Авторы блога выражают сердечную благодарность коллективу сайта «Типичный программист» за их грамотную и четкую подготовку материалов, которые не дают скучать нам. Спасибо большое, ребята!

Трассировщик лучей на D: 2 комментария

  1. Спасибо)) Но мы просто перевели код на D и чуть-чуть улучшили его читаемость…

  2. Отличная работа! И исходный текст и обработанное фото смотрятся великолепно!

Добавить комментарий