如何像在 3dsMax 中一样实现鼠标缩放?

Posted

技术标签:

【中文标题】如何像在 3dsMax 中一样实现鼠标缩放?【英文标题】:How to implement zoom towards mouse like in 3dsMax? 【发布时间】:2019-06-01 02:58:28 【问题描述】:

当您通过移动鼠标滚轮放大/缩小时,我试图模仿 3dsmax 的行为。在 3ds max 中,此缩放将朝向鼠标位置。到目前为止,我已经想出了这个小 mcve:

import math
from ctypes import c_void_p

import numpy as np
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from glm import *


class Camera():

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

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

    def zoom(self, *args):
        delta = -args[1] * 0.1
        distance = length(self.target - self.eye)
        self.eye = self.target + (self.eye - self.target) * (delta + 1)

    def zoom_towards_cursor(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.z

        p0 = vec3(x, height - y, 0.0)
        p1 = vec3(x, height - y, 1.0)
        v1 = unProject(p0, self.view, self.projection, viewport)
        v2 = unProject(p1, self.view, self.projection, viewport)

        world_from = vec3(
            (-v1.z * (v2.x - v1.x)) / (v2.z - v1.z) + v1.x,
            (-v1.z * (v2.y - v1.y)) / (v2.z - v1.z) + v1.y,
            0.0
        )

        self.eye.z = self.eye.z * (1.0 + 0.1 * args[1])

        view = lookAt(self.eye, self.target, self.up)
        v1 = unProject(p0, view, self.projection, viewport)
        v2 = unProject(p1, view, self.projection, viewport)

        world_to = vec3(
            (v1.z * (v2.x - v1.x)) / (v2.z - v1.z) + v1.x,
            (-v1.z * (v2.y - v1.y)) / (v2.z - v1.z) + v1.y,
            0.0
        )

        offset = world_to - world_from
        print(self.eye.z, world_from, world_to, offset)

        self.eye += offset
        self.target += offset


class GlutController():

    def __init__(self, camera):
        self.camera = camera
        self.zoom = self.camera.zoom

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


class MyWindow:

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

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

        self.startup()

        glutReshapeFunc(self.reshape)
        glutDisplayFunc(self.display)
        glutMouseWheelFunc(self.controller.glut_mouse_wheel)
        glutKeyboardFunc(self.keyboard_func)
        glutIdleFunc(self.idle_func)

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

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

            if key in ['1']:
                self.controller.zoom = self.camera.zoom
                print("Using normal zoom")
            elif key in ['2']:
                self.controller.zoom = self.camera.zoom_towards_cursor
                print("Using zoom towards mouse")

        except Exception as e:
            import traceback
            traceback.print_exc()

    def startup(self):
        glEnable(GL_DEPTH_TEST)

        aspect = self.width / self.height
        params = 
            "eye": vec3(10, 10, 10),
            "target": vec3(0, 0, 0),
            "up": vec3(0, 1, 0)
        
        self.cameras = [
            Camera(**params)
        ]
        self.camera = self.cameras[0]
        self.model = mat4(1)
        self.controller = GlutController(self.camera)

    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

    def display(self):
        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)

        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(degrees(self.camera.fov), self.width / self.height, self.camera.near, self.camera.far)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        e = self.camera.eye
        t = self.camera.target
        u = self.camera.up
        gluLookAt(e.x, e.y, e.z, t.x, t.y, t.z, u.x, u.y, u.z)
        glColor3f(1, 1, 1)
        glBegin(GL_LINES)
        for i in range(-5, 6):
            if i == 0:
                continue
            glVertex3f(-5, 0, i)
            glVertex3f(5, 0, i)
            glVertex3f(i, 0, -5)
            glVertex3f(i, 0, 5)
        glEnd()

        glBegin(GL_LINES)
        glColor3f(1, 1, 1)
        glVertex3f(-5, 0, 0)
        glVertex3f(0, 0, 0)
        glVertex3f(0, 0, -5)
        glVertex3f(0, 0, 0)

        glColor3f(1, 0, 0)
        glVertex3f(0, 0, 0)
        glVertex3f(5, 0, 0)
        glColor3f(0, 1, 0)
        glVertex3f(0, 0, 0)
        glVertex3f(0, 5, 0)
        glColor3f(0, 0, 1)
        glVertex3f(0, 0, 0)
        glVertex3f(0, 0, 5)
        glEnd()

        glutSwapBuffers()


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

在这个 sn-p 中,您可以通过按“1”或“2”键在 2 种缩放模式之间切换。

当按“1”时,我正在进行标准缩放,到目前为止一切顺利。

问题是在按下“2”时,在这种情况下,我尝试将代码从 thread 调整为 python/pyopengl/pygml,但因为我不太了解该答案的基础数学,所以我不知道不太清楚如何纠正不良行为。

您将如何修复发布的代码,使其能够像 3dsmax 一样正确放大/缩小鼠标?

【问题讨论】:

【参考方案1】:

一种可能的解决方案是沿射线移动相机,从相机位置到光标(鼠标)位置,然后平行移动目标位置。

self.eye    = self.eye    + ray_cursor * delta
self.target = self.target + ray_cursor * delta

为此,光标的窗口位置必须是“未投影”(unProject)。

计算光标在世界空间中的位置(例如在远平面上):

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

从眼睛位置穿过光标的光线由从眼睛位置到世界空间光标位置的归一化向量给出:

ray_cursor = normalize(pt_world - self.eye)

当您从视口矩形获取窗口高度时,您的代码中存在问题,因为高度是.w 组件而不是.z 组件:

v = glGetIntegerv(GL_VIEWPORT)
viewport = vec4(float(v[0]), float(v[1]), float(v[2]), float(v[3]))
width  = viewport.z
height = viewport.w

函数zoom_towards_cursor的完整代码清单:

def zoom_towards_cursor(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]))
    width  = viewport.z
    height = viewport.w

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

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

另见Python OpenGL 4.6, GLM navigation

预览:

【讨论】:

太棒了,当我回到家时,我会试一试以检查与 max 的可能差异......但到目前为止,您的方法看起来既简单又正确。顺便说一句,我不会立即验证您的答案,因此我们将让读者有机会正确地支持您的答案,因为这是应得的。再次,tyvm ;) 快速提问,您知道如何转换此代码以使用 Ortho 相机吗?在这种情况下询问cos,没有eyetarget的概念,而您只有leftrightbottomupnearfar参数跨度> @BPL Zoom 与正交投影的工作方式完全不同。由于没有透视,当物体远离相机时,它们并没有“变小”。你必须改变视野。这意味着您必须根据缩放系数线性缩放 leftrightbottomup 没错.. 我正在将透视相机重构为 Camera 和 OrthoCamera, PerspectiveCamera,这是我发现的第一个挑战之一。调整平移很容易,但这个比较棘手......顺便说一句,你可以从正交切换到透视,因此正交摄像机也必须以某种方式保持眼睛和目标......? @BPL 对于 1 个明确定义的深度,正交投影可以切换到透视投影,反之亦然。您可以定义到相机的距离(深度),其中透视视锥体的边与正交长方体视图体积的边相交。

以上是关于如何像在 3dsMax 中一样实现鼠标缩放?的主要内容,如果未能解决你的问题,请参考以下文章

如何像在搅拌机中一样用鼠标拖动相机

Engine中如何实现鼠标滚轮缩放反置?

鼠标缩放实现,以鼠标位置为中心 [算法]

【缩放】实现svg以鼠标为焦点缩放

Three.js 3D缩放到鼠标位置

VB中如何实现图片自动缩放