Войти
ПрограммированиеСтатьиГрафика

Поиск ближайших объектов.

Автор:

При разработке приложений, работающих с 3D графикой, часто приходится решать три задачи:
1) Поиск объектов из множества S, находящихся в радиусе R от точки C.
2) Определение, какие объекты из множества рисуемых объектов попадают во "фрустум" (пирамиду, задающую область видимости) камеры.
3) Проверка пересечения геометрического примитива (обычно сферы, AABB или направленного отрезка) с множеством объектов S.

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

Изображение
Рисунок 1

Рассмотрим решение задачи проверки пересечения отрезка с множеством сфер. На рисунке 1 изображены отрезок и проекция множества сфер на плоскость. В начале выделяются сферы, лежащие на расстоянии меньшем или равном d от середины отрезка, где d равно половине длины отрезка. Эта область обозначена красным цветом. Затем выполняется проверка пересечения отрезка с объектами, попадающими в эту область. Безусловно, задачу можно решить и прямым перебором, но, в случае большого количества сфер, время вычисления может не устроить. Для оптимального, с точки зрения быстродействия, поиска ближайших объектов применяют специальные структуры данных. Например, для мира, описываемого "боксом" размером (dx,dy,dz), где dz<<dx, dz<<dy, а объекты, которого, описываются сферами примерно равного радиуса, и распределение объектов в пространстве равномерно, можно применить 2D сетку на плоскости XY. Для этого спроецируем все множество сфер на плоскость XY, и в каждую ячейку, в которую попадает сфера, запишем ссылку. Затем достаточно спроецировать интересующую нас область на плоскость XY, выделить ячейки, покрываемые этой областью, и осуществить перебор тех объектов, на которые ссылаются ячейки. Как видно из рисунка 2, сфера может оказаться в нескольких ячейках, поэтому, для избежания повторных проверок с одной и той же сферой, проверенную сферу маркируют специальным образом (например, записывают номер текущего кадра). Несмотря на свою примитивность, данный подход не имеет конкурентов по быстродействию в случае выполнения условия dz<<dx, dz<<dy и равномерного распределения объектов, и при этом расходует достаточно мало памяти.

Изображение 
Рисунок 2

Если распределение объектов неравномерно, а условия dz<<dx, dz<<dy выполняются, для экономии памяти используют деревья на плоскости, например quad tree или модификации k-D tree и BSP tree для плоскости. Поскольку речь зашла о k-D tree и BSP tree, правильнее перейти к пространственным k-D и BSP деревьям, которые в 3D пространстве мало проигрывают своему 2D аналогу по быстродействию и расходуют практически столько же памяти. Также можно использовать 3D сетку (малоэффективна из-за большого расхода памяти) или octant tree. При использовании пространственных деревьев, условия dz<<dx, dz<<dy и равномерное распределение объектов в пространстве становятся необязательными. Эти структуры данных расходуют мало памяти и достаточно эффективны по быстродействию, при этом решают задачу в общем случае. Ниже будет предложен вариант k-D деревьев с расширенной функциональностью, позволяющий избежать множественной проверки одного и того же объекта (в случае попадания на границу подпространств), и эффективно работающий с динамическими объектами. На рисунке 3 показана типичная картина разбиения пространства с помощью k-D деревьев.

Изображение
Рисунок 3

Постановка задачи: из множества сфер S необходимо выделить подмножество S1, которое пересекается со сферой s0. При этом считаем, что часть сфер выполняет некоторое (небольшое) перемещение в пространстве в дискретные моменты времени. Также введем три необязательных дополнительных условия, позволяющих уменьшить время работы функции проверки пересечения и сэкономить память:
1) Предположим, что нам известен "бокс", описывающий множество S.
2) Предположим, что нам известен радиус наибольшей сферы из множества S.
3) Предположим, что мы можем сделать оценку плотности распределения сфер внутри описывающего "бокса".
Выполнить эти три условия на практике достаточно легко, но, как уже было сказано выше, четкое следование им необязательно.

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

Изображение 
Рисунок 4

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

Введем следующие операции необходимые для работы с деревом:
1)  Добавление новой сферы.
2)  Перемещение сферы в пространстве, с изменением структуры дерева
3)  Удаление сферы
4)  Поиск сфер пересекающихся с заданной сферой.
В отличие от поиска, все остальные операции требуют изменения структуры дерева.

Алгоритм добавление новой сферы во множество сфер, представленное деревом:
1)  Для подпространства определенного "боксом", лежащим вдоль осей координат, вычисляем максимальное измерение.
2)  Если в дереве нет узла, представляющего это подпространство, создаем новый узел и запоминаем в нем "бокс" и индекс максимального измерения.
3)  Вычисляем ширину общей области для двух дочерних подпространств, BorderHalfWidth=min(MaxR,MaxBoxDimension*Density*0.25f), где MaxR - радиус наибольшей сферы, Density - безразмерная величина, определяющая плотность распределения сфер.
4)  Если радиус сферы превышает половину ширины общей области, прикрепляем сферу к узлу дерева и выходим из рекурсии.
5)  Определяем дочернее подпространство, в которое попадает сфера, и продолжаем для него рекурсию, начиная с пункта 1.

Алгоритм удаления сферы из множества:
1)  Удаляем элемент из списка прикрепленного к узлу дерева
2)  Если список прикрепленных к узлу элементов пуст, и у узла нет детей, удаляем узел, иначе выходим из цикла.
3)  Поднимаемся по дереву к родительскому узлу и переходим к пункту 2.

Алгоритм перемещения сферы в пространстве с изменением структуры дерева:
1)  Если перемещение сферы оказалось незначительным, т.е. сфера осталась внутри подпространства, определяемого "боксом" узла дерева, просто запоминаем новое положение и выходим.
2)  Удаляем элемент из списка, прикрепленного к узлу дерева
3)  Пока сфера не полностью попадает в подпространство, представленное "боксом" узла, поднимаемся по дереву с проверкой: если у узла нет детей и список элементов пуст, удаляем узел.
4)  Для найденного узла (он представляет содержащее сферу подпространство),  выполняем алгоритм добавления сферы.

Алгоритм проверки пересечения сферы с множеством сфер:
1)  Проверяем пересечение со всеми элементами списка прикрепленного к узлу дерева.
2)  Простым сравнением определяем, попадает ли сфера в позитивное подпространство, и, если попадает, продолжаем рекурсию с пункта 1 для узла, представляющего позитивное подпространство.
3)  Простым сравнением определяем, попадает ли сфера в негативное подпространство, и, если попадает, продолжаем рекурсию с пункта 1 для узла, представляющего негативное подпространство.

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

class SPHERE_SET_TREE
{
public:
  class POSITION;
  class ITEM;
  class NODE;

  // Структура задает элемент множества сфер
  struct SPHERE
  {
    void* pData; // Указатель на данные пользователя, шаблоны не применяются для сокращения записи
    POINT3D Center; // Центр сферы
    float Radius; // Радиус сферы

    SPHERE( void* const pData_=NULL, const POINT3D& Center_=POINT3D(0,0,0), const float Radius_=0) 
      : pData(pData_), Center(Center_), Radius(Radius_) {}
  };
  
  // Тип функции, которая принимает уведомление о пересечении
  typedef void (*SSTCALLBACK)(const SPHERE&);


  // Добавляет новую сферу в множество
  POSITION Push( const SPHERE& Sphere );
  // Удаляет сферу из множества
  void Pop( const POSITION& Pos );
  // Перемещает одну из сфер множества в пространстве с получением новой позиции в дереве
  void Move( const SPHERE& Sphere, POSITION& Pos );
  // Удаляет все сферы из множества
  void Clear();

  // Проверяет пересечение сферы "Sphere" с множеством сфер, 
  // заданным объектом класса SPHERE_SET_TREE
  void TestIntersection( const SPHERE& Sphere, const SSTCALLBACK Callback ) const;


  SPHERE_SET_TREE( const AABB& BoundingBox_, // "бокс" описывающий множество
                   const float MaxSphereRadius_,  // радиус наибольшей сферы
                   const float Density_ // Плотность распределения сфер в пространстве, (0,1), 
                                        // 0-низкая, 1-высокая
                   );
  ~SPHERE_SET_TREE() { Clear(); }


  // --------------------------------------------------------------------------------------------
private:
  NODE* pRootNode;
  const AABB BoundingBox;
  const float MaxSphereRadius;
  const float Density;

  SPHERE_SET_TREE( const SPHERE_SET_TREE& ); //запрет копирования
  SPHERE_SET_TREE& operator = ( const SPHERE_SET_TREE& );//запрет присваивания

  POSITION RecursivePush( NODE*& pNode, NODE* const pParent, const AABB& B, const SPHERE& Sphere);
  void DeleteItem( const POSITION& Pos ); //удаляет элемент из списка, прикрепленного к узлу
  bool DeleteNode( NODE*& pNode ); //удаляет узел дерева
  void RecursiveTestIntersection( const NODE* const pNode, const SPHERE& Sphere, 
                                  const SSTCALLBACK Callback ) const;
  bool IsSphereInsideBox( const SPHERE& Sphere, const AABB& Box ) const;//проверяет находится ли
                                                                        //сфера внутри "бокса"
  bool TestIntersection( const SPHERE&, const SPHERE& ) const;//проверяет пересечение двух сфер
};


// задает позицию элемента множества в дереве
class SPHERE_SET_TREE::POSITION
{
public:
  POSITION() : pNode(NULL), pItem(NULL) {}

private:
  friend SPHERE_SET_TREE;

  NODE* pNode;
  ITEM* pItem;

  POSITION( NODE* const pNode_, ITEM* const pItem_ ) : pNode(pNode_), pItem(pItem_) {}
};

// Внутреннее представление элемента множества
class SPHERE_SET_TREE::ITEM
{
public:
  SPHERE Sphere;
  ITEM* pPrev, * pNext;

  ITEM( const SPHERE& Sphere_, ITEM* const pNext_ ) : Sphere(Sphere_),pPrev(NULL),pNext(pNext_){}
  ~ITEM() { delete pNext; }

private:
  ITEM( const ITEM& );//запрет копирования
  ITEM& operator=( const ITEM& );//запрет присваивания
};

// Узел дерева
class SPHERE_SET_TREE::NODE
{
public:
  const AABB B;
  const int IndexOfMaxBoxDimension;
  ITEM* pItem;
  NODE* pParent, * pPositive, * pNegative;

  NODE( const AABB& B_, const int IndexOfMaxBoxDimension_, NODE* const pParent_ );
  ~NODE();

  bool IsEmpty() const { return !pItem && !pPositive && !pNegative; }

private:
  NODE( const NODE& );//запрет копирования
  NODE& operator=( const NODE& );//запрет присваивания
};


SPHERE_SET_TREE::NODE::NODE(const AABB& B_,const int IndexOfMaxBoxDimension_,NODE* const pParent_)
  : B(B_), IndexOfMaxBoxDimension(IndexOfMaxBoxDimension_), 
    pItem(NULL), pParent(pParent_), pPositive(NULL), pNegative(NULL) {}

SPHERE_SET_TREE::NODE::~NODE() 
{
  delete pItem; delete pPositive; delete pNegative;
}


SPHERE_SET_TREE::POSITION SPHERE_SET_TREE::RecursivePush( NODE*& pNode, NODE* const pParent, 
                                                          const AABB& B, const SPHERE& Sphere  )
{
  // Ищем максимальный размер бокса
  int IndexOfMaxBoxDimension=0;
  { const POINT3D Size(B.Size());float MaxBoxDimension=Size[0];
    for( int i=1; i<3; ++i ) if( Size[i]>MaxBoxDimension ) 
    { 
      MaxBoxDimension=Size[i]; 
      IndexOfMaxBoxDimension=i; 
    } 
  }

  // Если эта часть пространства не описана деревом, строим новый узел
  if( !pNode ) pNode=new NODE(B,IndexOfMaxBoxDimension,pParent); 

  assert( B.Equal(pNode->B) );
  assert( IndexOfMaxBoxDimension==pNode->IndexOfMaxBoxDimension );

  // Вычисляем ширину общей области двух подпространств
  const float MaxBoxDimension=B.Max[IndexOfMaxBoxDimension]-B.Min[IndexOfMaxBoxDimension],
              BorderHalfWidth=std::min(float(MaxSphereRadius),MaxBoxDimension*Density*0.25f);

  if( Sphere.Radius>=BorderHalfWidth ) //проверяем соотношение радиуса сферы и ширины границы
  { // прикрепляем сферу к узлу дерева
    pNode->pItem=new ITEM(Sphere,pNode->pItem); 
    if( pNode->pItem->pNext ) pNode->pItem->pNext->pPrev=pNode->pItem;
    return POSITION(pNode,pNode->pItem);
  }
  else
  { // продолжаем разбиение пространства
    AABB B2(B);
    const float MaxBoxDimensionCenter=
                (B.Max[IndexOfMaxBoxDimension]+B.Min[IndexOfMaxBoxDimension])*0.5f,
                UnusedSpace=0.5f*MaxBoxDimension-BorderHalfWidth;

    if( Sphere.Center[IndexOfMaxBoxDimension]>MaxBoxDimensionCenter )
    {
      B2.Min[IndexOfMaxBoxDimension]+=UnusedSpace;
      return RecursivePush(pNode->pPositive,pNode,B2,Sphere);
    }
    else
    {
      B2.Max[IndexOfMaxBoxDimension]-=UnusedSpace;
      return RecursivePush(pNode->pNegative,pNode,B2,Sphere);
    }
  }
}

SPHERE_SET_TREE::POSITION SPHERE_SET_TREE::Push( const SPHERE& Sphere )
{
  assert( Sphere.Radius>0 );

  return RecursivePush(pRootNode,NULL,BoundingBox,Sphere);
}

// удаляет элемент, при этом корректирует двусвязный список
void SPHERE_SET_TREE::DeleteItem( const POSITION& Pos )
{
  assert( Pos.pNode && Pos.pItem );

  if( Pos.pItem->pPrev ) 
    Pos.pItem->pPrev->pNext=Pos.pItem->pNext;
  else                 
    Pos.pNode->pItem=Pos.pItem->pNext;

  if( Pos.pItem->pNext )
  { 
    Pos.pItem->pNext->pPrev=Pos.pItem->pPrev; 
    Pos.pItem->pNext=NULL;
  }

  delete Pos.pItem;
}

// Если у узла нет детей и прикрепленных элементов, удаляет узел и корректирует дерево
bool SPHERE_SET_TREE::DeleteNode( NODE*& pNode )
{
  if( pNode && pNode->IsEmpty() )
  {
    if( pNode->pParent )
    {
      if( pNode->pParent->pPositive==pNode ) pNode->pParent->pPositive=NULL;
      if( pNode->pParent->pNegative==pNode ) pNode->pParent->pNegative=NULL;

      NODE* const pNode2=pNode;
      pNode=pNode->pParent;
      delete pNode2;
    }
    else
    {
      assert( pRootNode==pNode );
      pRootNode=NULL;
      delete pNode;
      pNode=NULL;
    }
    return true;
  }
  return false;
}

// Удаляет элемент и корректирует дерево
void SPHERE_SET_TREE::Pop( const POSITION& Pos )
{
  assert( Pos.pNode && Pos.pItem );

  DeleteItem(Pos);
  for( NODE* pNode=Pos.pNode; DeleteNode(pNode); );
}

// Перемещает сферу в пространстве, перестроение дерева минимально, 
// для незначительного перемещения
void SPHERE_SET_TREE::Move( const SPHERE& Sphere, POSITION& Pos )
{
  assert( Sphere.Radius>0 );
  assert( Pos.pNode && Pos.pItem );

  if( IsSphereInsideBox(Sphere,Pos.pNode->B) )
  {
    Pos.pItem->Sphere=Sphere;
  }
  else
  {
    DeleteItem(Pos);

    NODE* pNode=Pos.pNode;
    if( !DeleteNode(pNode) ) pNode=pNode->pParent;

    while( pNode && !IsSphereInsideBox(Sphere,pNode->B) )
      if( !DeleteNode(pNode) ) pNode=pNode->pParent;

    if( !pNode ) Pos=Push(Sphere);
    else         Pos=RecursivePush(pNode,pNode->pParent,pNode->B,Sphere);
  }
}

void SPHERE_SET_TREE::Clear() 
{ 
  delete pRootNode; pRootNode=NULL; 
}

SPHERE_SET_TREE::SPHERE_SET_TREE( const AABB& BoundingBox_, const float MaxSphereRadius_, 
                                  const float Density_ )
  : pRootNode(NULL), BoundingBox(BoundingBox_),MaxSphereRadius(MaxSphereRadius_),Density(Density_)
{}

// Рекурсивный проход по дереву для определения пересечения, 
// скорость работы функции достаточно высока из-за простых сравнений
void SPHERE_SET_TREE::RecursiveTestIntersection( const NODE* const pNode, const SPHERE& Sphere,
                                                 const SSTCALLBACK Callback ) const
{
  assert( pNode && !pNode->IsEmpty() );

  for( const ITEM* pItem=pNode->pItem; pItem; pItem=pItem->pNext )
    if( TestIntersection(Sphere,pItem->Sphere) )
      Callback(pItem->Sphere); // Callback вызывается в случае пересечения

  const int IndexOfMaxBoxDimension=pNode->IndexOfMaxBoxDimension;

  if( pNode->pPositive && Sphere.Center[IndexOfMaxBoxDimension]+Sphere.Radius>=
                          pNode->pPositive->B.Min[IndexOfMaxBoxDimension] )
    RecursiveTestIntersection(pNode->pPositive,Sphere,Callback); //продолжаем в позитивном 
                                                                 //подпространстве

  if( pNode->pNegative && Sphere.Center[IndexOfMaxBoxDimension]-Sphere.Radius<=
                          pNode->pNegative->B.Max[IndexOfMaxBoxDimension] )
    RecursiveTestIntersection(pNode->pNegative,Sphere,Callback); //продолжаем в негативном 
                                                                 //подпространстве
}

void SPHERE_SET_TREE::TestIntersection( const SPHERE& Sphere, const SSTCALLBACK Callback ) const
{
  if( pRootNode )
    RecursiveTestIntersection(pRootNode,Sphere,Callback);
}

Интересные материалы о пространственных деревьях:
http://sampl.eng.ohio-state.edu/~dalleyg/talks/Spatial%20Subdivis… echniques.ppt
http://vega.icu.ac.kr/~ailab/courses/2001/ICE710/multiD%20access%… -Gaede-98.pdf

7 марта 2003 (Обновление: 23 мар 2011)