вторник, 10 апреля 2012 г.

Практическое применение XNA. Часть 13.

Трафаретные тени (Stencil Shadows)

Большинство разработчиков компьютерных игр стремятся к фотореалистичности. Ведь чем сильнее сходство игрового изображентя и реальности, тем захватывающе становится пребывание в игровом мире. И как следствие, игра имеет большее количество поклонников. На реалистичность картинки влияет множество факторов. В идеале если разработчик математически опишет все процессы происходящие в окружающем его мире, то для реализации всех этих вычислений потребуется очень большие, по сегодняшним меркам, вычислительные ресурсы. Хотя бы потому, что все они должны происходить в реальном времени. Это потребует использования очень мощных микропроцессоров которых на данный момент не существует. Понятное дело, что наука не стоит на одном месте, специалисты в области нанотехнологий и микроэлектроники ищут все новые и новые пути решения тех или иных задач. Создаются новейшие центральные и видео процессоры, растет количество их ядер, совершенствуется внутренняя архитектура. Вспоминая революционные события прошлых лет, когда только происходил переход от первой шейдерной модели и фиксированного графического конвеера, к современным технологиям, казалось что еще немного, и компьютерное железо выйдет на такой уровень, когда  все вопросы по производительности исчезнут. Действительно многое изменилось в лучшую сторону, но вычислительной мощности как и раньше, попрежнему, не хватает. Одним из необходимых свойств, которыми должен обладать ваш рендер, является умение рисовать тени от света. Без теней о какой бы то нибыло реалистичности не может быть и речи. Вообще существует много различных способов создания тени. В этой главе мы рассмотрим создание и использование теней с помощью так называемого Stencil Buffer (буфер трафаретов). Что же такое буфер трафаретов? Я не непрасно начал разговор о новых технологиях в процессорном мире, и вот почему. Буфер трафаретов - это новшество которым стали обладать современные видеочипы. Он имеется у многих видеокарт и может отличаться друг от друга размерами.
Благодаря ему у нас появилась возможность быстро и качественно нарисовать тени от обьектов. Переходим к практической части. Создадим новый проект под названием myFirstWorld. Это будет не просто очередной пример, а совокупность рассмотренных ранее техник и примеров этой книги. Здесь мы будем использовать уже пройденый нами класс динамического террайна. Подключим класс летающей камеры, что бы полетать над сценой. Ну и конечно же разберемся с созданием и использованием теней. Первое что нам необходимо будет сделать - это проверить нашу видеокарту на наличие буфера трафаретов и выбрать его подходящий режим.
private static DepthFormat SelectStencilMode()
{
GraphicsAdapter adapter = GraphicsAdapter.DefaultAdapter;
SurfaceFormat format = adapter.CurrentDisplayMode.Format;
if (adapter.CheckDepthStencilMatch(DeviceType.Hardware,
                                  format,
                                  format,
                                  DepthFormat.Depth24Stencil8))
return DepthFormat.Depth24Stencil8;
else if (adapter.CheckDepthStencilMatch(DeviceType.Hardware,
                             format,
                             format,
                             DepthFormat.Depth24Stencil8Single))
return DepthFormat.Depth24Stencil8Single;
else if (adapter.CheckDepthStencilMatch(DeviceType.Hardware,
                                format,
                                format,
                                DepthFormat.Depth24Stencil4))
return DepthFormat.Depth24Stencil4;
else if (adapter.CheckDepthStencilMatch(DeviceType.Hardware,
                                format,
                                format,
                                DepthFormat.Depth15Stencil1))
return DepthFormat.Depth15Stencil1;
else
throw new InvalidOperationException("Could Not Find Stencil Buffer for Default Adapter");
}
Далее инициализируем графическое устройство и подключаем два игровых компонента Terrain и Camera.

public Game1()
{
graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth =
GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
graphics.PreferredBackBufferHeight =
GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
Aspect =
GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.AspectRatio;
Content.RootDirectory = "Content";
graphics.PreferredDepthStencilFormat = SelectStencilMode();
Terrain t = new Terrain(this);
Components.Add(t);
Camera c = new Camera(this);
Components.Add(c);
Scene s = new Scene(this);
Components.Add(s);
}
Далее добавляем новый игровой компонент который будет называться Scene. Выглядит он так.
    Model tractor;
    Matrix TractorWorld;
    Model mi24;
    Matrix mi24World;
    Model UAZ;
    Matrix UazWorld;
    Matrix[] transforms;
Переменные которые будут хранить наши модели сцены и их матрицы мира. Массив матриц трансформаций transforms для того чтобы модели правильно расположились в трехмерном пространстве с учетом внутренней иерархии самих моделей.

protected override void LoadContent()
    {
      tractor = Game.Content.Load<Model>("Models/tractor");
      transforms = new Matrix[tractor.Bones.Count];
      tractor.CopyAbsoluteBoneTransformsTo(transforms);
      TractorWorld = transforms[0] *
        Matrix.CreateRotationY(0.5f) *
        Matrix.CreateTranslation(new Vector3(150, 0, 100));
      mi24 = Game.Content.Load<Model>("Models/mi24");
      transforms = new Matrix[mi24.Bones.Count];
      mi24.CopyAbsoluteBoneTransformsTo(transforms);
      mi24World = transforms[0] *
        Matrix.CreateRotationY(-0.3f) *
        Matrix.CreateTranslation(new Vector3(120, 0, 150));
      UAZ = Game.Content.Load<Model>("Models/uaz");
      transforms = new Matrix[UAZ.Bones.Count];
      UAZ.CopyAbsoluteBoneTransformsTo(transforms);
      UazWorld = transforms[0] *
        Matrix.CreateRotationY(0.3f) *
        Matrix.CreateTranslation(new Vector3(140, 0, 150));
     
      base.LoadContent();
    }
В методе Load() мы загружаем модель и создаем позицию моделей с учетом трансформаций. Эти данные нам потребуются как для отрисовки самих моделей, так и их теней.

public override void Draw(GameTime gameTime)
    {
      DrawScene();
      DrawShadow();
      base.Draw(gameTime);
    }
Метод Draw() разбит на два внутренних метода. Один будет только рисовать модели другой рисовать их тени. Всё это можно делать и в одном методе но для понятности я эти действия разделил.
public void DrawScene()
    {    
      Game.GraphicsDevice.RenderState.CullMode = CullMode.None;
      foreach (ModelMesh mesh in tractor.Meshes)
      {
        foreach (BasicEffect effect in mesh.Effects)
        {
          effect.World = TractorWorld;
          effect.View = Camera.viewMatrix;
          effect.Projection = Camera.projectionMatrix;
          effect.EnableDefaultLighting();
        }
        mesh.Draw();
      }
     
      foreach (ModelMesh mesh in mi24.Meshes)
      {
        foreach (BasicEffect effect in mesh.Effects)
        {
          effect.World = mi24World;
          effect.View = Camera.viewMatrix;
          effect.Projection = Camera.projectionMatrix;
          effect.EnableDefaultLighting();
        }
        mesh.Draw();
      }

      foreach (ModelMesh mesh in UAZ.Meshes)
      {
        foreach (BasicEffect effect in mesh.Effects)
        {
          effect.World = UazWorld;
          effect.View = Camera.viewMatrix;
          effect.Projection = Camera.projectionMatrix;
          effect.EnableDefaultLighting();
        }
        mesh.Draw();
      }
    }
Как видите ничего нового тут нет. Стандартный способ отрисовки моделей с помощью BasicEffect. Запускаем приложение и видим нашу сцену пока что без теней.


Сразу бросается в глаза какая-то неестественность пейзажа. Иными словами не очень реалистично. Движемся далее.
void DrawShadow()
{
//Создаем матрицу тени
Matrix shadow = Matrix.CreateShadow(new Vector3(-50, -100, 40),
new Plane(new Vector3(0, 0.01f, 0),
new Vector3(500, 0.01f, 0),
new Vector3(0, 0.01f, 500)));
//Очищаем буфер трафаретов
Game.GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 0);
//Включаем буфер трафаретов
Game.GraphicsDevice.RenderState.StencilEnable = true;
// Вывод на экран элемента буфера в том случае, если он установлен в 0
Game.GraphicsDevice.RenderState.ReferenceStencil = 0;
Game.GraphicsDevice.RenderState.StencilFunction = CompareFunction.Equal;
//При выводе применяем операцию увеличения
Game.GraphicsDevice.RenderState.StencilPass = StencilOperation.Increment;
//Включаем альфа-смешивание для того, чтобы сделать тень полупрозрачной
//Выводим тень
foreach (ModelMesh mesh in tractor.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.AmbientLightColor = Vector3.Zero;
effect.Alpha = 0.5f;
effect.DirectionalLight0.Enabled = false;
effect.DirectionalLight1.Enabled = false;
effect.DirectionalLight2.Enabled = false;
effect.View = Camera.viewMatrix;
effect.Projection = Camera.projectionMatrix;
//При выводе тени умножаем мировую матрицу
//на матрицу вывода тени
effect.World = TractorWorld * shadow;
}
mesh.Draw();
}
foreach (ModelMesh mesh in mi24.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.AmbientLightColor = Vector3.Zero;
effect.Alpha = 0.5f;
effect.DirectionalLight0.Enabled = false;
effect.DirectionalLight1.Enabled = false;
effect.DirectionalLight2.Enabled = false;
effect.View = Camera.viewMatrix;
effect.Projection = Camera.projectionMatrix;
//При выводе тени умножаем мировую матрицу
//на матрицу вывода тени
effect.World = mi24World * shadow;
}
mesh.Draw();
}

foreach (ModelMesh mesh in UAZ.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.AmbientLightColor = Vector3.Zero;
effect.Alpha = 0.5f;
effect.DirectionalLight0.Enabled = false;
effect.DirectionalLight1.Enabled = false;
effect.DirectionalLight2.Enabled = false;
effect.View = Camera.viewMatrix;
effect.Projection = Camera.projectionMatrix;
//При выводе тени умножаем мировую матрицу
//на матрицу вывода тени
effect.World = UazWorld * shadow;
}
mesh.Draw();
}
//Отключаем буфер
Game.GraphicsDevice.RenderState.StencilEnable = false;
//Отключаем альфа-смешивание
Game.GraphicsDevice.RenderState.AlphaBlendEnable = false;
}

Вот результат с применением буфера трафаретов. Уже намного лучше. Только сейчас наши тени слишком черные. Если бы действия игры происходили где нибудь на луне, где нет атмосферы - то такие тени нам бы вполне подошли. Однако нам хотелось бы сделать тени полупрозрачными. Для этого воспользуемся настройками альфасмешивания.
Game.GraphicsDevice.RenderState.AlphaBlendEnable = true;
Game.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
Game.GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
В результате чего мы получили полупрозрачные тени.


Для большей понятности сути происходящего поясню. Stencil buffer это так называемая, проекция сцены на геометрическую фигуру Plane (прямоугольная плоскость не имеющая толщины). Вы заметили, что во время создания матрицы теней мы передаем параметрами позицию тени и создаем Plane на который эта тень будет спроецирована. Из плюсов такого подхода можно выделить простоту использования матрицы теней. Из минусов это то, что наши тени могут быть отброшены только на Plane. То есть тень не может быть отброшена саму модель. Для таких теней нам потребуется техника ShadowMapping. О ней я расскажу в следующих главах.

Практическое применение XNA. Часть 12.

Создание Terrain.


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


 


 Для начала немного теории. Многим из начинающих программистов игр первым делом приходит на ум использовать в качестве ландшафта готовую модель. Не скрою, сам таким был. Вообще создание и использование terraina задача не тривиальная и однозначно сказать какой способ наилучший нельзя. Всё зависит от задач которые ставятся перед разработчиком, и от свойств котрыми должен обладать ландшафт. Ну к примеру если вы будете созерцать землю из окна высокой башни, то действительно обойтись моделью да еще с низким содержанием полигонов, было бы разумно. Но если вы решили создать игру от первого лица, в которой вам предстоит перемещаться во всех направлениях по ландшафту, видеть его во всей своей красе (высокая детализация) да еще в придачу площадь этого самого ландшафта 100 квадратных километров, и переодически требуется получить высоту (координату) любой точки на поверхности земли хотя бы для того, что бы что то на землю поставить, тогда без динамического создания земли нам не обойтись. Вот об этом дальше речь и пойдет. Что вообще собой представляет terrain? Я бы его охарактеризовал так - это геометрическая фигура Plane (плоский квадрат или прямоугольник), имеющий сетчатую структуру с размером n х n,



с различными высотами вершин. Это как раз и имитирует неровности грунта.



Как создать такую сетку, теоретически, вам уже должно быть понятно. Так же как и любой примитив. К примеру вы решили создать terrain с размерами 100 х 100 вершин. Заметьте, я указал размер именно в вершинах, а не метрах или километрах, потому что генерировать я буду вершины, а так сказать условные километры будут зависеть от расстояния между вершинами. Для этого нам понадобится обычный цикл который выглядит примерно так
for(int x = 0; x < 100;  x ++)
{
   for(int z = 0; z < 100; z ++)
   {
      ...new Vector3(x, ..., z);
   }
}
где переменные x и z соответствуют координатам по оси X и Z. Пока, что это очень общий принцип создания сетки, но он необходим для понимания азов. Теперь если ширину и глубину мы получили, то как нам быть с самой интересной величиной - высотой. Для реализации этой задачи существует множество различных способов. Можно например, написать генератор случайных велечин в неком диапазоне высот, и поставить каждой вершине случайную высоту, но предсказать итоговую геометрию будет не просто. Вероятнее всего это будет некий ёжик, которого после десятка интерполяций удастся сгладить, но управлять формой ландшафта будет не так то просто. Я предлагаю пойти иным путем. Наверное многим бы пришелся по душе способ, в котором бы вы просто нарисовали на экране какого нибудь графического редактора желаемую местность, а именно вид сверху, а в игре получили задуменный рельеф. Что захотели то и получили. Или еще лучше, взяли фотографию с самлёта или спутника какой то понравившейся части земли, и довольно достоверно ее повторили у себя в игре. Принцип такого волшебного способа весьма прост. Это использование так называемой карты высот, ее роль играет обыкновенный графический файл в .bmp формате.



 Суть этого принципа заключается в том, чтобы использовать в качестве значения высоты цветовое значение точки изображения. Как вы наверняка уже знаете, цвет в компьютере представлен структурой состоящей из четырех переменных.
Color = {R, G, B, A};
R - хранит информацию о красной составляющей цвета, или правильней будет сказать это красный канал.
G - канал зеленой составляющей.
B - канал синей составляющей.
A - это канал прозрачности или альфа канал, о котором мы упоминали в предыдущих главах. Любой из этих каналов хранит значение в диапазоне от 0 до 255. Для сравнения истинно черный и истинно белый цвета выглядят так:
Color.Black = {0, 0, 0, 0};
Color.White = {255, 255, 255, 0};
В нашем подходе мы можем использовать любой из цветовых каналов при условии что карта высот чернобелая.
Как видите все эти цвета ничто иное как оттенки серого, а значит что любая цветовая точка или пиксел взятая из чернобелого изображения всегда определяется выражением R=G=B, а это и означает то, что изображение должно быть черно-белое. Движемся далее. Для большей наглядности принято считать темные участки карты высот впадинами и низинами ассоциирующиеся с плохой освещенностью, а возвышенности закрашиваются светлыми тонами и ассоциируются с хорошим освещением или заснеженными горными вершинами. Так и для восприятия понятней, да и значение точки чем светлее тем выше, то есть в нашем случае данные именно в том формате в каком нам и требуется. Теперь нам необходим механизм, который считает данные с нашего битмапа и заполнит ими значения в массиве позиций вершин будущего terraina. Плавно переходим от теории к практике. Создадим новый проект под названием myTerrain. Но в отличии от всех предыдущих примеров, для создания ландшафта мы используем оттдельный класс, а точнее  Microsoft.Xna.Framework.DrawableGameComponent
Теперь подробнее что это такое и зачем нам все это нужно. Во первых GameComponent и DrawableGameComponent это два игровых класса (компонента) которые имеют такие же как и главный класс методы, а именно Load(), Update() но метод Draw() имеет только DrawableGameComponent. Вызов этих методов происходит из екстернального кода, можно сказать автоматически без вашего участия, вам лишь остаётся в этих методах написать свой код. Для того чтобы создать новый игровой компонент вам надо в вновь созданом проекте, правой кнопкой мышки сделать следующие манипуляции
myTerrain > Добавить > Компонент. Далее в появившемся окне выбрать категорию XNA GameStudio 3.1 в левой части формы, а в правой части выбрать шаблон Game Component. Указать название игровому компоненту и нажать кнопку [Добавить]. После чего в ваш проект будет добавлен класс игрового компонента. Важно сказать, что игровой компонент это самостоятельный модуль, который вы сможете потом использовать в любых других проектах XNA Game Studio, если добавите его в проект.

После проделаных операций в вашем проекте должен появиться класс Terrain. Он уже будет иметь набор необходимых методов, но эти методы пока еще ни откуда не вызываются. Для того что бы это исправить, мы должны вновь созданный компонент добавить в коллекцию компонентов нашей игры, после чего они непременно начнут вызываться. Делается это так. В методе Initialize() мы создадим экземпляр игрового компонента, а затем добавим в коллекцию компонентов.
    protected override void Initialize()
    {
      Terrain terrain = new Terrain(this);
      Components.Add(terrain);
      base.Initialize();
    }
Вот после этого все начнет работать как надо. Теперь переходим собственно к самому изготовления terraina. Для начала явно добавляем в проект файл HeightMap.bmp по традиции в папку Content. Это наша карта высот. Еще из ресурсов нам понадобится текстура травы которой мы раскрасим нашу землю. Для этого добавим в папку Content файл grass.dds. Далее код который будет находться именно в компоненте Terrain.
    private int WIDTH;
    private int HEIGHT;
    private VertexBuffer vertexBuffer;
    private IndexBuffer indexBuffer;
    private VertexDeclaration vertexDecalaration;
    private float[,] heightData;
    private BasicEffect effect;
    Texture2D texture;
Здесь вы видите уже знакомые вам классы по урокам создания примитивов. Имеются также переменные которые будут хранить ширину и высоту карты высот, и двухмерный массив heightData[,] в котором как видно из названия будут храниться все высоты нашего ландшафта. Создаваться terrain будет в три этапа. Первым делом мы считаем данные из карты высот, и запишем их в массив heightData[,]. Вторым этапом будет создание вершин на основе данных из heightData[,],и записывания их в вершинный буфер.  Третьим по счету, будет создание всех индексов и записываниие их в буфер индексов. Для этого мы используем три отдельных метода, и вызовим их в той последовательности, что оговорили.
    public override void Initialize()
    {
      effect = new BasicEffect(Game.GraphicsDevice, null);
      texture = Game.Content.Load<Texture2D>("grass");
      LoadHeightData(Game.Content.Load<Texture2D>("HeightMap"));
      SetUpTerrainVertices(Game.GraphicsDevice);
      SetUpTerrainIndices(Game.GraphicsDevice);
      vertexDecalaration = new VertexDeclaration(Game.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
      base.Initialize();
    }

Теперь рассмотрим по отдельности каждый метод. Начнем с загрузки данных о высотах.
    private void LoadHeightData(Texture2D heightMap)
    {
      // установили высоту и
      // ширину карты высот в
      // глобальные переменные
      WIDTH = heightMap.Width;
      HEIGHT = heightMap.Height;
      // создали массив цветов для хранения
      // данных карты высот с размерностью
      // высоты умноженной на ширину
      Color[] heightMapColors = new Color[WIDTH * HEIGHT];
      // загружаем данные из карты высот
      // в массив цветов
      heightMap.GetData(heightMapColors);
      // создали массив значений высот
      heightData = new float[WIDTH, HEIGHT];
      // в цикле
      for (int x = 0; x < WIDTH; x++)
      {
        for (int y = 0; y < HEIGHT; y++)
        {
          // если карта высот ч/б то не важно из какого канала
          // брать значение. Мы возьмем из красного
          heightData[x, y] = heightMapColors[x + y * WIDTH].R;
        }
      }
    }
Метод создания вершинного буффера.
    public void SetUpTerrainVertices(GraphicsDevice device)
    {
      // создаем массив вершин с размерностью
      // количества всех точек изображения
      VertexPositionNormalTexture[] vertices =
        new VertexPositionNormalTexture[WIDTH * HEIGHT];
      for (int x = 0; x < WIDTH; x++)
        for (int y = 0; y < HEIGHT; y++)
        {
          // устанавливаем значение высоты в Y
          // каждого вектора позиции вершины
          // делением на 5 регулируем амплитуду
          // высоты
          vertices[x + y * WIDTH].Position = new Vector3(x, -heightData[x, y] / 5, y);
          vertices[x + y * WIDTH].Normal = new Vector3(0, 0, 1);
          // устанавливаем координаты текстуры делением
          // на 50 определяем мозаичность (tiling) текстуры
          vertices[x + y * WIDTH].TextureCoordinate.X = (float)x / 50.0f;
          vertices[x + y * WIDTH].TextureCoordinate.Y = (float)y / 50.0f;
        }
      // создаем вершинный буффер
      // на основе полученного массива
      vertexBuffer =
          new VertexBuffer(device, typeof(VertexPositionNormalTexture),
                           VertexPositionNormalTexture.SizeInBytes * WIDTH * HEIGHT, BufferUsage.Points);
      // записываем в него данные
      vertexBuffer.SetData(vertices);
    }

Метод создания индексного буффера.
    private void SetUpTerrainIndices(GraphicsDevice device)
    {
      short[] indices = new short[(WIDTH - 1) * (HEIGHT - 1) * 6];
      for (int x = 0; x < WIDTH - 1; x++)
      {
        for (int y = 0; y < HEIGHT - 1; y++)
        {
          indices[(x + y * (WIDTH - 1)) * 6] = (short)((x + 1) + (y + 1) * WIDTH);
          indices[(x + y * (WIDTH - 1)) * 6 + 1] = (short)((x + 1) + y * WIDTH);
          indices[(x + y * (WIDTH - 1)) * 6 + 2] = (short)(x + y * WIDTH);
          indices[(x + y * (WIDTH - 1)) * 6 + 3] = (short)((x + 1) + (y + 1) * WIDTH);
          indices[(x + y * (WIDTH - 1)) * 6 + 4] = (short)(x + y * WIDTH);
          indices[(x + y * (WIDTH - 1)) * 6 + 5] = (short)(x + (y + 1) * WIDTH);
        }
      }
      indexBuffer = new IndexBuffer(device, sizeof(short) * indices.Length, BufferUsage.WriteOnly, IndexElementSize.SixteenBits);
      indexBuffer.SetData(indices);
    }
Ну и наконец метод отрисовки всего этого с использованием BasicEffect.
    public override void Draw(GameTime gameTime)
    {
      effect.World = Matrix.CreateTranslation(0, 0, 0);
      effect.View = Matrix.CreateLookAt(new Vector3(100, 400, -250), new Vector3(100, 0, 100), Vector3.Up);
      effect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(18), 1.3f, 0.1f, 1000);
      effect.TextureEnabled = true;
      effect.Texture = texture;
      effect.Begin();
      foreach (EffectPass pass in effect.CurrentTechnique.Passes)
      {
        pass.Begin();
        Game.GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes);
        Game.GraphicsDevice.Indices = indexBuffer;
        Game.GraphicsDevice.VertexDeclaration = new VertexDeclaration(Game.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
        Game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, WIDTH * HEIGHT, 0, (WIDTH - 1) * (HEIGHT - 1) * 2);
        pass.End();
      }
      effect.End();
      base.Draw(gameTime);
    }
И в главном классе в методе Draw() добавим следующие строки

    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.CornflowerBlue);
      GraphicsDevice.RenderState.CullMode = CullMode.None;
      base.Draw(gameTime);
    }

Здесь вам должно быть всё знакомо по нашим предидущим примерам. В результате всей проделанной работы мы теперь можем посмотреть на на обьемную модель ландшафта созданную на основе плоской карты высот. Ниже для сравнения приводятся две иллюстрации.





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


  
 Вот таким симпатичным снимком мы и заканчиваем знакомство с понятием terraina. Теперь, я надеюсь, вам понятны базовые принципы динамического создания ландшафтов.

Практическое применение XNA. Часть 11.

Instancing.


В этой главе, мы с вами рассмотрим технологию отображения объектов имеющую название instancing, что в переводе с английского означает "отдельный". Суть этой техники такова, что за один вызов метода Draw(), мы отрисовуем не один обьект, как например наш куб, а множество таких же кубов или чего либо еще. Вот здесь важно сразу понять разницу и преимущество техники instancing. Pечь идет о том, что бы за единственный вызов метода Draw() получить к примеру 10000 моделей на экране. Как же такое возможно, спросите вы? А возможно такое стало с приходом видеокарт, которые поддерживают третью шейдерную модель, именно в ней за один вызов драйвера. Т.е., нам достаточно один раз вызвать DrawIndexedPrimitives(…) и получить на экране не однин, а несколько mesh’ей, расположенных по разным координатам и повернутых под разными углами.
Многие современные игры с помощью этой техники рисуют такие объекты как трава, астероиды и прочие множественные модели, счет которых может переваливать за тысячи экземпляров, и сами понимаете, если каждый раз вызывать Draw(), то нарисовав одну лишь траву, количество кустов которой например 10000, FPS (количество кадров в секунду) вашей игры может сильно пострадать. Что бы избежать подобных проблем, на помощь придет техника instancing.
 Для более глубокого изучения этой замечательной техники заведем новый проект под названием instancing. В нем мы создадим уже знакомый нам примитив, куб с помощью VertexPositionColored вершин и нарисуем его 10000 раз, и так начнем.
Глобальными переменными у нас будут выступать:


        private VertexDeclaration MyVertexDeclaration;
        private VertexPositionColor[] myVertices;
        private int[] myIndices;
        private VertexBuffer myVertexBuffer;
        private IndexBuffer myIndexBuffer;
        private Effect myEffect;
        private Matrix[] myInstansedWorld;
        private const int sizeofMatrix = sizeof(float) * 16;
        private int instanceDataSize;


Их назначение понятно из названия если вы внимательно ознакомились с предыдущими примерами, тем более что они уже использовались нами ранее. Далее создадим наш куб:


protected override void Initialize()
        {
          myEffect = Content.Load<Effect>("Instancing");
          //Создаем свой VertexDeclaration
          VertexElement[] elements = GetVertexElementVertexPositionColorMatrix();
          MyVertexDeclaration = new VertexDeclaration(graphics.GraphicsDevice, elements);
          // Строим треугольник и создаем вершинный и индексный буфер
          myVertices = new VertexPositionColor[8];
          myIndices = new int[36];

В методе Update() для большей наглядности мы пересчитываем наши матрицы мира так, что бы кубики вращались.

         protected override void Update(GameTime gameTime)
        {
          if(Keyboard.GetState().IsKeyDown(Keys.Escape))
          {
            this.Exit();
          }
          float ang = 0.001f;
          Matrix matrRot = Matrix.CreateRotationX(ang * (float)gameTime.ElapsedGameTime.TotalMilliseconds) *
                           Matrix.CreateRotationY(ang * (float)gameTime.ElapsedGameTime.TotalMilliseconds) *
                           Matrix.CreateRotationZ(ang * (float)gameTime.ElapsedGameTime.TotalMilliseconds);
          for (int i = 0; i < myInstansedWorld.Length; i++)
          {
            myInstansedWorld[i] = matrRot * myInstansedWorld[i];
          }
            base.Update(gameTime);
        }

 Далее следует самый главный метод Draw(). В нем следует особое внимание обратить на две новые сущности

DynamicVertexBuffer instanceDataStream
VertexStreamCollection vertexCollection

Благодаря этим классам и реализуется наша техника.

protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            // настримваем вывод
            graphics.GraphicsDevice.VertexDeclaration = MyVertexDeclaration;
            graphics.GraphicsDevice.Indices = myIndexBuffer;
            //graphics.GraphicsDevice.RenderState.DepthBufferEnable = true;
            // инициируем динамический вершинный буфер - поток с данными о матрицах
            DynamicVertexBuffer instanceDataStream = new DynamicVertexBuffer(graphics.GraphicsDevice, instanceDataSize, BufferUsage.WriteOnly);
            instanceDataStream.SetData(myInstansedWorld, 0, myInstansedWorld.Length, SetDataOptions.Discard);
            // назначаем вершинные буферы нашего GraphicsDevice
            VertexStreamCollection vertexCollection = graphics.GraphicsDevice.Vertices;
            vertexCollection[0].SetSource(myVertexBuffer, 0, VertexPositionColor.SizeInBytes);
            vertexCollection[0].SetFrequencyOfIndexData(myInstansedWorld.Length);
            vertexCollection[1].SetSource(instanceDataStream, 0, sizeofMatrix);
            vertexCollection[1].SetFrequencyOfInstanceData(1);
            // настраиваем effect
            myEffect.Parameters["matVP"].SetValue(Matrix.CreateLookAt(new Vector3(-40f, 40f, 40f), new Vector3(0f, 15f, 0f), new Vector3(0f, 1f, 0f)) *
                                                  Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(30), 1.5f, 0.01f, 2000.0f));           
            // стандартный вывод
            myEffect.Begin();
            foreach (EffectPass pass in myEffect.CurrentTechnique.Passes)
            {
              pass.Begin();
              graphics.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, myVertices.Length, 0, myIndices.Length);
              pass.End();
            }
            myEffect.End();
            // освобождаем поток с данными о матрицах
            instanceDataStream.Dispose();
            // очищаем вершинные буферы нашего GraphicsDevice
            vertexCollection[0].SetSource(null, 0, 0);
            vertexCollection[1].SetSource(null, 0, 0);
            base.Draw(gameTime);
        }
Запускаем программу и видим желаемый результат



Еще нужно упомянуть и о шейдере, с помощью которого мы осуществили вывод изображение
float4x4 matVP  : VIEWPROGECTION;
struct VS_INPUT
{
    float4  Position  : POSITION0;
    float4  Color   : COLOR0;
    float4x4 InstanceWorld : TEXCOORD0;
};
struct VS_OUTPUT
{
    float4  Position  : POSITION0;
    float4  Color   : COLOR0;
};
VS_OUTPUT VS(VS_INPUT input)
{
 VS_OUTPUT output = (VS_OUTPUT)0;
    output.Position = mul(input.Position, mul(transpose(input.InstanceWorld), matVP));
    output.Color = input.Color;
    return output;
}
struct PS_INPUT
{
 float4 Position  : POSITION0;
 float4 Color  : COLOR0;
};
float4 PS(PS_INPUT input) : COLOR0
{
 return input.Color;
}
technique Instancing
{
    pass Pass1
    {
        VertexShader = compile vs_3_0 VS();
        PixelShader  = compile ps_3_0 PS();
    }
}

как видите он достаточно прост. Здесь вам все должно быть знакомо, за исключением одного момента. Обратите внимание на стуруктуру VS_INPUT, а конкретно на поле:

{… float4x4 InstanceWorld:TEXCOORD0;}

Данное поле и будет у нас отвечать за получение матрицы мира для конкретной вершины.
Для того, что бы определить выигрыш по производительности этого подхода, нам понадобится сравнение FPS двух сцен с использованием instancing и без него. Для того чтобы измерить FPS нам понадобится слегка дополнить наш код следующими строками:

        int frameRate = 0;
        int frameCounter = 0;
        TimeSpan elapsedTime = TimeSpan.Zero;
        string gameName;
Добавили глобальные переменные с которыми будем проводить операции. Далее в методе Update()
          elapsedTime += gameTime.ElapsedGameTime;
          if (elapsedTime > TimeSpan.FromSeconds(1))
          {
            elapsedTime -= TimeSpan.FromSeconds(1);
            frameRate = frameCounter;
            frameCounter = 0;
          }


щитаем количество кадров в секунду и в методе Draw() выводим это значение в Title нашего окна.
            frameCounter++;
            string fps = frameRate.ToString();
            this.Window.Title = gameName + "   " + fps + "fps";
в результате частота обновления буффера составила около 430 кадров в секунду.


Неплохой результат. Но и это еще не предел для нашего примера. Если вспомнить, что в методе Update() мы производим пересчет всех матриц мира для вращения, то становится понятно что перемножение 10000 матриц не может не украсть драгоценный FPS. Если это убрать, что же теперь получится? К стати показатели из моей статьи могут резко отличаться от ваших. И происходить это будет неизбежно, в зависимости от конфигурации вашего компьютера. Само собой разумеется, чем мощнее видеокарта, тем выше будут ваши значения. Важно знать, что в среде разработки XNA, создатели платформы зафиксиовали FPS по умолчанию равной около 60 кадров в секунду. Этого значения хватает для комфортного созерцания картинки на экране. Переопределенный метод


protected override void Draw(GameTime gameTime)


не вызывается чаще установленного значения, экономя тем самым ресурсы компьютера, ведь они как воздух нужны для обработки игровых данных подобных физике или искусственному интеллекту. К тому же XNA если помните, ориентирована на консольную приставку XBOX 360, а в ней есть ряд обязательных требований по графике. Что бы снять ограничения по FPS и увидеть максимально возможное значение надо написать две строчки в конструкторе класса:


IsFixedTimeStep = false;


говорит о том, что мы хотим фиксированый временной шаг. И


graphics.SynchronizeWithVerticalRetrace = false;


говорит о том, что мы выключили вертикальную синхронизацию.
Всё это необходимо сделать, иначе FPS, будет держаться на отметке 60.
 После всех изменений нашем случае получился прирост FPS около 780 кадров в секунду.
Как видите вообще отлично, на таком количестве обьектов получить такую FPS. И на конец самое интересное. Изменим наш код так, что бы модели рисовались в цикле, так же как в предидущих примерах. Закомментарим содержимое метода Draw() кроме кода отвечающего за вывод значения FPS, и напишем следующее:

GraphicsDevice.Clear(Color.CornflowerBlue);
          GraphicsDevice.VertexDeclaration = MyVertexDeclaration;
          for (int i = 0; i < 10000; i++)
          {
            eff.World = myInstansedWorld[i];
            eff.View = Matrix.CreateLookAt(new Vector3(-40f, 40f, 40f), new Vector3(0f, 15f, 0f), new Vector3(0f, 1f, 0f));
            eff.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(30), 1.5f, 0.01f, 2000.0f);
            eff.VertexColorEnabled = true;
            eff.Begin();
            foreach (EffectPass pass in eff.CurrentTechnique.Passes)
            {
              pass.Begin();
              graphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, myVertices, 0, 8, myIndices, 0, 12);
              pass.End();
            }
            eff.End();
          }


Стартуем и смотрим на результат = 130;
Комментарии, как говорится, излишни. Где можно использовать instancing ( в нашем случае правильнее будет сказать hardware instancing ), потому что для реализации данной техники использовались исключительно ресурсы видеокарты!

Наверняка, помимо отрисовки примитивов с помощью instancinga, вам понадобится умение рисовать и готовые модели. Об этом вторая половина этой главы. Как всегда создадим новый проект с названием myInstancedModels. Нам понадобятся такие переменные

    public List<InstancedModelPart> ModelParts = new List<InstancedModelPart>();
    private List<Matrix> instanceMatrices;
    private Model myModel;
    private int myModelCount = 10000;
    private Effect instancedEffect;
    private Texture2D myTexture;

Метод Load() выглядит таким образом

    protected override void LoadContent()
    {
      spriteBatch = new SpriteBatch(GraphicsDevice);
      instancedEffect = Content.Load<Effect>("InstancedModel");
      instanceMatrices = new List<Matrix>();
      myTexture = Content.Load<Texture2D>("10");
      myModel = Content.Load<Model>("box");
      foreach (ModelMesh mesh in myModel.Meshes)
      {
        foreach (ModelMeshPart part in mesh.MeshParts)
        {
          ModelParts.Add(new InstancedModelPart(this, part, mesh.VertexBuffer, mesh.IndexBuffer));
        }
      }
      for (int X = 0; X < Math.Sqrt((double)myModelCount); X++)
      {
        for (int Z = 0; Z < Math.Sqrt((double)myModelCount); Z++)
        {
          instanceMatrices.Add(Matrix.CreateScale(0.1f) * Matrix.CreateTranslation(new Vector3(X * 2, 0, Z * 2)));
        }
      }
    }
здесь мы последовательно загрузили наш эффект, с помощью которого непосредственно будет происходить отрисовка моделей. Далее создается список мировых матриц, ктотрый будет хранить данные для каждой из множества моделей. Далее идет загрузка текстуры, которую мы хотим видеть на нашей модели, и сама модель. После этого запускается цикл в котором мы заполняем список ModelParts данными из модели. Как вы уже убедились ранее, каждая модель состоит из сетки, каждая сетка из треугольников (шестиугольные и восьмиугольные сетки мы пока не рассматриваем). Для того, что бы видеокарта которая как мы выяснили кроме точек, линий, и как результат того и другого - треугольников, в принципе рисовать ничего не умеет, смогла нарисовать модель, ей как и вслучае с примитивами необходим тот же вершинный буфер и буфер индексов. Где же их взять для нашей модели, ведь руками написать их задача не для слабонервных,тем более, если модель имеет сложную форму не такую как куб, а например, как тело человека. Ответ заключается в том, что все эти данные хранятся в самой модели, а попадают они туда в процессе создания во всевозможных генераторах геометрии таких как 3DS Max и тд. Наша задача их оттуда получить. Для этого мы создадим отдельный класс InstancedModelPart ,да и так будет нагляднее показан процесс изьятия данных.

    public InstancedModelPart(Game game, ModelMeshPart part, VertexBuffer vertexBuffer, IndexBuffer indexBuffer)
    {
      this.game = game;
      //передаем данные из параметров в переменые
      _primitiveCount = part.PrimitiveCount;
      _vertexCount = part.NumVertices;
      _vertexStride = part.VertexStride;
      _vertexDeclaration = part.VertexDeclaration;
      _vertexBuffer = vertexBuffer;
      _indexBuffer = indexBuffer;
      originalVertexDeclaration = part.VertexDeclaration.GetVertexElements();
      VertexElement[] extraElements = new VertexElement[4];
      short offset = 0;
      byte usageIndex = 1;
      short stream = 1;
      //получаем рвзмер Vector4 в байтах
      short sizeOfVector4 = sizeof(float) * 4;
      for (int i = 0; i < extraElements.Length; i++)
      {
        extraElements[i] = new VertexElement(stream, offset, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, usageIndex);
        offset += sizeOfVector4;
        usageIndex++;
      }
      _vertexDeclaration.Dispose();
      int length = originalVertexDeclaration.Length + extraElements.Length;
      VertexElement[] elements = new VertexElement[length];
      originalVertexDeclaration.CopyTo(elements, 0);
      extraElements.CopyTo(elements, originalVertexDeclaration.Length);
      _vertexDeclaration = new VertexDeclaration(game.GraphicsDevice, elements);
    }
Далее в методе Load() запускается еще один цикл, который заполняет список instanceMatrices матрицами мира так, что бы получился квадрат. Math.Sqrt((double)myModelCount)нужен для того, чтобы указав общее количество моделей, которое хотим увидеть, в данном случае

 private int myModelCount = 10000;

у нас получился квадрат. Далее следует главный метод Draw().

    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.CornflowerBlue);
      instancedEffect.CurrentTechnique = instancedEffect.Techniques["HardwareInstancing"];
      instancedEffect.Parameters["View"].SetValue(Matrix.CreateLookAt(new Vector3(100, 100, -100), new Vector3(100, -50, 200), Vector3.Up));
      instancedEffect.Parameters["Projection"].SetValue(Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(40), 1.6f, 0.1f, 1000f));
      instancedEffect.Parameters["Texture"].SetValue(myTexture);
      foreach (InstancedModelPart part in ModelParts)
      {
        GraphicsDevice.VertexDeclaration = part.VertexDeclaration;
        GraphicsDevice.Vertices[0].SetSource(part.VertexBuffer, 0, part.VertexStride);
        GraphicsDevice.Indices = part.IndexBuffer;
        instancedEffect.Begin();
        foreach (EffectPass pass in instancedEffect.CurrentTechnique.Passes)
        {
          pass.Begin();
          Matrix[] subList = new Matrix[myModelCount];
          instanceMatrices.CopyTo(0, subList, 0, myModelCount);
          const int sizeofMatrix = sizeof(float) * 16;
          int instanceDataSize = sizeofMatrix * subList.Length;
          DynamicVertexBuffer instanceDataStream = new DynamicVertexBuffer(GraphicsDevice, instanceDataSize, BufferUsage.WriteOnly);
          instanceDataStream.SetData(subList, 0, subList.Length, SetDataOptions.Discard);
          VertexStreamCollection vertices = GraphicsDevice.Vertices;
          vertices[0].SetFrequencyOfIndexData(subList.Length);
          vertices[1].SetSource(instanceDataStream, 0, sizeofMatrix);
          vertices[1].SetFrequencyOfInstanceData(1);
          GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, part.VertexCount, 0, part.PrimitiveCount);
          instanceDataStream.Dispose();
          vertices[0].SetSource(null, 0, 0);
          vertices[1].SetSource(null, 0, 0);
          pass.End();
        }
        instancedEffect.End();
      }
      base.Draw(gameTime);
    }
Здесь практически всё вам уже известно. Те же DynamicVertexBuffer и  VertexStreamCollection ну и конечно же
GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, part.VertexCount, 0, part.PrimitiveCount);
В качестве модели которую будем "размножать", был выбран куб




В результате всего вышесказанного получится изображение



состоящее из 10000 обьектов построеных, как и планировалось, квадратом.
А на последок скажу пару слов о шейдере

// Настройки для камеры
float4x4 View;
float4x4 Projection;
// Это приложение использует модель освещения Ламберта
float3 LightDirection ;
float3 DiffuseLight = 1;
float3 AmbientLight = 1;
texture Texture;
sampler Sampler = sampler_state
{
    Texture = (Texture);
   
    MinFilter = Linear;
    MagFilter = Linear;
    MipFilter = Linear;   
    AddressU = Wrap;
    AddressV = Wrap;
};
struct VertexShaderInput
{
    float4 Position : POSITION0;
    float3 Normal : NORMAL0;
    float2 TextureCoordinate : TEXCOORD0;
};
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float4 Color : COLOR0;
    float2 TextureCoordinate : TEXCOORD0;
};
VertexShaderOutput VertexShaderCommon(VertexShaderInput input, float4x4 instanceTransform)
{
    VertexShaderOutput output;


//Применяя мировуюматрицу и матрицы камеры расчитываем позицию в //пространстве.
    float4 worldPosition = mul(input.Position, instanceTransform);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
    // Расчитываем свет
    float3 worldNormal = mul(input.Normal, instanceTransform);   
    float diffuseAmount = max(-dot(worldNormal, LightDirection), 0);   
    float3 lightingResult = saturate(diffuseAmount * DiffuseLight + AmbientLight);   
    output.Color = float4(lightingResult, 1);
    // Копируем текстурные координаты
    output.TextureCoordinate = input.TextureCoordinate;
    return output;
}
VertexShaderOutput HardwareInstancingVertexShader(VertexShaderInput input, float4x4 instanceTransform : TEXCOORD1)
{
    return VertexShaderCommon(input, transpose(instanceTransform));
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return tex2D(Sampler, input.TextureCoordinate) * input.Color;
}
technique HardwareInstancing
{
    pass Pass1
    {
        VertexShader = compile vs_3_0  HardwareInstancingVertexShader();
        PixelShader = compile ps_3_0
 PixelShaderFunction();
    }
}
Шейдер довольно простой и предназначен только лишь для отображения модели без каких либо преобразований, использующий модель освещения Ламберта (о ней я ранее уже упоминал). Особого внимания требует лишь float4x4 instanceTransform. Именно сюда приходят данные о мировых матрицах для обработки.
На этом мы заканчиваем знакомство с Hardware Instancing. Думаю вы оцените эту технику и найдете ей применение.      

понедельник, 9 апреля 2012 г.

Lightning Models

Для того, что бы игры выглядели более реалистичными, необходимо по максимуму имитировать в них физические процессы происходящие в реальной жизни. Одним из таких процессов является - освещение. Далее мы разберем несколько наиболее известных моделей освещения применяемых в современной игровой индустрии.

Lambert Lighthing Model

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

I = max(0, dot(N, L));

где:
I - итоговый коэффициент затенения,
N - нормаль полигона в мировом пространстве,
L - направление света.

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

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
   const float3 lightDir = float3(1, 0, 1);
   float LdotN = max(0.0, dot(lightDir, input.Normal));
   return float4(0.5, 0.5, 0.5, 1) * LdotN;
}



Исходный код к статье  lambert.zip



Phong Lightning Model

Следующая модель освещения - это бликовая модель освещения Фонга.
В отличии от модели Ламберта, она не только учитывает нормали, но так-же учитывает отражение света по отношению к наблюдателю.
Код пиксельного шейдера приведен ниже:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    const float3 lightDir = float3(1, 0, 1);
    const float3 viewDir = normalize(float3(0, -75, 0));
    float LdotN = dot(lightDir, input.Normal);
    float3 Reflection = normalize(2.0f * input.Normal * LdotN - lightDir);
    float RdotV = max(0.0, dot(Reflection, viewDir));
    float SpecExp = 50;
    float4 SpecColor = float4(1, 1, 1, 1);
    float4 TotalSpecular = SpecColor * pow(RdotV, SpecExp);
    return float4(0.5, 0.5, 0.5, 1) * saturate(max(0.0, LdotN)) + TotalSpecular;
}

Исходный код к статье  PhongModel.zip


Normal + Specular Mapping


 Совмещая бликовую и диффузную модели освещения, можно получать более сложные эффекты. Одним из таких эффектов является Normal Mapping. Мы уже знаем что для расчета диффузного освещения нужно использовать дот продукт векторов нормали и направления света. Нормали обычно хранятся в самой модели. Можно сказать, что для одного полигона
(треугольника) нормаль расчитывается как среднее арифметическое из трех нормалей вершин полигона. Соответственно, детализация объекта ограничивается сложностью модели. Для того что-бы придать модели более детализированный вид, был придуман следующий способ. Художники сохраняли нормали в текстуру, и после текстурирования такой текстурой модели, нужно считать нормаль из нее и использовать для расчета освещения. В результате, можно добиться высокой визуальной сложности моделей, не затрагивая ее сетки.



Реализацию этой техники можно найти здесь SpecularBump.zip

четверг, 15 марта 2012 г.

How To Make A Game

In this blog I will talk about how to create a computer game step by step using the Microsoft XNA. I start my story from the simple to the complex. And at the end of the story we will create a full 3D game.






As a result, we get this game:



 So let's get started and good luck to everyone who wants to learn how to create games with their own hands.

Menu Creation

Our future game would consist of three main modules. This menu, a physical module and render. First, we look at how to create a menu for our, as well as for any other game. To run the menu, we need to create the following classes:

AnimeFont.cs;
BackGround.cs;
Button.cs;
Cursor.cs;
Delegates.cs;
MainMenu.cs;

From the title it should be clear that each class will do.

namespace _MENU_
{
  class AnimeFont : DrawableGameComponent
  {
    SpriteBatch spriteBatch;
    private Texture2D Texture1;
    int PositionX1;
    int PositionY1;
    Rectangle Rectangle1;
    private Texture2D Texture2;
    int PositionX2;
    int PositionY2;
    Rectangle Rectangle2;
    private Texture2D Texture3;
    int PositionX3;
    int PositionY3;
    Rectangle Rectangle3;
    private Texture2D Texture4;
    int PositionX4;
    int PositionY4;
    Rectangle Rectangle4;
    private Texture2D gameName;
    public AnimeFont(Game game) : base(game)
    {
      spriteBatch = new SpriteBatch(Game.GraphicsDevice);
      Texture1 = Game.Content.Load<Texture2D>(@"Menu/RB1");
      PositionX1 = game.GraphicsDevice.Viewport.Width - Texture1.Width;
      PositionY1 = game.GraphicsDevice.Viewport.Height - Texture1.Height / 2;
      Texture2 = Game.Content.Load<Texture2D>(@"Menu/RB2");
      PositionX2 = -game.GraphicsDevice.Viewport.Width / 2;
      PositionY2 = game.GraphicsDevice.Viewport.Height - Texture2.Height * 2;
      Texture3 = Game.Content.Load<Texture2D>(@"Menu/RB3");
      PositionX3 = game.GraphicsDevice.Viewport.Width - Texture3.Width / 2;
      PositionY3 = 100;
      Texture4 = Game.Content.Load<Texture2D>(@"Menu/RB4");
      PositionX4 = 50;
      PositionY4 = -Game.GraphicsDevice.Viewport.Height;
      gameName = Game.Content.Load<Texture2D>(@"Menu/GameName");
    }
    public override void Initialize()
    {
      base.Initialize();
    }
    public override void Update(GameTime gameTime)
    {
      PositionY1--;
      Rectangle1 = new Rectangle(PositionX1, PositionY1, Texture1.Width, Texture1.Height);
      if (PositionY1 < -800)
      {
        PositionY1 = Game.GraphicsDevice.Viewport.Height - Texture1.Height / 6;
      }
      PositionX2++;
      Rectangle2 = new Rectangle(PositionX2, PositionY2, Texture2.Width, Texture2.Height);
      if (PositionX2 > Game.GraphicsDevice.Viewport.Width)
      {
        PositionX2 = -Game.GraphicsDevice.Viewport.Width / 2;
      }
      PositionX3--;
      Rectangle3 = new Rectangle(PositionX3, PositionY3, Texture3.Width, Texture3.Height);
      if (PositionX3 < -Game.GraphicsDevice.Viewport.Width / 2)
      {
        PositionX3 = Game.GraphicsDevice.Viewport.Width;
      }
      PositionY4++;
      Rectangle4 = new Rectangle(PositionX4, PositionY4, Texture4.Width, Texture4.Height);
      if (PositionY4 > Game.GraphicsDevice.Viewport.Height)
      {
        PositionY4 = -Game.GraphicsDevice.Viewport.Height;
      }
      base.Update(gameTime);
    }
    public override void Draw(GameTime gameTime)
    {
      spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
      spriteBatch.Draw(Texture1, Rectangle1, new Color(255, 255, 255, 70));
      spriteBatch.Draw(Texture2, Rectangle2, new Color(255, 255, 255, 70));
      spriteBatch.Draw(Texture3, Rectangle3, new Color(255, 255, 255, 255));
      spriteBatch.Draw(Texture4, Rectangle4, new Color(255, 255, 255, 100));
      spriteBatch.Draw(gameName, new Rectangle(60, 70, gameName.Width / 2, gameName.Height / 2), new Color(255, 255, 255, 255));
      spriteBatch.End();
      base.Draw(gameTime);
    }
  }
}

namespace _MENU_
{
  public class BackGround : Microsoft.Xna.Framework.DrawableGameComponent
  {
    private SpriteBatch spriteBatch;
    private VideoPlayer videoPlayer;
    private Texture2D BackGroundTexture = null;
    private Video BackGroundVideo = null;
    private Rectangle BackGroundRectangle;
    private string BackGroundFilePath;
    public BackGround(Game game, Rectangle BackGroundRectangle, string BackGroundFilePath) : base(game)
    {
      this.BackGroundRectangle = BackGroundRectangle;
      this.BackGroundFilePath = BackGroundFilePath;
    }
    public override void Initialize()
    {
      spriteBatch = new SpriteBatch(Game.GraphicsDevice);
      try
      {
        BackGroundTexture = Game.Content.Load<Texture2D>(BackGroundFilePath);
      }
      catch
      {
        BackGroundVideo = Game.Content.Load<Video>(BackGroundFilePath);
        videoPlayer = new VideoPlayer();
        videoPlayer.IsLooped = true;
        videoPlayer.Play(BackGroundVideo);
      }
      base.Initialize();
    }
   
    public override void Draw(GameTime gameTime)
    {
      spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
      if (BackGroundTexture != null)
      {
        spriteBatch.Draw(BackGroundTexture, BackGroundRectangle, Color.White);
      }
      if (BackGroundVideo != null)
      {
        spriteBatch.Draw(videoPlayer.GetTexture(), BackGroundRectangle, Color.White);
      }
      spriteBatch.End();
      base.Draw(gameTime);
    }
  }
}

namespace _MENU_
{
  public class MenuButton : Microsoft.Xna.Framework.DrawableGameComponent
  {
    private SpriteBatch spriteBatch;
    private SpriteFont spriteFont;
   
    private Texture2D ButtonTexture;
    private Rectangle ButtonRectangle;
    private Color ButtonColor = Color.White;
    private string TexturePath;
    private string SpriteFontPath = string.Empty;
    private string Name = string.Empty;
    public event OnMenuButtonClick OnButtonClick;
    public MenuButton(Game game, Rectangle ButtonRectangle, string TexturePath)
      : base(game)
    {     
      this.ButtonRectangle = ButtonRectangle;
      this.TexturePath = TexturePath;
    }
    public MenuButton(Game game, Rectangle ButtonRectangle, string TexturePath, string SpriteFontPath, string Name)
      : base(game)
    {
      this.ButtonRectangle = ButtonRectangle;
      this.TexturePath = TexturePath;
      this.SpriteFontPath = SpriteFontPath;
      this.Name = Name;
    }
    protected override void LoadContent()
    {
      spriteBatch = new SpriteBatch(GraphicsDevice);
      ButtonTexture = Game.Content.Load<Texture2D>(TexturePath);
      if (SpriteFontPath != string.Empty)
      {
        spriteFont = Game.Content.Load<SpriteFont>(SpriteFontPath);
      }
      base.LoadContent();
    }
    public override void Update(GameTime gameTime)
    {
      Rectangle MouseRectangle = new Rectangle(Mouse.GetState().X, Mouse.GetState().Y, 1, 1);
      if (MouseRectangle.Intersects(ButtonRectangle))
      {
        ButtonColor = Color.White;
        if (Mouse.GetState().LeftButton == ButtonState.Pressed)
        {
          OnButtonClick();
        }
      }
      else
      {
        ButtonColor = Color.Gray;
      }
      base.Update(gameTime);
    }
    public override void Draw(GameTime gameTime)
    {
      spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
      spriteBatch.Draw(ButtonTexture, ButtonRectangle, ButtonColor);
      if (Name != string.Empty)
      {
        spriteBatch.DrawString(spriteFont, Name, new Vector2(ButtonRectangle.X, ButtonRectangle.Y + 8), Color.AliceBlue);
      }
      spriteBatch.End();
      base.Draw(gameTime);
    }
  }
}


namespace _MENU_
{
  public class Cursor : Microsoft.Xna.Framework.DrawableGameComponent
  {
    SpriteBatch spriteBatch;
    private Texture2D CursorTexture;
    private Rectangle CursorRectangle;
    private string CursorFilePath;
    public Cursor(Game game, Rectangle CursorRectangle, string CursorFilePath) : base(game)
    {
      this.CursorRectangle = CursorRectangle;
      this.CursorFilePath = CursorFilePath;
    }
    public override void Initialize()
    {
      spriteBatch = new SpriteBatch(Game.GraphicsDevice);
      CursorTexture = Game.Content.Load<Texture2D>(CursorFilePath);
      base.Initialize();
    }
    public override void Update(GameTime gameTime)
    {
      CursorRectangle = new Rectangle(Mouse.GetState().X, Mouse.GetState().Y, 50, 50);
      base.Update(gameTime);
    }
    public override void Draw(GameTime gameTime)
    {
      spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
      spriteBatch.Draw(CursorTexture, CursorRectangle, Color.White);     
      spriteBatch.End();
      base.Draw(gameTime);
    }
  }
}


namespace _MENU_
{
  public delegate void OnMenuButtonClick();
  public delegate void SwitchMainMenuToNewGame();
  public delegate void SwitchMainMenuToOptionsMenu();
  public delegate void SwitchMainMenuToTitleMenu();
  public delegate void SwitchTitleMenuToMainMenu();
  public delegate void SwitchMainMenuToExit();
}
namespace _MENU_
{
  public class MainMenu
  {
    public event SwitchMainMenuToNewGame SwitchMainMenuToGame;
    public event SwitchMainMenuToTitleMenu SwitchMainMenuToTitleMenu;
    public event SwitchMainMenuToExit SwitchMainMenuToExit;
    BackGround FirstBackGround;
    AnimeFont AFont;
    MenuButton StartNewGameButton;
    MenuButton OptionsGameButton;
    MenuButton TitleButton;
    MenuButton ExitGameButton;
    public MainMenu(Game game)
    {
      FirstBackGround = new BackGround(game, new Rectangle(0, 0, game.GraphicsDevice.Viewport.Width, game.GraphicsDevice.Viewport.Height), @"Menu/menu");
      game.Components.Add(FirstBackGround);
      AFont = new AnimeFont(game);
      game.Components.Add(AFont);
      StartNewGameButton = new MenuButton(game, new Rectangle(80, 150, 200, 40), @"Menu/Button3", @"Menu/DefaultFont", "       START");
      StartNewGameButton.OnButtonClick += new OnMenuButtonClick(StartNewGameButton_OnButtonClick);
      game.Components.Add(StartNewGameButton);
      OptionsGameButton = new MenuButton(game, new Rectangle(80, 200, 200, 40), @"Menu/Button3", @"Menu/DefaultFont", "      OPTIONS");
      OptionsGameButton.OnButtonClick += new OnMenuButtonClick(OptionsGameButton_OnButtonClick);
      game.Components.Add(OptionsGameButton);
      TitleButton = new MenuButton(game, new Rectangle(80, 250, 200, 40), @"Menu/Button3", @"Menu/DefaultFont", "       TITLE");
      TitleButton.OnButtonClick += new OnMenuButtonClick(TitleButton_OnButtonClick);
      game.Components.Add(TitleButton);
      ExitGameButton = new MenuButton(game, new Rectangle(80, 300, 200, 40), @"Menu/Button3", @"Menu/DefaultFont", "        EXIT");
      ExitGameButton.OnButtonClick += new OnMenuButtonClick(ExitButton_OnButtonClick);
      game.Components.Add(ExitGameButton);
      game.Components.Add(new Cursor(game, new Rectangle(),@"Menu/cursor"));
    }
    void TitleButton_OnButtonClick()
    {
      SwitchMainMenuToTitleMenu();
    }
    void OptionsGameButton_OnButtonClick()
    {
    }
    void StartNewGameButton_OnButtonClick()
    {
      SwitchMainMenuToGame();
    }
    void ExitButton_OnButtonClick()
    {
      SwitchMainMenuToExit();
    }

  }
}

The result of these classes you can see in the pictures above. Using delegates used in menu, we can switch between various modes of our future game engine. Need to immediately say that this game is based on the physical engine PhysX, and actually to the NET version of these libraries:

PhysXLoader.dll
NxCooking.dll
NxCharacter.dll

These libraries are available online for download.

This class fully configures the physics of the game, and synchronizes with the game render.
namespace _GAME_
{
  public delegate void ContactCallback(Actor a, Actor b, ContactPairFlag events);
  public class PhysX
  {
    private Model _box;
    private Model _sphere;
    public Core _core;
    public static Scene _scene;
    public static Actor Bitok;
    public static Actor ball1, ball2, ball3, ball4,
                        ball5, ball6, ball7, ball8,
                        ball9, ball10, ball11, ball12,
                        ball13, ball14, ball15, ball16;
    public static Actor Border1, Border2, Border3, Border4, Border5, Border6;  
    public PhysX(Game game)
    {
      _core = new Core();
      _core.SetParameter(PhysicsParameter.VisualizationScale, 2.0f);
      _core.SetParameter(PhysicsParameter.VisualizationScale, 1.0f);
      _core.SetParameter(PhysicsParameter.VisualizeCollisionShapes, true);
      _core.SetParameter(PhysicsParameter.VisualizeClothMesh, true);
      _core.SetParameter(PhysicsParameter.VisualizeJointLocalAxes, true);
      _core.SetParameter(PhysicsParameter.VisualizeJointLimits, true);
      _core.SetParameter(PhysicsParameter.VisualizeFluidPosition, true);
      _core.SetParameter(PhysicsParameter.VisualizeFluidEmitters, false); // Slows down rendering a bit to much
      _core.SetParameter(PhysicsParameter.VisualizeForceFields, true);
      _core.SetParameter(PhysicsParameter.VisualizeSoftBodyMesh, true);
      _core.SetParameter(PhysicsParameter.DefaultSleepLinearVelocitySquared, 2.0f * 2.0f);
      _core.SetParameter(PhysicsParameter.DefaultSleepAngularVelocitySquared, 2.0f * 2.0f);
      _core.SetParameter(PhysicsParameter.SkinWidth, 0.01f);
      _core.SetParameter(PhysicsParameter.VisualizeActorAxes, true);
      SceneDescription sceneDesc = new SceneDescription();
      sceneDesc.SimulationType = SimulationType.Software;
      sceneDesc.Gravity = new Vector3(0.0f, -50f, 0.0f);
      _scene = _core.CreateScene(sceneDesc);
      // Create the default material
      Material defaultMaterial = _scene.Materials[0];
      defaultMaterial.Restitution = 0.5f;
      defaultMaterial.StaticFriction = 0.1f;
      defaultMaterial.DynamicFriction = 0.1f;
    }
   
    public void Dispose()
    {
      _core.Dispose();
      _scene.Dispose();
    }
    public void LoadPhysX(ContentManager Content)
    {
      _box = Content.Load<Model>(@"PhysicsMeshes/cube");
      _sphere = Content.Load<Model>(@"PhysicsMeshes/sphere");
      SetupPhysicsScene();
    }
    void SetupPhysicsScene()
    {
      // --- Borders ---
      Border1 = CreateBox(new Vector3(0, -15, 44), new Vector3(20.5f, 10, 2), 0);
      Border1.Name = "b";
      Border2 = CreateBox(new Vector3(0, -15, -44), new Vector3(20.5f, 10, 2), 0);
      Border2.Name = "b";
      Border3 = CreateBox(new Vector3(24, -15, 20.75f), new Vector3(2, 10, 19.75f), 0);
      Border3.Name = "b";
      Border4 = CreateBox(new Vector3(24, -15, -20.75f), new Vector3(2, 10, 19.75f), 0);
      Border4.Name = "b";
      Border5 = CreateBox(new Vector3(-24, -15, 20.75f), new Vector3(2, 10, 19.75f), 0);
      Border5.Name = "b";
      Border6 = CreateBox(new Vector3(-24, -15, -20.75f), new Vector3(2, 10, 19.75f), 0);
      Border6.Name = "b";
      // --- Planes ---
      Actor Box = CreateBox(new Vector3(0, -18, 0), new Vector3(22, 10.35f, 42), 0);
      Box = CreateBox(new Vector3(0, -50, 0), new Vector3(2500, 10f, 2500), 0);
      // --- zaplatki ---
      Box = CreateBox(new Vector3(25, -15, 45), new Vector3(6, 10f, 2), 0);
      Box.GlobalOrientation = Matrix.Transform(Box.GlobalOrientation, Quaternion.CreateFromRotationMatrix(Matrix.CreateRotationY(MathHelper.PiOver4)));
      Box = CreateBox(new Vector3(-25, -15, 45), new Vector3(2, 10f, 6), 0);
      Box.GlobalOrientation = Matrix.Transform(Box.GlobalOrientation, Quaternion.CreateFromRotationMatrix(Matrix.CreateRotationY(MathHelper.PiOver4)));
      Box = CreateBox(new Vector3(25, -15, -45), new Vector3(2, 10f, 6), 0);
      Box.GlobalOrientation = Matrix.Transform(Box.GlobalOrientation, Quaternion.CreateFromRotationMatrix(Matrix.CreateRotationY(MathHelper.PiOver4)));
      Box = CreateBox(new Vector3(-25, -15, -45), new Vector3(6, 10f, 2), 0);
      Box.GlobalOrientation = Matrix.Transform(Box.GlobalOrientation, Quaternion.CreateFromRotationMatrix(Matrix.CreateRotationY(MathHelper.PiOver4)));
      Box = CreateBox(new Vector3(27, -15, 0), new Vector3(2, 10f, 10), 0);
      Box = CreateBox(new Vector3(-27, -15, 0), new Vector3(2, 10f, 10), 0);
      // --- Balls ---
      float AngDump = 100f;
      float Mass = 300f;
      Bitok = CreateSphere(new Vector3(0, 2.6f, 20), 1, 1);
      Bitok.AngularDamping = AngDump;
      Bitok.Mass = Mass;
      Bitok.Name = "Bitok";
      Bitok.Sleep();
      ball1 = CreateSphere(new Vector3(-2, 2.6f, -20), 1, 1);
      ball1.AngularDamping = AngDump;
      ball1.Mass = Mass;
      ball1.Name = "ball1";
      ball1.Sleep();
      ball2 = CreateSphere(new Vector3(0, 2.6f, -20), 1, 1);
      ball2.AngularDamping = AngDump;
      ball2.Mass = Mass;
      ball2.Name = "ball2";
      ball2.Sleep();
      ball3 = CreateSphere(new Vector3(2, 2.6f, -20), 1, 1);
      ball3.AngularDamping = AngDump;
      ball3.Mass = Mass;
      ball3.Name = "ball3";
      ball3.Sleep();
      ball4 = CreateSphere(new Vector3(-3, 2.6f, -21.8f), 1, 1);
      ball4.AngularDamping = AngDump;
      ball4.Mass = Mass;
      ball4.Name = "ball4";
      ball4.Sleep();
      ball5 = CreateSphere(new Vector3(-1, 2.6f, -21.8f), 1, 1);
      ball5.AngularDamping = AngDump;
      ball5.Mass = Mass;
      ball5.Name = "ball5";
      ball5.Sleep();
      ball6 = CreateSphere(new Vector3(1, 2.6f, -21.8f), 1, 1);
      ball6.AngularDamping = AngDump;
      ball6.Mass = Mass;
      ball6.Name = "ball6";
      ball6.Sleep();
      ball7 = CreateSphere(new Vector3(3, 2.6f, -21.8f), 1, 1);
      ball7.AngularDamping = AngDump;
      ball7.Mass = Mass;
      ball7.Name = "ball7";
      ball7.Sleep();
      ball8 = CreateSphere(new Vector3(-4, 2.6f, -23.6f), 1, 1);
      ball8.AngularDamping = AngDump;
      ball8.Mass = Mass;
      ball8.Name = "ball8";
      ball8.Sleep();
      ball9 = CreateSphere(new Vector3(-2, 2.6f, -23.6f), 1, 1);
      ball9.AngularDamping = AngDump;
      ball9.Mass = Mass;
      ball9.Name = "ball9";
      ball9.Sleep();
      ball10 = CreateSphere(new Vector3(0, 2.6f, -23.6f), 1, 1);
      ball10.AngularDamping = AngDump;
      ball10.Mass = Mass;
      ball10.Name = "ball10";
      ball10.Sleep();
      ball11 = CreateSphere(new Vector3(2, 2.6f, -23.6f), 1, 1);
      ball11.AngularDamping = AngDump;
      ball11.Mass = Mass;
      ball11.Name = "ball11";
      ball11.Sleep();
      ball12 = CreateSphere(new Vector3(4, 2.6f, -23.6f), 1, 1);
      ball12.AngularDamping = AngDump;
      ball12.Mass = Mass;
      ball12.Name = "ball12";
      ball12.Sleep();
      ball13 = CreateSphere(new Vector3(-1, 2.6f, -18.2f), 1, 1);
      ball13.AngularDamping = AngDump;
      ball13.Mass = Mass;
      ball13.Name = "ball13";
      ball13.Sleep();
      ball14 = CreateSphere(new Vector3(1, 2.6f, -18.2f), 1, 1);
      ball14.AngularDamping = AngDump;
      ball14.Mass = Mass;
      ball14.Name = "ball14";
      ball14.Sleep();
      ball15 = CreateSphere(new Vector3(0, 2.6f, -16.4f), 1, 1);
      ball15.AngularDamping = AngDump;
      ball15.Mass = Mass;
      ball15.Name = "ball15";
      ball15.Sleep();

      _scene.UserContactReport = new ContactReport();
      _scene.SetActorGroupPairFlags(0, 0, ContactPairFlag.OnTouch);
    }
    public void UpdatePhysX(GameTime gameTime)
    {
      _scene.Simulate((float)gameTime.ElapsedGameTime.TotalMilliseconds / 300f);     
      _scene.FetchResults(SimulationStatus.RigidBodyFinished, true);
    }
    public void DrawPhysX()
    {
      foreach (Actor actor in _scene.Actors)
      {
        Vector3 actualColor = new Vector3(1, 1, 1);
        foreach (Shape shape in actor.Shapes)
        {
          switch (shape.Type)
          {
            case ShapeType.Box:
              DrawPhysicsBox((BoxShape)shape, actualColor);
              break;
            case ShapeType.Sphere:
              if (actor.IsSleeping)
              {
                DrawPhysicsSphere((SphereShape)shape, /*new Vector3(0, 0, 1)*/actualColor, true);
              }
              else
              {
                DrawPhysicsSphere((SphereShape)shape, actualColor, false);
              }
                break;
          }
        }
      }
    }
    Actor CreateKinematicBox(Vector3 pos, Vector3 boxDim, string name, float mass)
    {
      BodyDescription bodyDescription = new BodyDescription(mass);
      bodyDescription.BodyFlags = BodyFlag.Kinematic;
      ActorDescription actorDesc = new ActorDescription();
      actorDesc.GlobalPose = Matrix.CreateTranslation(pos);
      actorDesc.BodyDescription = bodyDescription;
      actorDesc.Name = name;
      actorDesc.Shapes.Add(new BoxShapeDescription(boxDim));
      return _scene.CreateActor(actorDesc);
    }
    Actor CreateBox(Vector3 pos, Vector3 boxDim, float density)
    {
      ActorDescription actorDesc = new ActorDescription();
      BodyDescription bodyDesc = new BodyDescription();
      BoxShapeDescription boxShapeDesc = new BoxShapeDescription();
      boxShapeDesc.Dimensions = new Vector3(boxDim.X, boxDim.Y, boxDim.Z);
      boxShapeDesc.LocalPose = Matrix.CreateTranslation(0, boxDim.Y, 0);
      actorDesc.Shapes.Add(boxShapeDesc);
      actorDesc.GlobalPose = Matrix.CreateTranslation(pos);
      if (density > 0)
      {
        actorDesc.BodyDescription = bodyDesc;
        actorDesc.Density = density;
      }
      //if (!actorDesc.IsValid())
      //  throw new InvalidDataException();
      return _scene.CreateActor(actorDesc);
    }
    void DrawPhysicsBox(BoxShape shape, Vector3 color)
    {
      {
        Matrix[] transforms = new Matrix[_box.Bones.Count];
        _box.CopyAbsoluteBoneTransformsTo(transforms);
        foreach (ModelMesh mesh in _box.Meshes)
        {
          foreach (BasicEffect effect in mesh.Effects)
          {
            effect.DiffuseColor = color;
            effect.EnableDefaultLighting();
            effect.World = Matrix.CreateScale(shape.Dimensions.X, shape.Dimensions.Y, shape.Dimensions.Z) * transforms[mesh.ParentBone.Index] * shape.GlobalPose;
            effect.View = Camera.ViewMatrix;
            effect.Projection = Camera.ProjectionMatrix;
          }
          mesh.Draw();
        }
      }
    }
    Actor CreateSphere(Vector3 pos, float radius, float density)
    {
      // Add a single-shape actor to the scene
      ActorDescription actorDesc = new ActorDescription();
      BodyDescription bodyDesc = new BodyDescription();
      // The actor has one shape, a sphere
      SphereShapeDescription sphereDesc = new SphereShapeDescription(radius);
      sphereDesc.LocalPose = Matrix.CreateTranslation(new Vector3(0, radius, 0));
      actorDesc.Shapes.Add(sphereDesc);
      if (density > 0)
      {
        actorDesc.BodyDescription = bodyDesc;
        actorDesc.Density = density;
      }
      actorDesc.GlobalPose = Matrix.CreateTranslation(pos);
      Actor pActor = _scene.CreateActor(actorDesc);
      return pActor;
    }
    void DrawPhysicsSphere(SphereShape sphereShape, Vector3 color, bool IsSleeping)
    {
      Matrix[] transforms = new Matrix[_sphere.Bones.Count];
      _sphere.CopyAbsoluteBoneTransformsTo(transforms);
      foreach (ModelMesh mesh in _sphere.Meshes)
      {
        foreach (BasicEffect effect in mesh.Effects)
        {
          effect.World = Matrix.CreateScale(sphereShape.Radius, sphereShape.Radius, sphereShape.Radius) * transforms[mesh.ParentBone.Index] * sphereShape.GlobalPose;
          effect.View = Camera.ViewMatrix;
          effect.Projection = Camera.ProjectionMatrix;
          effect.DiffuseColor = Color.White.ToVector3();
          //effect.DiffuseColor = color;
          effect.EnableDefaultLighting();
        }
        mesh.Draw();
      }
    }
  }
 
  public class ContactReport : UserContactReport
  {
    private List<ContactReportPair> _contactPairs;
    public struct ContactReportPair
    {
      public Actor A, B;
      public ContactCallback Callback;
      public ContactReportPair(Actor a, Actor b, ContactCallback callback)
      {
        A = a;
        B = b;
        Callback = callback;
      }
    }
    public ContactReport()
    {
      _contactPairs = new List<ContactReportPair>();
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball11, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball12, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball13, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball14, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball15, PhysX.Border1, new ContactCallback(CapsuleAndGroundPlaneContact)));

      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball11, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball12, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball13, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball14, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball15, PhysX.Border2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball11, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball12, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball13, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball14, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball15, PhysX.Border3, new ContactCallback(CapsuleAndGroundPlaneContact)));

      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball11, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball12, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball13, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball14, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball15, PhysX.Border4, new ContactCallback(CapsuleAndGroundPlaneContact)));

      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball11, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball12, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball13, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball14, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball15, PhysX.Border5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball11, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball12, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball13, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball14, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball15, PhysX.Border6, new ContactCallback(CapsuleAndGroundPlaneContact)));

      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball1, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball7, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball8, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball9, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball10, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.Bitok, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball2, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball7, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball8, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball9, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball10, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball1, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball3, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball7, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball8, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball9, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball10, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball2, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball4, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball7, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball8, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball9, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball10, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball3, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball5, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball7, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball8, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball9, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball10, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball4, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.ball6, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.ball7, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.ball8, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.ball9, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.ball10, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball5, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.ball7, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.ball8, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.ball9, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.ball10, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball6, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.ball8, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.ball9, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.ball10, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball7, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.ball9, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.ball10, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball8, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.ball10, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball9, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.ball11, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball10, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball11, PhysX.ball12, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball11, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball11, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball11, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball12, PhysX.ball13, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball12, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball12, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball13, PhysX.ball14, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball13, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
      _contactPairs.Add(new ContactReportPair(PhysX.ball14, PhysX.ball15, new ContactCallback(CapsuleAndGroundPlaneContact)));
    }
    public override void OnContactNotify(ContactPair contactInformation, ContactPairFlag events)
    {
      foreach (ContactReportPair pair in _contactPairs)
      {
        if ((pair.A == contactInformation.ActorA || pair.A == contactInformation.ActorB) && (pair.B == contactInformation.ActorA || pair.B == contactInformation.ActorB))
        {
          pair.Callback(contactInformation.ActorA, contactInformation.ActorB, events);
        }
      }
    }
    private void CapsuleAndGroundPlaneContact(Actor a, Actor b, ContactPairFlag events)
    {     
      if (b.Name == "b")
      {
        float Volume = (a.LinearVelocity.X + a.LinearVelocity.Z) / 2;
        Sound.PlayBoard(Volume);
      }
      else
      {
        float Volume = (a.LinearVelocity.X + a.LinearVelocity.Z) / 2;
        Sound.PlayBall1(Volume);
      }
    }
  }
}

As you can see, in this class also manages audio that sounds when the balls is collide in this game.

namespace _GAME_
{
  public class Sound
  {
    static SoundEffect soundEffectPopadanie;
    static SoundEffect soundEffectBitokLost;
    static SoundEffect soundEffectCue;
    static SoundEffect soundEffectBorder;
    static SoundEffect soundEffectBall1;
    static SoundEffect soundEffectBall2;
    static SoundEffect soundEffectBall3;
    static SoundEffect soundEffectBall4;
    static SoundEffect soundEffectMusik;
    static SoundEffect soundEffectAplod;
    static SoundEffectInstance soundInstancePopadanie;
    static SoundEffectInstance soundInstanceBitokLost;
    static SoundEffectInstance soundInstanceCue;
    static SoundEffectInstance soundInstanceBorder;
    static SoundEffectInstance soundInstanceBall1;
    static SoundEffectInstance soundInstanceBall2;
    static SoundEffectInstance soundInstanceBall3;
    static SoundEffectInstance soundInstanceBall4;
    static SoundEffectInstance soundInstanceMusik;
    static SoundEffectInstance soundInstanceAplod;
    Random rnd = new Random();
    public static void Init(Game game)
    {
      soundEffectBitokLost = game.Content.Load<SoundEffect>("Sound/Piu");
      soundInstanceBitokLost = soundEffectBitokLost.CreateInstance();
      soundEffectPopadanie = game.Content.Load<SoundEffect>("Sound/in");
      soundInstancePopadanie = soundEffectPopadanie.CreateInstance();
      soundEffectCue = game.Content.Load<SoundEffect>("Sound/cue");
      soundInstanceCue = soundEffectCue.CreateInstance();
      soundEffectBorder = game.Content.Load<SoundEffect>("Sound/border");
      soundInstanceBorder = soundEffectBorder.CreateInstance();
      soundEffectBall1 = game.Content.Load<SoundEffect>("Sound/ball1");
      soundInstanceBall1 = soundEffectBall1.CreateInstance();
      soundEffectBall2 = game.Content.Load<SoundEffect>("Sound/ball2");
      soundInstanceBall2 = soundEffectBall2.CreateInstance();
      soundEffectBall3 = game.Content.Load<SoundEffect>("Sound/ball3");
      soundInstanceBall3 = soundEffectBall2.CreateInstance();
      soundEffectBall4 = game.Content.Load<SoundEffect>("Sound/ball4");
      soundInstanceBall4 = soundEffectBall4.CreateInstance();
      soundEffectMusik = game.Content.Load<SoundEffect>("Sound/TwinPeaks");
      soundInstanceMusik = soundEffectMusik.CreateInstance();
      soundInstanceMusik.IsLooped = true;
      soundEffectAplod = game.Content.Load<SoundEffect>("Sound/aplod");
      soundInstanceAplod = soundEffectAplod.CreateInstance();
    }
    public static void PlayCue(float volume)
    {
      soundInstanceCue.Volume = volume;
      soundInstanceCue.Play();
    }
    public static void PlayBall1(float volume)
    {
      //int n = rnd.Next(1, 5);
      float v = Math.Abs(volume / 10);
      //if (n == 1)
      //{
      //  soundInstanceBall1.Volume = Clamp(0, 1, v);
      //  soundInstanceBall1.Play();
      //}
      //else  if (n == 2)
      //{
      //  soundInstanceBall2.Volume = Clamp(0, 1, v);
      //  soundInstanceBall2.Play();
      //}
      //else if (n == 3)
      //{
      //  soundInstanceBall3.Volume = Clamp(0, 1, v);
      //  soundInstanceBall3.Play();
      //}
      //else if (n == 4)
      //{
      //  soundInstanceBall4.Volume = Clamp(0, 1, v);
      //  soundInstanceBall4.Play();
      //}
      if (soundInstanceBall1.State == SoundState.Stopped)
      {
        soundInstanceBall1.Volume = Clamp(0, 1, v);
        soundInstanceBall1.Play();
      }
      else if (soundInstanceBall2.State == SoundState.Stopped)
      {
        soundInstanceBall2.Volume = Clamp(0, 1, v);
        soundInstanceBall2.Play();
      }
      else if (soundInstanceBall3.State == SoundState.Stopped)
      {
        soundInstanceBall3.Volume = Clamp(0, 1, v);
        soundInstanceBall3.Play();
      }
      else if (soundInstanceBall4.State == SoundState.Stopped)
      {
        soundInstanceBall4.Volume = Clamp(0, 1, v);
        soundInstanceBall4.Play();
      }
    }
    public static void PlayBoard(float volume)
    {
      float v = Math.Abs(volume / 2);
      soundInstanceBorder.Volume = Clamp(0, 1, v);
      soundInstanceBorder.Play();
    }
    public static void PlayPopadanie()
    {
      soundInstancePopadanie.Play();
    }
    public static void BitokLost()
    {
      soundInstanceBitokLost.Play();
    }
    public static void PlayMusik()
    {
      soundEffectMusik.Play();
    }
    public static void PlayAplod()
    {
      if (soundInstanceAplod.State == SoundState.Stopped)
      {
        soundInstanceAplod.Play();
      }
    }
    static float Clamp(float min, float max, float value)
    {
      if (value < min)
        value = 0;
      if (value > 1)
        value = 1;     
      return value;
    }
  }
}

To be continue...

Physically Based Rendering