Современная 3D графика
“Современная 3D графика”
План повествования
- Введение
- Зачем сегодня нужна 3D графика.
- Что затронем в статье
- История 3D графики
- Первые эксперименты с отображением 3D объектов.
- Эволюция от wireframe до path tracing
- Поворотные моменты связанные с развитием 3D графики
- Основы 3D рендеринга
- Что такое сцена, меш, вершины, нормали и UV координаты
- Конвейер пайплайна шаг за шагом
- Разница между растеризацией и трассировкой лучей
- Необходимость оптимизации
- Материалы и PBR
- Что такое PBR и зачем он нужен.
- Основные карты материала.
- Принцип работы
- Ускорение рендеринга: BVH и пространственные структуры
- Raycast и зачем нужно искать пересечение луча с объектами
- Простые решения и их неэффективность
- Идея BVH, плюсы и минусы
- Альтернативы BVH
- Шейдеры и вычислительные блоки GPU
- Что такое шейдер?
- Как они работают на конвейере
- Пример простого шейдера
- Compute shaders: выход за пределы графики
- Архитектура GPU
- Почему GPU так хорош в графике
- Современные тренды и заключение
- Realtime ray tracing: DXR, Vulkan
- DLSS
- Возможное будущее графики
Введение
В наше время мы всё чаще видим применение 3D графики во многих сферах жизни. Но зачем нужна эта графика и почему именно “3D”?
Некоторым может показаться что трёхмерная графика есть просто “потому что это красиво”, но это только один из пунктов, хоть и не мало важный. Компьютерная трёхмерная графика используется во многих областях, начиная от развлекательного контента как видеоигры, заканчивая такими серьёзными областями как авиация, робототехника и геология. 3D графика передаёт объём и положение объектов, даёт инструменты для моделирования физики и создания визуальных симуляций - от игр до инженерных симуляций. Другими словами – трёхмерная графика является окном в другой мир, мир, где почти всё подчинено задумке автора. Но за каждой, даже за простой картинкой, скрывается не один десяток лет алгоритмических инноваций, компромиссов между скорость и точностью и постоянная гонка за тем, чтобы обмануть глаз и не перегрузить процессор.
Эта магическая отрасль работает также как и многие точные науки в мире – на математике, а если конкретней, – на целом арсенале математики: линейная алгебра, аналитическая геометрия, математический анализ, тригонометрия и даже теории вероятностей и статистике.
В этой статье мы пройдём путь от первых попыток нарисовать объёмный объект на экране до современных методов, ускоряющих визуализацию в реальном времени. Мы разберёмся как устроена 3D сцена изнутри, зачем нужны шейдеры и почему всё построено на ухищрениях. По мере продвижения будут вводиться новые термины нужные для понимания

Рисунок 1. Кадр, созданный с помощью фотореалистичного рендер движка.
История 3D графики
Одним из первых серьёзных стимулов для развития компьютерной графики стали военные и аэрокосмические проекты. Большое количество информации требовало не только вычислений, но и способ показать результат человеку – это и породило первые системы интерактивной 3D графики.
Первая графика работала на векторном дисплее компьютера Whirlwind I 1950 года, это была простейшая векторная графика, где можно было рисовать линии специальный световым пером или через математические формулы — это была самая первая 2D графика. Всего через 13 лет, в 1963 году Лоуренс Робертс впервые описал объект не рисунком, а координатами – и заставил компьютер рассчитать, как тот должен выглядеть на экране с учётом перспективы, это была главная тема его диссертации “Machine Perception of Three-Dimensional Solids”. Суть его работы заключалась в том, что объём можно описать числами, превратить в картинку с помощью математики и это будет выглядеть как 3D.

Рисунок 2. Примерный кадр, который получился в работе Лоуренса Робертса.
Его объект хоть и обладал правильной перспективой, но был “прозрачным” – абсолютно все рёбра были видны, избавиться от этой прозрачности оказалось следующей большой задачей для всей отрасли. В том же году этот же автор предложил способ скрывать задние грани у простых объектов вроде куба на основе ориентации граней, но для сложных сцен этого было мало.
Через несколько лет в 1967 году Артур Эйри разработал первый общий алгоритм скрытия невидимых рёбер, который был универсальным, но низко производительным. Его суть заключалась в том, что для каждого ребра проводились расчёты для пересечения с другими гранями. Это было очень медленно (O()), но универсально, работало для любых объектов.
В середине 1960х начали появляться эксперименты закрашивания полигонов одним цветом, главными проблемами было определить какой полигон виден в каждой точке и иметь достаточную вычислительную мощность.
В конце 1960х годов в лаборатории Ивана Сазерленда начали экспериментировать с закрашенными полигонами. Первая проблема описанная выше решалась 2-я способами: ручной сортировкой, что было очень долгим, и алгоритмом художника, принцип которого состоял в том, чтобы рисовать полигоны от заднего к переднему. Этот алгоритм работал если сцена простая и объекты не пересекаются. Также стали применять (именно применять, потому что изобрёл его фактически Иоганн Ламберт в 1760 году) алгоритм плоского затемнения (flat shading), который позволял изменять яркость полигонов на основе их поворота к камере, без этой функции каждая закрашенная модель выглядела бы плоско.

Рисунок 3. Как выглядело плоское затемнение.
В 1971 году Генри Гуро предлагает вычислять цвет на вершинах и интерполировать по полигону – стали доступны плавные переходы, но гладкие блики всё ещё невозможны.

Рисунок 4. Затемнение Гуро.
Буй Туонг Фонг придумал интерполировать нормаль, а не цвет. В итоге стало возможно имитировать гладкую поверхность объекта даже на грубой сетке. В целом, затемнение Гуро можно назвать первым ухищрением в графике – имитация более высокой плавности поверхности, чем она являлась на самом деле, буквально – игра света.

Рисунок 5. Затемнение Фонга.
Дальнейшее развитие данной науки только ускорялось.
За следующие 7 лет область компьютерной графики пополнилась ещё несколькими очень важными функциями:
- Кривые Безье – плавные линии, описываемые 2-я контрольными точками. Их Пьер Безье изначально использовал для проектирования кузовов автомобилей.
- Coon Patches – способ склеивать куски гладких поверхностей, также Стивен Кунс (автор идеи) заложил основы параметрического представления поверхностей.
- Метод Subdivision или же рекурсивное сглаживание – Эдвин Кэтмулл показал, как их грубой сетки итеративно получать более плавную поверхность.
- Normal mapping – способ имитировать мелкие детали без изменения геометрии. Это обман зрения, поверхность остаётся плоской, но свет ведёт себя так, будто она рельефна. Запатентовал Джим Блинн.

Рисунок 6. Кривые Безье.

Рисунок 7. Coons Patches.

Рисунок 8. Работа Subdivision.

Рисунок 9. Карта нормалей (а), высоко полигональная модель (b), низко полигональная модель (c), низко полигональная модель с картой нормалей (d).
Эти методы не просто улучшали картинку - они меняли сам подход. Вместо того чтобы наращивать полигоны, графика пошла по пути интеллектуального сжатия реальности: несколько контрольных точек вместо тысячи вершин, одна карта вместо рельефа. Это и есть суть “ухищрений” — не обмануть зрителя, а заставить его мозг поверить в иллюзию, не перегружая железо.
Всё это уже позволяло создавать почти бесконечное количество вещей, но была одна проблема – скорость и время. Вся 3D графика того времени была offline, то есть не рассчитанной на мгновенный результат и реагирование на изменение. Кадр мог визуализироваться часами. И чтобы перенести великие идеи в реальное время, требовалась не только математика, но и аппаратная революция и новая архитектура: графический конвейер.
Эти offline методы продолжали развиваться: в 1980-х появилась трассировка лучей, в 1990-х метод radiosity, а в 2000-х физически корректный path tracing. Но все они оставались за пределами реального времени до тех пор, пока в конце 2010-х не пришли RT-ядра и нейросети. Однако об этом в заключении. А пока вернёмся к тому, как устроен тот самый графический конвейер, что сегодня делает возможным и игры, и симуляции, и даже намёк на ту самую трассировку в реальном времени.
Основы 3D рендеринга
Теперь, после того как мы разобрались с тем, как появилась графика, можно разобраться как заставить компьютер нарисовать высокодетализированную сцену со светом, реалистичными материалами и объёмом и сделать это за 16 миллисекунд. Ответ лежит в 3х вещах: сцене, меше и строго организованном конвейере.
Начнём с основных терминов:
- Сцена – это виртуальный мир, который мы рендерим, он состоит из объектов, источников света и камеры (упрощённо). Кроме того, сцена может включать фон, туман, небо и другие элементы окружения.
- Меш (сетка) – это геометрия объекта, представляет собой набор треугольников, которые образуют форму.
- Нормали – это векторы поверхности каждого треугольника, по умолчанию они направлены перпендикулярно поверхности. Нормали, как уже упоминалось, можно изменять чтобы сделать плоский полигон похожим на рельефный. Ещё нормали используются для расчёта освещения, например затемнения Фонга.
- UV-координаты – это 2D координаты привязанные к каждой вершине меша, они необходимо чтобы оборачивать текстуру вокруг 3D объекта. Это как делать выкройку ткани для пошива.
1 Этап. Так с чего же начинается отрисовка кадра? Конечно же с подготовки данных процессором. Представьте, что процессор — это менеджер, он умный, но его “мало”, он говорит видеокарте (то есть группе обычных рабочих) какую повторяющуюся работу нужно делать.
Процессору надо подготовить или обновить данные для отправки задачи в видеокарту. Видеокарта не может сама считывать данные из памяти CPU, но она имеет uniform buffer objects – область памяти, куда CPU загружает всякие общие данные (позиция и направление камеры, флаги, коэффициенты и т.д.).
2 Этап. Проход вершинного шейдера (на GPU). Применяется модельная трансформация для расчёта мировых координат каждой вершины. Эта трансформация позволяет получить абсолютные координаты точек объектов, учитывая их смещение и поворот в сцене.
Далее применяется видовая трансформация – она возвращает view space – грубо говоря набор координат точек, где камера является центром (0, 0, 0), а координаты всех точек зависят от положения камеры.
Последним шагом является преобразование проекции в clip space. Это проекция “всего” на экран в диапазоне от -1 до 1, другими словами, это однородные координаты [x, y, z, w].
3 Этап. Примитивная сборка и обрезка. Тут вершины собираются в треугольники, обрезаются примитивы, выходящие за пределы view frustrum (обрезаются слишком дальние и слишком близкие к камере треугольники). И в конце происходит перспективное деление x/w, y/w, z/w и получаются NDC – Normalized Device Coordinates в диапазоне [-1; 1]
4 Этап. Трансформация вида. Преобразование координат вершин в координаты на мониторе [-1, 1] -> [0, w] × [0, h]. Теперь X и Y это реальные пиксели на мониторе, а Z – глубина Z-буфера.
5 Этап. Растеризация. Для каждого треугольника определяются покрываемые пиксели (фрагменты), выполняется интерполяция данных вершин (UV, нормали и тд) для каждого фрагмента.
6 Этап. Фрагментный или же пиксельный шейдер. Для каждого фрагмента вычисляется цвет пикселя текстуры, освещения и влияние других спецэффектов. На выходе возвращает цвет пикселя.
7 Этап. Output merging. На данном этапе проводится:
- Z-test чтобы ближние объекты закрывали дальние.
- Необязательный stencil test чтобы ограничить рисование только на тех пикселях, где значение равно “1”.
- Blending: смешение с уже существующим цветом, например для прозрачности. Тоже не обязателен.
В конце этого этапа цвет записывается во framebuffer.
8 Этап. Post processing. Ещё полностью необязательный этап в пайплане, на нём применяются эффекты к готовому изображению. Иногда специально для эффектов генерируются специальные кадры, для большей совместимости с эффектом и финальным качеством.
Вот несколько популярных эффектов которые могут применять к кадру:
- Bloom – ореол свечения вокруг ярких объектов
- Depth of field – глубина поля, размытие на близком и дальнем расстоянии от виртуальной камеры.
- SSAO – эффект затемнение в области углов и заглублениях, делает сцену объёмней и реалистичней.
Важно отметить, что все эти эффекты – это всего лишь цифровые имитации реальных оптических явлений. Например, в реальной жизни bloom это рассеивание света в хрустале глаза. DOF – биологическая и оптическая невозможность сфокусироваться на всех расстояниях сразу. А SSAO, которое, кстати, означает screen space ambient occlusion, в реальной жизни лучше всего видно в пасмурную погоду, где полутени образуются из-за много миллиардной выборки света, где какие-то фотоны света доходят до неба, а какие-то поглощаются ближайшими поверхностями.

Рисунок 10. Эффект Bloom.

Рисунок 11. Эффект DOF.

Рисунок 12. Эффект SSAO.
И это только малая часть ухищрений – всё построено на обмане – некоторые методы более правдоподобные, а некоторые нет.
Кстати, в классическом конвейере растеризации программируемыми этапами считаются вершинный и фрагментный шейдеры - остальное выполняется по фиксированному алгоритму.
Описанный сверху пайплайн относится к растеризации. Существует ещё пайплайн трассировки лучей. Его ключевая особенность заключается в том, что мы не проецируем треугольники на экран, а испускает лучи от камеры и смотрим во что они попали, где и в какой материал.
Распишем пайплайн трассировки лучей.
1 Этап. Генерация первичных лучей – для каждого пикселя экрана из камеры выпускается луч в направлении сцены.
2 Этап. Луч проверяет пересечение со всеми объектами. Без ускорения это будет катастрофически медленно O(N) на луч -> миллионы лучей * миллионы треугольников. С ускорением (например BVH) стоимость падает до O(log N) – уже вполне реально, но всё ещё тяжело.
3 Этап. Шейдер пересечения. Когда найдено ближайшее пересечение, вызывается шейдер для этого объекта. В этом шейдере берутся данные треугольника (нормаль, UV, материал), решается:
- Отражается ли этот луч?
- Преломляется?
- Рассеивается?
При необходимости вызываются вторичные лучи:
- Shadow ray – к источнику света чтобы проверить, в тени ли точка.
- Reflection ray – по закону отражения.
- Refraction ray – при прозрачности.
- Diffuse ray – для рассеянного света.
4 Этап. Промах. Определяется что делать, если луч ни во что не попал. Обычно возвращается цвет фона.
5 Этап. Рекурсия, которая обычно применяется в трассировке путей. Вторичные лучи тоже проходят этапы 2-4, и так до максимальной глубины (обычно после пятого отскока вклад становится незначительным). На каждом шаге накапливается вклад в итоговый цвет пикселя по физической модели уравнения рендеринга.
6 Этап. Аккумуляция и Denoising. Эти процессы чаще присущи рендерингу в реальном времени, например в играх. Пускать 1 луч на каждый пиксель недостаточно, а пускать много и смешивать – слишком медленно. Современные инженеры додумались накапливать результат трассировки во времени – temporal accumulation – и применять шумоподавление с помощью нейросетей.
Немного поговорим про архитектурные особенности трассировки лучей. Трассировка лучей задача очень непростая, компьютер не знал в какой треугольник попадёт луч и ему приходилось перебирать все, до тех пор, пока не придумали BVH который строит иерархию ограничивающих объёмов, позволяя быстро отсекать целые ветви сцены, в которые луч заведомо не попадает, и хоть оно всё ещё имело свои недостатки (например необходимость перерасчёта BVH при движении сетки), это облегчило работу с этим пайпланом.
Было ещё несколько проблем:
- Некоторые лучи могли отскочить 10 раз, а другие ни разу.
- Неоднородная нагрузка GPU – лучи летят в разные стороны и это плохо ложится на кеш GPU. Потому что GPU работает эффективно только когда тысячи потоков одновременно обращаются к близким данным - а при трассировке обращения хаотичны, и кэш постоянно сбрасывается.
К 2025 год в индустрии активно используются самые нагруженные рендер-движки для offline рендера, которые позволяют получать изображения неотличимые от реальности. А в real time подходе это только начинается – современные игры используют гибридную трассировку: основной кадр делается растеризацией, а трассировку используют только для теней, отражений и глобального освещения.

Рисунок 13. Визуальное представление растеризации и трассировки лучей.
Было бы очень плохо если бы я не упомянул разницу между трассировкой лучей и трассировкой путей.
Первое — общий инструмент для расчёта пересечений: он может использоваться только для отражений или только для теней. Второе — полный физический симулятор света: на каждом отскоке луч случайно выбирает новое направление, имитируя рассеяние, отражение и преломление. Path tracing позволяет добиться фотореализма в кино, но его стоимость колоссальна — поэтому в играх используют упрощённую, гибридную ray tracing. В offline рендере используется ещё более приближенная к физическим законам модель движка.

Рисунок 14. Шум из-за недостаточной выборки лучей.
Ускорение рендеринга: BVH и пространственные структуры
Raycast – зачем искать пересечение луча с объектами?
Ray-object intersection – это фундаментальная операция в 3D графика, физике и VR. Её единственная цель: ответить на вопрос – “во что попал луч?”. Каждая операция рэйкаста это геометрический тест, требующий 50-100 операций (не будем вдаваться в подробности рассчёта): проверка плоскости, барицентрические координаты, расчёт расстояния.
Почему это критично:
- Трассировка лучей/путей: нужно рассчитать отражение, тень или цвет пикселя, нужно знать с чем столкнулся луч.
- Физические движки: проверка коллизий (пуля в игре).
- VR/AR: определение, на какой объект смотри пользователь.
- AI: навигация.
Основная проблема? Компьютер не знает куда попадёт луч, ему нужно перебирать все треугольники на поиск пересечения. Возьмём стандартное FullHD разрешение – 2 073 600 пикселей – и сцену с 1 миллионом треугольников. Один кадр FullHD при 1 луче на пиксель и 1 млн треугольников требует более 2 триллионов проверок. На 60 FPS - это 124 триллиона в секунду.
Выводы? Без оптимизации и ускорений – с трассировкой невозможно работать.
Люди стали придумывать как можно сгруппировать треугольники самым эффективным способом. Распишем несколько решений и их особенности.
- Brute force. Простой перебор каждого треугольника на пересечение с данным лучом. Сложность O(N) на луч. При сцене на 10 000 треугольников и 1-м миллионе лучей – 10 миллиардов проверок что занимает секунды GPU на кадр.
- Regular grid. Равномерная сетка. Пространство сцены делится на кубические ячейки, луч проверяет треугольники в тех ячейках, через которые проходит луч. Легко реализовать. Из минусов:
- Если в одной ячейке 500 треугольников, а в другой 0, то эффективность минимальна.
- Для большой сцены сетка занимает гигабайты памяти
- Octree. Восьмеричное дерево. Пространство рекурсивно делится на 8 подкубов, листья содержат геометрию. Лучше работает с неоднородными сценами, чем сетка.
Если все объекты в одном месте, то дерево будет крайне несбалансированным. А если геометрия ещё и двигается, то придётся перестраивать дерево – а это дорого. В идеале сложность будет O(log N), но на практике данный способ медленнее чем BVH, о котором сейчас будет поднята тема.
BVH (Bounding Volume Hierarchy).
Суть идеи – вместо того чтобы проверять все треугольники мы группируем объекты в коробки, группируем в более крупные коробки и строим дерево, где корень – “коробка всего мира”.
Как именно это ускоряет трассировку? Луч сначала проверяет корневую коробку. Если луч в неё не попал, то вся сцена отбрасывается за 1 шаг. Если попал – проверяются дочерние коробки. И так рекурсивно, пока не дойдём до треугольников. На практике 99% треугольников отсекаются на верхних уровнях дерева.
Как строится BVH?
Коробка почти всегда является прямоугольником, выровненным по осям.
Берётся множество объектов, выбирается ось, по которой объекты наиболее разбросаны, объекты сортируются по этой оси и делятся на две части. Потом для каждой части строится свой узел, пока в листе не останется <= 4 треугольников.
Плюсы BVH:
- В среднем сложность выходит O(log N). Для 1 миллиона треугольников приходится примерно 20 проверок, вместо 1 миллиона.
- Аппаратная поддержка. Специальные RT блоки в современных видеокартах оптимизированы под пересечение луча с прямоугольными “коробками”.
- Способ работает с любой геометрией.
Но есть и минусы:
- Занимает дополнительно 30% памяти от размера геометрии.
- Для анимации требуется перестройка дерева – что дорого.
- Метод не всегда даёт оптимальное дерево, вероятность неоптимального дерева увеличивается с ростом сложности сцены.
BVH стал стандартом, потому что обеспечивает стабильную O(log N) сложность при умеренном потреблении памяти и отлично ложится на GPU-кэш.

Рисунок 15. Визуализация уровней BVH.
Какие ещё бывают альтернативы BVH?
- K-d tree. Пространство делится плоскостями по осям. Имеет более компактную упаковку, но только для статичных сцен. Перестроение при анимации стоит очень дорого. В основном используется для offline рендеров.

Рисунок 16. Визуализация k-d дерева.
- BIH (Bounding Interval Hierarchy). Гибрид BVH и r-d дерева. Каждый узел хранит интервалы по одной оси. Потребляет меньше памяти, но построение медленное.
- Spatial hashing. Объекты хешируются по координатам в ячейки хэш таблицы. Не подходит для глобальной трассировки, к тому же коллизии хешей замедлят работу трассировщика.
Подводя итоги этой главы, можно вывести, что BVH не идеален. Он потребляет много памяти и не любит анимацию, но работает лучше и стабильнее всех остальных аналогов. Другими словами – это инженерный компромисс, доведённый до совершенства: вместо того чтобы считать всё, научились считать достаточно, чтобы обмануть глаз.
Материалы и PBR
Что такое PBR и зачем он нужен? Фотореалистичный результат в 3D графике зависит не столько от количества полигонов, сколько от того, насколько правдоподобно описаны материалы. PBR (Physically Based Rendering) — это подход, который опирается на физику света, а не на художественные ухищрения. Он задаёт единые правила того, как поверхность взаимодействует с освещением, чтобы результат выглядел одинаково реалистично в любых сценах и при любом освещении.
Главная идея PBR — консистентность и предсказуемость. Один и тот же материал должен вести себя одинаково как в Blender 3D, так и в Unreal Engine или Unity. Это достигается за счёт унифицированных моделей отражения и набора текстурных карт, которые описывают свойства поверхности.
PBR-материал обычно состоит из нескольких карт, каждая из которых отвечает за конкретный физический параметр:
- Albedo — чистый цвет поверхности без учёта света и теней.
- Normal Map — создаёт иллюзию мелких неровностей без увеличения количества полигонов.
- Metallic — определяет, является ли материал металлом или диэлектриком. Металлы отражают почти весь свет, а диэлектрики (дерево, пластик, камень) рассеивают его внутри.
- Roughness — степень шероховатости поверхности. Чем выше значение, тем мягче и шире отражение.
- Ambient Occlusion — карта локальной затенённости, усиливающая реализм в углублениях и стыках. Обычно не используется в фотореалистичных рендерах.
Некоторые движки также используют дополнительные карты:
- Height — карта высот. По сути, хранит относительную высоту точек и часто используется для генерации normal map или параллакса.
- Emission – свечение. В играх обычно нужен для простого обозначения что “эта текстура излучает свет” и для постобработки. В фотореалистичных движках может излучать свет.
- Opacity — Прозрачность. Карта регулирующая прозрачность пикселя на основе диапазона чёрный-белый”
Перейдём к “двигателю” фотореалистичной графики. В основе PBR лежит понятие BSDF (Bidirectional Scattering Distribution Function) — функция, описывающая, как свет рассеивается при взаимодействии с поверхностью. Она объединяет две составляющие:
- BRDF (Bidirectional Reflectance Distribution Function) — отражение света;
- BTDF (Bidirectional Transmittance Distribution Function) — пропускание света сквозь такие материалы как стекло, жидкость, пластик.
Blender, Unreal и другие движки используют BSDF-шейдеры как универсальный инструмент для описания разных типов материалов — от металла до полупрозрачных тканей.
В практическом виде BSDF можно представить как сумму нескольких моделей:
- Diffuse - рассеянное отражение, например, матовая краска
- Specular – зеркальные поверхности,
- Transmission/Subsurface - подповерхностное рассеяние.
Все эти компоненты подчиняются принципу сохранения энергии — поверхность не может отражать больше энергии, чем получает, благодаря чему материалы выглядят естественно при любом освещении. В любом случае художники могут нарушать этот закон, при создании шейдеров, если хотят стилизовать свою графику.

Рисунок 17. Демонстрация влияния параметра metallic на внешний вид. У левого куба metallic 1.0, а у правого metallic 0.0.

Рисунок 18. Демонстрация влияния неровностей, добавленной картой нормалей. Для лучшего эффекта параметр metallic оставлен на 1.0.

Рисунок 19. Демонстрация влияния параметра шероховатости, в данном случае выставлено 0.15. Для лучшего эффекта параметр metallic оставлен на 1.0.

Рисунок 20. Демонстрация влияния параметра emission в фотореалистичном движке рендеринга.
В целях оптимизации разработчики часто комбинируют 3 карты (AO, roughness и metallic) в одну. Это возможно благодаря тому, что каждому упомянутому параметру нужен только один канал данных, то есть значение от 0 до 1. Поэтому их можно упаковать в разные цветовые каналы одной текстуры: AO в красный, roughness в зелёный, metallic в синий. Это уменьшает количество обращений к памяти и ускоряет рендеринг, особенно в игровых движках.
Во всех современных движках шейдеры собираются из нодов (nodes) — визуальных блоков, соединённых линиями. Так художник буквально собирает материал из кусочков логики и текстур.
В примере ниже три текстуры соединены с соответствующими входами стандартного BSDF-шейдера. Нода Bump используется для преобразования карты высот в нормали.

Рисунок 21. Использование нод в шейдере в blender 3D. В данном примере мы соединяем 3 текстуры в соответствующие каналы унифицированного шейдера.
Шейдеры и вычислительные блоки GPU.
Так как разработчики создают окружение и почему вся графика такая разная? Всё дело в шейдерах – это мини-программы для GPU для обработки одного элемента данных: вершины, пикселя или луча. Мы уже знакомы с несколькими типами шейдеров:
- Vertex Shader – шейдер преобразования вершин.
- Fragment Shader – текстурирование, освещение, спецэффекты.
- Geometry Shader – генератор дополнительной геометрии.
- Tessellation Shader – шейдер увеличивающий детализацию существующей сетки.
- Compute Shader – физика, ИИ и другие пользовательские параллельные задачи.
- Raytracing shader – реалистичное отражения и глобальное освещение.
Напомню, что пайплайн шейдеров по большой части фиксированный – разработчики создают фрагментные и вершинные шейдеры.
Шейдеры работают в конвейере по принципу “Data parallelism”. GPU не выполняет шейдеры последовательно, он запускает тысячи идентичных копий одного шейдера одновременно, где каждый обрабатывает свой элемент. 1 копия вершинного шейдера обрабатывает 1 вершину и аналогично 1 копия фрагментного шейдера обрабатывает 1 пиксель.
Пройдёмся по тому, как рисуется квадрат:
- CPU отправляет 4 вершины квадрата в GPU.
- GPU запускает 4 экземпляра вершинного шейдера. Все работают параллельно.
- Растеризатор определяет, какие пиксели покрываются этим квадратом. Например, если квадрат занимает область 100×100 пикселей, то он покрывает 10 000 фрагментов.
- GPU запускает 10 000 экземпляров фрагментного шейдера - по одному на каждый покрытый пиксель.
- Результат: 10 000 цветов записываются в буфер кадра.
Каждый экземпляр шейдера получает свои координаты, UV и нормали. Таким образом, при увеличении числа пикселей нагрузка на GPU растёт линейно, но благодаря массовому параллелизму он справляется с ней на порядки эффективнее, чем CPU.
Если сравнивать скорость средне-бюджетного процессора и средне-бюджетной видеокарты, то разница для примитивной 3D сцены будет такой: 300-600 кадров в секунду на GPU и примерно 20-30 на CPU.
Рассмотрим простой шейдер на языке GLSL – OpenGL Shading Language – специализированный язык для написания шейдеров.

Рисунок 22. Код простого фрагментного шейдера.
Шейдер из примера принимает нормаль vNormal и направление света uLightDirection, вычисляет освещённость через скалярное произведение, умножает базовый цвет на освещённость, добавляет минимальное освещение и возвращает цвет пикселя FragColor.
Комментарий “#version 330 core” обязателен – он нужен компилятору чтобы понимать какие инструкции разрешены.
Сейчас код шейдера выглядит довольно простым. Что если мы его усложним, добавив, к примеру, if-else?

Рисунок 23. Код предыдущего шейдера с добавлением if else ветки.
Теперь шейдер будет возвращать красный цвет, если нормаль смотрит вверх, в противном случае – синий. Казалось бы – ничего не предвещает беды, однако так делать крайне не советуется.
Всё дело в том, как GPU обрабатывает команды. Он группирует несколько пикселей (обычно 32) в варпы и одновременно выполняет одинаковые инструкции для всех потоков варпа. Если в одном варпе некоторые пиксели попадают в if, а другие в else, GPU вынужден выполнить обе ветки, а затем выбрать нужный результат. Это называется дивергенцией и снижает производительность, особенно если ветки содержат тяжёлые вычисления. В нашем примере, vNormal.z различается у соседних пикселей, что почти гарантированно вызывает дивергенцию. Поэтому использование if-else, зависящего от локальных данных - неэффективно.
Но что же получается - if/else нельзя использовать?
Можно, но с умом.
Если условие зависит от глобальных, одинаковых для всех пикселей данных, например, от uniform bool - то дивергенции не возникает. Все потоки в варпе идут по одной ветке, и производительность остаётся высокой.

Рисунок 24. Пример использования if else, в котором производительность не упадёт.
Здесь uUseRedEffect — одинаков для всех пикселей, и все варпы идут по одной ветке. Нет дивергенции = нет потерь производительности. Но что, если if зависит от локальных данных и его всё-таки нужно использовать? Это возможно, но требует осторожности. Вместо if можно использовать математические функции, которые не создают ветвлений. Например, step, mix, smoothstep - они работают одинаково для всех пикселей, но дают разный результат в зависимости от входа.

Рисунок 25. Блок кода с дивергенцией.
Функция step(edge, x) в GLSL возвращает 0, если x < edge, и 1, если x >= edge. Таким образом, она заменяет бинарное условие if на числовое значение 0/1, с которым можно работать математически.
Такой код не вызовет дивергенцию, так как все “пиксели” выполняют одинаковую команду, но с разными значениями. Это путь предсказуемой логики.
Математические операции предпочтительнее ветвлений, когда условие зависит от локальных данных.
Перейдём к Compute shaders.
В отличие от всех остальных шейдеров, compute shaders не является частью графического конвейера. Он работает независимо от рендеринга и может использовать GPU для любых вычислений.
Чем он отличается?
- Нет связи с вершинами или пикселями — только ваши данные (буферы, текстуры).
- Могут читать и писать в одни и те же буферы, создавая циклы вычислений.
- Работают не на каждый кадр, а по необходимости — например, для физики или постобработки.
Он работает с произвольными буферами без предположений о структуре данных. Его вход и выход — это указатели на память, а не атрибуты вершин или фрагментов. Шейдер не вызывается автоматически, а запускается явно через dispatch с заданным количеством групп и нитей. Каждая нить выполняет одинаковый код, но с уникальным индексом и работает независимо.
Также у него нет встроенной логики рендеринга. Нет rasterization, нет Z-test, нет blending. Нет ограничений на типы операций — только на время выполнения в 2 секунды как мера защиты от зависания.
Он используется там, где нужно параллельно обработать большие массивы данных без привязки к изображению.
Физика частиц, симуляция жидкости, обработка анимационных костей, генерация шума, сжатие текстур, сортировка объектов по глубине, постобработка изображений — всё это можно сделать на compute shader, не трогая графический конвейер.
Ликбез по архитектуре GPU.
- Ядро архитектуры: GPU состоит из множества вычислительных блоков, в NVIDIA это Streaming Multiprocessors, в AMD — Compute Units. Каждый SM/CU содержит регистры, ALU, блоки текстурирования, кеш L1/shared и менеджер потоков.
- Парадигма исполнения: SIMT (Single Instruction, Multiple Threads) исполнение одинаковых инструкций на десятках/сотнях потоков одновременно. Потоки группируются в “warps” у NVIDIA обычно по 32 или “wavefronts” у AMD, обычно 64.
- Память и кеши: есть многоуровневая иерархия — регистры (самые быстрые), shared/L1 (быстрый, локальный), L2 (общий), глобальная память (VRAM) с высокой пропускной способностью, текстурные кэши и специальные блоки.
- Специализированные блоки: текстурные блоки samp, ROPs, RT-ядра для трассировки лучей, тензор/матричные ядра для ускорения нейросетевых вычислений DLSS, денойзинг.
- Схема работы: аппарат разбивает работу на тысячи нитей, планировщик заполняет SM потоками, а GPU скрывает задержку памяти переключением на другие группы потоков.

Рисунок 26. Общая архитектура GPU.
Почему GPU так хорош для графики?
GPU стал тем, чем он есть, не потому что это просто “мощная видеокарта”, а потому что его архитектура изначально сконструирована под задачу массового повторяющегося вычисления одного и того же типа операций над большими объёмами данных. В классической графике у вас тысячи вершин и миллионы фрагментов, и для каждого нужно выполнить один и тот же набор трансформаций и вычислений - умножить матрицы, интерполировать атрибуты, посчитать освещение. GPU решает это одновременно: тысячи лёгких потоков запускаются на одинаковой инструкции и обрабатывают разные данные параллельно, а аппаратная организация регистров, кешей и блоков текстурирования оптимизирована под такие потоки. Это даёт огромный выигрыш по пропускной способности — не по скорости одного потока, а по суммарной пропускной способности системы.
Ещё одна важная деталь - специализированные блоки. Видеочипы давно уже не только про арифметику, там есть быстрые блоки для выборки и фильтрации текстур, блоки вывода ROPs, и сейчас — специализированные юниты для трассировки лучей и для нейросетевой обработки изображений. Эти блоки снимают часть нагрузки с универсальных ALU и позволяют решать задачи, которые раньше были слишком дорогими в реальном времени. Наличие таких ускорителей — причина, по которой современные движки могут гибридно смешивать растеризацию и кусочки трассировки, не ломая общую скорость рендеринга кадров.
Наконец, важен уровень памяти и способ её использования: у GPU высокая пропускная способность VRAM и локальные кеши, которые позволяют быстро подкачивать текстуры и буферы. Но это одновременно и “подводный камень”: чтобы получить максимальную выгоду, нужно писать шейдеры и алгоритмы так, чтобы соседние потоки работали с близкими адресами и минимизировали случайные обращения в глобальную память. В результате — если код хорошо ложится на парадигму GPU (низкая дивергенция, кооперативный доступ к памяти, минимальное ветвление), можно получить десятки и сотни кратный прирост производительности по сравнению с CPU реализацией того же алгоритма.

Рисунок 27. Как изменялась графики сквозь года.
Современные тренды и заключение
Сейчас графика развивается по двум взаимосвязанным путям: усиление аппаратной части рендера и активное применение машинного обучения в конвейерах рендеринга. С появлением стандартов и API для трассировки лучей, таких как Microsoft DXR и спецификации Vulkan Ray Tracing, трассировка перестала быть эксклюзивностью академических рендереров — она стала первым классом функциональности в современных графических API. Это открыло путь к гибридным рендерам, где растеризация остаётся основой кадра, а трассировка берёт на себя теневые, отражательные и глобально-освещённые аспекты сцены. Такие возможности сделали трассировку удобной и переносимой между платформами и поставщиками драйверов.
Параллельно развивается класс технологий, который принято сводить под “нейронным ускорением” — DLSS у NVIDIA и аналогичные технологии у других вендоров. Идея простая и эффективная: рендерить часть работы в облегчённом виде (меньше пикселей или ниже качество) и затем восстановить картинку с помощью нейросети, используя данные прошлого кадра, движение и дополнительные сигналы. Это даёт возможность “отдать” часть честного рендеринга нейросети и получить заметный прирост FPS при минимальной потере качества, а в некоторых сценариях — и с улучшением качества по сравнению с классическим апскейлингом. Последние итерации таких систем уже умеют не только апскейлить, но и генерировать промежуточные кадры, улучшать реконструкцию лучевой информации и снижать шум в трассированных сценах, что ещё сильнее сдвигает границу между real-time и offline качеством.
Что дальше? Нельзя сказать, что впереди одна технологическая дюжина — скорее это несколько параллельных течений, которые будут толкать графику вперёд. Во-первых, гибридные рендереры, где растеризация отвечает за большую часть картинки, а трассировка точечно поднимает правдоподобие — уже стандарт; дальше будут убирать всё больше упрощений у трассировки и переносить туда больше задач по освещению и отражениям. Во-вторых, нейросети будут всё активнее встроены в пайплайн: от денойзинга и апскейла до генерации материалов и автоматического ретопологии ассетов. В-третьих, растёт роль “умных” драйверов и аппаратных ускорителей — от RT ядер до матричных блоков для трансформеров, которые дадут новые инструменты и облегчат разработку более сложных эффектов в реальном времени.
Современная 3D-графика перестала быть гонкой только за полигоны и шейдерные трюки. Сегодня это про сочетание правильной математики, архитектуры и инженерных компромиссов: где и как применить трассировку, какие данные упаковать в текстуры, какие вычисления доверить GPU, а какие — нейросети. Если вы учитесь разбираться в этом мире, вы не просто изучаете приёмы отрисовки — вы учитесь думать в терминах системы: баланс производительности и качества, переносимости и удобства художника, а также практически полезных оптимизаций. Понимание этих связей и принципов даёт гораздо больший выигрыш, чем набор трюков: оно делает ваши решения устойчивыми к изменениям в железе и софте, и позволяет создавать сцены, которые одинаково хорошо выглядят в разных движках и на разном железе.

Scotium