Всем привет! Меня зовут Григорий Дядиченко, я уже что-то разрабатываю на Unity десять лет. Давно ничего не писал, и тут собрался с силами и решил, что хочу написать про компьютерную графику. А точнее пройтись по её базе в контексте Unity. Если интересуетесь темой — добро пожаловать под кат!

Всего я в разработке 10 лет (даже чуть больше). Но последние 8 лет я занимаюсь разработкой под заказ. Поэтому задачи у меня довольно разноплановые, но довольно часто это связано с крутой графикой на слабых устройствах. Будь то AR, VR или мобилки. Я когда-то писал статью про оптимизированный для мобилок акрил скажем. Так как проектов у меня обычно много, то часто я отдаю какие-то работы на подряд. И если на игровую логику, вёрстку, адаптивные интерфейсы подрядчиков бывает найти не трудно, то что-то действительно сложно по графике могут сделать единицы. Да и многие плавают даже в базе того как работают графические чипы, видеопамять, да и рендер в целом.

Хочется составить набор статей, который покроет основы компьютерной графики в контексте Unity. Свои рендереры на плюсах, CPU рендеринг и подобные темы я разбирать не хочу, а вот что как и почему работает в движке можно обсудить. И для этого я придумал следующий набор тем, чтобы удобно добавить в закладки и база была под рукой:

1. О графическом конвейере (о работе графического конвейера)
2. О видеокартах (архитектура GPU, работа параллельных вычислений, Warps/Wavefronts, branch divergence, texture fetch)
3. О линейной алгебре (матрицы, пространственные преобразования, проекции, системы координат)
4. О цвете и свете (цветовые пространства, модели освещения, тени)
5. О шейдерах (работа с вертексами, развёртки, оптимизация)
6. О compute шейдерах (симуляция частиц, обработка изображений)
7. О оптимизациях (батчинг, SRP батчер, GPU инстансинг)

Если я о чём-то забыл или что-то ещё интересно - напишите в комментариях. Итак, начнём с простого, о чём написано уже много, поэтому эта статья больше вводная.

Что такое графический конвейер?

Каждый кадр в видеоигре, 3D-приложении или даже интерфейсе — это результат работы графического конвейера (rendering pipeline). Это процесс, в котором геометрия сцены превращается в пиксели на экране. Графический конвейер — это последовательность этапов, через которые проходят 3D-объекты, чтобы превратиться в 2D-изображение. С точки зрения компьютера 2д объекты так же проходят через этот конвейер.

Основные этапы

  1. Вершинный шейдер
    Обработки вершин

  2. Геометрический шейдер (опционально)
    Модификация и генерация новых примитивов

  3. Клипинг
    Отсечение невидимых частей объектов

  4. Растеризация
    Преобразование геометрии в пиксели

  5. Фрагментный шейдер
    Расчёт цвета пикселей

  6. Тесты и смешивание
    Работа с Depth, Stencil, Alpha Blending

Вершинный шейдер

Вершинный шейдер (Vertex Shader) — это программа, выполняемая на графическом процессоре (GPU) на этапе графического конвейера, которая обрабатывает каждую вершину геометрического объекта. Он применяет преобразования модель → мир → камера → проекция.

// Пример вершинного шейдера в HLSL  
v2f vert (appdata v) {  
    v2f o;  
    o.vertex = UnityObjectToClipPos(v.vertex); // Локальные → экранные координаты  
    o.uv = v.uv;  
    return o;  
}  

В вершинной части конвейера часто пишутся деформации вроде анимации воды, колышущейся травы, "дыхания" объектов. И для вершинной анимации.

Хитрый трюк связанный с этой частью конвейера. В вебе и на мобилках, особенно старых, очень медленно работает Skinning, поэтому через вершинные анимации можно оптимизировать это. В текстуру записывается смещение вершин, задаётся правило смещения в зависимости от цвета текстуры. И дальше можно последовательно проигрывать смещая вертексы в шейдере по кадрам приводя 3д модель к нужному состоянию. Работает в разы быстрее скининга на старых устройствах.

Shader "Custom/VertexAnimationTexture" {
    Properties {
        _MainTex ("Albedo", 2D) = "white" {}
        _AnimTex ("Animation Texture", 2D) = "black" {} // R32G32B32A32_Float
        _AnimLength ("Animation Length", Float) = 1.0
    }
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                uint vertexId : SV_VertexID;
            };

            struct v2f {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            sampler2D _MainTex, _AnimTex;
            float _AnimLength;

            v2f vert (appdata v) {
                v2f o;
                
                // Вычисляем текущий кадр анимации
                float time = frac(_Time.y / _AnimLength); // Зацикленная анимация
                float2 animUV = float2(
                    (float)v.vertexId / (float)_AnimTex_TexelSize.z, // X = vertexId
                    time                                              // Y = время
                );
                
                // Читаем позицию из текстуры
                float3 animPos = tex2Dlod(_AnimTex, float4(animUV, 0, 0)).xyz;
                
                // Применяем новую позицию
                o.pos = UnityObjectToClipPos(float4(animPos, 1));
                o.uv = v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }
    }
}

Геометрический шейдер 

Геометрический шейдер (Geometry Shader) — это этап графического конвейера, который работает между вершинным и фрагментным шейдерами и позволяет динамически создавать, изменять или удалять геометрические примитивы (точки, линии, треугольники).

[maxvertexcount(3)] // Максимум 3 вершины на выходе (треугольник)  
void geom(triangle v2f input[3], inout TriangleStream<v2f> stream) {  
    v2f output;  
    for (int i = 0; i < 3; i++) {  
        output.vertex = input[i].vertex + float4(0, 0.5, 0, 0); // Сдвигаем вершины вверх  
        output.uv = input[i].uv;  
        stream.Append(output);  
    }  
    stream.RestartStrip();  
}  

Важно: В URP/HDRP геометрические шейдеры поддерживаются, но требуют аккуратной настройки.

Применяется для генерации травы и листвы из точек, для визуализации дебаг параметров (например нормали моделей), разбиения меша на части (эффекты разрушаемости).

Клиппинг

Клиппинг (Clipping) — это процесс отсечения частей графических примитивов (треугольников, линий, точек), которые находятся вне области видимости (например, за границами экрана или вне заданного пространства).

Как работает в конвейере?

  1. View Frustum Culling:

    • Unity автоматически отбрасывает объекты вне пирамиды видимости камеры.

  2. Clipping Space:

    • Вершины преобразуются в Clip Space (координаты от [-1, 1]).

    • Если вершина вне этого диапазона — она отсекается.

  3. Backface Culling:

    • Треугольники, повёрнутые "спиной" к камере, не растеризуются.

Клиппинг — ключевой этап рендеринга, который отбрасывает невидимые данные для оптимизации. В современных движках (Unity, Unreal) многие виды клиппинга настраиваются автоматически.

Растеризация

Растеризация (Rasterization) — это процесс преобразования векторных графических примитивов (треугольников, линий, точек) в растровое изображение (пиксели) для отображения на экране.

Как работает растеризация?

  1. На вход подаются примитивы (обычно треугольники после преобразования в экранные координаты).

  2. Определяются пиксели, покрываемые примитивом (с учётом их глубины, формы и размера).

  3. Для каждого пикселя генерируется фрагмент (данные для фрагментного шейдера: цвет, глубина, текстурные координаты и др.).

Растеризация — это мост между математическим описанием сцены и её пиксельным представлением. Современные GPU используют сложные алгоритмы для её ускорения, но разработчикам важно учитывать разрешение, плотность полигонов и настройки антиалиасинга для баланса между качеством и производительностью.

Фрагментный шейдер

Фрагментный шейдер (Fragment Shader) — это программа, выполняемая на GPU для каждого потенциального пикселя (фрагмента) примитива (треугольника, линии, точки) в процессе растеризации. Он определяет окончательный цвет, прозрачность и другие свойства пикселя перед записью в буфер кадра.

// Пример фрагментного шейдера в HLSL  
fixed4 frag (v2f i) : SV_Target {  
    fixed4 col = tex2D(_MainTex, i.uv); // Чтение текстуры  
    return col * _Color;  
}  

Применяется для реалистичного текстурирования, расчёта освещения (включая PBR-рендеринг), создания визуальных эффектов (например, dissolve, параллакс-маппинг), стилизации графики (toon-шейдинг, пиксель-арт), постобработки (bloom, размытие) и управления прозрачностью (альфа-обрезание, полупрозрачность). Оптимизированная работа фрагментного шейдера критична для производительности, особенно при сложных материалах или на мобильных устройствах.

Тесты и смешивание

Тесты — это этапы проверки фрагментов перед отрисовкой, включающие:

  • Тест глубины (Z-test) – отсеивание невидимых пикселей (если gl_FragDepth не проходит сравнение с Z-буфером).

  • Тест шаблона (Stencil test) – маскирование областей рендера (например, для зеркал или сложных UI).

  • Альфа-тест – отбраковка прозрачных фрагментов (через discard или clip()).

Смешивание (Blending) – комбинирование цвета нового фрагмента с уже отрендеренным в буфере кадра, управляется:

  • Формулами (например, SRC_ALPHA, ONE_MINUS_SRC_ALPHA для стандартной прозрачности).

  • Режимами (аддитивное, мультипликативное, наложение света).

Как это работает?

1. Тесты:

  • Глубина (Z-test): После фрагментного шейдера GPU сравнивает значение gl_FragDepth с содержимым Z-буфера. Если фрагмент "дальше" существующего значения — он отбрасывается.

  • Стенсил (Stencil test): Пиксель проверяется по шаблону в stencil-буфере (например, маска для портала). Если тест не пройден — фрагмент не рисуется.

  • Альфа-отсечение (Alpha test): Фрагменты с альфа-каналом ниже порога (например, if (alpha &lt; 0.5) discard) удаляются до смешивания.

2. Смешивание (Blending):

  • Формула: Цвет фрагмента (src) комбинируется с цветом в буфере кадра (dst) по правилу:

    final_color = src.rgb * src.a + dst.rgb * (1 - src.a)  
  • Режимы: Аддитивный (src + dst), умножение (src * dst), наложение света (для эффектов bloom).

3. Порядок операций:

  1. Фрагментный шейдер вычисляет цвет.

  2. Применяются тесты (stencil → depth → alpha).

  3. Если фрагмент "выжил" — выполняется смешивание с буфером.

Оптимизация:

  • Ранний тест глубины (Early-Z) пропускает ненужные фрагменты до шейдера.

  • Для непрозрачных объектов отключайте blending командой Blend Off.

Пример (HLSL в Unity):

Blend SrcAlpha OneMinusSrcAlpha // Стандартная прозрачность  
ZWrite Off                    // Отключает запись глубины для полупрозрачных  

Этот механизм обеспечивает корректное наложение объектов и эффектов с контролем производительности.

URP vs HDRP vs Built-in: ключевые отличия

Built-in — пайплайн с базовым Forward/Deferred рендерингом, без оптимизаций под современные GPU. URP — оптимизированное решение для мобильных и ПК среднего уровня: Forward+, SRP Batcher, но без сложных эффектов. HDRP — AAA-рендеринг с Deferred, Ray Tracing и физически точным освещением, но требует мощного железа. Выбор зависит от платформы: URP для мобилок и инди-проектов, HDRP — для фотореализма, а Built-in лучше заменить на URP. Главный плюс Built-in в большой базе реализованных ассетов, который со временем нивелируется.

Заключение

Графический конвейер — это основа рендеринга в реальном времени. Понимание его работы помогает:

  • Писать эффективные шейдеры.

  • Оптимизировать производительность.

  • Выбирать правильный рендер-пайплайн в Unity.

Если вам интересны новости Unity разработки и в целом тема Unity - подписывайтесь на мой блог в телеграм. Я публикую там интересные новости и обзоры на них, свои мысли про бизнес, про фриланс и про разработку. Постараюсь до конца лета дописать серию статей целиком. Плюс лучший показатель того, что надо тема интересна - надо писать. Спасибо за внимание!

Комментарии (2)


  1. supremestranger
    13.07.2025 14:36

    Хорошая статья.

    Ранний тест глубины и stencil работает только при определенных соблюденных критериях:
    - нет discard'а
    - нет записи в глубину
    - возможно еще что-то платформозависимое


    1. DyadichenkoGA Автор
      13.07.2025 14:36

      Ага. Спасибо!