Какое устройство пк имеет обработчик пиксельных и вершинных шейдеров

Обновлено: 05.07.2024

Всем привет! Так как я поставила себе цель когда-нибудь выпустить блог о том, какие технологии улучшения трехмерной графики применяются в топовых играх в последнее время, то я решила сначала разобраться в том, как за последнее время плотного использования трехмерной графики в видеоиграх она вообще достигла таких высот. Или проще говоря засчет чего это происходит, что отвечает за детализацию изображений, которое мы видим на своих экранах, запуская очередную любимую игру.

Всем нам нравится, когда в играх реалистичные пейзажи: красиво переливающиеся блики солнца на водной глади, движения каждого листочка на ветке во время ветра, волосы персонажей выглядят лучше, чем у тебя и много другое. В общем все, что нас так притягивает в современных играх не взялось из неоткуда(ежу понятно). Так кто же «виновник торжества»

Может кто-то уже и догадался по названию поста(глупый вопрос, конечно, там ж написано), речь пойдет о шейдерах. Что это, для чего и почему. Постараюсь разложить все по полочкам. Начнем:)

Что вообще такое эти самые шейдеры. Шейдеры — это программки, которые исполняются на видеокарте и описывают эффекты, методы обработки и прорисовки графики. Изначально GPU преимущественно обрабатывали 2D графику. В это время появляются так называемые акселераторы(ускорители). Они берут на себя часть операций, связанную с обработкой графических данных, сами прорисовывают каждый пиксель и определяю его цвет. Все это использовали видеокарты NVIDIA Riva128 и 3Dfx Voodoo. Эти карты брадли на себя только такие задачи как текстурирование и закраску, все остальное выполнял СPU. Естественно картинка была соответствующей. Подумайте сами, процессор и так загружен другими системными задачами, а тут еще ему и графические данные прорисовывать приходится, которые, по сравнению с арифметическими расчетами, просто звери.


Но в те времена этого было достаточно конечному пользователю. Далее начали применяться графические библиотеки такие как Open Gl, Direct 3d. Они снимали с CPU значительную часть нагрузки и брали на себя. Теперь видеокарта строила сцену с нуля. Производителям видеоигр в те времена такое решение было удобно. Все применяемые в тех играх эффекты были уже аппаратно встроены в видеокарты. Их было достаточно для игр того поколения. Но ничего не стоит на месте, разработчики видеоигр стремились вырваться вперед по сравнению с конкурентами и появилась потребность как-то улучшать имеющиеся эффекты 3D графики. Тогда то и появилась проблема — для того, чтоб апгрейдить какой-то эффект, разработчикам видеоигр приходилось подождать годик, чтоб вышла видеокарта, поддерживающая данный эффект. Что было и дорого и длительно по времени. Тогда — то и появились шейдеры. Так началась новая эра развития трехмерной графики в игровой индустрии.


Теперь пробежимся коротенько по видам шейдера дабы лучше понять их применение.
Вершинные шейдеры. Из назавания понятно, что они оперируют с вершинами(координаты вершин, текстурные координаты) Сопоставляют координаты вершин объекта и координаты вершин текстуры. Следовательно, они отвечают за любое трансформирование картинки. Например, колышащиеся деревья и трава на ветру, лицевая анимация и динамично перемещающийся свет.


Пиксельные шейдеры или геометрические. В отличие от вершинных, геометрические шейдеры оперируют на уровне примитивов. Проще говоря, они делят изображение на примитивы(точки, отрезки, треугольники) и работают с каждым в отдельности. Сглаживают, освещают, анимируют, добавляют эффекты HDR и Motion Blur. Главное назначение пиксельных шейдеров заключается в комплексном смешивании текстур, просчете освещенности и оптических свойств моделей, постобработке кадра. В общем, почти все, что мы видим в современных играх, — плод трудов пиксельных шейдеров!

Естественно, как вы навреное уже поняли, шейдеры пишутся игровыми программистами. С развитием трехмерной графики, написание шейдеров усложняется. Раньше они вообще состояли из небольшого куска кода на ассемблероподобном языке, а сейчас же существует несколько шейдерных языков, которые ориентированые на достижение максимального качества визуализации. Ни одна современня игра не обходится без шейдеров, если вы видите потрясную картинку с ослепительным эффектом, то знайте, это все работа шейдеров. Без них мы бы до сих пор играли в игры типо первых Quake, Doom, Unreal. Так же необходимо отметить, что современные игры сочетают в себе много шейдеров одновременно, которые выполняются параллельно вашей видеокартой.

Однако, но хоть шейдеры — это программы, их количество напрямую связано с качеством вашей видеокарты. Их количество настроено изначально на заводе-производителе и самому изменить это параметр невозможно. Хотя, обладателям консолей не стоит этого бояться т.к игры изначально оптимизируют под консоли и она в любом случае пойдет на приемлемом качестве. И как раз из-за шейдеров обладателям ПК приходится сверять системные характеристики и постоянно увеличивать мощность своего компьютера. Так же нужно помнить, что всего хорошего должно быть в меру. Программист, который разрабатывает шейдеры должен четко понимать что, где и как шейдер должен делать. Иначе получится полная ж… иными словами переизбыток шейдеров ничего хорошего не сулит!


И под конец хочу сказать, что без шейдеров не было бы ничего из того, что мы сейчас имеем. Шейдеры — это мощное средство для реализации достижений сложной трехмерной графики. Чем сложнее эффекты, тем прокаченней и больше по количеству нужно шейдеров. И… у меня плохо с заключением… и если вы еще до сих пор не поняли что такое шейдеры, то сходите съешьте шоколадку и прочитайте этот пост еще раз:) Спасибо за прочтение!


Эй-эй, это между прочим пример сглаживания!)

A Пиксельный Шейдер - это компонент GPU (графический процессор), который может быть запрограммирован для работы на пиксельной основе и заботиться о таких вещах, как освещение и отображение ударов.

A Вершинный Шейдер также является компонентом GPU и также программируется с использованием определенного сборочного языка, такого как пиксельные шейдеры, но ориентированы на геометрию сцены и могут делать такие вещи, как добавление мультяшных силуэтных краев к объектам и т. д.

ни лучше каждый из них имеет свое конкретное применение. Большинство современных графических карт, поддерживающих DirectX 9 или лучше включить эти функции.

есть несколько ресурсов в интернете для того, чтобы лучше понять, как использовать эти вещи. NVidia и ATI особенно хорошие ресурсы для документов по этой теме.

DirectX 10 и OpenGL 3 представили шейдер геометрии в качестве третьего типа.

в порядке перевода-

Вершинный Шейдер - принимает одноточечный и может отрегулировать его. Может использоваться для разработки сложных * * vertex lighting calcs в качестве настройки для следующего этапа и / или деформации точек вокруг (колебание, масштаб и т. д.).

каждый результирующий примитив передается в

Геометрия Шейдеров - принимает каждый преобразованный примитивный (треугольник и т. д.) и может выполнять вычисления на нем. Это может добавить новые точки, забрать их или переместить их по мере необходимости. Это может использоваться для динамического добавления или удаления уровней детализации из одной базовой сетки, создания математических сеток на основе точки (для сложных систем частиц) и других подобных задач.

каждый результирующий примитив преобразуется в scanline, и каждый пиксель, который охватывает промежуток, проходит через

Пиксельный Шейдер (фрагмент Шейдер в OpenGL) - вычисляет цвет пикселя на экране на основе того, что проходит вершинный шейдер, связанных текстур и пользовательских данных. Это не может прочитать текущий экран вообще, просто выяснить, какой цвет/прозрачность этот пиксель должен быть для текущего примитива.

эти пиксели затем помещаются в текущий буфер рисования (экран, backbuffer, render-to-texture, что угодно)

все шейдеры могут получить доступ к глобальным данным, таким как матрица мировоззрения и разработчик смогите пройти в простые переменные для их для использования для освещать или любой другой цели. Шейдеры обрабатываются на ассемблерном языке, но современные версии DirectX и OpenGL имеют встроенные компиляторы языка высокого уровня c-like, встроенные в HLSL и GLSL соответственно. У NVidia также есть компилятор шейдеров CG, который работает на обоих API.

[отредактировано, чтобы отразить неправильный порядок, который я имел раньше (геометрия->вершина->пиксель), как отмечено в комментарии.]

теперь есть 3 новых шейдеры, используемые в DirectX 11 и тесселяции. Новый полный порядок шейдеров-вершина- > корпус - >тесселяция->домен->геометрия - >пиксель. Я еще не использовал эти новые, поэтому не чувствую себя квалифицированным, чтобы описать их точно.

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

пиксельные (или, точнее, фрагментарные) шейдеры принимают значения, интерполированные из обработанных в вершинном шейдере, и генерируют фрагменты пикселей. Большинство "крутых" вещей выполняется в пиксельных шейдерах. Здесь происходят такие вещи, как поиск текстуры и освещение.

Специфический DirectX:

шейдеров:

набор программ, реализующих добавление графических объектов к объектам, которые не определены в фиксированном конвейере рендеринга. Из-за этого мы можем иметь наши собственные графические эффекты в соответствии с нашими потребностями - то есть., Мы больше не ограничены предопределенным " фиксированным" оперативный.

HLSL: (язык высокого уровня затенения):

HLSL-это язык программирования, такой как C++, который используется для реализации шейдеров (пиксельных шейдеров / вершинных шейдеров).

Вершинных Шейдеров:

вершинный шейдер-это программа, выполняемая на GPU видеокарты, которая работает на каждой вершине индивидуально. Это облегчает нам возможность написать собственный алгоритм для работы с вершиной.

Пиксельные Шейдеры:

пиксельный шейдер-это программа, выполняемая на GPU видеокарты во время процесс растеризации для каждый пиксель. Это дает нам возможность получить доступ / манипулировать отдельными пикселями напрямую . Этот прямой доступ к пикселям позволяет нам достичь различных специальных эффектов, таких как мультитекстурирование, освещение на пиксель, глубина резкости, моделирование облаков, моделирование огня и сложные методы затенения.

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

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

их можно использовать для создания специальных эффектов, теней, освещения и т. д.

Так как каждый пиксель / вершина управляется индивидуально этими шейдеры поддаются очень параллельной архитектуре современных графических процессоров.

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




Немного теории

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

Примеры где используются вершинные шейдеры


Деформация объектов — реалистичные волны, эффект ряби от дождя, деформация при попадании пули, всё это можно сделать вершинными шейдерами, и это будет выглядеть реалистичнее, чем тоже самое сделанное через Bump Mapping в фрагментной части шейдера. Так как это изменение геометрии. В шейдерах уровня 3.0 на эту тему есть техника под названием Dispacement Mapping, так как в них появился доступ к текстурам в вершинной части шейдера.


Анимация объектов. Игры выглядит живее и интереснее, когда растения реагируют на персонажа или деревья покачиваются на ветру. Для этого так же используются вертексные шейдеры


Мультяшное освещение или стилизованное. Во многих играх с точки зрения стиля значительно интереснее выглядит не pbr освещение, а стилизация. При этом не имеет смысла рассчитывать ничего в фрагментной части.


Скининг. В данный момент времени в игровых движках эта задача решена, но тем не менее в вертексных шейдерах полезно разбираться, чтобы понимать, как оно работает.

Простые примеры работы с вертексами


Не хочется чтобы получилось, как в старых уроках по рисованию совы, поэтому пойдём последовательно по этапам. Создадим стандартный surface шейдер. Это можно сделать по правой кнопке мыши в Project View или в верхней панели во вкладке Assets. Create->Shader->Standard Surface Shader.

И получим такую стандартную заготовку.

Shader "Custom/SimpleVertexExtrusionShader"
Properties
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" <>
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
>
SubShader
Tags < "RenderType"="Opaque" >
LOD 200

struct Input
float2 uv_MainTex;
>;

half _Glossiness;
half _Metallic;
fixed4 _Color;

void surf (Input IN, inout SurfaceOutputStandard o)
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
>
ENDCG
>
FallBack "Diffuse"
>

Удалим из него всё лишнее, чтобы оно не отвлекало, так как в данный момент времени оно не нужно. И получим такой короткий шейдер.

Shader "Custom/SimpleVertexExtrusionShader"
Properties
_Color ("Color", Color) = (1,1,1,1)
>
SubShader
Tags < "RenderType"="Opaque" >
LOD 200

struct Input
float4 color : COLOR;
>;

void surf (Input IN, inout SurfaceOutputStandard o)
fixed4 c = _Color;
o.Albedo = c.rgb;
>
ENDCG
>
FallBack "Diffuse"
>


Просто цвет на модели с освещением. За расчёт освещения в данном случае отвечает Unity.

Для начала добавим самый простой эффект из примеров Unity. Экструзия по нормали, и на его примере разберём, как это работает.

Shader "Custom/SimpleVertexExtrusionShader"
Properties
_Color ("Color", Color) = (1,1,1,1)
_Amount ("Extrusion Amount", Range(0,1)) = 0.5
>
SubShader
Tags < "RenderType"="Opaque" >
LOD 200

struct Input
float4 color : COLOR;
>;

fixed4 _Color;
float _Amount;

void vert (inout appdata_full v)
v.vertex.xyz += v.normal * _Amount;
>
void surf (Input IN, inout SurfaceOutputStandard o)
fixed4 c = _Color;
o.Albedo = c.rgb;
>
ENDCG
>
FallBack "Diffuse"
>

Можно заметить важную особенность шейдеров. Хотя шейдер исполняется каждый кадр — результат получившийся в ходе работы шейдера не сохраняется в меше, а используется только для отрисовки. Поэтому к функциям шейдера нельзя относится, как к тому же Update в скриптах. Они применяются каждый кадр не изменяя данные меша, а просто модифицируя меш для дальнейшей отрисовки.

Shader "Custom/SimpleVertexExtrusionWithTime"
Properties
_Color ("Color", Color) = (1,1,1,1)
_Amplitude ("Extrusion Amplitude", float) = 1
>
SubShader
Tags < "RenderType"="Opaque" >
LOD 200

struct Input
float4 color : COLOR;
>;

fixed4 _Color;
float _Amplitude;

void vert (inout appdata_full v)
v.vertex.xyz += v.normal * _Amplitude * (1 - _SinTime.z);
>
void surf (Input IN, inout SurfaceOutputStandard o)
fixed4 c = _Color;
o.Albedo = c.rgb;
>
ENDCG
>
FallBack "Diffuse"
>

Итак, это были простые примеры. Пора рисовать сову!

Деформация объектов

Но на самом деле surface шейдеры в юнити это сама по себе большая и объёмная тема. Плюс не всегда хочется связываться с освещением Unity. Иногда нужно схитрить и написать максимально быстрый шейдер, который обладает только нужным набором заготовленных эффектов. В юнити можно писать более низкоуровневые шейдеры.

Низкоуровневые шейдеры


По старой доброй традиции работы с шейдерами здесь и в дальнейшем будем мучать стенфордского кролика.

В целом так называемый ShaderLab в юнити — это по сути визуализация инспектора с полями в материалах и некоторое упрощение написания шейдеров.

Возьмём общую структуру Shaderlab шейдера:

Shader "MyShaderName"
Properties
// свойства материала
>
SubShader // сабшейдер для определённого железа (можно определить директивой компиляции)
Pass
// проход шейдера
>
// для некоторых эффектов может понадобится несколько проходов
>
// может понадобится больше сабшейдеров
FallBack "VertexLit" // в случае если на определённом железе не работает шейдер, то к каком откатиться
>

Скажем возьмём один из самых частых примеров — шейдер для вывода цвета нормалей:

Shader "Custom/SimpleNormalVisualization"
Properties
>
SubShader
Pass
CGPROGRAM

struct v2f float4 pos : SV_POSITION;
fixed3 color : COLOR0;
>;

v2f vert (appdata_base v)
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + 0.5;
return o;
>

fixed4 frag (v2f i) : SV_Target
return fixed4 (i.color, 1);
>
ENDCG
>
>
FallBack "VertexLit"
>


В данном случае в вертексной части мы записываем в цвет вершины преобразованное значение нормали, а в пиксельной части применяем этот цвет в качестве цвета модели.

Функция UnityObjectToClipPos — это вспомогательная функция Unity (из файла UnityCG.cginc), которая переводит вертексы объекта в позицию связанную с камерой. Без неё объект, при попадании в зону видимости (фруструм) камеры, будет рисоваться в координатах экрана вне зависимости от положения трансформа. Так как первоначально позиции вертексов представлены в координатах объекта. Просто значения относительно его пивота.

Важное уточнение. Названия аттрибутов меша не играют никакой роли. То есть допустим в аттрибут color вы можете писать вектор отклонения от первоначальной позиции (таким образом иногда делают эффект, когда идёт персонаж, чтобы трава от него «отталкивалась»). То как будет обрабатываться данный аттрибут целиком и полностью зависит от вашего шейдера.

Заключение

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

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

Charles Petzold

В 1975 году художник-монументалист Ричард Хаас (Richard Haas) так закрасил боковую сторону здания в районе Сохо на Манхэттене, что она стала напоминать фасад классического здания с такими деталями, как кошка в окне. Время не пощадило эту роспись, но в те дни все выглядело очень реалистично и обмануло многих.

Такая роспись — тромплей (trompe-l’œil) (обман глаз) — использует тени и затенение для создания третьего измерения на плоской двухмерной поверхности. Мы склонны поддаваться таким иллюзиям, но в то же время стремимся проверить себя, способны ли мы понять этот трюк. Один из простых способов — попытаться рассматривать роспись с разных ракурсов, чтобы понять, одинаково ли она выглядит.

Схожий умственный процесс происходит, когда компьютерная программа отображает некую графику, которая балансирует на границе двух- и трехмерности. Действительно ли это трехмерное изображение? Или просто двухмерное, полученное с помощью изощренного перекрытия и затенения? Мы не в состоянии перемещаться из стороны в сторону, чтобы установить истину, но, возможно, сумеем заставить программу повернуть свою графику и посмотреть, что из этого выйдет.

Вывод программы ThreeRotatingTriangles


Рис. 1. Вывод программы ThreeRotatingTriangles

Включаем данные в эффект

Общая структура предыдущей программы ThreeTriangles сохранена в ThreeRotatingTriangles. Класс, который предоставляет реализацию Direct2D-эффекта, теперь называется RotatingTriangleEffect, а не SimpleTriangleEffect, но по-прежнему реализует интерфейсы ID2D1EffectImpl и ID2D1DrawTransform.

Прежняя SimpleTriangleEffect вообще не была гибкой. В ее код были «зашиты» вершины для отображения трех перекрывающихся треугольников. RotatingTriangleEffect позволяет определять вершины вне класса; кроме того, и реализация эффекта, и вершинный шейдер (vertex shader) были усовершенствованы для поддержки матричных преобразований.

В целом, реализация такого эффекта, как RotatingTriangleEffect, содержит статический метод, который сам регистрируется, вызывая RegisterEffectFromString и сопоставляя себя с идентификатором класса. В программе ThreeRotatingTriangles класс ThreeRotatingTrianglesRenderer вызывает этот статический метод в своем конструкторе для регистрации эффекта.

ThreeRotatingTrianglesRenderer также определяет объект типа ID2D1Effect как закрытое поле:

Чтобы использовать этот эффект, программа должна создать этот объект, ссылаясь на идентификатор класса эффекта в вызове CreateEffect. В классе ThreeRotatingTrianglesRenderer это происходит в методе CreateDeviceDependentResources:

Затем может быть выполнен рендеринг эффекта вызовом метода DrawImage. Вот как ThreeRotatingTrianglesRenderer делает вызов в своем методе Render:

Но между этими двумя вызовами можно сделать куда больше. ID2D1Effect наследует от ID2D1Properties, который имеет методы SetValue и GetValue, позволяющие программе задавать свойства эффекта. Эти свойства могут варьироваться от простых параметров эффекта, выражаемых булевыми флагами, до больших буферов данных. Однако SetValue и GetValue используются нечасто. Они требуют идентификации конкретного свойства по индексу, и поэтому для большей ясности программы предпочтительнее использовать методы SetValueByName и GetValueByName.

Но это же означает, что реализация эффекта, такая как класс RotatingTriangleEffect, должна сама указывать, что может принимать свойства различных типов, и предоставлять методы для установки и получения этих свойств.

Эта информация предоставляется реализацией эффекта, когда она регистрируется с помощью RegisterEffectFromString. Обязательным в этом вызове является некоторый XML, включающий имена и типы различных свойств, которые поддерживаются реализацией эффекта. RotatingTriangleEffect поддерживает четыре свойства со следующими именами и типами данных:

  • VertexData типа blob, т. е. буфер памяти, на который ссылаются байтовым указателем (byte pointer);
  • ModelMatrix типа matrix4x4;
  • ViewMatrix типа matrix4x4;
  • ProjectionMatrix типа matrix4x4.

Имена этих типов данных специфичны для Direct2D-эффектов. Когда эффект, который поддерживает свойства, регистрируется, реализация эффекта должна также предоставить массив объектов D2D1_VALUE_TYPE_BINDING. Каждому из этих объектов в массиве сопоставляется именованное свойство, например VertexData, с двумя методами в реализации эффекта, задающими и возвращающими данные. В случае VertexData эти два метода называются SetVertexData и GetVertexData. (Определяя эти Get-методы, не забудьте использовать ключевое слово const, а иначе вы получите одну из тех странных ошибок шаблонов, причину которых найти крайне трудно.)

Аналогично класс RotatingTriangleEffect определяет методы SetModelMatrix и GetModelMatrix и т. д. Эти методы не вызываются любой прикладной программой — на самом деле они являются закрытыми на уровне RotatingTriangleEffect. Вместо этого программа вызывает методы SetValueByName и GetValueByName объекта ID2D1Effect, которые потом обращаются к Set- и Get-методам в реализации эффекта.

Буфер вершин

Класс ThreeRotatingTrianglesRenderer регистрирует RotatingTrianglesEffect в своем конструкторе и выполняет рендеринг эффекта в своем методе Render. Но между этими двумя вызовами класс рендера вызывает SetValueByName объекта ID2D1Effect, чтобы передать данные в реализацию эффекта.

Первый из четырех свойств эффекта, перечисленных ранее, — VertexData, которое является набором вершин, используемых для определения буфера вершин. В случае Direct2D-эффектов количество элементов в буфере вершин должно быть кратно трем с группированием в треугольники. Программа ThreeRotatingTriangles отображает только три треугольника с тремя вершинами в каждом, но эффект способен обрабатывать более крупный буфер.

Формат буфера вершин, ожидаемый RotatingTriangleEffect, определяется в одной из структур в RotatingTriangleEffect.h:

Это тот же формат, который используется SimpleTriangleEffect, но определенный несколько иначе. На рис. 2 показано, как метод CreateDeviceDependentResources в ThreeRotatingTrianglesRenderer передает массив вершин в эффект после его создания.

Рис. 2. Создание эффекта и присваивание ему буфера вершин

X- и Y-координаты основаны на синусах и косинусах углов с приростом по 40 градусов и с радиусом 1000. Z-координаты варьируются от –1000 для переднего плана до 1000 для заднего. В прежней программе SimpleTriangleEffect я с большой осторожностью задавал Z-координаты между 0 и 1 из-за соглашений, применяемых для проекции трехмерного вывода. Как вы увидите, здесь это не обязательно, потому что преобразования камеры будут применяться к вершинам.

Вызов SetValueByName с именем VertexData заставляет объект ID2D1Effect вызвать метод SetVertexBuffer в RotatingTriangleEffect для передачи данных. Этот метод приводит байтовый указатель обратно к его исходному типу и вызывает CreateVertexBuffer для сохранения информации так, чтобы ее можно было передать вершинному шейдеру.

Применение преобразований

На рис. 3 представлен метод Update в ThreeRotatingTrianglesRenderer, вычисляющий три матричных преобразования и выполняющий три вызова SetValueByName. Этот код слегка упрощен, чтобы удалить проверки на возвращаемые HRESULT-значения, указывающие на ошибки.

Рис. 3. Задание трех матриц преобразований

Как обычно, Update вызывается с частотой обновления кадров на экране. Первая вычисляемая им матрица применяется к вершинам для их поворота вокруг оси Y. Вторая матрица — стандартное преобразование вида из камеры, которое приводит к смещению сцены, чтобы наблюдатель находился в начале координат трехмерной координатной системы и смотрел прямо вдоль оси Z. Третья — стандартная матрица проекции, которая нормализует X- и Y-координаты до значений между –1 и 1, а Z-координаты — до значений между 0 и 1.

Эти матрицы должны применяться к координатам вершин. Так почему бы программе просто не умножить их на массив вершин, определенный в методе CreateDeviceDependentResources, а затем не присвоить новый буфер вершин в RotatingTrianglesEffect?

Разумеется, можно определить динамический буфер вершин, изменяемый с частотой смены кадров на экране, и в некоторых случаях это необходимо. Но, если вершины нужно модифицировать лишь матричными преобразованиями, динамический буфер вершин не столь эффективен, как в случае поддержания того же буфера на протяжении всей программы и применения преобразований на более поздних стадиях конвейера, а именно: в вершинном шейдере, выполняемом GPU.

Это означает, что вершинному шейдеру нужны новые матричные преобразования для каждого кадра, и это вызывает следующий вопрос: как реализация эффекта помещает данные в вершинный шейдер?

Буфер шейдерных констант

Данные переносятся из кода приложения в шейдер через механизм, называемый буфером констант (constant buffer). Пусть его название не обманывает вас — не думайте, будто его содержимое остается постоянным в течение всего выполнения программы. Это совершенно не так. Очень часто буфер констант изменяется на каждом кадре. Однако содержимое буфера констант постоянно для всех вершин в каждом кадре, и формат этого буфера фиксируется при компиляции программы.

Формат буфера констант вершинного шейдера определяется в двух местах: в коде на C++ и в самом вершинном шейдере. В реализации эффекта он выглядит так:

Когда метод Update в рендере вызывает SetValueByName для задания одной из матриц, объект ID2D1Effect вызывает соответствующий Set-метод в классе RotatingTrianglesEffect. Эти методы именуются SetModelMatrix, SetViewMatrix и SetProjectionMatrix, и они просто переносят матрицу в подходящее поле объекта m_vertexShaderConstantBuffer.

Объект ID2D1Effect предполагает, что любой вызов SetValueByName приводит к изменению эффекта, что скорее всего влечет за собой соответствующее изменение в графическом выводе, поэтому в реализации эффекта вызывается метод PrepareForRender. Именно здесь реализация эффекта получает возможность вызвать SetVertexShaderConstantBuffer, чтобы передать содержимое VertexShaderConstantBuffer вершинному шейдеру.

Новый вершинный шейдер

Теперь, наконец, вы можете посмотреть на HLSL-код (High Level Shader Language), выполняющий основную часть работы по вращению вершин этих трех треугольников и их ориентации в трехмерном пространстве. Это новый и улучшенный вершинный шейдер, полностью показанный на рис. 4.

Рис. 4. Вершинный шейдер для эффекта поворачивания треугольников

Обратите внимание на то, как определены структуры: VertexShaderInput в шейдере имеет тот же формат, что и структура PositionColorVertex, определенная в заголовочном файле C++. VertexShaderConstantBuffer имеет тот же формат, что и одноименная структура в коде на C++. Структура VertexShaderOutput аналогична структуре PixelShaderInput в пиксельном шейдере.

В вершинном шейдере, сопоставленном с SimpleTriangleEffect в прошлой статье, буфер ClipSpaceTransforms предоставлялся автоматически для преобразования из пространства сцены (пиксельных координат, используемых для вершин треугольников) в пространство проекции (clip space), которое включает нормализованные X- и Y-координаты в диапазоне от –1 до 1 и Z-координаты в диапазоне от 0 до 1.

Это больше не нужно, поэтому я убрал все без каких-либо последствий. Вместо этого эквивалентную работу выполняет матрица проекции. Как видите, функция main применяет три матрицы к позиции входной вершины и присваивает результат полю clipSpaceOuput в структуре output.

Поле clipSpaceOutput обязательно. Именно так осуществляется управление буфером глубины (depth buffer), а результаты проецируются на поверхность отображения. Однако поле sceneSpaceOutput структуры VertexShaderOutput не требуется. Если вы удалите это поле (а также аналогичное поле из структуры PixelShaderInput пиксельного шейдера), программа будет выполняться, как и раньше.

Развертывание по строкам и развертывание по столбцам

Шейдер выполняет три операции умножения позиций на матрицы:

В математической записи эти умножения выглядят так:

Когда выполняется это умножение, четыре числа, описывающие точку (x, y, z, w), умножаются на четыре числа в первом столбце матрицы (m11, m21, m31, m41), затем четыре результата суммируются, и процесс продолжается со вторым, третьим и четвертым столбцами.

Вектор (x, y, z, w) состоит из четырех чисел, хранящихся в смежных ячейках памяти. Подумайте, что будет в случае оборудования, которое выполняет параллельную обработку перемножения матриц. Полагаете ли вы, что параллельное выполнение этих четырех умножений могло бы оказаться быстрее, если бы числа в каждом столбце тоже хранились в смежной памяти? Это кажется весьма вероятным, что подразумевает оптимальным хранить значения матрицы в памяти в порядке m11, m21, m31, m41, m12, m22 и т. д.

Такой порядок называют развертыванием матрицы по столбцам (column-major order). Блок памяти начинается с первого столбца матрицы, затем идет второй, третий и четвертый. И именно такую организацию матриц в памяти предполагают вершинные шейдеры, выполняя их перемножения.

Однако DirectX обычно хранит матрицы в памяти не так. Структуры XMMATRIX и XMFLOAT4X4 в библиотеке DirectX Math хранят матрицы в порядке развертывания по строкам (row-major order): m11, m12, m13, m14, m21, m22 и т. д. Многим из нас это покажется более естественным порядком, потому что именно в таком порядке мы читаем строки текста: сначала вдоль строки, потом вниз.

Как бы то ни было, но между DirectX- и шейдерным кодом есть несовместимость, и вот почему вы заметите в коде на рис. 3, что каждая матрица обрабатывается вызовом XMMatrixTranspose до отправки в вершинный шейдер. Функция XMMatrixTranspose преобразует матрицы с развертыванием по строкам в матрицы с развертыванием по столбцам (и обратно, если вам это нужно).

Это самое распространенное решение этой проблемы, но оно не единственное. В качестве альтернативы можно указать флаг компиляции шейдера под порядок развертывания по строкам или оставить матрицы не транспонированными и просто переставить порядок развертывания матрицы и вектора при перемножении:

Последний шаг

Затенение трех треугольников определенно демонстрирует, что цвета вершин интерполируются по поверхности каждого треугольника, но результат получается весьма грубым. Конечно, я могу добиться большего в использовании этого мощного инструмента затенения, чтобы имитировать отражение света. И это будет нашим последним шагом в этом погружении в мир Direct2D.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Дугу Эриксону (Doug Erickson).

Модель 2.0 вершинных и пиксельных шейдеров принесла в язык множество значительных усовершенствований, по сравнению с появившимися в DirectX 8.0 версиями 1.0 и 1.1. Поскольку уже вышел DirectX 9.0 и выпущены видеокарты, совместимые с версиями 2.0 вершинных и пиксельных шейдеров, книга сосредотачивается на разработке шейдеров, основанных именно на этой технологии.

Хотя настоятельно рекомендуется использовать видеокарту, поддерживающую вершинные и пиксельные шейдеры версии 2.0, вы можете приобщиться к разработке рассматриваемых в данной книге шейдеров, используя вспомогательный растеризатор (reference rasterizer). Вспомогательный растеризатор эмулирует функциональность вершинных и пиксельных шейдеров с помощью программного обеспечения. Однако, следует помнить, что такая эмуляция осуществляется значительно медленнее, чем визуализация, выполняемая видеокартой!

Я предполагаю, что вы уже обладаете базовыми знаниями трехмерной графики, поэтому позвольте начать с краткого обзора значительных изменений, внесенных во второе поколение шейдеров по сравнению с их предшественниками.

Вершинные шейдеры версий 2.0 и 2.1 включают следующие усовершенствования:

Поддержка целочисленных и логических типов данных и соответствующие инструкции инициализации.

Увеличение числа временных регистров и регистров констант.

Увеличено максимальное число инструкций в программе. Разработчикам предоставляется больше гибкости (минимальные требования стандарта повышены со 128 до 256, но конкретное оборудование может поддерживать и больше).

Множество новых макроинструкций, включая сложные операции, такие как синус/косинус, абсолютная величина и возведение в степень.

Поддержка инструкций, управляющих ходом выполнения программы, таких как ветвления и циклы.

Следующий список перечисляет усовершенствования, внесенные в пиксельные шейдеры 2.0 и 2.x:

Поддержка вычислений с плавающей точкой с 32-разрядной точностью.

Поддержка произвольной адресной выборки и маскирования компонентов в регистрах.

Увеличение числа доступных регистров констант и временных регистров.

Значительное увеличение количества инструкций — с 8 до 64 арифметических и 32 текстурных инструкций. Пиксельные шейдеры 2.x могут иметь больше инструкций чем задано по умолчанию и позволяют аппаратуре выходить за рамки установленных стандартом минимальных требований.

Поддержка целочисленных и логических констант, счетчиков цикла и предикатных регистров.

Поддержка динамического управления ходом выполнения программы, включая циклы и ветвления.

Градиентные инструкции, позволяющие шейдеру определять производые любого входного регистра.

Благодаря этому обширному набору усовершенствований разработчики могут полностью раскрыть свой творческий потенциал и создать потрясающие эффекты. Сейчас, наверное, хорошо сделать краткий обзор архитектуры, чтобы вы лучше поняли, как идут информационные потоки в видеокарте.

Поскольку мы сосредотачиваемся главным образом на использовании Microsoft HLSL, то не будем детально обсуждать инструкции и синтаксис, применяемые при написании шейдеров непосредственно на языке ассемблера. Если вы пишете шейдер на ассемблере, он состоит из набора простых инструкций непосредственно выполняемых процессором видеокарты. Однако это делает программирование более сложным, поскольку вам приходится управлять переменными и регистрами, и некоторые простые концепции транслируются в несколько инструкций ассемблера. С другой стороны, HLSL — это язык высокого уровня, позволяющий вам писать шейдеры более логическим способом, не отвлекаясь на различные надоедливые вопросы микро-управления, возникающие при ручном написании шейдеров на ассемблере.
За дополнительной информацией вы можете обратиться к поставляемой с DirectX 9.0 SDK документации, которая есть на прилагаемом к книге компакт-диске.

Представлять трехмерное окружение в терминах истинного объемного представления слишком дорого. Как я покажу позже, такое представление потребовало бы значительных процессорных мощностей и большого объема памяти. Большинство объектов являются твердыми и непрозрачными, и вы видите только их внешние формы. Когда вы смотрите на яйцо, то видите только его внешнюю сторону и, просто посмотрев на него, не можете сказать есть ли у него что-нибудь внутри. Поэтому трехмерная графика упрощается, беря на вооружение эту идею и представляя только внешнюю оболочку объекта, вместо того, чтобы пытаться представить его в целом. Думайте об этом, как о создании фигурки из папье-маше, для которой сперва надо создать задающий форму проволочный каркас.

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

Я должен пояснить использование мной термина полигон (polygon). Хотя определение полигона подразумевает фигуру с произвольным количеством сторон, в трехмерной компьютерной графике принято использовать простейший вариант полигона — треугольник. В этой книге термины полигон и треугольник означают одно и то же.

При визуализации трехмерной графики информация передается графическому оборудованию через API визуализации, такой как Direct3D или OpenGL. Как только эта информация получена, видеокарта выполняет программу вершинного шейдера для каждой вершины вашей сетки. На рис. 1.1 показана определяемая спецификацией функциональная диаграмма реализации вершинных шейдеров 2.0.


Рис. 1.1. Функциональная диаграмма архитектуры оборудования для вершинных шейдеров

Рис. 1.1. Функциональная диаграмма архитектуры оборудования для вершинных шейдеров

Как видно на рис. 1.1, вершины поступают потоком, который предоставляется разработчиком через API трехмерной визуализации, такой как Direct3D или OpenGL. Поток содержит всю информацию, необходимую для правильной обработки геометрии в процессе визуализации, такую как координаты местоположения, цвета и координаты текстуры. Поступившая информация помещается в соответствующие входные регистры от v0 до v15 и может использоваться программой вершинного шейдера. Это означает, что каждая вершина обрабатывается индивидуально и ее описание может содержать до 16 блоков информации, передаваемых через входные регистры. Программа вершинного шейдера имеет доступ ко многим другим регистрам, необходимым ей для выполнения своей задачи, состоящей в получении входных данных, их обработке и преобразовании в форму, которая используется пиксельным шейдером для выполнения итогового затенения и визуализации.

Регистры констант доступны только для чтения, должны быть инициализированы заранее и используются для хранения статических данных шейдера. Согласно стандарту вершинных шейдеров 2.0, регистры констант являются векторами и могут хранить числа с плавающей точкой, целые числа или логические значения. Обратите внимание, что регистры вершинного шейдера представляют собой четырехкомпонентные векторы, причем их отдельные компоненты могут обрабатываться параллельно или в индивидуальном порядке с помощью адресной выборки и маскирования.

С левой стороны рис. 1.1 расположены временные регистры, предназначенные для хранения генерируемых вершинным шейдером промежуточных результатов. Очевидно, что согласно своему назначению, эти регистры доступны и для чтения и для записи. Обратите внимание на регистры с именами a0 и aL. Это счетчики для индексной адресации и управления циклами. Это особый случай, а все остальные регистры могут использоваться внутри вашего шейдера для любых нужных вам целей. Также следует помнить, что поскольку HLSL — это язык высокого уровня, вам не надо заботиться о распределении регистров. Это происходит независимо от вас во время компиляции шейдера в его исполняемую форму.

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

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

Интерполяцией (interpolation) называется процесс в ходе которого на основании данных вершин полигона формируются соответствующие данные для каждого его пикселя. Вообразите линию от одной вершины к другой. Если одна вершина белая, а другая черная, то для точек в середине линии вам требуется промежуточный цвет, серый. Интерполяция осуществляет тот же процесс, но для всего треугольника. Процесс отсечения, в свою очередь, проверяет какие части треугольника видны на экране. Он позволяет не тратить впустую драгоценное время, занимаясь затенением невидимых частей полигонов. Благодаря этому сокращается общий объем работы, которую должна выполнить видеокарта. После того, как растеризатор определит параметры пикселя, для каждого отображаемого на экране пикселя вызывается пиксельный шейдер. На рис. 1.2 приведена функциональная диаграмма архитектуры пиксельных шейдеров.


Рис. 1.2. Функциональная диаграмма архитектуры оборудования для пиксельных шейдеров

Рис. 1.2. Функциональная диаграмма архитектуры оборудования для пиксельных шейдеров

Как видно из диаграммы на рис. 1.2, аппаратура передает вычисленные пиксели через входные регистры цвета и регистры текстуры. Эти значения вычисляются путем перспективной интерполяции значений, сформированных вершинным шейдером. Регистры v0 и v1 предназначаются для интерполяции рассеиваемой и отражаемой компонент освещения. Регистры с t0 по tN содержат интерполированные координаты просмотра текстуры. Можно заметить, что на рис. 1.2 к регистрам текстуры идут двунаправленные стрелки. Это объясняется конструкторским решением, которое позволяет выполнять запись в регистры текстуры и использовать их в качестве временных регистров. И, наконец, регистры с s0 по sN указывают на текстуры из которых пиксельный шейдер будет осуществлять выборку во время обработки пикселя. Хотя назначение этих регистров явно указано, они могут применяться для передачи от вершинного шейдера к пиксельному любой информации.

Регистры констант с c0 по cN доступны только для чтения и разработчик должен заранее записать в них значения, которые будут использоваться шейдером. И, наконец, временные регистры с r0 по rN, доступны для чтения и для записи и используются для хранения промежуточных результатов в ходе обработки пикселя. Когда вы используете HLSL распределение регистров выполняется автоматически без участия пользователя.

Выходные регистры, такие как oC0 и oDepth, используются аппаратурой при визуализации итогового пикселя. Они определяют итоговый цвет, затуманивание и глубину, а ваша задача состоит в написании пиксельного шейдера, вычисляющего эти значения. После того, как данные пикселя прошли через пиксельный шейдер, выходная информация используется для смешивания с содержимым вторичного буфера, которое затем будет показано на экране.

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

Большинство регистров в пиксельных шейдерах, за исключением нескольких адресных регистров, являются векторами, состоящими из четырех значений с плавающей точкой. Преимущество векторной архитектуры состоит в возможности одновременной обработки нескольких значений. По умолчанию все значения с плавающей точкой обрабатываются как 32-разрядные. Однако спецификация пиксельных шейдеров позволяет работать и с 16-разрядными значениями с плавающей точкой, для чего предусмотрены специальные инструкции. В некоторых аппаратных реализациях вычисления с 16-разрядными значениями выполняются значительно быстрее.

Следует отметить еще, что для векторов в вершинных и пиксельных шейдерах предусмотрены операции адресной выборки и маскирования, позволяющие работать с отдельными компонентами. Операция адресной выборки (swizzling) позволяет получить произвольную комбинацию компонент вектора. Дополнительную информацию об адресной выборке и маскировании вы найдете в справочнике по HLSL из приложения А.

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

Читайте также: