Однажды, просматривая сайт “Типичный программист”, я наткнулся на очень интересную штуку, размещенную в рубрике “Красивый хак”. Та штука, которую я увидел, очень удивила меня, прежде всего тем, что весь код размещался на визитной карточке, но при этом он выполнял очень сложные процедуры математической отрисовки трехмерной графики с физическими эффектами!
Естественно, мне захотелось повторить или хотя бы попробовать выступить переводчиком с С++ (с единственного языка программирования, который я даже не пробовал учить), поэтому я рискнул потратить некоторое время на качественный перевод и небольшие исправления приведенного в статье “Трассировщик лучей на визитке” и вот что получилось…
Адаптированный код на 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 и чуть-чуть улучшили его читаемость…
Отличная работа! И исходный текст и обработанное фото смотрятся великолепно!