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

Читаем глубину и нормаль.
Мы начинаем этот урок с файлов из предыдущего урока, и будем расширять их по мере необходимости.
Первое изменение - удалить весь код из скрипта C#, который мы использовали для перемещения волны. Затем мы не будем указывать камере, отображать глубину объектов, вместо этого мы сделаем так, чтобы она отображала текстуру, которая включает в себя как глубину, так и нормали.
private void Start(){
//берем камеру и указываем ей визуализировать текстуру глубины/нормалей
cam = GetComponent();
cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.DepthNormals;
}
И это все настройки, которые нам нужны для доступа к нормалям. Затем мы редактируем шейдер.
Мы также удалим весь код, используемый для волновой функции. Затем мы переименуем _CameraDepthTexture в _CameraDepthNormalsTexture, чтобы она заполнялась Unity.
//Показываем значение в инспекторе
Properties{
[HideInInspector]_MainTex ("Texture", 2D) = "white" {}
}
…
//текстура глубины/нормали
sampler2D _CameraDepthNormalsTexture;
С помощью этой настройки мы теперь можем читать текстуру глубины внутри нашего фрагментарного шейдера. Если мы просто сделаем это и просто нарисуем текстуру на экране, мы уже увидим что-то интересное.
//фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
//читаем глубину/нормаль
float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);
return depthnormal;
}

Но то, что мы можем видеть, - это не то, что мы действительно хотим, мы видим только красные и зеленые значения и немного синего на расстоянии. Это потому, что, как видно из названия, эта текстура содержит нормали, а также текстуру глубины, поэтому мы должны сначала ее декодировать. К счастью, Unity дает нам метод, который делает именно это. Мы должны дать ей значение глубины/нормали, а также два других значения, в которые функция будет писать глубину и нормали.
В отличие от текстуры глубины, значение глубины, которое мы имеем сейчас, уже линейно между камерой и дальним планом, поэтому мы можем легко адаптировать код из предыдущего урока, чтобы снова получить расстояние от камеры.
// фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
// читаем глубину/нормаль
float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);
//декодируем глубину/нормаль
float3 normal;
float depth;
DecodeDepthNormal(depthnormal, depth, normal);
//получаем глубину как дистанцию от камеры
depth = depth * _ProjectionParams.z;
return depth;
}

Но вернемся к использованию нормалей. Когда мы просто выведем нормали как цвета на экране, мы получим довольно хороший результат.
// фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
// читаем глубину/нормаль
float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);
// декодируем глубину/нормаль
float3 normal;
float depth;
DecodeDepthNormal(depthnormal, depth, normal);
// получаем глубину как дистанцию от камеры
depth = depth * _ProjectionParams.z;
return float4(normal, 1);
}

Но если мы поворачиваем камеру, мы можем видеть, что точка на поверхности не всегда имеет одни значения нормали, потому что нормали хранятся относительно камеры. Поэтому, если мы хотим получить нормаль в мире, мы должны пойти на дополнительные шаги.
В мировое пространство.
Мы можем легко преобразовать наши нормали из системы координат камеры в мировое пространство, но, к сожалению, Unity не дает нам функции для этого, поэтому мы должны сделать это сами в нашем шейдере. Итак, мы вернемся к нашему скрипту C# и передадим в шейдер нужные значения.
Сначала мы получаем ссылку на нашу камеру. Мы уже получили камеру в нашем методе Start, поэтому мы можем непосредственно сохранить ее в переменной класса. Затем в методе OnRenderImage мы получаем матрицу преобразования из пространства просмотра в мировое пространство, и передаем ее в наш шейдер. Причина, по которой мы не можем передать матрицу в наш шейдер один раз в методе Start, заключается в том, что мы хотим перемещать и поворачивать нашу камеру после запуска эффекта, и матрица изменяется, когда мы это делаем.
using UnityEngine;
//Скрипт лежит на объекте камеры
public class NormalPostprocessing : MonoBehaviour {
//Материал, который применяется при постпроцессинге
[SerializeField]
private Material postprocessMaterial;
private Camera cam;
private void Start(){
// берем камеру и указываем ей визуализировать текстуру глубины/нормалей
cam = GetComponent();
cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.DepthNormals;
}
//метод автоматически вызывается после того как камера орендерила
private void OnRenderImage(RenderTexture source, RenderTexture destination){
//Получаем матрицу преобразования и передаем ее в шейдер
Matrix4x4 viewToWorld = cam.cameraToWorldMatrix;
postprocessMaterial.SetMatrix("_viewToWorld", viewToWorld);
//Рисуем пиксель из текстуры источника в текстуру приемник
Graphics.Blit(source, destination, postprocessMaterial);
}
}
Затем мы можем использовать эту матрицу в нашем шейдере. Мы добавляем для него новую переменную, а затем умножаем ее на нормаль перед ее использованием. Мы преобразуем ее в матрицу 3x3 перед умножением, поэтому изменение позиции не применяется, а только вращение. Это все, что нам нужно для нормалей.
//матрица для конвертирования из пространства просмотра в мировое пространство
float4x4 _viewToWorld;
normal = normal = mul((float3x3)_viewToWorld, normal);
return float4(normal, 1);
}

Цвет верха.
Теперь, когда у нас есть нормали мира, мы можем сделать простой эффект. Мы можем покрасить верхнюю часть всех объектов в сцене в определенный цвет.
Для этого мы просто сравниваем нормаль с вектором вверх. Мы делаем это через dot продукт, который возвращает 1, когда оба нормированных вектора указывают в одном направлении (когда поверхность плоская), 0, когда они ортогональны (в нашем случае на стенах) и -1, когда они противоположны каждому другой (в нашем случае это означает крышу над камерой).
float up = dot(float3(0,1,0), normal);
return up;
}

Чтобы сделать более очевидным то, что сверху, а что нет, теперь мы можем взять это плавное значение и использовать step, чтобы отличать верхнюю и верхнюю части. Если второе значение меньше, оно вернет 0, и мы увидим черное, если оно больше, мы увидим белый.
float up = dot(float3(0,1,0), normal);
up = step(0.5, up);
return up;

Следующий шаг - вернуть исходные цвета, где мы не определили верх поверхности. Для этого мы просто читаем основную текстуру, а затем делаем линейную интерполяцию между этим цветом и цветом, который мы определяем, сверху (белый на данный момент).
float up = dot(float3(0,1,0), normal);
up = step(0.5, up);
float4 source = tex2D(_MainTex, i.uv);
float4 col = lerp(source, float4(1,1,1,1), up);
return col;

Настраиваемость.
И в качестве последнего шага мы добавим некоторую настраиваемость. Поэтому мы добавляем свойство и глобальную переменную для значения обрезания направления вверх и для верхнего цвета.
_upCutoff ("up cutoff", Range(0,1)) = 0.7
_topColor ("top color", Color) = (1,1,1,1)
//настройка эффекта
float _upCutoff;
float4 _topColor;
Затем мы заменяем фиксированный 0,5, который мы использовали ранее для нашего значения отсечки с новой переменной отсечки, и линейно интерполируем на верхний цвет вместо белого цвета. Затем мы также можем умножить цвет вверх с альфа-значением верхнего цвета, таким образом, когда мы понижаем альфа-значение, верхняя часть пропускает часть исходного цвета.
float up = dot(float3(0,1,0), normal);
up = step(_upCutoff, up);
float4 source = tex2D(_MainTex, i.uv);
float4 col = lerp(source, _topColor, up * _topColor.a);
return col;
}

Этот эффект был в основном сделан, чтобы показать, как работает текстура depthnormals. Если вам нужен эффект снега, то, вероятно, лучше просто сделать это в шейдере для объекта, на котором снег включен вместо эффекта постпроцессинга. Извините, я не придумал лучшего примера.
Исходники.
using UnityEngine;
// Скрипт лежит на объекте камеры
public class NormalPostprocessing : MonoBehaviour {
// Материал, который применяется при постпроцессинге
[SerializeField]
private Material postprocessMaterial;
private Camera cam;
private void Start(){
// берем камеру и указываем ей визуализировать текстуру глубины/нормалей
cam = GetComponent();
cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.DepthNormals;
}
// метод автоматически вызывается после того как камера орендерила
private void OnRenderImage(RenderTexture source, RenderTexture destination){
// получаем матрицу преобразования и передаем ее в шейдер
Matrix4x4 viewToWorld = cam.cameraToWorldMatrix;
postprocessMaterial.SetMatrix("_viewToWorld", viewToWorld);
//Рисуем пиксель из текстуры источника в текстуру приемник
Graphics.Blit(source, destination, postprocessMaterial);
}
}
Shader "Tutorial/018_Normal_Postprocessing"{
//Показываем значения в инспекторе
Properties{
[HideInInspector]_MainTex ("Texture", 2D) = "white" {}
_upCutoff ("up cutoff", Range(0,1)) = 0.7
_topColor ("top color", Color) = (1,1,1,1)
}
SubShader{
// Нам не нужен кулинг или запись/сравнение буфера глубины
Cull Off
ZWrite Off
ZTest Always
Pass{
CGPROGRAM
//включаем полезные функции
#include "UnityCG.cginc"
//определяем вершинный и фрагментный шейдер
#pragma vertex vert
#pragma fragment frag
// текстура отрендеренного экрана
sampler2D _MainTex;
//матрица для конвертирования из пространства камеры в мировое пространство
float4x4 _viewToWorld;
//текстура глубины/нормалей
sampler2D _CameraDepthNormalsTexture;
//настройка эффекта
float _upCutoff;
float4 _topColor;
//the object data that's put into the vertex shader
struct appdata{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
//Данные для фрагментного шейдера
struct v2f{
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
//вертексный шейдер
v2f vert(appdata v){
v2f o;
//Конвертируем позицию из пространства объекта в пространство экрана
o.position = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
//фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
//читаем глубину/нормаль
float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);
//декодируем глубину/нормаль
float3 normal;
float depth;
DecodeDepthNormal(depthnormal, depth, normal);
//получаем глубину как дистанцию от камеры
depth = depth * _ProjectionParams.z;
normal = mul((float3x3)_viewToWorld, normal);
float up = dot(float3(0,1,0), normal);
up = step(_upCutoff, up);
float4 source = tex2D(_MainTex, i.uv);
float4 col = lerp(source, _topColor, up * _topColor.a);
return col;
}
ENDCG
}
}
}
Надеюсь, я смог передать, как получить доступ к обычным текстурам и это станет прочной основой для будущих эффектов.
Вы также можете найти исходники здесь: https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/018_NormalPostprocessing/NormalPostprocessing.cs https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/018_NormalPostprocessing/NormalPostprocessing.shader
Переводчик Беляев В.А. ака seaman
Комментариев нет:
Отправить комментарий