Быстрый шейдер снега в 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/