Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету. |
Визуализация примитивов
2.6. Треугольники
Для визуализации наборов треугольников с различной топологией в XNA Framework имеется три типа примитивов: PrimitiveType.TriangleList, PrimitiveType.TriangleFan и PrimitiveType.TriangleStrip.Начнем с самого простого примитива, PrimitiveType.TriangleList.
2.6.1. Несвязанные треугольники (PrimitiveType.TriangleList)
Этот примитив предназначен для визуализации набора несвязанных треугольников: первый треугольник строится с использованием 0-й, 1-й и 2-й вершин, второй треугольник – 3-й, 4-й и 5-й вершин, третий треугольник – 6-й, 7-й и 8-й вершин и т.д. (рисунок 2.21).
По умолчанию XNA Framework отображает на экране только те треугольники, вершины которых расположены на экране по часовой стрелке. К примеру, при визуализации треугольников, изображенных на рисунке 2.21 на экране отобразятся только крайние треугольники ( v0, v1, v2 ) и ( v6, v7, v8 ). А вот средний треугольник ( v3, v4, v5 ) будет отброшен, так как его вершины перечисляются против часовой стрелки. Такое на первый взгляд странное поведение XNA Framework обусловлено особенностью отсечения невидимых треугольников в трехмерных сценах. Однако при визуализации двухмерных изображений эта функциональность оказывается не только излишней, но и вредной. Поэтому разработчики XNA Framework заботливо предусмотрели свойство GraphicsDevice.RenderState.CullMode, управляющее режимами отсечения треугольников:
public CullMode CullMode { get; set; }
Это свойство может принимать следующие значения перечислимого типа CullMode:
- CullMode.None - отсечение выключено
- CullMode.Clockwise - отсекаются треугольники, вершины которых расположены на экране по часовой стрелке
- CullMode.CounterClockwise - отсекаются треугольники, у которых вершины расположены на экране против часовой стрелки.
По умолчанию свойству GraphicsDevice.RenderState.CullMode присваивается значение CullMode.CounterClockwise, то есть видеокарта отбрасывает все треугольники, у которых вершины расположены против часовой стрелки. Для отключения этой функциональности достаточно присвоить этому свойству значения CullMode.None.
В листинге 2.22 приведен исходный код основных фрагментов программы (Ex12), рисующей в центре экрана треугольник (рисунок 2.22).
public partial class MainForm : Form { const string effectFileName = "Data \\ColorFill.fx"; GraphicsDevice device = null; PresentationParameters presentParams; Effect effect = null; VertexDeclaration decl = null; VertexPositionColor[] vertices = null; FillMode fillMode=FillMode.Solid; bool closing = false; … private void MainFormLoad(object sender, EventArgs e) … { // Создаем массив для хранения трех вершин треугольника vertices = new GraphicsBuffer<TransformedColored>(3); // Задаем вершины треугольника vertices[0] = new VertexPositionColor(new Vector3(0.0f, 0.4f, 0.0f), 4> XnaGraphics.Color.Coral); vertices[1] = new VertexPositionColor(new Vector3(0.4f, -0.4f, 0.0f), 4> XnaGraphics.Color.LightGreen); vertices[2] = new VertexPositionColor(new Vector3(-0.4f, -0.4f, 0.0f), 4> XnaGraphics.Color.Yellow); } private void MainFormPaint(object sender, PaintEventArgs e) { device.Clear(XnaGraphics.Color.CornflowerBlue); // Выключаем отсечение треугольников device.RenderState.CullMode = Cull.None; device.VertexDeclaration = decl; // Рисуем треугольник effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin() ; device.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3); pass.End() ; } effect.End(); device.Present(); }Листинг 2.22.
Как видно, листинг программы мало чем отличается от предыдущих примеров. Единственное разница заключается в отключении режима отсечения треугольников и использовании примитивов типа PrimitiveType.TriangleList.
Режимы закраски
Как известно, многие приложения 3D моделирования вроде 3ds Max или Maya позволяют отображать сцену в режиме проволочного каркаса ( Wireframe ). Благодаря этому разработчик может ясно видеть топологию сцены, в частности, взаимное расположение всех треугольников на сцене. XNA Framework тоже поддерживает подобную функциональность, позволяя отображать вместо закрашенных треугольников их проволочный каркас. Управление этой функциональностью осуществляется при помощи свойства RenderState.FillMode класса GraphicsDevice:
FillMode FillMode { get; set; }
Свойство может принимать следующие значения перечислимого типа FillMode:
- FillMode.Point - визуализируются только точки, расположенные на вершинах треугольника. Визуализируемые точки являются полноценными точками XNA Framework: к примеру, их размер можно изменять при помощи свойства device.RenderState. PointSize.
- FillMode.WireFrame - визуализирует каркас треугольника, который рисуется с использование oбычных линий вроде TrianglePrimitive.LineList или TrianglePrimitive.LineStrip.
- FillMode.Solid - закрашивает внутреннюю область треугольника.
По умолчанию свойству RenderState.FillMode присвоено значение FillMode.Solid, то есть треугольники рисуются закрашенными.
Для демонстрации практического использования свойства FillMode мы добавим в нашу программу ( Ex12 ) возможность циклической смены режимов отображения треугольников при помощи клавиши пробел (листинг 2.23).
public partial class MainForm : Form { // Режим отображения треугольников FillMode fillMode=FillMode.Solid; ... private void MainForm_Paint(object sender, PaintEventArgs e) { ... // Выключаем отсечение треугольников device.RenderState.CullMode = Cull.None; // Задаем режим отображения треугольников device.RenderState.FillMode = fillMode; // Зазаем размер точек device.RenderState.PointSize = 3.0f; ... // Рисуем треугольник effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, verteces.Length / 3); pass.End(); } effect.End(); ... } private void MainForm_KeyDown(object sender, KeyEventArgs e) { // Если нажата клавиша пробел if (e.KeyCode == Keys.Space) { // Изменяем режим отображения switch (fillMode) { case FillMode.Point: fillMode = FillMode.WireFrame; break; case FillMode.WireFrame: fillMode = FillMode.Solid; break; case FillMode.Solid: fillMode = FillMode.Point; break; } // Перерисовываем экран Invalidate(); } } }Листинг 2.23.
Узор Серпинского
Перейдем к более сложному примеру. Наше следующее приложение будет строить узор Серпинского путем рекурсивного разбиения треугольника, визуализируемого в каркасном режиме. Построение узора начинается с базового треугольника (рисунок 2.23). На первой интеграции в данный большой треугольник вписывается другой треугольник меньшего размера, вершины которого расположены в середине сторон большого треугольника (рисунок 2.24). В результате большой треугольник оказывается как бы разбит на 4 треугольника. На втором этапе данные в три треугольника, примыкающие к вершинам исходного большого треугольника, вписываются по три треугольника (рисунок 2.25). На третьем этапе в образовавшиеся девять треугольников вписываются уже девять треугольников (рисунок 2.26), на четвертом этапе вписывается уже 27 треугольников и так далее. В идеале процесс должен продолжаться до бесконечности, одна ко на практике вполне можно ограничиться десятком итераций (рисунок 2.27), так как размер треугольников, генерируемых в последующих итерациях, будет уже меньше размера пикселей экрана.
Так как приложение будет визуализировать десятки или даже сотни тысяч треугольников, очень важно поместить их в единый массив и вывести одним вызовом метода DrawUserPrimitives. Однако для создания такого массива очень полезно заранее знать количество треугольников, которые будут визуализированы за n итераций. Это поможет нам избежать многочисленных изменений размера массива по мере генерации треугольников. Давайте попробуем найти зависимость числа визуализируемых треугольников от количества интеграций. И так, при нулевом количестве итераций мы визуализируем 1 треугольник. При одной итерации число треугольников становится 1 + 1 = 2. При двух итерациях количество треугольников будет равно 1 + 1 + 3 = 5, при трех 1 + 1 + 3 + 9 = 14. Таким образом, мы можем вывести некоторую общую закономерность для n итераций:
( 2.4) |
где
- tc - количество треугольников, визуализируемых при n итераций.
В принципе, это выражение вполне приемлемо, однако знак суммы смотрится не особо красиво. Однако открыв учебник высшей математики вроде можно найти весьма интересное соотношение:
( 2.5) |
Соответственно, выражение 2.4 можно переписать без использования n элементов:
( 2.6) |
Гораздо более наглядное выражение, не так ли? Однако так как разные видеокарты могут визуализировать разное число треугольников, не исключено, что приложению придется решать и образную задачу. Допустим, мы определим в приложении число итераций ( n ) равным 11, то есть узор Серпинского будет содержать треугольников с общим количеством вершин 88574 ? 3 = 265722. Но ведь некоторые видеокарты могут оказаться не способными визуализировать такое количество треугольников за один присест. Как приложение должно повести себя в подобном случае? Наиболее простое решение – сократить количество интеграций до максимально приемлемого. А для этого нам придется определять максимальное количество итераций ( n ), при котором количество треугольников не превышает заданное значение tc. Для этого выражение (2.6) достаточно переписать как
( 2.7) |
после чего взять от обоих частей выражения логарифм по основанию 3:
( 2.8) |
где
- - функция, возвращающая целое число, не превышающее x (то аналог метода Math.Floor из C#).
К слову
Согласно выражению 2.8 на компьютере с Intel GMA 900 приложение может выполнить до 9-ти итераций, на NVIDIA NV 2x-3x до 12-ти итераций, а на ATI R2xx-R5xx до 13-ти итераций.
После такого небольшого математического экскурса можно приступать реализации нашего приложения. Визуализация треугольников будет осуществляться в два этапа:
- Заполнение графического буфера информацией о треугольниках.
- Визуализация треугольников одним вызовом метода DrawUserPrimitives.
Вычисление координат треугольников мы организуем с использованием рекурсивной функции DrawTriangle принимающей в качестве параметров координаты треугольника и количество оставшихся итераций. Эта функция будет помещать в массив вершин координаты текущего треугольника, после чего выполнять деление этого треугольника на три части и вызывать саму себя для этих частей, но уже с уменьшенным количеством оставшихся итераций на 1. Процесс повторяется до тех пор, пока количество оставшихся итераций не достигнет 0.
Исходный код основных фрагментов программы с подробными комментариями приведен в листинге 2.24.
// Примем Examples\Ch02\Ex13 public partial class MainForm : Form { // Число итераций для визуализации треугольника Серпинского. Если видеокарта не способна // визуализировать такое количество треугольников, число итераций автоматически уменьшается // до приемлемого значения const int n = 15; const string effectFileName = "Data \\ColorFill.fx"; GraphicsDevice device = null; PresentParameters presentParams; Effect effect = null; VertexDeclaration decl = null; // Массив вершин узора Серпинского VertexPositionColor[] vertices = null; // Индекс текущей вершины (глобальная переменная, используемая при рекурсивном формировании // узора Серпинского) int currentVertex; // Рекурсивная функция, заносящая в массив vertices информацию о вершинах узора. // a, b, c - координаты текущего треугольника // pass - число оставшихся итераций void DrawTriangle(Vector2 a, Vector2 b, Vector2 c, int pass) { // Если это последняя итерация, выходим из функции if (pass <= 0) return; // Уменьшаем количество оставшихся итераций pass -= 1; // Помещаем в массив вершины треугольника вписанного в текущий "большой" треугольник vertices[currentVertex] = new VertexPositionColor (new Vector3(ab.X, ab.Y, 0.0f), XnaGraphics.Color.Black); vertices[currentVertex + 1] = new VertexPositionColor (new Vector3(ac.X, ac.Y, 0.0f), XnaGraphics.Color.Black); vertices[currentVertex + 2] = new VertexPositionColor (new Vector3(bc.X, bc.Y, 0.0f), XnaGraphics.Color.Black); // Увеличиваем индекс текущей вершины currentVertex += 3; // Вычисляем координаты середины сторон треугольника Vector2 ab = new Vector2((a.X + b.X) / 2.0f,(a.Y+b.Y)/2.0f); Vector2 ac = new Vector2((a.X + c.X) / 2.0f,(a.Y+c.Y)/2.0f); Vector2 bc = new Vector2((b.X + c.X) / 2.0f,(b.Y+c.Y)/2.0f); // Вызываем этот рекурсивный метод для образовавшихся трех крайних треугольников, примыкающих // к углам текущего треугольника DrawTriangle(a, ab, ac, pass); DrawTriangle(b, ab, bc, pass); DrawTriangle(c, ac, bc, pass); } private void MainForm_Load(object sender, EventArgs e) { ... // Определяем максимальное количество треугольников, которое текущая видеокарта может // визуализировать за один присест int maxTriangleCount = Math.Min(device.GraphicsDevice Capabilities.MaxPrimitiveCount, device.GraphicsDeviceCapabilities.MaxVertexIndex / 3); // Вычисляем по формуле 2.8 максимальное количество итераций визуализации узора, которые // можно выполнить на текущей видеокарте int maxPass = (int) Math.Floor(Math.Log(2 * maxTriangleCount - 1, 3)); // При необходимости уменьшаем количество интеграций, которое задаются константой n int passes = Math.Min(n, maxPass); // Вычисляем по формуле 2.6 количество треугольников, формирующих данный узор Серпинского. int triangleCount = ((int)Math.Pow(3, passes) + 1) / 2; // Выделяем память для хранения информации о вершинах треугольниках vertices = new VertexPositionColor[3 * triangleCount]; Text += " Количество итераций: " + passes.ToString(); // Вершины начального треугольника Vector2 a = new Vector2(0.0f, 0.9f); Vector2 b = new Vector2(-0.9f, -0.9f); Vector2 c = new Vector2(0.9f, -0.9f); // Обнуляем индекс текущей вершины currentVertex = 0; // Заносим в массив вершины самого большого треугольника vertices[currentVertex] = new VertexPositionColor(new Vector3(a.X, a.Y, 0.0f), XnaGraphics.Color.Black); vertices[currentVertex + 1] = new VertexPositionColor(new Vector3(b.X, b.Y, 0.0f), XnaGraphics.Color.Black); vertices[currentVertex + 2] = new VertexPositionColor(new Vector3(c.X, c.Y, 0.0f), XnaGraphics.Color.Black); currentVertex += 3; // Выполняет рекурсивное деление треугольника в течении pass итераций CreateTriangle(a, b, c, passes); } private void MainForm_Paint(object sender, PaintEventArgs e) { ... // Очищаем экран device.Clear(ClearFlags.Target, Color.White, 0.0f, 0); device.BeginScene(); // Отключаем отсечение треугольников device.RenderState.CullMode = Cull.None; // Используем каркасную визуализацию треугольников device.RenderState.FillMode = FillMode.WireFrame; device.VertexFormat = TransformedColored.Format; // Рисуем треугольники device.DrawUserPrimitives(PrimitiveType.TriangleList, verteces.NumberElements/3, verteces); device.EndScene(); device.Present(); } } }Листинг 2.24.