Подтвердить что ты не робот

Внедрение комплексной камеры, основанной на поворотах

Я использую 3D-движок для пространственной визуализации и пишу камеру со следующими функциями навигации:

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

Фотокамера не должна переворачиваться - то есть "вверх" остается вверх. Из-за этого я представляю камеру с местоположением и двумя углами, вращения вокруг осей X и Y (Z будет рулон.) Затем матрицу просмотра пересчитывают с использованием местоположения камеры и этих двух углов. Это отлично работает для панорамирования и поворота глаза, но не для вращения вокруг произвольной точки. Вместо этого я получаю следующее поведение:

  • Сам глаз, по-видимому, движется дальше вверх или вниз, чем он должен
  • Глаз не перемещается вверх или вниз, когда m_dRotationX равно 0 или pi. (Gimbal lock? Как я могу избежать этого?)
  • Вращение глаза инвертируется (изменение вращения заставляет его смотреть дальше, когда он должен смотреть дальше вниз, вниз, когда он должен смотреть дальше), когда m_dRotationX находится между pi и 2pi.

(a) Что вызывает этот "дрейф" во вращении?

Это может быть gimbal lock. Если это так, то стандартный ответ на этот вопрос - "использовать кватернионы для представления вращения", много раз говорил здесь о SO (1, 2, 3, но, к сожалению, без конкретных деталей (. Это лучший ответ, который я нашел до сих пор, он редок.) Я изо всех сил пытался реализовать камера с использованием кватернионов, сочетающая два вышеперечисленных типа вращения. Я, по сути, строю кватернион, используя два вращения, но комментатор ниже сказал, что нет никакой причины - прекрасно сразу создать матрицу.

Это происходит при изменении поворотов X и Y (которые представляют направление просмотра камеры) при вращении вокруг точки, но не происходит просто при прямом изменении поворотов, т.е. вращении камеры вокруг себя. Для меня это не имеет смысла. Это те же значения.

(б) Будет ли другой подход (например, кватернионы) лучше для этой камеры? Если да, то как мне реализовать все три функции навигации камеры выше?

Если другой подход будет лучше, тогда, пожалуйста, рассмотрите вопрос о конкретном реализованном примере этого подхода. (Я использую DirectX9 и С++ и D3DX * библиотеку, которую предоставляет SDK.) В этом втором случае я добавлю и награду щедростью через пару дней, когда я могу добавить ее к вопросу. Это может звучать так, как будто я прыгаю с пистолетом, но я нахожусь вовремя, и вам нужно быстро реализовать или решить эту проблему (это коммерческий проект с ограниченным сроком.) Подробный ответ также улучшит архивы SO, поскольку большинство ответы на камеру, которые я читал до сих пор, являются легкими для кода.

Спасибо за вашу помощь:)


Некоторые разъяснения

Спасибо за комментарии и ответ! Я попытаюсь прояснить несколько вещей о проблеме:

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

  • Комментарий JCooper утверждает, что я страдаю от блокировки карданного вала, и мне нужно:

добавьте еще одно вращение на ваше преобразование, которое вращает eyePos полностью в плоскости y-z, прежде чем применять преобразование, и затем другой поворот, который перемещает его назад. Повернуть вокруг y по следующему углу непосредственно перед и после применения матрицу валкового вала (один из углов нужно будет отменить; попробовать это самый быстрый способ решить, какой). double fixAngle = atan2(oEyeTranslated.z,oEyeTranslated.x);

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

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

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


Мой текущий код

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

// Adjust this to change rotation speed when dragging (units are radians per pixel mouse moves)
// This is both rotating the eye, and rotating around a point
static const double dRotatePixelScale = 0.001;
// Adjust this to change pan speed (units are meters per pixel mouse moves)
static const double dPanPixelScale = 0.15;

switch (m_eCurrentNavigation) {
    case ENavigation::eRotatePoint: {
        // Rotating around m_oRotateAroundPos
        const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dRotatePixelScale * D3DX_PI;
        const double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dRotatePixelScale * D3DX_PI;

        // To rotate around the point, translate so the point is at (0,0,0) (this makes the point
        // the origin so the eye rotates around the origin), rotate, translate back
        // However, the camera is represented as an eye plus two (X and Y) rotation angles
        // This needs to keep the same relative rotation.

        // Rotate the eye around the point
        const D3DXVECTOR3 oEyeTranslated = m_oEyePos - m_oRotateAroundPos;
        D3DXMATRIX oRotationMatrix;
        D3DXMatrixRotationYawPitchRoll(&oRotationMatrix, dX, dY, 0.0);
        D3DXVECTOR4 oEyeRotated;
        D3DXVec3Transform(&oEyeRotated, &oEyeTranslated, &oRotationMatrix);
        m_oEyePos = D3DXVECTOR3(oEyeRotated.x, oEyeRotated.y, oEyeRotated.z) + m_oRotateAroundPos;

        // Increment rotation to keep the same relative look angles
        RotateXAxis(dX);
        RotateYAxis(dY);
        break;
    }
    case ENavigation::ePanPlane: {
        const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dPanPixelScale;
        const double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dPanPixelScale;
        m_oEyePos += GetXAxis() * dX; // GetX/YAxis reads from the view matrix, so increments correctly
        m_oEyePos += GetYAxis() * -dY; // Inverted compared to screen coords
        break;
    }
    case ENavigation::eRotateEye: {
        // Rotate in radians around local (camera not scene space) X and Y axes
        const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dRotatePixelScale * D3DX_PI;
        const double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dRotatePixelScale * D3DX_PI;
        RotateXAxis(dX);
        RotateYAxis(dY);
        break;
    }

Методы RotateXAxis и RotateYAxis очень просты:

void Camera::RotateXAxis(const double dRadians) {
    m_dRotationX += dRadians;
    m_dRotationX = fmod(m_dRotationX, 2 * D3DX_PI); // Keep in valid circular range
}

void Camera::RotateYAxis(const double dRadians) {
    m_dRotationY += dRadians;

    // Limit it so you don't rotate around when looking up and down
    m_dRotationY = std::min(m_dRotationY, D3DX_PI * 0.49); // Almost fully up
    m_dRotationY = std::max(m_dRotationY, D3DX_PI * -0.49); // Almost fully down
}

И для создания матрицы вида из этого:

void Camera::UpdateView() const {
    const D3DXVECTOR3 oEyePos(GetEyePos());
    const D3DXVECTOR3 oUpVector(0.0f, 1.0f, 0.0f); // Keep up "up", always.

    // Generate a rotation matrix via a quaternion
    D3DXQUATERNION oRotationQuat;
    D3DXQuaternionRotationYawPitchRoll(&oRotationQuat, m_dRotationX, m_dRotationY, 0.0);
    D3DXMATRIX oRotationMatrix;
    D3DXMatrixRotationQuaternion(&oRotationMatrix, &oRotationQuat);

    // Generate view matrix by looking at a point 1 unit ahead of the eye (transformed by the above
    // rotation)
    D3DXVECTOR3 oForward(0.0, 0.0, 1.0);
    D3DXVECTOR4 oForward4;
    D3DXVec3Transform(&oForward4, &oForward, &oRotationMatrix);
    D3DXVECTOR3 oTarget = oEyePos + D3DXVECTOR3(oForward4.x, oForward4.y, oForward4.z); // eye pos + look vector = look target position
    D3DXMatrixLookAtLH(&m_oViewMatrix, &oEyePos, &oTarget, &oUpVector);
}
4b9b3361

Ответ 1

Мне кажется, что "Roll" не может быть возможным, учитывая то, как вы формируете свою матрицу представлений. Независимо от всего остального кода (некоторые из которых выглядят немного забавно), вызов D3DXMatrixLookAtLH(&m_oViewMatrix, &oEyePos, &oTarget, &oUpVector); должен создать матрицу без рулона, если задан [0,1,0] как вектор "Вверх", если oTarget-oEyePos не будет параллельным вектор. Это не похоже на то, что вы ограничиваете m_dRotationY в пределах (-.49pi,+. 49pi).

Возможно, вы можете уточнить, как вы знаете, что происходит "roll". У вас есть наземная плоскость, а линия горизонта этой плоскости земли отходит от горизонтали?

В стороне, в UpdateView, D3DXQuaternionRotationYawPitchRoll кажется совершенно ненужным, так как вы сразу же поворачиваетесь и меняете его на матрицу. Просто используйте D3DXMatrixRotationYawPitchRoll, как это было в событии мыши. Кватернионы используются в камерах, потому что они являются удобным способом накопления поворотов, происходящих в координатах глаз. Поскольку вы используете только две оси вращения в строгом порядке, ваш способ накопления углов должен быть прекрасным. Векторное преобразование (0,0,1) также не является необходимым. oRotationMatrix должен иметь эти значения в записях (_31,_32,_33).


Обновление

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

Local rotation

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

Normal pitch around a point

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

Locked pitch around a point

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

Off axis pitch would cause roll

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

Camera orientation is handled separate from translation

Вместо этого он остается вертикально, но вы получаете странный перевод.

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


Обновление 2

Я думаю, что ответ Generic Human, вероятно, лучший. Остается вопрос, как много тона должно применяться, если вращение вне оси, но пока мы это проигнорируем. Возможно, это даст вам приемлемые результаты.

Суть ответа такова: перед движением мыши ваша камера находится на c 1= m_oEyePos и ориентирована на M 1= D3DXMatrixRotationYawPitchRoll(&M_1,m_dRotationX,m_dRotationY,0). Рассмотрим опорную точку a= m_oRotateAroundPos. С точки зрения камеры эта точка a '= M 1 (a-c 1).

Вы хотите изменить ориентацию камеры на M 2= D3DXMatrixRotationYawPitchRoll(&M_2,m_dRotationX+dX,m_dRotationY+dY,0). [Важно: поскольку вы не позволяете m_dRotationY выйти за пределы определенного диапазона, вы должны убедиться, что dY не нарушает это ограничение.] Когда камера меняет ориентацию, вы также хотите, чтобы ее положение вращалось вокруг a в новую точку c 2. Это означает, что a не изменится с точки зрения камеры. Т.е. M 1 (ac 1) == M 2 (ac 2).

Итак, мы решаем для c 2 (помните, что транспонирование матрицы вращения такая же, как и обратная):

M <суб > 2суб > T M <суб > 1суб > (ас <суб > 1суб > ) == (ас <суб > 2) = >

-M <суб > 2суб > TМ <суб > 1суб > (а-с <суб > 1суб > ) + а == с <суб > 2суб >

Теперь, если мы рассмотрим это как преобразование, применяемое к c 1, мы можем видеть, что он сначала отрицается, а затем переводится a, затем поверните на M 1, затем поверните на M 2 T, снова отменили, а затем снова перевели a. Это преобразования, которые хорошо подходят графическим библиотекам, и все они могут быть зажаты в одну матрицу преобразования.

@Generic Human заслуживает доверия за ответ, но вот код для него. Конечно, вам нужно реализовать функцию для проверки изменения высоты тона до ее применения, но это просто. Этот код, вероятно, имеет пару опечаток, поскольку я не пытался скомпилировать:

case ENavigation::eRotatePoint: {
    const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dRotatePixelScale * D3DX_PI;
    double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dRotatePixelScale * D3DX_PI;
    dY = validatePitch(dY); // dY needs to be kept within bounds so that m_dRotationY is within bounds

    D3DXMATRIX oRotationMatrix1; // The camera orientation before mouse-change
    D3DXMatrixRotationYawPitchRoll(&oRotationMatrix1, m_dRotationX, m_dRotationY, 0.0);

    D3DXMATRIX oRotationMatrix2; // The camera orientation after mouse-change
    D3DXMatrixRotationYawPitchRoll(&oRotationMatrix2, m_dRotationX + dX, m_dRotationY + dY, 0.0);

    D3DXMATRIX oRotationMatrix2Inv; // The inverse of the orientation
    D3DXMatrixTranspose(&oRotationMatrix2Inv,&oRotationMatrix2); // Transpose is the same in this case

    D3DXMATRIX oScaleMatrix; // Negative scaling matrix for negating the translation
    D3DXMatrixScaling(&oScaleMatrix,-1,-1,-1);

    D3DXMATRIX oTranslationMatrix; // Translation by the reference point
    D3DXMatrixTranslation(&oTranslationMatrix,
         m_oRotateAroundPos.x,m_oRotateAroundPos.y,m_oRotateAroundPos.z);

    D3DXMATRIX oTransformMatrix; // The full transform for the eyePos.
    // We assume the matrix multiply protects against variable aliasing
    D3DXMatrixMultiply(&oTransformMatrix,&oScaleMatrix,&oTranslationMatrix);
    D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oRotationMatrix1);
    D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oRotationMatrix2Inv);
    D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oScaleMatrix);
    D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oTranslationMatrix);

    D3DXVECTOR4 oEyeFinal;
    D3DXVec3Transform(&oEyeFinal, &m_oEyePos, &oTransformMatrix);

    m_oEyePos = D3DXVECTOR3(oEyeFinal.x, oEyeFinal.y, oEyeFinal.z) 

    // Increment rotation to keep the same relative look angles
    RotateXAxis(dX);
    RotateYAxis(dY);
    break;
}

Ответ 2

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

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

Ответ 3

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

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

  • Запишите локальные координаты A, которые равны A '= M × (A - C).
  • Поверните камеру, как в обычном режиме "поворота глаз". Обновите матрицу представлений M так, чтобы она была изменена на M 2, а C не изменился.
  • Теперь мы хотели бы найти C 2, что A '= M 2 × (A - C 2).
    Это легко сделать с помощью уравнения C 2= A - M 2 -1 × A '.
  • Voilà, камера была повернута и из-за того, что локальные координаты A не изменились, A остается в том же месте и в том же масштабе и расстоянии.

В качестве дополнительного бонуса поведение вращения теперь согласовано между режимом "поворот глаз" и "поворот точки".

Ответ 4

Если я правильно понимаю, вы удовлетворены компонентом вращения в финальной матрице (за исключением элементов управления с инвертированным вращением в задаче № 3), но не с частью перевода, так это?

Проблема, похоже, связана с тем, что вы относитесь к ним по-другому: вы каждый раз пересчитываете часть вращения с нуля, но накапливаете часть перевода (m_oEyePos). В других комментариях упоминаются проблемы с точностью, но на самом деле они более значительны, чем просто точность FP: накопление вращений с небольшими значениями рыскания/шага просто не совпадает - математически --- как одно большое вращение от накопленного рыскания/высоты. Следовательно, расхождение поворота/сдвига. Чтобы исправить это, попробуйте пересчитать положение глаз с нуля одновременно с частью вращения, аналогично тому, как вы находите "oTarget = oEyePos +...":

oEyePos = m_oRotateAroundPos - dist * D3DXVECTOR3(oForward4.x, oForward4.y, oForward4.z)

dist можно фиксировать или вычислять из положения старого глаза. Это будет поддерживать точку поворота в центре экрана; в более общем случае (который вас интересует), -dist * oForward здесь следует заменить старым/начальным m_oEyePos - m_oRotateAroundPos, умноженным на старое/начальное вращение камеры, чтобы привести его в пространство камеры (поиск постоянного вектора смещения в система координат камеры), затем умножается на инвертированное вращение новой камеры, чтобы получить новое направление в мире.

Это, конечно же, будет зависеть от карданного замка, когда шаг будет прямо вверх или вниз. Вам нужно будет точно определить, какое поведение вы ожидаете в этих случаях, чтобы решить эту часть. С другой стороны, блокировка в m_dRotationX = 0 или = pi довольно странная (это рыскание, а не шаг, справа?) И может быть связано с выше.