В прошлом посте (Вся наша жизнь спираль: Синтез субатомных частиц) мы кратенько рассмотрели сложности работы с float и рендер туманностей.
Сегодня мы рассмотрим планеты и построение сцены относительно игрока.
Начнем с простого.
Свет.
Источником глобального света в космической сцене выступает звезда. Для начала нужно выбрать, какой именно тип источника мы будем использовать.
Их всего 3:
- Point Light - свет излучается из точки по некому радиусу с затуханием
- Spot Light - или прожектор. Источник излучается из точки по конусу, опять таки с затуханием
- Direction Light - самый простой источник света. Свет идет по направлению, без источника как такового.
Логично предположить, что для звезды лучше всего подойдет последний. Звезда не точка, это огромная сфера, каждая точка которой излучает свет.
Хорошо, с этим определились. Но, Direction Light светит только в одном направлении, а звезда во все сразу. Это проблема, но решается достаточно просто. Мы просто берем, и привязываем направление света на позицию игрока. Тогда получится, что свет всегда идет от звезды к игроку.
А как быть с планетами и прочими макро объектами? Ведь если игрок сместится, то также сместится освещенная часть планеты.
Тут нам помогут уже простейшие шейдера.
Первым делом исключим наши объекты из списка объектов, на которые влияет источник света (иными словами сделаем их Unlit). А дальше, возьмем позицию источника света, посчитаем вектор направления к планете и возьмем dot product от каждой нормали вершины планеты. Вуаля, мы получим маску, которая будет эмулировать этот самый DirectionLight
Вторая проблема, которая вытекает из первой - тени. Большие объекты, особенно если у них есть кольца - отбрасывают на них тень, которая
а) Привязана к источнику света
б) Имеет мерзкую привычку глючить на больших расстояниях
Снова обращаемся к шейдеру.
Достаточно большие объекты, где эти проблемы становятся заметными опять-таки планеты. Планеты имеют форму сферы, поэтому можно взять и просто спроецировать сферу на нужную нам поверхность с искажением от центра проецируемого объекта по направлению от источника света
Делается это достаточно просто (хотя тут ноды лучше не использовать)
/*
* INPUT:
* LightLocationWS Light Position in WorldSpace
* LightSourceRadius Light Radius
* ParentBodyLocationWS Parent Body (Shadow Caster) location in WorldSpace
* ParentBodyRadius Parent Body Radius
* WorldPosition AbsoluteWorldPositionWS
* VertexNormal VertexNormalWS
*/
MaterialFloat3 LL = LightLocationWS;
MaterialFloat3 PL = ParentBodyLocationWS;
MaterialFloat LSR = LightSourceRadius;
MaterialFloat PBR = ParentBodyRadius;
MaterialFloat3 rc = WorldPosition - PL;
MaterialFloat c = dot(rc, rc) - (PBR*PBR);
MaterialFloat b = dot(normalize(LL - PL), rc);
MaterialFloat d = b*b - c;
MaterialFloat t = -b - sqrt(abs(d));
MaterialFloat st = step(0, min(t,d));
MaterialFloat shadow = lerp(-1, t, st);
return shadow;
В результате получится вот такой эффект, который не зависит от расстояния и от размера объекта
Со светом разобрались, теперь перейдем непосредственно к построению сцены.
В прошлом посте я рассказывал о пересчете позиций и размеров объектов через их угловой размер. Используем это, приняв то, что игрок будет находится всегда в координате [0,0,0], и то, что нам известно его физическое положение в мире.
Но это сработает только с планетами, звездами и прочими макрообъектами, а что делать с другими игроками? Их рисуем как есть по смещению к позиции игрока - они достаточно маленькие и смысла пересчитывать их размеры нет. Однако, что будет, если другой игрок достаточно далеко, а планета, к которой мы летим, нет, и она уже видна на экране? В этом случае другой игрок может оказаться ЗА планетой, хотя по факту он должен быть перед ней.
В решении этой проблемы нам поможет инструмент, под названием Custom Depth Pass и CustomDepth Stencil Buffer (CustomDepth Stencil Write Mask и CustomDepth Stencil Value)
Это бит поле поможет нам отсортировать объекты таким образом, чтобы объекты правильно накладывались друг на друга.
Самый простой вариант (но не самый корректный) отсортировать объекты через дополнительную фазу Translucent (через приоритеты + через стенсил)
где CheckDepth это CustomNode, которая имеет следующее внутри себя
uint SceneDepth = Depth;
return SceneDepth > 1 ? 0 : 1;
SceneDepth тут это как раз таки то самое CustomDepth Stencil Value, которое можно задать свое для каждого объекта в отдельности. Если объекты пересекаются, то значения буфера складываются. Например, если у нас пересеклись объекты со значением 1 и 2, то на выходе будет 3. Тогда, в материале объекта, со значением 1, если полученное число будет больше 1, то нам нужно скрыть этот объект.
Почему именно такие цифры.
Как я уже говорил, Этот буфер является бит маской. и он представлен в виде 8ми позиций в двоичной системы счисления. Для простоты расчетов этот буфер проверяется побитого. И чтобы еще больше упростить расчет, то следует указывать только один бит для объекта.
Следовательно цифры, которые мы можем использовать принадлежат степени двойки, т.е.
1 2 4 8 16 ну итд
1, или 0b00000001 - это самый дальний объект (skycube), который должен быть скрыт, если его пересекает любой другой объект, следовательно если на него приходит любое другое значение, кроме как 1, то мы его скрываем.
Следующий объект 2 (0b00000010). Он не должен быть скрыт, когда к нему приходит значение 3 (собственное значение 2 + 1 от старкуба), но должен быть скрыт, когда приходит значение 4, ну итд.
При этом, если пересекаются два одинаковых объекта, то сокрытие одного или другого объекта будет разруливать не стенсил, но Translucency Sort Priority, который должен зависеть от дальности этих объектов.
У игроков, т.к. мы не изменяем их позиции и не пересчитываем их размер, мы укажем 0b10000000 для стенсил буфера, и таким образом они всегда будут рисоваться перед всеми остальными объектами. Сравнивать их дополнительно не нужно, т.к. если они должны быть ЗА объектом, то они уже давно вышли за расстояние, на котором их может быть видно, поэтому этот случай мы просто не рассматриваем.
Ну а теперь самое вкусное на сегодня - планеты
Всем известно, что когда нужно что-то нарисовать, то у меня лапки. Поэтому я сразу не рассматривал идею с рисованием текстур планет и сразу перешел к генерации из шума.
Нужно было учесть несколько моментов - у нас нет сцен на поверхности планет, поэтому нет смысла выделывать полноценный генератор, аля Elite. При этом все это должно смотреться интересно и +- реалистично.
Пришлось попотеть - написать свою, более гибкую функцию генерации шума, придумать, как прорисовывать детальки, и как передавать цвет на все это дело.
В результате, получилось следующее:
Планета рисуется через функцию слоев материала (часть функционала для рисования ландшафта в UE4). Задается общий шум для ландшафта, а потом отдельными слоями дорисовываются детали: каньоны, горы, итд.
Цвет планеты задается через Атлас, который содержит кривую, которая в свою очередь привязана к градации серого цвета пикселя.
Из полученного результата мы генерируем нормаль и все это применяем на модель света, описанную выше. В результате получаем нечто, похожее на это:
и в довершение всего, применим к этому SkyAtmosphere - стандартный компонент визуализации UE4 (который наконец доделали в 4.26)
Здесь осталась одна нерешенная проблема. Изначально, для наложения деталей я выбрал не верный подход через 6planar mapping. Для слоя можно выбрать одну из 6ти сторон планеты, но не более того. В ближайшее время это будет заменено на проекцию по произвольному вектору, что позволит добавить более мелкие детали, и сделать более незаметным то, что поверхность планеты на самом деле двухмерна.
И на этом пока что все. До связи до следующего раза.