Русский
Русский
English
Статистика
Реклама

Glsl

Перевод Визуализация списка женщин-лауреатов Нобелевской премии в виде кристаллов в 3d с использованием Vue, WebGL, three.js

13.06.2020 22:23:27 | Автор: admin
image

Год 1 | вдохновение


В этом месяце я очень долго пыталась определиться с датасетом и идеей для его обработки. Хотя я начала думать о нем еще в мае, по факту законить удалось только через 8 месяцев (черт, я плоха), а описать проект мне удалось еще спустя месяц (оу, я чертовски плоха).

Идея проекта пришла ко мне после просмотра фильма Безумно богатые азиаты. Мне очень понравилась актриса Мишель Йео, но идея оформилась только после того, как я прочитала больше о ней и узнала, насколько она была выдающейся и крутой. Это заставило меня задуматься выдающихся женщинах, о которых я понятия не имею. И вот возникла идея как-то это визуализировать.


1 неделя | данные


После формирования концепции, возник следующий вопрос как собрать необходимые данные? Я быстро пришла к идее использовать Википедию, попытаться спарсить лучших женщин оттуда. Я понятия не имела, как определить лучших (просмотры страницы? длина страницы? ссылки на страницу?). Хотя я посмотрела статью на The Pudding про визуализацию на основе данных википедии, я все еще не была уверена, что хочу двигаться в эту сторону. А еще мне очень не хватало свободного времени.

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

image

С этого момента я начала изучать разные методы использования API Википедии. Разобраться было непросто (я не была уверена, нужно ли мне использовать Wikidata или MediaWiki, это те варианты, которые возникали при поиске Wikipedia API в Google), но, к счастью, уже упомянутая выше статья на Pudding помогла мне закрыть эти вопросы. Быстрый просмотр их репозитория wiki-billboard-data привел меня к выбору одного из скриптов с wikijs, и я могла просто воспользоваться этим пакетом.

Все, что мне нужно было сделать, это скопировать и вставить список знатных женщин-лауреатов в электронную таблицу и выполнить небольшое форматирование/чистку.

Один из самых важных уроков, которые я узнала о сборе данных электронные таблицы отлично подходят для их очистки. Раньше я пыталась писать код для очистки данных, и это занимало у меня намного больше времени. Когда я собирала данные для проекта по Гамильтону, я буквально набирала запятые между каждой ячейкой в строке в моем CSV настолько я была глупа.


Затем я экспортировала электронную таблицу как CSV, потом использовала онлайн-конвертер, чтобы получить данные в формате JSON.

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

Несмотря на то, что создание идеи заняло целую вечность, сбор данных уложился всего в несколько часов.

неделя 2 | скетч


Не буду врать, я так долго думала об этом проекте, что даже не могу вспомнить все идеи, которые у меня возникали в процессе. Все, что я помню, это то, что я просматривала номинантов премии Information is Beautiful, когда решила, что мне нужно как-то выделить те записи, которые мне действительно нравятся в моих сохранениях. Это заставило меня осознать, что я должна лучше организовать свою доску на Pinterest, так как раньше я сваливала все мои идеи и видение на одну доску. В процессе чистки, я наткнулась на эту великолепную картину с кристаллами художника Ребекки Шаперон, которую я сохранила на будущее несколько лет назад. Почти сразу я поняла, что я хочу программно воссоздать их. Ведь было бы прекрасно визуализировать этих выдающихся женщин в качестве ярких разноцветных кристаллов?

image

Возможно, у меня были другие идеи до этого момента (да, очень возможно), но я не могу их вспомнить идея с кристаллами подходила идеально.

После этого я продумала другие детали: размер кристаллов должен отражать влияние женщины количество ссылок на ее страницу в Википедии. Число граней на кристалле будет сопоставлено с количеством источников в нижней части ее страницы (поскольку она многогранна), а цвет будет определяться категорией премии. Единственное, что мне пока было непонятно, это как расположить кристаллы. Я долго думала, что мне надо просто выложить их в 2-х измерениях по x / y и заставить читателя прокручивать их.

А потом я приняла участие в семинаре Мэтта ДесЛориерса по креативному кодингу, где он преподавал canvas, three.js и WebGL. Семинар открыл мне концепт третьего измерения, и я сразу поняла, что собираюсь использовать год получения награды в качестве оси z. Далее один мой друг предложил сделать потоки-нити, связывающие тех, кто сотрудничал друг с другом таким образом, чтобы их положение тоже менялось в зависимости от этого (но в итоге у меня не было времени на реализацию этой идеи).

Вся концепция пришла ко мне так быстро и естественно, что я не сделала ни одного наброска в процессе. Я просмотрела свой блокнот, но там вообще ничего нет.

неделя 3 & 4 | кодинг


Это был отличный месяц для кодинга.

В течение последних нескольких лет я хотела создать что-то с физическим воплощением, а в прошлом году я наконец решила сделать это моей целью. Но большую часть года я не знала, что это будет. Каждый раз, когда я размышляла об этом, я просто зацикливалась на двухмерных проекциях на стенах и не знала, как использовать весь объем.

И вот однажды меня осенило (я не уверена, что послужило причиной), что я не умею думать в трехмерном физическом пространстве, как раз потому, что я постоянно работаю в плоском диджитальном формате. Если бы я смогла научиться работать с 3D в цифре, то смогла бы мыслить в объеме и в физическом мире. Я добавила three.js и WebGL вверх своего списка для изучения.

В конце октября я прошла семинар Мэтта по креативному кодингу и изучила основы three.js, введение в фрагментированные и вершинные шейдеры. Я выучила правило буравчика: используйте большой палец для оси X (увеличивается вправо), указательный палец для оси Y (увеличивается вверх, что противоположно SVG и canvas) и средний палец для оси Z (для увеличения экрана и приближения его). Я узнала, что WebGL работает не в пикселях, а в единицах (похожая ассоциация футы или метры).

Затем в ноябре я вписалась в один конкурс по WebGL. Хотя я раньше не имела с ним дела, я надеялась, что дедлайн даст мне мотивацию, необходимую для завершения проекта. Я начала 1-го декабря и поставила себе цель делать чуть-чуть каждый будний день, пока не смогу дойти до чего-то презентабельного 23-го декабря (финал конкурса).

Сперва я прочла первые две главы Руководства по программированию на WebGL, в котором рассказывалось, как настроить WebGL. Затем я повторила свои знания по three.js. После я запустила блокнот из Observable, чтобы понять минимально-необходимые настройки для рисования чего-либо в three.js (рендерер, камера и сцена, а затем вызвать renderer.render(scene, camera) для отображения ). Мне всегда нравилось понимать самые основы работы кода, так что это было очень полезно.

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

image

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

image

А потом, в конце концов, форма кристалла, который был у меня в голове.

image

Позже я поняла, что есть лучшие способы делать то, что я хотела (и на самом деле PolyhedronGeometry это довольно утомительный способ делать это), но я была очень рада возможности попрактиковаться в координатах x / y / z в WebGL.

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

image

Следующей задачей стало научиться использовать фрагментный шейдер для окрашивания кристалла. Здесь мне пришлось немного отклониться от плана, потому что я не могла понять, как использовать glslify (узловая модульная система для GLSL, на которой написаны шейдеры, которые я взяла в качестве образца). Вместо этого я начала изучать различные инструменты компоновки/сборки, чтобы в конечном итоге развернуть свой код в Интернете. В конце концов, я решила использовать Parcel (вместо Vue CLI, который я регулярно использую последние полгода), потому что он имеет встроенную поддержку как Vue, так и GLSL.

Вот такой кристалл с наложением того же паттерна шума у меня получился.

image

Результат мне не нравился, поэтому я решила, что мне нужно больше узнать о шейдерах и использовании цветов в шейдерах. Именно в этот момент я обратилась к Books of Shaders Патрисио Гонсалеса Виво и, в частности, к главе Shaping Functions. Я узнала о синусах, косинусах, полиномах и экспонентах функциях, которые могут брать число (или набор чисел) и выводить на их основе другие. Я также узнала о смешивании цветов и о том, как мы можем взять два цвета, и не только получить новый цвет посередине между этими двумя, но также смешивать цвета в формате RGB и создавать совершенно новые на их основе. Если мы объединим это с функциями формы, то сможем получить градиенты и формы:

image
Эти были получены путем смешивания синего и желтого, а также изменения значений RGB в каждой позиции с помощью степеней, синусов, абсолютных значений, шагов и сглаживаний

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

image

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


Примерно в это же время я наткнулась на демо Bloom (эффект постобработки для создания свечения), и в нем были круглые, похожие на драгоценные камни объекты, которые выглядели довольно похоже на кристаллы, которые я и хотела получить:

image

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

Поэтому я заменила PolyhedronGeometry на SphereGeometry, установила высоту на 4 и ширину на свои данные, растянула фигуру, установив вертикальный масштаб в два раза больше по горизонтали, добавила jitter (дрожание) для каждой вершины, и у меня получились гораздо более интересные формы:

image

Теперь, когда я определилась с формами, настало время вернуться к цвету. На этот раз я использовала два цвета и смешивала их с функциями формы:

image
image
Мне нравится первый вариант выглядит как картофель

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

image
Код для добавления нормалей сторон.

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

image

Затем занялась фоном. Я создала пол с помощью PlaneGeometry и случайного дрожания y-позиции каждой вершины (вдохновленной этой статьей из codrops), а также небо, создав огромную сферу вокруг сцены. Я экспериментировала с тремя различными типами источников света: полусферой, рассеянным (чтобы создать длянеба ощущение заката/восхода солнца), и направленным (чтобы отбрасывать тени от кристаллов на пол).

image

В конце я добавила звездочки, которые визуализируют всех мужчин, которые выиграли премию за тот же период времени, а также аннотации для каждого кристалла и для десятилетий. Это было сложно, потому что с тем текстом, который у меня был, использовать TextGeometry было практически бесполезно. Решение, которое я нашла после поиска в интернете, визуализировать текст в HTML5 Canvas, создать PlaneGeometry и использовать этот Canvas в качестве текстуры для заполнения PlaneGeometry. Очень интересный подход.

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

image

Вот ссылка на финальный результат на GitHub

И видео c демонстрацией результата:



Если вы хотите попробовать еще что-то или начать с более простых проектов:

Подробнее..

Пишем простой Path Tracer на старом добром GLSL

19.12.2020 16:19:22 | Автор: admin

На волне ажиотажа вокруг новых карточек от Nvidia с поддержкой RTX, я, сканируя хабр в поисках интересных статей, с удивлением обнаружил, что такая тема, как трассировка путей, здесь практически не освящена. "Так дело не пойдет" - подумал я и решил, что неплохо бы сделать что-нибудь небольшое на эту тему, да и так, чтоб другим полезно было. Тут как кстати API собственного движка нужно было протестировать, поэтому решил: запилю-ка я свой простенький path-tracer. Что же из этого вышло вы думаю уже догадались по превью к данной статье.

Немного теории

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

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

трассировка лучей от позиции наблюдателятрассировка лучей от позиции наблюдателя

Одним из первых вопросов, которым можно задаться при рассмотрении данного алгоритма: а до какого вообще момента нам нужно моделировать движение луча? С точки зрения физики, практически каждый объект, что нас окружает, отражает хоть сколько-то света. Соответственно, естественный способ моделирования пути фотона - вычисление траектории движения вплоть до тех пор, пока количество отраженного света от объекта станет настолько малым, что им можно пренебречь. Такой метод безусловно имеет место быть, однако является крайне непроизводительным, и поэтому в любом трассирующем лучи алгоритме все же приходится жертвовать физической достоверностью изображения ради получения картинки за хоть сколько-то вменяемое время. Чаще всего выбор числа отражений луча зависит от сцены - для грубых диффузных поверхностей требуется гораздо меньше итераций, нежели для зеркальных или металлических (можете вспомнить классический пример с двумя зеркалами в лифте - свет в них отражается огромное число раз, создавая эффект бесконечного туннеля, и мы должны уметь это моделировать).

различные материалы, отрисованные физически-корректным рендерингомразличные материалы, отрисованные физически-корректным рендерингом

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

Я же для своего алгоритма решил условно выделить для материала объекта следующие параметры:

  • Отражательная способность (reflectance) - какое количество и какой волны свет отражает каждый объект

  • Шероховатость поверхности (roughness) - насколько сильно лучи рассеиваются при столкновении с объектом

  • Излучение энергии (emittance) - количество и длина волны света, которую излучает объект

  • Прозрачность (transparency/opacity) - отношение пропущенного сквозь объект света к отраженному

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

Реализуем наш алгоритм на GLSL

К сожалению (или к счастью?) сегодня мы не будем делать профессиональный трассировщик путей, и ограничимся лишь базовым алгоритмом с возможностью трассировать на сцене параллелепипеды и сферы. Для них относительно легко находить пересечения c лучем, рассчитывать нормали, да и в целом такого набора примитивов уже достаточно, чтобы отрендерить классический cornell-box.

один из вариантов cornell box'a для тестирования корректности рендерингаодин из вариантов cornell box'a для тестирования корректности рендеринга

Основной алгоритм мы будем делать во фрагментном GLSL шейдере. В принципе, даже если вы не знакомы с самим языком, код будет достаточно понятным, так как во многом совпадает с языком С: в нашем распоряжении есть функции и структуры, возможность писать условные конструкции и циклы, и ко всему прочему добавляется возможность пользоваться встроенными примитивами для математических расчетов - vec2, vec3, mat3 и т.д.

Ну что же, давайте к коду! Для начала зададим наши примитивы и структуру материала:

struct Material{    vec3 emmitance;    vec3 reflectance;    float roughness;    float opacity;};struct Box{    Material material;    vec3 halfSize;    mat3 rotation;    vec3 position;};struct Sphere{    Material material;    vec3 position;    float radius;};

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

bool IntersectRaySphere(vec3 origin, vec3 direction, Sphere sphere, out float fraction, out vec3 normal){    vec3 L = origin - sphere.position;    float a = dot(direction, direction);    float b = 2.0 * dot(L, direction);    float c = dot(L, L) - sphere.radius * sphere.radius;    float D = b * b - 4 * a * c;    if (D < 0.0) return false;    float r1 = (-b - sqrt(D)) / (2.0 * a);    float r2 = (-b + sqrt(D)) / (2.0 * a);    if (r1 > 0.0)        fraction = r1;    else if (r2 > 0.0)        fraction = r2;    else        return false;    normal = normalize(direction * fraction + L);    return true;}bool IntersectRayBox(vec3 origin, vec3 direction, Box box, out float fraction, out vec3 normal){    vec3 rd = box.rotation * direction;    vec3 ro = box.rotation * (origin - box.position);    vec3 m = vec3(1.0) / rd;    vec3 s = vec3((rd.x < 0.0) ? 1.0 : -1.0,        (rd.y < 0.0) ? 1.0 : -1.0,        (rd.z < 0.0) ? 1.0 : -1.0);    vec3 t1 = m * (-ro + s * box.halfSize);    vec3 t2 = m * (-ro - s * box.halfSize);    float tN = max(max(t1.x, t1.y), t1.z);    float tF = min(min(t2.x, t2.y), t2.z);    if (tN > tF || tF < 0.0) return false;    mat3 txi = transpose(box.rotation);    if (t1.x > t1.y && t1.x > t1.z)        normal = txi[0] * s.x;    else if (t1.y > t1.z)        normal = txi[1] * s.y;    else        normal = txi[2] * s.z;    fraction = tN;    return true;}

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

Хранить все наши объекты будем в обычных массивах, благо, GLSL создавать их также позволяет. Сцена у нас небольшая, поэтому никаких оптимизаций при поиске пересечения не реализуем - просто проходим по всем объектам и вычисляем ближайшее расстояние до точки пересечения:

#define FAR_DISTANCE 1000000.0#define SPHERE_COUNT 3#define BOX_COUNT 8Sphere spheres[SPHERE_COUNT];Box boxes[BOX_COUNT];bool CastRay(vec3 rayOrigin, vec3 rayDirection, out float fraction, out vec3 normal, out Material material){    float minDistance = FAR_DISTANCE;    for (int i = 0; i < SPHERE_COUNT; i++)    {        float D;        vec3 N;        if (IntersectRaySphere(rayOrigin, rayDirection, spheres[i], D, N) && D < minDistance)        {            minDistance = D;            normal = N;            material = spheres[i].material;        }    }    for (int i = 0; i < BOX_COUNT; i++)    {        float D;        vec3 N;        if (IntersectRayBox(rayOrigin, rayDirection, boxes[i], D, N) && D < minDistance)        {            minDistance = D;            normal = N;            material = boxes[i].material;        }    }    fraction = minDistance;    return minDistance != FAR_DISTANCE;}

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

Трассировка пути

В нашей реализации каждый объект может излучать свет, отражать свет, и поглощать (случай с преломлением пока опустим). В таком случае формулу для расчета отраженного света от поверхности можно задать следующим образом: L' = E + f*L, где E - излучаемый объектом свет (emittance), f - отражаемый объектом свет (reflectance), L - свет, упавший на объект, и L' - то, что объект в итоге излучает.

И в итоге такое выражение легко представить в виде итеративного алгоритма:

// максимальное количество отражений луча#define MAX_DEPTH 8vec3 TracePath(vec3 rayOrigin, vec3 rayDirection){    vec3 L = vec3(0.0); // суммарное количество света    vec3 F = vec3(1.0); // коэффициент отражения    for (int i = 0; i < MAX_DEPTH; i++)    {        float fraction;        vec3 normal;        Material material;        bool hit = CastRay(rayOrigin, rayDirection, fraction, normal, material);        if (hit)        {            vec3 newRayOrigin = rayOrigin + fraction * rayDirection;            vec3 newRayDirection = ...            // рассчитываем, куда отразится луч            rayDirection = newRayDirection;            rayOrigin = newRayOrigin;            L += F * material.emmitance;            F *= material.reflectance;        }        else        {            // если столкновения не произошло - свет ничто не испускает            F = vec3(0.0);        }    }    // возвращаем суммарный вклад освещения    return L;}

Если бы мы писали наш код на условном C++, можно было бы напрямую получать L как результат работы рекурсивно вызываемой функции CastRay. Однако, GLSL не разрешает рекурсивные вызовы функций в любом виде, поэтому приходится развернуть наш алгоритм так, чтобы он работал итеративно. С каждым отражением мы уменьшаем коэффициент, на который умножается испускаемый или отражаемый объектом свет, и тем самым повторяем описанную выше формулу. В моей реализации потенциально каждый объект может излучать какое-то количество света, поэтому emittance учитывается при каждом столкновении. Если же луч ни с чем не сталкивается, мы считаем, что никакого света до нас не дошло. В принципе для таких случаев можно добавить выборку из карты окружения или задать "дневной свет", но после экспериментов с этим я понял, что больше всего мне нравится текущая реализация, с пустотой вокруг сцены.

Об отражениях

Теперь давайте решим следующий вопрос: а по какому же принципу луч отражается от объекта? Очевидно, что в нашем path-tracer'е это будет зависеть от нормали в точке падения и микро-рельефа поверхности. Если обратиться к реальному миру, мы увидим, что для гладких материалов (таких как отполированный металл, стекло, вода) отражение будет очень четким, так как все лучи, падающие на объект под одним углом, будут и отражаться примерно под одинаковым углом (см. specular на картинке ниже), когда как для шероховатых, неровных поверхностей мы наблюдаем очень размытые отражения, чаще всего диффузные (см. diffuse на картинке ниже), так как лучи распространяются по полусфере относительно нормали объекта. Именно этой закономерностью мы и воспользуемся, задав итоговое направление отраженного луча как D = normalize(a * R + (1 - a) * T), где a - коэффициент шероховатости/гладкости поверхности, R - идеально отраженный луч, T - луч, отраженный в случаном направлении в полусфере относительно нормали. Очевидно, что при коэффициенте a = 1 в такой формуле мы всегда будет получать идеальное отражение луча, а при a = 0, наоборот, равномерно распределенное по полусфере. При коэффициенте шероховатости, лежащем в интервале от 0 до 1, на выходе будем иметь некоторое распределение лучей, ориентированное по углу отражения, что в вполне корректно и как раз характерно для глянцевых поверхностей (см. glossy на картинке ниже).

распределение лучей для различных типов поверхностейраспределение лучей для различных типов поверхностей

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

#define PI 3.1415926535vec3 RandomSpherePoint(vec2 rand){    float cosTheta = sqrt(1.0 - rand.x);    float sinTheta = sqrt(rand.x);    float phi = 2.0 * PI * rand.y;    return vec3(        cos(phi) * sinTheta,        sin(phi) * sinTheta,        cosTheta    );}vec3 RandomHemispherePoint(vec2 rand, vec3 n){    vec3 v = RandomSpherePoint(rand);    return dot(v, n) < 0.0 ? -v : v;}

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

vec3 hemisphereDistributedDirection = RandomHemispherePoint(Random2D(), normal);vec3 randomVec = normalize(2.0 * Random3D() - 1.0);vec3 tangent = cross(randomVec, normal);vec3 bitangent = cross(normal, tangent);mat3 transform = mat3(tangent, bitangent, normal);vec3 newRayDirection = transform * hemisphereDistributedDirection;

Небольшое примечание: здесь и далее Random?D генерирует случайные числа в интервале от 0 до 1. В GLSL шейдере делать это можно разными способами. Я использую следующую функцию, генерирующую случайным шум без явных паттернов (любезно взята со StackOverflow по первому запросу):

float RandomNoise(vec2 co){    return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);}

В комбинации с такими параметрами как координатой текущего пикселя (gl_FragCoord), временем работы приложения, и еще какими-то независимыми переменными можно сгенерировать достаточно много псевдослучайных чисел. Вы также можете пойти иначе и просто передавать массив случайных чисел в шейдер, но это уже дело вкуса.

отражения с различной шероховатостьюотражения с различной шероховатостью

После всех наших модификацийкод нашей функции TracePath будет выглядеть вот так:

vec3 TracePath(vec3 rayOrigin, vec3 rayDirection){    vec3 L = vec3(0.0);    vec3 F = vec3(1.0);    for (int i = 0; i < MAX_DEPTH; i++)    {        float fraction;        vec3 normal;        Material material;        bool hit = CastRay(rayOrigin, rayDirection, fraction, normal, material);        if (hit)        {            vec3 newRayOrigin = rayOrigin + fraction * rayDirection;            vec3 hemisphereDistributedDirection = RandomHemispherePoint(Random2D(), normal);            randomVec = normalize(2.0 * Random3D() - 1.0);            vec3 tangent = cross(randomVec, normal);            vec3 bitangent = cross(normal, tangent);            mat3 transform = mat3(tangent, bitangent, normal);                        vec3 newRayDirection = transform * hemisphereDistributedDirection;                            vec3 idealReflection = reflect(rayDirection, normal);            newRayDirection = normalize(mix(newRayDirection, idealReflection, material.roughness));                        // добавим небольшое смещение к позиции отраженного луча            // константа 0.8 тут взята произвольно            // главное, чтобы луч случайно не пересекался с тем же объектом, от которого отразился            newRayOrigin += normal * 0.8;            rayDirection = newRayDirection;            rayOrigin = newRayOrigin;            L += F * material.emmitance;            F *= material.reflectance;        }        else        {            F = vec3(0.0);        }    }    return L;}

Преломление света

Давайте рассмотрим еще такой важный для нас эффект, как преломление света. Все же помнят, как соломка, находящаяся в стакане, кажется сломанной в том месте, где она пересекается с водой? Этот эффект происходит потому, что свет, переходя между двумя средами с разными свойствами, меняет свою волновую скорость. Вдаваться в подробности того, как это работает с физической точки зрения мы не будем, вспомним лишь, что если свет падаем под углом a, то угол преломления b можно посчитать по следующей несложной формуле (см. закон Снеллиуса): b = arcsin(sin(a) * n1 / n2), где n1 - показатель преломления среды, из которой пришел луч, a n2 - показатель преломления среды, в которую луч вошел. И к счастью для нас, показатели преломления уже рассчитаны для интересующих нас сред, достаточно лишь открыть википедию, или, накрайняк, учебник по физике.

Угол падения, отражения и преломленияУгол падения, отражения и преломления

Стоит заметить следующий интересный факт: sin(a) принимает значения от 0 для 1 для острых углов. Относительный показатель преломления n1 / n2 может быть любым, в том числе большим 1. Но тогда выходит, что аргумент sin(a) * n1 / n2 не всегда находится в области определения функции arcsin. Что же происходит с углом преломления? Почему наша формула не работает для такого случая, хотя с физической точки зрения ситуация вполне возможная?

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

эффект Френеляэффект Френеля

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

float FresnelSchlick(float nIn, float nOut, vec3 direction, vec3 normal){    float R0 = ((nOut - nIn) * (nOut - nIn)) / ((nOut + nIn) * (nOut + nIn));    float fresnel = R0 + (1.0 - R0) * pow((1.0 - abs(dot(direction, normal))), 5.0);    return fresnel;}

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

vec3 IdealRefract(vec3 direction, vec3 normal, float nIn, float nOut){    // проверим, находимся ли мы внутри объекта    // если да - учтем это при рассчете сред и направления луча    bool fromOutside = dot(normal, direction) < 0.0;    float ratio = fromOutside ? nOut / nIn : nIn / nOut;    vec3 refraction, reflection;    refraction = fromOutside ? refract(direction, normal, ratio) : -refract(-direction, normal, ratio);    reflection = reflect(direction, normal);    // в случае полного внутренного отражения refract вернет нам 0.0    return refraction == vec3(0.0) ? reflection : refraction;}

Чтобы учитывать эффект френеля, будем просто генерировать случайное число, и на его основе решать, преломится ли луч или отразится. Также не забываем, что у наших объектов есть параметр плотности, который также отвечает за отношение преломленного света к отраженному. Соединить оба этих условия можно по-разному, но я взял самый примитивный вариант:

bool IsRefracted(float rand, vec3 direction, vec3 normal, float opacity, float nIn, float nOut){    float fresnel = FresnelSchlick(nIn, nOut, direction, normal);    return opacity > rand && fresnel < rand;}

Теперь наконец можно склеить все вместе: добавим в нашу функцию TracePath логику, которая бы рассчитывала преломление света и при этом учитывала и шереховатость объекта - ее для полупрозрачных тел никто не отменял:

#define N_IN 0.99#define N_OUT 1.0vec3 TracePath(vec3 rayOrigin, vec3 rayDirection){    vec3 L = vec3(0.0);    vec3 F = vec3(1.0);    for (int i = 0; i < MAX_DEPTH; i++)    {        float fraction;        vec3 normal;        Material material;        bool hit = CastRay(rayOrigin, rayDirection, fraction, normal, material);        if (hit)        {            vec3 newRayOrigin = rayOrigin + fraction * rayDirection;            vec3 hemisphereDistributedDirection = RandomHemispherePoint(Random2D(), normal);            randomVec = normalize(2.0 * Random3D() - 1.0);            vec3 tangent = cross(randomVec, normal);            vec3 bitangent = cross(normal, tangent);            mat3 transform = mat3(tangent, bitangent, normal);            vec3 newRayDirection = transform * hemisphereDistributedDirection;                            // проверяем, преломится ли луч. Если да, то меняем логику рассчета итогового направления            bool refracted = IsRefracted(Random1D(), rayDirection, normal, material.opacity, N_IN, N_OUT);            if (refracted)            {                vec3 idealRefraction = IdealRefract(rayDirection, normal, N_IN, N_OUT);                newRayDirection = normalize(mix(-newRayDirection, idealRefraction, material.roughness));                newRayOrigin += normal * (dot(newRayDirection, normal) < 0.0 ? -0.8 : 0.8);            }            else            {                vec3 idealReflection = reflect(rayDirection, normal);                newRayDirection = normalize(mix(newRayDirection, idealReflection, material.roughness));                newRayOrigin += normal * 0.8;            }            rayDirection = newRayDirection;            rayOrigin = newRayOrigin;            L += F * material.emmitance;            F *= material.reflectance;        }        else        {            F = vec3(0.0);        }    }    return L;}

Для коэффициентов преломления N_IN и N_OUT я взял два очень близких числаdoo. Это не совсем физически-корректно, однако создает желаемый эффект того, что поверхности сделаны из стекла (как шар на первом скриншоте статьи). Можете смело их изменить и посмотреть, как поменяется угол преломления лучей, проходящих сквозь объект.

Давайте уже запускать лучи!

Дело осталось за малым: инициализировать нашу сцену в начале шейдера, передать внутрь все параметры камеры, и запустить лучи по направлению взгляда. Начнем с камеры: от нее нам потребуется несколько параметров: direction - направление взгляда в трехмерном пространстве. up - направление "вверх" относительно взгляда (нужен чтобы задать матрицу перевода в мировое пространство), а также fov - угол обзора камеры. Также передадим для рассчета чисто утилитарные вещи - экранную позицию обрабатываемого пикселя (от 0 до 1 по x и y) и размер окна для рассчета отношения сторон. В математику в коде тоже особо углубляться не буду - о том, как переводить из пространство экрана в пространство мира можно почитать к примеру в этой замечательной статье.

vec3 GetRayDirection(vec2 texcoord, vec2 viewportSize, float fov, vec3 direction, vec3 up){    vec2 texDiff = 0.5 * vec2(1.0 - 2.0 * texcoord.x, 2.0 * texcoord.y - 1.0);    vec2 angleDiff = texDiff * vec2(viewportSize.x / viewportSize.y, 1.0) * tan(fov * 0.5);    vec3 rayDirection = normalize(vec3(angleDiff, 1.0f));    vec3 right = normalize(cross(up, direction));    mat3 viewToWorld = mat3(        right,        up,        direction    );    return viewToWorld * rayDirection;}

Как бы ни прискорбно это было заявлять, но законы, по которым отражаются наши лучи имеют некоторую случайность, и одного семпла на пиксель нам будет мало. И даже 16 семплов на пиксель не достаточно. Но не расстраивайтесь! Давайте найдем компромисс: каждый кадр будем считать от 4 до 16 лучей, но при этом результаты кадров аккамулировать в одну текстуру. В итоге мы делаем не так много работы каждый кадр, можем летать по нашей сцене (хоть и испытывая на своих глазах ужасные шумы), а при статичной картинке качество рендера будет постепенно расти, пока не упрется в точность float'а. Преимущества такого подхода видны невооруженным взглядом:

рендер одного кадра и нескольких, сложенных вместерендер одного кадра и нескольких, сложенных вместе

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

// ray_tracing_fragment.glslin vec2 TexCoord;out vec4 OutColor;uniform vec2 uViewportSize;uniform float uFOV;uniform vec3 uDirection;uniform vec3 uUp;uniform float uSamples;void main(){    // заполняем нашу сцену объектами    InitializeScene();    vec3 direction = GetRayDirection(TexCoord, uViewportSize, uFOV, uDirection, uUp);    vec3 totalColor = vec3(0.0);    for (int i = 0; i < uSamples; i++)    {        vec3 sampleColor = TracePath(uPosition, direction);        totalColor += sampleColor;    }    vec3 outputColor = totalColor / float(uSamples);    OutColor = vec4(outputColor, 1.0);}

Аккамулируем!

Давайте закроем вопрос с тем, как мы будем отображать результат работы трассировщика. Очевидно, что если мы решили накапливать кадры в одной текстуре, то классический вариант с форматом вида RGB (по байту на каждый канал) нам не подойдет. Лучше взять что-то вроде RGB32F (проще говоря формат, поддерживающий числа с плавающей точкой одинарной точности). Таким образом мы сможем накапливать достаточно большое количество кадров прежде чем упремся в потолок из-за потерь точности вычислений.

Также сходу напишем шейдер, принимающий нашу аккамулирующую текстуру и вычисляющий среднее от множества кадров. Тут же применим тональную коррекцию изображения, а затем гамма-коррекцию (в коде я использую самый простой вариант tone-mapping'а, вы можете взять что-то посложнее, к примеру кривую Рейнгарда):

// post_process_fragment.glslin vec2 TexCoord;out vec4 OutColor;uniform sampler2D uImage;uniform int uImageSamples;void main(){    vec3 color = texture(uImage, TexCoord).rgb;    color /= float(uImageSamples);    color = color / (color + vec3(1.0));    color = pow(color, vec3(1.0 / 2.2));    OutColor = vec4(color, 1.0);}

И на этом часть с GLSL кодом можно считать оконченной. Как будет выглядеть наш трассировщий снаружи - вопрос открытый и зависит полностью от вас. Ниже я конечно же приведу реализацию рендеринга в контексте API моего движка, на котором я и писал данный трассировщик, но информация эта скорее опциональная. Думаю вам не составит труда написать связующий код на том фреймворке или графическом API, с которым лично вам удобнее всего работать:

virtual void OnUpdate() override{    // получаем текущую камеру и текстуру, в которую осуществляется рендер    auto viewport = Rendering::GetViewport();    auto output = viewport->GetRenderTexture();    // получим текущие параметры камеры (позицию, угол обзора и т.д.)    auto viewportSize = Rendering::GetViewportSize();    auto cameraPosition = MxObject::GetByComponent(*viewport).Transform.GetPosition();    auto cameraRotation = Vector2{ viewport->GetHorizontalAngle(), viewport->GetVerticalAngle() };    auto cameraDirection = viewport->GetDirection();    auto cameraUpVector = viewport->GetDirectionUp();    auto cameraFOV = viewport->GetCamera<PerspectiveCamera>().GetFOV();    // проверим, что камера неподвижна. От этого зависит, нужно ли очищать предыдущий кадр    bool accumulateImage = oldCameraPosition == cameraPosition &&                           oldCameraDirection == cameraDirection &&                           oldFOV == cameraFOV;    // при движении снизим количество семплов ради приемлемой частоты кадров    int raySamples = accumulateImage ? 16 : 4;    // установим все униформы в шейдере, осуществляющем трассировку лучей    this->rayTracingShader->SetUniformInt("uSamples", raySamples);    this->rayTracingShader->SetUniformVec2("uViewportSize", viewportSize);    this->rayTracingShader->SetUniformVec3("uPosition", cameraPosition);    this->rayTracingShader->SetUniformVec3("uDirection", cameraDirection);    this->rayTracingShader->SetUniformVec3("uUp", cameraUpVector);    this->rayTracingShader->SetUniformFloat("uFOV", Radians(cameraFOV));    // меняем тип блендинга в зависимости от того, аккамулируем ли мы кадры в одну текстуру    // также считаем количество кадров, чтобы потом получить среднее значение    if (accumulateImage)    {        Rendering::GetController().GetRenderEngine().UseBlending(BlendFactor::ONE, BlendFactor::ONE);        Rendering::GetController().RenderToTextureNoClear(this->accumulationTexture, this->rayTracingShader);        accumulationFrames++;    }    else    {        Rendering::GetController().GetRenderEngine().UseBlending(BlendFactor::ONE, BlendFactor::ZERO);        Rendering::GetController().RenderToTexture(this->accumulationTexture, this->rayTracingShader);        accumulationFrames = 1;    }    // рассчитаем среднее от множества кадров и сохраним в рендер-текстуру камеры    this->accumulationTexture->Bind(0);    this->postProcessShader->SetUniformInt("uImage", this->accumulationTexture->GetBoundId());    this->postProcessShader->SetUniformInt("uImageSamples", this->accumulationFrames);    Rendering::GetController().RenderToTexture(output, this->postProcessShader);    // обновим сохраненные параметры камеры    this->oldCameraDirection = cameraDirection;    this->oldCameraPosition = cameraPosition;    this->oldFOV = cameraFOV;}

В заключение

Ну что же, на этом все! Всем спасибо за прочтение этой небольшой статьи. Хоть мы и не успели рассмотреть множество интересных эффектов в path-tracing'е, опустили обсуждение различных ускоряющих трассировку структур данных, не сделали денойзинг, но все равно итоговый результат получился весьма сносным. Надеюсь вам понравилось и вы нашли для себя что-то новое. Я же по традиции приведу в конце несколько скриншотов, полученных с помощью path-tracer'а:

эффект бесконечного туннеля от двух параллельных зеркалэффект бесконечного туннеля от двух параллельных зеркалпреломление света при взгляде сквозь прозрачную сферупреломление света при взгляде сквозь прозрачную сферунепрямое освещение и мягкие тенинепрямое освещение и мягкие тени

Ссылки на связанные ресурсы

Подробнее..

Перевод Рендеринг каустики воды в реальном времени

30.09.2020 10:08:50 | Автор: admin
В этой статье я представлю свою попытку обобщения вычислений каустики в реальном времени с помощью WebGL и ThreeJS. Тот факт, что это попытка, важен, ведь найти решение, работающее во всех случаях и обеспечивающее 60fps сложная, если не невозможная задача. Но вы увидите, что при помощи моей методики можно достичь достаточно приличных результатов.

Что такое каустика?


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

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


В этом посте мы сосредоточимся на каустике, вызванной преломлением света, то есть на том, что обычно происходит под водой.

Чтобы добиться стабильных 60fps, нам нужно вычислять её на графической карте (GPU), поэтому мы будем вычислять каустику только шейдерами, написанными на GLSL.

Для её вычисления нам потребуется:

  • вычислить преломлённые на поверхности воды лучи (в GLSL это легко, потому что для этого существует встроенная функция)
  • вычислить при помощи алгоритма нахождения пересечений точки, в которых эти лучи сталкиваются с окружением
  • вычислить яркость каустики, проверяя точки сближения лучей



Хорошо известное демо воды на WebGL


Меня всегда поражало это демо Эвана Уоллеса, демонстрирующее визуально реалистичную каустику воды на WebGL: madebyevan.com/webgl-water


Рекомендую прочитать его статью на Medium, в которой объясняется, как вычислять каустику в реальном времени при помощи меша фронта света и функций частной производной GLSL. Его реализация чрезвычайно быстра и очень красиво выглядит, но имеет некоторые недостатки: она работает только с кубическим бассейном и сферическим мячом в бассейне. Если поместить под воду акулу, то демо не будет работать: в шейдерах жёстко прописано, что под водой находится сферический мяч.

Он поместил под воду сферу потому, что вычисление пересечения между преломлённым лучом света и сферой это лёгкая задача, в которой используется очень простая математика.

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


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

Работа с ограничениями GLSL


В написанных на GLSL (OpenGL Shading Language) шейдерах мы можем иметь доступ только к ограниченному объёму информации о сцене, например:

  • Атрибуты текущей отрисовываемой вершины (позиция: 3D-вектор, нормаль: 3D-вектор и т.п.). Мы можем передавать свои атрибуты GPU, но они должны иметь встроенный тип GLSL.
  • Uniform, то есть константы для всего текущего отрисовываемого меша в текущем кадре. Это могут быть текстуры, матрица проецирования камеры, направление освещения и т.п. Они должны иметь встроенный тип: int, float, sampler2D для текстур, vec2, vec3, vec4, mat3, mat4.

Однако нет возможности получать доступ к мешам, присутствующим в сцене.

Именно поэтому демо webgl-water можно сделать только с простой 3D-сценой. Проще вычислять пересечение преломлённого луча и очень простой фигуры, которую можно представить с помощью uniform. В случае сферы её можно задать позицией (3D-вектор) и радиусом (float), поэтому эту информацию можно передавать шейдерам с помощью uniform, а для вычисления пересечений требуется очень простая математика, легко и быстро выполняемая в шейдере.

Некоторые выполняемые в шейдерах методики трассировки лучей передают меши в текстурах, но в 2020 году при рендеринге реального времени на WebGL такое решение неприменимо. Нужно помнить, что для получения достойного результата мы должны вычислять 60 изображений в секунду с большим количеством лучей. Если мы вычисляем каустику, используя 256x256=65536 лучей, то каждую секунду нам придётся выполнять значительное количество вычислений пересечений (которое также зависит от количества мешей в сцене).

Нам нужно найти способ представить подводное окружение в виде uniform и вычислить пересечение, сохраняя при этом достаточную скорость.

Создание карты окружений


Когда требуется вычисление динамических теней, то хорошо известной техникой является shadow mapping. Она часто используется в видеоиграх, хорошо выглядит и быстро выполняется.

Shadow mapping это техника, выполняемая в два прохода:

  • Сначала 3D-сцена рендерится с точки зрения источника освещения. Эта текстура содержит не цвета фрагментов, а глубину фрагментов (расстояние между источником освещения и фрагментом). Эта текстура называется картой теней (shadow map).
  • Затем карта теней используется при рендеринге 3D-сцены. При отрисовке фрагмента на экране мы знаем, есть ли другой фрагмент между источником освещения и текущим фрагментом. Если это так, то мы знаем, что текущей фрагмент находится в тени, и нужно отрисовывать его чуть темнее.

Подробнее о shadow mapping можно прочитать в этом превосходном туториале по OpenGL: www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping.

Также можно посмотреть интерактивный пример на ThreeJS (нажмите T, чтобы отобразить в левом нижнем углу карту теней): threejs.org/examples/?q=shadowm#webgl_shadowmap.

В большинстве случаев эта методика работает хорошо. Она может работать с любыми неструктурированными мешами в сцене.

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

Вот результат создания карты окружения:


Env map: в каналах RGB хранится позиция по XYZ, в альфа-канале глубина

Как вычислить пересечение луча и окружения


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

Алгоритм работает следующим образом:

  • Этап 1: начинаем с точки пересечения между лучом света и поверхностью воды
  • Этап 2: вычисляем преломление с помощью функции refract
  • Этап 3: переходим от текущей позиции в направлении преломлённого луча по одному пикселю в текстуре карты окружения.
  • Этап 4: сравниваем зарегистрированную глубину окружения (хранящуюся в текущем пикселе текстуры окружения) с текущей глубиной. Если глубина окружения больше, чем текущая глубина, то нам нужно двигаться дальше, поэтому мы снова применяем этап 3. Если глубина окружения меньше текущей глубины, то это значит, что луч столкнулся с окружением в позиции, считанной из текстуры окружения и мы нашли пересечение с окружением.



Текущая глубина меньше, чем глубина окружения: нужно двигаться дальше


Текущая глубина больше, чем глубина окружения: мы нашли пересечение

Текстура каустики


После нахождения пересечения мы можем вычислить яркость каустики (и текстуру яркости каустики) с помощью методики, описанной Эваном Уоллесом в его статье. Получившаяся текстура выглядит примерно так:


Текстура яркости каустики (обратите внимание, что эффект каустики менее важен на акуле, потому что она ближе к поверхности воды, что снижает сходимость лучей освещения)

Эта текстура содержит информацию о яркости освещения для каждой точки в 3D-пространстве. При рендеринге готовой сцены мы можем считывать эту яркость освещения из текстуры каустики и получить следующий результат:



Реализацию этой методики можно найти в репозитории Github: github.com/martinRenou/threejs-caustics. Поставьте ей звёздочку, если вам понравилось!

Если вы хотите посмотреть на результаты вычисления каустики, то можете запустить демо: martinrenou.github.io/threejs-caustics.

Об этом алгоритме пересечения


Это решение сильно зависит от разрешения текстуры окружения. Чем больше текстура, тем лучше точность алгоритма, но тем и дольше выполняется поиск решения (прежде чем найти его, нужно считать и сравнить большее количество пикселей).

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

Более того, в WebGL запрещены циклы while (и на то есть веская причина), поэтому нам нужно реализовать алгоритм в цикле for, который может быть развёрнут компилятором. Это означает, что нам требуется условие завершения цикла, известное во время компиляции, обычно это значение максимальной итерации, которое заставляет нас прекратить поиск решения, если мы не нашли его в течение максимального количества попыток. Такое ограничение приводит к неверным результатам каустики, если преломление оказывается слишком важным.

Наша методика не так быстра, как упрощённый способ, предолженный Эваном Уоллесом, однако она гораздо более гибкая, чем подход с полномасштабной трассировкой лучей, а также может использоваться для рендеринга в реальном времени. Однако скорость по-прежнему зависит от некоторых условий направления света, яркости преломлений и разрешения текстуры окружения.

Завершаем обзор демо


В этой статье мы рассмотрели вычисление каустики воды, но в демо использовались и другие техники.

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

У нас есть ещё идеи по дальнейшему улучшению методики, в том числе:

  • Хроматические аберрации на каустике: сейчас мы применяем хроматические аберрации к поверхности воды, но этот эффект также должен быть видим на подводной каустике.
  • Рассеивание света в объёме воды.
  • Как посоветовали Мартин Жерар и Алан Волф в Twitter, мы можем повысить производительность с помощью иерархических карт окружения (которые будут использоваться как деревья квадрантов для поиска пересечений). Также они посоветовали рендерить карты окружения с точки зрения преломлённых лучей (предполагая, что они совершенно плоские), благодаря чему производительность станет независимой от угла падения освещения.

Благодарности


Эта работа по реалистичной визуализации воды в реальном времени была проведена в QuantStack и финансировалась ERDC.
Подробнее..

Membrane game шикарная игра для аутистов в 20 строк кода

11.04.2021 14:15:43 | Автор: admin

https://www.shadertoy.com/view/fs23Wt
Код игры написан целиком на языке математики. Давайте его разберем.

  1. N отвечает за размер клеток.

  2. pow(1.02, iTime) создает равномерную анимацию клеток, 1.02 - скорость анимации; рано или поздно это вызывет переполнение буфера, но анимаию можно отключить сделав t=1 или сделать так чтобы она замедлялась со временем t=iTime.

  3. dx и dy - смещение позиции курсора по x и по y.

  4. float f = float((x-dx)*(x-dx)t+(y-dy)*(y-dy)*t); // Это правила игры - в эту функцию (на самом деле не функцию) вы можете внести изменения и получить новую логику игры.

  5. float F = abs(f*sin((x)/N)*sin((y)/N)); // Создает клеточную мембрану для функции (не функции) f.

  6. int R = int(floor(F*pow(16.0, 6.0-ceil(log2(F)/4.0)))); // Адаптирует цвета мембраны добавив нули в конце шестнадцатиричной записи числа или обрезае его по правому краю. По сути это готовый индекс цвета X11, который используется в HTML или Photoshop.

Короче всё просто. Я на самом деле новичок и в математике и в GLSL, но моим друзьям понравилась эта игра, и мне интересно, как её оценит хабр. Если у вас есть идеи, вы можете форкнуть. Тех, кто захочет попробовать перенести на другие языки программирования, предупрежу, что такие вычисления для каждого пикселя весьма затруднительны для процессора - надо программировать шейдеры (ну лично мой опыт, но вообще я не против.)

Подробнее..

Recovery mode Часы и волны

12.05.2021 22:22:20 | Автор: admin

Я показывал хабру свою графику, хотя меня и заминусовали, я хочу показать снова то, над чем я работал, может кому то из большой аудитории хабра понравится анимация, графика или идея, которая уже давно лежит на поверхности. Это прототип стрелочных часов, измеряющих время в 7300000-7320000 больших размерах, может столько и не нужно, но обычным 12-часовым часам явно не хватает делений для измерения хотя бы сегодняшней даты.

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

Картинка красивая, но лучше посмотреть работу часов, и их анимацию тут; исходники прилагаются.

Прототип часов

Вернемся к прототипу часов. Тут нет цифр, но к этому быстро привыкаешь. Хочу обратить внимание на другие качественные особенности: вывод месяца и даты (зеленая дуга), визуализаия четырехзначного номера года (4 серых дуги для обозначения каждой цифры текущего года, расположенные под углом 90 градусов по отношению к друг-другу, их может быть меньше, если в номере года встречаются нули). А также первая и вторая половина (pm/am) суток явно указываются часовой срелкой (на темное или светлое поле) - это обеспечивается вращающимся циферблатом.

Я не понимаю, почему когда когда круглый цифреблат часов способен вмещать в себя день, месяц, год, - почему в массовой продаже не выпускаются такие часы, более точные? Если речь идет о стрелочных часах, то даже за большие деньги вам предложат скорее всего пару дополнительных циферблатов в лучшем случае - но если мы часы, минуты, секунды меряем по одному и тому же циферблату, почему другие единицы того же времени используют дополнительные циферблаты?

Мне нравится такая реализация, я не понимаю почему таких штук нет в продаже. Мне кажется, такие календарные часы могли бы вытеснить устаревшие версии, которые годятся к использованию только вместе с календарем.

Подробнее..

Recovery mode Частицы в Godot 3, сглаживание маленьких частиц, и система отпечатков на шейдерах

04.09.2020 10:14:53 | Автор: admin

Использование разных частиц в Godot 3. Без использования эффектов постобработки.

Ссылки:

Запустить WebGL2 и скачать другие версии ссылка на itch.io.

Исходный код на github.

Статья разбита на разделы:

  1. Сглаживание мелких частиц, разные способы.

  2. Много шаров - частиц без потери производительности. И 3d-проекция в шейдере.

  3. Система отпечатков Screen space decals.

  4. Прочее.


1. Сглаживание частиц

Использую плоский объект размером в 2 или 4 раза больше чем отображаемая графика, самая критичная графика по центру текстуры, выглядит так, слева в 2 раза больше справа в 4 раза:

И черное становится прозрачным.

Идея в том чтобы иметь хотяб 1 пиксель вокруг основной линии при даже самых маленьких углах поворота и удалении от объекта. Я использую 4-х кратный отступ на самом дальнем объекте:

Код шейдера particlelineAAbase.shader включает три версии для теста - без фильтрации, с процедурной фильтрацией используя dFd* функцию, и используя текстуру, порядок слева на право:


2. Проекция трехмерных частиц на плоскость

Использую intersection и projection логику в фрагментном шейдере.

Идея в том что - отобразить 1000 трехмерных шаров-частиц для видеокарты(GPU) стоит слишком дорого, и 1000 трехмерных шаров-частиц у меня уже нагружают GPU на 100%. Когда рендеринг 1000 кругов(не шаров) использует меньше 25% GPU.

И поворачивая плоский круг или квадрат относительно камеры можно иметь полноценный шар-частицу:

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

Исходный код шейдера шара particle_cloud_base.shader и код шейдера куба и линии.

Для шара оригинал кода взят из iquilezles.org.


3. Система отпечатков Screen space decals

Использую код шейдера из Screen-Space-Decals, мои модификации:

  1. Поворот относительно позиции, по нормали к поверхности при установке.

  2. Затухание на краях куба с dacal.

  3. Логика Material-ID для того чтобы Decals оставались только на выбранной поверхности не затрагивая другие объекты рядом.

Работает так:

Про логику Material-ID:

В Godot нет возможности записывать дополнительную информацию в основном render-pass, информацию о типе материала. Поэтому я использую отдельный Viewport в котором есть только статическая сцена:

Красный канал для material-ID синий для значения глубины(depth). Глубина(depth) используется чтобы вырезать объекты. Логика выглядит так, на скриншоте частицы и персонаж не существуют на Material-ID Viewport и вырезаются по depth:

Использование отдельного Viewport это очевидный overhead, в качестве оптимизации можно уменьшить разрешение, это можно тестировать в этой версии - в меню Debug (мышку в левый верхний угол для показа меню) и установить множитель для теста, от 0.25 до 1.


4. Прочее

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

В таком случае нужна всего одна плоскость, минимум треугольников, чтоб генерировать частицы любой длины и они будут выглядеть объемными.

В качестве оригинала такой логики я использовал этот шейдер Garden Fireworks - @P_Malin.

Шейдер для картинки загрузки - мой старый шейдер.

Анимация форм функций - моя старая демка. (старая версия так и не заработала в WebGL,по прежнему работает только в браузере на Linux)

Логика - 360 линий поворачиваются на 1 градус каждая и вращаются вокруг центральной оси, каждая линия имеет форму одной из функций. Выглядит также как в оригинальной демке:

Баги:

В ходе написания этих шейдеров нашел несколько багов в драйверах Nvidia:

Краш компилятора, баг с матрицами в Vetrex шейдере, также запуская некоторые шейдеры в Vulkan там тоже есть баг в драйвере.

Лицензия и используемые материалы:

  1. Все 3d модели взяты со sketchfab и имеют CC-non comercial лицензию.

  2. Музыка взята с сайта patrickdearteaga.com

  3. Весь мой код и все шейдеры имеют лицензию MIT license.

Ссылка на список используемых ресурсов.

Спасибо за чтение этой статьи.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru