HLSL-шейдеры в Unity подробнее
Структура ShaderLab + HLSL, vertex/fragment, custom URP shader.
Shader Graph закрывает большинство задач, но рано или поздно вам нужно либо что-то нестандартное, либо понять, что граф генерирует под капотом. Эта глава — короткое введение в “ручные” шейдеры для URP.
Что такое шейдер в Unity
Шейдер в Unity — это файл .shader с ShaderLab структурой, внутри которой блоки кода на
HLSL (или раньше CG — тот же синтаксис, но с устаревшими макросами).
ShaderLab — это “обёртка”: декларация properties, sub-shaders, passes, render states. HLSL — это непосредственно вершинный (vertex) и фрагментный (fragment) код, выполняющийся на GPU.
Shader "Имя/В/Меню"
{
Properties { ... } // что видно в Inspector материала
SubShader { ... } // одна или несколько имплементаций
Fallback "Standard" // если ни один SubShader не подошёл
}
WebGL/WebGPU: вы пишете vertex и fragment shader напрямую (GLSL/WGSL), сами создаёте программу, компилируете, передаёте uniforms.
Unity делает большую часть работы: компилирует ваш HLSL в нужный target (DX11/12, Vulkan, Metal, OpenGL ES, WebGL/WebGPU), управляет привязкой uniform’ов через Material и Renderer.
Минимальный URP-шейдер
Вот шаблон Unlit-шейдера для URP, который окрашивает объект в цвет из материала:
Shader "Custom/URPUnlitSimple"
{
Properties
{
_BaseColor ("Base Color", Color) = (1, 1, 1, 1)
_BaseMap ("Base Map", 2D) = "white" {}
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"Queue" = "Geometry"
}
Pass
{
Name "ForwardUnlit"
Tags { "LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// Структура входа в vertex
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
// Структура выхода vertex → входа fragment
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
};
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float4 _BaseMap_ST; // tiling/offset
CBUFFER_END
Varyings Vert(Attributes IN)
{
Varyings OUT;
OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
return OUT;
}
half4 Frag(Varyings IN) : SV_Target
{
half4 tex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
return tex * _BaseColor;
}
ENDHLSL
}
}
}
Что тут важно:
Tags { "RenderPipeline" = "UniversalPipeline" }— говорит, что этот SubShader — для URP. Без него URP проигнорирует шейдер.HLSLPROGRAM ... ENDHLSL— границы HLSL-блока. (Раньше былоCGPROGRAM ... ENDCG— устарело для URP/HDRP.)CBUFFER_START(UnityPerMaterial)— обязательно для SRP Batcher совместимости. Без CBUFFER батчер не будет сливать draw calls этого шейдера.TransformObjectToHClip— переводит object-space позицию в clip-space, готовый для растеризации.TRANSFORM_TEX— применяет tiling/offset из_BaseMap_ST.SAMPLE_TEXTURE2D— макрос для семплирования текстуры (кроссплатформенно).
Что такое properties и почему они должны совпадать с CBUFFER
Properties
{
_BaseColor ("Base Color", Color) = (1, 1, 1, 1)
}
В Properties вы описываете то, что хочет редактировать пользователь через Inspector. В CBUFFER объявляете эти же поля в HLSL для использования в коде. Имена должны совпадать.
Типы Properties:
Color—float4Vector—float4Float,Range(0, 1)—float2D,3D,Cube— текстуры
Освещение в URP
Unlit-шейдер выше игнорирует свет. Чтобы получить освещение, нужно подключить URP lighting helpers и дополнить Attributes/Varyings world-space-данными (normal + position нужны для расчёта PBR):
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD1; // ← нужны для освещения
float3 normalWS : TEXCOORD2;
float2 uv : TEXCOORD0;
};
Varyings Vert(Attributes IN)
{
Varyings OUT;
VertexPositionInputs vp = GetVertexPositionInputs(IN.positionOS.xyz);
VertexNormalInputs vn = GetVertexNormalInputs(IN.normalOS);
OUT.positionCS = vp.positionCS;
OUT.positionWS = vp.positionWS;
OUT.normalWS = vn.normalWS;
OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
return OUT;
}
half4 Frag(Varyings IN) : SV_Target
{
half4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv) * _BaseColor;
InputData inputData = (InputData)0;
inputData.positionWS = IN.positionWS;
inputData.normalWS = normalize(IN.normalWS);
inputData.viewDirectionWS = GetWorldSpaceNormalizeViewDir(IN.positionWS);
SurfaceData surfaceData = (SurfaceData)0;
surfaceData.albedo = baseColor.rgb;
surfaceData.alpha = baseColor.a;
surfaceData.metallic = 0;
surfaceData.smoothness = 0.5;
surfaceData.normalTS = float3(0, 0, 1);
return UniversalFragmentPBR(inputData, surfaceData);
}
UniversalFragmentPBR — встроенная функция URP, считает физически-корректное освещение для всех
видимых источников света, тени, GI. Полный аналог “Lit” shader, но управляется вами.
GetVertexPositionInputs / GetVertexNormalInputs — URP helpers, рассчитывают все варианты
координат (object/world/clip/view) одним вызовом.
Render states
В Pass можно задавать:
Cull Back/Cull Front/Cull Off— отбраковка полигонов по направлению нормали.ZWrite On/Off— писать ли в Z-буфер.ZTest LEqual— какой Z-тест.Blend SrcAlpha OneMinusSrcAlpha— режим смешивания (для прозрачности).ColorMask RGBA— какие каналы писать.
Для прозрачного объекта типовой набор:
Tags { "Queue" = "Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
Variants — #pragma multi_compile и shader_feature
Один шейдер может иметь много вариантов (с/без normal map, с/без alpha cutout, …). Управляются через keywords:
#pragma multi_compile _ MY_FEATURE_ON— генерирует обе версии всегда (для рантайм-переключений).#pragma shader_feature _ MY_FEATURE_ON— генерирует только используемые материалом версии (уменьшает билд).
#pragma shader_feature_local _NORMALMAP
#ifdef _NORMALMAP
TEXTURE2D(_NormalMap);
SAMPLER(sampler_NormalMap);
#endif
half3 GetNormal(float2 uv)
{
#ifdef _NORMALMAP
return UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, uv));
#else
return float3(0, 0, 1);
#endif
}
Каждый keyword умножает количество вариантов. 5 features → 32 варианта. 10 → 1024. Это
замедляет компиляцию, раздувает билд и память шейдеров. Используйте shader_feature_local
(вариант существует только для конкретного материала, а не для всех).
Custom shaders в Shader Graph
В Shader Graph можно вставлять блок Custom Function, выполняющийся как HLSL-сниппет:
void RGBtoHSV_float(float3 rgb, out float3 hsv)
{
float Cmax = max(max(rgb.r, rgb.g), rgb.b);
float Cmin = min(min(rgb.r, rgb.g), rgb.b);
// ... классический алгоритм
hsv = float3(h, s, v);
}
Это даёт мост между визуальным графом и нестандартными математическими функциями, для которых нет готовых нод.
Что почитать дальше
- Catlike Coding tutorials — лучший практический материал по URP/HDRP шейдерам.
- Unity URP Shader Library в файле
Packages/com.unity.render-pipelines.universal/ShaderLibrary/— это исходники “Lit”, “Unlit”, “SimpleLit” shaders, из которых можно учиться. - Frame Debugger — посмотрите, какие именно constants и текстуры идут в ваш шейдер каждый draw call.
На этом главный раздел про 3D-разработку завершён. Дальше — глоссарий и общие ссылки на углубление.