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

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

< Лекция 4 || Лекция 5: 123456789101112

Искра вылетает все время из одного и того же места диска, но так как диск постоянно вращается, начальный угол поворота вершины все время оказывается разным. Такой подход имеет два достоинства: цвет вершины остается постоянным, а сами траектории движения вершин становятся более хаотичными. Для учета угла поворота диска в момент появления вершины используется слагаемое diskSpeed  \cdot  (time – localTime) , являющееся постоянным на протяжении всей жизни вершины (т.е. до следующей итерации).

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

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

Как и в случае с эффектом, вращающим диск, мы начнем с реализации класса, инкапсулирующего вершинный шейдер. Для начала определимся с параметрами, принимаемыми эффектом и способом их передачи ( таблица 5.5). В эффекте визуализации вращающегося диска входные параметры, уникальные для каждой вершины, передавались через координаты вершины. Но эффект визуализации искр содержит заметно больше входных параметров, поэтому нам придется передавать часть параметров через текстурные координаты. Применение текстурных координат никоим образом не сказывается точности передаваемых значений, ведь они, как и координаты и цвета вершин, проецируются компилятором HLSL на универсальные входные регистры v0, v1 … v15 . Код класса, эмулирующего работу вершинного шейдера, приведен в листинге 5.14.

Таблица 5.5. Входные параметры вершинного шейдера, поведение искры
Описание параметра Аналогичный параметр из формул предыдущего раздела Общий для всех вершин Место хранения
Текущее время time Да Входной параметр time
Длительность "итерации ", в течении которой движения искр не повторяются timeLoop Да Входной параметр timeLoop
Скорость вращения диска diskSpeed Да Входной diskSpeed
Цвет искры - Нет Цвет вершины
Время появления искры vertexStartTime Нет Координата X
Начальное расстояние вершины от центра диска distance0 Нет Координата Y
Локальный угол поворота вершины angle0 Нет Координата Z
Начальная скорость удаления вершины от центра tSpeed Нет Текстурная координата X
Начальная угловая скорость вершины rSpeed Нет Текстурная координата Y
static class FireworkEffect
{
// Константы с замедлениями искр. Общие для всех вершин
const float tSlowing = 0.105f;
const float rSlowing = 0.25f; 
// Константа времени жизни искр
const float liveTime = 4.0f;
// Входные	параметр time, timeLoop, diskSpeed
public	static float time;
public	static float timeLoop;
public	static float diskSpeed;
// Код вершинного шейдера.
Листинг 5.14.
// input - входные данные вершины, 
// output - выходные данные вершины
public static void VertexShader
(VertexPositionColorTexture[][] input,
VertexPositionColor[][] output) 
{ 
// Перебираем все вершины (в реальном вершинном 
шейдере этих циклов не будет). Так как при 
// тестировании производительности число вершин 
может превысить лимит примитивов, которые 
// может визуализировать за один проход GPU Intel 
GMA9xx, используется несколько массивов 
// вершин
for (int j = 0; j < input.Length; j++) 
{
for (int i = 0; i < input[j].Length; i++) 
{ 
// Вычисляем время, прошедшее с первого появления искры
float currentTime = time - input[j][i].Position.X; 
// Вычисляем локальное время, циклически 
пробегающее от 0 до timeLoop
float localTime = currentTime % timeLoop; 
// Определяем время, которое осталось существовать искре float 
remainTime = liveTime - localTime;
// Ограничиваем величину локального времени, 
чтобы искра останавливалась по достижению 
// нулевой скорости
float td = Math.Min(localTime, input[j][i].
TextureCoordinate.X / tSlowing); 
// Вычисляем расстояние вершины от центра диска
float distance = input[j][i].Position.Y + td * 
 (input[j][i].TextureCoordinate.X - td * tSlowing / 2.0f);
// Ограничиваем величину локального времени, 
чтобы искра останавливалась по достижению 
// нулевой угловой скорости
float tr = Math.Min(localTime, input[j][i].TextureCoordinate.Y / 
rSlowing); 
// Вычисляем текущий угол поворота искры вокруг диска
float angle = input[j][i].Position.Z + diskSpeed * 
(time - localTime) + 
tr * (input[j][i].TextureCoordinate.Y - tr * rSlowing / 2.0f); 
// Вычисляем координаты вершины на основе 
расстояния и угла поворота
output[j][i].Position.X = distance * (float)Math.Sin(angle);
output[j][i].Position.Y = distance * (float)Math.Cos(angle);
output [j ] [i] .Position. Z = 0.0f;
// Если искра появилась на экране, но еще не потухла
if ((currentTime >= 0) && (remainTime > 0)) 
{
Vector4 color = input[j][i].Color.ToVector4(); 
// Определяем коэффициент прозрачности вершины
color.W = remainTime / liveTime;
output[j][i].Color = new XnaGraphics.Color(color); 
}
else 
{
output[j][i].Color = new XnaGraphics.Color(0, 0, 0, 0); 
     } 
   } 
  } 
 } 
}

Коротко пробежимся по основным моментам программы. Информация о вершинах теперь хранится в структуре VertexPositionColorTexture , предоставляющей помимо знакомых нам полей Position и Color еще и поле TextureCoordinate , содержащее компоненты X и Y текстурных координат вершины. Выражения 5.7 и 5.8 были переписаны с использованием схемы Горнера, позволяющей сократить число операций при вычислении степенного многочлена. Кроме того, такая запись хорошо ложится на команду mad вершинного процессора. Так же код эффекта содержит конструкцию if , делающую невидимыми искры, которые согласно логике работы приложения еще не появились на экране.

Примечание

Кстати, HSLS реализует вычисление разложения функций sin и cos в ряд Тейлора посредством схемы Горнера.

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

public partial class MainForm : Form
{
// Эффект для простой закраски объектов
const string effectFileName = "Data\\ColorFill.fx";
const int slices = 64;
const float diskSpeed = 3.0f; const float
 diskRadius = 0.018f;
// Число искр
const int fireworkVerticesCount = 300000; 
// Движения искр будут повторяться через каждые 20 секунд
const float timeLoop = 20.0f; 
// Минимальная скорость вершины
const float minSpeed = 0.3f; 
// Максимальная скорость вершины
const float maxSpeed = 0.45f; 
// Размер искры
const float pointSize = 1.0f;
// Декларация формата вершины. Диск и искры в режиме 
эмуляции вершинного шейдера используют 
// общий формат вершин
VertexDeclaration decl; 
// Массивы вершин с искрами
VertexPositionColorTexture[][] fireworkVertices = null; 
// Массивы вершин, обработанных эмулятором вершинного шейдера
VertexPositionColor[][] transformedFireworkVertices = null;
// Эффект, общий для диска и искр (в режиме эмуляции) Effect effect 
= null;
Random rnd = new Random();
Stopwatch stopwatch;
bool closing = false;
// Счетчики FPS
// Временя, прошедшее с момента последнего вычисления 
количества кадров в секунду
float lastTime = 0; 
// Число кадров, визуализированных за это время
int frameCount = 0;
private void MainForm_Load(object sender, EventArgs e) 
{ 
// Определяем число точек, которые может визуализировать
 видеокарта за один присест
int maxVerticesCount = Math.Min(device.GraphicsDeviceCapabilities.
MaxVertexIndex, 
device.GraphicsDeviceCapabilities.MaxPrimitiveCount); 
// Определяем количество массивов вершин, которые потребуются 
для визуализации
// fireworkVerticesCount вершин
int arrayCount = (int)Math.Ceiling((float)fireworkVerticesCount / 
(float)maxVerticesCount);
// Создаем массивы вершин
fireworkVertices = new VertexPositionColorTexture[arrayCount][]; 
// Создаем массивы вершин, трансформированных вершинным шейдером
transformedFireworkVertices = new VertexPositionColor[arrayCount][];
// Перебираем вершины
for (int k = 0; k < fireworkVerticesCount; k++) 
{ 
// Определяем индекс массива вершин, соответствующего текущей вершине
int j = k / maxVerticesCount; 
// Определяем индекс текущей вершины в массиве вершин
int i = k % maxVerticesCount; 
// Если мы перешли к новому массиву if (i == 0) 
{ 
// Определяем количество оставшихся вершин
int remain = fireworkVerticesCount - j * maxVerticesCount;
 // Число вершин в массиве не может превышать maxVerticesCount 
 remain = Math.Min(remain, 
maxVerticesCount);
// Выделяем память для текущих массивов вершин
fireworkVertices[j] = new VertexPositionColorTexture[remain]; 
transformedFireworkVertices[j] = new 
VertexPositionColor[remain]; 
}
// Вычисляем время появления вершины после запуска программы
fireworkVertices[j][i].Position.X = (float)rnd.NextDouble() * timeLoop;
// Определяем ее начальное удаление от центра диска
fireworkVertices[j][i].Position.Y = (float)rnd.NextDouble() * 
diskRadius;
// Определяем начальный угол поворота вершины относительно диска
fireworkVertices[j][i].Position.Z = (float)rnd.NextDouble() * 2.0f *
(float) Math. PI;
// Определяем начальную линейную скорость вершины
fireworkVertices[j][i].TextureCoordinate.X = minSpeed + 
(float)rnd.NextDouble() * (maxSpeed - minSpeed); 
// Определяем начальную угловую скорость вершины
fireworkVertices[j][i].TextureCoordinate.Y = diskSpeed / 4.0f * 
(1.0f + 0.01f * (float)rnd.NextDouble()); 
// Вычисляем цвет вершины
byte red = (byte)(255 * Math.Abs(Math.Sin(fireworkVertices[j][i].
Position.Z * ^ 3)) ) ;
byte green = (byte)(255 * Math.Abs(Math.Cos(fireworkVertices[j][i].
Position.Z * ^ 2) ) ) ;
fireworkVertices[j][i].Color = new XnaGraphics.Color
(red, green, 128, 255); 
}
      } 
}
Листинг 5.15.

Чтобы иметь возможность наглядно оценить эффект переноса вычислений с CPU на GPU , мы будем визуализировать 300.000 искр. Так как ряд видеокарт (например, Intel GMA 9xx ) не могут визуализировать такое количество примитивов за один присест 25 92, приходится автоматически разбивать массив вершин на ряд "подмассивов " меньшего размера, поддерживаемых текущей видеокартой. Так же стоит отметить, что из-за эмуляции вершинных шейдеров диска и искр, они используют общий эффект и декларацию формата вершины.

Код визуализации искр является достаточно тривиальным, если не считать того факта, что искры могут храниться в разных массивах вершин (листинг 5.16).

private void MainFormPaint(object sender, PaintEventArgs e)
{
// Настраиваем параметры GPU для визуализации
device.RenderState.CullMode = CullMode.None;
device. RenderState.AlphaBlendEnable = true;
device.RenderState.BlendFunction = BlendFunction.Add;
device.RenderState.SourceBlend = Blend.SourceAlpha;
device.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
device.RenderState.PointSize = pointSize;
device.VertexDeclaration = decl;
// Определяем время, прошедшее с момента запуска приложения
float time = (float)stopwatch.ElapsedTicks / 
(float)Stopwatch.Frequency;
// Настраиваем параметры эффекта, общие для всех вершин
FireworkEffect.time = time;
FireworkEffect.timeLoop = timeLoop;
FireworkEffect.diskSpeed = diskSpeed;
// Выполняем эмуляцию вершинного шейдера
FireworkEffect.VertexShader(fireworkVertices, 
transformedFireworkVertices);
effect.Begin();
for (int i = 0; i < effect.CurrentTechnique.Passes.Count; i++)
{
EffectPass currentPass = effect.CurrentTechnique.Passes[i];
currentPass.Begin();
// Перебираем все массивы вершин
for (int j = 0; j < transformedFireworkVertices.Length; j++)
{ // Визуализируем текущий массив вершин
device.DrawUserPrimitives(PrimitiveType.PointList, 
 transformedFireworkVertices[j], 0, 
 transformedFireworkVertices[j].Length);
}
currentPass.End(); 
} 
effect.End();
// Отключаем альфа-смешивание, которое не требуется 
при визуализации диска 
device.RenderState.AlphaBlendEnable = false;
float angle = diskSpeed * time;
// Выполняем эмуляцию вершинного шейдера диска
DiskEffect.angle = angle;
DiskEffect.VertexShader(diskVertices, transformedDiskVertices); 
// Визуализируем диск
// Оканчиваем визуализацию кадра 
device.Present();
// Увеличиваем счетчик кадров
frameCount++; 
// Если прошла одна секунда
if (time - lastTime >= 1)
{ 
// Отображаем в заголовке формы текущий FPS
Text = ((float)frameCount / (time - lastTime)).ToString(); 
// Сбрасываем счетчики
lastTime = time; frameCount = 0; 
} 
}
Листинг 5.16.

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

Результаты тестирования на компьютерах с видеокартами NVIDIA GeForce 7600GT и Intel GMA 9xx приведены в таблице 5.6. Так как конфигурация компьютеров заметно отличается, эти данные будут использоваться не для сравнения видеокарт между собой, а исключительно для оценки прироста производительности от внедрения вершинных шейдеров. Как видно, цифры сейчас колеблются в пределах 10 кадров в секунду, что явно недостаточно для обеспечения плавной анимации. Но уверяю вас, что к концу шестой главы частота кадров будет измеряться в сотнях кадров в секунду, причем это прирост будет достигнут без какого-либо ухудшения качества изображения.

Таблица 5.6. Производительность примера Ch05\Ex10 на разных GPU
Конфигурация компьютера FPS
Intel Core2 Duo E6300, i945P, 2GB RAM, DDR2-667, GeForce 7600GT 256MB, Windows Vista Ultimate x64, ForceWare 158.24 10,7
Intel Pentium-4 3.4GHz, i915P, 512MB RAM, ATI Radeon x700 Pro, Windows XP Pro SP2, ASUS Driver 8.05 6.3
Intel Core2 Duo E4300, i946GZ (GMA 3000), 2GB RAM DDR2-667, Windows Vista Ultimate x64, GMA Driver 7.14.10.1283 9.9

Примечание

Для корректного измерения производительности приложения при создании устройства свойство PresentationParameters.PresentationInterval должно быть установлено в PresentInterval.Immediate, в противном случае частота кадров будет зависеть от частоты вертикальной развертки монитора.

Вспомогательный метод загрузки эффекта из файла

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

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

class Helper 
{
// Класс исключения, которое генерируется при
 возникновении проблем во время загрузки эффекта
public class LoadAndCompileEffectException : Exception 
{
public LoadAndCompileEffectException(string message) : base(message) 
{ 
        } 
}
// Загружает эффект из файла и выбирает наиболее подходящую технику. 
При возникновении 
// проблем генерирует исключение LoadEffectException.
public static Effect LoadAndCompileEffect(GraphicsDevice device,
 string filename) 
{
CompiledEffect compiledEffect;
try
{
compiledEffect = Effect.CompileEffectFromFile(filename, null,
 null, CompilerOptions.None,
TargetPlatform.Windows); 1`
1
}
catch (IOException ex) 
{
throw new LoadAndCompileEffectException(ex.Message); 
}
if (!compiledEffect.Success) 
{
throw new LoadAndCompileEffectException(String.Format
("Ошибка при компиляции эффекта: \r\n{0}",
compiledEffect.ErrorsAndWarnings)); 
}
Effect effect = new Effect(device, compiledEffect.GetEffectCode(), 
CompilerOptions.NotCloneable, null);
if (!effect.CurrentTechnique .Validate())
{
throw new LoadAndCompileEffectException(String.Format
("Ошибка при валидации " + 
 "техники \"{0}\" эффекта \"{1}\"\n\rСкорее всего, 
 функциональность шейдера превышает " +
 "возможности GPU", effect.CurrentTechnique.Name, filename));
}
return effect; 
} 
}
Листинг 5.17.

Полноценный эффект

Имея на руках код метода-эмулятора вершинного шейдера, написанного на C# с учетом специфики языка HLSL, создание эффекта не представляет какой-либо принципиальной сложности (листинг 5.18). Тем не менее, перевод C# -кода в HLSL не должен сводиться к механической трансляции - как-никак, HLSL содержит гибкие средства для векторных вычислений, позволяющие повысить качество промежуточного ассемблерного кода. В частности, мы можем значительно сократить объем вычислений, реализовав параллельный расчет текущего расстояния вершины от центра круга и угла поворота.

// Файл Firework.fx
//
// Константы tSlowing и rSlowing объединены в один двухмерный 
вектор, что позволит
// распараллелить расчет расстояния от вершины от центра 
и угла поворота вершины
static float2 slowing = {0.105, 0.25};
static float liveTime = 4.0;
float diskSpeed; float time;
float timeLoop;
struct VertexInput {
float3 pos : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD; 
};
struct VertexOutput 
{
float4 pos : POSITION;
float4 color : COLOR; 
};
VertexOutput MainVS(VertexInput input) 
{
VertexOutput output;
float currentTime = time - input.pos.x; float localTime = currentTime
 % timeLoop; float remainTime = liveTime - localTime;
// Расстояние от центра диска и угол поворота вершины 
рассчитываются параллельно float2 t = min(localTime.xx,
 input.texcoord / slowing); float2 sCoord = input.pos.yz + t * 
 (input.texcoord -t * slowing / 2.0f);
// Формула расчета угла поворота по сравнению с формулой 
расчета расстояния от центра 
// содержит один добавочный член
sCoord.y += diskSpeed * (time - localTime);
// Заменяем два умножения константы на sin и cos одним 
умножением на вектор (sin, cos) output.pos.xy = sCoord.x * 
float2(sin(sCoord.y), cos(sCoord.y)); output.pos.zw = float2(0.0, 1.0);
output.color.rgb = input.color.rgb;
if ((remainTime > 0) && (currentTime >= 0)) 
{
output.color.a = remainTime / liveTime; 
}
else 
{
output.color.a = 0; 
}
return output; 
}
float4 MainPS(float4 color:COLOR):COLOR 
{
return color; 
}
technique Firework 
{
pass p0
{
VertexShader = compile vs_1_1 MainVS();
PixelShader = compile ps_1_1 MainPS(); 
}
}
Листинг 5.18.
< Лекция 4 || Лекция 5: 123456789101112
Андрей Леонов
Андрей Леонов

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