Как реализовать трекбол в OpenGL?

15

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

Мой вопрос заключается в том, нужно ли преобразовывать (x, y) пиксельные координаты в мировые координаты или я должен просто делать все в пространстве изображения (учитывая, что пространство изображения - это 2D-проекция сцены, измеренная в пикселях)?

РЕДАКТИРОВАТЬ

Ответ Ричи Сэмса очень хороший. Тем не менее, я думаю, что я придерживаюсь немного другого подхода, пожалуйста, поправьте меня, если я ошибаюсь или неправильно что-то понимаю.

В моем приложении у меня есть SimplePerspectiveCameraкласс , который получает positionот камеры, position of the targetмы смотрим, в upвекторе fovy, aspectRatio, nearи farрасстояние.

Из них я строю свои матрицы просмотра и проекции. Теперь, если я хочу увеличить / уменьшить масштаб, я обновляю поле зрения и обновляю матрицу проекции. Если я хочу панорамировать, я перемещаю положение камеры и наблюдаю за дельтой, которую производит мышь.

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

Для каждой пары координат можно вычислить Z-значение дается формулой для шара, т.е. SQRT (1-х ^ 2-у ^ 2), а затем вычислить для векторов , которые идут от targetдо PointMousePressedи от targetдо PointMouseMoved, делает поперечный продукт чтобы получить ось вращения и использовать любой метод для расчета новой позиции камеры.

Однако мое самое большое сомнение заключается в том, что значения (x, y, z) приведены в пиксельных координатах, а при вычислении используемых мной векторов targetэто точка в мировых координатах. Разве это смешивание системы координат не влияет на результат вращения, который я пытаюсь сделать?

BRabbit27
источник
1
Означает ли «трекбол» камеру, которая вращается вокруг объекта, как в приложениях для 3D-моделирования? Если это так, я думаю, что обычно это делается путем отслеживания координат 2D-мыши и отображения x = yaw, y = pitch для поворота камеры.
Натан Рид
1
@NathanReed другой параметр основан на осевом угле. Вы проецируете 2 точки мыши на (виртуальную) сферу, а затем находите вращение от одной к другой.
храповик урод
@NathanReed Да, это то, что я имел в виду под трекболом, я думал, что это было распространенное имя в сообществе CG.
BRabbit27
@ratchetfreak да, мой подход учитывает вращение по оси. Я сомневаюсь, что если необходимо отобразить 2D-координаты мыши на world-координировать или нет. Я знаю, что могу использовать (x, y), чтобы вычислить zзначение сферы радиуса r, однако я не уверен, живет ли эта сфера в мировом пространстве или пространстве изображений и каковы значения. Возможно, я обдумываю проблему.
BRabbit27
2
На вашем редактировании: Да. Вам необходимо преобразовать ваши значения (x, y, z) в мировое пространство, используя матрицу просмотра.
RichieSams

Ответы:

16

Предполагая, что вы имеете в виду камеру, которая вращается в зависимости от движения мыши:

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

Сферические координаты изображения

float m_theta;
float m_phi;
float m_radius;

float3 m_target;

Камера расположена в P, который определяется m_theta, m_phi и m_radius. Мы можем свободно вращаться и двигаться в любом месте, изменяя эти три значения. Однако мы всегда смотрим и вращаем m_target. m_target является локальным источником сферы. Однако мы можем свободно перемещать это происхождение в любое место в мировом пространстве.

Есть три основные функции камеры:

void Rotate(float dTheta, float dPhi);
void Zoom(float distance);
void Pan(float dx, float dy);

В простейших формах Rotate () и Zoom () тривиальны. Просто измените m_theta, m_phi и m_radius соответственно:

void Camera::Rotate(float dTheta, float dPhi) {
    m_theta += dTheta;
    m_phi += dPhi;
}

void Camera::Zoom(float distance) {
    m_radius -= distance;
}

Панорамирование немного сложнее. Панорамирование камеры определяется как перемещение камеры влево / вправо и / или вверх / вниз относительно текущего вида камеры. Самый простой способ сделать это - преобразовать текущий вид камеры из сферических координат в декартовы. Это даст нам вверх и правильные векторы.

void Camera::Pan(float dx, float dy) {
    float3 look = normalize(ToCartesian());
    float3 worldUp = float3(0.0f, 1.0f, 0.0f, 0.0f);

    float3 right = cross(look, worldUp);
    float3 up = cross(look, right);

    m_target = m_target + (right * dx) + (up * dy);
}

inline float3 ToCartesian() {
    float x = m_radius * sinf(m_phi) * sinf(m_theta);
    float y = m_radius * cosf(m_phi);
    float z = m_radius * sinf(m_phi) * cosf(m_theta);
    float w = 1.0f;

    return float3(x, y, z, w);
}

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

Чтобы закончить кастрюлю, мы перемещаем m_target вдоль вверх и правых векторов.

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

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

введите описание изображения здесь

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

Последний шаг - использовать этот класс Camera. Просто вызовите соответствующую функцию-член внутри функций MouseDown / Up / Scroll вашего приложения:

void MouseDown(WPARAM buttonState, int x, int y) {
    m_mouseLastPos.x = x;
    m_mouseLastPos.y = y;

    SetCapture(m_hwnd);
}

void MouseUp(WPARAM buttonState, int x, int y) {
    ReleaseCapture();
}

void MouseMove(WPARAM buttonState, int x, int y) {
    if ((buttonState & MK_LBUTTON) != 0) {
        if (GetKeyState(VK_MENU) & 0x8000) {
            // Calculate the new phi and theta based on mouse position relative to where the user clicked
            float dPhi = ((float)(m_mouseLastPos.y - y) / 300);
            float dTheta = ((float)(m_mouseLastPos.x - x) / 300);

            m_camera.Rotate(-dTheta, dPhi);
        }
    } else if ((buttonState & MK_MBUTTON) != 0) {
        if (GetKeyState(VK_MENU) & 0x8000) {
            float dx = ((float)(m_mouseLastPos.x - x));
            float dy = ((float)(m_mouseLastPos.y - y));

            m_camera.Pan(-dx * m_cameraPanFactor, dy * m_cameraPanFactor);
        }
    }

    m_mouseLastPos.x = x;
    m_mouseLastPos.y = y;
}

void MouseWheel(int zDelta) {
    // Make each wheel dedent correspond to a size based on the scene
    m_camera.Zoom((float)zDelta * m_cameraScrollFactor);
}

Переменные m_camera * Factor - это просто масштабные коэффициенты, которые изменяют скорость вращения / панорамирования / прокрутки камеры.

Приведенный выше код представляет собой упрощенную версию системы камер с псевдокодом, которую я сделал для стороннего проекта: camera.h и camera.cpp . Камера пытается имитировать систему камер Майя. Код бесплатный и с открытым исходным кодом, поэтому не стесняйтесь использовать его в своем собственном проекте.

RichieSams
источник
1
Полагаю, деление на 300 - это всего лишь параметр чувствительности вращения с учетом перемещения мыши?
BRabbit27
Верный. Это то, что хорошо сработало с моим решением в то время.
RichieSams
-2

В случае, если вы хотите взглянуть на готовое решение. У меня есть порт THREE.JS контроллеров TrackBall в C ++ и C #

Михаил IV
источник
Я склонен думать, что это так. Он может узнать из кода внутри, как работает трекбол.
Майкл IV
1
@MichaelIV Тем не менее, Алан Вульф имеет смысл. Вы можете значительно улучшить свой ответ, включив соответствующий код в сам ответ, чтобы сделать его автономным и ориентированным на будущее, если ссылка однажды не будет работать.
Мартин Эндер