Эти примеры шейдеров для встроенного конвейера рендеринга демонстрируют основы написания пользовательских шейдеров и охватывают распространенные варианты использования.
Информацию о написании шейдеров см. в разделе Написание шейдеров.
Настройка сцены
Первый шаг — создать несколько объектов, которые вы будете использовать для тестирования шейдеров. Выберите Игровой объект > 3D-объект3D-игровой объект, например куб, ландшафт или тряпичная кукла. Подробнее
См. Словарь > Капсулы в главном меню . Затем расположите камеру так, чтобы она показывала капсулу. Дважды щелкните Капсулу в Иерархии, чтобы
сфокусируйте на нем вид сцены, затем выберите объект «Основная камера» и нажмите Игровой объект > Выровнять по виду
из главного меню.
Создайте новый Материал, выбрав Создать > МатериалРесурс, определяющий способ визуализации поверхности. Подробнее
См. в Словарь из меню в представлении проекта. Новый материал под названием Новый материал появится в представлении проекта.
Создание шейдера
Теперь аналогичным образом создайте новый ресурс Shader. Выберите Создать > ШейдерПрограмма, работающая на графическом процессоре. Подробнее
См. в Словарь > Неосвещенный шейдер в меню в представлении проекта.
Это создает базовый шейдер, который просто отображает текстуру без освещения.
Другие пункты в меню Создать > Шейдер создают базовые шейдеры.
или другие типы, например базовый поверхностный шейдерупрощенный способ написания шейдеры для встроенного конвейера рендеринга. Подробнее
См. в Словарь.
Связывание сетки, материала и шейдера
Заставьте материал использовать шейдер с помощью инспектораматериала. Окно Unity, в котором отображается информация о текущем выбранном игровом объекте, активе или настройки проекта, позволяющие просматривать и редактировать значения. Дополнительная информация
См. в Словарь или просто перетащите шейдер на материал в Просмотр проекта. Инспектор материалов будет отображать белую сферу при использовании этого шейдера.
Теперь перетащите материал на сеткуосновной графический примитив Unity. Меши составляют большую часть ваших 3D-миров. Unity поддерживает триангулированные или четырехугольные полигональные сетки. Поверхности Nurbs, Nurms, Subdiv должны быть преобразованы в полигоны. Подробнее
См. в объекте Словарь либо в СценаСцена содержит окружение и меню вашей игры. Думайте о каждом уникальном файле сцены как об уникальном уровне. В каждой сцене вы размещаете свое окружение, препятствия и декорации, по сути проектируя и создавая свою игру по частям. Подробнее
См. в Словарь или в представлениях Hierarchy. Либо выберите объект и в инспекторе заставьте его использовать материал из Mesh RendererA компонент сетки, который берет геометрию из фильтра сетки и визуализирует ее в положении, определенном компонентом преобразования объекта. Подробнее
См. в разделе Словарь Материалы компонента.
После того как все это настроено, вы можете приступить к просмотру кода шейдера и увидеть результаты ваших изменений в шейдере на капсуле в Scene ViewИнтерактивный взгляд на мир, который вы создаете. Вы используете Scene View для выбора и размещения пейзажей, персонажей, камер, источников света и всех других типов игровых объектов. Дополнительная информация
См. в Словарь.
Основные части шейдера
Чтобы начать изучение кода шейдера, дважды щелкните ресурс шейдера в представлении проекта. Код шейдера откроется в вашем редакторе скриптов (MonoDevelop или Visual Studio).
Шейдер начинается с этого кода:
Shader "Unlit/NewUnlitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Этот исходный шейдер не выглядит очень простым! Но не волнуйтесь, мы рассмотрим каждую часть шаг за шагом.
Давайте рассмотрим основные части нашего простого шейдера.
Shader
Команда Shader содержит строку с именем шейдера. Вы можете использовать символы косой черты «/», чтобы поместить шейдер в подменю при выборе шейдера в инспекторе Материалов.
Properties
Блок Properties содержит переменные шейдера (текстуры, цвета и т. д.), которые будут сохранены как часть материала и отображены в инспекторе материалов. В нашем неосвещенном шаблоне шейдера объявлено одно свойство текстуры.
SubShader
Шейдер может содержать один или несколько подшейдеров, которые в основном используются для реализации шейдеров для различных возможностей графического процессора. В этом уроке нас это мало волнует, поэтому все наши шейдеры будут содержать только один субшейдер.
Pass
Каждый вложенный шейдер состоит из нескольких проходов, и каждый проход представляет собой выполнение кода вершин и фрагментов для одного и того же объекта, визуализируемого с использованием материала шейдера. . Многие простые шейдеры используют только один проход, но шейдерам, взаимодействующим с освещением, может потребоваться больше (подробности см. в конвейере освещения). Команды внутри Pass обычно устанавливают фиксированное состояние функции, например, режимы наложения.
CGPROGRAM .. ENDCG
Эти ключевые слова окружают части кода HLSL в вершинных и фрагментных шейдерах. Обычно именно здесь находится большая часть интересного кода. Дополнительные сведения см. в разделе вершинные и фрагментные шейдеры.
Простой неосвещенный шейдер
Шаблон неосвещенного шейдера делает еще несколько вещей, которые абсолютно необходимы для отображения объекта с текстурой. Например, он поддерживает туман и поля тайлинга/смещения текстуры в материале. Давайте упростим шейдер до минимума и добавим больше комментариев:
Shader "Unlit/SimpleUnlitTexturedShader"
{
Properties
{
// we have removed support for texture tiling/offset,
// so make them not be displayed in material inspector
[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
CGPROGRAM
// use "vert" function as the vertex shader
#pragma vertex vert
// use "frag" function as the pixel (fragment) shader
#pragma fragment frag
// vertex shader inputs
struct appdata
{
float4 vertex : POSITION; // vertex position
float2 uv : TEXCOORD0; // texture coordinate
};
// vertex shader outputs ("vertex to fragment")
struct v2f
{
float2 uv : TEXCOORD0; // texture coordinate
float4 vertex : SV_POSITION; // clip space position
};
// vertex shader
v2f vert (appdata v)
{
v2f o;
// transform position to clip space
// (multiply with model*view*projection matrix)
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
// just pass the texture coordinate
o.uv = v.uv;
return o;
}
// texture we will sample
sampler2D _MainTex;
// pixel shader; returns low precision ("fixed4" type)
// color ("SV_Target" semantic)
fixed4 frag (v2f i) : SV_Target
{
// sample texture and return it
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
Вершинный шейдерПрограмма, которая запускается для каждой вершины 3D-модели во время рендеринга модели. Подробнее
См. в Словарь — это программа, которая запускается для каждой вершины 3D-модели. Довольно часто ничем особо интересным он не занимается. Здесь мы просто преобразуем позицию вершины из пространства объекта в так называемое «пространство отсечения», которое используется графическим процессором для растрирования объекта на экране. Мы также передаем исходную координату текстуры без изменений — она понадобится нам для выборки текстуры во фрагментном шейдере.
Фрагментный шейдер – это программа, которая работает с каждым пикселемнаименьшей единицей в компьютерное изображение. Размер пикселя зависит от разрешения вашего экрана. Пиксельное освещение рассчитывается для каждого пикселя экрана. Подробнее
Посмотрите в Словарь, что объект занимает на экране и обычно используется для вычисления и вывести цвет каждого пикселя. Обычно на экране миллионы пикселей, и выполняются фрагментные шейдеры.
для всех! Оптимизация фрагментных шейдеров — довольно важная часть общей работы над производительностью игры.
За некоторыми определениями переменных или функций следует семантическое обозначение, например : POSITION или : SV_Target. Эти означающие семантики сообщают «значение» этих переменных графическому процессору. Подробнее см. на странице семантики шейдеров.
При использовании на красивой модели с хорошей текстурой наш простой шейдер выглядит очень хорошо!
Even simpler single color shader
Давайте еще больше упростим шейдер — создадим шейдер, который отрисовывает весь объект одним цветом. Это не очень полезно, но эй, мы учимся здесь.
Shader "Unlit/SingleColor"
{
Properties
{
// Color property for material inspector, default to white
_Color ("Main Color", Color) = (1,1,1,1)
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// vertex shader
// this time instead of using "appdata" struct, just spell inputs manually,
// and instead of returning v2f struct, also just return a single output
// float4 clip position
float4 vert (float4 vertex : POSITION) : SV_POSITION
{
return mul(UNITY_MATRIX_MVP, vertex);
}
// color from the material
fixed4 _Color;
// pixel shader, no inputs needed
fixed4 frag () : SV_Target
{
return _Color; // just return it
}
ENDCG
}
}
}
На этот раз вместо использования структур для ввода (appdata) и вывода (v2f) функции шейдера просто определяют ввод вручную. Работают оба способа, и какой из них вы выберете, зависит от вашего стиля написания кода и предпочтений.
Использование нормалей сетки для развлечения и получения прибыли
Перейдем к шейдеру, отображающему нормали сетки в мировом пространстве. Без лишних слов:
Shader "Unlit/WorldSpaceNormals"
{
// no Properties block this time!
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// include file that contains UnityObjectToWorldNormal helper function
#include "UnityCG.cginc"
struct v2f {
// we'll output world space normal as one of regular ("texcoord") interpolators
half3 worldNormal : TEXCOORD0;
float4 pos : SV_POSITION;
};
// vertex shader: takes object space normal as input too
v2f vert (float4 vertex : POSITION, float3 normal : NORMAL)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
// UnityCG.cginc file contains function to transform
// normal from object to world space, use that
o.worldNormal = UnityObjectToWorldNormal(normal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 c = 0;
// normal is a 3D vector with xyz components; in -1..1
// range. To display it as color, bring the range into 0..1
// and put into red, green, blue components
c.rgb = i.worldNormal*0.5+0.5;
return c;
}
ENDCG
}
}
}
Помимо получения красивых цветов, нормали используются для всевозможных графических эффектов – освещения, отражений, силуэтов и т. д.
В приведенном выше шейдере мы начали использовать один из встроенных в Unity включаемых файлов шейдера. Здесь использовался файл UnityCG.cginc, содержащий удобную функцию UnityObjectToWorldNormal. Мы также использовали вспомогательную функцию UnityObjectToClipPos, которая преобразует вершину из объектного пространства в экран. Это просто упрощает чтение кода и делает его более эффективным при определенных обстоятельствах.
Мы видели, что данные могут передаваться из вершинного во фрагментный шейдер с помощью так называемых «интерполяторов» (или иногда называемых «вариантами»). В языке затенения HLSL они обычно помечаются семантикой TEXCOORDn, и каждый из них может содержать до 4-х компонентного вектора (см. страницу семантика). для подробностей).
Также мы изучили простой прием визуализации нормализованных векторов (в диапазоне от –1,0 до +1,0) в виде цветов: просто умножьте их наполовину и прибавьте половину. Дополнительные примеры визуализации данных вершин см. в разделе Визуализация данных вершин.
Отражение окружающей среды с использованием нормалей мирового пространства
Когда Skyboxспециальный тип материала, используемый для представления неба . Обычно шестигранник. Подробнее
См. в Словарь используется в сцене как источник отражения (см. Окно освещения),
затем, по сути, «по умолчанию» Reflection Probeкомпонент рендеринга, который захватывает сферический обзор его окружения во всех направлениях, скорее как камера. Захваченное изображение затем сохраняется как кубическая карта, которую можно использовать для объектов с отражающими материалами. Подробнее
Создается See in Словарь, содержащий данные скайбокса.
Зонд отражения представляет собой Cubemapколлекцию из шести квадратных текстур, которые могут представлять отражения в окружающей среде или скайбокс, нарисованный позади вашей геометрии. Шесть квадратов образуют грани воображаемого куба, окружающего объект; каждая грань представляет вид по направлениям мировых осей (вверх, вниз, влево, вправо, вперед и назад). Подробнее
См. в текстуре Словарь; мы расширим приведенный выше шейдер нормалей мирового пространства, чтобы изучить его.
Код уже начинает немного усложняться. Конечно, если вам нужны шейдеры, которые автоматически работают со светом, тенями, отражениями и остальной частью системы освещения, гораздо проще использовать поверхностные шейдеры. Этот пример предназначен для того, чтобы показать вам, как использовать части системы освещения «вручную».
Shader "Unlit/SkyReflection"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
half3 worldRefl : TEXCOORD0;
float4 pos : SV_POSITION;
};
v2f vert (float4 vertex : POSITION, float3 normal : NORMAL)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
// compute world space position of the vertex
float3 worldPos = mul(_Object2World, vertex).xyz;
// compute world space view direction
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// world space normal
float3 worldNormal = UnityObjectToWorldNormal(normal);
// world space reflection vector
o.worldRefl = reflect(-worldViewDir, worldNormal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the default reflection cubemap, using the reflection vector
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.worldRefl);
// decode cubemap data into actual color
half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
// output it!
fixed4 c = 0;
c.rgb = skyColor;
return c;
}
ENDCG
}
}
}
В приведенном выше примере используется несколько вещей из встроенных включаемых файлов шейдера:
- unity_SpecCube0, unity_SpecCube0_HDR, Object2World, UNITY_MATRIX_MVP из встроенные переменные шейдера. unity_SpecCube0 содержит данные для активного датчика отражения.
- UNITY_SAMPLE_TEXCUBE — это встроенный макрос для выборки кубической карты. Большинство обычных кубических карт объявляются и используется с использованием стандартного синтаксиса HLSL (samplerCUBE и texCUBE), однако кубические карты проб отражения в Unity объявляются особым образом для экономии слотов сэмплера. Если вы не знаете, что это такое, не волнуйтесь, просто знайте, что для использования кубической карты unity_SpecCube0 вам необходимо использовать макрос UNITY_SAMPLE_TEXCUBE.
- Функция UnityWorldSpaceViewDir из UnityCG.cginc и функция DecodeHDR из того же файла. Последний используется для получения фактического цвета из данных зонда отражения, поскольку Unity хранит кубическую карту зонда отражения в специально закодированном виде.
- reflect — это просто встроенная функция HLSL для вычисления отражения вектора вокруг заданной нормали.
Отражение окружения с картой нормалей
Часто Карты нормалейТип текстуры карты рельефа, который позволяет добавлять детали поверхности, такие как неровности, канавки и царапины. к модели, которая улавливает свет, как будто они представлены реальной геометрией. Подробнее
См. в Словарь используются для создания дополнительных деталей на объектах без создания дополнительной геометрии. . Давайте посмотрим, как создать шейдер, отражающий окружающую среду, с текстурой карты нормалей.
Теперь математика становится действительно сложной, поэтому мы сделаем это за несколько шагов. В приведенном выше шейдере отражение направление вычислялось для каждой вершины (в вершинном шейдере), а фрагментный шейдер выполнял только отражение просмотр кубической карты зонда. Однако, как только мы начнем использовать карты нормалей, сама нормаль поверхности должна быть рассчитана для каждого пикселя, что означает, что мы также должны вычислить, как окружающая среда отражается для каждого пикселя!
Итак, прежде всего, давайте перепишем приведенный выше шейдер, чтобы он делал то же самое, за исключением того, что мы переместим некоторые вычисления во фрагментный шейдер, чтобы они вычислялись для каждого пикселя:
Shader "Unlit/SkyReflection Per Pixel"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float3 worldPos : TEXCOORD0;
half3 worldNormal : TEXCOORD1;
float4 pos : SV_POSITION;
};
v2f vert (float4 vertex : POSITION, float3 normal : NORMAL)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(_Object2World, vertex).xyz;
o.worldNormal = UnityObjectToWorldNormal(normal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// compute view direction and reflection vector
// per-pixel here
half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
half3 worldRefl = reflect(-worldViewDir, i.worldNormal);
// same as in previous shader
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
fixed4 c = 0;
c.rgb = skyColor;
return c;
}
ENDCG
}
}
}
Это само по себе мало что нам дает — шейдер выглядит точно так же, за исключением того, что теперь он работает медленнее, так как выполняет больше вычислений для каждого пикселя на экране, а не только для каждой вершины модели. Однако очень скоро нам понадобятся эти расчеты. Для более высокой точности графики часто требуются более сложные шейдеры.
Теперь нам тоже нужно научиться чему-то новому; так называемое «касательное пространство». Текстуры карт нормалей чаще всего выражаются в координатном пространстве, которое можно рассматривать как «следующее за поверхностью» модели. В нашем шейдере нам нужно будет знать базисные векторы касательного пространства, прочитать вектор нормали из текстуры, преобразовать его в мировое пространство, а затем выполнить всю математику из приведенного выше шейдера. Давайте приступим!
Shader "Unlit/SkyReflection Per Pixel"
{
Properties {
// normal map texture on the material,
// default to dummy "flat surface" normalmap
_BumpMap("Normal Map", 2D) = "bump" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float3 worldPos : TEXCOORD0;
// these three vectors will hold a 3x3 rotation matrix
// that transforms from tangent to world space
half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.x
half3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.y
half3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z
// texture coordinate for the normal map
float2 uv : TEXCOORD4;
float4 pos : SV_POSITION;
};
// vertex shader now also needs a per-vertex tangent vector.
// in Unity tangents are 4D vectors, with the .w component used to
// indicate direction of the bitangent vector.
// we also need the texture coordinate.
v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(_Object2World, vertex).xyz;
half3 wNormal = UnityObjectToWorldNormal(normal);
half3 wTangent = UnityObjectToWorldDir(tangent.xyz);
// compute bitangent from cross product of normal and tangent
half tangentSign = tangent.w * unity_WorldTransformParams.w;
half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
// output the tangent space matrix
o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
o.uv = uv;
return o;
}
// normal map texture from shader properties
sampler2D _BumpMap;
fixed4 frag (v2f i) : SV_Target
{
// sample the normal map, and decode from the Unity encoding
half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));
// transform normal from tangent to world space
half3 worldNormal;
worldNormal.x = dot(i.tspace0, tnormal);
worldNormal.y = dot(i.tspace1, tnormal);
worldNormal.z = dot(i.tspace2, tnormal);
// rest the same as in previous shader
half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
half3 worldRefl = reflect(-worldViewDir, worldNormal);
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
fixed4 c = 0;
c.rgb = skyColor;
return c;
}
ENDCG
}
}
}
Фу, это было довольно сложно. Но смотрите, отражения нормалей!
Добавление дополнительных текстур
Давайте добавим больше текстур в показанный выше шейдер с картой нормалей и отражением неба. Мы добавим текстуру базового цвета, показанную в первом неосвещенном примере, и карту окклюзии, чтобы затемнить полости.
Shader "Unlit/More Textures"
{
Properties {
// three textures we'll use in the material
_MainTex("Base texture", 2D) = "white" {}
_OcclusionMap("Occlusion", 2D) = "white" {}
_BumpMap("Normal Map", 2D) = "bump" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// exactly the same as in previous shader
struct v2f {
float3 worldPos : TEXCOORD0;
half3 tspace0 : TEXCOORD1;
half3 tspace1 : TEXCOORD2;
half3 tspace2 : TEXCOORD3;
float2 uv : TEXCOORD4;
float4 pos : SV_POSITION;
};
v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(_Object2World, vertex).xyz;
half3 wNormal = UnityObjectToWorldNormal(normal);
half3 wTangent = UnityObjectToWorldDir(tangent.xyz);
half tangentSign = tangent.w * unity_WorldTransformParams.w;
half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
o.uv = uv;
return o;
}
// textures from shader properties
sampler2D _MainTex;
sampler2D _OcclusionMap;
sampler2D _BumpMap;
fixed4 frag (v2f i) : SV_Target
{
// same as from previous shader...
half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));
half3 worldNormal;
worldNormal.x = dot(i.tspace0, tnormal);
worldNormal.y = dot(i.tspace1, tnormal);
worldNormal.z = dot(i.tspace2, tnormal);
half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
half3 worldRefl = reflect(-worldViewDir, worldNormal);
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
fixed4 c = 0;
c.rgb = skyColor;
// modulate sky color with the base texture, and the occlusion map
fixed3 baseColor = tex2D(_MainTex, i.uv).rgb;
fixed occlusion = tex2D(_OcclusionMap, i.uv).r;
c.rgb *= baseColor;
c.rgb *= occlusion;
return c;
}
ENDCG
}
}
}
Кот из шариков выглядит хорошо!
Примеры текстурирования
Шаблон процедурной шахматной доски
Вот шейдер, который выводит шахматную доску на основе координат текстуры сетки:
Shader "Unlit/Checkerboard"
{
Properties
{
_Density ("Density", Range(2,50)) = 30
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
float _Density;
v2f vert (float4 pos : POSITION, float2 uv : TEXCOORD0)
{
v2f o;
o.vertex = UnityObjectToClipPos(pos);
o.uv = uv * _Density;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float2 c = i.uv;
c = floor(c) / 2;
float checker = frac(c.x + c.y) * 2;
return checker;
}
ENDCG
}
}
}
Ползунок плотности в блоке Свойства управляет плотностью шахматной доски. В вершинном шейдере UV сетки умножаются на значение плотности, чтобы получить их от диапазона от 0 до 1 до диапазона от 0 до плотности. Предположим, плотность была установлена равной 30 — это сделает входные данные i.uv во фрагментный шейдер, содержащие значения с плавающей запятой от нуля до 30 для различных мест визуализируемого меша.
Затем код фрагментного шейдера берет только целую часть входной координаты с помощью встроенной в HLSL функции floor и делит ее на два. Напомним, что входными координатами были числа от 0 до 30; это приводит к тому, что все они «квантуются» до значений 0, 0,5, 1, 1,5, 2, 2,5 и т. д. Это было сделано для обоих компонентов x и y входной координаты.
Далее мы складываем эти координаты x и y вместе (каждая из них имеет только возможные значения 0, 0,5, 1, 1,5, …) и берем только дробную часть, используя другую встроенную функцию HLSL, трещина. Результатом этого может быть только 0,0 или 0,5. Затем мы умножаем его на два, чтобы получить 0,0 или 1,0, и выводим в виде цвета (в результате получается черный или белый цвет соответственно).
Tri-planar texturing
Для сложных или процедурных сеток вместо их текстурирования с использованием обычных координат UV иногда бывает полезно просто «проецировать» текстуру на объект с трех основных направлений. Это называется «трехплоскостным» текстурированием. Идея состоит в том, чтобы использовать нормаль поверхности для взвешивания трех направлений текстуры. Вот шейдер:
Shader "Unlit/Triplanar"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Tiling ("Tiling", Float) = 1.0
_OcclusionMap("Occlusion", 2D) = "white" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
half3 objNormal : TEXCOORD0;
float3 coords : TEXCOORD1;
float2 uv : TEXCOORD2;
float4 pos : SV_POSITION;
};
float _Tiling;
v2f vert (float4 pos : POSITION, float3 normal : NORMAL, float2 uv : TEXCOORD0)
{
v2f o;
o.pos = UnityObjectToClipPos(pos);
o.coords = pos.xyz * _Tiling;
o.objNormal = normal;
o.uv = uv;
return o;
}
sampler2D _MainTex;
sampler2D _OcclusionMap;
fixed4 frag (v2f i) : SV_Target
{
// use absolute value of normal as texture weights
half3 blend = abs(i.objNormal);
// make sure the weights sum up to 1 (divide by sum of x+y+z)
blend /= dot(blend,1.0);
// read the three texture projections, for x,y,z axes
fixed4 cx = tex2D(_MainTex, i.coords.yz);
fixed4 cy = tex2D(_MainTex, i.coords.xz);
fixed4 cz = tex2D(_MainTex, i.coords.xy);
// blend the textures based on weights
fixed4 c = cx * blend.x + cy * blend.y + cz * blend.z;
// modulate by regular occlusion map
c *= tex2D(_OcclusionMap, i.uv);
return c;
}
ENDCG
}
}
}
Расчет освещения
Обычно, если вам нужен шейдер, работающий с конвейером освещения Unity, вы напишет поверхностный шейдер. Это делает большую часть «тяжелой работы» для вас, и ваш код шейдера просто должен определить свойства поверхности.
Однако в некоторых случаях требуется обойти стандартный путь шейдера поверхности; либо потому что вы хотите поддерживать только некоторое ограниченное подмножество всего конвейера освещения из соображений производительности, или вы хотите сделать нестандартные вещи, которые не совсем «стандартное освещение». Следующие примеры покажет, как получить данные об освещении из написанных вручную вершинных и фрагментных шейдеров. Просмотр кода, сгенерированного поверхностными шейдерами (с помощью инспектора шейдеров), также хороший учебный ресурс.
Простое рассеянное освещение
Первое, что нам нужно сделать, это указать, что наш шейдер действительно нуждается в передаче ему информации об освещении. конвейер рендеринга Unity поддерживает различные способы рендеринга Процесс отрисовки графики на экран (или текстуры рендеринга). По умолчанию основная камера в Unity отображает изображение на экране. Подробнее
См. в Словарь; здесь мы будем использовать упреждающий рендерингпуть рендеринга по умолчанию, который отображает каждый объект в одном или больше проходов, в зависимости от света, воздействующего на объект. Сами источники света также обрабатываются Forward Rendering по-разному, в зависимости от их настроек и интенсивности. Подробнее
См. в Словарь один.
Начнем с поддержки только одного направленного источника света. Упреждающий рендеринг в Unity работает путем рендеринга основного направленного света, окружающего света, карт освещенияпредварительно обработанной текстуры, которая содержит эффекты источников света. на статических объектах сцены. Карты освещения накладываются поверх геометрии сцены для создания эффекта освещения. Подробнее
Просмотр в Словарь и отражения за один проход, называемый ForwardBase. В шейдере на это указывает добавление тега прохода: Tags {“LightMode”=“ForwardBase”}. Это заставит данные о направленном свете передаваться в шейдер через некоторые встроенные переменные.
Вот шейдер, который вычисляет простое рассеянное освещение для каждой вершины и использует одну основную текстуру:
Shader "Lit/Simple Diffuse"
{
Properties
{
[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
// indicate that our pass is the "base" pass in forward
// rendering pipeline. It gets ambient and main directional
// light data set up; light direction in _WorldSpaceLightPos0
// and color in _LightColor0
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc" // for UnityObjectToWorldNormal
#include "UnityLightingCommon.cginc" // for _LightColor0
struct v2f
{
float2 uv : TEXCOORD0;
fixed4 diff : COLOR0; // diffuse lighting color
float4 vertex : SV_POSITION;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
// get vertex normal in world space
half3 worldNormal = UnityObjectToWorldNormal(v.normal);
// dot product between normal and light direction for
// standard diffuse (Lambert) lighting
half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
// factor in the light color
o.diff = nl * _LightColor0;
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
// sample texture
fixed4 col = tex2D(_MainTex, i.uv);
// multiply by lighting
col *= i.diff;
return col;
}
ENDCG
}
}
}
Это заставляет объект реагировать на направление света - его части, обращенные к свету, освещаются, а части, обращенные в другую сторону, не освещаются вообще.
Рассеянное освещение с окружающим фоном
Приведенный выше пример не учитывает окружающее освещение и световые датчики. Давайте исправим это!
Оказывается, мы можем сделать это, добавив всего одну строку кода. Как окружающий, так и световой зондсветовые зонды хранят информацию о том, как свет проходит через пространство в вашем место действия. Набор световых зондов, расположенных в заданном пространстве, может улучшить освещение движущихся объектов и статических пейзажей LOD в этом пространстве. Подробнее
See in Словарь данные передаются шейдерам в форме сферических гармоник, а Функция ShadeSH9 из UnityCG.cginc включаемый файл выполняет всю работу по его оценке, учитывая нормальное мировое пространство. р>
Shader "Lit/Diffuse With Ambient"
{
Properties
{
[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
fixed4 diff : COLOR0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
half3 worldNormal = UnityObjectToWorldNormal(v.normal);
half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
o.diff = nl * _LightColor0;
// the only difference from previous shader:
// in addition to the diffuse lighting from the main light,
// add illumination from ambient or light probes
// ShadeSH9 function from UnityCG.cginc evaluates it,
// using world space normal
o.diff.rgb += ShadeSH9(half4(worldNormal,1));
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col *= i.diff;
return col;
}
ENDCG
}
}
}
На самом деле этот шейдер становится очень похожим на встроенный устаревший шейдер Diffuse!
Реализация отбрасывания теней
В настоящее время наш шейдер не может ни получать, ни отбрасывать тени. Давайте сначала реализуем отбрасывание теней.
Чтобы отбрасывать тени, шейдер должен иметь тип прохода ShadowCaster в любом из его вложенные шейдеры или любой откат. Проход ShadowCaster используется для рендеринга объекта в карту теней, и обычно он довольно прост — вершинному шейдеру нужно только оценить положение вершины, а фрагментный шейдер почти ничего не делает. Карта теней — это всего лишь буфер глубиныхранилище памяти, в котором хранится глубина z-значения каждого пикселя изображения, где z -value — это глубина для каждого визуализируемого пикселя от плоскости проекции. Подробнее
См. в Словарь, так что даже цвет, выводимый фрагментным шейдером, на самом деле не вопрос.
Это означает, что для многих шейдеров проход отбрасывателя теней будет почти одинаковым (если только объект не имеет пользовательских деформаций на основе вершинного шейдера или не имеет альфа-вырезов/полупрозрачных частей). Самый простой способ получить его — с помощью команды шейдера UsePass:
Pass
{
// regular lighting pass
}
// pull in shadow caster from VertexLit built-in shader
UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"
Однако мы здесь учимся, поэтому давайте сделаем то же самое, так сказать, «вручную». Для более короткого кода мы заменили проход освещения («ForwardBase») кодом, который делает только нетекстурированное окружение. Под ним есть проход «ShadowCaster», который позволяет объекту поддерживать отбрасывание теней.
Shader "Lit/Shadow Casting"
{
SubShader
{
// very simple lighting pass, that only does non-textured ambient
Pass
{
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
fixed4 diff : COLOR0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
half3 worldNormal = UnityObjectToWorldNormal(v.normal);
// only evaluate ambient
o.diff.rgb = ShadeSH9(half4(worldNormal,1));
o.diff.a = 1;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return i.diff;
}
ENDCG
}
// shadow caster rendering pass, implemented manually
// using macros from UnityCG.cginc
Pass
{
Tags {"LightMode"="ShadowCaster"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
struct v2f {
V2F_SHADOW_CASTER;
};
v2f vert(appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
float4 frag(v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}
Теперь внизу есть плоскость с использованием обычного встроенного шейдера Diffuse, так что мы можем видеть наши тени работают (помните, наш текущий шейдер пока не поддерживает получение теней!).
Мы использовали директиву #pragma multi_compile_shadowcaster. Это приводит к тому, что шейдер компилируется в несколько вариантов с разными макросами препроцессора, определенными для каждого (см. страницу с несколькими вариантами шейдеров для получения подробной информации). При рендеринге в карту теней случаи точечного освещения и других типов освещения требуют немного другого кода шейдера, поэтому эта директива необходима.
Получение теней
Для реализации поддержки получения теней потребуется скомпилировать базовый проход освещения в несколько вариантов, чтобы правильно обрабатывать случаи «направленного света без теней» и «направленного света с тенями». Директива #pragma multi_compile_fwdbase делает это (см. несколько вариантов шейдера для подробностей). На самом деле он делает гораздо больше: он также компилирует варианты для различных типов карт освещения, включенного или выключенного GI в реальном времени и т. д. В настоящее время нам все это не нужно, поэтому мы явно пропустим эти варианты.
Затем, чтобы получить фактические вычисления затенения, мы #include “AutoLight.cginc” шейдер включаем файл и используем SHADOW_COORDS, TRANSFER_SHADOW, Макросы SHADOW_ATTENUATION из него.
Вот шейдер:
Shader "Lit/Diffuse With Shadows"
{
Properties
{
[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
// compile shader into multiple variants, with and without shadows
// (we don't care about any lightmaps yet, so skip these variants)
#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
// shadow helper functions and macros
#include "AutoLight.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
SHADOW_COORDS(1) // put shadows data into TEXCOORD1
fixed3 diff : COLOR0;
fixed3 ambient : COLOR1;
float4 pos : SV_POSITION;
};
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
half3 worldNormal = UnityObjectToWorldNormal(v.normal);
half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
o.diff = nl * _LightColor0.rgb;
o.ambient = ShadeSH9(half4(worldNormal,1));
// compute shadows data
TRANSFER_SHADOW(o)
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
// compute shadow attenuation (1.0 = fully lit, 0.0 = fully shadowed)
fixed shadow = SHADOW_ATTENUATION(i);
// darken light's illumination with shadow, keep ambient intact
fixed3 lighting = i.diff * shadow + i.ambient;
col.rgb *= lighting;
return col;
}
ENDCG
}
// shadow casting support
UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"
}
}
Смотрите, теперь у нас есть тени!
Другие примеры шейдеров
Туман
Shader "Custom/TextureCoordinates/Fog" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//Needed for fog variation to be compiled.
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct vertexInput {
float4 vertex : POSITION;
float4 texcoord0 : TEXCOORD0;
};
struct fragmentInput{
float4 position : SV_POSITION;
float4 texcoord0 : TEXCOORD0;
//Used to pass fog amount around number should be a free texcoord.
UNITY_FOG_COORDS(1)
};
fragmentInput vert(vertexInput i){
fragmentInput o;
o.position = UnityObjectToClipPos(i.vertex);
o.texcoord0 = i.texcoord0;
//Compute fog amount from clip space position.
UNITY_TRANSFER_FOG(o,o.position);
return o;
}
fixed4 frag(fragmentInput i) : SV_Target {
fixed4 color = fixed4(i.texcoord0.xy,0,0);
//Apply fog (additive pass are automatically handled)
UNITY_APPLY_FOG(i.fogCoord, color);
//to handle custom fog color another option would have been
//#ifdef UNITY_PASS_FORWARDADD
// UNITY_APPLY_FOG_COLOR(i.fogCoord, color, float4(0,0,0,0));
//#else
// fixed4 myCustomColor = fixed4(0,0,1,0);
// UNITY_APPLY_FOG_COLOR(i.fogCoord, color, myCustomColor);
//#endif
return color;
}
ENDCG
}
}
}