Что такое вершинный шейдер

Что такое шейдеры? Просто о сложном для начинающих

Что такое вершинный шейдер. Смотреть фото Что такое вершинный шейдер. Смотреть картинку Что такое вершинный шейдер. Картинка про Что такое вершинный шейдер. Фото Что такое вершинный шейдер

«Что такое шейдеры?» – очень частый вопрос любопытных игроков и начинающих игровых разработчиков. В этой статье доходчиво и понятно об этих страшных шейдерах расскажу.

Двигателем прогресса в сторону фотореалистичности картинки в компьютерной графике я считаю именно компьютерные игры, поэтому давайте именно в разрезе видео-игр и поговорим о том, что такое “шейдеры”.

До того, как появились первые графические ускорители, всю работу по отрисовке кадров видеоигры выполнял бедняга центральный процессор.

Так вот, центральный процессор (CPU – Central Processing Unit) слишком умный парень, чтобы заставлять его заниматься такой рутиной. Вместо этого логично выделить какой-то аппаратный модуль, который разгрузит CPU, чтобы тот смог заниматься более важным интеллектуальным трудом.

Таким аппаратным модулем стал – графический ускоритель или видеокарта (GPU – Graphics Processing Unit). Теперь CPU подготавливает данные и загружает рутинной работой коллегу. Учитывая, что GPU сейчас это не просто один коллега, это толпа миньонов-ядер, то он с такой работой справляется на раз.

Но мы пока не получили ответа на главный вопрос: Что такое шейдеры? Подождите, я подвожу к этому.

Хорошая, интересная и близкая к фото-реализму графика, требовала от разработчиков видеокарт реализовывать многие алгоритмы на аппаратном уровне. Тени, свет, блики и так далее. Такой подход – с реализацией алгоритмов аппаратно называется “Фиксированный пайплайн или конвейер” и там где требуется качественная графика он теперь не встречается. Его место занял “Программируемый пайплайн”.

Запросы игроков “давайте, завозите хороший графоний! удивляйте!”, толкали разработчиков игр (и производителей видеокарт соответственно) все к более и более сложным алгоритмам. Пока в какой-то момент зашитых аппаратных алгоритмов им стало слишком мало.

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

И вот, наконец, мы дошли до ответа на наш главный вопрос.

“Что такое шейдеры?”

Ше́йдер (англ. shader — затеняющая программа) — это программа для видеокарточки, которая используется в трёхмерной графике для определения окончательных параметров объекта или изображения, может включать в себя описание поглощения и рассеяния света, наложения текстуры, отражения и преломление, затенение, смещение поверхности и множество других параметров.

Графический пайплайн

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

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

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

Впервые полноценная поддержка шейдеров появилась в видеокартах серии GeForce 3, но зачатки были реализованы ещё в GeForce256 (в виде Register Combiners).

Виды шейдеров

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

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

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

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

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

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

Пиксельными шейдерами выполняют наложение текстур, освещение, и разные текстурные эффекты, такие как отражение, преломление, туман, Bump Mapping и пр. Пиксельные шейдеры также используются для пост-эффектов.

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

На чем пишут шейдеры?

Изначально шейдеры можно было писать на assembler-like языке, но позже появились шейдерные языки высокого уровня, похожие на язык С, такие как: Cg, GLSL и HLSL.

Такие языки намного проще чем C, ведь задачи решаемые с их помощью, гораздо проще. Система типов в таких языках отражает нужды программистов графики. Поэтому они предоставляют программисту специальные типы данных: матрицы, семплеры, векторы и тп.

RenderMan

Все что мы обсудили выше относится к realtime графике. Но существуют non-realtime графика. В чем разница – realtime – реальное время, тоесть здесь и сейчас – давать 60 кадров в секунду в игре, это процесс реального времени. А вот рендерить комплексный кадр для ультрасовременной анимации по несколько минут это non-realtime. Суть во времени.

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

Супер-реалистичная графика в Sand piper

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

*Видео могут забанить на Youtube, если оно не открывается, погуглите pixar sandpiper – короткометражный мультфильм про храброго песочника очень милый и пушистый. Умилит и продемонстрирует насколько крутой может быть компьютерная графика.

Так вот это RenderMan от фирмы Pixar. Он стал первым языком программирования шейдеров. API RenderMan является фактическим стандартом для профессионального рендеринга, используется во всех работах студии Pixar и не только их.

Полезная информация

Теперь Вы знаете что такое шейдеры, но помимо шейдеров, есть другие очень интересные темы в разработке игр и компьютерной графике, которые наверняка Вас заинтересуют:

Если остались вопросы

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

Источник

Shader — это не магия. Написание шейдеров в Unity. Вертексные шейдеры

Всем привет! Меня зовут Дядиченко Григорий, и я основатель и 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

CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows

// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0

struct Input
<
float2 uv_MainTex;
>;

half _Glossiness;
half _Metallic;
fixed4 _Color;

// Add instancing support for this shader. You need to check ‘Enable Instancing’ on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)

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»
>

Как в ней что работает и в целом подробно мы её разберём в статье после базовой практики, плюс частично будем разбираться по ходу реализации шейдеров. Пока пусть часть вещей останутся, как данность. Если коротко, тут нет никакой магии (в плане того, как подцепляются параметры и прочее) Просто по определённым ключевым словам юнити генерирует код за вас, чтобы не писать его с нуля. Поэтому этот процесс достаточно не очевиден. Подробнее про surface шейдер и его свойства в Unity можно прочитать тут. docs.unity3d.com/Manual/SL-SurfaceShaders.html

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

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

#pragma surface surf Standard fullforwardshadows

struct Input
<
float4 color : COLOR;
>;

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

Что такое вершинный шейдер. Смотреть фото Что такое вершинный шейдер. Смотреть картинку Что такое вершинный шейдер. Картинка про Что такое вершинный шейдер. Фото Что такое вершинный шейдер

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

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

Для этого добавим в строчку #pragma surface surf Standard fullforwardshadows модификатор vertex:vert. Если в качестве параметра функции мы передаём inout appdata_full v то в сущности эта функция является модификатором вертексов. По своей сути — это часть вертексного шейдера, который создан кодогенерацией юнити, которая осуществляет предварительную обработку вертексов. Так же в блок Properties добавим поле _Amount принимающее значения от 0 до 1. Для использования поля _Amount в шейдере, нам так же нужно определить его там. В функции мы будем просто сдвигать на нормаль в зависимости от _Amount, где 0 — это стандартная позиция вертекса (нулевой сдвиг), а 1 — сдвиг ровно на нормаль.

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

#pragma surface surf Standard fullforwardshadows vertex:vert

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

#pragma surface surf Standard fullforwardshadows vertex:vert

struct Input
<
float4 color : COLOR;
>;

fixed4 _Color;
float _Amplitude;

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

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

На тему одного эффекта деформации у меня уже написана целая статья с подробным разбором математики процесса и логики мышления при разработке подобного эффекта habr.com/ru/post/435828 Это и будет нашей совой.

Все шейдеры в статье написаны на hlsl. У этого языка на самом деле есть своя объёмная документация, о которой многие забывают и удивляются откуда берётся половина зашитых функций, хотя они определены в HLSL docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-intrinsic-functions

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

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

Что такое вершинный шейдер. Смотреть фото Что такое вершинный шейдер. Смотреть картинку Что такое вершинный шейдер. Картинка про Что такое вершинный шейдер. Фото Что такое вершинный шейдер

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

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

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

Такие директивы компиляции как
#pragma vertex vert
#pragma fragment frag
определяют какие функции шейдера компилировать в качестве вершинного и фрагментного шейдера соответственно.

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

Shader «Custom/SimpleNormalVisualization»
<
Properties
<
>
SubShader
<
Pass
<
CGPROGRAM

#pragma vertex vert
#pragma fragment frag

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), которая переводит вертексы объекта в позицию связанную с камерой. Без неё объект, при попадании в зону видимости (фруструм) камеры, будет рисоваться в координатах экрана вне зависимости от положения трансформа. Так как первоначально позиции вертексов представлены в координатах объекта. Просто значения относительно его пивота.

Этот блок.
struct v2f <
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
>;
Это определение структуры, которая будет обрабатываться в вертексной части и передаваться в фрагментную. В данном случае в ней определено, чтобы из меша забиралось два параметра — позиция вершины и цвет вершины. Подробнее про то, какие данные возможно прокидывать в юнити, можно прочитать по этой ссылке docs.unity3d.com/Manual/SL-VertexProgramInputs.html

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

Заключение

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

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

Источник

Вершинные шейдеры и преобразования

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

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

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

Изображение на рис. 1 во многом похоже на то, что рисовала программа ThreeTriangles в предыдущей статье из этой рубрики (msdn.microsoft.com/magazine/dn768854). Но в программе ThreeRotatingTriangles, которая рассматривается в этой статье и исходный код которой прилагается, действительно выполняется поворачивание этой сборки из трех треугольников. Визуальный эффект довольно интересен, и, когда три треугольника движутся относительно друг друга, программа подтверждает, что в ней и вправду происходит какая-то обработка трехмерной графики. Однако, посмотрев ее код, вы обнаружите, что программа по-прежнему написана исключительно на основе Direct2D, а не Direct3D, и просто использует мощь Direct2D-эффектов.

Что такое вершинный шейдер. Смотреть фото Что такое вершинный шейдер. Смотреть картинку Что такое вершинный шейдер. Картинка про Что такое вершинный шейдер. Фото Что такое вершинный шейдер
Рис. 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.

Учтите, что эти методы SetValueByName и GetValueByName являются частью объекта ID2D1Effect, возвращаемого вызовом CreateEffect. Объект ID2D1Effect передает значения этих свойств написанному вами классу реализации эффекта, т. е. классу, реализующему интерфейсы ID2D1EffectImpl и ID2D1DrawTransform. Это кажется окольным путем, но делается так, чтобы зарегистрированные эффекты можно было использовать без обращения к классам, реализующим эффекты.

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

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

Имена этих типов данных специфичны для 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.

Чарльз Петцольд (Charles Petzold)давний «пишущий» редактор MSDN Magazine и автор книги «Programming Windows, 6th edition» (O’Reilly Media, 2013) о написании приложений для Windows 8. Его веб-сайт находится по адресу charlespetzold.com.

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *