|
Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету. |
Вершинные шейдеры
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.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.