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

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

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

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

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

Для начала разберёмся с #include <iostream> - это инициализация заголовочного файла 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

  • и т.д.

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

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

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

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

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

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

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

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

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

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

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

#include <iostream> // инициализация библиотеки
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
}

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

Немного видоизменив наши циклы 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; 
		}
	}

Результат:

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

float aspect = (float) width / heigh;

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

x = x * aspect;

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

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

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

float pixelAspect = 11.0f / 24.0f;

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

x = x * aspect * pixelAspect;

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

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

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;
	}

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

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

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

#include <iostream>
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;
	}
}

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

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

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

#include <iostream>
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 <math.h> И допишем функции для работы с векторами:

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;

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

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

На

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

А теперь перепишем вновь нашу функцию 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$@"

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

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

Код:

#include <iostream>
#include <math.h>
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;
}

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

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

Комментарии (5)


  1. Ivan_Gudoshnikov
    07.02.2022 04:48
    +4

    ASCII анимации... Верните мой 1997ой!


  1. cadovvl
    07.02.2022 12:52

    Прикольно.


  1. AlMed
    09.02.2022 12:01

    Для вывода пончика код отсутствует, также неясно что это за функция size():

    int gradientSize = size(gradient) - 2;


  1. COKPOWEHEU
    09.02.2022 13:46

    Не хватает переводов строки в конце каждой строки и возврата каретки в левый верхний угол. А то разрешение экрана 120х30 символов немножко нестандартное.
    Зачем экранный буфер выделяется в динамической памяти? Его размер ведь заранее известен и не слишком велик. Кстати, раз уж выделяете, не забывайте удалять за собой.
    Задержка для анимации здесь, похоже, идет на времени выполнения кода. Но ведь usleep никто не отменял.
    Завершение работы по количеству итераций, но ведь есть сигналы чтобы завершалось (корректно, с освобождением памяти!) по ^C.
    Про size() уже писали. Исправьте на sizeof().
    Ну и самый большой косяк — в статье много кода, но мало объяснений алгоритма.


  1. Battary
    09.02.2022 13:52
    +2

    Если честно, я ожидал тут увидеть историю как программист скопил капитал, ушел из программирования и осуществив свою мечту открыл собственную кондитерскую, в которой успешно применяет свой опыт из программирования. Кликбейт засчитан :)