如何像3ds max一样实现alt + MMB摄像机旋转? [英] How to implement alt+MMB camera rotation like in 3ds max?

查看:124
本文介绍了如何像3ds max一样实现alt + MMB摄像机旋转?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

必备条件

让我通过提供一些我们将在其中使用的样板代码来开始这个问题:

Let me start the question by providing some boilerplate code we'll use to play around:

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子类,覆盖startuprender,您将拥有一个非常简单的glut窗口,该窗口具有简单的功能,例如相机旋转/缩放以及一些渲染点/三角形的方法/quads和indexed_triangles/indexed_quads.

In case you want to use the above code you'll just need to install pyopengl and pygml. After that, you can just create your own BaseWindow subclass, override startup and render and you should have a very basic glut window with simple functionality such as camera rotation/zooming as well as some methods to render points/triangles/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()

待办事项

这里的最终目标是弄清楚如何在按Alt + MMB时模拟3dsmax所使用的旋转.

The end goal here is to figure out how to emulate the rotation used by 3dsmax when pressing Alt+MMB.

现在,使用当前代码,您可以使用WASDQE键(移动为加速键),向左/向右键以围绕其/场景中心旋转相机或使用鼠标滚轮进行缩放来移动.如您所见,偏移值是硬编码的,只需将其调整为在框内平稳运行即可(我知道有适当的方法可以使相机动力学矢量独立于cpu,这不是我要问的重点)

Right now with the current code you can move around using WASDQE keys (shift to accelerate), left/right button to rotate camera around it's/scene's center or zooming by using mouse wheel. As you can see, offset values are hardcoded, just adjust them to run smoothly in your box (I know there are proper methods to make the camera kinetics vectors to be cpu independent, that's not the point of my question)

参考

让我们尝试进一步剖析3dsmax2018上按alt + MMB时相机的行为.

Let's try to disect a little bit more how the camera behaves when pressing alt+MMB on 3dsmax2018.

1)在主屏幕"上旋转(当您按下右上方的Gizmo上的主屏幕按钮时,将发生摄像头位置在固定位置,并将目标定位在(0,0,0)):

1) Rottion at "home" (camera at home happens when you press the home button on the top right gizmo, that will place the camera position at a fixed location and target at (0,0,0)):

2)平移和旋转:

3)缩放/平移和旋转:

3) Zooming/Panning and rotation:

4)用户界面

问题:因此,接下来将在按下alt + MMB时添加必要的位来实现Arcball旋转...我说是arcball旋转cos,我假设3ds max在幕后使用了该方法,但是我我不太确定这是max使用的方法,因此我首先想知道3ds max在按下alt + MMB时使用的确切数学,然后将必要的代码添加到Camera完成任务

QUESTION: So next would be adding the necessary bits to implement arcball rotation when pressing alt+MMB... I say arcball rotation cos I assume 3ds max uses that method behind the curtains but I'm not really sure that's the method used by max so first I'd like to know what are the exact maths used by 3ds max when pressing alt+MMB and then just add the necessary code to the Camera class to achieve that task

推荐答案

您必须将围绕x和y轴的旋转矩阵应用于视图矩阵.首先应用绕y轴(向上矢量)的旋转矩阵,然后应用当前视图矩阵,最后应用沿x轴的旋转:

You have to apply a rotation matrix around the x and y axis to the view matrix. First apply the rotation matrix around the y axis (up vector) then the current view matrix and finally the rotation on the x axis:

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

旋转必须与"3ds max"中的完全一样,除非必须定义旋转原点的正确位置(枢轴-pivotWorld).

The rotation works exactly as in "3ds max", ecept the proper location of the origin of the rotation (the pivot - pivotWorld) has to be defined.

一个可行的解决方案是,枢轴是摄影机目标(self.target).
首先,目标是(0,0,0),这是世界的起源.只要视图的目标是世界的中心,就可以绕着世界的起源旋转是预期的行为. 如果视图是平移的,则目标仍位于视口的中心,因为它的移动方式与视点相同-self.eyeself.target被平行"移动. 这会导致场景仍然看起来围绕视图中心(新目标)的一个点旋转,并且似乎与"3ds max"中的行为完全相同.

A plausible solution is, that the pivot is the camera target (self.target).
At the begin the target is (0, 0, 0) which is the origin of the world. To rotate around the origin of the world, is the expected behaviour as long the target of the view is the center of the world. If the view is pan, then the target is still in the center of the viewport, because it is moved in the same way as the point of view - self.eye ans self.target are moved "parallel". This causes the the scene, still appears to rotate around a point in the center of the view (the new target) and seems to be the exact same behaviour, as in "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)的方向必须保持不变,但可能必须根据缩放来更改到目标的距离.
我建议对类BaseCamera的方法zoom进行以下更改:

But still there is an issue left. What is if the scene is zoomed?
In the current implementation the distance from the camera to the target point is kept constant. In case of zoom this may be incorrect, the direction from the point of view (self.eye) to the target (self.target) has to stay the same, but possibly the distance to the target has to be changed according to the zoom.
I suggest to do the following changes to the method zoom of the class 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

这篇关于如何像3ds max一样实现alt + MMB摄像机旋转?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆