Как реализовать панорамирование камеры как в 3dsMax? - PullRequest
0 голосов
/ 13 декабря 2018

Какая математика необходима для достижения эффекта панорамирования камеры, используемого в 3ds max?

В 3ds max расстояние между курсором и сеткой всегда будет оставаться одинаковым на протяжении всего движения (mouse_down + mouse_motion+ mouse_up).

Моя наивная и неудачная попытка была попытка переместить камеру на плоскость XY, используя dt (время кадра), умноженное на некоторую жестко закодированную константу, и результат действительно уродлив и не интуитивен.

Код, который я получил до сих пор:

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

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

def pan(self, delta):
    forward = vec3.normalize(self.target - self.eye)
    right = vec3.normalize(vec3.cross(forward, self.up))
    up = vec3.normalize(vec3.cross(forward, right))

    if delta.x:
        right = right*delta.x
    if delta.y:
        up = up*delta.y

    self.eye+=(right+up)
    self.target+=(right+up)

Не могли бы вы объяснить, как работает математика панорамирования камеры в 3dsmax?

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

Сначала на мой вопрос уже ответил @ Rabbid76, но есть еще один случай, когда его алгоритм не будет работать должным образом.Он не обрабатывает должным образом случай, когда панорамирование запускается из пустого пространства (иначе говоря, когда значение буфера глубины принимает дальнее значение = 1,0).В 3dsmax панорамирование камеры выполняется правильно во всех ситуациях, независимо от того, какое значение буфера глубины.

Ответы [ 2 ]

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

[...] но есть еще один случай, когда его алгоритм не будет работать должным образом.Он не обрабатывает должным образом случай, когда панорамирование начинается с пустого пространства [...]

В решении глубина объекта берется из буфера глубины, в той позиции, гдепроисходит щелчок мышью.Если это «пустое пространство», позиция, в которой не было нарисовано ни одного объекта, глубина - это максимум диапазона глубины (обычно 1).Это приводит к быстрому окрашиванию.

Решением или обходным решением будет использование глубины репрезентативного положения сцены.например, происхождение мира:

pt_drag = glm.vec3(0, 0, 0)

Конечно, это не может привести к надлежащему результату в каждом случае.Если объекты сцены не находятся вокруг происхождения мира, этот подход потерпит неудачу.Я рекомендую рассчитать центр ограничивающей рамки, выровненной по оси сцены.Используйте эту точку для репрезентативной «глубины»:

box_min = ... # glm.vec3
box_max = ... # glm.vec3

pt_drag = (box_min + box_max) / 2

Глубина точки может быть вычислена путем преобразования с использованием матрицы вида и проекции и окончательного деления перспективы:

o_clip = self.proj * self.view * glm.vec4(pt_drag, 1)
o_ndc  = glm.vec3(o_clip) / o_clip.w

Это можно применить к функции glut_mouse:

def glut_mouse(self, button, state, x, y):
    self.drag = state == GLUT_DOWN
    self.last_mouse_pos = glm.vec2(x, self.height-y)
    self.mouse_down_pos = glm.vec2(x, self.height-y)

    if self.drag:
        depth_buffer = glReadPixels(x, self.height-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
        self.last_depth = depth_buffer[0][0]
        if self.last_depth == 1:
            pt_drag = glm.vec3(0, 0, 0)
            o_clip  = self.proj * self.view * glm.vec4(pt_drag, 1)
            o_ndc   = glm.vec3(o_clip) / o_clip.w
            if o_ndc.z > -1 and o_ndc.z < 1:
                self.last_depth = o_ndc.z * 0.5 + 0.5

Предварительный просмотр:

Ключ к решению проблемы хорошего самочувствияэто найти «правильную» глубину.При проекции в перспективе перетаскивание, при котором движение мыши воздействует на объект в движении 1: 1, проецируемое в область просмотра, корректно работает только на четко определенной глубине.Объекты с разной глубиной смещаются в разном масштабе, когда они проецируются на видовой экран, это «природа» перспективы.

Чтобы найти «правильную» глубину, существуют разные возможности, которые зависят от ваших потребностей:

  • Считывание глубины из буфера глубины в текущей позиции мыши:
depth_buffer = glReadPixels(x, self.height-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)    
self.last_depth = depth_buffer[0][0]
  • Получить минимальную и максимальную глубину буфера глубины (кромезначение для дальней плоскости, 1.0 ) и рассчитать среднюю глубину.Конечно, весь буфер глубины должен быть исследован в этом случае:
d_buf = glReadPixels(0, 0, self.width, self.height, GL_DEPTH_COMPONENT, GL_FLOAT)
d_vals = [float(d_buf[i][j]) for i in range(self.width) for j in range(self.height) if d_buf[i][j] != 1]
if len(d_vals) > 0:
    self.last_depth = (min(d_vals) + max(d_vals)) / 2 
  • Используйте происхождение мира:
pt_drag = glm.vec3(0, 0, 0)
o_clip  = self.proj * self.view * glm.vec4(pt_drag, 1)
o_ndc   = glm.vec3(o_clip) / o_clip.w
if o_ndc.z > -1 and o_ndc.z < 1:
    self.last_depth = o_ndc.z * 0.5 + 0.5 
  • Вычисление центра ограничительной рамки сцены.

  • Реализация лучевого вещания, которое идентифицирует объект по лучу, который начинается в точке обзора, проходящей черезположение курсора (мыши).Этот алгоритм может быть усовершенствован путем определения объекта, который находится "ближе всего" к лучу, когда ни один объект не попадет.

0 голосов
/ 16 декабря 2018

Ваше решение будет работать при ортогональной проекции, но при перспективной проекции оно не работает.Обратите внимание, что в перспективной проекции матрица проекции описывает отображение из трехмерных точек в мире, которые они видят из камеры-обскуры, в двумерные точки области просмотра.

Величина смещения для глаза и положения целизависит от глубины объекта, который перетаскивается в окне просмотра.

Если объект находится близко к положению глаза, то перемещение в окне просмотра приводит к небольшому смещению положения глаза и цели:

Если расстояние от объекта до глаза слишком велико, то перемещение в области просмотра приводит к большому смещению положения глаза и цели:

Чтобы сделать то, что вы хотите, вы должны знать размер области просмотра, матрицы вида и матрицы проекции:

self.width   # width of the viewport
self.height  # height of the viewport
self.view    # view matrix
self.proj    # prjection matrix

Измените метод pane, чтобы он получал новую и старую позицию мыши.Обратите внимание, что ось y должна быть перевернута (self.height-y).Получите глубину точки попадания (объекта) с помощью glReadPixels, используя тип формата GL_DEPTH_COMPONENT:

def glut_mouse(self, button, state, x, y):
    self.drag = state == GLUT_DOWN
    self.last_mouse_pos = glm.vec2(x, self.height-y)
    self.mouse_down_pos = glm.vec2(x, self.height-y)
    if self.drag:
        depth_buffer = glReadPixels(x, self.height-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
        self.last_depth = depth_buffer[0][0]
        print(self.last_depth)

def glut_motion(self, x, y):
    if not self.drag:
       return
    old_pos = self.last_mouse_pos
    new_pos = glm.vec2(x, self.__vp_size[1]-y)
    self.last_mouse_pos = new_pos 
    self.pan(self.last_depth, old_pos, new_pos)

def pan(self, depth, old_pos, new_pos):
    # .....

Положение мыши дает позицию в пространстве окна, где zкоордината - глубина точки попадания или объекта:

wnd_from    = glm.vec3(old_pos[0], old_pos[1], float(depth))
wnd_to      = glm.vec3(new_pos[0], new_pos[1], float(depth))

Эти позиции могут быть преобразованы в мировое пространство с помощью glm.unProject:

vp_rect     = glm.vec4(0, 0, self.width, self.height)
world_from  = glm.unProject(wnd_from, self.view, self.proj, vp_rect)
world_to    = glm.unProject(wnd_to, self.view, self.proj, vp_rect)

Миркосмическое смещение глаза и целевой позиции - это расстояние от старой до новой мировой позиции:

world_vec   = world_to - world_from

Наконец, рассчитайте новый глаз и целевую позицию и обновите матрицу вида:

self.eye    = self.eye - world_vec
self.target = self.target - world_vec
self.view   = glm.lookAt(self.eye, self.target, self.up)

Я тестировал код на следующем примере:

Предварительный просмотр:

Полный код Python:

import os
import math
import numpy as np
import glm
from OpenGL.GLUT import *
from OpenGL.GL import *
from OpenGL.GL.shaders import *
from OpenGL.arrays import *
from ctypes import c_void_p

class MyWindow:

    __caption = 'OpenGL Window'
    __vp_size = [800, 600]
    __vp_valid = False
    __glut_wnd = None

    __glsl_vert = """
        #version 450 core

        layout (location = 0) in vec3 a_pos;
        layout (location = 1) in vec3 a_nv;
        layout (location = 2) in vec4 a_col;

        out vec3 v_pos;
        out vec3 v_nv;
        out vec4 v_color;

        uniform mat4 u_proj;
        uniform mat4 u_view;
        uniform mat4 u_model;

        void main()
        {
            mat4 model_view = u_view * u_model;
            mat3 normal     = transpose(inverse(mat3(model_view)));

            vec4 view_pos   = model_view * vec4(a_pos.xyz, 1.0);

            v_pos       = view_pos.xyz;
            v_nv        = normal * a_nv;  
            v_color     = a_col;
            gl_Position = u_proj * view_pos;
        }
    """

    __glsl_frag = """
        #version 450 core

        out vec4 frag_color;
        in  vec3 v_pos;
        in  vec3 v_nv;
        in  vec4 v_color;

        void main()
        {
            vec3  N    = normalize(v_nv);
            vec3  V    = -normalize(v_pos);
            float ka   = 0.1;
            float kd   = max(0.0, dot(N, V)) * 0.9;
            frag_color = vec4(v_color.rgb * (ka + kd), v_color.a);
        }
    """

    __program = None
    __vao = None
    __vbo = None
    __no_vert = 0

    def __init__(self, w, h):

        self.__vp_size = [w, h]

        glutInit()
        glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
        glutInitWindowSize(self.__vp_size[0], self.__vp_size[1])
        __glut_wnd = glutCreateWindow(self.__caption)

        self.__program = compileProgram( 
            compileShader( self.__glsl_vert, GL_VERTEX_SHADER ),
            compileShader( self.__glsl_frag, GL_FRAGMENT_SHADER ),
        )

        self.___attrib = { a : glGetAttribLocation (self.__program, a) for a in ['a_pos', 'a_nv', 'a_col'] }
        print(self.___attrib)

        self.___uniform = { u : glGetUniformLocation (self.__program, u) for u in ['u_model', 'u_view', 'u_proj'] }
        print(self.___uniform)

        v = [ -1,-1,1,  1,-1,1,  1,1,1, -1,1,1, -1,-1,-1,  1,-1,-1,  1,1,-1, -1,1,-1 ]
        c = [ 1.0, 0.0, 0.0,   1.0, 0.5, 0.0,    1.0, 0.0, 1.0,   1.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 0.0, 1.0 ]
        n = [ 0,0,1, 1,0,0, 0,0,-1, -1,0,0, 0,1,0, 0,-1,0 ]
        e = [ 0,1,2,3, 1,5,6,2, 5,4,7,6, 4,0,3,7, 3,2,6,7, 1,0,4,5 ]
        attr_array = []
        for si in range(6):
            for vi in range(6):
                ci = [0, 1, 2, 0, 2, 3][vi]
                i = si*4+ci
                attr_array.extend( [ v[e[i]*3], v[e[i]*3+1], v[e[i]*3+2] ] )
                attr_array.extend( [ n[si*3], n[si*3+1], n[si*3+2] ] )
                attr_array.extend( [ c[si*3], c[si*3+1], c[si*3+2], 1 ] ); 
        self.__no_vert = len(attr_array) // 10

        vertex_attributes = np.array(attr_array, dtype=np.float32)

        self.__vbo = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, self.__vbo)
        glBufferData(GL_ARRAY_BUFFER, vertex_attributes, GL_STATIC_DRAW)

        self.__vao = glGenVertexArrays(1)
        glBindVertexArray(self.__vao)
        glVertexAttribPointer(0, 3, GL_FLOAT, False, 10*vertex_attributes.itemsize, None)
        glEnableVertexAttribArray(0)
        glVertexAttribPointer(1, 3, GL_FLOAT, False, 10*vertex_attributes.itemsize, c_void_p(3*vertex_attributes.itemsize))
        glEnableVertexAttribArray(1)
        glVertexAttribPointer(2, 4, GL_FLOAT, False, 10*vertex_attributes.itemsize, c_void_p(6*vertex_attributes.itemsize))
        glEnableVertexAttribArray(2)

        glEnable(GL_DEPTH_TEST)
        glUseProgram(self.__program)

        glutReshapeFunc(self.__reshape)
        glutDisplayFunc(self.__mainloop)
        glutMouseFunc(self.glut_mouse)
        glutMotionFunc(self.glut_motion)

        self.drag = False

        self.eye    = glm.vec3(-3, -7, 6)
        self.target = glm.vec3(0, 0, 0)
        self.up     = glm.vec3(0, 0, 1)

        self.near  = 0.1
        self.far   = 100.0
        aspect     = self.__vp_size[0]/self.__vp_size[1]
        self.proj  = glm.perspective(glm.radians(90.0), aspect, self.near, self.far)
        self.view  = glm.lookAt(self.eye, self.target, self.up)
        self.model = glm.mat4(1)  

    def run(self):
        self.__starttime = 0
        self.__starttime = self.elapsed_ms()
        glutMainLoop()

    def elapsed_ms(self):
      return glutGet(GLUT_ELAPSED_TIME) - self.__starttime

    def __reshape(self, w, h):
        self.__vp_valid = False

    def __mainloop(self):

        if not self.__vp_valid:
            self.width      = glutGet(GLUT_WINDOW_WIDTH)
            self.height     = glutGet(GLUT_WINDOW_HEIGHT)
            self.__vp_size  = [self.width, self.height]
            self.__vp_valid = True
            aspect          = self.width / self.height
            self.proj       = glm.perspective(glm.radians(90.0), aspect, self.near, self.far)

        glUniformMatrix4fv(self.___uniform['u_proj'], 1, GL_FALSE, glm.value_ptr(self.proj) )
        glUniformMatrix4fv(self.___uniform['u_view'], 1, GL_FALSE, glm.value_ptr(self.view) )
        glUniformMatrix4fv(self.___uniform['u_model'], 1, GL_FALSE, glm.value_ptr(self.model) )

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

        glDrawArrays(GL_TRIANGLES, 0, self.__no_vert)

        glutSwapBuffers()
        glutPostRedisplay()

    def glut_mouse(self, button, state, x, y):
        self.drag = state == GLUT_DOWN
        self.last_mouse_pos = glm.vec2(x, self.height-y)
        self.mouse_down_pos = glm.vec2(x, self.height-y)
        if self.drag:
            depth_buffer = glReadPixels(x, self.height-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
            self.last_depth = depth_buffer[0][0]
            print(self.last_depth)

    def glut_motion(self, x, y):
        if not self.drag:
          return
        old_pos = self.last_mouse_pos
        new_pos = glm.vec2(x, self.__vp_size[1]-y)
        self.last_mouse_pos = new_pos 
        self.pan(self.last_depth, old_pos, new_pos)

    def pan(self, depth, old_pos, new_pos):

        wnd_from    = glm.vec3(old_pos[0], old_pos[1], float(depth))
        wnd_to      = glm.vec3(new_pos[0], new_pos[1], float(depth))

        vp_rect     = glm.vec4(0, 0, self.width, self.height)
        world_from  = glm.unProject(wnd_from, self.view, self.proj, vp_rect)
        world_to    = glm.unProject(wnd_to, self.view, self.proj, vp_rect)

        world_vec   = world_to - world_from

        self.eye    = self.eye - world_vec
        self.target = self.target - world_vec
        self.view   = glm.lookAt(self.eye, self.target, self.up)

window = MyWindow(800, 600)
window.run()
...