ampersander
ampersander

Reputation: 288

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

in order to understand 3D graphics (mostly the matrix transformation part), I've made a simple 3D engine in tkinter.

The problem is that now, after adding a camera, I am stuck. I wanted to bind WASDEQ keys to move the camera (W is up, S is down, A is left, D is right, E is forwatd, Q is backwards) and arrow keys to rotate it. However, upon trying out these features, I discovered that they are always done relative to the XYZ axes. That means that if I have my camera pointed straight downwards, I would expect (after pressing E) to go toward the place it's looking at. However, it goes to the negative Z axis.

The same goes to rotating the camera. For some reason, looking left and right is always done relative to the camera's current position, but looking up and down is done relative to the X axis, not the current camera position. If anyone could point me towards a solution or a material which would explain the necessary transformation order, I would be grateful.

Here is the entire code, it's relatively short:

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()

Upvotes: 1

Views: 819

Answers (1)

Rabbid76
Rabbid76

Reputation: 210889

Matrix multiplications are not Commutative. The rotation and movement is an ongoing process. The movement depends on the previous rotation and movement:

new_view = translate * rotate * current_view
current_view = new_view 

Create a global variable for the for the view matrix. Apply the new tranalation and rotation to the view matrix and store the view matrix for the next frame. Reset the variables for rotation and translation:

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)

        # [...]

Upvotes: 1

Related Questions