Возможно вы искали: 'Канитель по Беспределу'

May 31 2025 10:53:50
  • Как сделать 8Gamers.Ru домашней страницей?
  • Игры
    • База данных по играх
    • Игровые новости
    • Игровая индустрия
    • Обзоры на игры
    • Прохождения игр
    • Гайды к играм
    • Превью о играх
    • Игровые тизеры
    • Игровые арты
    • Игровые обои
    • Игровые скриншоты
    • Игровые обложки
    • Игровые трейлеры
    • Игровое видео
    • Вышедшие игры
    • Ближайшие релизы игр
  • Кино и ТВ
    • База данных по кино
    • Статьи о кино
    • Постеры
    • Кадры из кино
    • Кино трейлеры
    • Сегодня в кино
    • Скоро в кино
  • Комиксы и манга
    • Манга по алфавиту
    • База данных по комиксах
    • Читать онлайн комиксы
    • Читать онлайн манга
    • База персонажей
  • Читы и коды
    • Чит-коды для PC игр
    • Чит-коды для консольных игр
    • Трейнеры
    • Коды Game Genie
  • Моддинг
    • Модификации
    • Карты к играм
    • Программы для моддинга
    • Статьи о моддинге
  • Геймдев
    • Всё о создании игр
    • Список движков
    • Утилиты в помощь игроделу
    • Конструкторы игр
    • Игровые движки
    • Библиотеки разработки
    • 3D-модели
    • Спрайты и тайлы
    • Музыка и звуки
    • Текстуры и фоны
  • Рецензии
    • Игры
    • Кино
    • Аниме
    • Комиксы
    • Мангу
    • Саундтреки
  • Саундтреки
    • Лирика
  • Файлы
    • Патчи к играм
    • Русификаторы к играм
    • Сохранения к играм
    • Субтитры к кино
  • Медиа
    • Видео
    • Фото
    • Аудио
    • Фан-арты
    • Косплей
    • Фото с виставок
    • Девушки из игр
    • Рисунки
    • Рисуем онлайн
    • Фотохостинг
  • Юмор
    • Анекдоты
    • Афоризмы
    • Истории
    • Стишки и эпиграммы
    • Тосты
    • Цитаты
  • Флеш
    • Азартные
    • Аркады
    • Бродилки
    • Гонки
    • Для девочек
    • Для мальчиков
    • Драки
    • Квесты
    • Леталки
    • Логические
    • Мультфильмы
    • Открытки
    • Приколы
    • Разное
    • Спорт
    • Стратегии
    • Стрелялки
Статистика

Статей: 87772
Просмотров: 96425698
Игры
Injustice:  Gods Among Us
Injustice: Gods Among Us
...
Dark Souls 2
Dark Souls 2
Dark Souls II - вторая часть самой хардкорной ролевой игры 2011-2012 года, с новым героем, сюжето...
Battlefield 4
Battlefield 4
Battlefield 4 - продолжение венценосного мультиплеер-ориентированного шутера от первого ли...
Кино
Steins;Gate
Steins;Gate
Любители японской анимации уже давно поняли ,что аниме сериалы могут дать порой гораздо больше пи...
Ку! Кин-дза-дза
Ку! Кин-дза-дза
Начинающий диджей Толик и всемирно известный виолончелист Владимир Чижов встречают на шумной моск...
Обзоры на игры
• Обзор Ibara [PCB/PS2] 18407
• Обзор The Walking ... 18853
• Обзор DMC: Devil M... 19921
• Обзор на игру Valk... 15921
• Обзор на игру Stars! 17810
• Обзор на Far Cry 3 18000
• Обзор на Resident ... 16063
• Обзор на Chivalry:... 17561
• Обзор на игру Kerb... 18021
• Обзор игры 007: Fr... 16667
Превью о играх
• Превью к игре Comp... 18003
• Превью о игре Mage... 14502
• Превью Incredible ... 14763
• Превью Firefall 13523
• Превью Dead Space 3 16378
• Превью о игре SimC... 14772
• Превью к игре Fuse 15479
• Превью Red Orche... 15589
• Превью Gothic 3 16388
• Превью Black & W... 17402
Главная » Статьи » Всё о XNA » Octree

Octree

Введение.

Современные компьютерные игры и игровые движки соревнуются между собой в плане технологичности и уровня детализации игрового мира. Зачастую на экране мы видим практически искусственную вселенную с огромным множеством игровых объектов. Но то, что мы видим на экране - это лишь верхушка айсберга, все остальное скрыто под водой (т.е. не попадает в поле зрения камеры). Наверное многие замечали, в некоторых играх, что FPS иногда падает, если камера видит большое открытое пространство со множеством объектов, например ландшафт и лес на нем (особенно такие скачки FPS заметны на слабых машинах).

Как ни странно, но эти скачки обусловленны оптимизацией. Если бы подобной оптимизации не было, то FPS был бы низким всегда. Итак, что же это за оптимизация? Это так называемые алгоритмы разбиения пространства. Данные алгоритмы позволяют быстро находить только те объекты, которые видны камере, и рисовать только их. Чем меньше объектов видны камере, тем больше FPS выдает игра.

Существует несколько основных алгоритмов разбиения пространства:

* BSP - binary space partition
* kD-tree
* Quadtree
* Octree
* BVH - bounding volume hierarchy

У каждого из этих алгоритмов есть свои плюсы и минусы, мы не будем говорить обо всех этих алгоритмах, только об Octree.

Алгоритм Octree.

Octree - октарное дерево (октодерево), древовидная структура, где в каждом узле может содержаться до 8 потомков. В этих узлах помимо вложенных узлов, также содержаться объекты игрового мира. Т.о. имея древовидное представление игровых объектов, можно делать быстрый перебор этих объектов, откидывая целые поддеревья, без лишних проверок.

Как видно из определения - идея octree довольно проста. Достаточно взять куб, в который вписаны все игровые объекты, поделить его на 8 дочерних кубов и привязать к ним объекты, которые в них попали. Затем каждый из этих дочерних кубов поделить еще на 8 и т.д. до тех пор пока... пока что-то не произойдет? Критериев завершения деления кубов может быть несколько, например можно остановить деление, если уровнь вложенности превывсил максимально допустимый. Или размер узлов стал меньше чем минимально допустимый. Еще одним критерием остановки деления может стать количество игровых объектов, привязанных к узлу дерева.
А если один и тот же игровой объект попал в несколько узлов одновременно? Здесь есть 2 варианта - либо занести объект в каждый из этих узлов, либо приписать объект в родительский узел. Мы будем приписывать такие объекты родительскому узлу. Далее в статье мы это обсудим.

Ну поделили мы эти кубы, а дальше что? После того, как дерево построено, к нему можно делать запросы, которые позволяют получить список видимых объектов. Собственно для этого, мы и хотим использовать само дерево.

А что если объекты двигаются, например ракеты выпущенные из базуки? Как их перемещать по дереву? Это один из самых сложных вопросов. Практически все алгоритмы разбиения пространства очень хорошо работают со статическими объектами (некоторые лучше чем Octree), но вот с динамикой всегда есть проблемы. В Octree есть более-менее простые способы решения этой проблемы. О них мы поговорим далее в статье.

В общем-то с теорией все. Начинаем кодировать!

Tools & Project

Для написания кода я буду использовать VS 2010 Ultimate и XNA 4.0 beta. Создайте новый проект Windows Game (4.0) и назовите его... да как хотите :) У меня он называется OctreeTutorial.

Design

Прежде всего поговорим немного о проектировании. Для рендеринга объектов мы будем использовать стандартные классы XNA (просто для скорости разработки), а вот для самого Octree напишем 2 класса:

* Octree - собственно само дерево. Данный класс содержит корневой узел и предоставляет методы для построения дерева, его обновления и выполнения запросов для поиска видимых объектов.

* Node - узел дерева. Содержит список вложенных улов и список объектов, привязанных к данному узлу. Также содержит BoundingBox - ограничивающий объем, по которому будет вычисляться видимость узла.

Кроме этих 2 основных классов мы разработаем несколько методов расширения (Extension method), классы камеры и игровых сущностей, а также счетчик FPS.

Подготовка сцены.

Итак, у нас есть пустой проект игры, с классом Game1 и проект контента (также пустой). Для начала давайте добавим файл шрифта, чтобы иметь возможность выводить текстовые данные. Не буду описывать, как это делается - в интернете полно инфы по этому поводу. Я назвал файл шрифта Arial.spritefont.

Помимо шрифта нам понадобится тестовая модель (желательно как можно проще).
Итак, у нас есть шрифт и модель, давайте нарисуем несколько экземпляров этой модели и посчитаем FPS.

Начнем с FPS. Создайте новый класс FpsCounter и добавьте в него следующий код:FpsCounter

 

public class FpsCounter
{
 private float _frameTime = 0;
 
 private int _framesPerSecond = 0;
 
 private int _frameCounter = 0;
 
 private TimeSpan _elapsedTime = TimeSpan.Zero;
 
 public int FramesPerSecond
 {
  get { return _framesPerSecond; }
 }
 
 public float FrameTime
 {
  get { return _frameTime; }
 }
 
 public void Update(GameTime gameTime)
 {
  //накапливаем время прошедшее с момента отрисовки последнего кадра
  _elapsedTime += gameTime.ElapsedGameTime;
 
  //если накопленное время больше секунды, то считаем кадры
  if (_elapsedTime > TimeSpan.FromSeconds(1))
  {
   _elapsedTime -= TimeSpan.FromSeconds(1);
   _framesPerSecond = _frameCounter;
   _frameCounter = 0;
  }
 
  //увеличиваем счетчик кадров
  _frameCounter++;
 
  //получаем время затраченное на отрисовку одного кадра
  if (_framesPerSecond > 0)
  {
   _frameTime = 1000f / _framesPerSecond;
  }
 }
}



Теперь откройте файл Game1.cs и добавьте в него следующий код (я убрал лишние коментарии, для краткости):

public class Game1 : Microsoft.Xna.Framework.Game
{
 private GraphicsDeviceManager _graphics;
 private SpriteBatch _spriteBatch;
 private SpriteFont _font;
 private FpsCounter _fpsCounter; 
 
 public Game1()
 {
  _graphics = new GraphicsDeviceManager(this);
  _graphics.SynchronizeWithVerticalRetrace = false;
  this.IsFixedTimeStep = false;
  Content.RootDirectory = "Content";
  _fpsCounter = new FpsCounter();
 }
 
 protected override void Initialize()
 {
  base.Initialize();
 }
 
 protected override void LoadContent()
 {
  _spriteBatch = new SpriteBatch(GraphicsDevice);
  _font = Content.Load<SpriteFont>("Arial");
 }
 
 protected override void UnloadContent()
 {
  Content.Unload();
 }
 
 protected override void Update(GameTime gameTime)
 {
  if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
    Keyboard.GetState().IsKeyDown(Keys.Escape))
   this.Exit();
 
  _fpsCounter.Update(gameTime);
 
  base.Update(gameTime);
 }
 
 protected override void Draw(GameTime gameTime)
 {
  GraphicsDevice.Clear(Color.CornflowerBlue);
 
  _spriteBatch.Begin();
  _spriteBatch.DrawString(_font, string.Format("FPS: {0} Frame time: {1}", _fpsCounter.FramesPerSecond, _fpsCounter.FrameTime), Vector2.Zero, Color.White);
  _spriteBatch.End();
 
  base.Draw(gameTime);
 }
}


Теперь наша игра умеет считать FPS и выводить его на экран (у меня FPS ~8000(!)).
Давайте нарисуем модель. Для рисования нам, прежде всего, понадобится класс камеры. Создайте новый файл Camera.cs и добавьте в него приведенный ниже код.

public class Camera
{
 private MouseState _prevMouseState;
 
 private Vector3 _right;
 
 private Vector3 _up;
 
 private Vector3 _direction;
 
 private Vector3 _position;
 
 public float RotationSpeed;
 
 public float MovingSpeed;
 
 public float Fov;
 
 public float Aspect;
 
 public float NearClip;
 
 public float FarClip;
 
 public Matrix View;
 
 public Matrix Projection;
 
 public Camera()
 {
  Fov = MathHelper.Pi / 3;
  Aspect = 800.0f / 480.0f;
  NearClip = 1;
  FarClip = 10000;
 
  RotationSpeed = 1;
  MovingSpeed = 1;
 
  _up = Vector3.Up;
  _right = Vector3.Right;
  _direction = Vector3.Cross(_up, _right);
  View = Matrix.CreateLookAt(_position, _direction, _up);
  Projection = Matrix.CreatePerspectiveFieldOfView(Fov, Aspect, NearClip, FarClip);
 }
 
 public void Update(GameTime gameTime)
 {
  MouseState mouseState = Mouse.GetState();
 
  //коэффициент времени, нужен чтобы отвязать скорость движения камеры от FPS
  float coeff = (float)gameTime.ElapsedGameTime.Milliseconds / 1000.0f;
 
  if (mouseState.LeftButton == ButtonState.Pressed)
  {    
   //получаем углы поворота камеры
   float yaw = MathHelper.ToRadians(_prevMouseState.X - mouseState.X) * RotationSpeed * coeff;
   float pitch = MathHelper.ToRadians(_prevMouseState.Y - mouseState.Y) * RotationSpeed * coeff;
 
   //получаем матрицу вращения в локальном базисе матрицы вида камеры
   Matrix rotation = Matrix.CreateFromAxisAngle(_up, yaw) * Matrix.CreateFromAxisAngle(_right, pitch);
 
   //вращаем базис
   Vector3.Transform(ref _up, ref rotation, out _up);
   Vector3.Transform(ref _right, ref rotation, out _right);
  }
 
  _prevMouseState = mouseState;
 
  _direction = Vector3.Cross(_up, _right);
 
  KeyboardState keyboardState = Keyboard.GetState();
  Vector3 prevPos = _position;
 
  float boost = 1;
  if (keyboardState.IsKeyDown(Keys.LeftShift))
  {
   boost = 10;
  }
 
  if (keyboardState.IsKeyDown(Keys.W))
  {
   _position -= _direction * MovingSpeed * boost * coeff;
  }
 
  if (keyboardState.IsKeyDown(Keys.S))
  {
   _position += _direction * MovingSpeed * boost * coeff;
  }
 
  if (keyboardState.IsKeyDown(Keys.A))
  {
   _position += _right * MovingSpeed * boost * coeff;
  }
 
  if (keyboardState.IsKeyDown(Keys.D))
  {
   _position -= _right * MovingSpeed * boost * coeff;
  }
 
  //получаем матрицы
  View = Matrix.CreateTranslation(_position) * Matrix.CreateLookAt(Vector3.Zero, _direction, _up);
  Projection = Matrix.CreatePerspectiveFieldOfView(Fov, Aspect, NearClip, FarClip);
 }
}


Кроме камеры, нам понадобится класс игровой сущности, который будет содержать ссылку на модель, мировую матрицу и ограничивающий объем. Создайте новый класс GameEntity и вставьте следующий код:

 

public class GameEntity
{
 public Matrix World;
 
 public BoundingBox OriginalBoundingBox;
 
 public BoundingBox TransformedBoundingBox;
 
 public Model Model { get; set; }
}

 


Теперь все готово для отрисовки наших моделей. Измените класс Game1 следующим образом:

private Camera _camera;
private List<GameEntity> _entities;
private int _modelsCount;
...
 
public Game1()
{
 _graphics = new GraphicsDeviceManager(this);
 _graphics.SynchronizeWithVerticalRetrace = false;
 this.IsFixedTimeStep = false;
 Content.RootDirectory = "Content";
 
 _fpsCounter = new FpsCounter();
 _camera = new Camera();
 _camera.RotationSpeed = 10;
 _camera.MovingSpeed = 10;
 _entities = new List<GameEntity>();
 _modelsCount = 5000;
}
 
protected override void LoadContent()
{
 _spriteBatch = new SpriteBatch(GraphicsDevice);
 _font = Content.Load<SpriteFont>("Arial");
 Model model = Content.Load<Model>("cats");
 //создаем игровые сущности и располагаем их в случайном порядке в пределах куба {-50, -50, -50; 50, 50, 50}
 Random r = new Random((int)DateTime.Now.Ticks);
 for (int i = 0; i < _modelsCount; i++)
 {
  GameEntity entity = new GameEntity();
  entity.Model = model;
  entity.World = Matrix.CreateTranslation(new Vector3((float)r.NextDouble(), (float)r.NextDouble(), -(float)r.NextDouble()) * 100 - Vector3.One * 50);
  entity.OriginalBoundingBox = BoundingBox.CreateFromSphere(model.Meshes[0].BoundingSphere);
  entity.Update(new GameTime());
  _entities.Add(entity);
 }
}
protected override void Update(GameTime gameTime)
{
 if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
  Keyboard.GetState().IsKeyDown(Keys.Escape))
  this.Exit();
 
 _camera.Update(gameTime);
 
 _fpsCounter.Update(gameTime);
 
 base.Update(gameTime);
}
 
protected override void Draw(GameTime gameTime)
{
 GraphicsDevice.Clear(Color.CornflowerBlue);
 
 GraphicsDevice.DepthStencilState = DepthStencilState.Default;
 
 for (int i = 0; i < _modelsCount; i++)
 {
  _entities[i].Model.Draw(
   _entities[i].World,
   _camera.View,
   _camera.Projection);
 }
 
 _spriteBatch.Begin();
 _spriteBatch.DrawString(_font, string.Format("FPS: {0} Frame time: {1}", _fpsCounter.FramesPerSecond, _fpsCounter.FrameTime), Vector2.Zero, Color.White);
 _spriteBatch.End();
 
 base.Draw(gameTime);
}


Если запустить проект сейчас, то вы увидете на экране 5000 моделей. При этом на моей машине я имею ~90 FPS. А ведь при пустой сцене было ~8000. FPS упал почти в 90 раз!!! Естественно, в реальной игре такого просто не должно быть. Здесь-то и выручают алогоритмы разбиения пространства. Конечно, мы не сможем вернуть показатель в прежнее значение 8000, но мы сможем увеличить текущее значение.

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

Нам потребуется класс DebugRenderer, который будет отрисовывать BoundingBox'ы.

public class DebugRenderer
 
{
 private BasicEffect _effect;
 
 private VertexPositionColor[] _bboxVerts;
 
 private short[] _bboxIndices;
 
 public GraphicsDevice Device { get; private set; }
 
 public DebugRenderer(GraphicsDevice device)
 {
  Device = device;
 
  _effect = new BasicEffect(Device);
  _effect.VertexColorEnabled = true;
  _effect.LightingEnabled = false;
 
  _bboxVerts = new VertexPositionColor[8];
  _bboxIndices = new short[]
  {
   0, 1,
   1, 2,
   2, 3,
   3, 0,
   0, 4,
   1, 5,
   2, 6,
   3, 7,
   4, 5,
   5, 6,
   6, 7,
   7, 4,
  };
 }
 
 public void DrawBoundingBox(ref BoundingBox boundingBox, ref Matrix view, ref Matrix projection, ref Color color)
 {
  Vector3[] corners = boundingBox.GetCorners();
  for (int i = 0; i < 8; i++)
  {
   _bboxVerts[i].Position = corners[i];
   _bboxVerts[i].Color = color;
  }
 
  _effect.View = view;
  _effect.Projection = projection;
 
  foreach (EffectPass pass in _effect.CurrentTechnique.Passes)
  {
   pass.Apply();
 
   Device.DrawUserIndexedPrimitives(
    PrimitiveType.LineList,
    _bboxVerts,
    0,
    8,
    _bboxIndices,
    0,
    _bboxIndices.Length / 2);
  }
 }
}


Также потребуется вспомогательный класс утилит, в котором будут содержаться различные функции, в частности функция траснформации BoundingBox'ов (спасибо mike'у за нее).

public static class Utilities
{
 public static void TransformBox(ref BoundingBox inBox, ref Matrix mat, out BoundingBox result)
 {
  Vector3 position = (Vector3)(0.5f * (inBox.Min + inBox.Max));
  Vector3 offset;
  Vector3 dim = new Vector3();
 
  Vector3.Subtract(ref inBox.Max, ref position, out offset);
 
  dim.X = ((offset.X * Math.Abs(mat.M11)) + (offset.Y * Math.Abs(mat.M21))) + (offset.Z * Math.Abs(mat.M31));
  dim.Y = ((offset.X * Math.Abs(mat.M12)) + (offset.Y * Math.Abs(mat.M22))) + (offset.Z * Math.Abs(mat.M32));
  dim.Z = ((offset.X * Math.Abs(mat.M13)) + (offset.Y * Math.Abs(mat.M23))) + (offset.Z * Math.Abs(mat.M33));
 
  Vector3.Transform(ref position, ref mat, out position);
 
  Vector3.Subtract(ref position, ref dim, out result.Min);
  Vector3.Add(ref position, ref dim, out result.Max);
 }
}

 

Теперь добавьте в файл игровой сущности код, отвечающий за ее обновление:

 

 

public void Update(GameTime gameTime)
{
 //трансформируем ограничивающий объем
 Utilities.TransformBox(ref OriginalBoundingBox, ref World, out TransformedBoundingBox);
}

 

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

 

private DebugRenderer _debugRenderer;
private Color _bboxColor;
private KeyboardState _prevKeyboardState;
private bool _drawDebug;
...
 
//в конструкторе
_bboxColor = Color.Blue;
...
 
protected override void Initialize()
{
 base.Initialize();
 
 _debugRenderer = new DebugRenderer(GraphicsDevice);
}
protected override void Update(GameTime gameTime)
{
 if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
  Keyboard.GetState().IsKeyDown(Keys.Escape))
  this.Exit();
 
 KeyboardState currentKeyboardState = Keyboard.GetState();
 if (currentKeyboardState != _prevKeyboardState)
 {
  //переключаем флаг отрисовки ограничивающих объемов
  if (currentKeyboardState.IsKeyDown(Keys.B))
  {
   _drawDebug = !_drawDebug;
  }
 }
 
 _prevKeyboardState = currentKeyboardState;
 
 //обновляем все игровые сущности
 for (int i = 0; i < _entities.Count; i++)
 {
  _entities[i].Update(gameTime);
 }
 
 //обновляем камеру
 _camera.Update(gameTime);
 
 //считаем FPS
 _fpsCounter.Update(gameTime);
 
 base.Update(gameTime);
}
 
protected override void Draw(GameTime gameTime)
{
 GraphicsDevice.Clear(Color.CornflowerBlue);
 
 GraphicsDevice.DepthStencilState = DepthStencilState.Default;
 
 for (int i = 0; i < _modelsCount; i++)
 {
  _entities[i].Model.Draw(
   _entities[i].World,
   _camera.View,
   _camera.Projection);
 
  if (_drawDebug)
  {
   _debugRenderer.DrawBoundingBox(ref _entities[i].TransformedBoundingBox, ref _camera.View, ref _camera.Projection, ref _bboxColor);
  }    
 }
 
 _spriteBatch.Begin();
 _spriteBatch.DrawString(_font, string.Format("FPS: {0} Frame time: {1}", _fpsCounter.FramesPerSecond, _fpsCounter.FrameTime), Vector2.Zero, Color.White);
 _spriteBatch.End();
 
 base.Draw(gameTime);
}

 


Итак, сцена готова. Теперь мы можем включать и отключать отрисовку ограничивающих объемов, обновлять и рисовать модели. Приступим к реализации самого Octree.

Построение Octree.

Создайте в проекте игры новую папку SpacePartition и добавьте в нее 2 класса Node и Octree. Но прежде чем писать код, давайте подумаем, что из себя представляют эти 2 класса?

Node (узел) - это объект, содержащий список игровых объектов, список дочерних узлов и ограничивающий объем. Так же, нам понадобится ссылка на родительский узел (если такой есть) - это даст нам возможность переходить по уровням дерева в обоих направлениях (от родителей к потомкам и наоборот).

Octree - это объект, содержащий ссылку на корневой узел, и предоставляющий методы для выполнения запросов. Таже он должен предоставлять методы для построения и обновления дерева.

Вот такие простые требования к классам. Давайте попробуем их реализовать. Начнем с узла:

public class Node
{
 public BoundingBox BoundingBox;
 
 public List<GameEntity> Entities { get; private set; }
 
 public List<Node> Children { get; private set; }
 
 public Node Parent { get; private set; }
 
 public int NestingLevel { get; private set; }
 
 public Node(Node parent, BoundingBox bbox, int nestingLevel)
 {
  Parent = parent;
  BoundingBox = bbox;
  NestingLevel = nestingLevel;
  Entities = new List<GameEntity>();
  Children = new List<Node>();
 }
}


Класс узла достаточно прост, потому что не содержит логики. Все самое интересное будет содержаться в классе Octree. Итак, для начала нам потребуются несколько свойств и полей:

* Root - корневой узел дерева
* _maxNestingLevel - максимальный уровень вложенности узлов.
* _nodeStack - стэк узлов, для обхода дерева. Он нужен для того, чтобы избавиться от рекурсии при обходе дерева.
* _objectNodeMap - карта "объект-узел". Служит для быстрого добавления/удаления объектов. Мы уже говорили, что в нашем дереве каждый объект может быть привязан только к одному узлу, поэтому мы можем составить карту "объект-узел". Благодаря этой карте, имея ссылку на объект, мы можем производить быстрый поиск узла, к которому он привязан.
* _leafs - трехмерный массив листьев дерева. Т.к. дерево равномерное, то его можно представить в виде обычной сетки, где каждая ячейка - это лист дерева (узел самого низкого уровня). С помощью этой сетки можно быстро вычислять в какой лист попал объект, по его координатам.
* _leafSize, _leafMassiveSize - вспомогательные данные для вычисления листа, по координатам объекта.

Это главные члены класса. Давайте опишем их в коде:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Octree
{
 private int _maxNestingLevel;
 
 private Stack<Node> _nodeStack;
 
 private Dictionary<int, Node> _objectNodeMap;
 
 private Node[, ,] _leafs;
 
 private int _leafMassiveSize;
 
 private float _leafSize;
 
 public Node Root { get; private set; }
 
 public Octree(int maxNestingLevel)
 {
  _maxNestingLevel = maxNestingLevel;
  _nodeStack = new Stack<Node>();
  _objectNodeMap = new Dictionary<int, Node>();
 }
}
У нас есть готовая структура дерева. Теперь мы можем построить его узлы. Для этого нам потребуется множество вспомогательных методов. С них и начнем. В класс Utilities добавьте следующие методы:

 

public static void GetCenter(this BoundingBox bbox, out Vector3 center)
{
 center = new Vector3(
  (bbox.Min.X + bbox.Max.X) / 2,
  (bbox.Min.Y + bbox.Max.Y) / 2,
  (bbox.Min.Z + bbox.Max.Z) / 2);
}
 
public static void GetSize(this BoundingBox bbox, out Vector3 size)
{
 size = new Vector3(
  bbox.Max.X - bbox.Min.X,
  bbox.Max.Y - bbox.Min.Y,
  bbox.Max.Z - bbox.Min.Z);
}
 
public static double Round(double value, int digits)
{
 double scale = Math.Pow(10.0, digits);
 double round = Math.Floor(Math.Abs(value) * scale + 0.5);
 return (Math.Sign(value) * round / scale);
}

 

Данные методы позволяют вычислить размер и координаты центра BoundingBox'а.

Также, в самом классе дерева, нам понадобятся следующие вспомогательные методы:
* SplitNode(Node parent) - разделяет указанный узел на 8 дочерних.
* GetLeaf(GameEntity entity) - находит лист дерева, в который входит данная игровая сущность.
* RegistrateEntity(GameEntity entity, Node node) - регистрирует указанную сущность в указанном узле. Этот метод нужен при добавлении сущностей в дерево.
* AddEntity(GameEntity entity) - добавляет указанную сущность в дерево.

Начнем с метода SplitNode. Данный метод должен разделить указанный узел на 8 дочерних, связать родительский и дочерние узлы, а также, если дочерние узлы имеют максимальный уровень вложенности - то занести их в массив листьев дерева. Вот код данного метода:

 

private void SplitNode(Node parent)
{
 Vector3 min = parent.BoundingBox.Min;
 Vector3 max = parent.BoundingBox.Max;
 
 //получаем координаты центра нода
 Vector3 c = (min + max) / 2.0f;
 
 //получаем дочерние ноды
 BoundingBox tempBbox = new BoundingBox();
 
 //левый верхний передний
 tempBbox.Min = new Vector3(min.X, c.Y, min.Z);
 tempBbox.Max = new Vector3(c.X, max.Y, c.Z);
 parent.Children.Add(new Node(parent, tempBbox, parent.NestingLevel + 1));
 
 //правый верхний передний
 tempBbox.Min = new Vector3(c.X, c.Y, min.Z);
 tempBbox.Max = new Vector3(max.X, max.Y, c.Z);
 parent.Children.Add(new Node(parent, tempBbox, parent.NestingLevel + 1));
 
 //левый нижний передний
 tempBbox.Min = new Vector3(min.X, min.Y, min.Z);
 tempBbox.Max = new Vector3(c.X, c.Y, c.Z);
 parent.Children.Add(new Node(parent, tempBbox, parent.NestingLevel + 1));
 
 //правый нижний передний
 tempBbox.Min = new Vector3(c.X, min.Y, min.Z);
 tempBbox.Max = new Vector3(max.X, c.Y, c.Z);
 parent.Children.Add(new Node(parent, tempBbox, parent.NestingLevel + 1));
 
 
 //левый верхний задний
 tempBbox.Min = new Vector3(min.X, c.Y, c.Z);
 tempBbox.Max = new Vector3(c.X, max.Y, max.Z);
 parent.Children.Add(new Node(parent, tempBbox, parent.NestingLevel + 1));
 
 //правый верхний задний
 tempBbox.Min = new Vector3(c.X, c.Y, c.Z);
 tempBbox.Max = new Vector3(max.X, max.Y, max.Z);
 parent.Children.Add(new Node(parent, tempBbox, parent.NestingLevel + 1));
 
 //левый нижний задний
 tempBbox.Min = new Vector3(min.X, min.Y, c.Z);
 tempBbox.Max = new Vector3(c.X, c.Y, max.Z);
 parent.Children.Add(new Node(parent, tempBbox, parent.NestingLevel + 1));
 
 //правый нижний задний
 tempBbox.Min = new Vector3(c.X, min.Y, c.Z);
 tempBbox.Max = new Vector3(max.X, c.Y, max.Z);
 parent.Children.Add(new Node(parent, tempBbox, parent.NestingLevel + 1));
 
 //заносим новые узлы в массив листьев, если уровень вложенности достиг максимального
 if (parent.NestingLevel + 1 >= _maxNestingLevel)
 {
  if (_leafs == null)
  {
   _leafSize = parent.Children[0].BoundingBox.Max.X - parent.Children[0].BoundingBox.Min.X;
   _leafMassiveSize = (int)Utilities.Round(((Root.BoundingBox.Max.X - Root.BoundingBox.Min.X) / _leafSize), 0);
   _leafs = new Node[_leafMassiveSize, _leafMassiveSize, _leafMassiveSize];
  }
  for (int i = 0; i < 8; i++)
  {
   //заносим листы в массив
   Vector3 center;
   parent.Children[i].BoundingBox.GetCenter(out center);
   int x = (int)(Math.Abs(Root.BoundingBox.Min.X - center.X) / _leafSize);
   int y = (int)(Math.Abs(Root.BoundingBox.Min.Y - center.Y) / _leafSize);
   int z = (int)(Math.Abs(Root.BoundingBox.Min.Z - center.Z) / _leafSize);
   _leafs[x, y, z] = parent.Children[i];
  }
 }
}

Следующий метод GetLeaf - производит быстрый (без перебора всего дерева) поиск листа дерева, в который входит указанная сущность. Мы будем добавлять сущность в дерево не сверху вниз, а снизу вверх. Т.е. сначала мы находим лист, в который попала сущность. Если она полностью входит в этот лист, то приписываем ее к этому листу, если нет, переходим на уровень выше и т.д. пока не найдем такой узел, в который данная сущность будет входить полностью (без пересечений BoundingBox'а узла).

 

private Node GetLeaf(GameEntity entity)
{
 Vector3 leafSize;
 BoundingBox bbox = entity.TransformedBoundingBox;
 BoundingBox rootBbox = Root.BoundingBox;
 _leafs[0, 0, 0].BoundingBox.GetSize(out leafSize);
 
 //получаем индексы листа, в котором расположена точка Min bbox'а объекта
 int minX = (int)((bbox.Min.X - rootBbox.Min.X) / leafSize.X);
 if (minX < 0)
 {
  minX = 0;
 }
 
 if (minX > _leafMassiveSize - 1)
 {
  minX = _leafMassiveSize - 1;
 }
 
 int minY = (int)((bbox.Min.Y - rootBbox.Min.Y) / leafSize.Y);
 if (minY < 0)
 {
  minY = 0;
 }
 
 if (minY > _leafMassiveSize - 1)
 {
  minY = _leafMassiveSize - 1;
 }
 
 int minZ = (int)((bbox.Min.Z - rootBbox.Min.Z) / leafSize.Z);
 if (minZ < 0)
 {
  minZ = 0;
 }
 
 if (minZ > _leafMassiveSize - 1)
 {
  minZ = _leafMassiveSize - 1;
 }
 
 return _leafs[minX, minY, minZ];
}

 

Метод RegistrateEntity просто заносит указанную сущность в указанный узел, при этом, добавляя запись в карту "объект-узел":

private void RegistrateEntity(GameEntity entity, Node node)
{
 node.Entities.Add(entity);
 int hashCode = entity.GetHashCode();
 if (_objectNodeMap.ContainsKey(hashCode))
 {
  _objectNodeMap[hashCode] = node;
 }
 else
 {
  _objectNodeMap.Add(hashCode, node);
 }
}


Пожалуй самый интересный метод из вспомогательных - это метод AddEntity. Этот метод ищет узел, в который попала сущность, начиная от самого нижнего уровня листьев дерева и постепенно поднимаясь вверх. Если сущность лежит за пределами BoundingBox'а корневого узла дерева или пересекается с ним, то сущность привязывается к листу. Если сущность полностью входит в один из узлов, то она привязывается к этому узлу:

private void AddEntity(GameEntity entity)
{
 //получаем лист, в котором находится объект
 Node leaf = GetLeaf(entity);
 
 _nodeStack.Clear();
 _nodeStack.Push(leaf);
 
 while (_nodeStack.Count > 0)
 {
  Node node = _nodeStack.Pop();
 
  ContainmentType containmentType = ContainmentType.Disjoint;
 
  node.BoundingBox.Contains(ref entity.TransformedBoundingBox, out containmentType);
  switch (containmentType)
  {
   case ContainmentType.Contains:
    //если объект полностью помещается в узел, то регистрируем его в этом узле
    RegistrateEntity(entity, node);
    break;
 
   case ContainmentType.Disjoint:
    //если объект не пересекается с узлом вообще, то значит объект вышел за пределы дерева
    //поэтому регистрируем его в листе, который вычислили ранее
    RegistrateEntity(entity, leaf);
    break;
 
   case ContainmentType.Intersects:
    //если объект пересекается с узлом, то нужно проверить является ли узел корневым
    if (node.Parent == null)
    {
     //если да, то объект лежит на границе дерева, поэтому регистрируем его
     //в ЛИСТЕ
     RegistrateEntity(entity, leaf);
    }
    else
    {
     //если нет, то передаем объект в родительский узел
     _nodeStack.Push(node.Parent);
    }
    break;
  }
 }
}


Итак, у нас есть все необходимые вспомогательные методы для того, чтобы построить дерево. За построение дерева отвечает метод Build(List entities). Алгоритм построения довольно прост. Сначала мы получаем BoundingBox корневого узла, вычисляя его на основе списка сущностей. Затем нам нужно запустить процесс разделения корневого узла, до тех пор, пока дерево не достигнет максимального уровня вложенности. После этого, мы можем добавлять сущности в дерево по очереди. Вот код метода:

public void Build(List<GameEntity> entities)
{
 if (entities.Count == 0)
 {
  return;
 }
 
 //получаем bbox корневого узла
 BoundingBox rootBbox = entities[0].TransformedBoundingBox;
 foreach (var item in entities)
 {
  rootBbox = BoundingBox.CreateMerged(rootBbox, item.TransformedBoundingBox);
 }
 
 //делаем его правильным кубом
 Vector3 center;
 rootBbox.GetCenter(out center);
 Vector3 size;
 rootBbox.GetSize(out size);
 float maxSize = Math.Max(size.X, Math.Max(size.Y, size.Z)) / 2;
 rootBbox.Min = new Vector3(center.X - maxSize, center.Y - maxSize, center.Z - maxSize);
 rootBbox.Max = new Vector3(center.X + maxSize, center.Y + maxSize, center.Z + maxSize);
 
 //инициализируем дерево
 Root = new Node(null, rootBbox, 0);
 _nodeStack.Clear();
 _nodeStack.Push(Root);
 
 while (_nodeStack.Count > 0)
 {
  Node parent = _nodeStack.Pop();
  if (parent.NestingLevel < _maxNestingLevel)
  {
   SplitNode(parent);
 
   if (parent.NestingLevel + 1 < _maxNestingLevel)
   {
    foreach (var child in parent.Children)
    {
     _nodeStack.Push(child);
    }
   }
  }
 }
 
 //добавляем объекты в дерево
 foreach (var obj in entities)
 {
  AddEntity(obj);
 }
}


Теперь мы можем построить октарное дерево. Добавьте следующий код в класс Game1:

 

private Octree _octree;
...
 
//в конструкторе
_octree = new Octree(4);
...
 
protected override void LoadContent()
{
 _spriteBatch = new SpriteBatch(GraphicsDevice);
 _font = Content.Load<SpriteFont>("Arial");
 Model model = Content.Load<Model>("cats");
 
 //создаем игровые сущности и располагаем их в случайном порядке в пределах куба {-50, -50, -50; 50, 50, 50}
 Random r = new Random((int)DateTime.Now.Ticks);
 for (int i = 0; i < _modelsCount; i++)
 {
  GameEntity entity = new GameEntity();
  entity.Model = model;
  entity.World = Matrix.CreateTranslation(new Vector3((float)r.NextDouble(), (float)r.NextDouble(), -(float)r.NextDouble()) * 100 - Vector3.One * 50);
  entity.OriginalBoundingBox = BoundingBox.CreateFromSphere(model.Meshes[0].BoundingSphere);
  entity.Update(new GameTime());
  _entities.Add(entity);
 }
 
 _octree.Build(_entities);
}
Если вы запустите проект сейчас, то особой разницы не увидите. Давайте, нарисуем структуру нашего дерева. Для этого в класс Octree добавьте следующий код:

 

public void Draw(DebugRenderer debugRenderer, ref Matrix view, ref Matrix projection)
{
 //цвет узлов будет изменяться от белого к красному
 //белый - корневой узел
 //красный - листья
 _nodeStack.Clear();
 _nodeStack.Push(Root);
 
 while (_nodeStack.Count > 0)
 {
  Node node = _nodeStack.Pop();
 
  //рисуем узел, только если он не пустой
  if (node.Entities.Count > 0)
  {
   float nestingLevel = (float)node.NestingLevel / _maxNestingLevel;
   Color c = new Color(1.0f, 1.0f - nestingLevel, 1.0f - nestingLevel);
   debugRenderer.DrawBoundingBox(ref node.BoundingBox, ref view, ref projection, ref c);
  }    
 
  if (node.Children.Count > 0)
  {
   foreach (var item in node.Children)
   {
    _nodeStack.Push(item);
   }
  }
 }
}

 

 

В метод Draw класса Game1 добавьте следующий код:

 

if (_drawDebug)
{
 _octree.Draw(_debugRenderer, ref _camera.View, ref _camera.Projection);
}
Теперь мы можем видеть структуру нашего дерева.

Выполнение запроса к Octree.

Запрос к дереву - это собственно основная цель нашего проекта. Octree должно быстро найти видимые для камеры объекты, при этом пропуская целые поддеревья для оптимизации. Мы создадим новый метод Query(BoundingFrustum frustum, List visibleEntities), который будет находить список видимых объектов для указанной пирамиды (BoundingFrustum).
Давайте подумаем как должен работать данный метод.

* По идее он должен вернуть список, но в этом случае, дерево будет возвращать новый экземпляр списка после каждого запроса. Запрос будет выполняться на каждом кадре, значит если сейчас у нас 90 FPS, то octree будет генерировать 90 списков в секунду. Так нам никакой памяти не хватит, да и сборщик мусора будет работать только на наше дерево. Поэтому мы будем передавать один и тот же список в метод Query, предварительно его очищая.
* Запрос должен начинаться с корневого узла. Если узел не пересекается с пирамидой, то значит он невидим, соответсвенно невидны все его вложенные узлы и объекты, значит дальнейшую проверку делать не надо.
* Если узел полностью входит в пирамиду, значит все его вложенные узлы и объекты видимы, значит проверка тоже не нужна, остается только добавить все вложенные объекты в список.
* Если узел пересекается с пирамидой, то придется проверять все его вложенные узлы и объекты.
Для начала нам потребуется вспомогательный метод GetSubtree(Node node, List visibleEntities), который позволит добавить в список видимых объектов целое поддерево. Мы будем вызывать этот метод в случае, когда узел полностью входит в пирамиду:

private Stack<Node> _subtreeStack;
...
 
// в конструкторе
_subtreeStack = new Stack<Node>();
...
 
private void GetSubtree(Node node, List<GameEntity> visibleEntities)
{
 _subtreeStack.Clear();
 _subtreeStack.Push(node);
 
 while (_subtreeStack.Count > 0)
 {
  Node n = _subtreeStack.Pop();
  visibleEntities.AddRange(n.Entities);
 
  for (int i = 0; i < n.Children.Count; i++)
  {
   _subtreeStack.Push(n.Children[i]);
  }
 }
}

 

Теперь можно реализовать метод Query:

 

public void Query(BoundingFrustum frustum, List<GameEntity> visibleEntities)
{
 ContainmentType containmentType = ContainmentType.Disjoint;
 
 _nodeStack.Clear();
 _nodeStack.Push(Root);
 
 while (_nodeStack.Count > 0)
 {
  Node node = _nodeStack.Pop();
  frustum.Contains(ref node.BoundingBox, out containmentType);
 
  switch (containmentType)
  {
   //если узел полностью входит в пирамиду,
   //то заносим все поддерево в список видимых сущностей
   case ContainmentType.Contains:
    GetSubtree(node, visibleEntities);
    break;
 
   //case ContainmentType.Disjoint:
   // ничего не делаем
   //    break;
 
   //если узел пересекается с пирамидой, то проверяем видимость всех его объектов
   //а вложенные узлы добавляем в стэк для дальнейшей проверки
   case ContainmentType.Intersects:
    ContainmentType entContType = ContainmentType.Disjoint;
    for (int i = 0; i < node.Entities.Count; i++)
    {
     _entityTestCount++;
     entContType = ContainmentType.Disjoint;
     frustum.Contains(ref node.Entities[i].TransformedBoundingBox, out entContType);
     if (entContType != ContainmentType.Disjoint)
     {
      visibleEntities.Add(node.Entities[i]);
     }
    }
 
    for (int i = 0; i < node.Children.Count; i++)
    {
     _nodeStack.Push(node.Children[i]);
    }
 
    break;
  }
 }
}

 

 

Как видите, метод довольно прост. Теперь нужно добавить пирамиду видимости в класс Camera:


 

public BoundingFrustum BoundingFrustum { get; private set; }
...
 
// в конструкторе
BoundingFrustum = new BoundingFrustum(Matrix.Identity);
...
 
// в конце метода Update
BoundingFrustum.Matrix = View * Projection;
Мы уже можем построить запрос. Для этого нужно в Game1 добавить список видимых объектов:
private List<GameEntity> _visibleEntities;
...
 
// в конструкторе
_visibleEntities = new List<GameEntity>();
...
 
protected override void Update(GameTime gameTime)
{
 if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
  Keyboard.GetState().IsKeyDown(Keys.Escape))
  this.Exit();
 
 KeyboardState currentKeyboardState = Keyboard.GetState();
 if (currentKeyboardState != _prevKeyboardState)
 {
  //переключаем флаг отрисовки ограничивающих объемов
  if (currentKeyboardState.IsKeyDown(Keys.B))
  {
   _drawDebug = !_drawDebug;
  }
 }
 
 _prevKeyboardState = currentKeyboardState;
 
 //обновляем все игровые сущности
 for (int i = 0; i < _entities.Count; i++)
 {
  _entities[i].Update(gameTime);
 }
 
 //обновляем камеру
 _camera.Update(gameTime);
 
 //выполняем запрос к дереву
 _visibleEntities.Clear();
 _octree.Query(_camera.BoundingFrustum, _visibleEntities); 
 
 //считаем FPS
 _fpsCounter.Update(gameTime);
 
 base.Update(gameTime);
}
 
protected override void Draw(GameTime gameTime)
{
 GraphicsDevice.Clear(Color.CornflowerBlue);
 
 GraphicsDevice.DepthStencilState = DepthStencilState.Default;
 
 for (int i = 0; i < _visibleEntities.Count; i++)
 {
  _visibleEntities[i].Model.Draw(
   _visibleEntities[i].World,
   _camera.View,
   _camera.Projection);
 
  if (_drawDebug)
  {
   _debugRenderer.DrawBoundingBox(ref _visibleEntities[i].TransformedBoundingBox, ref _camera.View, ref _camera.Projection, ref _bboxColor);
  }
 }
 
 if (_drawDebug)
 {
  _octree.Draw(_debugRenderer, ref _camera.View, ref _camera.Projection);
 }
 
 _spriteBatch.Begin();
 _spriteBatch.DrawString(_font, string.Format("FPS: {0} Frame time: {1}", _fpsCounter.FramesPerSecond, _fpsCounter.FrameTime), Vector2.Zero, Color.White);
 _spriteBatch.End();
 
 base.Draw(gameTime);
}

 

Вот и все. Теперь мы рисуем только те объекты, которые реально видны камере.
Но как это проверить? Давайте выведем debug-информацию из octree:

private int _visibleObjectCount;
 
private int _nodeTestCount;
 
private int _entityTestCount;
 
private long _queryTime;
 
private Stopwatch _timer;
...
 
// в конструкторе
_timer = new Stopwatch();
...
 
public void Query(BoundingFrustum frustum, List<GameEntity> visibleEntities)
{
 _visibleObjectCount = 0;
 _nodeTestCount = 0;
 _entityTestCount = 0;
 _timer.Reset();
 _timer.Start();
 
 ContainmentType containmentType = ContainmentType.Disjoint;
 
 _nodeStack.Clear();
 _nodeStack.Push(Root);
 
 while (_nodeStack.Count > 0)
 {
  Node node = _nodeStack.Pop();
  _nodeTestCount++;
  frustum.Contains(ref node.BoundingBox, out containmentType);
 
  switch (containmentType)
  {
   //если узел полностью входит в пирамиду,
   //то заносим все поддерево в список видимых сущностей
   case ContainmentType.Contains:
    GetSubtree(node, visibleEntities);
    break;
 
   //case ContainmentType.Disjoint:
   // ничего не делаем
   //    break;
 
   //если узел пересекается с пирамидой, то проверяе видимость всех его объектов
   //а вложенные узлы добавляем в стэк для дальнейшей проверки
   case ContainmentType.Intersects:
    ContainmentType entContType = ContainmentType.Disjoint;
    for (int i = 0; i < node.Entities.Count; i++)
    {
     _entityTestCount++;
     entContType = ContainmentType.Disjoint;
     frustum.Contains(ref node.Entities[i].TransformedBoundingBox, out entContType);
     if (entContType != ContainmentType.Disjoint)
     {
      visibleEntities.Add(node.Entities[i]);
     }
    }
 
    for (int i = 0; i < node.Children.Count; i++)
    {
     _nodeStack.Push(node.Children[i]);
    }
 
    break;
  }
 }
 
 _visibleObjectCount = visibleEntities.Count;
 _timer.Stop();
 _queryTime = _timer.ElapsedMilliseconds;
}


В класс Game1 нужно добавить код для вывода информации на экран:

 

//в методе Draw
_spriteBatch.DrawString(_font, string.Format(
	"FPS: {0} Frame time: {1}\n{2}", 
	_fpsCounter.FramesPerSecond, 
	_fpsCounter.FrameTime, 
	_octree.DebugInfo), 
	Vector2.Zero, 
	Color.White);
 

 


Теперь мы можем видеть сколько проверок происходит в запросе, сколько видимых объектов рисуется, какое время занимает выполнение запроса. У меня запрос выполняется мешьше чем за одну миллисекунду - это очень хороший показатель для 5000 объектов.
Если в камеру не попадает ни одного объекта, то моя машина выдает ~1000 FPS, если попадают все 5000 объектов, то ~90 FPS . Согласитесь - это существенный прирост скорости.
Обновление Octree (Dynamic Octree).

Наше дерево будет работать правильно, до тех пор, пока мы не решим двигать объекты по сцене. При передвижении объектов мы также должны перестроить дерево, иначе запрос будет возвращать неправильные результаты, т.к. объеты могут перемещаться из узла в узел. Давайте подумаем, что нам нужно, для того, чтобы обновить дерево? Ну, во-первых нужно знать, какие объекты обновлять, во-вторых - каждый из этих объектов сначала нужно удалить из дерева, в-третьих - каждый из этих объектов нужно добавить в дерево заново, но уже в новый узел.

Мы будем обновлять все наши объекты. Хотя в реальной игре нужно следить, за тем, чтобы дерево обновлялось только для тех объектов, у которых реально изменилась мировая матрица.

У нас уже есть метод добавления новых сущностей в дерево. Теперь нам нужно написать метод удаления сущности из дерева, а уже потом мы напишем метод обновления дерева, который будет обновлять дерево для целого списка объектов.

public bool RemoveObject(GameEntity entity)
{
 int hashCode = entity.GetHashCode();
 if (_objectNodeMap.ContainsKey(hashCode))
 {
  Node node = _objectNodeMap[hashCode];
  bool r1 = node.Entities.Remove(entity);
  bool r2 = _objectNodeMap.Remove(hashCode);
 
  return r1 && r2;
 }
 
 return false;
}
 


При удалении объекта из дерева, мы с помощью карты "объект-узел" находим нужный узел, и удаляем из него объект, а затем удаляем запись об этом объекте из самой карты.

Имея метод удаления сущностей из дерева, мы можем написать метод обновления дерева:

 

public void Update(List<GameEntity> entities)
{
 _timer.Reset();
 _timer.Start();
 
 for (int i = 0; i < entities.Count; i++)
 {
  GameEntity ent = entities[i];
 
  //удаляем объект
  bool result = RemoveObject(ent);
  Debug.Assert(result, "Не удалось удалить объект из дерева.");
 
  //добавляем объект в дерево.
  AddEntity(ent);
 }
 
 _timer.Stop();
 _updateTime = _timer.ElapsedMilliseconds;
}

Нам еще нужо изменить класс Game1 так, чтобы увидеть динамику. Добавьте в него следующий код:

 

private List<Vector3> _moves;
...
 
//в конструкторе
_moves = new List<Vector3>();
...
 
protected override void LoadContent()
{
 _spriteBatch = new SpriteBatch(GraphicsDevice);
 _font = Content.Load<SpriteFont>("Arial");
 Model model = Content.Load<Model>("cats");
 
 //создаем игровые сущности и располагаем их в случайном порядке в пределах куба {-50, -50, -50; 50, 50, 50}
 Random r = new Random((int)DateTime.Now.Ticks);
 for (int i = 0; i < _modelsCount; i++)
 {
  GameEntity entity = new GameEntity();
  entity.Model = model;
  entity.World = Matrix.CreateTranslation(new Vector3((float)r.NextDouble(), (float)r.NextDouble(), -(float)r.NextDouble()) * 100 - Vector3.One * 50);
  entity.OriginalBoundingBox = BoundingBox.CreateFromSphere(model.Meshes[0].BoundingSphere);
  entity.Update(new GameTime());
  _entities.Add(entity);
 
  int x = r.Next(0, 1);
  x = x == 0 ? -1 : 1;
  int y = r.Next(0, 1);
  y = y == 0 ? -1 : 1;
  int z = r.Next(0, 1);
  z = z == 0 ? -1 : 1;
  _moves.Add(new Vector3(x, y, z) * 0.1f);
 }
 
 _octree.Build(_entities);
}
protected override void Update(GameTime gameTime)
{
 if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
  Keyboard.GetState().IsKeyDown(Keys.Escape))
  this.Exit();
 
 KeyboardState currentKeyboardState = Keyboard.GetState();
 if (currentKeyboardState != _prevKeyboardState)
 {
  //переключаем флаг отрисовки ограничивающих объемов
  if (currentKeyboardState.IsKeyDown(Keys.B))
  {
   _drawDebug = !_drawDebug;
  }
 }
 
 _prevKeyboardState = currentKeyboardState;
 
 //обновляем все игровые сущности
 for (int i = 0; i < _entities.Count; i++)
 {
  Vector3 move = _moves[i];
  _entities[i].World.Translation = _entities[i].World.Translation + move;
 
  Vector3 translation = _entities[i].World.Translation;
  if (translation.X > 50 || translation.X < -50)
  {
   move = new Vector3(-move.X, move.Y, move.Z);
  }
  if (translation.Y > 50 || translation.Y < -50)
  {
   move = new Vector3(move.X, -move.Y, move.Z);
  }
  if (translation.Z > 50 || translation.Z < -50)
  {
   move = new Vector3(move.X, move.Y, -move.Z);
  }
 
  _moves[i] = move;
 
  _entities[i].Update(gameTime);
 }
 
 //обновляем дерево
 _octree.Update(_entities);
 
 //выполняем запрос к дереву
 _visibleEntities.Clear();
 _octree.Query(_camera.BoundingFrustum, _visibleEntities);
 
 //обновляем камеру
 _camera.Update(gameTime);
 
 //считаем FPS
 _fpsCounter.Update(gameTime);
 
 base.Update(gameTime);
}

 


Если запустить проект сейчас, то вы увидите как объекты перемещаются в пределах куба, размером 100х100х100. Дерево динамически перестраивается вслед за изменениями сцены. На этом пожалуй все.

Заключение.

Как видно, алгоритмы разбиения пространства могут дать существенный прирост производительности. Я уже упоминал, что некоторые алгоритмы работают быстрее чем Octree (тот же BVH), но только со статическими объектами. Octree дает приемлемый результат как на статике, так и на динамике.

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

1055 Прочтений •  [Octree] [08.08.2012] [Комментариев: 0]
Добавил: Ukraine Vova
Ссылки
HTML: 
[BB Url]: 
Похожие статьи
Название Добавил Добавлено
• Octree Ukraine Vova 08.08.2012
Ни одного комментария? Будешь первым :).
Пожалуйста, авторизуйтесь для добавления комментария.

Проект входит в сеть сайтов «8Gamers Network»

Все права сохранены. 8Gamers.NET © 2011 - 2025

Статьи
Рецензия на Pressure
Рецензия на Pressure
Чтобы обратить на себя внимание, начинающие маленькие разработчики, как правило, уходят в жанры, ...
Рецензия на Lost Chronicles of Zerzura
Рецензия на Lost Chron...
Игры, сделанные без любви и старания, похожи на воздушный шар – оболочка есть, а внутри пусто. Lo...
Рецензия на The Bridge
Рецензия на The Bridge
«Верх» и «низ» в The Bridge — понятия относительные. Прогуливаясь под аркой, можно запросто перей...
Рецензия на SimCity
Рецензия на SimCity
Когда месяц назад состоялся релиз SimCity, по Сети прокатилось цунами народного гнева – глупые ош...
Рецензия на Strategy & Tactics: World War 2
Рецензия на Strategy &...
Название Strategy & Tactics: World War II вряд ли кому-то знакомо. Зато одного взгляда на ее скри...
Рецензия на игру Scribblenauts Unlimited
Рецензия на игру Scrib...
По сложившейся традиции в информационной карточке игры мы приводим в пример несколько похожих игр...
Рецензия на игру Walking Dead: Survival Instinct, The
Рецензия на игру Walki...
Зомби и продукция-по-лицензии — которые и сами по себе не лучшие представители игровой биосферы —...
Обратная связь | RSS | Донейт | Статистика | Команда | Техническая поддержка