Опубликован: 28.04.2009 | Доступ: свободный | Студентов: 1838 / 107 | Оценка: 4.36 / 4.40 | Длительность: 16:40:00
Специальности: Программист
Лекция 5:

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

< Лекция 4 || Лекция 5: 123456789101112
5.5. Шейдерный фейерверк

Итак, теперь вы уже знакомы с основами языков HLSL и Vertex Shader 1.1. Настало время опробовать полученные знания в более-менее сложном проекте. Ведь как гласит народная мудрость, теория без практики бесполезна, а практика без теории может быть даже вредна.

В качестве отправной точки для приложения мы возьмем хранитель экрана из 4-й главы и поставим перед собой "сверхзадачу ": реализовать функциональность данного хранителя экрана, используя исключительно вершинные шейдеры. Иными словами, центральный процессор должен будет отсылать на видеокарту только команды "нарисовать диск " и "нарисовать искры ", а всю остальную работу по вращению диска и моделированию полета искр должен выполнять вершинный процессор GPU. Это весьма объемная и нетривиальная задача, поэтому мы разобьем ее на ряд более простых этапов, по мере реализации которых мы продолжим знакомиться с новыми возможностями HLSL и языка Vertex Shader 1.1.

5.5.1. Моделирование вращения диска

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

// Определяем интервал времени, прошедший с момента
 визуализации предыдущего кадра float delta =
 (float)(currentTime - lastTime);
// Корректируем угол поворота диска diskAngle
 += diskSpeed * delta;
// Рассчитываем новые координаты вершин диска
diskVertices[0] = new VertexPositionColor
(new Vector3(0.0f, 0.0f, 0.0f),
XnaGraphics.Color.LightGray);
for (int i = 0; i <= slices; i++) 
{
float angle = (float)i / (float)slices * 2.0f * (float)Math.PI;
float x = diskRadius * (float)Math.Sin(diskAngle + angle);
float y = diskRadius * (float)Math.Cos(diskAngle + angle);
byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3)));
byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2)));
diskVertices[i + 1] = new VertexPositionColor
(new Vector3(x, y, 0.0f), new XnaGraphics.Color(red, 
green, 128)); 
};
Листинг 5.8.

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

  • Результат вычисления переменной angle для конкретной вершины всегда является константой. Поэтому значение переменной angle разумнее всего один раз рассчитать для каждой вершины и затем передавать в шейдер в качестве параметра.
  • Цвет вершины является постоянной величиной, поэтому его тоже лучше один раз рассчитать заранее и передавать в вершинный шейдер в качестве параметра.
  • Так как центральная вершина диска всегда остается неизменной, она рассчитывается независимо от остальных вершин. Но язык Vertex Shader 1.1 не поддерживает ветвления, поэтому нам придется рассчитывать параметры центральной вершины наравне с остальными, считая что она удалена от центра диска на нуль единиц.

На следующем этапе мы должны определиться с информацией, передаваемой в вершинный шейдер и составить небольшую табличку наподобие таблицы 5.4. Информацию общую для всех вершин логично предавать через параметры шейдера, отображаемые на константные регистры. А вот информацию об удалении вершины от начала координат и угле ее локального поворота мы будем передавать через координаты вершины. Возможно, это вам покажется очень странным, но нечего противоестественного в этом нет - в разделе 5.3.1 говорилось, что атрибуты вершинны (координаты, цвет и т.п.) просто отображаются на входные регистры виртуального процессора v0, v1 … v15, а уж как трактовать информацию, хранимую в этих регистрах - это уже дело исключительно вершинного шейдера.

Таблица 5.4. Входные параметры вершинного шейдера, визуализирующего диск
Описание параметра Аналогичная переменная из листинга 5.8 Общий для всех вершин Место хранения
Угол поворота всех вершин, меняющийся с течением времени diskAngle Да Входной параметр angle
Расстояние текущей вершины от центра диска diskRadius Нет Координата вершины X
Локальный угол поворота текущей вершины Angle Нет Координата вершины Y
Цвет текущей вершины red/green Нет Цвет вершины

Прототип вершинного шейдера

В принципе, теперь можно приступать к написанию вершинного шейдера, но мы с этим делом немного повременим. Дело в том, что вершинные шейдеры достаточно капризны и трудоемки в плане отладки, а подобные сложные шейдеры мы еще никогда не писали. Поэтому для начала мы создадим на C# класс DiskEffect, эмулирующий функциональность нашего будущего вершинного шейдера (листинг 5.9). Это позволит нам, если что-то пойдет не так, легко поставить точку останова в коде шейдера и проверить корректность входных параметров или выполнить трассировку "шейдера " по шагам с просмотром состояния переменных15В DirectX SDK имеется утилита PIX for Windows, позволяющая выполнять трассировку ассемблерного кода шейдера. Использование данной утилиты будет рассмотрено в следующей главе .

// Эмулятор эффекта вращения диска
static class DiskEffect
{
// Параметр эффекта
public static float angle;
// Вершинный шейдер
// input - входная информация о вершине
// output - выходная информация о вершине
public static void VertexShader(VertexPositionColor[]
 input, VertexPositionColor[] 
output)
{
// Перебираем все вершины (в коде реального вершинного
 шейдера цикла не будет, ведь он 
// автоматически будет вызываться для каждой вершины for 
(int i = 0; i < input.Length; i++) 
{ 
// Вычисляем итоговый угол поворота вершины. Информация
 об углах поворота вершины берется из 
// параметра angle и координаты Y
float a = input[i].Position.Y + angle;
 // Вычисляем координаты вершины. Расстояние вершины от
  центра диска берется из координаты 
X output[i].Position.X = input[i].Position.X * (float)Math.Sin(a); 
output[i].Position.Y = input[i].Position.X * (float)
Math.Cos(a); output[i].Position.Z = 0; 
// Цвет вершины проходит через вершинный шейдер без 
изменений output[i].Color = input[i].Color; 
} 
       } 
}
Листинг 5.9.

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

public partial class MainForm : Form 
{
// Обычный эффект для визуализации объектов. Пропускает 
через себя информацию о вершинах без 
// изменений. Вращение диска осуществляется посредством 
класса-эмулятора вершинного шейдера const string
effectFileName = "Data\\ColorFill.fx";
// Число сегментов в диске
const int slices = 64;
// Скорость вращения диска
public const float diskSpeed = 3.0f; 
// Радиус диска
public const float diskRadius = 0.018f;
GraphicsDevice device;
PresentationParameters presentParams;
VertexDeclaration diskDeclaration; 
// Массив с информацией о вершинах диска
VertexPositionColor[] diskVertices = null; 
// Массив с информацией о вершинах диска, 
обработанных вершинным шейдеров. Используется 
// исключительно для эмуляции работы вершинного шейдера
VertexPositionColor[] transformedDiskVertices = null;
Effect diskEffect = null;
Stopwatch stopwatch; bool closing = false;
private void MainFormLoad(object sender, EventArgs e) 
{
// Создаем графическое устройство
device = new GraphicsDevice(GraphicsAdapter.
DefaultAdapter, DeviceType.Hardware, 
this.Handle, options, presentParams);
// Декларация формата вершины
diskDeclaration = new VertexDeclaration(device,
VertexPositionColor.VertexElements);
// Создаем массив вершин диска
diskVertices = new VertexPositionColor[slices + 2]; 
// Создаем массив вершин диска, обработанных 
вершинным шейдером (используется при эмуляции 
// вершинного шейдера)
transformedDiskVertices = new VertexPositionColor[slices + 2];
// Заносим в массив вершин информацию о вершинах 
диска (цвета, углы поворота и расстояния от 
// центра)
diskVertices[0] = new VertexPositionColor(new 
Vector3(0.0f, 0.0f, 0.0f),
XnaGraphics.Color.LightGray);
for (int i = 0; i <= slices; i++) {
float angle = (float)i / (float)slices * 2.0f * 
(float)Math.PI; byte red = (byte)(255 *
Math.Abs(Math.Sin(angle * 3))); byte green = (byte)
(255 * Math.Abs(Math.Cos(angle * 2))); 
// Заносим в массив информацию о текущей вершине
diskVertices[i + 1] = new VertexPositionColor(new
 Vector3(diskRadius, angle, 
0.0f), new XnaGraphics.Color(red, green, 128)); 
};
// Создаем эффект для визуализации объекта
diskEffect = new Effect(device, compiledEffect.GetEffectCode(),
CompilerOptions.NotCloneable, null);
// Создаем и запускаем таймер
stopwatch = new Stopwatch(); 
stopwatch.Start(); }
private void MainFormPaint(object sender, PaintEventArgs e) 
{
// Вычисляем новый угол поворота диска и присваиваем 
его  "параметру эффекта "
float time = (float)stopwatch.ElapsedTicks / 
(float)Stopwatch.Frequency; DiskEffect.angle =
diskSpeed * time;
// Выполняем  "виртуальный вершинный шейдер "
DiskEffect.VertexShader(diskVertices, transformedDiskVertices);
// Задаем декларацию формата вершины
device.VertexDeclaration = diskDeclaration;
// Визуализируем диск
diskEffect.Begin();
for (int i = 0; i < diskEffect.CurrentTechnique.Passes.Count; i++)
{
EffectPass currentPass = diskEffect.CurrentTechnique.Passes[i] ;
 currentPass.Begin(); 
// Используем трансформированные вершины
device.DrawUserPrimitives(PrimitiveType.TriangleFan, 
transformedDiskVertices, 0, diskVertices.Length
- 2);
currentPass.End(); 
} diskEffect.End() ;
device.Present(); 
}
}
Листинг 5.10.

Готовое приложение находится в example.zip с книгой в каталоге Exampes\Ch05\Ex05.

< Лекция 4 || Лекция 5: 123456789101112
Андрей Леонов
Андрей Леонов

Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету.