Кондитерская программиста. Bon Appetit
Всем привет, в этой статье пойдёт речь о любопытных экспериментах с С++ и 3D графикой. Будем открывать свою собственную кондитерскую-программиста. Bon Appetit!
Для начала давайте немного пробежимся по структуре программ на С++.
Как только мы с Вами создадим проект на С++ в среде 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
и т.д.
Совсем забыл, для написания кода, нам ещё понадобится понимания термина «массив». Итак, массив — это область памяти, где могут последовательно храниться несколько значений. Массив можно представить в виде здания, где в каждой квартире живут «данные»:
А также нам понадобится циклы, мы будем использовать цикл 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
}
И вот, что мы получаем на выходе:
Немного видоизменив наши циклы 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
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
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;
Приблизим сферу поменяв значение в строке
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
#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;
}
Мы получили наше любимое блюдо «пончик». Всё готово, и как говорят итальянцы «buon appetito», что по русски «приятного аппетита».
Post Scriptum: Данная статья была подготовлена в рамках мастер-класса «Сделай пончик с помощью кода» и является методическим материалом для самостоятельного изучения информации в рамках мастер-класса. Так же хотел бы вырозить благодарность источникам, которыми я вдохновлялся и пользовался в процессе написания статьи и подготовки к мастер-классу: сайт metanit, а также youtube-каналом Onigiri.