Трассировщик лучей на 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: Авторы блога выражают сердечную благодарность коллективу сайта «Типичный программист» за их грамотную и четкую подготовку материалов, которые не дают скучать нам. Спасибо большое, ребята!

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

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

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

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