Введение.
Один из моих любимых эффектов постпроцессинга - контуры. Выполнение контуров с помощью постобработки имеет много преимуществ. Лучше обнаруживать края в постпроцессинге - вам не нужно менять все ваши материалы, чтобы придать им контурный эффект.
Чтобы понять, как создавать контуры с помощью постобработки, лучше всего сначала понять, как получить доступ к глубине и нормалям сцены.
Глубина контура.
Мы начинаем с шейдера и скрипта C# из урока постпроцесингу с нормалями.
Первые изменения, которые мы делаем, это удаляем свойства и переменные, которые были специфичны для шейдера «цвет сверху». Т.е. значение отсечки и цвета. Мы также удаляем матрицу преобразования из пространства камеры в мировое пространство, потому что наши контуры не имеют определенного вращения в мире, поэтому мы можем его игнорировать. Затем мы удаляем весь код после той части, где мы вычисляем глубину и нормали.
// показываем значения для редактирования в инспекторе
Properties{
[HideInInspector]_MainTex ("Texture", 2D) = "white" {}
}
//фрагентный шейдер
fixed4 frag(v2f i) : SV_TARGET{
// читаем глубину/нормаль
float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);
//декодируем глубину / нормаль
float3 normal;
float depth;
DecodeDepthNormal(depthnormal, depth, normal);
// получаем глубину как расстояние от камеры
depth = depth * _ProjectionParams.z;
}
Затем мы удаляем часть, где мы записываем матрицу камеры в шейдер из нашего сценария C#.
// метод который автоматически вызывается Unity после того как камера все отрендерила
private void OnRenderImage(RenderTexture source, RenderTexture destination){
// рисуем пиксели из текстуры источника в текстуру приемник
Graphics.Blit(source, destination, postprocessMaterial);
}
То, как мы собираемся рассчитывать контуры, состоит в том, что мы будем читать из нескольких пикселей вокруг пикселя, который мы обсчитываем, и вычисляем разницу в глубине и нормалях к центральному пикселю. Чем они сильнее отличаются друг от друга, тем сильнее контур.
Чтобы вычислить положение соседних пикселей, нам нужно знать, насколько большой пиксель. К счастью, мы можем просто добавить переменную с определенным именем, и Unity сообщит нам размер. Поскольку технически мы работаем с пикселями текстуры (текселями), это называется размером текселя.
Мы можем просто создать переменную texturename_TexelSize для любой текстуры и получить размер текселя.
//Текстура глубины/нормалей
sampler2D _CameraDepthNormalsTexture;
//размер текселя текстуры
float4 _CameraDepthNormalsTexture_TexelSize;
Затем мы скопируем код для доступа к глубине и нормалям, но изменим имена, и получим пиксель из текстуры по координатам сдвинутым вправо.
//читаем соседний пиксель
float4 neighborDepthnormal = tex2D(_CameraDepthNormalsTexture,
uv + _CameraDepthNormalsTexture_TexelSize.xy * offset);
float3 neighborNormal;
float neighborDepth;
DecodeDepthNormal(neighborDepthnormal, neighborDepth, neighborNormal);
neighborDepth = neighborDepth * _ProjectionParams.z;
Теперь, когда у нас есть два образца, мы можем вычислить разницу и нарисовать ее на экране.
float difference = depth - neightborDepth;
return difference;
Мы уже видим контуры в левой части объектов. Прежде чем перейти к следующему образцу, я хотел бы поместить код для чтения образца и сравнения его с центральным значением в отдельную функцию, чтобы нам не нужно было писать его 4 раза. Эта функция нуждается в глубине центрального пикселя, координатах uv центрального пикселя и смещении в качестве аргументов. Мы будем определять смещение в пикселях.
Мы просто копируем код из нашей фрагментной функции в новый метод и заменяем имена глубины и uv именами аргументов. Чтобы использовать смещение, мы умножаем его на координаты x и y размера текселя, а затем добавляем результат к координатам uv, как и ранее.
После того как мы настроим новый метод, мы вызываем его в фрагментной функции и выводим результат на экран.
void Compare(float baseDepth, float2 uv, float2 offset){
// читаем соседний пиксель
float4 neighborDepthnormal = tex2D(_CameraDepthNormalsTexture,
uv + _CameraDepthNormalsTexture_TexelSize.xy * offset);
float3 neighborNormal;
float neighborDepth;
DecodeDepthNormal(neighborDepthnormal, neighborDepth, neighborNormal);
neighborDepth = neighborDepth * _ProjectionParams.z;
return baseDepth - neighborDepth;
}
…
float depthDifference = Compare(depth, i.uv, float2(1, 0));
return depthDifference;
}
Результат должен выглядеть так же, как и раньше, но теперь проще расширить шейдер, чтобы читать образцы в нескольких направлениях. Аналогично мы выбираем пиксели вверх, вправо и вниз и добавляем результаты всех образцов вместе.
// фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
// читаем глубину/нормаль
float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);
// декодируем глубину/нормаль
float3 normal;
float depth;
DecodeDepthNormal(depthnormal, depth, normal);
// получаем глубину как расстояние от камеры
depth = depth * _ProjectionParams.z;
float depthDifference = Compare(depth, i.uv, float2(1, 0));
depthDifference = depthDifference + Compare(depth, i.uv, float2(0, 1));
depthDifference = depthDifference + Compare(depth, i.uv, float2(0, -1));
depthDifference = depthDifference + Compare(depth, i.uv, float2(-1, 0));
return depthDifference;
}
Нормали контура.
Использование глубины уже дает нам довольно хорошие очертания, но мы можем пойти дальше, используя также предоставленные нам нормали. Нормали мы также будем выбирать в нашей функции сравнения. Однако функция может возвращать только одно значение в hlsl, поэтому мы не можем использовать возвращаемое значение здесь. Вместо использования возвращаемого значения мы можем добавить два новых аргумента с ключевым словом inout. С помощью этого ключевого слова значение, которое мы передаем в функцию, может быть перезаписано в ней, и изменения применяются к версии переданной в функцию переменной, а не только к ее версии в функции. Еще одна вещь, которая нам нужна для создания контуров из нормалей, - это контур центрального пикселя, поэтому мы добавляем это также к списку наших аргументов.
void Compare(inout float depthOutline, inout float normalOutline,
float baseDepth, float3 baseNormal, float2 uv, float2 offset){
После того, как мы изменили это, вернемся к фрагментной функции, создадим новую переменную для разницы нормалей и изменим способ, которым мы вызываем метод сравнения, чтобы соответствовать нашим новым аргументам.
void Compare(inout float depthOutline, inout float normalOutline,
float baseDepth, float3 baseNormal, float2 uv, float2 offset){
// читаем соседний пиксель
float4 neighborDepthnormal = tex2D(_CameraDepthNormalsTexture,
uv + _CameraDepthNormalsTexture_TexelSize.xy * offset);
float3 neighborNormal;
float neighborDepth;
DecodeDepthNormal(neighborDepthnormal, neighborDepth, neighborNormal);
neighborDepth = neighborDepth * _ProjectionParams.z;
float depthDifference = baseDepth - neighborDepth;
depthOutline = depthOutline + depthDifference;
}
float depthDifference = 0;
float normalDifference = 0;
Compare(depthDifference, normalDifference, depth, normal, i.uv, float2(1, 0));
Compare(depthDifference, normalDifference, depth, normal, i.uv, float2(0, 1));
Compare(depthDifference, normalDifference, depth, normal, i.uv, float2(0, -1));
Compare(depthDifference, normalDifference, depth, normal, i.uv, float2(-1, 0));
return depthDifference;
Это не изменит выход метода, но новая архитектура позволяет нам также измерять разницу в нормалях. Легкий и быстрый способ сравнить два нормализованных вектора - взять dot продукт. Проблема dot продукт заключается в том, что когда векторы указывают в одном и том же направлении, он равен 1, а когда векторы отходят друг от друга, он становится меньше. Это противоположно тому, что мы хотим. Исправление состоит в том, чтобы вычесть dot продукт из 1. Тогда, когда результатом dot произведения является 1, общий результат равен 0, а когда результат dot произведения становится ниже, общий результат увеличивается. После того, как мы вычислим нормальную разницу, мы добавим ее в общую разницу, и изменим результат, чтобы показать разницу нормалей.
float3 normalDifference = baseNormal - neighborNormal;
normalDifference = normalDifference.r + normalDifference.g + normalDifference.b;
normalOutline = normalOutline + normalDifference;
return normalDifference;
С этими изменениями мы можем видеть контуры, но они отличаются от тех что у нас были раньше, потому что они генерируются из нормалей вместо глубины. Затем мы можем объединить два контура для создания комбинированного контура.
return depthDifference + normalDifference;
Настройка контура.
Следующий шаг - сделать контуры более настраиваемыми. Чтобы сделать это, мы добавляем две переменные для контуров глубины и нормалей. Множитель, чтобы контуры выглядели сильнее или слабее, и смещение, которое может сделать серее те части контуров, которые мы, возможно, не захотим потерять.
// показываем значения для редактирования в инспекторе
Properties{
[HideInInspector]_MainTex ("Texture", 2D) = "white" {}
_NormalMult ("Normal Outline Multiplier", Range(0,4)) = 1
_NormalBias ("Normal Outline Bias", Range(1,4)) = 1
_DepthMult ("Depth Outline Multiplier", Range(0,4)) = 1
_DepthBias ("Depth Outline Bias", Range(1,4)) = 1
}
// переменные для настройки эффекта
float _NormalMult;
float _NormalBias;
float _DepthMult;
float _DepthBias;
Чтобы использовать переменные, после получения всех разностей пикселей, мы просто умножаем разностные переменные на множители, затем ограничиваем их между 0 и 1 и передаем разницу в степень смещения. Ограничение между 0 и 1 важно, потому что в противном случае показатель экспоненты отрицательного числа может привести к неверным результатам. У HLSL есть собственная функция для фиксации переменной между 0 и 1, называемая «saturate».
depthDifference = depthDifference * _DepthMult;
depthDifference = saturate(depthDifference);
depthDifference = pow(depthDifference, _DepthBias);
normalDifference = normalDifference * _NormalMult;
normalDifference = saturate(normalDifference);
normalDifference = pow(normalDifference, _NormalBias);
return depthDifference + normalDifference;
С этим вы теперь можете немного настроить свои контуры в инспекторе - я немного увеличил контуры как нормалей, так и глубины, и уменьшил шум, увеличив смещение, но лучше всего поиграть с настройками и посмотреть, что лучше подходит для вашей сцены.
Наконец, мы хотим добавить наши контуры на сцену, а не просто иметь их как отдельную вещь. Для этого мы сначала объявляем цвет контура как свойство и переменную шейдера.
_OutlineColor ("Outline Color", Color) = (0,0,0,1)
float4 _OutlineColor;
Чтобы применить контуры, в конце фрагментной функции, мы читаем исходную текстуру и выполняем линейную интерполяцию от исходного цвета к нашему контурному цвету через комбинированный контур, таким образом, пиксели, которые ранее были черными, теперь являются исходным цветом а белые имеют контурный цвет.
float outline = normalDifference + depthDifference;
float4 sourceColor = tex2D(_MainTex, i.uv);
float4 color = lerp(sourceColor, _OutlineColor, outline);
return color;
Основными недостатками постпроцессных контуров являются то, что вы должны применять их ко всем объектам в сцене. Как система решает, что контур, а что нет, может не соответствовать стилю, который вы имеете в виду, и вы получаете артефакты алиасинга (видимые «лесенки») довольно быстро.
Хотя нет никаких простых исправлений для первых двух проблем, вы можете смягчить последний из них, используя сглаживание в вашей постобработке, например, FXAA или TXAA (стеки постобработки Unity предоставляют их вам, но если вы используете v2, вам придется переделать эффект на эффект в стеке).
Еще один важный момент, который следует иметь в виду, заключается в том, что вам нужно использовать модели, которые соответствуют этому способу построения контуров - если вы добавите слишком много деталей в свою геометрию, эффект будет рисовать большую часть ваших объектов черным, что, вероятно, не является предполагаемым поведением.
Исходники.
Shader "Tutorial/019_OutlinesPostprocessed"
{
//показываем значения для редактирования в инспекторе
Properties{
[HideInInspector]_MainTex ("Texture", 2D) = "white" {}
_OutlineColor ("Outline Color", Color) = (0,0,0,1)
_NormalMult ("Normal Outline Multiplier", Range(0,4)) = 1
_NormalBias ("Normal Outline Bias", Range(1,4)) = 1
_DepthMult ("Depth Outline Multiplier", Range(0,4)) = 1
_DepthBias ("Depth Outline Bias", Range(1,4)) = 1
}
SubShader{
// Показываем что нам не нужен кулинг и сравнение/запись в буфер глубины
Cull Off
ZWrite Off
ZTest Always
Pass{
CGPROGRAM
// включаем полезные фугкции
#include "UnityCG.cginc"
// определяем вершинный и фрагментный шейдеры
#pragma vertex vert
#pragma fragment frag
// текстура для визуализации экрана
sampler2D _MainTex;
// текстура глубины/нормалей
sampler2D _CameraDepthNormalsTexture;
//размер текселя текстуры глубины/нормалей
float4 _CameraDepthNormalsTexture_TexelSize;
//переменный для настройки эффекта
float4 _OutlineColor;
float _NormalMult;
float _NormalBias;
float _DepthMult;
float _DepthBias;
// данные передаваемые в вершинный шейдер
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;
}
void Compare(inout float depthOutline, inout float normalOutline,
float baseDepth, float3 baseNormal, float2 uv, float2 offset){
//читаем соседний пиксель
float4 neighborDepthnormal = tex2D(_CameraDepthNormalsTexture,
uv + _CameraDepthNormalsTexture_TexelSize.xy * offset);
float3 neighborNormal;
float neighborDepth;
DecodeDepthNormal(neighborDepthnormal, neighborDepth, neighborNormal);
neighborDepth = neighborDepth * _ProjectionParams.z;
float depthDifference = baseDepth - neighborDepth;
depthOutline = depthOutline + depthDifference;
float3 normalDifference = baseNormal - neighborNormal;
normalDifference = normalDifference.r + normalDifference.g + normalDifference.b;
normalOutline = normalOutline + normalDifference;
}
//фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
// читаем глубину/нормаль
float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);
//декодируем глубину/нормаль
float3 normal;
float depth;
DecodeDepthNormal(depthnormal, depth, normal);
// получаем глубину как расстояние от камеры
depth = depth * _ProjectionParams.z;
float depthDifference = 0;
float normalDifference = 0;
Compare(depthDifference, normalDifference, depth, normal, i.uv, float2(1, 0));
Compare(depthDifference, normalDifference, depth, normal, i.uv, float2(0, 1));
Compare(depthDifference, normalDifference, depth, normal, i.uv, float2(0, -1));
Compare(depthDifference, normalDifference, depth, normal, i.uv, float2(-1, 0));
depthDifference = depthDifference * _DepthMult;
depthDifference = saturate(depthDifference);
depthDifference = pow(depthDifference, _DepthBias);
normalDifference = normalDifference * _NormalMult;
normalDifference = saturate(normalDifference);
normalDifference = pow(normalDifference, _NormalBias);
float outline = normalDifference + depthDifference;
float4 sourceColor = tex2D(_MainTex, i.uv);
float4 color = lerp(sourceColor, _OutlineColor, outline);
return color;
}
ENDCG
}
}
}
using UnityEngine;
using System;
//скрипт котоый висит на камеры
public class OutlinesPostprocessed : MonoBehaviour {
// материал который используется для постпроцесинга
private Material postprocessMaterial;
private Camera cam;
private void Start(){
// получаем камеру и говорим ей визуализировать текстуру глубины/нормалей
cam = GetComponent();
cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.DepthNormals;
}
// метод, который автоматически вызывается Unity после камера закончила визуализацию
private void OnRenderImage(RenderTexture source, RenderTexture destination){
// рисуем пиксели их текстуры-источника в текстуру приемник
Graphics.Blit(source, destination, postprocessMaterial);
}
}
Надеюсь, я смог показать вам, как добавить приятные контуры в вашу игру и как они работают.
Вы также можете найти исходники здесь:
https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/019_OutlinesPostprocessed/OutlinesPostprocessed.shader
https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/019_OutlinesPostprocessed/OutlinesPostprocessed.cs
Перевод Беляев В.А. ака seaman.
Комментариев нет:
Отправить комментарий