Быстрый шейдер снега в Unity
Вы когда-нибудь задумывались, сколько времени займет, чтобы покрыть снегом все текстуры в вашей игре? Наверно не раз. Мы хотим показать вам, как создать Image Effect (шейдер снега в экранном пространстве). Время года на вашей сцене в Unity изменится мгновенно.


Как это работает?
Выше вы видите два скриншота одной и той же сцены. Вторая картинка отличается только тем, что к камере на ней применен эффект снега. Никаких других изменений к текстурам не было применено. Как такое возможно?
Идея проста. Предполагается, что снег должен быть там, где нормаль к визуализированному пикселю смотрит вверх (на земле, на крышах и т.д.). Еще должен быть легкий переход между текстурой снега и первоначальной, если нормаль пикселя смотрит в любом другом направлении (елки, стены).
Получаем нужные данные
Чтобы представленный эффект работал, необходимо минимум две вещи:
- Установить Deferred Rendering (Forward rendering не работает корректно с этим эффектом. Возникнет проблема с шейдером глубины).
- Установить depthTextureMode камеры в режим DepthNormals
Второе условие можно легко установить в самом скрипте эффекта. Ссылка на документацию в Unity. Режим DepthNormals позволит нам читать глубину экрана (как далеко от камеры располагаются пиксели) и нормали (в каком направлении «смотрят» пиксели).
Если вы никогда не создавали fullscreen effect, надо знать, что они строятся как минимум на одном скрипте и одном шейдере. Обычно этот шейдер из полученных данных вместо 3D объекта визуализирует все изображение на экране. В нашем случае полученными данными являются визуализированное камерой изображение и некоторые предустановленные свойства.
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
public class ScreenSpaceSnow : MonoBehaviour
{
public Texture2D SnowTexture;
public Color SnowColor = Color.white;
public float SnowTextureScale = 0.1f;
[Range(0, 1)]
public float BottomThreshold = 0f;
[Range(0, 1)]
public float TopThreshold = 1f;
private Material _material;
void OnEnable()
{
// динамическое создание материала, который будет использовать наш шейдер
_material = new Material(Shader.Find("TKoU/ScreenSpaceSnow"));
// сообщает камере визуализацию глубины и нормалей
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
}
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
//устанавливает свойства шейдера
_material.SetMatrix("_CamToWorld", GetComponent<Camera>().cameraToWorldMatrix);
_material.SetColor("_SnowColor", SnowColor);
_material.SetFloat("_BottomThreshold", BottomThreshold);
_material.SetFloat("_TopThreshold", TopThreshold);
_material.SetTexture("_SnowTex", SnowTexture);
_material.SetFloat("_SnowTexScale", SnowTextureScale);
// выполнить шейдер на входной текстуре (src) и записать на вывод (dest)
Graphics.Blit(src, dest, _material);
}
}
Это лишь основная система, она пока не создает снег. Веселье начинается дальше…
Шейдер снега
Наш шейдер снега является неосвещенным (unlit). Мы не даем ему информацию об освещенности объектов, поскольку в экранном пространстве света нет. Вот основной шаблон:
Shader "TKoU/ScreenSpaceSnow"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
// Выключить отбрасывание и глубину
Cull Off ZWrite Off ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// магия творится здесь
}
ENDCG
}
}
}
Обратите внимание, что при создании нового неосвещенного шейдера в Unity (Create->Shader->Unlit Shader) вы получите почти такой же код.
Давайте теперь сконцентрируемся на важной части — фрагментном шейдере. Для начала нам необходимо захватить все данные, которые передает скрипт ScreenSpaceSnow:
sampler2D _MainTex;
sampler2D _CameraDepthNormalsTexture;
float4x4 _CamToWorld;
sampler2D _SnowTex;
float _SnowTexScale;
half4 _SnowColor;
fixed _BottomThreshold;
fixed _TopThreshold;
half4 frag (v2f i) : SV_Target
{
}
Не переживайте, если не можете понять, зачем это нужно. Сейчас объясню.
Ищем куда положить снег
Как я объяснил ранее, мы хотим положить снег на поверхности, которые направлены вверх. Мы настроили камеру таким образом, чтобы получать данные о нормалях и глубине текстуры, и теперь имеем к ним доступ. В данном случае код выглядит следующим образом:
sampler2D _CameraDepthNormalsTexture;
Почему вызов сделан именно так? Можно прочитать в документации Unity:
Текстуры глубины доступны для сэмплирования в шейдерах как глобальные свойства. Объявив сэмплер под названием _CameraDepthTexture, вы сможете сэмплировать основную текстуру глубины для камеры.
_CameraDepthTexture всегда ссылается на основную текстуру глубины для камеры.
Сейчас давайте получим нормали:
half4 frag (v2f i) : SV_Target
{
half3 normal;
float depth;
DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
normal = mul( (float3x3)_CamToWorld, normal);
return half4(normal, 1);
}
Документация Unity сообщает, что глубина и нормали запакованы в 16-битные пакеты. Чтобы развернуть запись, необходимо вызвать DecodeDepthNormal, как показано выше.
Полученные таким образом нормали являются нормалями в пространстве камеры. Это значит, что при вращении камеры они будут меняться. Нам не нужно, чтобы так происходило, поэтому мы умножаем их на матрицу _CamToWorld, заданную скриптом ранее. Это превратит нормали в мировые координаты, а они не будут зависеть от положения камеры.
Чтобы шейдер был скомпилирован, он должен что-то возвращать, поэтому выше вы можете заметить, что я установил оператор возврата. Чтобы понять, правильны ли наши вычисления, неплохо бы просмотреть результат.

Мы визуализируем это в RGB. В Unity Y по умолчанию смотрит в зенит. Это значит, что зеленым цветом показано значение координаты Y. Неплохо!
Теперь давайте попробуем преобразовать это в коэффициент количества снега.
half4 frag (v2f i) : SV_Target
{
half3 normal;
float depth;
DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
normal = mul( (float3x3)_CamToWorld, normal);
half snowAmount = normal.g;
half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;
snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);
return half4(snowAmount, snowAmount, snowAmount, 1);
}
Конечно, нам надо использовать канал G. Уже должно получиться неплохо, но я хочу пойти дальше и сделать настройку в пределах максимума и минимума покрытой снегом поверхности. Это позволит задавать, сколько снега должно быть на сцене.

Текстура снега
Без текстуры снег может выглядеть нереалистично. Это самая сложная часть: как сообщить текстуру трехмерным объектам, если у вас только двумерное изображение (мы ведь работаем в экранном пространстве, помните)? Один из способов — найти мировое положение пикселя. Затем мы можем использовать мировые координаты X и Y как координаты текстуры.
half4 frag (v2f i) : SV_Target
{
half3 normal;
float depth;
DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
normal = mul( (float3x3)_CamToWorld, normal);
// ищет количество снега
half snowAmount = normal.g;
half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;
snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);
// находит цвет снега
float2 p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);
float3 vpos = float3( (i.uv * 2 - 1) / p11_22, -1) * depth;
float4 wpos = mul(_CamToWorld, float4(vpos, 1));
wpos += float4(_WorldSpaceCameraPos, 0) / _ProjectionParams.z;
half3 snowColor = tex2D(_SnowTex, wpos.xz * _SnowTexScale * _ProjectionParams.z) * _SnowColor;
return half4(snowColor, 1);
}
Здесь задействованы вычисления, которые выходят за рамки данной статьи. Вам нужно знать только то, что vpos является положением области просмотра, wpos — это мировые координаты, полученные умножением матрицы _CamToWorld на положение области просмотра и преобразованные в действительное мировое положение путем деления на дальнюю плоскость (_ProjectionParams.z). Наконец, мы вычисляем цвет снега, используя координаты XZ по настраиваемому параметру _SnowTexScale и дальней плоскости, чтобы получить нормальное значение. Фух…

Соединяем все!
Пришло время соединить все вместе!
half4 frag (v2f i) : SV_Target
{
half3 normal;
float depth;
DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
normal = mul( (float3x3)_CamToWorld, normal);
// ищет количество снега
half snowAmount = normal.g;
half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;
snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);
// ищет цвет снега
float2 p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);
float3 vpos = float3( (i.uv * 2 - 1) / p11_22, -1) * depth;
float4 wpos = mul(_CamToWorld, float4(vpos, 1));
wpos += float4(_WorldSpaceCameraPos, 0) / _ProjectionParams.z;
wpos *= _SnowTexScale * _ProjectionParams.z;
half4 snowColor = tex2D(_SnowTex, wpos.xz) * _SnowColor;
// получить цвет и интерполяцию для текстуры снега
half4 col = tex2D(_MainTex, i.uv);
return lerp(col, snowColor, snowAmount);
}
Здесь мы получаем первоначальный цвет и его линейную интерполяцию для snowColor, используя snowAmount.

Финальный штрих: давайте зададим значение _TopThreshold равным 0.6:

Вуаля!
Заключение
Вот результат для всей сцены. Красиво?


Вы можете скачать шейдер здесь и свободно использовать его в своем проекте!
Источник: http://blog.theknightsofunity.com/make-it-snow-fast-screen-space-snow-shader/
