~2 мин чтения

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

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:

  • Colorfloat4
  • Vectorfloat4
  • Float, Range(0, 1)float
  • 2D, 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
}
Variant explosion

Каждый 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-разработку завершён. Дальше — глоссарий и общие ссылки на углубление.