Компьютерная графика
GPU Graphics Pipeline
Каждую секунду GPU современной видеокарты обрабатывает 10-50 миллиардов пикселей. За 16 миллисекунд одного кадра 60fps конвейер трансформирует миллионы вершин, растеризует миллионы треугольников и запускает миллиарды фрагментных шейдеров - параллельно, на тысячах ядер. Понять этот конвейер значит понять, почему реалистичная графика возможна в реальном времени.
- **Игровые движки (Unreal, Unity):** весь рендеринг - это набор draw calls, каждый из которых прогоняет геометрию через описанный конвейер; G-buffer deferred rendering позволяет тысячи источников света.
- **WebGL и WebGPU:** браузерные игры и 3D-визуализации используют тот же конвейер через JavaScript; Three.js абстрагирует шейдеры, но под капотом - идентичные стадии.
- **Machine learning на GPU (CUDA):** GPU изначально проектировались для графического конвейера; параллелизм растеризации напрямую применим к матричным операциям нейросетей.
Вершинный шейдер и MVP-трансформация
3D-сцена хранится в объектных координатах: вершины куба заданы относительно центра куба. Чтобы отобразить сцену на экран, нужно последовательно применить три матричных преобразования: Model (объект → мир), View (мир → камера), Projection (камера → clip space). Произведение MVP = Projection × View × Model вычисляется один раз на вершину в вершинном шейдере.
После вершинного шейдера идёт Primitive Assembly: GPU собирает вершины в треугольники по индексному буферу (EBO). Затем - Geometry Shader (опционально): генерирует новые примитивы. Потом Clipping: треугольники, выходящие за frustum, обрезаются (Sutherland-Hodgman в пространстве clip coordinates). После деления на w получаем Normalized Device Coordinates (NDC) в кубе [-1,1]³.
**Нормальная матрица:** простое преобразование нормалей матрицей model даёт неверный результат при неравномерном масштабировании. Правильная матрица для нормалей: transpose(inverse(mat3(model))). Это классическая ошибка начинающих шейдерщиков.
Вершинный шейдер применяет MVP-матрицу и выводит gl_Position в clip space. Какое преобразование GPU выполняет автоматически после шейдера перед растеризацией?
Растеризация и барицентрические координаты
Растеризация - преобразование геометрического треугольника в набор пикселей (фрагментов). GPU проверяет каждый пиксель экрана: попадает ли его центр внутрь треугольника? Проверка - через три тест полуплоскостей (edge functions). Современные GPU выполняют эту проверку для блоков 2×2 пикселей параллельно (quad parallelism).
Перспективно-корректная интерполяция критична для текстур: при простой линейной интерполяции в screen space текстурные координаты искажаются при острых перспективных углах (шахматная доска на полу выглядит «компрессированной»). GPU автоматически выполняет perspective-correct interpolation.
**Quad parallelism и производительность:** GPU тестирует фрагменты блоками 2×2. Если треугольник покрывает только один пиксель из блока - все 4 шейдерных вызова всё равно запускаются (для вычисления производных dFdx/dFdy для mip-mapping). Очень маленькие треугольники неэффективны: overshading может достигать 75%.
При растеризации атрибуты (UV, нормали) интерполируются барицентрически. Почему простая линейная интерполяция в screen space даёт неверный результат для текстур?
Фрагментный шейдер и PBR-освещение
Фрагментный шейдер вычисляет цвет каждого пикселя. Он получает интерполированные атрибуты (нормаль, UV, позицию) и вычисляет освещение. Physically Based Rendering (PBR) - стандарт в современных играх и движках (Unreal, Unity HDRP) - моделирует взаимодействие света с поверхностью физически корректно.
PBR основан на уравнении рендеринга (rendering equation, Kajiya 1986): Lo(p, ωo) = Le + ∫ fr(p, ωi, ωo) · Li(p, ωi) · (n·ωi) dωi. BRDF fr описывает, как поверхность рассеивает свет. Cook-Torrance BRDF для specular части: D (distribution of normals, GGX) × F (Fresnel, Schlick) × G (geometry attenuation, Smith) / (4 · NdotV · NdotL).
**Texturing pipeline:** перед чтением текстуры GPU вычисляет LOD (level of detail) через производные dFdx(uv), dFdy(uv) - отсюда quad parallelism из предыдущего концепта. Mip-mapping выбирает нужный уровень детализации и interpolates между уровнями (trilinear filtering) для устранения алиасинга.
В PBR-материале параметр roughness = 0 и metallic = 1 (полированный металл). Как это повлияет на вид поверхности?
Output Merger: z-буфер, стенсил, альфа-смешение
Output Merger - финальная стадия GPU-конвейера: определяет, какие фрагменты попадут в framebuffer и как. Три теста выполняются в порядке: Scissor Test (прямоугольная область), Stencil Test (маска из stencil buffer), Depth Test (z-буфер). Прошедшие все тесты фрагменты участвуют в Alpha Blending.
Прозрачные объекты требуют сортировки от дальнего к ближнему (painter's algorithm) перед отрисовкой, так как alpha blending некоммутативен: порядок смешения влияет на результат. В производственных рендерах используется Order-Independent Transparency (OIT) - Depth Peeling или Weighted Blended OIT (WBOIT) для избежания сортировки.
**Render targets и G-buffer (Deferred Rendering):** вместо одного framebuffer GPU может рисовать одновременно в несколько текстур (MRT - Multiple Render Targets). Deferred shading рисует геометрию в G-buffer (albedo, normals, depth), затем освещение применяется отдельным проходом - это позволяет эффективно обрабатывать тысячи источников света.
GPU выполняет фрагментный шейдер для каждого фрагмента, и только потом z-тест отбрасывает скрытые. Значит, шейдер работает впустую для скрытых пикселей.
Early-Z: современные GPU выполняют z-тест ДО фрагментного шейдера, отбрасывая заведомо скрытые фрагменты. Фрагментный шейдер запускается только для потенциально видимых пикселей. Исключение: если шейдер пишет в gl_FragDepth или вызывает discard - Early-Z отключается.
Early-Z - ключевая оптимизация производительности. Без неё сцена с много скрытыми поверхностями тратила бы всё время GPU на шейдинг невидимых пикселей. Именно поэтому избегают discard в шейдерах там, где это возможно.
При рендеринге прозрачного стекла поверх непрозрачной стены с alpha blending (Porter-Duff «over»), в каком порядке нужно рисовать объекты?
Ключевые идеи
- **Вершинный шейдер:** MVP-трансформация object→world→camera→clip space; нормали требуют отдельной матрицы (transpose(inverse(model))).
- **Растеризация:** edge functions + барицентрические координаты для интерполяции; perspective-correct interpolation обязательна для текстур; Early-Z отсекает скрытые фрагменты до шейдера.
- **Фрагментный шейдер:** Cook-Torrance PBR BRDF (D×F×G); metallic определяет F0, roughness ширит distribution; mip-map LOD через производные dFdx/dFdy.
- **Output Merger:** z-буфер для скрытых поверхностей, alpha blending (Porter-Duff «over») для прозрачности, stencil для масок; прозрачные объекты требуют сортировки от дальнего к ближнему.
Связанные темы
GPU-конвейер объединяет линейную алгебру, вычислительную геометрию и физику света:
- Тени и глобальное освещение — Shadow maps строятся отдельным проходом через тот же конвейер; ray tracing (DXR/Vulkan RT) добавляет стадию после растеризации
- Polygon clipping (Sutherland-Hodgman) — Clipping треугольников по frustum в clip space - применение алгоритма Sutherland-Hodgman внутри GPU-конвейера
Вопросы для размышления
- Early-Z отключается, если фрагментный шейдер использует discard или записывает в gl_FragDepth. Почему это архитектурное ограничение неустранимо - почему GPU не может «угадать» результат discard заранее?
- Deferred shading переносит освещение на отдельный проход, читая данные из G-buffer. При этом MSAA (multisampling antialiasing) работает плохо с deferred. Почему геометрическая информация (нормали, глубина) несовместима с усреднением по сэмплам MSAA?
- PBR-материал описывается набором текстур (albedo, metallic, roughness, normal map). Какие физические ограничения накладывает принцип сохранения энергии на BRDF - и почему некоторые самодельные шейдеры без PBR выглядят «нереально»?