
Тени
Содержание
1 Вступление
2 Краткая теория
2.1 ShadowMapping
2.2 Рендер Depth Texture
2.3 Рендер Shadow Map
2.4 Сборка Теней
2.5 Настройка Теней в unity
2.6 Алгоритм фильтрации Percentage Closer Filtering
3 Практика. Пишем шейдер
3.1 Отбрасывание теней
3.2 Получение теней
Вступление
Всем доброго времени суток! За окном декабрь, а значит самое время писать шейдеры! Текущим объектом нашего исследования были выбраны тени. Для понимания того, что тут вообще будет происходить, вам необходимо знать, что такое вертесный и фрагментный шейдеры.
Краткая теория
Начнем мы, как всегда, с теории. Для начала разберемся, что такое тени и какими они бывают в юнити.
Когда на пути светового луча попадается какой-либо объект, то на поверхности, находящийся за ним, образуется темный силуэт этого объекта. В таких случаях мы говорим, что объект отбрасывает тень. В реальном мире переход из темной области в освещенную происходит плавно, образуя полутень (с англ. penumbra ).

Прямой поддержки полутени в юнити нет , но ее можно попробовать имитировать с помощью алгоритмов фильтрации.
Unity рендерит реалтайм тени с помощью самого распространенного на данный момент алгоритма - Shadow Mapping.
Shadow Mapping
Shadow Mapping - это алгоритм рендеринга динамических теней, где информация о тенях хранится в текстуре. Главным его достоинством является быстрая отрисовка теней геометрически-сложных объектов.
Для того, чтобы разобраться, как юнити реализует данный алгоритм, создадим простую сцену: куб, две сферы, бросающих на него тень, камера и источник направленного света (directional light).

Теперь посмотрим, как происходит рендер одного кадра нашей сцены. В этом нам поможет инструмент для отладки frame debugger. Откываем Windows->FrameDebugger.
Нажимаем кнопку Enable.
Перед нами появилось окно с какими-то непонятным вещами внутри.

Разложим все по полочкам.
1 . Ползунок, показывающий номер шага. В моем случае номер шага равен 4, а всего 24 шага. Хотя правильно называть это не шагами, а draw call'ми.
2 . Окно вызовов отрисовки (draw call'ов). Здесь находятся сгруппированные draw call'ы.
Это запрос отрисовки, направленный графическому API. Чем запросов больше, тем выше нагрузка.
Для оптимизации draw call'ы однотипных объектов объединяют (батчат / batching)
3 . Окно информации о draw call'е, выбранном во втором окне. Здесь есть данные о шейдере, pass'е(проходе), ключевых словах, смешивании, текстурах, векторах, матрицах, текстурах и т.д.
Что же нам показывает frame debugger ?
Рендер depth texture
Прежде, чем рендерить геометрию, unity делает дополнительный проход, в котором создает depth texture(текстуру глубины). Это текстура, в которой каждый пиксель содержит информацию о "глубине" обектов на сцене.

- Размер текстуры глубины совпадает с разрешением экрана
- Значения пикселей в текстуре глубины находятся в диапазоне от 0 до 1 с нелинейным распространением. 0 - далеко от камеры, черный цвет. 1 - близко к камере, белый цвет.
- Показывает только видимые объекты. Т.е. если один закрыт другим объектом, то информация о нем не будет занесена в текстуру глубины.
- Рассчитывается с "точки зрения" камеры в clip space //(пространство камеры)
Рендер shadow map
Далее по списку идет рендер карты теней, которая используется для shadow mapping'а.
Идея Shadow mapping очень проста - мы поместим камеру в положение источника света и отрендерим для нее depth map. Её мы будем использовать для проверки видимости объектов. По сути, shadow map будет показывать, какое расстояние прошел луч от источника света, прежде чем столкнулся с объектом. Если расстояние(в shadow_space) от источника света до точки меньше, чем расстояние, закодированное для этой-же точки в shadow map, то это значит, что эта точка затенена.

Т.к. directional light распространяется равномерно и независимо от расстояния, то для него будет использоваться ортогональная камера. Плюс ко всем, directinal light олицетворяет собой солнце, а значит, по идее, расстояние от него до каждой точки должно быть одинаковым, а shadow map очень большой. На самом деле это не так. Расстояние для рендера Shadow map выбирается в зависимости от положения камеры.
Вот так примерно выглядит shadow map для нашей сцены.

- Размер текстуры задается в настройках теней
- Значения пикселей в текстуре глубины находятся в диапазоне от 0 до 1 с нелинейным распространением. 0 - далеко от камеры, черный цвет. 1 - близко к камере, белый цвет.
- Показывает только видимые объекты. Т.е. если один закрыт другим объектом, то информация о нем не будет занесена в текстуру глубины.
- Рассчитывается с "точки зрения" источника света в clip space
- Рендерится для каждого отдельного источника света
А почему их четыре ? Это все потому, что мы используем четыре теневых каскада в сцене. Подробнее о них мы поговорим чуть позже.
Сборка теней
в frame debuggere этот этап назван collect shadows, и я не знаю, как это лучше перевестиИтак, что мы имеем ? У нас есть карта глубины с точки зрения камеры, у нас есть карта глубины с точки зрения источника света. Конечно, они сохранены в разных системах отсчета ( clip spaces), но мы знаем пространственные отношения и направления "взгляда" обоих точек зрения. А это в свою очередь означает, что мы можем сравнивать глубину depth map и shadow map. Другими словами, мы можем сравнивать два вектора(вектор, выходящий из источника света и вектор, выходящий из камеры). Если они, заканчиваются в одной точке и эта точка затенена, то мы сохраняем цвет тени в отдельную текстуру. Если же вектор света или вектор взгляда сталкиваются по пути с каким-либо объектом и не достиюте точки, то она не затенена или ее не видно, следовательно, нам не нужно сохранять значение тени в ней. В итоге получается текстура с тенями. На значения, сохраненные в ней, умножается цвета объетов во время рендера сцены.
В итоге мы имеем такой алгоритм shadow mapping'а в юнити:
- Создание карты глубины камеры (depth map)
- Создание карт глубины всех источников света (shadow map)
- Вычисление теней
- Сборка теней
Настройка теней
Теперь, когда мы знаем, как происходит ренер теней, можно перейти к изучению их настроек.
Открываем Edit->Project Settings->Quality

В Quality Setting мы устанавливаем параметры для всех источников света. Тем не менее, некоторые из них можно будет переопределить уже на каждом конкретном источнике света в зависимости от его задачи.
1 . Shadows - этот параметр отвечает за качество теней.
- Diable Shadows - тени не будут рендерится
- Hard Shadows Only - будет происходить рендер только жестких(hard) теней
- Hard and Soft Shadows - будет происходить рендер мягких(soft) и жестких(hard) теней в зависимости от настроек источника света
2 . Shadow Resolution - настройка разрешения карты теней. Чем больше разрешение, тем качественнее на сцене будут тени
- Low Resolution - размер карты теней с низким разрешением (1024x1024 )
- Medium Resolution - размер карты теней со средний разрешением (2048x2048 )
- High Resolution - размер карты теней с высоким разрешением (4096x4096)
- Very Hight Resolution - размер карты теней с очень высоким разрешением (не особо понятно, в чем разница, размер карты теней 4096x4096)
Когда сцена имеет большие размеры, карта теней покрывает огромную территорию и на каждый пиксель карты теней приходится слишком большой участок территории, а следовательно, тени смотрятся очень ступенчато ( этот эффект называют aliasing).


Чем больше текстура, тем качественнее тени, но и ресурсов будет использоваться больше. Особенно это касается памяти. А зачем нам рендерить качественные тени для объектов, находящихся далеко от камеры ? Для них вполне бы подошли текстуры с маленьким разрешением. Эту проблему решают теневые каскады.
Под теневыми каскадами подразумевают разбиение пространства на каскады в зависимости от удаления до камеры. Каждый каскад уже реализует свою карту теней. Именно для них и рендерится несколько теневых карт с разным разрешением.

Как видно на картинке выше, тень дальней сферы, проходя через границу теневого каскада, немного теряет в качестве.
3 . Shadow Projection - параметр, на основе которого выбирается теневой каскад. По умолчанию используется Stable fit.
- Close Fit - в зависимости от глубины камеры
- Stable Fit - в зависимости от расстояния до камеры
-
4 . Shadow Distance - расстояние от камеры, после которого тени не будут рендерится. По умолчанию стоит 150. При установки значения shadow distance = 8, тень от дальней сферы уже не рендерилась. Так же, при маленьком shadow distance необходимость в теневых каскадах отпадает.
Чем меньше shadow distance, тем быстрее рендерится сцена. У меня количество draw call'ов уменьшилось с 24 до 16.

5 . Shadowmask Mode - в каком режиме будет работать shadowmask (текстура, в которой сохранены тени от статических объектов на другие статические объекты. Используется в mixed-lighting'е)
- Shadowmask - статические объекты отбрасывают всегда запеченные тени
- Distance Shadowmask - использование реал-тайм теней до shadow distance, а статических после нее
6 . Shadow Near Plane Offset - смещение ближнего плана(near plane) камеры, которая рендерит теневую карту. По умолчанию равно 3.
7 . Shadow Cascades - количество теневых каскадов.
- No Cascades - без теневых каскадов, тени ограничиваются только shadow distance
- Two Cascades - два теневых каскада
- Four Cascades - четыре теневых каскада
8 . Cascade splits - Панель, где можно настроить расстояние, на котором активируется и выключается каждый из каскадов. Расстояние считается в процентах от shadow distance.

Это были общие настройки для теней. Также в unity можно настроить отображение теней для каждого отдельного источника света.

1 . Shadow Type - тип теней.
- Hard Shadows - жесткие тени
- Soft Shadows - мягкие тени
-
2 . Strength - коэффициент, на который умножаются тени при их сборке. 0 - нет теней, 1- черные тени.
3 . Resolution - разрешение карты теней для данного источника света.
- Use Quality Settings - использует значение, которое мы выставили в Quality Settings
- Low Resolution - размер карты теней для данного источника света 512x512
- Medium Resolution - размер карты теней для данного источника света 1024x1024
- High Resolution - размер карты теней для данного источника света 2048x2048
- Very Hight Resolution - размер карты теней для данного источника света 4096x4096
4 . Bias - коэффициент, который добавляется к глубине. Он как-бы немного вдавливает тень. По умолчанию равен 0.05. Если выставить слишком большое значение, тень слишком далеко отодвинется от объекта и будет казаться, что она отдельно от него, если слишком маленькое, появится эффект shadow acne.
5 . Normal Bias - делает все то-же самое, что и bias, только выравнивание идет по нормалям объекта. Если установить слишком высокое значение, то тень станет слишком "вжатой в себя" или очень узкой.
6 . Near Plane - смещение ближнего плана камеры для данного источника света.
Алгоритм фильтрации Percentage Closer Filtering (PCF)
PCF - это алгоритм фильтрации, основанный на усреднении значений между соседними текселями тени в shadow map. В нем для каждого пикселя мы берем значения соседних пикселей в Shadow Map и усредняем их. Получается своеобразный переход из затененной в освещенную область. Узнать поподробнее о PCF, а также о других алгоритмах можно здесь

Пишем шейдер
!Переходим! к практике!
Обозначим задачи.
- Предмет должен отбрасывать тени
- Шейдер должен принимать тени от directional light'а
- Тени должны быть мягкими
Создадим новый материал, для него создадим новый Unlit шейдер и применим его к сфере.
Как видно на изображении ниже, сфера перестала отбрасывать тень, т.к. в unlit шейдере нет её реализации.

Отбрасывание теней (Casting Shadow)
Итак, перейдем к реализации отбрасывания теней. Если мы сейчас откроем frame debugger, то увидим, что наша unlit сфера не рендерится ни в shadowmap, ни в depth map. Это происходит потому, что в шейдере unlit сферы нет прохода с тэгом "ShadowCaster", из которого движок берет информацию о глубине на основе положения объекта.
Добавим дополнительный проход в наш шейдер:
Pass { Tags{"LightMode"="ShadowCaster"} Blend One Zero CGPROGRAM #pragma target 3.0 #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float4 vert(float4 vertex:POSITION):SV_POSITION { return mul(UNITY_MATRIX_MVP,vertex); } float4 frag():SV_Target { return 0; } ENDCG } }
В этом проходе нам важна только позиция, поэтому просто переведем положение объекта из object space в clip space.
Ну, собственно, вот и все. Теперь наш шейдер пишет глубину, а это значит, что объект отбрасывает тень.
Но тут возникает небольшая проблема: у нас не производится смещения в зависимости от bias'а и normal bias'а.
Чтобы правильно скорректировать положение сферы в зависимости от bias'а, мы применим функции, реализованные в " UnityCG.cginc":
float4 UnityClipSpaceShadowCasterPos(float3 vertex, float3 normal) { float4 clipPos; // Important to match MVP transform precision exactly while rendering // into the depth texture, so branch on normal bias being zero. if (unity_LightShadowBias.z != 0.0) { float3 wPos = mul(_Object2World, float4(vertex,1)).xyz; float3 wNormal = UnityObjectToWorldNormal(normal); float3 wLight = normalize(UnityWorldSpaceLightDir(wPos)); // apply normal offset bias (inset position along the normal) // bias needs to be scaled by sine between normal and light direction // (http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/) // // unity_LightShadowBias.z contains user-specified normal offset amount // scaled by world space texel size. float shadowCos = dot(wNormal, wLight); float shadowSine = sqrt(1-shadowCos*shadowCos); float normalBias = unity_LightShadowBias.z * shadowSine; wPos -= wNormal * normalBias; clipPos = mul(UNITY_MATRIX_VP, float4(wPos,1)); } else { clipPos = mul(UNITY_MATRIX_MVP, float4(vertex,1)); } return clipPos; }
float4 UnityApplyLinearShadowBias(float4 clipPos) { clipPos.z += saturate(unity_LightShadowBias.x/clipPos.w); float clamped = max(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE); clipPos.z = lerp(clipPos.z, clamped, unity_LightShadowBias.y); return clipPos; }
Изменим наш вертексный шейдер:
float4 vert(float4 vertex:POSITION, float3 normal:NORMAL):SV_POSITION { float4 clipPos = UnityClipSpaceShadowCasterPos(vertex.xyz,normal); return UnityApplyLinearShadowBias(clipPos); }
Ну вот теперь наш шейдер может полноценно отбрасывать тень. Следующий этап будет сложнее, т.к. в нем мы должны реализовать получение теней.

Получение теней (Receiving Shadows)
Для того, чтобы узнать, затенена ли точка, мы должны взять значение глубины из shadow map, сравнить его с расстоянием(в shadow space) от источника света до этой точки, и если оно больше, то точка затенена.
Возвращаемся в наш первый проход(pass), и введем новую переменную _ShadowCoord, в которую мы в будущем запишем координаты точки в screen space. А почему нам не подойдет clip space ? Да все потому, что каждая точка в shadow map, доступ к которой мы имеем по средствам uv координат, отражает положение соответствующей ей позиции на сцене. UV координаты находятся в пределах от 0 до 1, а clip space координаты от -1 до 1. Поэтому, если напрямую запрашивать информацию о глубине из shadow map при помощи clip space координат, мы получим ошибку. Итак, мы добавили в структуру v2f переменную _ShadowCoord.
struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; float4 _ShadowCoord:TEXCOORD2; float4 wPos:TEXCOORD3; // мировые координаты точки }
Теперь спускаемся в вертексный шейдер и присваиваем переменной значение. Для того, чтобы значение было в screen space, нужно привести переменную к виду, где минимальному значению clip space (-1) будет соответствовать минимально значение screen space (0). Это делается при помощи функции ComputeScreenPos(float4 clip_pos), объявленной в "UnityCG.cginc"
inline float4 ComputeNonStereoScreenPos (float4 pos) { float4 o = pos * 0.5f; #if defined(UNITY_HALF_TEXEL_OFFSET) o.xy = float2(o.x, o.y * _ProjectionParams.x) + o.w * _ScreenParams.zw; #else o.xy = float2(o.x, o.y * _ProjectionParams.x) + o.w; #endif o.zw = pos.zw; return o; } inline float4 ComputeScreenPos (float4 pos) { float4 o = ComputeNonStereoScreenPos(pos); #ifdef UNITY_SINGLE_PASS_STEREO o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w); #endif return o; }
Сейчас векртексный шейдер должен выглядеть как-то так
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); o.wPos = mul(_Object2World,v.vertex); o._ShadowCoord = ComputeScreenPos(o.vertex); return o; }
Следующим шагом станет сравнивание расстояния до точки и глубины, соответствующей этой точке. Для этого нам нужно достать shadow map, которая объявлена в подключаемом файле "UnityShadowLibrary.cginc". Поднимаемся выше, и подключаем этот файл в наш шейдер.
#include "UnityShadowLibrary.cginc"
И сразу же скажем шейдеру, что мы хотим получить извне shadow map
uniform sampler2D _ShadowMapTexture;
Во фрагментном шейдере создадим переменную для теневых координат, к которым применим проекцию путем деления xy на w.
float2 shadow_coords = i._ShadowCoord.xy/i._ShadowCoord.w;
Если не делать проекцию, то мы получим неправильное значение из shadow map.

Достаем значение глубины из shadow map:
float map_dist = tex2D(_ShadowMapTexture,shadow_coords).r;
Достаем расстояние до объекта в shadow space ( система кординат с точки зрения источника света) при помощи предоставляемой unity матрицы unity_WorldToShadow[x], которая переводит из мировых в "теневые" координаты. X - номер каскада. Мы будем считать тень только для самого первого каскада.
float shadow_dist = mul(unity_WorldToShadow[0],i.wPos);
Создадим переменную тени
float shadow = 1; if(shadow_dist>map_dist) shadow = 0; ... return col*shadow;
Создадим новый материал, применим его к кубу, на который сферы отбрасывают тень. Назначим этому материалу наш шейдер и накинем на него какую-нибудь текстуру. Вот что у меня получилось:

Получились довольно грубые тени с резким переходом.

Исправлять это дело мы будет при помощи алгоритма фильтрации PCF. Но для этого нам нужно запросить у движка еще одну переменную - размер текселя shadow map.
uniform float2 _ShadowMapTexture_TexelSize;
И, собственно, написать саму функцию фильтрации:
float PCF (sampler2D shadowMap, float2 uv) { float result = 0.0; for(int x=-3; x<=3;x++) for(int y=-3;y<=3;y++) { float2 offset = float2(x,y)*_ShadowMapTexture_TexelSize; result+= tex2D(shadowMap,uv+offset).r; } return result/20; }
Здесь для сэмплирования мы взяли 3x3 текселя. Коэффициент, на который нужно делить результат я подбирал "на глаз", т.к. для каждого размера куба он будет резличен.
А теперь применим эту функцию в фрагментном шейдере:
if(shadow_dist>map_dist) shadow = PCF(_ShadowMapTexture,shadow_coords);
И вот что получается:

Было/стало
Ну, в принципе, вот и все. Первый блин готов. Но он имеет ряд недостатков:
- Получает тени только от directional light'а
- Используется достаточно дорогой PCF
- Не реализовывает CSM (Cascaded shadow mapping)
Shader "Unlit/Shadows" { 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 "AutoLight.cginc" #include "UnityCG.cginc" #include "UnityShadowLibrary.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; float4 _ShadowCoord:TEXCOORD2; float4 wPos:TEXCOORD3; SHADOW_COORDS(2) }; uniform sampler2D _ShadowMapTexture; uniform float2 _ShadowMapTexture_TexelSize; 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); o._ShadowCoord = ComputeScreenPos(o.vertex); o.wPos = mul(unity_ObjectToWorld,v.vertex); return o; } float PCF (sampler2D shadowMap, float2 uv) { float result = 0.0; for(int x=-3; x<=3;x++) for(int y=-3;y<=3;y++) { float2 offset = float2(x,y)*_ShadowMapTexture_TexelSize; result+= tex2D(shadowMap,uv+offset).r; } return result/20; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); float2 shadow_coords = i._ShadowCoord.xy/i._ShadowCoord.w; float map_dist = tex2D(_ShadowMapTexture,shadow_coords).r; float shadow_dist = mul(unity_WorldToShadow[0],i.wPos); float shadow=1; if(shadow_dist>map_dist) shadow = PCF(_ShadowMapTexture,shadow_coords); UNITY_APPLY_FOG(i.fogCoord, col); return col*shadow; } ENDCG } Pass { Tags{"LightMode"="ShadowCaster"} Blend One Zero CGPROGRAM #pragma target 3.0 #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float4 vert(float4 vertex:POSITION, float3 normal:NORMAL):SV_POSITION { float4 clipPos = UnityClipSpaceShadowCasterPos(vertex.xyz,normal); return UnityApplyLinearShadowBias(clipPos); } float4 frag():SV_Target { return 0; } ENDCG } } }
Как видите, это не совсем рабочий вариант шейдера, и он во много уступает встроенному в юнити шейдеру, но для понимания процесса рендера теней сойдет. В следующих статьях мы попробуем исправить его недостатки.
Если есть вопросы, пишите в личку или в комментариях, постараюсь ответить.
Удачи!
Смотрите также:
Комментарии
Статья огонь! Максимально исчерпывающая ^^
Шикарно, как и ранее.
Как раз есть, где применить. Спасибо!
<a href= http://mosros.flybb.ru/viewtopic.php?f=2&t=635>Процесс получения диплома стоматолога: реально ли это сделать быстро?</a>