netlib.narod.ru< Назад | Оглавление | Далее >

Обнаружение столкновений сеток

Даже использование для представления уровней простых сеток может вызвать большие проблемы, например, в том случае, когда трехмерные объекты сталкиваются с другими объектами мира, или когда пользователь щелкает мышью по сетке (столкновение мыши с сеткой). Например, как вы узнаете, когда ваш игрок или другие свободно передвигающиеся персонажи столкутся со стенами, или когда сетки столкнутся между собой? Обнаружение таких проблем называется обнаружением столкновений (collision detection). В этом разделе мы узнаем как определить, что сетка сталкивается с другой сеткой и как определить, что на эту сетку (с помощью мыши) указал пользователь.

Столкновение с миром

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

Испускание лучей

Чтобы проверить, блокирует ли полигон путь от одной точки до другой, вы создаете воображаемый луч, идущий по этим точкам и проверяете, пересекает ли он плоскость. Помните плоскости? Я говорил о них в разделе «Плоскости и отсечение». Полигон — это просто плоскость с точно заданными размерами. Создав представляющую полигон плоскость, вы можете использовать алгебру для обнаружения пересечений (рис. 8.7).

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

Все это показано на рис. 8.8. На иллюстации вы видите полигон и линию. Линия представляет луч, идущий от начальной точки до конечной. На полпути луч пересекает полигон.


Рис. 8.8. Полигон блокирует путь лучу

Рис. 8.8. Полигон блокирует путь лучу


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

Здесь на помощь приходит D3DX! Единственная функция выполняет проверку пересечения, гарантирует, что пересечение луча с плоскостью происходит внутри полигона, сообщает точные координаты точки пересечения и в качестве подарка возвращает расстояние от начала луча до точки пересечения. Поговорим об этой полезной функции! Итак, что это за функция? Это D3DXIntersect, прототип которой выглядит так:

HRESULT D3DXIntersect(
     LPD3DXBASEMESH    pMesh,          // Сетка для проверки пересечения
     CONST D3DXVECTOR3 *pRayPos,       // Начало луча
     CONST D3DXVECTOR3 *pRayDir,       // Направление луча
     BOOL              *pHit,          // Флаг наличия пересечения
     DWORD             *pFaceIndex,    // Грань, которую пересекает луч
     FLOAT             *pU,
     FLOAT             *pV,
     FLOAT             *pDist,         // Расстояние до точки пересечения
     LPD3DXBUFFER      *ppAllHits,     // Установите NULL
     DWORD             *pCountOfHits); // Установите NULL

Сразу видно, что D3DXIntersect работает с сетками (ID3DXBaseMesh), и это очень хорошо, поскольку вы работаете с объектами сеток. Затем вы видите, что надо указать начальную точку луча (в pRayPos). Для pRayPospRayDir) вы можете использовать макросы D3DXVECTOR3.

Аргумент pRayDir представляет вектор направления, похожий на вектор нормали. Например, чтобы луч был направлен вверх, вы задаете значение –1.0 для компоненты Y. Из остальных аргументов мы будем иметь дело только с pDist. Это указатель на переменную типа FLOAT, в которую будет записано расстояние от начала луча до точки пересечения.

Блокировка пути

Теперь, узнав о делающей все функции проверки пересечения, может протестируем ее? Вот небольшой пример, показывающий функцию D3DXIntersect в действии. Приведенная ниже функция получает указатель на сетку (представляющую ваш уровень) для проверки пересечений, плюс начальную и конечную точки линии, пересечение с которой проверяется. В ответ вы получите расстояние до места пересечения и точные координаты точки пересечения.

BOOL CheckIntersection(ID3DXMesh *Mesh,
                       float XStart, float YStart, float ZStart,
                       float XEnd, float YEnd, float ZEnd,
                       float *Distance)
{
    BOOL Hit;                        // Флаг наличия пересечения
    float u, v, Dist;                // Рабочие переменные и
                                     // расстояние до пересечения
    float XDiff, YDiff, ZDiff, Size; // Разность и размер
    DWORD FaceIndex;                 // Пересеченная грань
    D3DXVECTOR3 vecDir;              // Вектор направления для луча

    // Вычисляем разность между начальной и конечной точками
    XDiff = XEnd - XStart;
    YDiff = YEnd - YStart;
    ZDiff = ZEnd - ZStart;

    // Вычисляем вектор направления
    D3DXVec3Normalize(&vecDir, &D3DXVECTOR3(XDiff, YDiff, ZDiff));

    // Выполняем проверку пересечения
    D3DXIntersect(Mesh,
                  &D3DXVECTOR3(XStart,YStart,ZStart), &vecDir,
                  &Hit, &FaceIndex, &u, &v, &Dist, NULL, NULL);

    // Если обнаружено пересечение, смотрим, находится ли оно
    // в пределах линии (расстояние до точки пересечения
    // должно быть меньше, чем длина линии)
    if(Hit == TRUE) {

        // Получаем длину луча
        Size = (float)sqrt(XDiff*XDiff+YDiff*YDiff+ZDiff*ZDiff);

        // Луч не пересекается с полигоном
        if(Dist > Size)
            Hit = FALSE;
        else {
            // Луч пересекает полигон, сохраняем
            // расстояние до точки пересечения
            if(Length != NULL)
                *Length = Dist;
        }
    }
    // Возвращаем TRUE если пересечение произошло
    // и FALSE если нет
    return Hit;
}

Перемещение вверх и вниз

Одна из дополнительных выгод использования обнаружения столкновений на уровне полигонов заключается в том, что вы можете заставить объект следовать изменениям высоты находящихся под ним полигонов. Другими словами, мы можем воссоздать хождение по поверхности полигонов! Посмотрите, как это здорово! Представьте себе возможность рисовать уровни в программе трехмерного моделирования и не беспокоиться об определении тех областей, по которым могут ходить игроки — сами полигоны определяют это! Это делает работу с сетками квадродеревьев и октодеревьев еще проще.

Для выполнения проверки пересечения с нижележащей поверхностью в движок NodeTree добавлены три функции. Это показанные ниже функции GetClosestHeight, GetHeightAbove и GetHeightBelow:

float GetClosestHeight(ID3DXMesh *Mesh,
                       float XPos, float YPos, float ZPos)
{
    float YAbove, YBelow;

    // Получаем высоту над и под проверяемой точкой
    YAbove = GetHeightAbove(Mesh, XPos, YPos, ZPos);
    YBelow = GetHeightBelow(Mesh, XPos, YPos, ZPos);

    // Смотрим, какая из высот ближе к проверяемой точке
    // и возвращаем это значение
    if(fabs(YAbove-YPos) < fabs(YBelow-YPos))
        return YAbove;   // Ближе высота над, возвращаем ее

    return YBelow;       // Ближе высота под, возвращаем ее
}

float GetHeightBelow(ID3DXMesh *Mesh,
                     float XPos, float YPos, float ZPos)
{
    BOOL  Hit;        // Флаг касания полигона
    float u, v, Dist; // Рабочие переменные и расстояние
                      // до точки пересечения
    DWORD FaceIndex;  // Грань, которую пересекает луч

    // Проводим тест пересечения для сетки
    D3DXIntersect(Mesh,
                  &D3DXVECTOR3(XPos, YPos, ZPos),
                  &D3DXVECTOR3(0.0f, -1.0f, 0.0f),
                  &Hit, &FaceIndex, &u, &v, &Dist, NULL, NULL);

    // Если пересечение произошло, возвращаем ближайшую высоту снизу
    if(Hit == TRUE)
        return YPos - Dist;

    return YPos; // Если пересечения нет, возвращаем полученную высоту
}

float GetHeightAbove(ID3DXMesh *Mesh,
                     float XPos, float YPos, float ZPos)
{
    BOOL  Hit;        // Флаг касания полигона
    float u, v, Dist; // Рабочие переменные и расстояние
                      // до точки пересечения
    DWORD FaceIndex;  // Грань, которую пересекает луч

    // Проводим тест пересечения для сетки
    D3DXIntersect(Mesh,
                  &D3DXVECTOR3(XPos, YPos, ZPos),
                  &D3DXVECTOR3(0.0f, 1.0f, 0.0f), \
                  &Hit, &FaceIndex, &u, &v, &Dist, NULL, NULL);

    // Если пересечение произошло, возвращаем ближайшую высоту сверху
    if(Hit == TRUE)
        return YPos + Dist;

    return YPos; // Если пересечения нет, возвращаем полученную высоту
}

У каждой из трех представленных выше функций есть свое особое предназначение. GetClosestHeight возвращает высоту полигона (координату Y), который находится ближе всего к указанной точке. Например, если вы проверяете точку в трехмерном пространстве (скажем, с координатой Y = 55) и есть полигон на 10 единиц выше точки и другой полигон на 5 единиц ниже ее, то функция GetClosestHeight вернет 50 (потому что нижний полигон ближе к проверяемой точке).

GetHeightAbove и GetHeightBelow сканируют соответствующее направление (верх или низ) и возвращают высоту ближайшего полигона. Используя GetHeightBelow, можно определить где разместить ваши объекты (в плане высоты) в мире. При перемещении объекта вы изменяете его высоту, основываясь на высоте ландшафта под ним. Кроме того, вы можете определить не слишком ли круто наклонен полигон, чтобы перемещаться по нему. Чтобы увидеть этот метод в действии, взгляните на пример использования квадродеревьев и октодеревьев, находящийся на прилагаемом к книге CD-ROM.

Быстрая проверка пересечения

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

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

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

Обнаружение столкновений в классе cNodeTreeMesh

Чтобы усовершенствовать класс cNodeTreeMesh, можно добавить ранее упомянутые функции проверки пересечений и высоты. Теперь, используя пару простых функций, упакованных в замечательный класс, вы можете загрузить любую сетку уровня, и позволить персонажам ходить вокруг, ударяясь о стены и стоя на твердой земле (а не проваливаясь сквозь землю!).

Когда сталкиваются сетки

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

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

Перед тем, как начать делать это, вам надо разобраться с несколькими вещами, включая недостатки использования ограничивающих сфер. Взгляните на рис. 8.9, где показаны два монстра. У них есть хвосты — очень длинные хвосты. Они влияют на общий размер ограничивающей сферы сетки — сферы будут большими, чтобы охватить всю сетку (включая хвост). Если вы будете передвигать двух монстров на рис 8.9, то заметите, что две ограничивающие сферы могут пересекаться, хотя сами монстры не касаются друг друга.


Рис. 8.9. Сетки, содержащие выступающие части

Рис. 8.9. Сетки, содержащие выступающие части (такие, как хвосты монстров) могут при обнаружении столкновений использовать чрезмерно большие ограничивающие сферы


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

Оставив эту проблему в стороне, можно создать единую функцию, проверяющую пересекаются ли две ограничивающие сферы:

BOOL CheckSphereIntersect(
              float XCenter1, float YCenter1, float ZCenter1,
              float Radius1,
              float XCenter2, float YCenter2, float ZCenter2,
              float Radius2)
{
    float XDiff, YDiff, ZDiff, Distance;

    // Вычисляем расстояние между центрами
    XDiff = (float)fabs(XCenter2-XCenter1);
    YDiff = (float)fabs(YCenter2-YCenter1);
    ZDiff = (float)fabs(ZCenter2-ZCenter1);

    Distance = (float)sqrt(XDiff*XDiff+YDiff*YDiff+ZDiff*ZDiff);

    // Возвращаем TRUE, если две сферы пересекаются
    if(Distance <= (Radius1 + Radius2))
        return TRUE;

    // Нет пересечения
    return FALSE;
}

Если вызвать показанную функцию, передав ей координаты центра и радиусы двух ограничивающих сфер, она вернет TRUE, если сферы пересекаются, и FALSE, если сферы не пересекаются. Чтобы определить, пересекаются ли две сферы, мы вычисляем расстояние между их центрами, которое в случае пересечения должно быть меньше или равно сумме радиусов.

СОВЕТ
Чтобы оптимизировать функцию CheckSphereIntersect, можно убрать из кода вызов функции sqrt, как показано ниже:
Distance = XDiff*XDiff+YDiff*YDiff+ZDiff*ZDiff;
float RadiusDistance = (Radius1+Radius2)*(Radius1+Radius2)*3.0f;

// Возвращаем TRUE, если сферы пересекаются
if(Distance <= RadiusDistance)
    return TRUE;

return FALSE; // Нет пересечения
Расстояние сравнивается с произведением суммы радиусов без использования функции sqrt для вычисления действительного расстояния между центральными точками.

Щелчки мыши и сетки

В последнем исследовании пересечений сеток мы сосредоточимся на возможности, которая наверняка потребуется вам при работе с трехмерной графикой: возможности щелкнуть по сетке и точно узнать, какая грань была выбрана. Вы уже знаете, как просканировать сетку, чтобы увидеть, какой полигон пересекает луч (посмотрите раздел «Испускание лучей» ранее в этой главе) — теперь вам надо сформировать луч, проходящий через позицию курсора мыши в трехмерной сцене и посмотреть, на какой полигон указывает мышь.

Чтобы точно определить, по какому полигону сетки щелкнул игрок, помимо использования функции D3DXIntersect (которая была описана в разделе «Испускание лучей» ранее в этой главе), вы воспользуетесь ее аргументом DWORD *PFaceIndex. Это указатель на переменную типа DWORD, которая будет содержать индекс грани, пересекаемой выпущенным вами лучом.

Обратите внимание, что если вы испускаете луч из воображаемой точки размещения зрителя (центра экрана) к курсору мыши, вам надо проверить на столкновение каждый полигон сцены. Ближайшая к зрителю точка пересечения (та, расстояние до которой меньше всего) и даст полигон, по которому щелкнул пользователь. Все это можно вычислить с помощью следующего кода (где используется код из примеров DirectX SDK):

// Graphics = ранее инициализированный объект cGraphics
// Mouse = ранее инициализированный объект мыши cInputDevice
D3DXVECTOR3 vecRay, vecDir;   // Местоположение и направление луча
D3DXVECTOR3 v;                // Временный вектор
D3DXMATRIX  matProj, matView; // Матрицы проекции и вида
D3DXMATRIX  m;                // Временная матрица

// Получаем текущие преобразования проекции и вида
Graphics.GetDeviceCOM()->GetTransform(D3DTS_PROJECTION,
                                      &matProj);
Graphics.GetDeviceCOM()->GetTransform(D3DTS_VIEW,
                                      &matView);

// Инвертируем матрицу вида
D3DXMatrixInverse(&m, NULL, &matView);

// Фиксируем координаты мыши (подготавливаемся к чтению)
Mouse.Read();

// Вычисляем вектор луча выбора в экранном пространстве
v.x = (((2.0f * Mouse.GetXPos()) / Graphics.GetWidth()) -
      1) / matProj._11;
v.y = -(((2.0f * Mouse.GetYPos()) / Graphics.GetHeight()) -
      1) / matProj._22;
v.z = 1.0f;

// Преобразуем луч в экранном пространстве
vecRay.x = m._41;
vecRay.y = m._42;
vecRay.z = m._43;
vecDir.x = v.x*m._11 + v.y*m._21 + v.z*m._31;
vecDir.y = v.x*m._12 + v.y*m._22 + v.z*m._32;
vecDir.z = v.x*m._13 + v.y*m._23 + v.z*m._33;

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

// Mesh = ранее загруженный объект cMesh
ID3DXMesh *pMesh;
BOOL      Hit;
DWORD     Face;
float     u, v, Dist;

// Получаем указатель на объект ID3DXMesh из cMesh
pMesh = Mesh->GetParentMesh()->m_Mesh;

// Вызываем показанный ранее код для получения
// векторов луча и проверяем пересечение

ID3DXIntersect(pMesh, &vecRay, &vecDir, &Hit, &Face,
               &u, &v, &Dist, NULL, NULL);

// Если Hit равно TRUE, пользователь щелкнул по сетке

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


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz