Triplanar mapping - отличное решение для текстурирования сложной геометрии, для которой сложно или невозможно использовать традиционные UV без очевидного растяжения и / или текстурных швов. Это также метод, страдающий от нерешительных и откровенно неправильных реализаций карт нормалей.

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

Примеры шейдеров Unity доступны здесь:
https://github.com/bgolus/Normal-Mapping-for-a-Triplanar-Shader

Оглавление

  1. Трипланарное картографирование
    « Попробуйте сыграть Что теперь?»
  2. Проблема
    Не совсем нормальное отображение
    Наивный метод (иначе говоря, просто используйте касательные сетки)
  3. Карты нормалей касательного пространства
    Касательные стороны
  4. Подходя к решению
    Базовый Swizzle
    Смешивание в деталях
  5. Трипланарное отображение нормалей
    Переход UDN
    Переход белого цвета
    Переориентированное отображение нормалей
  6. « Основополагающая правда »
    В погоне за правдой
  7. Другие методы для трехплоскостного отображения нормалей
    Частные производные экранного пространства
    Реконструкция касательной между продуктами
  8. Трипланарное смешивание
    Растушевывание деталей
  9. Дополнительные мысли
    Зеркальные UV
    Поверхностные шейдеры Unity
    Трипланарные нормали для других средств визуализации (не Unity)

Трипланарное картирование

«Попробуйте сыграть». Что теперь?

Краткое описание того, что имеется в виду, когда кто-то упоминает трехплоскостное картографирование. Трипланарное проекционное отображение мирового пространства - это техника, которая применяет текстуры к объекту с трех направлений, используя положение в мировом пространстве. Три самолета. Это также называется «текстурированием без УФ-излучения», «УФ-проекцией в мировом пространстве» и некоторыми другими названиями. Существуют также вариации в пространстве объекта по этой технике. Представьте, что вы смотрите на объект прямо сверху, сбоку и спереди и применяете текстуру к объекту с этих направлений.

Ландшафт - популярный вариант использования, потому что его геометрия часто сложно текстурировать. Базовый ландшафт обычно имеет одну УФ-проекцию сверху. Мы могли бы назвать это «монопланарным» спроектированным UV. Для холмов это выглядит нормально, но на крутых скалах направление одиночной проекции может выглядеть довольно плохо.

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

Полностью исчезло растяжение текстуры!

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



Я пропущу большую часть этого и сразу перейду к сути этой статьи.

Эта проблема

Не совсем нормальное отображение

Обычное наивное «решение» для трехплоскостного отображения нормалей состоит в том, чтобы просто использовать касательные к сетке. Часто это может показаться «достаточно близким». Затем вы полагаетесь на то, что ваши текстуры достаточно неоднозначны, чтобы неправильная установка нормалей не была очевидна. Не делай этого. Есть более дешевые методы, которые выглядят значительно лучше.

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

Одно из дешевых решений, о котором я упоминаю, появилось еще в 2003 году. Оно исходит от GPU Gems 3 от Nvidia.

GPU Gems 3: создание сложных процедурных ландшафтов с помощью GPU https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch01.html

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

Итак, прежде чем мы углубимся в то, что делает эта статья о GPU Gems 3, давайте посмотрим на наивный метод и какие проблемы у него есть.

Наивный метод (он же просто используйте касательные к сетке)

Это первое, что пробует большинство людей, и на первый взгляд это «просто работает». Итак, давайте попробуем использовать базовую текстуру камня и карту нормалей.

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

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

Для сравнения, вот как это должно выглядеть.

Неровности появляются наружу, а не внутрь, и все они совпадают с направлением света. Мягкое смешение между сторонами немного странно для этой карты нормалей, но игнорируя то, что вышесказанное можно считать «наземной истиной».

Вот они рядом.

Карты нормалей касательного пространства

Боковая касательная

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

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

Карты нормалей - это направления относительно ориентации текстуры. Помимо этого, существует масса деталей реализации и мелких деталей, но они не меняют эту основную предпосылку. Для Unity красный цвет показывает, насколько он прав (ось x), зеленый - насколько он вверх (ось y), а синий - насколько он перпендикулярен поверхности (ось z). Все это относительно текстуры UV и вершинной нормали. Иногда это называют картой нормалей + Y. ¹

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

Для графики в реальном времени вершины меша хранят касательную, или ориентацию слева направо UV. Это передается вместе с нормалью вершины и битангенсом (иногда называемым бинормалью ²) фрагментному шейдеру как касательная к матрице преобразования мирового пространства. Это сделано для того, чтобы направление, сохраненное в карте нормалей, можно было повернуть из касательного пространства в мировое пространство. Суть в том, что касательные совпадают только с ориентацией сохраненного UV текстуры. Если ваши трипланарные проецируемые UV-развертки не соответствуют этому (что гарантировано не для большей части меша!), То вы получите нормали, которые выглядят так, как будто они перевернуты, или сбоку, или повернуты в другую сторону, но не так, как вы хотите.

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

edit: Дополнительное примечание о касательных к сетке и трехплоскостном отображении. Вообще говоря, если вы выполняете трипланарное отображение, используемая сетка не нужна и даже не должна хранить UV в вершинах. По большому счету, сетка также не должна иметь касательных. Для сеток, которые импортируются или поставляются с Unity, по умолчанию Unity импортирует или генерирует касательные для сетки. Это единственная причина, по которой наивный метод вообще возможен. Меши, которые создаются в коде по умолчанию, не имеют касательных, даже UV или нормалей, а наивный метод был бы еще хуже.

Приближаясь к решению

Основной Swizzle

Что вам действительно нужно для трипланарного отображения, так это вычислить уникальные касательные и битангенсы для каждой «грани» трипланарной текстуры UV. Но, если вы помните, в самом начале я сказал, не делайте этого. Почему? Потому что мы можем делать удивительно приличные приближения с очень небольшим количеством данных. Мы можем использовать мировую ось в качестве касательных. Еще проще и дешевле мы можем поменять местами некоторые компоненты карты нормалей (также известные как swizzle) и получить тот же результат!

// Basic Swizzle
// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Get the sign (-1 or 1) of the surface normal
half3 axisSign = sign(i.worldNormal);
// Flip tangent normal z to account for surface normal facing
tnormalX.z *= axisSign.x;
tnormalY.z *= axisSign.y;
tnormalZ.z *= axisSign.z;
// Swizzle tangent normals to match world orientation and triblend
half3 worldNormal = normalize(
    tnormalX.zyx * blend.x +
    tnormalY.xzy * blend.y +
    tnormalZ.xyz * blend.z
    );

Здесь я опускаю большую часть шейдера. Об этом поговорим позже. Главное, на что смотреть - это первая и последние 3 строки. Обратите внимание, что UV-развертки для каждой плоскости и компоненты карты нормалей имеют одинаковый порядок. Как мы обсуждали выше, карта нормалей касательного пространства должна быть выровнена по ориентации ее UV. Поскольку мы используем мировую позицию для UV, это выравнивание!

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

Итак, можем ли мы как-то добавить округлости в измененные карты нормалей?

Смешивание в деталях

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

Для этого есть несколько приемов. Первая идея, о которой приходят люди, - это «сложить их вместе», но на самом деле это не решение, оно просто выравнивает обе нормали. Некоторые более умные люди могут подумать: "А как насчет наложения смеси?" Он работает нормально, но его популярность связана исключительно с тем, что его просто сделать в Photoshop, не нарушая все полностью. Это не потому, что это отдаленно правильно или даже особенно дешево. Это еще один случай, когда метод обходится дороже, чем использование правильного способа. В основном есть два конкурирующих приближения, которые используются наиболее часто: так называемое «белое пятно» и нормальное смешивание «UDN». Они очень похожи как по реализации, так и по результатам. UDN немного дешевле, но может немного сгладить края, а Whiteout выглядит немного лучше, но немного дороже. Для современных настольных и консольных графических процессоров нет особых причин не использовать метод Whiteout вместо метода UDN. Но смесь UDN все еще используется для мобильных устройств.

Вы можете узнать больше о различных методах наложения карт нормалей в блоге Стивена Хилла:

Детальное смешивание
http://blog.selfshadow.com/publications/blending-in-detail/

Трипланарное отображение нормалей

Так как же наложение карты нормалей касательного пространства применяется к трипланарному отображению? Мы можем рассматривать каждую плоскость как отдельную смесь карт нормалей, где одна из «карт нормалей», с которой мы сочетаемся, является нормалью вершин! Смешивание UDN из этой статьи на самом деле оказывается особенно дешевым, поскольку оно просто добавляет значения карты нормалей x и y к нормали вершины! Давайте посмотрим, как это выглядит.

Смесь UDN

// UDN blend
// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Swizzle world normals into tangent space and apply UDN blend.
// These should get normalized, but it's very a minor visual
// difference to skip it until after the blend.
tnormalX = half3(tnormalX.xy + i.worldNormal.zy, i.worldNormal.x);
tnormalY = half3(tnormalY.xy + i.worldNormal.xz, i.worldNormal.y);
tnormalZ = half3(tnormalZ.xy + i.worldNormal.xy, i.worldNormal.z);
// Swizzle tangent normals to match world orientation and triblend
half3 worldNormal = normalize(
    tnormalX.zyx * blend.x +
    tnormalY.xzy * blend.y +
    tnormalZ.xyz * blend.z
    );

Выглядит неплохо, не правда ли? Смесь UDN довольно популярна, потому что она настолько дешевая и эффективная. Но у смеси есть один недостаток. Из-за того, как работает математика, карта нормалей немного сглаживается под углами больше 45 градусов. Это приводит к небольшой потере деталей в смешанной нормали.

Смесь Whiteout

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

// Whiteout blend
// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Swizzle world normals into tangent space and apply Whiteout blend
tnormalX = half3(
    tnormalX.xy + i.worldNormal.zy,
    abs(tnormalX.z) * i.worldNormal.x
    );
tnormalY = half3(
    tnormalY.xy + i.worldNormal.xz,
    abs(tnormalY.z) * i.worldNormal.y
    );
tnormalZ = half3(
    tnormalZ.xy + i.worldNormal.xy,
    abs(tnormalZ.z) * i.worldNormal.z
    );
// Swizzle tangent normals to match world orientation and triblend
half3 worldNormal = normalize(
    tnormalX.zyx * blend.x +
    tnormalY.xzy * blend.y +
    tnormalZ.xyz * blend.z
    );

Они довольно похожи, если не сравнивать их напрямую.

Здесь вы можете увидеть, что нормали для смешивания UDN не выглядят неправильно, но выглядят немного сглаженными по сравнению с Whiteout.

И здесь у нас есть два вполне правдоподобных приближения трехплоскостного отображения нормалей. Также нет очевидных визуальных проблем с освещением. И то, и другое достаточно для большинства случаев использования. И оба они быстрее, чем наивный вариант или даже обычная карта нормалей. Белое пятно также является признаком для стен, выровненных по оси, например, техника прямого взбивания! Я подозреваю, что никто даже не узнает, что Whiteout не идеален, если им не покажут их рядом, и даже тогда чертовски сложно выделить проблемы.

А как насчет техники в GPU Gems 3? Фактически он реализует ту же идею, что и два вышеупомянутых шейдера. Он выполняет то же переключение компонентов карты нормалей, но отбрасывает компонент «z» карты нормалей и вместо этого использует ноль. Почему?

Если вы внимательно посмотрите на код GPU Gems 3, он действительно будет таким же, как и смесь UDN! Ни тот, ни другой фактически не используют компонент z карт нормалей. Их реализация оказывается на несколько инструкций быстрее, чем написанный мной шейдер смешивания UDN, но дает идентичные результаты!

// GPU Gems 3 blend
// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Swizzle tangemt normals into world space and zero out "z"
half3 normalX = half3(0.0, tnormalX.yx);
half3 normalY = half3(tnormalY.x, 0.0, tnormalY.y);
half3 normalZ = half3(tnormalZ.xy, 0.0);
// Triblend normals and add to world normal
half3 worldNormal = normalize(
    normalX.xyz * blend.x +
    normalY.xyz * blend.y +
    normalZ.xyz * blend.z +
    i.worldNormal
    );

Смесь GPU Gems 3 - самый быстрый вариант из этих трех шейдеров. ³ Но вряд ли дополнительные затраты на использование смеси Whiteout будут заметны даже для мобильной виртуальной реальности. А разница в качестве может быть проблемой для некоторых материалов и для взыскательных художников.

Переориентированное отображение нормалей

Так что насчет изображения «Основополагающей истины», которое я постоянно показываю? В статье «Подробное наложение», на которую я ссылался выше, описывается несколько других методов, помимо смешивания UDN и Whiteout. Сюда входит метод, который они называют переориентированным нормальным отображением. Это фантастический метод; в итоге он оказывается немного дороже, чем методы GPU Gems 3 или Whiteout, но довольно близок к «наземной истине»! Фактически, все примеры изображений «наземной истины», приведенные выше в этой статье, используют эту технику! Кроме того, он все еще, вероятно, быстрее, чем наивный метод в целом, даже несмотря на то, что он создает более сложный шейдер.

В Blending in Details показанная функция RNM делает некоторые предположения о передаваемых ей картах нормалей, которые не верны для Unity. Необходимо внести незначительные изменения в исходный код. В комментариях к статье Стивен Хилл приводит этот пример использования RNM с Unity. С этой функцией трипланарный шейдер наложения выглядит следующим образом.

// Reoriented Normal Mapping blend
// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Get absolute value of normal to ensure positive tangent "z" for blend
half3 absVertNormal = abs(i.worldNormal);
// Swizzle world normals to match tangent space and apply RNM blend
tnormalX = rnmBlendUnpacked(half3(i.worldNormal.zy, absVertNormal.x), tnormalX);
tnormalY = rnmBlendUnpacked(half3(i.worldNormal.xz, absVertNormal.y), tnormalY);
tnormalZ = rnmBlendUnpacked(half3(i.worldNormal.xy, absVertNormal.z), tnormalZ);
// Get the sign (-1 or 1) of the surface normal
half3 axisSign = sign(i.worldNormal);
// Reapply sign to Z
tnormalX.z *= axisSign.x;
tnormalY.z *= axisSign.y;
tnormalZ.z *= axisSign.z;
// Triblend normals and add to world normal
half3 worldNormal = normalize(
    normalX.xyz * blend.x +
    normalY.xyz * blend.y +
    normalZ.xyz * blend.z +
    i.worldNormal
    );

А вот функция, с которой я связался выше.

// Reoriented Normal Mapping for Unity3d
// http://discourse.selfshadow.com/t/blending-in-detail/21/18

float3 rnmBlendUnpacked(float3 n1, float3 n2)
{
    n1 += float3( 0,  0, 1);
    n2 *= float3(-1, -1, 1);
    return n1*dot(n1, n2)/n1.z - n2;
}

Я говорю «для Unity3d», но это должно работать для любых карт нормалей касательного пространства, которые были распакованы в нормализованный диапазон от -1 до 1.

«Основополагающая правда»

В погоне за правдой

За исключением того, что переориентированное отображение нормалей все еще не соответствует действительности.

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

Я также немного соврал выше, описывая, как работает отображение нормалей касательного пространства. В частности, битангенс, который я в основном замалчиваю. Я описываю это как «зеленую стрелку» на примере касательной текстуры. Это не совсем так. Для отображения нормали касательного пространства битангенс для определяется как пересечение нормали и касательной. Касательная и битангенс должны быть перпендикулярны нормали и друг к другу. Если вы посмотрите на пример трипланарной касательной текстуры, вы увидите, что касательная и битангенциальная стрелки начинают наклоняться и в конечном итоге превращаются в треугольник в углах. В результате касательная и битангенс перпендикулярны нормали, но не друг к другу.

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

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

Итак, все сводится к решению, какой должна быть основная истина, которую вы пытаетесь достичь. По сути, вам нужно решить, что должна представлять карта нормалей. Нормаль ли это результат смещения поверхности, выровненной по проекции текстуры, или нормали к поверхности? Если это смещение по проекции, смесь Whiteout ближе к истине! Если это смещение по нормали к поверхности, то более близким методом является RNM. На мой взгляд, RNM сохраняет больше деталей на картах нормалей и в целом выглядит лучше. В конечном итоге все сводится к личным предпочтениям и производительности.

Кроме наивного метода. Это всегда неправильно.

Другие методы для картографирования трипланарных нормалей

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

Частные производные экранного пространства

Можно восстановить матрицу вращения касательного пространства, используя частные производные экранного пространства. Эта идея пришла в виде Derivative Mapping, популяризированного Мортеном Микклесоном. Этот метод неправильно называется отображение нормалей без касательных, но он отличается от отображения нормалей касательного пространства. Это отличный вариант для трехплоскостного картирования и имеет множество преимуществ. В этой статье я не буду вдаваться в производное отображение. Если вы хотите спрыгнуть в кроличью нору, попробуйте две ссылки выше вместе с этой (которая также имеет ссылки на две другие):



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



// Cotangent frame from Screen Space Partial Derivates
// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Normalize surface normal
half3 vertexNormal = normalize(i.worldNormal);
// Calculate the cotangent frame for each plane
half3x3 tbnX = cotangent_frame(vertexNormal, i.worldPos, uvX);
half3x3 tbnY = cotangent_frame(vertexNormal, i.worldPos, uvY);
half3x3 tbnZ = cotangent_frame(vertexNormal, i.worldPos, uvZ);
// Apply cotangent frame and triblend normals
half3 worldNormal = normalize(
    mul(tnormalX, tbnX) * blend.x +
    mul(tnormalY, tbnY) * blend.y +
    mul(tnormalZ, tbnZ) * blend.z
    );

А вот функция cotangent_frame(), модифицированная для использования с Unity.

// Unity version of http://www.thetenthplanet.de/archives/1180
float3x3 cotangent_frame(float3 normal, float3 position, float2 uv)
{
    // get edge vectors of the pixel triangle
    float3 dp1 = ddx( position );
    float3 dp2 = ddy( position ) * _ProjectionParams.x;
    float2 duv1 = ddx( uv );
    float2 duv2 = ddy( uv ) * _ProjectionParams.x;
    // solve the linear system
    float3 dp2perp = cross( dp2, normal );
    float3 dp1perp = cross( normal, dp1 );
    float3 T = dp2perp * duv1.x + dp1perp * duv2.x;
    float3 B = dp2perp * duv1.y + dp1perp * duv2.y;
    // construct a scale-invariant frame
    float invmax = rsqrt( max( dot(T,T), dot(B,B) ) );
    // matrix is transposed, use mul(VECTOR, MATRIX)
    return float3x3( T * invmax, B * invmax, normal );
}

Я бы не стал предлагать использовать этот вариант для производственного трехплоскостного картирования, поскольку он дороже, чем описанные выше варианты, хотя его также можно рассматривать как очень близкий к версии «наземной истины». У него также есть некоторые проблемы, когда многоугольная форма геометрии может стать заметной как резкие края в нормалях. Это связано с тем, что касательные вычисляются по фактической геометрической поверхности, а не по гладко интерполированному значению. Является ли это проблемой, зависит от используемого вами альбедо, карты нормалей и геометрии.

Кстати, вы можете использовать это в своих интересах, чтобы получить нормали граненной поверхности в стиле low poly.

half3 normal = normalize(cross(dp1, dp2));

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

Реконструкция касательной к кросс-произведению

А как насчет того, чтобы воспроизвести расчет касательных на сетке? Хотя я упоминал, что это неправильно, это можно сделать. Шейдеры ландшафта Unity используют очень похожую технику для вычисления касательных в вершинном шейдере. Здесь это делается во фрагментном шейдере.

// Tangent Reconstruction
// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Get the sign (-1 or 1) of the surface normal
half3 axisSign = sign(i.worldNormal);
// Construct tangent to world matrices for each axis
half3 tangentX = normalize(cross(i.worldNormal, half3(0, axisSign.x, 0)));
half3 bitangentX = normalize(cross(tangentX, i.worldNormal)) * axisSign.x;
half3x3 tbnX = half3x3(tangentX, bitangentX, i.worldNormal);
half3 tangentY = normalize(cross(i.worldNormal, half3(0, 0, axisSign.y)));
half3 bitangentY = normalize(cross(tangentY, i.worldNormal)) * axisSign.y;
half3x3 tbnY = half3x3(tangentY, bitangentY, i.worldNormal);
half3 tangentZ = normalize(cross(i.worldNormal, half3(0, -axisSign.z, 0)));
half3 bitangentZ = normalize(-cross(tangentZ, i.worldNormal)) * axisSign.z;
half3x3 tbnZ = half3x3(tangentZ, bitangentZ, i.worldNormal);
// Apply tangent to world matrix and triblend
// Using clamp() because the cross products may be NANs
half3 worldNormal = normalize(
    clamp(mul(tnormalX, tbnX), -1, 1) * blend.x +
    clamp(mul(tnormalY, tbnY), -1, 1) * blend.y +
    clamp(mul(tnormalZ, tbnZ), -1, 1) * blend.z
    );

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

Трипланарное смешивание

Смешивание из деталей

Если вы когда-либо работали над трипланарным отображением, вы, вероятно, знакомы с фрагментом кода, который выглядит примерно так:

float3 blend = abs(normal.xyz);
blend /= blend.x + blend.y + blend.z;

Причина этого разделения - нормализовать сумму. Функция normalize () в шейдерах возвращает вектор с нормализованной величиной, но мы хотим, чтобы компоненты вектора имели нормализованную сумму, равную 1,0. Сумма, по сути, всегда будет больше 1,0 для «нормализованного» числа с плавающей запятой 3, где-то между 1,0 и 1,7321, если мы ничего с ней не сделаем. Поэтому, если вы используете абсолютное значение нормалей для смешивания текстур альбедо, углы будут слишком яркими. Деление на сумму гарантирует, что новая сумма всегда будет равна 1,0.

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

float3 blend = abs(normal.xyz);
blend = (blend - 0.2) * 7.0;
blend = max(blend, 0);
blend /= blend.x + blend.y + blend.z;

Это помогает немного повысить резкость, но это любопытный фрагмент кода, потому что * 7.0 бесполезен! Изменение 7.0 на любое ненулевое число или его полное удаление не имеет никакого эффекта, но этот фрагмент кода, кажется, обнаруживается в половине трехпланарных реализаций, которые я вижу. Небольшая мысль объяснит, почему в этом нет необходимости; деление числа само на себя всегда равно 1, и умножение его на какое-то ненулевое значение не меняет этого.

Насколько я могу судить, этот фрагмент кода, похоже, происходит из той же статьи о GPU Gems 3, на которую я ссылался ранее! К сожалению, та часть статьи, которая была написана неправильно, по-видимому, наиболее часто использовалась повторно.

Однако - 0.2 в порядке, просто оставьте после этого умножение. Существует также дополнительная оптимизация, которая может быть произведена путем злоупотребления скалярным произведением для суммирования компонентов перед разделением.

float3 blend = abs(normal.xyz);
blend = max(blend - 0.2, 0);
blend /= dot(blend, float3(1,1,1));

Это небольшая оптимизация, в исходной версии используется 6 инструкций, а в оптимизированной версии - 4. Но нет причин не делать этого, поскольку результаты идентичны, и она удаляет бесполезное магическое число.

Это довольно незначительно, но достаточно, если ваша геометрия - это в основном плоские стены. Но что, если мы хотим еще более резкого перехода? Вы можете увеличить вычитаемое значение, но углы станут заметно острее перед остальными, а затем поднимутся слишком высоко, и он начнет чернеть. Значение -0,55 - это максимально возможное значение, прежде чем возникнут очевидные проблемы. Это связано с тем, что в углах нормализованный вектор равен (0,577, 0,577, 0,577), поэтому вычитание большего количества приведет к тому, что функция max () превратит углы в (0,0, 0,0, 0,0), и тогда вы не только потеряете значения, но вы делите на ноль!

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

float3 blend = pow(normal.xyz, 4);
blend /= dot(blend, float3(1,1,1));

Это небольшая разница, но это та же стоимость 4 инструкций. Если вы хотите больше контролировать резкость наложения с помощью свойства материала, это немного дороже. Unity покажет это как 5 инструкций, но на самом деле это больше похоже на 11 инструкций. Другие жестко закодированные мощности будут меньше (на 1 больше инструкции для каждого увеличения степени на 2, так что pow (normal, 8) - это 5 инструкций, 16 - это 6 инструкций и т. Д.

Низкое увеличение может по-прежнему не очень хорошо смотреться с кирпичом, но я намеренно использую текстуру «наихудшего случая», чтобы сделать смесь очевидной. С другой текстурой и освещением может быть полезно слегка мягкое смешивание.

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

// Asymmetric Triplanar Blend
float3 blend = 0;
// Blend for sides only
float2 xzBlend = abs(normalize(normal.xz));
blend.xz = max(0, xzBlend - 0.67);
blend.xz /= max(0.00001, dot(blend.xz, float2(1,1)));
// Blend for top
blend.y = saturate((abs(normal.y) - 0.675) * 80.0);
blend.xz *= (1 - blend.y);

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

// Height Map Triplanar Blend
float3 blend = abs(normal.xyz);
blend /= dot(blend, float(1,1,1));
// Height value from each plane's texture. This is usually
// packed in to another texture or (less optimally) as a separate 
// texture.
float3 heights = float3(heightX, heightY, heightZ) + (blend * 3.0);
// _HeightmapBlending is a value between 0.01 and 1.0
float height_start = max(max(heights.x, heights.y), heights.z) - _HeightmapBlending;
float3 h = max(heights - height_start.xxx, float3(0,0,0));
blend = h / dot(h, float3(1,1,1));

Магия blend * 3.0 в том, чтобы уменьшить область смешивания. Это создает форму перехода, похожую на Gems 3. Наложение карты высот лучше всего работает с широкой начальной областью слияния, но с зажатыми краями. Один только силовой метод - это слишком сглаживание, позволяющее смешению показывать плохое растяжение текстуры.

Приведенный выше код наложения высоты примерно основан на методе, описанном в этой статье.

Шейдер смешения высоты от Octopoid
http://untitledgam.es/2017/01/height-blending-shader/

Дополнительные мысли

Зеркальные UV

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

Но решить это несложно. Самое простое решение - немного сместить все 3 плоскости, чтобы снизить вероятность идеального перекрытия. Добавьте 0.33 к UV плоскости Y и 0.67 к UV плоскости Z, и все готово. Однако это может привести к нежелательным проблемам с выравниванием, если вы хотите, чтобы текстуры совпадали.

В качестве альтернативы вы также можете перевернуть UV. Это может быть дополнительно полезно, если вы используете текстуры с очевидной ориентацией, например текст. Умножьте компонент x UV на знак оси нормали к поверхности, затем проделайте то же самое с картами нормалей касательного пространства, чтобы отменить отражение.

// Projected UV Flip
// Triplanar uvs
float2 uvX = i.worldPos.zy; // x facing plane
float2 uvY = i.worldPos.xz; // y facing plane
float2 uvZ = i.worldPos.xy; // z facing plane
// Get the sign (-1 or 1) of the surface normal
half3 axisSign = sign(i.worldNormal);
// Flip UVs to correct for mirroring
uvX.x *= axisSign.x;
uvY.x *= axisSign.y;
uvZ.x *= -axisSign.z;
// Tangent space normal maps
half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));
half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));
half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));
// Flip normals to correct for the flipped UVs
tnormalX.x *= axisSign.x;
tnormalY.x *= axisSign.y;
tnormalZ.x *= -axisSign.z;

Вы, конечно, можете использовать и флип, и смещение вместе, если хотите.

Шейдеры Unity Surface

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

Здесь есть функциональный пример Surface Shader, преобразующий мировые нормали в касательное пространство в функции surf:
https://github.com/bgolus/Normal-Mapping-for-a-Triplanar-Shader/blob/master /TriplanarSurfaceShader.shader

Трипланарные нормали для других средств визуализации (не Unity)

Обсуждаемые выше методы могут использоваться в любом рендерере в реальном времени или иным образом. Уловка будет заключаться в понимании мировой системы координат рендерера и ориентации карты нормалей. Unity - это левосторонняя система координат мирового пространства Y вверх и карты нормалей + Y. Из-за этого многие линии плоскости Z добавили отрицательные знаки, которых нет в других линиях. Если бы Unity был правшой, а Y - вверх, ему бы они не понадобились. Рендерерам, использующим карты нормалей -Y, вероятно, потребуется много дополнительных линий с добавленными отрицательными знаками, поскольку ориентация в мировом пространстве и ось Y карт нормалей будут инвертированы для многих (если не всех) плоскостей! Это одна из самых больших сложностей с трипланарным картированием нормалей, которая укусит большинство людей. Это можно понять, просто зная систему координат и ориентацию карты нормалей, но даже в этом случае легко ошибиться. Попробуйте смотреть на лицо по очереди и переворачивайте знак, пока он не будет выглядеть правильно, а затем проверьте и обратную сторону! Поначалу будет много проб и ошибок. Подумайте об этом как о применении научного метода!

¹ ^ Unreal Engine и некоторые другие инструменты используют так называемые карты нормалей -Y. Это означает, что зеленый канал инвертирован по сравнению с тем, что использовали бы Unity и другие инструменты + Y. Зеленый в этом случае означает, насколько он внизу. Первоначально разница связана с соглашениями OpenGL и DirectX для обработки UV. Позиция UV 0,0, 0,0 в OpenGL находится внизу слева, а в DirectX - вверху слева. Это означает, что по умолчанию в UV-развертке поток вверх и вправо для OpenGL и вниз и вправо для DirectX. В конечном счете, просто использовать любое соглашение, к которому вы больше всего привыкли, и переворачивать карты нормалей в шейдере. Поскольку Unity должен поддерживать как OpenGL, так и DirectX, он фактически переворачивает текстуры вверх ногами для платформ DirectX!

² ^ Битангенс vs бинормаль. Подавляющее большинство литературы и шейдеров будут называть три вектора касательного пространства нормальным, касательным и бинормальным. Бит касательный вектор, возможно, является более точным описательным названием для третьего вектора, поскольку это вторая касательная к поверхности. Касательная - это линия, которая касается кривой или поверхности, но не пересекает их. Те, кто выступает против термина битангенс, указывают на то, что он уже имеет особое, не связанное с этим значение в мире математики. Битангенс - это традиционно линия, являющаяся касательной к двум точкам кривой. Итак, очевидно, что это даже отдаленно не похоже. Однако технически бинормальность имела и предыдущее значение, хотя оно ближе к тому, как оно используется для наших целей. Бинормаль - это произведение вектора нормали и вектора касательной кривой. Однако под кривой они явно подразумевают бесконечно тонкую линию, а не поверхность. Если вы думаете о нормали как о направлении, указывающем прямо от поверхности, нормаль для линии - это любое направление, перпендикулярное касательной. Так что бинормаль там хорошее название, так как это еще одна нормальная линия. Для поверхности термин бинормаль имеет меньший смысл, поскольку она больше не перпендикулярна поверхности и, следовательно, больше не является нормалью. Вот почему битангенс становится привлекательным, потому что он имитирует использование префикса bi- в бинормали, но более точно описывает его как дополнительный касательный, которым он является.

Так что, по сути, оба варианта ошибочны. Если вы хотите разобраться, то даже термин "касательная" здесь немного неверен, поскольку он используется для описания единичного вектора, который, в частности, является как касательной к поверхности, так и производной x координаты текстуры, которая сама по себе касательная. Таким образом, это одна линия, которая является касательной для двух кривых, поэтому, может быть, касательную следует называть битангенсом? И бинормаль / битангенс должны быть бибитангенсом? Или кобитангенс? Кроссбитангенс? Фред? …

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

³ ^ На рабочем столе смеси GPU Gems 3 и UDN работают немного дешевле, чем смесь Whiteout, хотя код для Whiteout, похоже, не делает чего-то большего. Это связано с тем, что смесь Gems 3 не требует использования компонента z карты нормалей. В большинстве современных игровых движков карты нормалей хранят только компоненты x и y и восстанавливают компонент z. Часть того, что делает функция UnpackNormal() Unity, - это реконструкция компонента z. Поскольку Gems 3 не использует z, шейдер может пропустить его реконструкцию. Это приводит к тому, что трехпланарный шейдер наложения GPU Gems 3 сохраняет примерно 14 инструкций по сравнению с Whiteout. Может показаться, что смешанный шейдер Gems 3 будет очевидным выбором для мобильных устройств, но это не так. На мобильных устройствах Unity хранит карты нормалей для всех трех компонентов вместо восстановления z. Это сделано специально для того, чтобы не платить за реконструкцию и экономить часть текстурной памяти за счет незначительного снижения визуального качества. В результате шейдеры Gems 3 и Whiteout практически идентичны по производительности на мобильных устройствах. В некоторых случаях смесь Whiteout может быть даже немного быстрее, в зависимости от того, как компилятор шейдера выберет оптимизацию.