Кондитерская программиста. Bon Appetit

Всем привет, в этой статье пойдёт речь о любопытных экспериментах с С++ и 3D графикой. Будем открывать свою собственную кондитерскую-программиста. Bon Appetit!

image-loader.svg

Для начала давайте немного пробежимся по структуре программ на С++.

Как только мы с Вами создадим проект на С++ в среде VisualStudio, нас встретит вот такой вот код:

#include 
using namespace std;
int main()
{
	cout << "Hello world!”;
} 

Давайте немного проясним, что же эти «странные» строчки означают?

Для начала разберёмся с #include — это инициализация заголовочного файла iostream с классами, функциями и переменными для организации ввода-вывода в языке программирования C++. Он включён в стандартную библиотеку C++. Название образовано от Input/Output Stream («поток ввода-вывода»). В языке C++ и его предшественнике, языке программирования Си, нет встроенной поддержки ввода-вывода, вместо этого используется библиотека функций. iostream управляет вводом-выводом. Библиотека использует объекты cin,  cout,  cerr и clog для передачи информации и из стандартных потоков ввода, вывода, ошибок без буферизации и ошибок с буферизацией соответственно. Являясь частью стандартной библиотеки C++, эти объекты также являются частью стандартного пространства имён — std.

Ну всё, теперь мы разобрались с первой строчкой! Получается, что iostream — библиотека, которая помогает нам вводить и выводить данные, этого нам будет достаточно.

Далее идёт объявление пространства имён using namespace std, что же это такое? Интернет нам даёт следующее определение: Пространство имен — это декларативная область, в рамках которой определяются различные идентификаторы (имена типов, функций, переменных, и т. д.). Пространства имен используются для организации кода в виде логических групп и с целью избежания конфликтов имен, которые могут возникнуть, особенно в таких случаях, когда база кода включает несколько библиотек. Все идентификаторы в пределах пространства имен доступны друг другу без уточнения. Идентификаторы за пределами пространства имен могут обращаться к членам с помощью полного имени для каждого идентификатора, например std::cin или std::cout для одного идентификатора using std::string, для всех идентификаторов в пространстве имен using namespace std;. Код в файлах заголовков всегда должен содержать полное имя в пространстве имен.

Получается, что, если не вдаваться в подробности и сложную терминологию, пространство имён (using namespace std) — это наш универсальный помощник, который помогает преобразовать длинные и сложные конструкции вроде std::cin >> a в простую конструкцию cin >> a, а для этого всего-то надо прописать в начале программы, после объявления библиотек, одну строчку: using namespace std — нашу палочку-выручалочку, пространство имён std.

Теперь перейдём к телу программы, а именно к функции int main () — это самая главная функция в нашей программе. Даже её название переводится с английского на русский как «главная», именно с неё и будет начинаться выполняться код, который мы будем прописывать. (Небольшое замечание: в программе может быть великое множество функций, но всегда присутствует главная функция main, где все эти функции объявляются, это своеобразная «стартовая линия» нашей программы, отправная точка маршрута.) Тип у функции может быть разный, например int или float, char или double и т.д., но все эти типы объединяет одно, они возвращают какое-то значение после своего вызова, за исключением void — оно ничего не возвращает.

Вы также могли заметить внутри тела функции (внутри фигурных скобок: { }) строчкуcout << "Hello world!”, здесь мы как раз можем наблюдать яркий пример применения пространства имён std, поэтому вместо std::cout << "Hello world!” мы пишем cout << "Hello world!", это ли не магия? В данном случае команда cout отвечает за вывод в консоль фразы, которая написана в » », а именно фразу «Hello world!», что означает «Привет мир!». Важно отметить, что в конце таких команд ставится точка с запятой (;), что означает для компилятора конец команды.

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

  • void: тип без значения

  • int: представляет целое число. В зависимости от архитектуры процессора может занимать 2 байта (16 бит) или 4 байта (32 бита). Диапазон предельных значений соответственно также может варьироваться от –32768 до 32767 (при 2 байтах) или от −2 147 483 648 до 2 147 483 647 (при 4 байтах).

  • float: представляет вещественное число с плавающей точкой в диапазоне +/- 3.4E-38 до 3.4E+38. В памяти занимает 4 байта (32 бита)

  • double: представляет вещественное число двойной точности с плавающей точкой в диапазоне +/- 1.7E-308 до 1.7E+308. В памяти занимает 8 байт (64 бита)

  • char: представляет один символ. Занимает в памяти 1 байт (8 бит). Может хранить любое значение из диапазона от -128 до 127

  • и т.д.

Совсем забыл, для написания кода, нам ещё понадобится понимания термина «массив». Итак, массив — это область памяти, где могут последовательно храниться несколько значений. Массив можно представить в виде здания, где в каждой квартире живут «данные»:

image-loader.svg

А также нам понадобится циклы, мы будем использовать цикл for, про остальные циклы, вы можете почитать вот здесь.

Итак, цикл for — параметрический цикл (цикл с фиксированным числом повторений). Для организации такого цикла необходимо осуществить три операции:

  • Инициализация — присваивание параметру цикла начального значения;

  • Условие — проверка условия повторения цикла, чаще всего — сравнение величины параметра с некоторым граничным значением;

  • Модификация — изменение значения параметра для следующего прохождения тела цикла.

Эти три операции записываются в скобках и разделяются точкой с запятой; . Как правило, параметром цикла является целочисленная переменная.

Инициализация параметра осуществляется только один раз — когда цикл for начинает выполняться.

Проверка Условия повторения цикла осуществляется перед каждым возможным выполнением тела цикла. Когда выражение, проверяющее Условие становится ложным (равным нулю), цикл завершается. Модификация параметра осуществляется в конце каждого выполнения тела цикла. Параметр может как увеличиваться, так и уменьшаться.

for (Инициализация; Условие; Модификация)
{
		Блок Операций;
}

Теперь давайте попробуем вывести что-нибудь, в консольную строку, например символ @ для этого с комментариями, представлен ниже:

#include  // инициализация библиотеки
using namespace std; // инициализация пространства имён std
int main() // начало главной функции main()
{
	// инициализация переменных, которые являются неким разрешением консоли (количество символов, которое помещается в консоли)
	int width = 120;
	int heigh = 30;
	// инициализация массива символов выводимых на экран
	char* screen = new char[width * heigh + 1]; // +1 - это дополнительный нулевой символ,
	screen[width * heigh] = '\0';               // который существует для остановки вывода 
	                                            // символов массива

	for (int i = 0; i < width; i++)
	{
		for (int j = 0; j < heigh; j++)
		{
			screen[i + j * width] = '@'; // заполняем массив какими-то символами,                                                                   как пример символ "@"
		}
	}
	cout << screen; // выводим на экран массив screen
}

И вот, что мы получаем на выходе:

image-loader.svg

Немного видоизменив наши циклы for мы сможем создать круг с помощью символа @:

for (int i = 0; i < width; i++)
	{
		for (int j = 0; j < heigh; j++)
		{
			float x = (float)i / width * 2.0f - 1.0f;
			float y = (float)j / heigh * 2.0f - 1.0f;
			char pixel = ' '; 
			if ((x * x + y * y) < 0.5) 
				pixel = '@';
			screen[i + j * width] = pixel; 
		}
	}

Результат:

image-loader.svg

Теперь создадим новую переную, которая хранит соотношение сторон нашей консоли:

float aspect = (float) width / heigh;

А также во вложенном цикле for по j координату по ширине (по Ох) домножим на переменную, в которой хранится соотношение сторон нашей консоли:

x = x * aspect;

На выходе мы получим такой «цилиндр»:

image-loader.svg

Но тут выясняется, что помимо соотношения сторон консоли, у нас есть соотношение сторон каждого символа, для символа @ соотношение будет равно 11 на 24 px (пикселя), что же нам делать?

Решение есть! Добавим это значение в наш код:

float pixelAspect = 11.0f / 24.0f;

А теперь снова поменяв наш код (вложенный цикл for по j):

x = x * aspect * pixelAspect;

И вот, что мы получаем, домножив наше значение на соотношение сторон символа:

image-loader.svg

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

int moving = 20000; // переменная отвечающая за продолжительность перемещения нашего шара
	for (int t = 0; t < moving; t++)
	{
		for (int i = 0; i < width; i++)
		{
			for (int j = 0; j < heigh; j++)
			{
				float x = (float)i / width * 2.0f - 1.0f;
				float y = (float)j / heigh * 2.0f - 1.0f;
				x *= aspect * pixelAspect; // домножим так же на соотношение сторон символа

				x += sin(t * 0.001);

				char pixel = ' ';
				if ((x * x + y * y) < 0.5)
					pixel = '@';
				screen[i + j * width] = pixel;
			}
		}
		cout << screen;
	}

Ура, мы оживили нашу фигуру:

image-loader.svg

Вам не кажется, что стало как-то скучно?

image-loader.svg

Давайте украсим нашу фигуру! Для этого перепишем наш код:

#include 
using namespace std;
int main()
{
	// некое разрешение консол (количество символов, которое помещается в консоли)
	int width = 120;
	int heigh = 30;
	// массив символов выводимых на экран
	char* screen = new char[width * heigh + 1]; // +1 - это дополнительный нулевой символ,
	screen[width * heigh] = '\0';               // который существует для остановки вывода 
	                                            // символов массива

	float aspect = (float)width / heigh; // переменная, которая хранит соотношение сторон нашей консоли
	float pixelAspect = 11.0f / 24.0f;   // перменная, которая хранит соотношение сторон символа

	char gradient[] = ".:!/r(l1Z4H9W8$@"; // градиент из символов, символы – «цвет» нашей фигуры
	int gradientSize = size(gradient) - 2;

	int moving = 50000; // переменная отвечающая за продолжительность перемещения нашего шара
	for (int t = 0; t < moving; t++)
	{
		for (int i = 0; i < width; i++)
		{
			for (int j = 0; j < heigh; j++)
			{
				float x = (float)i / width * 2.0f - 1.0f;
				float y = (float)j / heigh * 2.0f - 1.0f;
				//x *= aspect; // координаты по ширине (по Ох) домножаем на переменную, в которой хранится
				//             // соотношение сторон консоли
				x *= aspect * pixelAspect; // домножим так же на соотношение сторон символа

				x += sin(t * 0.001);

				char pixel = ' ';

				float dist = sqrt(x * x + y * y);
				int color = (int)(1.0f / dist);
		
        // определение яркости символа
				if (color < 0)
					color = 0;
				else if (color > gradientSize)
					color = gradientSize;
				pixel = gradient[color];
				screen[i + j * width] = pixel;
			}
		}
		cout << screen;
	}
}

Получим такую вот картину:

image-loader.svg

Теперь поменяем градиент ».:!/r (l1Z4H9W8$@» на градиент »  .:!/r00    », чтобы убрать задний фон, и посмотрим что из этого выйдет:

image-loader.svg

Код на данном этапе:

#include 
using namespace std;
int main()
{
	// некое разрешение консол (количество символов, которое помещается в консоли)
	int width = 120;
	int heigh = 30;
	// массив символов выводимых на экран
	char* screen = new char[width * heigh + 1]; // +1 - это дополнительный нулевой символ,
	screen[width * heigh] = '\0';               // который существует для остановки вывода 
	                                            // символов массива

	float aspect = (float)width / heigh; // переменная, которая хранит соотношение сторон нашей консоли
	float pixelAspect = 11.0f / 24.0f;   // перменная, которая хранит соотношение сторон символа

	//char gradient[] = ".:!/r(l1Z4H9W8$@"; // градиент из символов
	char gradient[] = "  .:!/r00    "; // градиент: "  !/r(l1        "
	int gradientSize = size(gradient) - 5;

	int moving = 50000; // переменная отвечающая за продолжительность перемещения нашего шара
	for (int t = 0; t < moving; t++)
	{
		for (int i = 0; i < width; i++)
		{
			for (int j = 0; j < heigh; j++)
			{
				float x = (float)i / width * 2.0f - 1.0f;
				float y = (float)j / heigh * 2.0f - 1.0f;
				//x *= aspect; // координаты по ширине (по Ох) домножаем на переменную, в которой хранится
				//             // соотношение сторон консоли
				x = x * aspect * pixelAspect; // домножим так же на соотношение сторон символа

				x = x + sin(t * 0.001);

				char pixel = ' ';

				float dist = sqrt(x * x + y * y);
				int color = (int)(1.0f / dist);
				// определение яркости символа
				if (color < 0)
					color = 0;
				else if (color > gradientSize)
					color = gradientSize;
				pixel = gradient[color];
				screen[i + j * width] = pixel;
			}
		}
		cout << screen;
	}
}

А теперь перейдём к самому пончику:

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

if (color < 0)
	color = 0;
else if (color > gradientSize)
	color = gradientSize;

На

float clamp(float value, float min, float max)
{
	return fmax(fmin(value, max), min); // т.е. мы берём максимум (fmax) от минимума (fmin) и минимум от максимума (fmax) 
}

Таким образом мы ограничиваем значение с обоих сторон: минимальным значением и максимальным. Получаем:

/*if (color < 0)
		color = 0;
	else if (color > gradientSize)
		color = gradientSize;*/
// меняем систему из if и else на функцию clamp
color = clamp(color, 0, gradientSize);

Теперь перепишем наши координаты, заменив

float x = (float)i / width * 2.0f - 1.0f; 
float y = (float)j / heigh * 2.0f - 1.0f; 

На 

vec2 uv = vec2(i, j) / vec2(width, heigh) * 2.0f - 1.0f; 

На двумерную структуру по двум координатам, заключив vec2 в структуру:

struct vec2
{
	float x, y;
	
	vec2(float value) : x(value), y(value) {}
	vec2(float _x, float _y): x(_x), y(_y) {}

	vec2 operator+(vec2 const& other) { 
		return vec2(x + other.x, y + other.y); 
	}
	vec2 operator-(vec2 const& other) {
		return vec2(x - other.x, y - other.y);
	}
	vec2 operator*(vec2 const& other) {
		return vec2(x * other.x, y * other.y);
	}
	vec2 operator/(vec2 const& other) {
		return vec2(x / other.x, y / other.y);
	}
};

Подробнее о структурах тут.

Также создадим структуру для 3 координат:

struct vec3
{
	float x, y, z;
	vec3(float _value) : x(_value), y(_value), z(_value) {};
	vec3(float _x, vec2 const& v) : x(_x), y(v.x), z(v.y) {};
	vec3(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {};
	vec3 operator+(vec3 const& other) 
  { 
    return vec3(x + other.x, y + other.y, z + other.z); 
  }
	vec3 operator-(vec3 const& other) 
  { 
    return vec3(x - other.x, y - other.y, z - other.z);
  }
	vec3 operator*(vec3 const& other) 
  { 
    return vec3(x * other.x, y * other.y, z * other.z); 
  }
	vec3 operator/(vec3 const& other) 
  { 
    return vec3(x / other.x, y / other.y, z / other.z); 
  }
	vec3 operator-() 
  { 
    return vec3(-x, -y, -z); 
  }
};

И допишем внутри цикла:

vec3 ro = vec3(-5, 0, 0);
vec3 rd = vec3(1, uv);

Теперь добавим библиотеку math.h, которая отвечает за неэлементарную математику в С++: #include И допишем функции для работы с векторами:

double sign(double a) { return (0 < a) - (a < 0); }
double step(double edge, double x) { return x > edge; }
float length(vec2 const& v) { return sqrt(v.x * v.x + v.y * v.y); }
float length(vec3 const& v) { return sqrt(v.x * v.x + v.y * v.y + v.z * v.z); }
vec3 norm(vec3 v) { return v / length(v); }
float dot(vec3 const& a, vec3 const& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
vec3 abs(vec3 const& v) { return vec3(fabs(v.x), fabs(v.y), fabs(v.z)); }
vec3 sign(vec3 const& v) { return vec3(sign(v.x), sign(v.y), sign(v.z)); }
vec3 step(vec3 const& edge, vec3 v) { return vec3(step(edge.x, v.x), step(edge.y, v.y), step(edge.z, v.z)); }
vec3 reflect(vec3 rd, vec3 n) { return rd - n * (2 * dot(n, rd)); }
vec3 rotateX(vec3 a, double angle)
{
	vec3 b = a;
	b.z = a.z * cos(angle) - a.y * sin(angle);
	b.y = a.z * sin(angle) + a.y * cos(angle);
	return b;
}
vec3 rotateY(vec3 a, double angle)
{
	vec3 b = a;
	b.x = a.x * cos(angle) - a.z * sin(angle);
	b.z = a.x * sin(angle) + a.z * cos(angle);
	return b;
}
vec3 rotateZ(vec3 a, double angle)
{
	vec3 b = a;
	b.x = a.x * cos(angle) - a.y * sin(angle);
	b.y = a.x * sin(angle) + a.y * cos(angle);
	return b;
}
vec2 sphere(vec3 ro, vec3 rd, float r) {
	float b = dot(ro, rd);
	float c = dot(ro, ro) - r * r;
	float h = b * b - c;
	if (h < 0.0) return vec2(-1.0);
	h = sqrt(h);
	return vec2(-b - h, -b + h);
}
vec2 box(vec3 ro, vec3 rd, vec3 boxSize, vec3& outNormal) {
	vec3 m = vec3(1.0) / rd;
	vec3 n = m * ro;
	vec3 k = abs(m) * boxSize;
	vec3 t1 = -n - k;
	vec3 t2 = -n + k;
	float tN = fmax(fmax(t1.x, t1.y), t1.z);
	float tF = fmin(fmin(t2.x, t2.y), t2.z);
	if (tN > tF || tF < 0.0) return vec2(-1.0);
	vec3 yzx = vec3(t1.y, t1.z, t1.x);
	vec3 zxy = vec3(t1.z, t1.x, t1.y);
	outNormal = -sign(rd) * step(yzx, t1) * step(zxy, t1);
	return vec2(tN, tF);
}
float plane(vec3 ro, vec3 rd, vec3 p, float w) {	
return -(dot(ro, p) + w) / dot(rd, p);
}

Теперь нормализуем наш вектор:

vec3 rd = vec3(1, uv);

С помощью функции norm() и получим:

 vec3 rd = norm(vec3(1, uv));

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

vec2 uv = vec2(i, j) / vec2(width, heigh) * 2.0f - 1.0f;
				
vec3 ro = vec3(-5, 0, 0);
vec3 rd = norm(vec3(1, uv));

uv.x = uv.x * aspect * pixelAspect;
uv.x = uv.x + sin(t * 0.001);

char pixel = ' ';
int color = 0;

vec2 intersection = sphere(ro, rd, 1);
if (intersection.x > 0)
		color = 10;
color = clamp(color, 0, gradientSize); 
pixel = gradient[color];
screen[i + j * width] = pixel;

image-loader.svg

Приблизим сферу поменяв значение в строке

vec3 ro = vec3(-5, 0, 0)

На

vec3 ro = vec3(-2, 0, 0)

image-loader.svg

А теперь перепишем вновь нашу функцию main:

int main()
{
	int width = 120;
	int heigh = 30;     
	float aspect = (float)width / heigh;
	float pixelAspect = 11.0f / 24.0f; 
	char gradient[] = "  .:!/r00    ";
	int gradientSize = size(gradient) - 2;
	char* screen = new char[width * heigh + 1];
	screen[width * heigh] = '\0';
	for (int t = 0; t < 10000; t++)
	{
		vec3  light = norm(vec3(sin(t*0.001), cos(t*0.001), -1.0));
		for (int i = 0; i < width; i++)
		{
			for (int j = 0; j < heigh; j++)
			{
				vec2 uv = vec2(i, j) / vec2(width, heigh) * 2.0f - 1.0f;
				vec3 ro = vec3(-2, 0, 0);
				vec3 rd = norm(vec3(1, uv));
				uv.x = uv.x * aspect * pixelAspect;
				uv.x = uv.x + sin(t * 0.001);
				char pixel = ' ';
				int color = 0;
				vec2 intersection = sphere(ro, rd, 1);
				if (intersection.x > 0)
				{
					vec3 itPoint = ro + rd * intersection.x;
					vec3 n = norm(itPoint);
					float diff = dot(n, light);
					color = (int) (diff * 20);
				}
                  		color = clamp(color, 0, gradientSize); 
				pixel = gradient[color];
				screen[i + j * width] = pixel;
			}
		}
		cout << screen;
	}
}

Снова поменяем градиент, чтобы получить сферу сменим

char gradient[] = "  .:!/r00    "

на

char gradient[] = " .:!/r(l1Z4H9W8$@"

и получим такой результат:

image-loader.svg

Если же сменим градиент обратно, то и получим:

image-loader.svg

Код:

#include 
#include 
using namespace std;

float clamp(float value, float min, float max)
{
	return fmax(fmin(value, max), min); // т.е. мы берём максимум (fmax) от минимума (fmin) и минимум от максимума (fmax) 
}

struct vec2
{
	float x, y;
	vec2(float value) : x(value), y(value) {}
	vec2(float _x, float _y): x(_x), y(_y) {}
	vec2 operator+(vec2 const& other) {		return vec2(x + other.x, y + other.y);	}
	vec2 operator-(vec2 const& other) {		return vec2(x - other.x, y - other.y);	}
	vec2 operator*(vec2 const& other) {		return vec2(x * other.x, y * other.y);	}
	vec2 operator/(vec2 const& other) {		return vec2(x / other.x, y / other.y);	}
};
struct vec3
{
	float x, y, z;

	vec3(float _value) : x(_value), y(_value), z(_value) {};
	vec3(float _x, vec2 const& v) : x(_x), y(v.x), z(v.y) {};
	vec3(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {};

	vec3 operator+(vec3 const& other) { return vec3(x + other.x, y + other.y, z + other.z); }
	vec3 operator-(vec3 const& other) { return vec3(x - other.x, y - other.y, z - other.z); }
	vec3 operator*(vec3 const& other) { return vec3(x * other.x, y * other.y, z * other.z); }
	vec3 operator/(vec3 const& other) { return vec3(x / other.x, y / other.y, z / other.z); }
	vec3 operator-() { return vec3(-x, -y, -z); }

};

double sign(double a) { return (0 < a) - (a < 0); }
double step(double edge, double x) { return x > edge; }
float length(vec2 const& v) { return sqrt(v.x * v.x + v.y * v.y); }
float length(vec3 const& v) { return sqrt(v.x * v.x + v.y * v.y + v.z * v.z); }
vec3 norm(vec3 v) { return v / length(v); }
float dot(vec3 const& a, vec3 const& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
vec3 abs(vec3 const& v) { return vec3(fabs(v.x), fabs(v.y), fabs(v.z)); }
vec3 sign(vec3 const& v) { return vec3(sign(v.x), sign(v.y), sign(v.z)); }
vec3 step(vec3 const& edge, vec3 v) { return vec3(step(edge.x, v.x), step(edge.y, v.y), step(edge.z, v.z)); }
vec3 reflect(vec3 rd, vec3 n) { return rd - n * (2 * dot(n, rd)); }
vec3 rotateX(vec3 a, double angle)
{
	vec3 b = a;
	b.z = a.z * cos(angle) - a.y * sin(angle);
	b.y = a.z * sin(angle) + a.y * cos(angle);
	return b;
}
vec3 rotateY(vec3 a, double angle)
{
	vec3 b = a;
	b.x = a.x * cos(angle) - a.z * sin(angle);
	b.z = a.x * sin(angle) + a.z * cos(angle);
	return b;
}
vec3 rotateZ(vec3 a, double angle)
{
	vec3 b = a;
	b.x = a.x * cos(angle) - a.y * sin(angle);
	b.y = a.x * sin(angle) + a.y * cos(angle);
	return b;
}
vec2 sphere(vec3 ro, vec3 rd, float r) {
	float b = dot(ro, rd);
	float c = dot(ro, ro) - r * r;
	float h = b * b - c;
	if (h < 0.0) return vec2(-1.0);
	h = sqrt(h);
	return vec2(-b - h, -b + h);
}
vec2 box(vec3 ro, vec3 rd, vec3 boxSize, vec3& outNormal) {
	vec3 m = vec3(1.0) / rd;
	vec3 n = m * ro;
	vec3 k = abs(m) * boxSize;
	vec3 t1 = -n - k;
	vec3 t2 = -n + k;
	float tN = fmax(fmax(t1.x, t1.y), t1.z);
	float tF = fmin(fmin(t2.x, t2.y), t2.z);
	if (tN > tF || tF < 0.0) return vec2(-1.0);
	vec3 yzx = vec3(t1.y, t1.z, t1.x);
	vec3 zxy = vec3(t1.z, t1.x, t1.y);
	outNormal = -sign(rd) * step(yzx, t1) * step(zxy, t1);
	return vec2(tN, tF);
}
float plane(vec3 ro, vec3 rd, vec3 p, float w) {	return -(dot(ro, p) + w) / dot(rd, p);	}

int main()
{
	int width = 120;
	int heigh = 30;     
	float aspect = (float)width / heigh;
	float pixelAspect = 11.0f / 24.0f; 
	char gradient[] = " .:!/r(l1Z4H     "; // " .:!/r(l1Z4H9W8$@"
	int gradientSize = size(gradient) - 2;

	char* screen = new char[width * heigh + 1];
	screen[width * heigh] = '\0';
	for (int t = 0; t < 100000; t++)
	{
		vec3  light = norm(vec3(sin(t*0.001), cos(t*0.001), -1.0));
		for (int i = 0; i < width; i++)
		{
			for (int j = 0; j < heigh; j++)
			{
				vec2 uv = vec2(i, j) / vec2(width, heigh) * 2.0f - 1.0f;
				
				vec3 ro = vec3(-2, 0, 0);
				vec3 rd = norm(vec3(1, uv));

				uv.x = uv.x * aspect * pixelAspect;
				uv.x = uv.x + sin(t * 0.001);

				char pixel = ' ';
				int color = 0;

				vec2 intersection = sphere(ro, rd, 1);

				if (intersection.x > 0)
				{
					vec3 itPoint = ro + rd * intersection.x;
					vec3 n = norm(itPoint);
					float diff = dot(n, light);
					color = (int) (diff * 20);
				}
         		color = clamp(color, 0, gradientSize); 
				pixel = gradient[color];
				screen[i + j * width] = pixel;
			}
		}
		cout << screen;
	}
}

И немного поменяв код, дописав к нашему коду функцию:

float getDisk(vec3 p, float t)
{
	vec2 q = vec2(length(vec2(p.x, p.y)) - 1.0, p.z);
	return length(q) - 0.5;
}

image-loader.svg

Мы получили наше любимое блюдо «пончик». Всё готово, и как говорят итальянцы «buon appetito», что по русски «приятного аппетита».

Post Scriptum: Данная статья была подготовлена в рамках мастер-класса «Сделай пончик с помощью кода» и является методическим материалом для самостоятельного изучения информации в рамках мастер-класса. Так же хотел бы вырозить благодарность источникам, которыми я вдохновлялся и пользовался в процессе написания статьи и подготовки к мастер-классу: сайт metanit, а также youtube-каналом Onigiri.

© Habrahabr.ru