Создается C#-приложение, в котором средствами OpenTK генерируются частицы (рис. 1).
Рис. 1. Все частицы рождаются в одной точке
Тип проекта - Console Application.
Приводимый код основан на примере, имеющемся в поставке OpenTK.
Частицы - это вершины OpenGL, для хранения свойств которых используется VBO - Vertex Buffer Object - объект, содержащий свойства вершин.
VertexC4ubV3f[] VBO = new VertexC4ubV3f[MaxParticleCount];
Свойства Direction и Age (направление и возраст) частиц хранит объект ParticleAttributes
ParticleAttribut[] ParticleAttributes = new ParticleAttribut[MaxParticleCount];
VertexC4ubV3f и ParticleAttribut - это заданные в приводимой ниже программе структуры.
В программе для частиц заданы свойства Direction и Age (направление движения и возраст). Реально, однако на состояние сцены влияет только значение свойства Direction.
Все частицы рождаются в одной точке, имеют случайно заданный цвет и летят в случайно сгенерированных направлениях.
using System;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
using OpenTK.Input; // Для Key.Escape
namespace Particles
{
// VBO - Vertex Buffer Object. Средство для работы с массивами, содержащими свойства вершин
class T09_VBO_Dynamic : GameWindow
{
uint VBOHandle;
static int MaxParticleCount = 500;
int VisibleParticleCount;
// Создаем VBO
VertexC4ubV3f[] VBO = new VertexC4ubV3f[MaxParticleCount];
// Структура свойств частиц (Direction и Age)
ParticleAttribut[] ParticleAttributes = new ParticleAttribut[MaxParticleCount];
// Структура, употребляемая при выводе вершин
struct VertexC4ubV3f
{
public byte R, G, B, A;
public Vector3 Position;
public static int SizeInBytes = 16;
}
// Структура, употребляемая для обновления сцены
struct ParticleAttribut
{
public Vector3 Direction;
public uint Age;
// Далее можно задать Rotation, Radius и прочие свойства частиц
}
//
// Создает окно 800x600 с указанным заголовком
public T09_VBO_Dynamic() : base(400, 300)
{
// Вертикальная синхронизация - синхронизация кадровой частоты с частотой вертикальной развёртки монитора
// При этом максимальное значение FPS с вертикальной синхронизацией приравнивается к частоте обновления монитора
this.VSync = VSyncMode.Off;
}
// Загрузка ресурсов
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// Проверка версии OpenGL
Version version = new Version(GL.GetString(StringName.Version).Substring(0, 3));
Version target = new Version(1, 5);
if (version < target)
{
throw new NotSupportedException(String.Format("OpenGL {0} требуется (имеется только {1})", target, version));
}
// Цвет заливки окна вывода
GL.ClearColor(.1f, 0f, .1f, 0f);
GL.Enable(EnableCap.DepthTest); // Активизируем тест глубины
GL.PointSize(5f); // Размер частиц
GL.Enable(EnableCap.PointSmooth); // Точка округлой формы
GL.Hint(HintTarget.PointSmoothHint, HintMode.Nicest); // Наилучшее сглаживание
// Статус VBO. Работаем с массивами цвета и вершин
GL.EnableClientState(ArrayCap.ColorArray);
GL.EnableClientState(ArrayCap.VertexArray);
GL.GenBuffers(1, out VBOHandle); // Номер обработчика VBO
// Можно выполнить привязку здесь, поскольку имеется только 1 VBO
GL.BindBuffer(BufferTarget.ArrayBuffer, VBOHandle);
GL.ColorPointer(4, ColorPointerType.UnsignedByte, VertexC4ubV3f.SizeInBytes, (IntPtr)0);
GL.VertexPointer(3, VertexPointerType.Float, VertexC4ubV3f.SizeInBytes, (IntPtr)(4 * sizeof(byte)));
Random rnd = new Random();
Vector3 temp = Vector3.Zero;
// Генерация частиц
for (uint i = 0; i < MaxParticleCount; i++)
{
// Цвет частицы i
VBO[i].R = (byte)rnd.Next(0, 256);
VBO[i].G = (byte)rnd.Next(0, 256);
VBO[i].B = (byte)rnd.Next(0, 256);
VBO[i].A = (byte)rnd.Next(0, 256); // Не используется
// Координаты частицы i
VBO[i].Position = Vector3.Zero; // Все частицы появляются в начале координат
// Генерация вектора направления в диапазоне [-0.25f...+0.25f]
temp.X = (float)((rnd.NextDouble() - 0.5) * 0.5f);
temp.Y = (float)((rnd.NextDouble() - 0.5) * 0.5f);
temp.Z = (float)((rnd.NextDouble() - 0.5) * 0.5f);
ParticleAttributes[i].Direction = temp; // Вектор направления
ParticleAttributes[i].Age = 0; // Начальный возраст частицы
}
// Число родившихся частиц
VisibleParticleCount = 0;
}
protected override void OnUnload(EventArgs e)
{
GL.DeleteBuffers(1, ref VBOHandle);
}
// Вызывается при изменении размеров окна вывода
// Подходящее место для матриц проецирования и видовой матрицы
protected override void OnResize(EventArgs e)
{
GL.Viewport(0, 0, Width, Height);
GL.MatrixMode(MatrixMode.Projection);
Matrix4 p = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver4, Width / (float)Height, 0.1f, 50.0f);
GL.LoadMatrix(ref p);
GL.MatrixMode(MatrixMode.Modelview);
Matrix4 mv = Matrix4.LookAt(Vector3.UnitZ, Vector3.Zero, Vector3.UnitY);
GL.LoadMatrix(ref mv);
}
// Вызывается при обновлении изображения
protected override void OnUpdateFrame(FrameEventArgs e)
{
var keyboard = OpenTK.Input.Keyboard.GetState();
if (keyboard[Key.Escape]) Exit();
// Обновляем частицы. При использовании Physics SDK частота обновления существенно выше числа кадров в секунду
// Поэтому будут лишние циклы копирования в VBO
if (VisibleParticleCount < MaxParticleCount) VisibleParticleCount++;
Vector3 temp;
for (int i = MaxParticleCount - VisibleParticleCount; i < MaxParticleCount; i++)
{
if (ParticleAttributes[i].Age >= MaxParticleCount)
{
// Приводим непоявившиеся частицы в начальное состояние
ParticleAttributes[i].Age = 0;
VBO[i].Position = Vector3.Zero;
}
else
{
// Эти частицы уже в сцене
// Увеличиваем возраст частицы
ParticleAttributes[i].Age += (uint)Math.Max(ParticleAttributes[i].Direction.LengthFast * 10, 1);
// Получаем вектор temp, умножая вектор направления на время события
Vector3.Multiply(ref ParticleAttributes[i].Direction, (float)e.Time, out temp);
// Изменяем координаты частицы, складывая прежние координаты с вектором temp
Vector3.Add(ref VBO[i].Position, ref temp, out VBO[i].Position);
}
}
}
// Вызывается при выводе очередного кадра
// Параметр "e" содержит информацию о времени
protected override void OnRenderFrame(FrameEventArgs e)
{
this.Title = VisibleParticleCount + " - число частиц. FPS: " + string.Format("{0:F}", 1.0 / e.Time);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
GL.PushMatrix();
// Перемещаем частицы по оси Z
GL.Translate(0f, 0f, -5f);
// Сообщаем OpenGL, что можно пренебречь прежним VBO и предоставить память для нового VBO-буфера
// Без этого GL будет ждать завершения вывода старого VBO
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(VertexC4ubV3f.SizeInBytes * MaxParticleCount), IntPtr.Zero, BufferUsageHint.StreamDraw);
// Заполняем вновь выделенный буфер
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(VertexC4ubV3f.SizeInBytes * MaxParticleCount), VBO, BufferUsageHint.StreamDraw);
// Выводим только уже родившиеся частицы
GL.DrawArrays(PrimitiveType.Points, MaxParticleCount - VisibleParticleCount, VisibleParticleCount);
GL.PopMatrix();
SwapBuffers();
}
// Точка входа в приложение
static void Main()
{
using (T09_VBO_Dynamic example = new T09_VBO_Dynamic())
{
example.Title = "Пример вывода частиц";
example.Run(30.0, 0.0);
}
}
}
}