Для того, чтобы получить реалистичный мир внутри игры, необходимо учитывать взаимодействие различных форм рельефа между собой и с другими моделями. И если видимые линии пересечения между 3D-моделями портят органичность изображения, стоит задуматься над тем, как их устранить. Самый распространенный случай таких линий, который может быть знаком многим, — пересечение билбордов частиц с непрозрачной геометрией.
Другой пример — нарушающие естественность композиции пересечения скал и растительности с поверхностью ландшафта в сценах «на открытом воздухе».
Помимо разнообразных методов сглаживания (SSAA, MSAA, CSAA, FXAA, NFAA, CMAA, DLAA, TAA и др.), которые пусть и смягчают вызывающий внешний вид таких линий пересечений, но не исправляют ситуацию в полной мере, существуют и более действенные приемы. Их мы и рассмотрим.
В Unity существует встроенное решение для устранения видимых пересечений между прозрачными частицами и непрозрачной геометрией, которое называется soft particles. Шейдеры, которые поддерживают этот эффект, дополнительно усиливают прозрачность частиц в зависимости от того, насколько мала разница между глубиной фрагмента частицы и глубиной непрозрачной геометрии.
Принцип работы soft particles
Очевидно, для корректной работы soft particles необходим буфер глубины. В случае deferred shading буфер глубины формируется на этапе рендеринга полноэкранных буферов, и с учетом MRT (Multiple Render Targets, а не Magnetic Resonance Tomography) его наличие не выражается в дополнительных вычислительных затратах.
В случае forward shading и использования Unity Legacy Pipeline требовался дополнительный проход для рендеринга непрозрачной геометрии в буфер глубины[1]. Этот проход активируется назначением соответствующего значения для свойства Camera.depthTextureMode. Это свойство не доступно в окне инспектора, но доступно в API[2].
Теперь же можно реализовать собственный вариант Scriptable Render Pipeline с forward shading, которая при помощи MRT одновременно сможет отрендерить и буфер глубины, и буфер цвета.
Устранение линий пересечения в шейдерах, поддерживающих soft particles
В целом нет технических препятствий для того, чтобы использовать метод блендинга по глубине для устранения видимых пересечений 3D-моделей с ландшафтом:
Однако этот подход имеет несколько недостатков.
Первый недостаток связан с производительностью. Блендинг по глубине работает на этапе блендинга аппаратного пайплайна, то есть непосредственно после растеризации и вычисления фрагментного шейдера. На этой стадии результат исполнения фрагментного шейдера смешивается с результатом, записанным в выходном буфере[3][4][5] по формуле, предварительно определенной вызовами API[6][7][8][9].
Это наименее прогрессивная часть любого аппаратного пайплайна в том смысле, что она работает точно так же, как ее предшественница работала еще двадцать лет тому назад. GPU читает значение из памяти, смешивает его со значением фрагментного шейдера и записывает обратно в память.
Также существует разница в том, использовать ли блендинг по глубине для полностью прозрачных или частично прозрачных 3D-моделей. Прозрачные ― например, билборды частиц ― даже без блендинга по глубине целиком рендерятся прозрачными. В случае же с непрозрачными 3D-моделями реальной, ощутимой, видимой прозрачностью при блендинге по глубине будет наделено лишь очень небольшое число фрагментов, подавляющая же их часть так и останется непрозрачными. Но последнее вовсе не означает, что для их отрисовки не будет использован блендинг ― просто он будет работать вхолостую.
Второй недостаток связан с тем, как именно выбирается цвет для смешивания. Если вкратце, то все фрагменты, которые смешиваются в определенном экранном пикселе, лежат на одном луче, исходящем из мировой позиции камеры и проходящем через мировую позицию этого экранного пикселя. Это, в свою очередь, значит, что при любом изменении позиции или ориентации камеры будет наблюдаться параллакс: фрагменты 3D-модели, расположенные ближе к камере, будут двигаться быстрее, чем фрагменты ландшафта, расположенные дальше от камеры[10][11]. Особенно это заметно при взгляде с близкого расстояния при постоянном латеральном смещении камеры.
Латеральный параллакс при смещении камеры: фрагменты 3D-модели смещаются на большее расстояние по сравнению с фрагментами ландшафта
Латеральный параллакс при смещении камеры: при фиксации камеры на фрагменте ландшафта становится заметно, как быстро смещаются фрагменты модели
При вращении камеры параллакс наблюдается сразу по двум осям экранных координат. Впрочем, в динамике это меньше бросается в глаза, чем латеральный параллакс.
Азимутальный параллакс при смещении камеры: здесь мозгу сложнее распознать паттерн параллакса при смещении фрагментов по двум осям
Но ощутимее всего внешний вид блендинга по глубине меняется в зависимости от угла, под которым наблюдатель смотрит на поверхность ландшафта. Зона блендинга становится практически незаметной, когда направление взгляда перпендикулярно нормали поверхности ландшафта, но размер этой зоны быстро увеличивается, если наклонить камеру вниз.
Изменение ширины зоны блендинга при наклоне камеры
Блендинг по глубине мог бы оказаться неплохим вариантом для устранения линий пересечения 3D-моделей с ландшафтом, если бы не обилие артефактов, которые его сопровождают. Этот метод больше подходит для эффектов частиц, которые не статичны и, как правило, не содержат высокодетализированных текстур, поэтому эффекты параллакса в их случае не наблюдаются.
Другой вариант реализации блендинга с ландшафтом ― использование карты высот, доступ к которой Unity предоставляет через API TerrainData[12].
Зная позицию объекта Terrain и размеры ландшафта, указанные в TerrainData, и имея «на руках» карту высот, можно вычислить высоту ландшафта в любой точке, заданной в мировых координатах.
Параметры ландшафта, необходимые для сэмплирования карты высот
Ну а теперь, после вычисления высоты ландшафта, можно вычислить в шейдере и uv-координаты для сэмплирования карты высот ландшафта по мировым координатам.
Для того, чтобы была возможность использовать один и тот же код во фрагментных и вертексных шейдерах, при сэмплировании используется функция tex2Dlod. Кроме того, карта высот не имеет мип-уровней, поэтому сэмплить ее функцией tex2D, которая автоматически вычисляет мип-уровень, в принципе бессмысленно.
Можно попытаться воспроизвести устранение пересечений через прозрачность без использования буфера глубины. Это не решает других проблем, связанных с этим методом, но дает возможность убедиться в работоспособности блендинга с помощью карты высот.
Блендинг по глубине и блендинг с картой высот. Ширина зоны блендинга отличается при одинаковых параметрах шейдера
На иллюстрациях используются идентичные параметры блендинга для обоих методов. Ширина зон блендинга визуально отличается, поскольку блендинг с картой высот не зависит от угла между направлением взгляда наблюдателя и нормалью ландшафта.
Блендинг с картой высот как минимум в одном отношении лучше блендинга по глубине: он исправляет видимую невооруженным глазом зависимость блендинга от угла, под которым камера смотрит на ландшафт. К сожалению, эффект параллакса при этом все так же будет наблюдаться.
Чтобы избавиться от параллакса, необходимо смешивать фрагмент 3D-модели с фрагментом ландшафта, который находится вертикально под ним (выборка цвета для смешивания в этом случае не зависит от позиции и ориентации камеры).
Как исправить параллакс: выбор фрагмента ландшафта для блендинга
Конечно, речь здесь идет скорее о виртуальном фрагменте ландшафта. В зависимости от положения камеры возможна ситуация, когда фрагмент ландшафта, с которым необходимо смешать фрагмент 3D-модели, даже не будет попадать в поле зрения камеры. Похожая проблема есть в методике рендеринга локальных отражений в экранном пространстве (SSLR). Заключается она в том, что невозможно отрендерить отражение фрагмента, которого нет на экране[13].
В случае с ландшафтом цвет виртуального фрагмента можно с высокой точностью реконструировать, используя вспомогательные текстуры, которые предоставляет Unity API: карту нормалей[14], карту освещенности[15], текстуры с весовыми коэффициентами для смешивания слоев[16], и текстуры, входящие в состав слоев[17].
Реконструкция фрагмента ландшафта
Все текстуры, входящие в состав ландшафта, сэмплятся по тем же UV, что и карта высот. В случае со слоями координаты для сэмплинга корректируются параметрами тайлинга, указанными для конкретного слоя[18][19].
Так, блендинг с реконструкцией фрагментов ландшафта исправляет все проблемы, характерные для блендинга по глубине и блендинга с картой высот, включая параллакс.
Блендинг с реконструкцией фрагментов ландшафта
В этом месте самое время задаться вопросом, чего же стоит подобного рода компромисс? На первый взгляд, ресурсоёмкость реконструкции фрагментов ландшафта значительно превосходит ресурсоёмкость альфа-блендинга. Для реконструкции необходимо выполнить с дюжину дополнительных операций чтения из памяти. Для альфа-блендинга же нужна всего одна операция чтения из памяти и одна операция записи в память.
В реальности всё будет зависеть от особенностей аппаратной платформы. В пользу реконструкции фрагментов выступает текстурная компрессия, мип-мэппинг, вычислительная мощность ядер GPU и специфические оптимизации аппаратного пайплайна (early depth rejection). А против альфа-блендинга сыграет уже упомянутый выше факт о том, что это наименее прогрессивная часть любого GPU.
Тем не менее, место для оптимизации всегда найдется. Например, в случае с реконструкцией цвета ландшафта необходимость в этой реконструкции есть лишь для узкой полосы фрагментов 3D-модели, расположенных не выше определенной высоты над поверхностью ландшафта.
Динамическое ветвление в шейдерах может давать слабо предсказуемые результаты по производительности, но есть два момента, которые стоит принять во внимание:
Визуализация различных степеней когерентности
В случае с реконструкцией фрагментов учитываются оба этих момента: условие ветвления в большей части случаев позволит отсечь выполнение ресурсоемких операций по реконструкции цвета ландшафта, и это условие когерентно, за исключением очень небольшого числа фрагментов (на иллюстрации это те фрагменты, что лежат на границе между «красной» и «зеленой» зонами).
Когерентность реконструкции фрагментов ландшафта
Осталось добавить несколько замечаний, касающихся этого метода блендинга:
Блендинг с реконструкцией фрагментов ландшафта
При проектировании 3D-моделей невозможно учесть разнообразие рельефов ландшафта, совместно с которым эти модели предполагается использовать. Часто 3D-модели приходится достаточно глубоко «утапливать» в ландшафте или поворачивать для того, чтобы скрыть выступающие части, или наоборот ― показать скрытые, которые должны быть видимыми. «Утапливание» моделей ограничивает их применимость, а в случае, если 3D-модели рендерятся раньше ландшафта, еще и приводит к эффекту overdraw. Поворот же, в свою очередь, тоже подходит далеко не для всех 3D-моделей (например, не для домов и деревьев).
Чтобы скрыть выступающие элементы 3D-модели, ее необходимо «утопить» в ландшафте
Снэппинг ― термин, хорошо знакомый пользователям графических редакторов. Это функция, которая позволяет контрольным точкам «прилипать» к узлам пространственной сетки, а в 3D-редакторах ― к граням и поверхностям других объектов. Снэппинг к карте высот ландшафта в вертексном шейдере способен значительно упростить проектирование сцен.
3D-модель без снэппинга. 3D-модель с повертексным снэппингом. 3D-модель с повертексным снэппингом и блендингом. 3D-модель с повертексным снэппингом, блендингом и статическим освещением
Основная сложность в реализации снэппинга состоит в том, что необходимо разобраться, какие вертексы 3D-модели нужно снэппить к карте высот, а какие не стоит. Вертексы содержат лишь информацию о локальном характере поверхности (которой недостаточно) и не содержат никакой информации о ее топологии (которая необходима).
Как и в других прикладных случаях, эту проблему проще всего решить на этапе моделирования, напрямую внедрив необходимые параметры в вертексы. В качестве такого параметра стоит выбрать интуитивно понятный атрибут ― например, весовой коэффициент для снэппинга (а не дистанцию до границы незамкнутой поверхности, как хотелось бы для гибкости).
Кодирование весовых коэффициентов для снэппинга
Применимость повертексного снэппинга ограничена общим соответствием поверхности ландшафта и поверхности 3D-модели. Чтобы компенсировать значительные их расхождения, необходимо применять другие, более ресурсоемкие методы ― например, использовать 3D-модели со скиннингом.
Главная мысль, которую следует вынести из статьи: любому достаточно сложному и потенциально масштабируемому шейдеру необходимы исходные данные. И задача разработчика состоит в том, чтобы понять, каким образом можно эксплуатировать графическую систему: какие данные она предоставляет, как их можно комбинировать друг с другом и каким образом использовать в шейдерах.
В общем же случае можно сделать вывод, что единственный вариант преодолеть те рамки, которыми ограничены возможности графических эффектов, ― комбинировать результаты работы различных шейдеров.
Другой пример — нарушающие естественность композиции пересечения скал и растительности с поверхностью ландшафта в сценах «на открытом воздухе».
Помимо разнообразных методов сглаживания (SSAA, MSAA, CSAA, FXAA, NFAA, CMAA, DLAA, TAA и др.), которые пусть и смягчают вызывающий внешний вид таких линий пересечений, но не исправляют ситуацию в полной мере, существуют и более действенные приемы. Их мы и рассмотрим.
Блендинг по глубине
В Unity существует встроенное решение для устранения видимых пересечений между прозрачными частицами и непрозрачной геометрией, которое называется soft particles. Шейдеры, которые поддерживают этот эффект, дополнительно усиливают прозрачность частиц в зависимости от того, насколько мала разница между глубиной фрагмента частицы и глубиной непрозрачной геометрии.
Принцип работы soft particles
Очевидно, для корректной работы soft particles необходим буфер глубины. В случае deferred shading буфер глубины формируется на этапе рендеринга полноэкранных буферов, и с учетом MRT (Multiple Render Targets, а не Magnetic Resonance Tomography) его наличие не выражается в дополнительных вычислительных затратах.
В случае forward shading и использования Unity Legacy Pipeline требовался дополнительный проход для рендеринга непрозрачной геометрии в буфер глубины[1]. Этот проход активируется назначением соответствующего значения для свойства Camera.depthTextureMode. Это свойство не доступно в окне инспектора, но доступно в API[2].
Теперь же можно реализовать собственный вариант Scriptable Render Pipeline с forward shading, которая при помощи MRT одновременно сможет отрендерить и буфер глубины, и буфер цвета.
Устранение линий пересечения в шейдерах, поддерживающих soft particles
В целом нет технических препятствий для того, чтобы использовать метод блендинга по глубине для устранения видимых пересечений 3D-моделей с ландшафтом:
Посмотреть код
// Blending with depth buffer
#include "UnityCG.cginc"
float BlendStart;
float BlendEnd;
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
struct v2f
{
// ...
half4 projPos : TEXCOORD0;
};
v2f vert(appdata v)
{
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f,o);
// ...
o.projPos = ComputeScreenPos(o.pos);
COMPUTE_EYEDEPTH(o.projPos.z);
// ...
return o;
}
fixed4 frag(v2f i) : COLOR
{
fixed4 result = 0;
// ...
float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos));
float sceneZ = LinearEyeDepth(depth);
float partZ = i.projPos.z;
float fade = saturate( sceneZ - partZ );
result.a = smoothstep( BlendStart, BlendEnd, fade );
// ...
return result;
}
Однако этот подход имеет несколько недостатков.
Первый недостаток связан с производительностью. Блендинг по глубине работает на этапе блендинга аппаратного пайплайна, то есть непосредственно после растеризации и вычисления фрагментного шейдера. На этой стадии результат исполнения фрагментного шейдера смешивается с результатом, записанным в выходном буфере[3][4][5] по формуле, предварительно определенной вызовами API[6][7][8][9].
Это наименее прогрессивная часть любого аппаратного пайплайна в том смысле, что она работает точно так же, как ее предшественница работала еще двадцать лет тому назад. GPU читает значение из памяти, смешивает его со значением фрагментного шейдера и записывает обратно в память.
Также существует разница в том, использовать ли блендинг по глубине для полностью прозрачных или частично прозрачных 3D-моделей. Прозрачные ― например, билборды частиц ― даже без блендинга по глубине целиком рендерятся прозрачными. В случае же с непрозрачными 3D-моделями реальной, ощутимой, видимой прозрачностью при блендинге по глубине будет наделено лишь очень небольшое число фрагментов, подавляющая же их часть так и останется непрозрачными. Но последнее вовсе не означает, что для их отрисовки не будет использован блендинг ― просто он будет работать вхолостую.
Второй недостаток связан с тем, как именно выбирается цвет для смешивания. Если вкратце, то все фрагменты, которые смешиваются в определенном экранном пикселе, лежат на одном луче, исходящем из мировой позиции камеры и проходящем через мировую позицию этого экранного пикселя. Это, в свою очередь, значит, что при любом изменении позиции или ориентации камеры будет наблюдаться параллакс: фрагменты 3D-модели, расположенные ближе к камере, будут двигаться быстрее, чем фрагменты ландшафта, расположенные дальше от камеры[10][11]. Особенно это заметно при взгляде с близкого расстояния при постоянном латеральном смещении камеры.
Латеральный параллакс при смещении камеры: фрагменты 3D-модели смещаются на большее расстояние по сравнению с фрагментами ландшафта
Латеральный параллакс при смещении камеры: при фиксации камеры на фрагменте ландшафта становится заметно, как быстро смещаются фрагменты модели
При вращении камеры параллакс наблюдается сразу по двум осям экранных координат. Впрочем, в динамике это меньше бросается в глаза, чем латеральный параллакс.
Азимутальный параллакс при смещении камеры: здесь мозгу сложнее распознать паттерн параллакса при смещении фрагментов по двум осям
Но ощутимее всего внешний вид блендинга по глубине меняется в зависимости от угла, под которым наблюдатель смотрит на поверхность ландшафта. Зона блендинга становится практически незаметной, когда направление взгляда перпендикулярно нормали поверхности ландшафта, но размер этой зоны быстро увеличивается, если наклонить камеру вниз.
Изменение ширины зоны блендинга при наклоне камеры
Блендинг по глубине мог бы оказаться неплохим вариантом для устранения линий пересечения 3D-моделей с ландшафтом, если бы не обилие артефактов, которые его сопровождают. Этот метод больше подходит для эффектов частиц, которые не статичны и, как правило, не содержат высокодетализированных текстур, поэтому эффекты параллакса в их случае не наблюдаются.
Блендинг с картой высот
Другой вариант реализации блендинга с ландшафтом ― использование карты высот, доступ к которой Unity предоставляет через API TerrainData[12].
Зная позицию объекта Terrain и размеры ландшафта, указанные в TerrainData, и имея «на руках» карту высот, можно вычислить высоту ландшафта в любой точке, заданной в мировых координатах.
Параметры ландшафта, необходимые для сэмплирования карты высот
// Setting up a heightmap and uniforms to use with shaders...
Shader.SetGlobalTexture(Uniforms.TerrainHeightmap, terrain.terrainData.heightmapTexture);
Shader.SetGlobalVector(Uniforms.HeightmapScale, terrain.terrainData.heightmapScale);
Shader.SetGlobalVector(Uniforms.TerrainSize, terrain.terrainData.size);
Shader.SetGlobalVector(Uniforms.TerrainPos, terrain.transform.position);
Ну а теперь, после вычисления высоты ландшафта, можно вычислить в шейдере и uv-координаты для сэмплирования карты высот ландшафта по мировым координатам.
// Computes UV for sampling terrain heightmap...
float2 TerrainUV(float3 worldPos)
{
return (worldPos.xz - TerrainPos.xz) / TerrainSize.xz;
}
Для того, чтобы была возможность использовать один и тот же код во фрагментных и вертексных шейдерах, при сэмплировании используется функция tex2Dlod. Кроме того, карта высот не имеет мип-уровней, поэтому сэмплить ее функцией tex2D, которая автоматически вычисляет мип-уровень, в принципе бессмысленно.
// Returns the height of terrain at a given position in world space...
float TerrainHeight(float2 terrainUV)
{
float heightmapSample = tex2Dlod(TerrainHeightmap, float4(terrainUV,0,0));
return TerrainPos.y + UnpackHeightmap(heightmapSample) * HeightmapScale.y * 2;
}
Можно попытаться воспроизвести устранение пересечений через прозрачность без использования буфера глубины. Это не решает других проблем, связанных с этим методом, но дает возможность убедиться в работоспособности блендинга с помощью карты высот.
Посмотреть код
// Blending with terrain heightmap
#include "UnityCG.cginc"
float BlendStart;
float BlendEnd;
sampler2D_float TerrainHeightmap;
float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;
struct v2f
{
// ...
float3 worldPos : TEXCOORD0;
float2 heightMapUV : TEXCOORD1;
// ...
};
v2f vert(appdata v)
{
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f,o);
// ...
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.heightMapUV = TerrainUV(o.worldPos);
// ...
return o;
}
fixed4 frag(v2f i) : COLOR
{
fixed4 result = 0;
// ...
half height = TerrainHeight(i.heightMapUV);
half deltaHeight = i.worldPos.y - height;
result.a = smoothstep( BlendStart, BlendEnd, deltaHeight );
// ...
return result;
}
Блендинг по глубине и блендинг с картой высот. Ширина зоны блендинга отличается при одинаковых параметрах шейдера
На иллюстрациях используются идентичные параметры блендинга для обоих методов. Ширина зон блендинга визуально отличается, поскольку блендинг с картой высот не зависит от угла между направлением взгляда наблюдателя и нормалью ландшафта.
Блендинг с картой высот как минимум в одном отношении лучше блендинга по глубине: он исправляет видимую невооруженным глазом зависимость блендинга от угла, под которым камера смотрит на ландшафт. К сожалению, эффект параллакса при этом все так же будет наблюдаться.
Блендинг с реконструкцией фрагментов ландшафта
Чтобы избавиться от параллакса, необходимо смешивать фрагмент 3D-модели с фрагментом ландшафта, который находится вертикально под ним (выборка цвета для смешивания в этом случае не зависит от позиции и ориентации камеры).
Как исправить параллакс: выбор фрагмента ландшафта для блендинга
Конечно, речь здесь идет скорее о виртуальном фрагменте ландшафта. В зависимости от положения камеры возможна ситуация, когда фрагмент ландшафта, с которым необходимо смешать фрагмент 3D-модели, даже не будет попадать в поле зрения камеры. Похожая проблема есть в методике рендеринга локальных отражений в экранном пространстве (SSLR). Заключается она в том, что невозможно отрендерить отражение фрагмента, которого нет на экране[13].
В случае с ландшафтом цвет виртуального фрагмента можно с высокой точностью реконструировать, используя вспомогательные текстуры, которые предоставляет Unity API: карту нормалей[14], карту освещенности[15], текстуры с весовыми коэффициентами для смешивания слоев[16], и текстуры, входящие в состав слоев[17].
Реконструкция фрагмента ландшафта
Все текстуры, входящие в состав ландшафта, сэмплятся по тем же UV, что и карта высот. В случае со слоями координаты для сэмплинга корректируются параметрами тайлинга, указанными для конкретного слоя[18][19].
Посмотреть код
// Blending with reconstructed terrain fragments
#include "UnityCG.cginc"
float BlendStart;
float BlendEnd;
sampler2D_float TerrainHeightmapTexture;
sampler2D_float TerrainNormalTexture;
sampler2D TerrainAlphaMap;
float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;
Float4 TerrainLightmap_ST;
UNITY_DECLARE_TEX2D(TerrainSplatMap0);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap0);
half4 TerrainSplatMap0_ST;
UNITY_DECLARE_TEX2D(TerrainSplatMap1);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap1);
half4 TerrainSplatMap1_ST;
UNITY_DECLARE_TEX2D(TerrainSplatMap2);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap2);
half4 TerrainSplatMap2_ST;
UNITY_DECLARE_TEX2D(TerrainSplatMap3);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap3);
half4 TerrainSplatMap3_ST;
struct v2f
{
// ...
float3 worldPos : TEXCOORD0;
float2 heightMapUV : TEXCOORD1;
#if defined(LIGHTMAP_ON)
float2 modelLightMapUV : TEXCOORD2;
float2 terrainLightMapUV : TEXCOORD3;
#endif
// ...
};
v2f vert(appdata v)
{
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f,o);
// ...
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.heightMapUV = TerrainUV(o.worldPos);
#if defined(LIGHTMAP_ON)
o.modelLightMapUV = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
o.terrainLightMapUV = o.heightMapUV * TerrainLightmap_ST.xy + TerrainLightmap_ST.zw;
#endif
// ...
return o;
}
half3 TerrainNormal(float2 terrainUV)
{
return tex2Dlod( TerrainNormalTexture, float4(terrainUV,0,0) ).xyz * 2.0 - 1.0;
}
half4 TerrainSplatMap(float2 uv0, float2 uv1, float2 uv2, float2 uv3, half4 control)
{
half4 splat0 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap0, TerrainSplatMap0, uv0);
half4 splat1 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap1, TerrainSplatMap1, uv1);
half4 splat2 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap2, TerrainSplatMap2, uv2);
half4 splat3 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap3, TerrainSplatMap3, uv3);
half4 result = splat0 * control.r +
splat1 * control.g +
splat2 * control.b +
splat3 * control.a;
return result;
}
half3 TerrainNormalMap(float2 uv0, float2 uv1, float2 uv2, float2 uv3, half4 control)
{
half4 n0 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap0, TerrainSplatMap0, uv0);
half4 n1 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap1, TerrainSplatMap1, uv1);
half4 n2 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap2, TerrainSplatMap2, uv2);
half4 n3 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap3, TerrainSplatMap3, uv3);
half3 result = UnpackNormalWithScale(n0, 1.0) * control.r +
UnpackNormalWithScale(n1, 1.0) * control.g +
UnpackNormalWithScale(n2, 1.0) * control.b +
UnpackNormalWithScale(n3, 1.0) * control.a;
result.z += 1e-5;
return result;
}
half3 TerrainLightmap(float2 uv, half3 normal)
{
#if defined(LIGHTMAP_ON)
#if defined(DIRLIGHTMAP_COMBINED)
half4 lm = UNITY_SAMPLE_TEX2D(unity_Lightmap, uv);
half4 lmd = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, uv);
half3 result = DecodeLightmapRGBM(lm, unity_Lightmap_HDR);
result = DecodeDirectionalLightmap(result, lmd, normal);
#else
half4 lm = UNITY_SAMPLE_TEX2D(unity_Lightmap, uv);
half3 result = DecodeLightmapRGBM(lm, unity_Lightmap_HDR);
#endif
#else
half3 result = UNITY_LIGHTMODEL_AMBIENT.rgb;
#endif
return result;
}
fixed4 frag(v2f i) : COLOR
{
fixed4 result = 0;
// ...
// compute model color and put it to the result
// ...
// reconstruction of terrain fragment
float2 splatUV0 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap0);
float2 splatUV1 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap1);
float2 splatUV2 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap2);
float2 splatUV3 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap3);
half4 control = tex2D(_TerrainAlphaMap, i.heightMapUV);
half4 terrainColor = TerrainSplatMap(splatUV0, splatUV1, splatUV2, splatUV3, control);
half3 terrainSurfaceNormal = TerrainNormal(i.heightMapUV);
half3 terrainSurfaceTangent = cross(terrainSurfaceNormal, float3(0,0,1));
half3 terrainSurfaceBitangent = cross(terrainSurfaceTangent, terrainSurfaceNormal);
half3 terrainNormal = TerrainNormalMap(splatUV0, splatUV1, splatUV2, splatUV3, control);
terrainNormal = terrainNormal.x * terrainSurfaceTangent +
terrainNormal.y * terrainSurfaceBitangent +
terrainNormal.z * terrainSurfaceNormal;
half3 terrainLightmapColor = TerrainLightmap(i.heightMapUV, terrainNormal);
terrainColor *= terrainLightmapColor;
// blend model color & terrain color
half height = TerrainHeight(i.heightMapUV);
half deltaHeight = i.worldPos.y - height;
half blendingWeight = smoothstep(BlendStart, BlendEnd, deltaHeight);
result.rgb = lerp(result.rgb, terrainColor, blendingFactor);
return result;
}
Так, блендинг с реконструкцией фрагментов ландшафта исправляет все проблемы, характерные для блендинга по глубине и блендинга с картой высот, включая параллакс.
Блендинг с реконструкцией фрагментов ландшафта
Производительность реконструкции фрагментов ландшафта
В этом месте самое время задаться вопросом, чего же стоит подобного рода компромисс? На первый взгляд, ресурсоёмкость реконструкции фрагментов ландшафта значительно превосходит ресурсоёмкость альфа-блендинга. Для реконструкции необходимо выполнить с дюжину дополнительных операций чтения из памяти. Для альфа-блендинга же нужна всего одна операция чтения из памяти и одна операция записи в память.
В реальности всё будет зависеть от особенностей аппаратной платформы. В пользу реконструкции фрагментов выступает текстурная компрессия, мип-мэппинг, вычислительная мощность ядер GPU и специфические оптимизации аппаратного пайплайна (early depth rejection). А против альфа-блендинга сыграет уже упомянутый выше факт о том, что это наименее прогрессивная часть любого GPU.
Тем не менее, место для оптимизации всегда найдется. Например, в случае с реконструкцией цвета ландшафта необходимость в этой реконструкции есть лишь для узкой полосы фрагментов 3D-модели, расположенных не выше определенной высоты над поверхностью ландшафта.
Динамическое ветвление в шейдерах может давать слабо предсказуемые результаты по производительности, но есть два момента, которые стоит принять во внимание:
- Пропуск ненужных вычислений в ветвлении по условию следует делать, если это условие не выполняется в значительной части случаев.
- На эффективность подобной оптимизации влияет когерентность выполнения условия. В том случае, если сравнение выполнения условия дает разный результат для большого числа соседних фрагментов (то есть, условие некогерентно), в силу вступает аппаратная специфика GPU. Ключевой момент здесь ― гранулярность ветвления (branch granularity), то есть, число, определяющее, сколько именно потоков должен выполнять один и тот же код, использующий одни и те же ресурсы. Если хотя бы одному из потоков приходится выполнять ветвление, отличающееся от остальных потоков, всем потокам придется выполнять код этого ветвления. В случае, если гранулярность GPU достаточно велика, а условие выполняется недостаточно когерентно, никакой оптимизации от ветвления ожидать не стоит. Удивительно, но существует как минимум один GPU, обладающий оптимальной гранулярностью, равной 1 (PowerVR SGX).
Визуализация различных степеней когерентности
В случае с реконструкцией фрагментов учитываются оба этих момента: условие ветвления в большей части случаев позволит отсечь выполнение ресурсоемких операций по реконструкции цвета ландшафта, и это условие когерентно, за исключением очень небольшого числа фрагментов (на иллюстрации это те фрагменты, что лежат на границе между «красной» и «зеленой» зонами).
Когерентность реконструкции фрагментов ландшафта
Осталось добавить несколько замечаний, касающихся этого метода блендинга:
- Unity предоставляет все необходимые текстуры только в том случае, если у ландшафта включен режим Draw Instanced[20], иначе будет недоступна карта нормалей, что, в свою очередь, не позволит правильно реконструировать освещение ландшафта для блендинга.
- В Unity API нет доступа к текстуре, в которую запекаются смешанные слои (base map) и которая используется для упрощения рендеринга ландшафта по дистанции. Из-за этого невозможно реализовать вариант реконструкции фрагментов ландшафта с этой текстурой.
- Для кроссплатформенности пришлось разделить текстуры и сэмплеры в коде шейдеров, поскольку различные API имеют различные ограничения на число текстурных сэмплеров в шейдерах (например, Metal поддерживает всего 16 текстурных сэмплеров).
- Чтобы корректно реализовать блендинг 3D-моделей с ландшафтом, состоящим из множества объектов Terrain, потребуется написать собственный вариант SRP.
- Блендинг 3D-моделей с поверхностью ландшафта выглядит тем хуже, чем больше угол между нормалью поверхности 3D-модели и нормалью поверхности ландшафта.
- Освещение, реконструируемое для «виртуальных» фрагментов, немного отличается от освещения «реальных» фрагментов ландшафта. Это связано с тем, что «реальные» фрагменты ландшафта рендерятся по дискретной сетке, нормали которой сэмплятся из карты нормалей в вертексном шейдере и интерполируются между вертексами. Для «виртуальных» фрагментов нормали сэмплятся непосредственно из карты нормалей во фрагментном шейдере.
Блендинг с реконструкцией фрагментов ландшафта
Снэппинг с картой высот
При проектировании 3D-моделей невозможно учесть разнообразие рельефов ландшафта, совместно с которым эти модели предполагается использовать. Часто 3D-модели приходится достаточно глубоко «утапливать» в ландшафте или поворачивать для того, чтобы скрыть выступающие части, или наоборот ― показать скрытые, которые должны быть видимыми. «Утапливание» моделей ограничивает их применимость, а в случае, если 3D-модели рендерятся раньше ландшафта, еще и приводит к эффекту overdraw. Поворот же, в свою очередь, тоже подходит далеко не для всех 3D-моделей (например, не для домов и деревьев).
Чтобы скрыть выступающие элементы 3D-модели, ее необходимо «утопить» в ландшафте
Снэппинг ― термин, хорошо знакомый пользователям графических редакторов. Это функция, которая позволяет контрольным точкам «прилипать» к узлам пространственной сетки, а в 3D-редакторах ― к граням и поверхностям других объектов. Снэппинг к карте высот ландшафта в вертексном шейдере способен значительно упростить проектирование сцен.
3D-модель без снэппинга. 3D-модель с повертексным снэппингом. 3D-модель с повертексным снэппингом и блендингом. 3D-модель с повертексным снэппингом, блендингом и статическим освещением
Основная сложность в реализации снэппинга состоит в том, что необходимо разобраться, какие вертексы 3D-модели нужно снэппить к карте высот, а какие не стоит. Вертексы содержат лишь информацию о локальном характере поверхности (которой недостаточно) и не содержат никакой информации о ее топологии (которая необходима).
Как и в других прикладных случаях, эту проблему проще всего решить на этапе моделирования, напрямую внедрив необходимые параметры в вертексы. В качестве такого параметра стоит выбрать интуитивно понятный атрибут ― например, весовой коэффициент для снэппинга (а не дистанцию до границы незамкнутой поверхности, как хотелось бы для гибкости).
Кодирование весовых коэффициентов для снэппинга
Посмотреть код
// Per-vertex snapping with terrain heightmap
#include "UnityCG.cginc"
sampler2D_float TerrainHeightmapTexture;
float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;
struct v2f
{
// ...
float3 worldPos : TEXCOORD0;
float2 heightMapUV : TEXCOORD1;
// ...
};
v2f vert(appdata v)
{
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f,o);
// ...
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.heightMapUV = TerrainUV(o.worldPos);
float snappingWeight = v.color.r;
half height = TerrainHeight( o.heightMapUV );
o.worldPos.y = lerp( o.worldPos.y, height, snappingWeight );
o.pos = UnityWorldToClipPos( half4( o.worldPos, 1 ) );
// ...
return o;
}
Применимость повертексного снэппинга ограничена общим соответствием поверхности ландшафта и поверхности 3D-модели. Чтобы компенсировать значительные их расхождения, необходимо применять другие, более ресурсоемкие методы ― например, использовать 3D-модели со скиннингом.
Заключение
Главная мысль, которую следует вынести из статьи: любому достаточно сложному и потенциально масштабируемому шейдеру необходимы исходные данные. И задача разработчика состоит в том, чтобы понять, каким образом можно эксплуатировать графическую систему: какие данные она предоставляет, как их можно комбинировать друг с другом и каким образом использовать в шейдерах.
В общем же случае можно сделать вывод, что единственный вариант преодолеть те рамки, которыми ограничены возможности графических эффектов, ― комбинировать результаты работы различных шейдеров.
Ссылки
Посмотреть ссылки
[1] docs.unity3d.com/Manual/SL-CameraDepthTexture.html
[2] docs.unity3d.com/ScriptReference/Camera-depthTextureMode.html
[3] docs.unity3d.com/ScriptReference/RenderTexture.html
[4] www.khronos.org/opengl/wiki/Framebuffer_Object
[5] docs.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-id3d11device-createrendertargetview
[6] docs.unity3d.com/ScriptReference/Rendering.BlendMode.html
[7] docs.unity3d.com/ScriptReference/Rendering.BlendOp.html
[8] www.khronos.org/opengl/wiki/Blending
[9] docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-blend-state
[10] en.wikipedia.org/wiki/Parallax
[11] en.wikipedia.org/wiki/Parallax_scrolling
[12] docs.unity3d.com/2018.3/Documentation/ScriptReference/TerrainData-heightmapTexture.html
[13] pdfs.semanticscholar.org/ce6c/fcafe3581a7e4d7184a9727cc504bdc6b295.pdf
[14] docs.unity3d.com/2018.3/Documentation/ScriptReference/Terrain-normalmapTexture.html
[15] docs.unity3d.com/2018.3/Documentation/ScriptReference/Terrain-lightmapIndex.html
[16] docs.unity3d.com/2018.3/Documentation/ScriptReference/TerrainData-alphamapTextures.html
[17] docs.unity3d.com/2018.3/Documentation/ScriptReference/TerrainData-terrainLayers.html
[18] docs.unity3d.com/2018.3/Documentation/ScriptReference/TerrainLayer-tileSize.html
[19] docs.unity3d.com/2018.3/Documentation/ScriptReference/TerrainLayer-tileOffset.html
[20] docs.unity3d.com/2018.3/Documentation/ScriptReference/Terrain-drawInstanced.html
[2] docs.unity3d.com/ScriptReference/Camera-depthTextureMode.html
[3] docs.unity3d.com/ScriptReference/RenderTexture.html
[4] www.khronos.org/opengl/wiki/Framebuffer_Object
[5] docs.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-id3d11device-createrendertargetview
[6] docs.unity3d.com/ScriptReference/Rendering.BlendMode.html
[7] docs.unity3d.com/ScriptReference/Rendering.BlendOp.html
[8] www.khronos.org/opengl/wiki/Blending
[9] docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-blend-state
[10] en.wikipedia.org/wiki/Parallax
[11] en.wikipedia.org/wiki/Parallax_scrolling
[12] docs.unity3d.com/2018.3/Documentation/ScriptReference/TerrainData-heightmapTexture.html
[13] pdfs.semanticscholar.org/ce6c/fcafe3581a7e4d7184a9727cc504bdc6b295.pdf
[14] docs.unity3d.com/2018.3/Documentation/ScriptReference/Terrain-normalmapTexture.html
[15] docs.unity3d.com/2018.3/Documentation/ScriptReference/Terrain-lightmapIndex.html
[16] docs.unity3d.com/2018.3/Documentation/ScriptReference/TerrainData-alphamapTextures.html
[17] docs.unity3d.com/2018.3/Documentation/ScriptReference/TerrainData-terrainLayers.html
[18] docs.unity3d.com/2018.3/Documentation/ScriptReference/TerrainLayer-tileSize.html
[19] docs.unity3d.com/2018.3/Documentation/ScriptReference/TerrainLayer-tileOffset.html
[20] docs.unity3d.com/2018.3/Documentation/ScriptReference/Terrain-drawInstanced.html