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.