Рисуем мультяшный взрыв за 180 строчек голого C++

  • Tutorial
Неделю назад я опубликовал очередную главу из моего курса лекций по компьютерной графике; сегодня опять возвращаемся к трассировке лучей, но на сей раз пойдём самую чуточку дальше отрисовки тривиальных сфер. Фотореалистичность мне не нужна, для мультяшных целей подобный взрыв, как мне кажется, сойдёт.

Как всегда, в нашем распоряжении только голый компилятор, никаких сторонних библитек использовать нельзя. Я не хочу заморачиваться с оконными менеджерами, обработкой мыши/клавиатуры и тому подобным. Результатом работы нашей программы будет простая картинка, сохранённая на диск. Я совершенно не гонюсь за скоростью/оптимизацией, моя цель — показать основные принципы.

Итого, как в таких условиях нарисовать вот такую картинку за 180 строчек кода?



Давайте я даже анимированную гифку вставлю (шесть метров):



А теперь разобьём всю задачу на несколько этапов:

Этап первый: прочитать предыдущую статью


Да, именно так. Самым первым делом нужно прочитать предыдущую главу, которая рассказывает об основах трассировки лучей. Она совсем короткая, в принципе, всякие отражения-преломления можно не читать, но хотя бы до рассеянного освещения дочитать всё же рекомендую. Код достаточно простой, народ его даже на микроконтроллерах запускает:



Этап второй: рисуем одну сферу


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



Код смотреть здесь, но давайте я приведу основной прямо в тексте статьи:

#define _USE_MATH_DEFINES
#include <cmath>
#include <algorithm>
#include <limits>
#include <iostream>
#include <fstream>
#include <vector>
#include "geometry.h"

const float sphere_radius   = 1.5;

float signed_distance(const Vec3f &p) {
    return p.norm() - sphere_radius;
}

bool sphere_trace(const Vec3f &orig, const Vec3f &dir, Vec3f &pos) {
    pos = orig;
    for (size_t i=0; i<128; i++) {
        float d = signed_distance(pos);
        if (d < 0) return true;
        pos = pos + dir*std::max(d*0.1f, .01f);
    }
    return false;
}

int main() {
    const int   width    = 640;
    const int   height   = 480;
    const float fov      = M_PI/3.;
    std::vector<Vec3f> framebuffer(width*height);

#pragma omp parallel for
    for (size_t j = 0; j<height; j++) { // actual rendering loop
        for (size_t i = 0; i<width; i++) {
            float dir_x =  (i + 0.5) -  width/2.;
            float dir_y = -(j + 0.5) + height/2.;    // this flips the image at the same time
            float dir_z = -height/(2.*tan(fov/2.));
            Vec3f hit;
            if (sphere_trace(Vec3f(0, 0, 3), Vec3f(dir_x, dir_y, dir_z).normalize(), hit)) { // the camera is placed to (0,0,3) and it looks along the -z axis
                framebuffer[i+j*width] = Vec3f(1, 1, 1);
            } else {
                framebuffer[i+j*width] = Vec3f(0.2, 0.7, 0.8); // background color
            }
        }
    }

    std::ofstream ofs("./out.ppm", std::ios::binary); // save the framebuffer to file
    ofs << "P6\n" << width << " " << height << "\n255\n";
    for (size_t i = 0; i < height*width; ++i) {
        for (size_t j = 0; j<3; j++) {
            ofs << (char)(std::max(0, std::min(255, static_cast<int>(255*framebuffer[i][j]))));
        }
    }
    ofs.close();

    return 0;
}

Класс векторов живёт в файле geometry.h, описывать я его здесь не буду: во-первых, там всё тривиально, простое манипулирование двух и трёхмерными векторами (сложение, вычитание, присваивание, умножение на скаляр, скалярное произвдение), а во-вторых, gbg его уже подробно описал в рамках курса лекций по компьютерной графике.

Картинку я сохраняю в формате ppm; это самый простой способ сохранения изображений, хотя и не всегда самый удобный для дальнейшего просматривания.

Итак, в функции main() у меня два цикла: второй цикл просто сохраняет картинку на диск, а первый цикл — проходит по всем пикселям картинки, испускает луч из камеры через этот пиксель, и смотрит, не пересекается ли этот луч с нашей сферой.

Внимание, основная идея статьи: если в прошлой статье мы аналитически считали пересечение луча и сферы, то сейчас я его считаю численно. Идея простая: сфера имеет уравнение вида x^2 + y^2 + z^2 — r^2 = 0; но вообще функция f(x,y,z) = x^2 + y^2 + z^2 — r^2 определена во всём пространстве. Внутри сферы функция f(x,y,z) будет иметь отрицательные значения, а снаружи сферы положительные. То есть, функция f(x,y,z) задаёт для точки (x,y,z) расстояние (со знаком!) до нашей сферы. Поэтому мы просто будем скользить вдоль луча до тех пор, пока либо нам не надоест, либо функция f(x,y,z) станет отрицательной. Функция sphere_trace() именно это и делает.

Этап третий: примитивное освещение


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



Как и в прошлой статье, для простоты чтения я сделал один этап = один коммит. Изменения можно смотреть тут.

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

Vec3f distance_field_normal(const Vec3f &pos) {
    const float eps = 0.1;
    float d = signed_distance(pos);
    float nx = signed_distance(pos + Vec3f(eps, 0, 0)) - d;
    float ny = signed_distance(pos + Vec3f(0, eps, 0)) - d;
    float nz = signed_distance(pos + Vec3f(0, 0, eps)) - d;
    return Vec3f(nx, ny, nz).normalize();
}

В принципе, конечно, поскольку мы рисуем сферу, то нормаль можно получить гораздо проще, но я сделал так с заделом на будущее.

Этап четвёртый: давайте нарисуем паттерн на нашей сфере


А давайте нарисум какой-нибудь паттерн на нашей сфере, например, вот такой:



Для этого в предыдущем коде я изменил всего две строчки!

Как я это сделал? Разумеется, у меня нет никаких текстур. Я просто взял функцию g(x,y,z) = sin(x) * sin(y) * sin(z); она опять же определена во всём пространстве. Когда мой луч пересекает сферу в какой-то точке, то значение функции g(x,y,z) в этой точке мне задаёт цвет пикселя.

Кстати, обратите внимание на концентрические круги по сфере — это артефакты моего численного подсчёта пересечения.

Этап пятый: displacement mapping


Для чего я захотел нарисовать этот паттерн? А он мне поможет нарисовать вот такого ёжика:



Там, где мой паттерн давал чёрный цвет, я хочу продавить ямку на нашей сфере, а где он был белым, там наоборот, вытянуть горбик.

Чтобы это сделать, достаточно изменить три строчки в нашем коде:

float signed_distance(const Vec3f &p) {
    Vec3f s = Vec3f(p).normalize(sphere_radius);
    float displacement = sin(16*s.x)*sin(16*s.y)*sin(16*s.z)*noise_amplitude;
    return p.norm() - (sphere_radius + displacement);
}

То есть, я изменил расчёт расстояния до нашей поверхности, определив его как x^2+y^2+z^2 — r^2 — sin(x)*sin(y)*sin(z). По факту, мы определили неявную функцию.

Этап шестой: очередная неявная функция


А почему это я оцениваю произведение синусов только для точек, лежащих на поверхности нашей сферы? Давайте переопределим нашу неявную функцию вот так:

float signed_distance(const Vec3f &p) {
    float displacement = sin(16*p.x)*sin(16*p.y)*sin(16*p.z)*noise_amplitude;
    return p.norm() - (sphere_radius + displacement);
}

Разница с предыдущим кодом совсем маленькая, лучше посмотреть дифф. Вот что получится в итоге:



Таким образом мы можем определять несвязные компоненты в нашем объекте!

Этап седьмой: псевдослучайный шум


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



Как генерировать подобный шум — немного оффтоп, но вот основная идея: нужно сгенерировать случайных картинок с разными разрешениями, сгладить их, чтобы получить примерно такой набор:



А потом просто их просуммировать:



Подробнее прочитать можно здесь и здесь.

Давайте добавим немного кода, генерирующего этот шум, и получим такую картинку:



Обратите внимание, что в коде рендеринга я не изменил вообще ничего, изменилась только функция, которая «мнёт» нашу сферу.

Этап восьмой, финальный: добавляем цвет


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

Vec3f palette_fire(const float d) {
    const Vec3f   yellow(1.7, 1.3, 1.0); // note that the color is "hot", i.e. has components >1
    const Vec3f   orange(1.0, 0.6, 0.0);
    const Vec3f      red(1.0, 0.0, 0.0);
    const Vec3f darkgray(0.2, 0.2, 0.2);
    const Vec3f     gray(0.4, 0.4, 0.4);

    float x = std::max(0.f, std::min(1.f, d));
    if (x<.25f)
        return lerp(gray, darkgray, x*4.f);
    else if (x<.5f)
        return lerp(darkgray, red, x*4.f-1.f);
    else if (x<.75f)
        return lerp(red, orange, x*4.f-2.f);
    return lerp(orange, yellow, x*4.f-3.f);
}

Это простой линейный градиент между пятью ключевыми цветами. Ну а вот картинка!



Заключение


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

Комментарии 58

    +6
    Вот именно такая графика была в Героях Меча и Магии III
      +6
      За исключением того, что там в игре рендеринг был не трёхмерный :)
        +1

        И вообще там был православный пиксель арт)

          +10
          Короче, там все было совершенно по другому, но, вцелом, REPISOT прав))
            +2
            Как-то так image
              +5

              Откуда скрин?..

                0
                Герои Магии и Меча 3. До сих пор в них играю.
                  0
                  Спрашиваю, потому что не в курсе, как вообще получить такое состояние экрана в оригинальной игре. Это отдельный режим, или какая-то модификация, или отдельная игра на тех же ресурсах?
                    +1
                    Это абсолютно точно не оригинальная игра, и не hota. Больше всего тут даже не вид смущает, а количество стеков, их не более 7 должно быть. Могу предположить что это неофициальное дополнение WOG с очень кастомными настройками.
                      0
                      Больше всего смущает количество героев, как верно заметил комментатор ниже. В WoG я тоже подобного не видел, да и к тому же, как это реализовать в обычной игре, учитывая, что герои путешествуют в одиночку?
                  +3
                  Но тут же 2+2 армии на одном экране. Что за модификация?
                    0
                    Я подозреваю, что человек просто нашел скрин в гугле и сам не знает. Если сделать поиск по картинке, то можно узнать, что это "HeroesLand, или Герои меча и магии 3 онлайн"

      +14
      С интересом читаю ваши статьи. Особенно про трассировку лучей.
      Спасибо Вам за Вашу работу. Жду следующих Ваших публикаций.
        +1
        С интересом читаю ваши статьи. Особенно про трассировку лучей.
        Спасибо Вам за Вашу работу. Жду следующих Ваших публикаций.
        +2
        Спасибо!
        Вот бы такое про «звуковую оптику». Посмотрел на всякий случай, да зачитался:)
        Жаль у вас про звук только совсем немного было про робота.
          +2
          Толи я плохо ищу, толи в русскоязычном сегменте не хватает подобных материалов.
            +3
            Проще изучить английский, чем обойтись материалами доступными на русском.
            И это наверное не плохо — если все будут пользоваться одним языком, информация для всех будет доступна максимально целостной. Хотя это, конечно, не значит, что не нужно производить контент на иных языках в принципе.
              0
              Проблема скорее в удобстве понимания, так как при написании материалов на русском будут пытаться адаптировать под русскоговорящего человека, потому что не всегда ему понятно то, то написал американец. Сам иногда сталкиваюсь с таким, вроде и понимаешь о чём автор, но всё же не до конца, а если бы было такое же или пусть даже немного хуже, но на русском, то, с большой вероятностью, автор русской версии объяснил бы понятнее.
                0
                Особенно такие вещи заметны тогда, когда только начинаешь изучать что-то, так как нужно часто разбираться в мелких аспектах, которые, к тому же, чертовски важны.
                  0
                  Если у вас есть под рукой, то я хочу пример.
                    0
                    К сожалению, с ходу не могу дать пример, так как уже давненько не сталкивался, но помню, что были такие проблемы и неудобств они доставили нормально.
                      0
                      Жаль, потому что мой опыт ровно обратный, и мне интересно сравнить.
            +2
            Похожий пример на шейдерах.
              0
                +2
                Даже не знаю что и добавить)) Учитывая что я «долбанный» верстальщик, для меня это нечто грандиозное!)) Всегда мечтал работать с графикой.
                  +4
                  Не бывает долбаных верстальщиков. Бывают люди, которым интересно ковырять что-то помимо их основной работы/учёбы, и люди, которым неинтересно.
                    +1
                    Я не коим образом не хотел обделить верстальщиков, но всё же по факту вёрстка сайтов дело куда более проще чем графика, 3d или разработка тех же игр.
                    А свободное время можно уделять на другие занятия отличные от коддинга — спорт, творчество и тд.
                      +2

                      Оно все кажется сложным, если наблюдать, а не щупать.

                        +1
                        Ну начнём с того, что на этом сайте вряд ли много людей, которые рисуют или бегают, и при этом совсем не занимаются техническими хобби. А если продолжать, то технические хобби повышают уровень подготовки, что напрямую сказывается на карьерном росте.
                    0
                    Вот меня интересует вопрос, а можно ли материал одной текстурой задавать? Создать, допустим, 2-3 сотни различных материалов и каждому свой цвет определить. На каждый цвет/материал свои шейдеры, ну или местами один и тот же с разными переменными. А то сейчас по 6 текстур на объект в играх делают, никакой памяти не хватит.
                      +2
                      Так а ведь шесть текстур на объект — это и есть материал, заданный в текстуре (ну, если не считать карт нормалей всяких). Мы же задаём материал вообще каждой точке нашего объекта. Тут у меня капелька пота, здесь сажей испачкано, и т.п.

                      1) Я сильно сомневаюсь, что будет какой-то выигрыш в создании отдельной библиотеки материалов, которая покрывает весь игровой мир.

                      2) Ну аж как художники взвоют (и совершенно справедливо)! Сделать нормальный интерфейс работы с этой билиотекой просто нереально.
                        0
                        отдельной библиотеки материалов, которая покрывает весь игровой мир


                        Есть и такой подход ru.wikipedia.org/wiki/Мегатекстура
                          0
                          На самом деле, мегатекстуры по факту ничем не отличаются от использования обычных мелких текстур. Вопрос просто упаковки этого дела в памяти.
                        0
                        Если правильно понял про «цвет=материал», то сейчас в deferred подходах и так используется условно говоря «цветовое» кодирование материалов в буфере кадра для последующей обработки. Только в конце концов все равно же понядобятся эти «шесть текстур», ведь PBR шейдинг должен откуда-то брать данные о фактуре, цвете поверхности, физических свойствах. Да и более простые модели освещения все одно захотят несколько текстур.

                        Совсем уж альтернатива — это только процедурная генерация текстур.
                        +1
                        Где то я видел похожую статью, но в упрощенном виде. Попробовал — посмотрел на взыв в 16-17 FPS и забыл, только академический интерес.
                          0
                          «Графоний», конечно, у Вас всегда отменный. Настоящая магия программирования. Всё никак не найду в себе сил допройти (на практике) хоть одну из статей.
                          Но вот в этой генерации взрыва с точки зрения реализма смущает формирование по периферии клубов дыма оторванных от источника. Нечто подобное, конечно, можно наблюдать при мощных взрывах, например на испытаниях ядерных бомб — облака конденсируются в области перед ударной волной, но там совсем иной масштаб и механизм. Есть ли методы исправления этого недостатка? Может быть, достаточно использовать растущую сферу заполненную трёхмерным шумом Перлина?
                            0
                            Да, это проблема, и её нетрудно решить, сгенерировав нужный шум. Я решать не стал, так как обычно взрывы быстрые (в отличие от моей анимации), и этих артефактов просто не будет видно.
                              0
                              Но взрывы приятно скриншотить… А то раз быстрые, то можно и двумерными анимациями обойтись тогда.
                              А вообще мне кажется должна быть какая-то техника для моделирования физически корректных взрывов, подобно тому как ray tracing неплохо моделирует распространение света до учёта квантовых и гравитационных эффектов (хотя наверняка есть и такие модификации).
                            0
                            Вообще статья отличная, но в некоторых местах (особенно в либе) качество кода оставляет желать лучшего, что к сожалению затрудняет понимание
                              +2
                              Жду пулл реквест!
                              +1
                              Порт на c#: tinykaboom in c#. Считает немного долговато (порядка 30 сек на моей машине).
                              Параметр времени в анимации — это радиус увеличивающейся ограничивающей сферы?

                                0
                                Отлично! А теперь надо добавить фоновую (сферическую) картинку и сделать центр взрыва прозрачным.

                                Вы про какой параметр времени, я не очень понял?
                                  0
                                  Ну, чтобы кадры получать. Каждый кадр анимации — это ведь расчёт с каким-то значением параметра. Что мы тут изменяем, чтобы происходило движение во времени?
                                    0
                                    Я изменял радиус сферы и размер шума, т.к. он абсолютный.
                                +1
                                Не совсем ясно со сферической картинкой. Это картинка размером 2 π × π. Я должен «вырезать» из неё сегмент, который пробегает луч, формируя изображение?
                                  0
                                  Да, у нас есть картинка размером 2 π × π. Если у вас луч промахнулся мимо сцены, то сейчас вы просто рисуете пиксель постоянного цвета. А ведь ничто не мешает продолжить луч и пересечь его со сферой, например, радиуса 1000, которая описывает всю сцену. Получить точку (x,y,z) пересечения, превратить её в долготу и широту (φ, θ), которые сами живут в [0, 2π] ×[0, π], и вместо постоянного взять цвет из картинки.

                                  Сразу скажу про маленькую тонкость, я регулярно забываю, что у нас верх — это игрек, а не зед. Надо не ошибиться в формуле сферических координат :)
                                    +2
                                    Я добавил фон при помощи магических констант, взяв исходные известные углы для «окна» и наложив их на сферическую картинку. Я думал, что не важно какой имеет радиус внешняя сфера, т.к. с его изменением область проекции нашего изображения будет постоянной относительно всей площади сферы. Так?

                                    А прозрачность пока не знаю как делать.

                                      0
                                      Да, радиус абсолютно неважен, главное, чтобы он был больше сцены. А прозрачность очень просто: на данный момент вы считаете фоновый пиксель тогда, когда не пересеклись со взрывом, так? А теперь и тогда, когда пересеклись со взрывом, тоже считайте цвет фонового пикселя. И складывайте их цвета: для начала просто к цвету взрыва добавляйте цвет фона, помноженный на коэффициент прозрачности, который просто равен уровню шума в данном пикселе. Так вы получите серые части взрыва непрозрачными, и красно-жёлтые прозрачными.
                                        +2
                                        Что-то вроде этого получается:

                                        framebuffer[ i + j * width ] = ( framebuffer[ i + j * width ] * noise + palette_fire( noise ) ) * lightIntensity;


                                          0
                                          Ну вообще красота! Теперь добавить ещё объектов (для начала просто пару взрывов), потом просто шахматную доску снизу, и чтобы не-взрывы освещались бы в зависмости от расстояния до центра взрыва. Потом можно будет добавить тени от непрозрачных частей. В общем, простор для фантазии широчайший, и всё стоит десяток строк кода…

                                          Кстати, а что изменилось, что фон стал другим по сравнению с предыдущей картинкой?
                                            0
                                            Это центр сферической картинки, в отличие от предыдущего варианта. У меня в коде есть коэффициенты масштабирования (магические константы). Я их слегка меняю, чтобы по вертикали и горизонтали масштаб совпадал более менее.
                                              0
                                              Я не очень понял про магические константы. Скажите, ваш код сильно отличается от вот этого (см. последние семь строчек моего комментария)?
                                                0
                                                Ну, вообще, отличается. Я не использую значения высоты и ширины сферической картинки в расчётах, т.к. считаю в радианах. Ширина — 2 π, а — высота — π. Далее нужно посчитать углы для нашего окна и соотнести их с размерами в радианах, так получим коэффициенты масштабирования по направлениям. Заполняя буфер фоном мы берём цвета из сферического изображения с учётом этих коэффициентов. Я всё это делал интуитивно, поэтому получилось с точностью до каких-то множителей (нужно дополнительно посидеть, поразбираться). Начало отсчёта взял от центра сферической картинки.
                                                На последней картинке итак видно, что она не совсем соответствует проекции, которая должна быть, но мне вид с Солнцем понравился. А должен быть угол π / 3 по высоте (fov).
                                  +2
                                  Красота какая! Спасибо!
                                    +2
                                    Методом гугления и издевательств над кодом реймаршинга в Lua, получил вот такую картинку:image
                                    Теперь хотелось бы допилить этот код до рендеринга сцены в равноугольную панораму. Осталось только код для камеры нагуглить.
                                      0
                                      Картинка отличная, выкладывайте код!
                                      И давайте ссылку на определение нужной вам проекции.
                                        0
                                        Код сцены
                                        --$Name:Рейтрейсер$
                                        --$Info:порт рейтрейсера с pico-8$
                                        --$Author:Kerbal$
                                        --$Version:0.1$
                                        
                                        require "sprite"
                                        require "timer"
                                        require "click"
                                        
                                        local sqrt = math.sqrt
                                        local flr = math.floor
                                        local min = math.min
                                        local max = math.max
                                        local rnd = math.random
                                        local abs = math.abs
                                        
                                        local cam_shift_x = 0.1
                                        local cam_shift_y = 0.1
                                        
                                        local r_g_b = {
                                        [0] = {0,0,0},
                                        	{29, 43, 83},
                                        	{126, 37, 83},
                                        	{0, 135, 81},
                                        	{171, 82, 54},
                                        	{95, 87, 79},
                                        	{194, 195, 199},
                                        	{255, 241, 232},
                                        	{255, 0, 77},
                                        	{255, 163, 0},
                                        	{255, 236, 39},
                                        	{0, 228, 54},
                                        	{41, 173, 255},
                                        	{131, 118, 156},
                                        	{255, 119, 168},
                                        	{255, 204, 170},
                                        	{199, 240, 216}, --NOKIA SCREEN
                                        	{67, 82, 61}, --NOKIA INK 
                                        	{0,0,0},
                                        	{0,0,0},
                                        	{0,0,0},
                                        }
                                        --[[
                                        
                                        
                                        local colorGradients = {
                                          sky = {16, 16, 16, 16},
                                          green = {17, 16, 16, 17},
                                          red = {17, 16, 17},
                                        }
                                        ]]
                                        
                                        
                                        local colorGradients = {
                                          red = {1, 2, 8, 9, 10, 7},
                                          green = {0, 1, 5, 3, 11},
                                          sky = {15, 12, 12, 1},
                                          nokia1 = {16, 16, 16, 16},
                                          nokia2 = {17, 16, 16, 17},
                                          nokia3 = {17, 16, 17},
                                        }
                                        --math
                                        --local function length(x, y, z) return sqrt(x*x + y*y + z*z) end
                                        local function length(x, y, z)
                                          local y = y or 0
                                          local z = z or 0
                                          return (x*x + y*y + z*z)^0.5
                                        end
                                        
                                        local function norm(x, y, z)
                                          local y = y or 0
                                          local z = z or 0
                                          local l = length(x,y,z)
                                          return x/l, y/l, z/l
                                        end
                                        
                                        local function dot(xa, ya, za, xb, yb, zb)
                                          local za = za or 0
                                          local zb = zb or 0
                                          return xa*xb + ya*yb + za*zb
                                        end
                                        
                                        local function dot2(xa, ya, xb, yb)
                                          return xa*xb + ya*yb
                                        end
                                        
                                        --globals
                                        local ex, ey, ez = 0, 1, -1.5 --camera position
                                        local fov = 45 --camera FOV
                                        local tmin, tmax = .1, 200 --minimum and maximum distance from camera
                                        local maxSteps = 100 --maximum number of steps to take
                                        local lx, ly, lz = norm(.2, .5, -.6) --light direction
                                        
                                        --distance field functions
                                        
                                        ----------Shapes
                                        
                                        local function sdCircle(x, y, r) 
                                          return length(x, y) - r
                                        end
                                        
                                        local function sdBox(x, y, w, h)
                                            local n, m = abs(x)-w, abs(y)-h
                                            return length(max(max(n,0),max(m,0))) + min(max(n,m),0.0);
                                        end
                                        
                                        ----------Shapes
                                        
                                        ----------Primitives
                                        
                                        local function plane(x, y, z) return y end
                                        
                                        local function cube(x, y, z, side)
                                          return max(abs(x), abs(y), abs(z)) - side / 2
                                        end
                                        
                                        local function box(x, y, z, a, b, c)
                                          local l, n, k = abs(x) - a,  abs(y) - b, abs(z) - c
                                          return length(max(l, n, k, 0.0)) + min(max(l,max(n, k)),0.0)
                                        end
                                        
                                        --[[
                                        local function rbox(x, y, z, a, b, c, r)
                                          local l, n, k = abs(x) - a - r,  abs(y) - b - r, abs(z) - c - r
                                          return length(max(l, n, k, 0.0)) + min(max(l,max(n, k)),0.0)
                                        end
                                        ]]
                                        
                                        local function sphere(x, y, z, radius)
                                          local manhattanDistance = x + y + z
                                          --this if is to avoid errors due to pico's limited number range
                                          if manhattanDistance > radius * 2 then return manhattanDistance end
                                          return length(x, y, z) - radius
                                        end
                                        
                                        local function cone(x, y, z, h, r)
                                          local h, r = norm(h, r)
                                          local q = length(x, z)
                                          return dot2(h, r, q, y)
                                        end
                                        
                                        local function torus(x, y, z, rring, r)
                                          return (((x^2 + y^2)^0.5 - rring)^2 + z^2)^0.5 - r
                                        end
                                        
                                        local function torus2(x, y, z, rring, r)
                                          return (((y^2 + z^2)^0.5 - rring)^2 + x^2)^0.5 - r
                                        end
                                        
                                        local function capped_cylinder(x,y,z, h, r)
                                          local a, b = abs(length(y,z)) - h, abs(x) - r
                                          return min(max(a, b), 0.0) + length(max(a, b, 0.0))
                                        end
                                        
                                        local function sdTriPrism(x, y, z, w, h)
                                          local a,b,c = abs(x), abs(y), abs(z)
                                          return max(c - h, max(a * 0.866025 + y * 0.5, -y) - w * 0.5)
                                        end
                                        
                                        ----------Union, Subtraction, Intersection
                                        
                                        local function op_union (a, b)
                                          return min(a, b)
                                        end
                                        
                                        local function op_substraction (a, b)
                                          return max(-a, b)
                                        end
                                        
                                        local function op_intersection (a, b)
                                          return max(a, b)
                                        end
                                        
                                        local function cutbox(x, y, z, h)
                                          return max(-sphere(x, y, z, h*.7), box(x, y, z, h))
                                        end
                                        
                                        local function cutsphere(x, y, z, h)
                                          return max(sphere(x, y, z, h*.75), box(x, y, z, h))
                                        end
                                        
                                        --union combines two distance functions
                                        local function union(a, am, b, bm) if a < b then return a, am else return b, bm end end
                                        
                                        
                                        --scene defines the total distance function
                                        local function scene(x, y, z)
                                          local d, m = tmax, 0 --max distance is skyk
                                          d, m = union(d, m, plane(x, y, z), "green")
                                        --  d, m = union(d, m, cube(x-2, y-2, z-1, 1), "green")
                                          d, m = union(d, m, box(x-2, y-2, z-1, .1, .5, .5), "green")
                                          d, m = union(d, m, sdBox(x, z - 3, .2, .2), "green")
                                          d, m = union(d, m, sdBox(x, z, .2, .2), "green")
                                          d, m = union(d, m, sdBox(x - 3, z - 3, .2, .2), "green")
                                          d, m = union(d, m, sdBox(x - 3, z, .2, .2), "green")
                                          d, m = union(d, m, sdCircle(x - 3, y, .1), "red")
                                          d, m = union(d, m, sdCircle(y, z, .1), "red")
                                        --  d, m = union(d, m, cone(x-4, y-2, z, 0.01, 0.01), "green")
                                          d, m = union(d, m, torus( z-1, x-2, y-1, 1.5, .1), "green")
                                        --  d, m = union(d, m, torus2(x-3, y-1, z-2, 2, .1), "green")
                                          d, m = union(d, m, sphere(x, y-1, z, .5), "red")
                                          d, m = union(d, m, sdTriPrism(y-2, x-2, z, .3, .2), "red")
                                          d, m = union(d, m, sphere(x, y-.5, z, .3), "red")
                                          d, m = union(d, m, capped_cylinder(x-2, y-1, z, .2, .2), "red" )
                                        --  d, m = union(d, m, cutbox(x-2, y-1, z-1, 1), "red" )
                                          d, m = union(d, m, op_substraction(sphere(x-2, y-1, z-1, 0.7), box(x-2, y-1, z-1, .5, .5, .5)), "red" )
                                        --  d, m = union(d, m, cutsphere(x-2, y-1, z-.75, 1), "cnob" )
                                          d, m = union(d, m, sphere(x, y-1.5, z, .3), "red")
                                          d, m = union(d, m, sphere(x-1, y-1, z-1, .3), "green")
                                        return d,m
                                        end
                                        --calculates the normal at xyz
                                        local function sceneNormal(x, y, z)
                                          local eps = 0.1
                                          local xa, xb = scene(x+eps, y, z), scene(x-eps ,y ,z)
                                          local ya, yb = scene(x, y+eps, z), scene(x, y-eps ,z)
                                          local za ,zb = scene(x, y, z+eps), scene(x, y, z-eps)
                                          return norm(xa-xb, ya-yb, za-zb)
                                        end
                                        
                                        --rendering
                                        
                                        --returns smooth color based on position and gradient
                                        local function dither(x, y, gradient, value)
                                          local whole = flr(value * #gradient)
                                          local fraction = value * #gradient - whole
                                          local low = gradient[min(whole + 1, #gradient)]
                                          local high = gradient[min(min(whole + 1, #gradient) + 1, #gradient)]
                                          if fraction < 1/7 then return low end
                                          if fraction < 2/7 then if (x+1)%3==0 and y%3==0 then return high else return low end end
                                          if fraction < 3/7 then if x%2==0 and y%2==0 then return high else return low end end
                                          if fraction < 4/7 then if (x%2==0 and y%2==0) or (x%2~=0 and y%2~=0) then return high else return low end end
                                          if fraction < 5/7 then if x%2==0 and (y+1)%2==0 then return low else return high end end
                                          if fraction < 6/7 then if (x+1)%3==0 and y%3==0 then return low else return high end end
                                          return high
                                        end
                                        local function getShadowPoint(t, x, y, z) return x + lx * t, y + ly * t, z + lz * t end
                                        --computes the shoft shadow value
                                        local function shadow(x,y,z)
                                          --t starts at 0.2 so the shadow ray doesn't intersect
                                          --the object it's trying to shadow
                                          local res, t, distance, sx, sy, sz = 1, 0.2, 0, 0, 0, 0
                                          for i = 1, 6 do
                                            sx, sy, sz = getShadowPoint(t, x, y, z)
                                            distance, _ = scene(sx, sy, sz) --we don't care about the color
                                            res = min(res, 2 * distance / t) --increase 2 to get sharper shadows
                                            t = t + min(max(distance, .02), .2)
                                            if distance < .05 or t > 10.0 then break end
                                          end
                                          return min(max(res, 0), 1)
                                        end
                                        --calculates the final lighting and color
                                        local function render(x, y, t, tx, ty, tz, rx, ry, rz, color)
                                          local nx, ny, nz = sceneNormal(tx, ty, tz)
                                          local light = 0
                                          light = light + min(max(dot(nx, ny, nz, lx, ly, lz), 0), 1) --sun light
                                          light = light * shadow(tx,ty,tz) --shadow color
                                          light = min(max(light, 0), 1) --clamp final light value
                                          return dither(x, y, colorGradients[color], light)
                                        end
                                        --calculates the sky color
                                        local function sky(x, y, rx, ry, rz)
                                          local altitude = (min(max(ry, 0), 1) ^ 1.5)
                                          return dither(x, y, colorGradients.sky, altitude)
                                        end
                                        
                                        local function nokia1(x, y, rx, ry, rz)
                                          local altitude = (min(max(ry, 0), 1) ^ 1.5)
                                          return dither(x, y, colorGradients.nokia1, altitude)
                                        end
                                        
                                        
                                        --tracing
                                        --this is the heart of a ray tracer
                                        --the for loop pushes the test point forward until
                                        --it finds a surface that is close enough to render
                                        --the forward direction is based off the xy of the screen and fov
                                        local function getRayDirection(x, y) return norm(x / 64 - 1, (128 - y) / 64 - 1, 90 / fov) end
                                        local function getTestPoint(t, rx, ry, rz) return ex + rx * t, ey + ry * t, ez + rz * t end
                                        function trace(x,y)
                                          local rx, ry, rz = getRayDirection(x, y)
                                          local tx, ty, tz = 0, 0, 0
                                          local t, distance, color = 0, 0, 0
                                          for i = 1, maxSteps do
                                            tx, ty, tz = getTestPoint(t, rx, ry, rz)
                                            distance, color = scene(tx, ty, tz)
                                            --the test point is close enough, render
                                            if distance < .05 then return render(x, y, t, tx, ty, tz, rx, ry, rz, color) end
                                            --the test point is too far, give up, draw the sky
                                            if distance >= tmax then break end
                                            --move forward by some fraction
                                            t = t + distance * .7
                                          end
                                          return sky(x, y, rx, ry, rz)
                                        --  return nokia1(x, y, rx, ry, rz)
                                        end
                                        
                                        --just here to get pico to work the way it should be defaut tbh
                                        
                                        --function _init() cls() end
                                        --function _update() end
                                        
                                        --pick random points to trace, but only if they
                                        --havent been traced before
                                        --cache the expensive trace function, and set pixel
                                        --local traced = {}
                                        function _draw()
                                          for y = 1, 512 do
                                        	for x = 1, 512 do
                                          
                                        --    local x, y = flr(rnd(128)), flr(rnd(128))
                                            local i = x + y * 255
                                         --   if not traced[i] then
                                            
                                        --      pxl:val(x, y, i, j, t)
                                        --      print ("x ".."y "..trace(x,y))
                                        		local k = trace(x,y)
                                        --[[		print (r_g_b[k])
                                        		print (trace(x,y))
                                        		print (r_g_b[k][2])
                                        		print (r_g_b[k][3] or "nope")
                                        ]]
                                        		pxl:val(x,y, r_g_b[k][1], r_g_b[k][2], r_g_b[k][3])
                                         --     pset(x, y, trace(x,y))
                                        
                                              --traced[i] = true
                                        --	  end
                                            end
                                          end
                                        end
                                        
                                        sprite.direct(true)
                                        
                                        declare 'pxl' (false)
                                        
                                        function game:timer()
                                        --[[
                                           local x, y, i, j
                                           t = t + f
                                           v = v + r
                                           if t < 1 then f = 1 end
                                           if t > 254 then f = -1 end
                                           if v < 1 then r = 1 end
                                           if v > 254 then r = -1 end
                                           --   i = (x*y^0.5+t)%512
                                           for x = 0, 255 do
                                               for y = 0, 255 do
                                                  i = (x^2-y^2+t^1.3)%255
                                                  j = (x^2-y^2+v^1.3)%255
                                        --          pxl:val(x, y, i, j, t)
                                                  pxl:val(x, y, j, t, v)
                                               end
                                           end
                                        ]]  
                                          -- print (colorGradients["red"][3])
                                          -- table.insert(colorGradients["red"],colorGradients["red"][1])
                                          -- table.remove(colorGradients["red"],1)
                                           _draw() 
                                           pxl:draw_spr(sprite.scr())
                                           if ex > 1 or ex < -1 then cam_shift_x = cam_shift_x * -1 end
                                           if ey > 2 or ey < 0.3 then cam_shift_y = cam_shift_y * -1 end
                                           ex = ex + cam_shift_x
                                           ey = ey + cam_shift_y
                                        --   fps = (1000/(instead.ticks() - old_ticks))
                                        --   print (fps)
                                        --   old_ticks = instead.ticks()
                                        end
                                        
                                        function start(load)
                                           pxl = pixels.new(512, 512)
                                           timer:set(10)
                                        end
                                        
                                        

                                        Для запуска кода используется движок Instead.
                                        Код необходимо сохранить в файл с именем main3.lua
                                        instead-hub.github.io

                                        wiki.panotools.org/Equirectangular_Projection
                                        en.wikipedia.org/wiki/Equirectangular_projection

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                    Самое читаемое