Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

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! :)

Comments

  • IgnatzIgnatz Mod
    Posts: 5,396

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

  • Posts: 160

    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
    
Sign In or Register to comment.