3D First Person Class

Hello, everyone!
I have just started learning 3D yesterday and I’m already having a blast! Today, I made a First Person project with joystick controls! It took all day since I’m not that good with math. But that’s why I’m giving the code away to all those folks who need a simple way of making a First Person Camera.

Here’s the code:

displayMode(FULLSCREEN)
-- Use this function to perform your initial setup
function setup()
    Camera = FPCam({x = 0, y = 200, z = 0, limitYRot = true, fov = 90})
    
    Ground = mesh()
    Ground.vertices = {vec3(-700,0,-700),vec3(-700,0,700),vec3(700,0,-700),
    vec3(700,0,-700),vec3(-700,0,700),vec3(700,0,700)}
    Ground.texCoords = {vec2(0,0),vec2(0,1),vec2(1,0),vec2(1,0),vec2(0,1),vec2(1,1)}
    Ground.texture = readImage("SpaceCute:Background")
    
    UI = {
    JSRot = Joystick({x = WIDTH-150, y = 150, size = 170, colour = color(255,190,0)}),
    JSMov = Joystick({x = 150, y = 150, size = 170, colour = color(255,0,0)})
    }
    touches = {}
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)
    
    do
        local RotJs = UI.JSRot
        if RotJs.stickPos ~= vec2(0,0) then
            Camera.rotation.x = Camera.rotation.x + (RotJs.value.x*0.0005)
            Camera.rotation.y = Camera.rotation.y + (RotJs.value.y*0.0005)
        end
        local MovJs = UI.JSMov
        if MovJs.stickPos ~= vec2(0,0) then
            local Movement = (MovJs.value/10):rotate(-Camera.rotation.x)
            Camera.pos.x = Camera.pos.x + Movement.y
            Camera.pos.z = Camera.pos.z + Movement.x
        end
    end
    Camera:view()
    Ground:draw()
    
    viewMatrix(matrix())
    ortho()
    for _,ui in pairs(UI) do
        ui:draw()
    end
    for _,t in pairs(touches) do
        for _,ui in pairs(UI) do
            if ui.touched then
                ui:touched(t)
            end
        end
        if t.state == ENDED or t.state == CANCELLED then
            touches[t.id] = nil
        end
    end
end

function touched(t)
    touches[t.id] = {state=t.state,x=t.x,y=t.y,id=t.id}
end

FPCam = class()
function FPCam:init(data)
    self.pos = vec3()
    self.pos.x = data.x or 0
    self.pos.y = data.y or 100
    self.pos.z = data.z or 0
    self.fov = data.fov or 75
    self.rotation = vec2()
    self.rotation.x = data.xRot or 0
    self.rotation.y = data.yRot or 0
    self.limitYRot = data.limitYRot or false
    self.lookAt = vec3()
    self.lookAt.x = data.xLook or 0
    self.lookAt.y = data.yLook or 0
    self.lookAt.z = data.zLook or 0
    self.camM = data.camMode or 1
end

function FPCam:view()
    local posx,posy,posz = self.pos.x,self.pos.y,self.pos.z
    perspective()
    
    if self.camM == 1 then
        if self.limitYRot then
            if self.rotation.y > math.pi/2 then
                self.rotation.y = math.pi/2
                elseif self.rotation.y < -math.pi/2 then
                self.rotation.y = -math.pi/2
            end
        end
        self.lookAt.x = posx+math.cos(self.rotation.x)
        self.lookAt.y = posy+math.tan(self.rotation.y)
        self.lookAt.z = posz+math.sin(self.rotation.x)
    end
    camera(posx,posy,posz,self.lookAt.x,self.lookAt.y,self.lookAt.z)
end


Joystick = class()
function Joystick:init(data)
    self.pos = vec2(data.x,data.y)
    self.stickPos = vec2(0,0)
    self.c = color(data.colour.r,data.colour.g,data.colour.b,data.colour.a)
    self.size = data.size or 100
    self.visible = true
    if data.initialValue then
        self.value = data.initialValue * 1
        else
        self.value = vec2(0,0)
    end
end

function Joystick:draw()
    if self.visible then
        pushStyle()
        pushMatrix()
        
        fill(0,0)
        stroke(self.c)
        strokeWidth(self.size/20)
        ellipseMode(CENTER)
        
        translate(self.pos.x,self.pos.y)
        ellipse(0,0,self.size)
        
        fill(self.c)
        noStroke()
        
        translate(self.stickPos.x,self.stickPos.y)
        ellipse(0,0,self.size/1.5)
        
        popStyle()
        popMatrix()
    end
end

function Joystick:touched(t)
    if self.visible then
        local tv = vec2(t.x,t.y)
        local inside = tv:dist(self.pos) <= self.size/2
        
        if (t.state == ENDED or t.state == CANCELLED) and self.touching == t.id then
            self.touching = nil
            self.stickPos = vec2(0,0)
            
            elseif self.touching == t.id or (t.state == BEGAN and inside) then
            if t.state == BEGAN and inside then
                self.touching = t.id
            end
            if inside then
                self.stickPos = tv - self.pos
                else
                self.stickPos = (tv - self.pos):normalize() * (self.size/2)
            end
            self.value = self.stickPos
        end
    end
end

How to use the class:

The class has 2 modes:

  1. Look at mode (Forces the player to look at a 3D point)
  2. Rotate mode (The x and y rotation of the camera can be set)

Look at mode:
With the look at mode, you can set a 3D point for the camera to look at. You can switch your camera to look at mode with the .init() function (C = FPCam({camMode=0})) or with the .camM variable (C.camM = 0). set it to anything that isn’t 1 because mode 1 is rotate mode.

To set the x,y,z position to look at, you can do it like this

C = FPCam({xLook=100,yLook=0,zLook=300})

or like this

C.lookAt = vec3(100,0,300)

Rotate mode:
With the rotate mode, you can just rotate the camera rather than setting a location to look at. You can switch your camera to rotate mode, the same way you would switch your camera to look at mode except that you have to set it to mode 1 (example: C = FPCam({camMode=1})).

To change the rotation of the camera, all you have to do is this

C = FPCam({XRot=100,YRot=-200})

or this

C.rotation = vec2(100,200)

Note that the rotation is in radians, not degrees.
Also note that setting your camera to rotate mode and then running FPCam.view() will overwrite .lookAt.

You can stop your “head” from going below -math.pi and above math.pi (straight down and straight up) by setting limitYRot to true in either the .init() function or with the .limitYRot variable.

Other settings:
You can set the x,y,z position of the camera like this

C = Camera({x=0,y=300,z=400})

or like this

C.pos = vec3(0,300,400)

You can also set the FOV of your camera either like this

C = Camera({fov = 90})

or like this

C.fov = 90

Enjoy! :slight_smile:

You’re having fun! I greatly enjoyed 3D too, even though the math hurt my brain :s

Here’s the same thing but with different controls!:

--# Main

displayMode(FULLSCREEN)
-- Use this function to perform your initial setup
function setup()
    Camera = FPCam({x = 0, y = 200, z = 0, limitYRot = true, fov = 90})
    Cube = RectangularPrism({x = 0, y = -100, z = 0, w = 1000, h = 200, d = 1000})
    Cube.mesh.texture = readImage("SpaceCute:Background")
    Cube.mesh:setColors(255,255,255,255)
    
    UI = {
    --JSRot = Joystick({x = WIDTH-150, y = 150, size = 170, colour = color(255,190,0)}),
    JSMov = Joystick({x = 150, y = 150, size = 170, colour = color(255,0,0)})
    }
    touches = {}
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)
    
    do
        local MovJs = UI.JSMov
        if MovJs.stickPos ~= vec2(0,0) then
            local Movement = (MovJs.value/10):rotate(-Camera.rotation.x)
            Camera.pos.x = Camera.pos.x + Movement.y
            Camera.pos.z = Camera.pos.z + Movement.x
        end
    end
    Camera:view()
    Cube:draw()
    
    viewMatrix(matrix())
    ortho()
    for _,ui in pairs(UI) do
        ui:draw()
    end
    for _,t in pairs(touches) do
        local touchingUI = false
        for _,ui in pairs(UI) do
            if ui.touched then
                touchingUI = ui:touched(t) or touchingUI
            end
        end
        if not touchingUI then
            Camera.rotation.x = Camera.rotation.x + (t.deltaX*0.01)
            Camera.rotation.y = Camera.rotation.y + (t.deltaY*0.01)
        end
        if t.state == ENDED or t.state == CANCELLED or not touchingUI then
            touches[t.id] = nil
        end
    end
end

function touched(t)
    touches[t.id] = {state=t.state,x=t.x,y=t.y,id=t.id,deltaX=t.deltaX,deltaY=t.deltaY}
end

FPCam = class()
function FPCam:init(data)
    self.pos = vec3()
    self.pos.x = data.x or 0
    self.pos.y = data.y or 100
    self.pos.z = data.z or 0
    self.fov = data.fov or 75
    self.rotation = vec2()
    self.rotation.x = data.xRot or 0
    self.rotation.y = data.yRot or 0
    self.limitYRot = data.limitYRot or false
    self.lookAt = vec3()
    self.lookAt.x = data.xLook or 0
    self.lookAt.y = data.yLook or 0
    self.lookAt.z = data.zLook or 0
    self.camM = data.camMode or 1
end

function FPCam:view()
    local posx,posy,posz = self.pos.x,self.pos.y,self.pos.z
    perspective()
    
    if self.camM == 1 then
        if self.limitYRot then
            if self.rotation.y > math.pi/2 then
                self.rotation.y = math.pi/2
                elseif self.rotation.y < -math.pi/2 then
                self.rotation.y = -math.pi/2
            end
        end
        self.lookAt.x = posx+math.cos(self.rotation.x)
        self.lookAt.y = posy+math.tan(self.rotation.y)
        self.lookAt.z = posz+math.sin(self.rotation.x)
    end
    camera(posx,posy,posz,self.lookAt.x,self.lookAt.y,self.lookAt.z)
end

RectangularPrism = class()
function RectangularPrism:init(data)
    self.pos = vec3()
    self.pos.x = data.x
    self.pos.y = data.y
    self.pos.z = data.z
    self.w = data.w
    self.h = data.h
    self.d = data.d
    self.rotation = vec3()
    
    self.mesh = mesh()
    
    do -- A do loop because we won't be needing these local variables later on in this function.
        
        -- Below, we are setting up a few variables that will be needed a lot.
        local hlfW = self.w*0.5 -- the width will be along the x axis,
        local hlfH = self.h*0.5 -- the height will be along the y axis,
        local hlfD = self.d*0.5 -- and the depth will be along the z axis.
        
        -- We'll assume North is positive X, East is positive Z, South is negative X and West is negative Z,
        -- to make things easier to understand.
        -- Below, we are setting variables that define the corners in a rectangular prism.
        local topNorthWest = vec3(hlfW, hlfH, -hlfD)
        local topNorthEast = vec3(hlfW, hlfH, hlfD)
        local bottomNorthWest = vec3(hlfW, -hlfH, -hlfD)
        local bottomNorthEast = vec3(hlfW, -hlfH, hlfD)
        
        -- The southern side of the rectangular prism will have the same corners except their x axis will be negative.
        local topSouthWest = vec3(-hlfW, hlfH, -hlfD)
        local topSouthEast = vec3(-hlfW, hlfH, hlfD)
        local bottomSouthWest = vec3(-hlfW, -hlfH, -hlfD)
        local bottomSouthEast = vec3(-hlfW, -hlfH, hlfD)
        
        -- Now that we know all 8 corners in our prism, it should be easy to set the mesh vertices.
        
        -- Below, we are setting the vertices for the mesh.
        self.mesh.vertices = {
        topNorthWest,topNorthEast,topSouthWest,-- triangle 1 on top
        topNorthEast,topSouthWest,topSouthEast,-- triangle 2 on top
        bottomNorthWest,bottomNorthEast,bottomSouthWest,-- triangle 1 on bottom
        bottomNorthEast,bottomSouthWest,bottomSouthEast,-- triangle 2 on bottom
        bottomNorthWest,bottomNorthEast,topNorthWest,-- bottom triangle to North
        bottomNorthEast,topNorthWest,topNorthEast,-- top triangle to North
        bottomNorthEast,bottomSouthEast,topNorthEast,-- bottom triangle to East
        bottomSouthEast,topNorthEast,topSouthEast,-- top triangle to East
        bottomSouthWest,bottomSouthEast,topSouthWest,-- bottom triangle to South
        bottomSouthEast,topSouthWest,topSouthEast,-- top triangle to South
        bottomNorthWest,bottomSouthWest,topNorthWest,-- bottom triangle to West
        bottomSouthWest,topNorthWest,topSouthWest,-- top triangle to West
        }
    end
    
    -- Below we set some variables for the texture coordinates.
    local topLeft = vec2(0,1)
    local topRight = vec2(1,1)
    local bottomLeft = vec2(0,0)
    local bottomRight = vec2(1,0)
    
    -- Below, we are setting the texture coordinates.
    self.mesh.texCoords = {
    topLeft,topRight,bottomLeft, -- triangle 1 on top
    topRight,bottomLeft,bottomRight, -- triangle 2 on top
    topLeft,topRight,bottomLeft, -- triangle 1 on bottom
    topRight,bottomLeft,bottomRight, -- triangle 2 on bottom
    bottomLeft,bottomRight,topLeft, -- bottom triangle to North
    bottomRight,topLeft,topRight, -- top triangle to North
    bottomRight,bottomLeft,topRight, -- bottom triangle to East
    bottomLeft,topRight,topLeft, -- top triangle to East
    bottomRight,bottomLeft,topRight, -- bottom triangle to South
    bottomLeft,topRight,topLeft, -- top triangle to South
    bottomLeft,bottomRight,topLeft, -- bottom triangle to West
    bottomRight,topLeft,topRight -- top triangle to West
    }
    
end

function RectangularPrism:draw()
    pushMatrix()
    
    translate(self.pos.x,self.pos.y,self.pos.z)
    rotate(self.rotation.x, 1, 0, 0)
    rotate(self.rotation.y, 0, 1, 0)
    rotate(self.rotation.z, 0, 0, 1)
    
    self.mesh:draw()
    
    popMatrix()
end

Joystick = class()
function Joystick:init(data)
    self.pos = vec2(data.x,data.y)
    self.stickPos = vec2(0,0)
    self.c = color(data.colour.r,data.colour.g,data.colour.b,data.colour.a)
    self.size = data.size or 100
    self.visible = true
    if data.initialValue then
        self.value = data.initialValue * 1
        else
        self.value = vec2(0,0)
    end
end

function Joystick:draw()
    if self.visible then
        pushStyle()
        pushMatrix()
        
        fill(0,0)
        stroke(self.c)
        strokeWidth(self.size/20)
        ellipseMode(CENTER)
        
        translate(self.pos.x,self.pos.y)
        ellipse(0,0,self.size)
        
        fill(self.c)
        noStroke()
        
        translate(self.stickPos.x,self.stickPos.y)
        ellipse(0,0,self.size/1.5)
        
        popStyle()
        popMatrix()
    end
end

function Joystick:touched(t)
    if self.visible then
        local tv = vec2(t.x,t.y)
        local inside = tv:dist(self.pos) <= self.size/2
        
        if (t.state == ENDED or t.state == CANCELLED) and self.touching == t.id then
            self.touching = nil
            self.stickPos = vec2(0,0)
            
            elseif self.touching == t.id or (t.state == BEGAN and inside) then
            if t.state == BEGAN and inside then
                self.touching = t.id
            end
            if inside then
                self.stickPos = tv - self.pos
                else
                self.stickPos = (tv - self.pos):normalize() * (self.size/2)
            end
            self.value = self.stickPos
        end
        return self.touching and self.touching == t.id
    end
end