Быстрый шейдер снега в Unity

Быстрый шейдер снега в Unity

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

сцена без снега

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

Мы визуализируем это в 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.

снег в Unity

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

быстрый шейдер снега

Вуаля!

Заключение

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

вся сцена без снега

вся сцена со снегом

Вы можете скачать шейдер здесь и свободно использовать его в своем проекте!

Источник: http://blog.theknightsofunity.com/make-it-snow-fast-screen-space-snow-shader/

Понравилась статья? Поделиться с друзьями:
Автор natalya
Переводит для Вас самые интересные статьи про разработку игр. По образованию физик-программист. Техническими переводами начала подрабатывать еще на старших курсах и постепенно это переросло в основное занятие. Интересуется гуманитарными технологиями, пробует себя в журналистике.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *