使用自己的新轴的相机的 3D 旋转

3D Rotation of a camera using its own, new axes

为了理解3D图形(主要是矩阵变换部分), 我在 tkinter 中制作了一个简单的 3D 引擎。

问题是现在加了摄像头后卡住了。我想绑定 WASDEQ 键来移动相机(W 向上,S 向下,A 向左,D 向右,E 向前,Q 向后)和方向键旋转它。然而,在尝试这些功能时,我发现它们总是相对于 XYZ 轴完成的。这意味着如果我让我的相机笔直向下,我会期望(在按下 E 之后)朝向它正在看的地方。但是,它转到负 Z 轴。

旋转相机也是如此。出于某种原因,向左和向右看总是相对于相机的当前位置完成的,但向上和向下看是相对于 X 轴完成的,而不是当前相机位置。如果有人能指出我的解决方案或 material 来解释必要的转换顺序,我将不胜感激。

完整代码如下,比较短:

import numpy
import math
import tkinter

#Window dimensions
width = 800
height = 800

#An empty canvas
cnv = tkinter.Canvas(bg='white', width=width, height=height)

#Triangle rasterization
def drawTriangle(triangle):
    cnv.create_line(triangle[0][0],height-triangle[0][1],triangle[1][0],height-triangle[1][1])
    cnv.create_line(triangle[1][0],height-triangle[1][1],triangle[2][0],height-triangle[2][1])
    cnv.create_line(triangle[2][0],height-triangle[2][1],triangle[0][0],height-triangle[0][1])

#Adding a homogenous coordinate (w)
def homogenous(vertex):
    vertex.append(1)

#Transforming row major vertexes to column major vertexes
def transpose(vertex):
    return numpy.array([vertex]).T


#SPACE CONVERSION

#Our cube is in its model space. We want to put it onto our scene, while rotating it a bit and moving it further away from the camera.
#model space->world space
#Applying the transformation to all of our vertexes
def modelToWorld(vertex,x,y,z):
    # Rotation angles
    xangle = math.radians(x)
    yangle = math.radians(y)
    zangle = math.radians(z)

    # Rotation matrices
    xRotationMatrix = numpy.array(
        [[1, 0, 0, 0], [0, math.cos(xangle), -math.sin(xangle), 0], [0, math.sin(xangle), math.cos(xangle), 0],
         [0, 0, 0, 1]])
    yRotationMatrix = numpy.array(
        [[math.cos(yangle), 0, math.sin(yangle), 0], [0, 1, 0, 0], [-math.sin(yangle), 0, math.cos(yangle), 0],
         [0, 0, 0, 1]])
    zRotationMatrix = numpy.array(
        [[math.cos(zangle), -math.sin(zangle), 0, 0], [math.sin(zangle), math.cos(zangle), 0, 0], [0, 0, 1, 0],
         [0, 0, 0, 1]])
    # Translation along the negative Z axis
    TranslationMatrix = numpy.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, -6], [0, 0, 0, 1]])
    # Combining the transformations into one model matrix
    ModelMatrix = numpy.dot(yRotationMatrix, xRotationMatrix)
    ModelMatrix = numpy.dot(zRotationMatrix, ModelMatrix)
    ModelMatrix = numpy.dot(TranslationMatrix, ModelMatrix)
    return numpy.dot(ModelMatrix,vertex)


#Now we want to move our camera
#We cannot move the camera itself, we need to move the world. So in order to move the camera 1 unit closer to the cube,
#we need to move the cube closer to the camera. Remember, the camera always points to the negative Z axis.
#world space->view space
zoom_num=0
#View matrix
#Applying the transformation to all of our vertexes
xcam=0
ycam=0
zcam=0
camXangle=0
camYangle=0
camZangle=0
def worldToView(vertex):
    CamyRotationMatrix = numpy.array(
        [[math.cos(math.radians(camYangle)), 0, math.sin(math.radians(camYangle)), 0], [0, 1, 0, 0], [-math.sin(math.radians(camYangle)), 0, math.cos(math.radians(camYangle)), 0],
         [0, 0, 0, 1]])
    CamxRotationMatrix = numpy.array(
        [[1, 0, 0, 0], [0, math.cos(math.radians(camXangle)), -math.sin(math.radians(camXangle)), 0], [0, math.sin(math.radians(camXangle)), math.cos(math.radians(camXangle)), 0],
         [0, 0, 0, 1]])
    CamTranslationMatrix = numpy.array([[1, 0, 0, 0+xcam], [0, 1, 0, 0+ycam], [0, 0, 1, 0 + zcam], [0, 0, 0, 1]])
    CamRotationMatrix = numpy.dot(CamyRotationMatrix, CamxRotationMatrix)
    ViewMatrix = numpy.dot(CamRotationMatrix, CamTranslationMatrix)
    return numpy.dot(ViewMatrix,vertex)

#Now we need to apply the projection matrix to create perspective.
#view space->clip space

#Projection matrix
ProjectionMatrix = numpy.array([[0.8,0,0,0], [0,0.8,0,0],[0,0,-1.22,-2.22],[0,0,-1,0]])
#ProjectionMatrix = numpy.array([[0.25,0,0,0], [0,0.25,0,0],[0,0,-0.22,-1.22],[0,0,0,1]])
def viewToClip(vertex):
    return numpy.dot(ProjectionMatrix,vertex)


#In order to turn the resulting coordinates into NDC, we need to divide by W.
def perspectiveDivision(vertex):
    for j in range(4):
        vertex[j]=vertex[j]/vertex[3]
    return vertex

#Turning values from -1 to 1 into individual pixels on the screen
def viewportTransformation(vertex):
    vertex[0] = (vertex[0] * 0.5 + 0.5) * width
    vertex[1] = (vertex[1] * 0.5 + 0.5) * height
    return vertex

#Rounding the resulting values
def roundPixel(vertex):
    vertex[0]=  int(round(vertex[0][0]))
    vertex[1] = int(round(vertex[1][0]))
    return vertex




#Vertexes of cube triangles
cubeMesh2=[
                [-1,-1,-1],[1,-1,-1],[1,-1,1]  ,  [-1,-1,-1],[1,-1,1],[-1,-1,1], #TOP
                [1,-1,-1],[1,1,-1],[1,1,1]     ,  [1,-1,-1],[1,1,1],[1,-1,1],    #RIGHT
                [-1,1,-1],[-1,-1,-1],[-1,-1,1] ,  [-1,1,-1],[-1,-1,1],[-1,1,1],  #LEFT
                [1,1,-1],[-1,1,-1],[-1,1,1]    ,  [1,1,-1],[-1,1,1],[1,1,1],     #BOTTOM
                [-1,-1,1],[1,-1,1],[1,1,1]   ,  [-1,-1,1],[1,1,1],[-1,1,1],      #NEAR
                [1,-1,-1],[-1,-1,-1],[-1,1,-1]  ,  [1,-1,-1],[-1,1,-1],[1,1,-1]  #FAR
]

cubeMesh=[
[-2, 0, -2],[2, 0, -2],[-2, 0, 2],
[-2, 0, 2],[2, 0, -2],[2, 0, 2],
[-0.5, 0., -0.5 ],
[-0.5, 1., -0.5 ],
[-0.5, 0, 0.5],
[-0.5, 1, -0.5],
[-0.5, 0., 0.5],
[-0.5, 1., 0.5],
[-0.5, 0., -0.5],
[-0.5, 1., -0.5],
[0.5, 0., -0.5],
[-0.5, 1., -0.5],
[0.5, 0., -0.5],
[0.5, 1., -0.5],
[0.5, 0., -0.5],
[0.5, 1., -0.5],
[0.5, 0., 0.5],
[0.5, 1., -0.5],
[0.5, 0., 0.5],
[0.5, 1, 0.5],
[-0.5, 0., 0.5],
[-0.5, 1., 0.5],
[0.5, 0., 0.5],
[-0.5, 1., 0.5],
[0.5, 0., 0.5],
[0.5, 1., 0.5],
[-0.5, 1., -0.5],
[-0.5, 1., 0.5],
[0., 2., 0.0],
[0.5, 1., -0.5],
[0.5, 1., 0.5],
[0., 2., 0.0],
[-0.5, 1., -0.5],
[0.5, 1., -0.5],
[0., 2., 0.0],
[-0.5, 1, 0.5],
[0.5, 1, 0.5],
[0., 2., 0.0]
]


#An empty triangle
Triangle=[[0,0,0],[0,0,0],[0,0,0]]

#Colors
colors = [(255,0,0),(255,0,0),(0,255,0),(0,255,0),(0,0,255),(0,0,255),(255,255,0),(255,255,0),(0,255,255),(0,255,255),(255,0,255),(255,0,255)]

#Triangle counter
counter=0
cnv.pack()


for i in range(len(cubeMesh)):
    homogenous(cubeMesh[i])               # Adding a homogenous coordinate
    cubeMesh[i] = transpose(cubeMesh[i])  # Changing a row vector to a column vector


changingMesh = cubeMesh.copy()
j=0
def update():
        cnv.delete("all")
        counter=0
        global j
        for i in range(len(cubeMesh)):
            changingMesh[i]=modelToWorld(cubeMesh[i],0,0,0)              #Moving our model to its place on world coordinates
            changingMesh[i]=worldToView(changingMesh[i])               #Moving the world relative to our camera ("moving" the camera)
            changingMesh[i]=viewToClip(changingMesh[i])                #Applying projection
            changingMesh[i] = perspectiveDivision(changingMesh[i])  # Dividing by W to get to normalised device coordinates
            changingMesh[i] = viewportTransformation(changingMesh[i])  # Changing the normalised device coordinates to pixels on the screen
            changingMesh[i] = roundPixel(changingMesh[i])  # Rounding the resulting values to nearest pixel
            Triangle[i%3][0] = int(changingMesh[i][0])
            Triangle[i%3][1] = int(changingMesh[i][1])
            Triangle[i % 3][2] = int(changingMesh[i][2])
            if i%3==2:
                drawTriangle(Triangle)
                counter+=1
        j+=1
        if j == 365:
            j=0
        cnv.after(5,update)

update()
def move(event):
    global xcam
    global ycam
    global zcam
    if event.char=='q':
        zcam+=0.2
    if event.char == 'e':
        zcam-=0.2
    if event.char=='a':
        xcam+=0.2
    if event.char == 'd':
        xcam-=0.2
    if event.char=='w':
        ycam-=0.2
    if event.char == 's':
        ycam+=0.2

def rotateLeft(event):
    global camYangle
    camYangle-=4
def rotateRight(event):
    global camYangle
    camYangle+=4
def rotateUp(event):
    print("hey")
    global camXangle
    camXangle+=4
def rotateDown(event):
    global camXangle
    camXangle-=4

cnv.bind_all('<Key>', move)
cnv.bind_all('<Left>',rotateLeft)
cnv.bind_all('<Right>',rotateRight)
cnv.bind_all('<Up>',rotateUp)
cnv.bind_all('<Down>',rotateDown)


tkinter.mainloop()

矩阵乘法不是Commutative。旋转和移动是一个持续的过程。移动取决于之前的旋转和移动:

new_view = translate * rotate * current_view
current_view = new_view 

为视图矩阵创建一个全局变量。将新的平移和旋转应用于视图矩阵并存储下一帧的视图矩阵。重置旋转和平移变量:

xcam, ycam, zcam = 0, 0, 0
camXangle, camYangle, camZangle = 0, 0, 0
ViewMatrix = numpy.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])

def updateView():
    global ViewMatrix, xcam, ycam, zcam, camXangle, camYangle, camZangle

    CamyRotationMatrix = numpy.array(
        [[math.cos(math.radians(camYangle)), 0, math.sin(math.radians(camYangle)), 0], [0, 1, 0, 0], [-math.sin(math.radians(camYangle)), 0, math.cos(math.radians(camYangle)), 0],
         [0, 0, 0, 1]])
    CamxRotationMatrix = numpy.array(
        [[1, 0, 0, 0], [0, math.cos(math.radians(camXangle)), -math.sin(math.radians(camXangle)), 0], [0, math.sin(math.radians(camXangle)), math.cos(math.radians(camXangle)), 0],
         [0, 0, 0, 1]])
    CamTranslationMatrix = numpy.array([[1, 0, 0, 0+xcam], [0, 1, 0, 0+ycam], [0, 0, 1, 0 + zcam], [0, 0, 0, 1]])
    
    ViewMatrix = numpy.dot(CamyRotationMatrix, ViewMatrix)
    ViewMatrix = numpy.dot(CamxRotationMatrix, ViewMatrix)
    ViewMatrix = numpy.dot(CamTranslationMatrix, ViewMatrix)
    
    xcam, ycam, zcam = 0, 0, 0
    camXangle, camYangle, camZangle = 0, 0, 0
def worldToView(vertex):
    return numpy.dot(ViewMatrix,vertex)
def update():
    cnv.delete("all")
    counter=0
    global j
    
    updateView()  # <---
     
    for i in range(len(cubeMesh)):
        changingMesh[i]=modelToWorld(cubeMesh[i],0,0,0)              #Moving our model to its place on world coordinates
        changingMesh[i]=worldToView(changingMesh[i])               #Moving the world relative to our camera ("moving" the camera)

        # [...]