Прорыв барьера Z-порядка с помощью Direct2D-эффектов

OSzone.net » Microsoft » Разработка приложений » Другое » Прорыв барьера Z-порядка с помощью Direct2D-эффектов
Автор: Чарльз Петцольд
Иcточник: msdn.microsoft.com
Опубликована: 20.10.2014
В детстве мы начинаем рисовать еще до того, как учимся чтению и письму, и, несомненно, усваиваем крупицы того опыта. Мы обнаруживаем, что процесс рисования отражается в нанесении слоев краски на холст. То, что было нарисовано ранее, может быть отчасти перекрыто тем, что нарисовано позже.

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

*
Рис. 1. Три перекрытых треугольника

Три треугольника на рис. 1 также могли бы быть кусочками разноцветной бумаги, сложенной в такую пачку. Если бы вам потребовалось добавлять в нее все больше и больше треугольников, она превратилась бы в высокую стопку и только двухмерной поверхности стало бы мало — понадобилось бы третье измерение.

Даже в двухмерной графике существует рудиментарная концепция оси Z — виртуальное пространство, перпендикулярное двухмерному экрану или холсту. Слои плоских двухмерных объектов управляются так называемым Z-порядком фигур. Например, в средах на основе XAML подключаемое свойство Canvas.ZIndex определяет, какие элементы кажутся располагающимися поверх других, но на самом деле оно просто контролирует порядок, в котором визуализируются эти элементы на экране.

Проблема в следующем: в двухмерной графике Z-индекс всегда применяется к фигуре в целом. Вы не можете использовать этот тип Z-упорядочения для рисования трех фигур так, как показано на рис. 2, где первая располагается над второй, вторая — над третьей, а третья — над первой.

*
Рис. 2. Взаимно перекрывающиеся треугольники

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

Эффекты и GPU

Рендерингу содержимого рис. 2 можно колоссально помочь, одолжив некоторые концепции из мира трехмерной графики.

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

С точки зрения вычислений, это выглядит весьма накладно — не только из-за самого сравнения, но и за расчета Z-координат каждого пикселя каждой графической фигуры; и это действительно так. Вот почему это идеальная работа для передачи современному GPU с его мощными средствами параллельных вычислений.

Вычисление Z-координат каждого пикселя фигуры концептуально облегчается, если фигура является треугольником; помните, что любой многоугольник можно разложить на треугольники. Все, что нужно, — присвоить каждой из трех вершин треугольника трехмерную координатную точку, и тогда любую точку в границах треугольника можно вычислить как взвешенное среднее значение трех вершинных координат. (Данный процесс включает применение барицентрических координат — это не единственная концепция, используемая в компьютерной графике и разработанная немецким математиком Августом Фердинандом Мёбиусом.)

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

*
Рис. 3. Вывод программы ThreeTriangles

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

Изображение на рис. 3 было создано программой ThreeTriangles (она имеется в сопутствующем этой статье коде), которая выполняется в Windows 8.1 и Windows Phone 8.1. (Решение было создано в Visual Studio 2013 Update 2 с использованием нового шаблона Universal App, который позволяет сделать общим большой объем кода для Windows 8.1 и Windows Phone 8.1.)

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

При написании пользовательского эффекта для Direct2D вы получаете привилегию, обычно ограничиваемую программистам трехмерной графики: вы можете писать код, выполняемый в GPU. Этот код принимает форму небольших программ, называемых шейдерами, которые вы пишете на высокоуровневом шейдерном языке HLSL (high-level shading language), напоминающем C. Эти шейдеры компилируются Visual Studio в файлы объекта шейдера (.cso) в процессе обычной сборки проекта, а затем выполняются в GPU при запуске программы.

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

Для использования Direct2D-эффектами доступны три типа шейдеров.

Шейдеры, применяемые для Direct2D-эффектов, предъявляют несколько иные требования, чем шейдеры, связанные с Direct3D-программированием, но многие концепции одинаковы.

Встроенные и пользовательские эффекты

Direct2D включает около 40 предопределенных встроенных эффектов, которые выполняют различные манипуляции над изображениями в битовых картах, в частности размытие (blur) или повышение резкости (sharpen), а также различные виды операций с цветами.

Каждый из встроенных эффектов определяется по идентификатору класса, используемому для создания эффекта этого типа. Например, вы хотите применить эффект цветовой матрицы, которая позволяет указывать преобразование для изменения цветов в битовой карте. По-видимому, вы объявите объект типа ID2D1Effect как закрытое поле в своем классе рендеринга:

Microsoft::WRL::ComPtr<ID2D1Effect> m_colorMatrixEffect;

В методе CreateDeviceDependentResources вы можете создать этот эффект ссылкой на документированный идентификатор класса:

d2dContext->CreateEffect(
  CLSID_D2D1ColorMatrix, &m_colorMatrixEffect);

К этому моменту вы можете вызвать SetInput объекта эффекта для задания битовой карты и SetValue для указания матрицы преобразования. Рендеринг этой битовой карты со смещением цветов осуществляется вызовом:

d2dContext->DrawImage(m_colorMatrixEffect.Get());

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

Если вы заинтересованы в написании собственных эффектов, обязательно посмотрите просто бесценное решение Visual Studio для Windows 8.1, называемое Direct2D Custom Image Effects Sample. Оно включает три раздельных проекта для демонстрации трех типов шейдеров, доступных для Direct2D-эффектов. Все три программы требуют входную битовую карту.

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

Также простительным будет предположение, будто Direct2D-эффекты включают только один тип шейдера. Конечно, встроенные эффекты включают либо вершинный шейдер, либо пиксельный, но не оба сразу. Однако программа ThreeTriangles отличается и в этом отношении: в ней определен пользовательский эффект, где задействован как вершинный шейдер, так и пиксельный.

Регистрация, создание, рисование

Поскольку Direct2D-эффекты рассчитаны на то, что они заранее регистрируются и создаются по идентификатору класса, пользовательский эффект должен обеспечивать то же самое. Пользовательский эффект в программе ThreeTriangles — это класс SimpleTriangleEffect, который определяет статический метод для регистрации класса. Этот метод вызывается конструктором класса ThreeTrianglesRenderer, но эффект можно зарегистрировать где угодно в программе:

SimpleTriangleEffect::RegisterEffectAsync(d2dFactory)

Метод регистрации асинхронный, так как ему нужно загружать скомпилированные файлы шейдеров, и единственный метод, предоставляемый для этой цели в классе DirectXHelper, — ReadDataAsync.

Как и при использовании встроенного эффекта, класс ThreeTrianglesRenderer объявляет объект ID2D1Effect как закрытое поле в своем заголовочном файле:

Microsoft::WRL::ComPtr<ID2D1Effect> m_simpleTriangleEffect;

Метод CreateDeviceDependentResources создает пользовательский эффект так же, как и встроенный:

d2dContext->CreateEffect(
   CLSID_SimpleTriangleEffect, &m_simpleTriangleEffect)

Ранее при регистрации пользовательского эффекта этот идентификатор класса был сопоставлен с эффектом.

SimpleTriangleEffect ничего не принимает. (Отчасти это и делает его «простым»!) Рендеринг этого эффекта выполняется так же, как и встроенного:

d2dContext->DrawImage(m_simpleTriangleEffect.Get());

Простое использование этого эффекта может предполагать некоторую сложность внутри самого класса эффекта. Такой пользовательский эффект, как SimpleTriangleEffect, должен реализовать интерфейс ID2D1EffectImpl. Эффект может состоять из нескольких этапов, называемых преобразованиями, и каждый из них обычно представлен реализацией ID2D1DrawTransform. Если для обоих интерфейсов применяется единственный класс (как в случае с SimpleTriangleEffect), то этот класс должен реализовать IUnknown (три метода), ID2D1EffectImpl (три метода), ID2D1TransformNode (один метод), ID2D1Transform (три метода) и ID2D1DrawTransform (один метод).

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

От буфера вершин…

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

Первое представление этих данных вершин показано на рис. 4. Это простая структура с именем Vertex, которая включает трехмерную координатную точку и RGB-цвет. Массив этих структур определяет три треугольника, отображаемых программой. (Этот массив «зашит» в обязательный метод Initialize класса SimpleTriangleEffect; в реальной программе класс эффекта должен принимать массив вершин.)

Рис. 4. Определение вершины в SimpleTriangleEffect

// Определяем Vertex для простой инициализации
struct Vertex
{
  float x;
  float y;
  float z;
  float r;
  float g;
  float b;
};
// Каждый треугольник имеет три точки и три цвета
static Vertex vertices [] =
{
  // Треугольник 1
  {    0, -1000, 0.0f, 1, 0, 0 },
  {  985,  -174, 0.5f, 0, 1, 0 },
  {  342,   940, 1.0f, 0, 0, 1 },
  // Треугольник 2
  {  866,   500, 0.0f, 1, 0, 0 },
  { -342,   940, 0.5f, 0, 1, 0 },
  { -985,  -174, 1.0f, 0, 0, 1 },
  // Треугольник 3
  { -866,   500, 0.0f, 1, 0, 0 },
  { -643,  -766, 0.5f, 0, 1, 0 },
  {  643,  -766, 1.0f, 0, 0, 1 }
};
// Определяем структуру для эффекта
static const D2D1_INPUT_ELEMENT_DESC vertexLayout [] =
{
  { "MESH_POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0 },
  { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12 },
};

Значения x и y основаны на синусах и косинусах углов с шагом 40 градусов и радиусом 1000. Но заметьте, что z-координаты присвоены значения от 0 до 1, поэтому красные вершины имеют значение z, равное 1, зеленые — 0.5, а синие — 0. Подробнее об этом чуть позже.

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

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

К вершинному шейдеру…

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

Рис. 5. Файл SimpleTriangleEffectVertexShader.hlsl

// Ввод данных для каждой вершины в вершинный шейдер
struct VertexShaderInput
{
  float3 position : MESH_POSITION;
  float3 color : COLOR0;
};
// Вывод данных для каждой вершины из вершинного шейдера
struct VertexShaderOutput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Информация, предоставляемая для вершинных шейдеров Direct2D
cbuffer ClipSpaceTransforms : register(b0)
{
  float2x1 sceneToOutputX;
  float2x1 sceneToOutputY;
}
// Вызывается для каждой вершины
VertexShaderOutput main(VertexShaderInput input)
{
  // Структура вывода
  VertexShaderOutput output;
  // Дописываем значение w, равное 1, в координаты трехмерной входной точки
  output.sceneSpaceOutput = float4(input.position.xyz, 1);
  // Стандартные вычисления
  output.clipSpaceOutput.x =
    output.sceneSpaceOutput.x * sceneToOutputX[0] +
    output.sceneSpaceOutput.w * sceneToOutputX[1];
  output.clipSpaceOutput.y =
    output.sceneSpaceOutput.y * sceneToOutputY[0] +
    output.sceneSpaceOutput.w * sceneToOutputY[1];
  output.clipSpaceOutput.z = output.sceneSpaceOutput.z;
  output.clipSpaceOutput.w = output.sceneSpaceOutput.w;
  // Переносим цвет
  output.color = input.color;
  return output;
}

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

Структура VertexShaderInput является входной для main, и здесь то же самое, что и в буфере вершин, но с HLSL-типами данных для позиции в трехмерном пространстве и RGB-цветом.

Структура VertexShaderOutput определяет вывод от main. Первые два поля необходимы для Direct2D-эффектов. (Третье обязательное поле присутствовало бы, если бы эффект включал входную битовую карту.) Поле, названное мной sceneSpaceOutput, основано на входных координатах. Некоторые эффекты изменяют эти координаты; данный эффект ничего такого не делает и просто преобразует трехмерные входные координаты в четырехмерные гомогенные со значением w, равным 1:

output.sceneSpaceOutput = float4(input.position.xyz, 1);

Вершинный шейдер output также включает необязательное поле color, которое просто задает входной цвет:

output.color = input.color;

Обязательное поле в output, названное мной clipSpaceOutput, описывает координаты каждой вершины в терминах нормализованных координат, применяемых в трехмерной графике. Эти координаты — то же самое, что и координаты, генерируемые преобразованиями проекции камеры, описанными мной в предыдущей статье. В этих координатах clipSpaceOutput значения x варьируются от –1 с левой стороны экране до 1 на правой, значения y — от –1 снизу до 1 вверху, а значения z — от 0 для координат, ближайших к наблюдателю, до 1 для координат, наиболее удаленных от него. Как и предполагает имя поля, эти нормализованные координаты используются для отсечения трехмерной сцены на экране.

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

Однако преобразования предоставляются только для координат x и y, и вот почему я определил координаты z в исходном буфере вершин со значениями между 0 и 1. Другой подход — использовать преобразование проекции камеры в вершинном шейдере (как я продемонстрирую в будущей статье).

Эти значения z также автоматически используются в буфере глубин, чтобы пиксели с меньшими координатами z заслоняли пиксели с более высокими значениями z. Но это происходит, только если метод SetDrawInfo в классе эффекта вызывает SetVertexProcessing с флагом D2D1_VERTEX_OPTIONS_USE_DEPTH_BUFFER. (Любопытно, что в этом случае в окне Output в Visual Studio появляются ошибки COM при выполнении программы, но то же самое происходит и с кодом образца Direct2D-эффекта от самой Microsoft.)

И к пиксельному шейдеру

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

Вывод от вершинного шейдера имеет тот же формат, что и ввод в пиксельный шейдер. Как видно из пиксельного шейдера на рис. 6, структура PixelShaderInput идентична структуре VertexShaderOutput в вершинном шейдере.

Рис. 6. Файл SimpleTriangleEffectPixelShader.hlsl

// Ввод данных для каждого пикселя в пиксельный шейдер
struct PixelShaderInput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Вызываем для каждого пикселя
float4 main(PixelShaderInput input) : SV_TARGET
{
  // Просто возвращаем цвет с коэффициентом прозрачности, равным 1
  return float4(input.color, 1);
}

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

Вот интересная вариация для пиксельного шейдера: координаты z поля sceneSpaceOutput варьируются от 0 до 1, поэтому становится возможным визуализировать глубину каждого треугольника, используя эти координаты для конструирования серого оттенка и возвращать его из метода main:

float z = input.sceneSpaceOutput.z;
return float4(z, z, z, 1);

Усовершенствования?

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

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


Ссылка: http://www.oszone.net/25388/Direct2D