python - 如何像在 3ds max 中一样实现 alt+MMB 相机旋转?
问题描述
先决条件
让我通过提供一些我们将使用的样板代码来开始这个问题:
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
amount = self.velocity * dt
if keys['w']:
self.camera.move(0, 0, 1, amount)
if keys['s']:
self.camera.move(0, 0, -1, amount)
if keys['d']:
self.camera.move(1, 0, 0, amount)
if keys['a']:
self.camera.move(-1, 0, 0, amount)
if keys['q']:
self.camera.move(0, -1, 0, amount)
if keys['e']:
self.camera.move(0, 1, 0, amount)
if keys['+']:
self.camera.fov += radians(amount)
if keys['-']:
self.camera.fov -= radians(amount)
# -------- 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
您应该有一个非常基本的 glut 窗口,具有简单的功能,例如相机旋转/缩放以及一些渲染点/三角形/四边形和 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))
amount = (right * delta.y + self.up * delta.x)
M = glm.mat4(1)
M = glm.rotate(M, amount.z, glm.vec3(0, 0, 1))
M = glm.rotate(M, amount.y, glm.vec3(0, 1, 0))
M = glm.rotate(M, amount.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 使用的旋转。
现在使用当前代码,您可以使用 WASDQE 键四处移动(shift 加速),左/右键围绕它/场景的中心旋转相机或使用鼠标滚轮进行缩放。如您所见,偏移值是硬编码的,只需将它们调整为在您的盒子中平稳运行(我知道有适当的方法可以使相机动力学矢量独立于 cpu,这不是我的问题的重点)
参考
让我们尝试更多地剖析在 3dsmax2018 上按 alt+MMB 时相机的行为。
- 在“家”旋转(当您按下右上角 Gizmo 上的主页按钮时会发生家中的相机,这会将相机位置放置在固定位置并瞄准 (0,0,0)):
- 平移和旋转:
- 缩放/平移和旋转:
- 用户界面
问题:所以接下来将添加必要的位以在按下 alt+MMB 时实现轨迹球旋转...我说轨迹球旋转因为我假设 3ds max 在幕后使用该方法,但我不确定这是 max 使用的方法所以首先我想知道按 alt+MMB 时 3ds max 使用的确切数学是什么,然后只需将必要的代码添加到Camera
类中即可完成该任务
解决方案
您必须将围绕 x 和 y 轴的旋转矩阵应用于视图矩阵。首先应用绕 y 轴(向上向量)的旋转矩阵,然后应用当前视图矩阵,最后应用 x 轴上的旋转:
view-matrix = rotate-X * view-matrix * rotate-Y
pivotWorld
旋转的工作方式与“3ds max”中的完全一样,除了必须定义 旋转原点(枢轴 - )的正确位置。
一个合理的解决方案是,枢轴是相机目标 ( 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
推荐阅读
- php - 使用 PHP 中的键将数组转换为对象
- c++ - 当我无法在其中包含代码所需的头文件时,如何链接共享库?
- php - phpword中段落后的单行分隔符?
- sql - 在 INT 中转换 STR 时如何增加值?
- javascript - 反应将数组转换为对象的对象
- ajax - 行选择事件 ajax 不起作用(它没有被触发)
- dialogflow-es - 为什么每当我想查看我的代理在 Google 助理上的工作情况时,我总是收到错误 500?
- reactjs - 在 react-redux 应用程序中更新部分状态时,整个页面闪烁并跳转到顶部
- apache-flink - Apache Flink - 窗口化 - 用于计数的 AggregateFunction
- azure-devops - 看板:根据个人用户偏好自定义板项目上显示的字段数量