Triplanar Mapping

Введение.

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

Расчет плоскостей проекции.

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

struct v2f{
    float4 position : SV_POSITION;
    float3 worldPos : TEXCOORD0;
};

v2f vert(appdata v){
    v2f o;
    //Преобразуем положение вершины из координат объекта в координаты экрана
    o.position = UnityObjectToClipPos(v.vertex);
    //Рассчитываем мировую позицию вершины
    float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.worldPos = worldPos.xyz;
    return o;
}
 
Мы используем TRANSFORM_TEX для применения тайлинга и смещения текстуры, как мы привыкли. В моем шейдере я использую xy и zy, так что ось вверх по оси отображается на ось y текстуры для обеих текстур, не вращая их по отношению друг к другу, но вы можете поиграть с тем, как использовать эти значения (верхние UV названы произвольно).

fixed4 frag(v2f i) : SV_TARGET{
    //Рассчитываем UV-координаты для трех проекций
    float2 uv_front = TRANSFORM_TEX(i.worldPos.xy, _MainTex);
    float2 uv_side = TRANSFORM_TEX(i.worldPos.zy, _MainTex);
    float2 uv_top = TRANSFORM_TEX(i.worldPos.xz, _MainTex);
 
Получив правильные координаты, мы прочитаем текстуру в этих координатах, добавим три цвета и разделим результат на 3 (добавление трех цветов без деления на количество цветов будет очень ярким).

//Читаем цвет текстуры в трех позициях
fixed4 col_front = tex2D(_MainTex, uv_front);
fixed4 col_side = tex2D(_MainTex, uv_side);
fixed4 col_top = tex2D(_MainTex, uv_top);

//Комбинируем цвета
fixed4 col = (col_front + col_side + col_top) / 3;

//Добавляем оттенок цвета
col *= _Color;
return col;

Нормали.

Наш материал выглядит действительно странно. Это потому, что мы показываем среднее из трех проекций. Чтобы исправить это, нам нужно показать разные проекции, основываясь на направлении нормали к поверхности. Нормали сохраняются в файлах объектов, как и положение вершин.
Итак, что мы делаем,- получаем нормали в нашей входной структуре, преобразоваем их в нормали в мировой системе координат в вершинном шейдере (потому что наша проекция находится в мировом пространстве, если бы мы использовали проекцию пространства объектов, мы сохранили бы нормали в пространстве объектов).
Для преобразования нормалей из пространства объектов в мировое пространство мы должны умножить на нее обратную транспонированную матрицу. Не важно понимать, как это работает точно (сложность матриц сложна), но я хотел бы объяснить, почему мы не можем просто умножить его на матрицу объекта на мир, как мы делаем это с позицией. Нормали ортогональны поверхности, поэтому, когда мы масштабируем поверхность только по оси X, а не по оси Y, поверхность становится более крутой, но когда мы делаем то же самое с нашей нормалью, она также указывает больше вверх, чем раньше, и не является ортогональной поверхности. Вместо этого мы должны сделать нормаль более плоской к более крутой поверхности, и обратная матрица транспонирования делает это для нас. Затем мы также преобразуем матрицу в матрицу 3x3, отбросив части, которые будут перемещать нормали. (мы не хотим перемещать нормали, потому что они представляют собой направления вместо позиций).
Умножение обратной транспонированной матрицы на нормаль эквивалентно умножению нормаль на объектную матрицу (ранее мы умножали матрицу на вектор, порядок здесь важен).

struct appdata{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
};

struct v2f{
    float4 position : SV_POSITION;
    float3 worldPos : TEXCOORD0;
    float3 normal : NORMAL;
};

v2f vert(appdata v){
    v2f o;
    // Преобразуем положение вершины из координат объекта в координаты экрана
    o.position = UnityObjectToClipPos(v.vertex);
    // Рассчитываем мировую позицию вершины
    float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.worldPos = worldPos.xyz;
    //Расчитываем нормаль в мировом пространстве
    float3 worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
    o.normal = normalize(worldNormal);
    return o;
}
 
Чтобы проверить наши нормали, теперь мы можем просто вернуть их в наш шейдер фрагмента и увидеть различные оси как цвета.

fixed4 frag(v2f i) : SV_TARGET{
    return fixed4(i.normal.xyz, 1);
}
Чтобы преобразовать нормали к весам для разных проекций, мы начинаем с принятия абсолютного значения нормали. Это потому, что нормали идут в положительном и отрицательном направлениях. Вот почему в нашем отладочном представлении «задняя сторона» нашего объекта, где оси идут в отрицательном направлении, является черным.

float3 weights = i.normal;
weights = abs(weights);
 
После этого мы можем умножать разные проекции с весами, делая их только появляющимися на стороне, на которой мы проецируем его, а не на другие, где текстура выглядит растянутой. Мы умножаем проекцию от плоскости ху к весу z, потому что по отношению к этой оси она не растянута, и аналогично с другими осями.
Мы также удаляем деление на 3, потому что мы не складываем их все вместе.

//Генерируем веса из нормалей
float3 weights = i.normal;
//Показываем текстуру на обоих сторонах объекта (положительной и отрицательной)
weights = abs(weights);

//Комбинируем веса с цветами проекций
col_front *= weights.z;
col_side *= weights.x;
col_top *= weights.y;

//Складываем веса
fixed4 col = col_front + col_side + col_top;

//Применяем оттенок цвета
col *= _Color;
return col;
Это уже хорошо, но теперь у нас опять та же проблема, из за которой мы добавили деление на 3: компоненты нормалей складываются 3 раза, делая текстуру ярче, чем должно быть. Мы можем исправить это, разделив его на сумму его компонентов, заставив его быть не более 1.

//делаем сумму всех компонентов менее 1
weights = weights / (weights.x + weights.y + weights.z);
Теперь мы вернулись к ожидаемой яркости.
Последнее, что мы добавляем к этому шейдеру, - это возможность сделать разные направления более четкими, потому что прямо сейчас область, где они сливаются друг с другом, по-прежнему довольно большая, делая цвета выглядящими беспорядочными. Чтобы сделать это, мы добавляем новое свойство для резкости смешивания. Затем, прежде чем сделать весы суммированными до единицы, мы возводим веса в степень равную резкости. Потому что мы работаем только в диапазоне от 0 до 1, это приведет к снижению низких значений, если резкость высокая, но не изменит высокие значения. Мы делаем свойство типа range, чтобы иметь хороший слайдер в пользовательском интерфейсе шейдера.

//...

_Sharpness("Blend Sharpness", Range(1, 64)) = 1

//...

float _Sharpness;

//...

//Делаем переходы резче
weights = pow(weights, _sharpness)

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

Shader "Tutorial/010_Triplanar_Mapping"{
 //Показываем значения для редактирования в инспекторе
 Properties{
  _Color ("Tint", Color) = (0, 0, 0, 1)
  _MainTex ("Texture", 2D) = "white" {}
  _Sharpness ("Blend sharpness", Range(1, 64)) = 1
 }

 SubShader{
  //маетериал полностью не прозрачный и визуализируется с остальными непрозрачными
  Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

  Pass{
   CGPROGRAM

   #include "UnityCG.cginc"

   #pragma vertex vert
   #pragma fragment frag

   //Текстура и ее тайлинг/смещение
   sampler2D _MainTex;
   float4 _MainTex_ST;

   fixed4 _Color;
   float _Sharpness;

   struct appdata{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
   };

   struct v2f{
    float4 position : SV_POSITION;
    float3 worldPos : TEXCOORD0;
    float3 normal : NORMAL;
   };

   v2f vert(appdata v){
    v2f o;
    // Преобразуем положение вершины из координат объекта в координаты экрана
    o.position = UnityObjectToClipPos(v.vertex); 
    // Рассчитываем мировую позицию вершины
    float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.worldPos = worldPos.xyz;
    // Расчитываем нормаль в мировом пространстве
    float3 worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
    o.normal = normalize(worldNormal);
    return o;
   }

   fixed4 frag(v2f i) : SV_TARGET{
    // Рассчитываем UV-координаты для трех проекций
    float2 uv_front = TRANSFORM_TEX(i.worldPos.xy, _MainTex);
    float2 uv_side = TRANSFORM_TEX(i.worldPos.zy, _MainTex);
    float2 uv_top = TRANSFORM_TEX(i.worldPos.xz, _MainTex);
    
    // Читаем цвет текстуры в трех позициях
    fixed4 col_front = tex2D(_MainTex, uv_front);
    fixed4 col_side = tex2D(_MainTex, uv_side);
    fixed4 col_top = tex2D(_MainTex, uv_top); 
    // Генерируем веса из нормалей
    float3 weights = i.normal;
    // Показываем текстуру на обоих сторонах объекта (положительной и отрицательной)
    weights = abs(weights);
    // Делаем переходы резче
    weights = pow(weights, _Sharpness);
    // делаем сумму всех компонентов менее 1
    weights = weights / (weights.x + weights.y + weights.z);

    // Комбинируем веса с цветами проекций
    col_front *= weights.z;
    col_side *= weights.x;
    col_top *= weights.y;

    // Складываем веса
    fixed4 col = col_front + col_side + col_top;

    // Применяем оттенок цвета
    col *= _Color;

    return col;
   }

   ENDCG
  }
 }
 FallBack "Standard" //fallback добавляет проход рассчета теней
}
Надеюсь, этот урок помог вам понять, как сделать трехпланарный маппинг текстур в Unity.

Здесь вы также можете найти исходный код для этого шейдера: https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/010_Triplanar_Mapping/
Перевод Беляев В.А. ака seaman

Комментариев нет:

Отправить комментарий