Как реализовать вращение камеры alt + MMB как в 3ds max? - PullRequest
0 голосов
/ 28 января 2019

ПРЕДПОСЫЛКИ


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

mcve_framework.py:

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

import glm
from glm import cross, normalize, unProject, vec2, vec3, vec4


# -------- Camera --------
class BaseCamera():

    def __init__(
        self,
        eye=None, target=None, up=None,
        fov=None, near=0.1, far=100000,
        delta_zoom=10
    ):
        self.eye = eye or glm.vec3(0, 0, 1)
        self.target = target or glm.vec3(0, 0, 0)
        self.up = up or glm.vec3(0, 1, 0)
        self.original_up = glm.vec3(self.up)
        self.fov = fov or glm.radians(45)
        self.near = near
        self.far = far
        self.delta_zoom = delta_zoom

    def update(self, aspect):
        self.view = glm.lookAt(
            self.eye, self.target, self.up
        )
        self.projection = glm.perspective(
            self.fov, aspect, self.near, self.far
        )

    def move(self, dx, dy, dz, dt):
        if dt == 0:
            return

        forward = normalize(self.target - self.eye) * dt
        right = normalize(cross(forward, self.up)) * dt
        up = self.up * dt

        offset = right * dx
        self.eye += offset
        self.target += offset

        offset = up * dy
        self.eye += offset
        self.target += offset

        offset = forward * dz
        self.eye += offset
        self.target += offset

    def zoom(self, *args):
        x = args[2]
        y = args[3]
        v = glGetIntegerv(GL_VIEWPORT)
        viewport = vec4(float(v[0]), float(v[1]), float(v[2]), float(v[3]))
        height = viewport.w

        pt_wnd = vec3(x, height - y, 1.0)
        pt_world = unProject(pt_wnd, self.view, self.projection, viewport)
        ray_cursor = glm.normalize(pt_world - self.eye)

        delta = args[1] * self.delta_zoom
        self.eye = self.eye + ray_cursor * delta
        self.target = self.target + ray_cursor * delta

    def load_projection(self):
        width = glutGet(GLUT_WINDOW_WIDTH)
        height = glutGet(GLUT_WINDOW_HEIGHT)

        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(glm.degrees(self.fov), width / height, self.near, self.far)

    def load_modelview(self):
        e = self.eye
        t = self.target
        u = self.up

        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        gluLookAt(e.x, e.y, e.z, t.x, t.y, t.z, u.x, u.y, u.z)


class GlutController():

    FPS = 0
    ORBIT = 1
    PAN = 2

    def __init__(self, camera, velocity=100, velocity_wheel=100):
        self.velocity = velocity
        self.velocity_wheel = velocity_wheel
        self.camera = camera

    def glut_mouse(self, button, state, x, y):
        self.mouse_last_pos = vec2(x, y)
        self.mouse_down_pos = vec2(x, y)

        if button == GLUT_LEFT_BUTTON:
            self.mode = self.FPS
        elif button == GLUT_RIGHT_BUTTON:
            self.mode = self.ORBIT
        else:
            self.mode = self.PAN

    def glut_motion(self, x, y):
        pos = vec2(x, y)
        move = self.mouse_last_pos - pos
        self.mouse_last_pos = pos

        if self.mode == self.FPS:
            self.camera.rotate_target(move * 0.005)
        elif self.mode == self.ORBIT:
            self.camera.rotate_around_origin(move * 0.005)

    def glut_mouse_wheel(self, *args):
        self.camera.zoom(*args)

    def process_inputs(self, keys, dt):
        dt *= 10 if keys[' '] else 1
        ammount = self.velocity * dt

        if keys['w']:
            self.camera.move(0, 0, 1, ammount)
        if keys['s']:
            self.camera.move(0, 0, -1, ammount)
        if keys['d']:
            self.camera.move(1, 0, 0, ammount)
        if keys['a']:
            self.camera.move(-1, 0, 0, ammount)
        if keys['q']:
            self.camera.move(0, -1, 0, ammount)
        if keys['e']:
            self.camera.move(0, 1, 0, ammount)
        if keys['+']:
            self.camera.fov += radians(ammount)
        if keys['-']:
            self.camera.fov -= radians(ammount)


# -------- Mcve --------
class BaseWindow:

    def __init__(self, w, h, camera):
        self.width = w
        self.height = h

        glutInit()
        glutSetOption(GLUT_MULTISAMPLE, 16)
        glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH | GLUT_MULTISAMPLE)
        glutInitWindowSize(w, h)
        glutCreateWindow('OpenGL Window')

        self.keys = {chr(i): False for i in range(256)}

        self.startup()

        glutReshapeFunc(self.reshape)
        glutDisplayFunc(self.display)
        glutMouseFunc(self.controller.glut_mouse)
        glutMotionFunc(self.controller.glut_motion)
        glutMouseWheelFunc(self.controller.glut_mouse_wheel)
        glutKeyboardFunc(self.keyboard_func)
        glutKeyboardUpFunc(self.keyboard_up_func)
        glutIdleFunc(self.idle_func)

    def keyboard_func(self, *args):
        try:
            key = args[0].decode("utf8")

            if key == "\x1b":
                glutLeaveMainLoop()

            self.keys[key] = True
        except Exception as e:
            import traceback
            traceback.print_exc()

    def keyboard_up_func(self, *args):
        try:
            key = args[0].decode("utf8")
            self.keys[key] = False
        except Exception as e:
            pass

    def startup(self):
        raise NotImplementedError

    def display(self):
        raise NotImplementedError

    def run(self):
        glutMainLoop()

    def idle_func(self):
        glutPostRedisplay()

    def reshape(self, w, h):
        glViewport(0, 0, w, h)
        self.width = w
        self.height = h

Если вы хотите использовать приведенный выше код, вам просто нужно установить pyopengl и pygml.После этого вы можете просто создать свой собственный подкласс BaseWindow, переопределить startup и render, и у вас должно появиться очень простое окно переналадки с простыми функциями, такими как поворот / масштабирование камеры, а также некоторые методы для визуализации точек / треугольников./ quads and indexed_triangles / indexed_quads.

ЧТО СДЕЛАНО


mcve_camera_arcball.py

import time

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

import glm
from mcve_framework import BaseCamera, BaseWindow, GlutController


def line(p0, p1, color=None):
    c = color or glm.vec3(1, 1, 1)
    glColor3f(c.x, c.y, c.z)
    glVertex3f(p0.x, p0.y, p0.z)
    glVertex3f(p1.x, p1.y, p1.z)


def grid(segment_count=10, spacing=1, yup=True):
    size = segment_count * spacing
    right = glm.vec3(1, 0, 0)
    forward = glm.vec3(0, 0, 1) if yup else glm.vec3(0, 1, 0)
    x_axis = right * size
    z_axis = forward * size

    i = -segment_count

    glBegin(GL_LINES)
    while i <= segment_count:
        p0 = -x_axis + forward * i * spacing
        p1 = x_axis + forward * i * spacing
        line(p0, p1)
        p0 = -z_axis + right * i * spacing
        p1 = z_axis + right * i * spacing
        line(p0, p1)
        i += 1
    glEnd()


def axis(size=1.0, yup=True):
    right = glm.vec3(1, 0, 0)
    forward = glm.vec3(0, 0, 1) if yup else glm.vec3(0, 1, 0)
    x_axis = right * size
    z_axis = forward * size
    y_axis = glm.cross(forward, right) * size
    glBegin(GL_LINES)
    line(x_axis, glm.vec3(0, 0, 0), glm.vec3(1, 0, 0))
    line(y_axis, glm.vec3(0, 0, 0), glm.vec3(0, 1, 0))
    line(z_axis, glm.vec3(0, 0, 0), glm.vec3(0, 0, 1))
    glEnd()


class Camera(BaseCamera):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def rotate_target(self, delta):
        right = glm.normalize(glm.cross(self.target - self.eye, self.up))
        M = glm.mat4(1)
        M = glm.translate(M, self.eye)
        M = glm.rotate(M, delta.y, right)
        M = glm.rotate(M, delta.x, self.up)
        M = glm.translate(M, -self.eye)
        self.target = glm.vec3(M * glm.vec4(self.target, 1.0))

    def rotate_around_target(self, target, delta):
        right = glm.normalize(glm.cross(self.target - self.eye, self.up))
        ammount = (right * delta.y + self.up * delta.x)
        M = glm.mat4(1)
        M = glm.rotate(M, ammount.z, glm.vec3(0, 0, 1))
        M = glm.rotate(M, ammount.y, glm.vec3(0, 1, 0))
        M = glm.rotate(M, ammount.x, glm.vec3(1, 0, 0))
        self.eye = glm.vec3(M * glm.vec4(self.eye, 1.0))
        self.target = target
        self.up = self.original_up

    def rotate_around_origin(self, delta):
        return self.rotate_around_target(glm.vec3(0), delta)


class McveCamera(BaseWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def startup(self):
        glEnable(GL_DEPTH_TEST)

        self.start_time = time.time()
        self.camera = Camera(
            eye=glm.vec3(200, 200, 200),
            target=glm.vec3(0, 0, 0),
            up=glm.vec3(0, 1, 0),
            delta_zoom=30
        )
        self.model = glm.mat4(1)
        self.controller = GlutController(self.camera)
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)

    def display(self):
        self.controller.process_inputs(self.keys, 0.005)
        self.camera.update(self.width / self.height)

        glClearColor(0.2, 0.3, 0.3, 1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        self.camera.load_projection()
        self.camera.load_modelview()

        glLineWidth(5)
        axis(size=70, yup=True)
        glLineWidth(1)
        grid(segment_count=7, spacing=10, yup=True)

        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(-1, 1, -1, 1, -1, 1)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()

        glutSwapBuffers()


if __name__ == '__main__':
    window = McveCamera(800, 600, Camera())
    window.run()

TODO


Конечная цель здесь - выяснить, как имитировать вращение, используемое 3dsmax при нажатии Alt + MMB.

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

ССЫЛКИ

Давайте попробуем немного разобрать, как ведет себя камера при нажатии alt + MMB на 3dsmax2018.

1) Поворот в «доме» (камера вДомой происходит, когда вы нажимаете кнопку «Домой» в правом верхнем углу гизмо, которая установит положение камеры в фиксированном месте и цель (0,0,0)):

showcase

2) Панорамирование и вращение:

showcase

3) Масштабирование / панорамирование и вращение:

showcase

4) Интерфейс пользователя

showcase

ВОПРОС: Итак, следующим будет добавление необходимых битов для реализации вращения арбалета при нажатии alt + MMB ... Я говорю вращение арбола, потому что я предполагаю 3dsМакс использует этот метод за кулисами, но я не совсем уверен, что это метод уsed по max, так что сначала я бы хотел, чтобы знал, какие именно математические выражения используются 3ds max при нажатии alt + MMB, а затем просто добавляют необходимый код в класс Camera .для достижения этой задачи

1 Ответ

0 голосов
/ 29 января 2019

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

view-matrix = rotate-X * view-matrix * rotate-Y

Вращение работает точно так же, как в «3ds max», за исключением правильногоместоположение начала вращения (шарнир - pivotWorld) должно быть определено.

Правдоподобным решением является то, что ось является целью камеры (self.target).
В начале цель - (0, 0, 0), которая является источником мира.Поворот вокруг источника мира - это ожидаемое поведение, пока цель представления является центром мира.Если представление является панорамированием, то цель все еще находится в центре области просмотра, поскольку она перемещается так же, как и точка обзора - self.eye и self.target перемещаются «параллельно».Это приводит к тому, что сцена все еще вращается вокруг точки в центре вида (новая цель) и выглядит точно так же, как в «3ds max».

def rotate_around_target(self, target, delta):

    # get the view matrix
    view = glm.lookAt(self.eye, self.target, self.up)

    # pivot in world sapace and view space
    #pivotWorld = glm.vec3(0, 0, 0)
    pivotWorld = self.target

    pivotView = glm.vec3(view * glm.vec4(*pivotWorld, 1))  

    # rotation around the vies pace x axis
    rotViewX    = glm.rotate( glm.mat4(1), -delta.y, glm.vec3(1, 0, 0) )
    rotPivotViewX   = glm.translate(glm.mat4(1), pivotView) * rotViewX * glm.translate(glm.mat4(1), -pivotView)  

    # rotation around the world space up vector
    rotWorldUp  = glm.rotate( glm.mat4(1), -delta.x, glm.vec3(0, 1, 0) )
    rotPivotWorldUp = glm.translate(glm.mat4(1), pivotWorld) * rotWorldUp * glm.translate(glm.mat4(1), -pivotWorld)

    # update view matrix
    view = rotPivotViewX * view * rotPivotWorldUp

    # decode eye, target and up from view matrix
    C = glm.inverse(view)
    targetDist  = glm.length(self.target - self.eye)
    self.eye    = glm.vec3(C[3])
    self.target = self.eye - glm.vec3(C[2]) * targetDist 
    self.up     = glm.vec3(C[1])

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

class BaseCamera():

    def zoom(self, *args):
        x = args[2]
        y = args[3]
        v = glGetIntegerv(GL_VIEWPORT)
        viewport = vec4(float(v[0]), float(v[1]), float(v[2]), float(v[3]))
        height = viewport.w

        pt_wnd = vec3(x, height - y, 1.0)
        pt_world = unProject(pt_wnd, self.view, self.projection, viewport)
        ray_cursor = glm.normalize(pt_world - self.eye)

        # calculate the "zoom" vector
        delta       = args[1] * self.delta_zoom
        zoom_vec    = ray_cursor * delta

        # get the direction of sight and the distance to the target 
        sight_vec   = self.target - self.eye
        target_dist = glm.length(sight_vec)
        sight_vec   = sight_vec / target_dist

        # modify the distance to the target
        delta_dist = glm.dot(sight_vec, zoom_vec)
        if (target_dist - delta_dist) > 0.01: # the direction has to kept in any case
            target_dist -= delta_dist

        # update the eye postion and the target
        self.eye    = self.eye + zoom_vec
        self.target = self.eye + sight_vec * target_dist

...