3d 101

In response to a question on another thread I thought I’d try a very lightweight tutorial on the basics of 3d… hope you find it useful.

Below is code of the most limited thing you can do to get something working in 3d. The first thing you need is a mesh, all objects you draw are meshes, they are defined as a set of triangles specified as 3 vec3 coordinates for each corner of the triangle. In setup we create a basic triangle along the x,y plane sitting directly on top of the x axis.

Then in draw we must do 2 things before drawing the mesh. We need to configure our camera. This is 2 sets of coordinates, the first 3 numbers are the x,y,z of the position of the camera or eye. So below we put the camera on the ground 50 out from the screen. The second set of numbers is what the eye is looking at, so we put this looking straight back at the origin (0,0,0). You also need to call perspective to get it into perspective projection.

I’ve parameterised a couple of elements so you can see what impact they have.

-- 3d starter

-- Use this function to perform your initial setup
function setup()
    myMesh = mesh()
    myMesh.vertices = {vec3(-10,0,0), vec3(10,0,0), vec3(0,20,0)}
    myMesh:setColors(color(255))
    parameter.integer("cameraX",-40,40,0)
    parameter.integer("lookatX",-20,20,0)
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    camera(cameraX,0,-50,lookatX,0,0)
    perspective()
    myMesh:draw()
    
end

As a next step I’ve done 2 things.

First, let’s make our mesh have 2 triangles to make a square and then texture them. So in myMesh.vertices I now have 6 vertices to define 2 triangles. I need to add texture coordinates so that it knows how to put the texture onto the mesh, so I add texCoords, this is six vec2s, one for each vertex which tell it what location in the texture is at that vertex. Texture coordinates are from 0 → 1 regardless of the size of the image itself. So my texCoord array puts (0,0) at the bottom left and (1,1) at the top right. Finally we assign an image to myMesh.texture.

Now we have something marginally more interesting to look at, it’s a square, with a texture.

I have added new parameters as well, these control some movement of the mesh. In draw previously we moved the camera a bit. Translate, Rotate and Scale leave the camera as is, but move the world, so the mesh is drawn in a different place. I have included 2 translations to give a sense of how these can interact with rotate.


-- 3d starter

-- Use this function to perform your initial setup
function setup()
    myMesh = mesh()
    myMesh.vertices = {vec3(-10,0,0), vec3(10,0,0), vec3(-10,20,0),
                    vec3(-10,20,0), vec3(10,0,0), vec3(10,20,0)}
    myMesh.texCoords = {vec2(0,0), vec2(1,0), vec2(0,1),
                    vec2(0,1), vec2(1,0), vec2(1,1)}
    myMesh.texture = readImage("Cargo Bot:Crate Yellow 2")
    myMesh:setColors(color(255))
    parameter.integer("cameraX",-40,40,0)
    parameter.integer("lookatX",-20,20,0)
    parameter.integer("translateBeforeRotation",-40,40,0)
    parameter.integer("rotateAngle", -360,360,0)
    parameter.integer("translateAfterRotation",-40,40,0)
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    camera(cameraX,0,-50,lookatX,0,0)
    perspective()
    translate(translateBeforeRotation,0,0)
    rotate(rotateAngle,0,1,0)
    translate(translateAfterRotation,0,0)
    myMesh:draw()
    
end

So what we have done so far is create a flat square, which we have drawn in 3d space.

Now making the object itself 3d is just a case of adding more vertices. Below it is extended to have a right face, and a top face… continuing like this gets you to a cube pretty quick.

That’s really all the basics you need to know, everything else is just meshes with more elements.


-- 3d starter

-- Use this function to perform your initial setup
function setup()
    myMesh = mesh()
    myMesh.vertices = {
            --front face
            vec3(-10,0,0), vec3(10,0,0), vec3(-10,20,0),
            vec3(-10,20,0), vec3(10,0,0), vec3(10,20,0),
            --right side face
            vec3(-10,0,0), vec3(-10,0,20), vec3(-10,20,0),
            vec3(-10,20,0), vec3(-10,0,20), vec3(-10,20,20),
            --top face
            vec3(-10,20,0), vec3(10,20,0), vec3(-10,20,20),
            vec3(-10,20,20), vec3(10,20,0), vec3(10,20,20)
            }
    myMesh.texCoords = {
            --front face
            vec2(0,0), vec2(1,0), vec2(0,1),
            vec2(0,1), vec2(1,0), vec2(1,1),
            --right side face
            vec2(0,0), vec2(1,0), vec2(0,1),
            vec2(0,1), vec2(1,0), vec2(1,1),
            --top face
            vec2(0,0), vec2(1,0), vec2(0,1),
            vec2(0,1), vec2(1,0), vec2(1,1)
            }
    myMesh.texture = readImage("Cargo Bot:Crate Yellow 2")
    myMesh:setColors(color(255))
    parameter.integer("cameraX",-40,40,0)
    parameter.integer("lookatX",-20,20,0)
    parameter.integer("translateBeforeRotation",-40,40,0)
    parameter.integer("rotateAngle", -360,360,0)
    parameter.integer("translateAfterRotation",-40,40,0)
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    camera(cameraX,0,-50,lookatX,0,0)
    perspective()
    translate(translateBeforeRotation,0,0)
    rotate(rotateAngle,0,1,0)
    translate(translateAfterRotation,0,0)
    myMesh:draw()
    
end

So now we have the basics of an object, let’s look at creating a scene. To create a scene you need to think about whether the scene is static or dynamic. If you want different objects with different textures, then it’s just constructing the scene out of multiple meshes, but to keep it simple we’ll just work with a simple square again.

I have refactored the previous code to generate the vertices and texture coordinates in a function, then I am using this to create 3 seperate squares in myMesh at different world coordinates. This is a good way to create a static scene. Notice I only care about the location of the squares at creation time when I call addRectAtLocation, when I am drawing I just draw a single mesh.


-- 3d starter

-- Use this function to perform your initial setup
function setup()
    myMesh = mesh()
    vertices = {}
    texCoords = {}

    --create 3 surfaces
    addRectAtLocation(vec3(0,0,0))
    addRectAtLocation(vec3(-20,0,20))
    addRectAtLocation(vec3(20,20,10))
    
    myMesh.vertices = vertices
    myMesh.texCoords = texCoords
    myMesh.texture = readImage("Cargo Bot:Crate Yellow 2")
    myMesh:setColors(color(255))
    parameter.integer("cameraX",-40,40,0)
    parameter.integer("lookatX",-20,20,0)
    parameter.integer("translateBeforeRotation",-40,40,0)
    parameter.integer("rotateAngle", -360,360,0)
    parameter.integer("translateAfterRotation",-40,40,0)
end

function addRectAtLocation (location)
    table.insert(vertices, vec3(-10,0,0)+location)
    table.insert(vertices, vec3(10,0,0)+location)
    table.insert(vertices, vec3(-10,20,0)+location)
    table.insert(vertices, vec3(-10,20,0)+location)
    table.insert(vertices, vec3(10,0,0)+location)
    table.insert(vertices, vec3(10,20,0)+location)
    table.insert(texCoords, vec2(0,0))
    table.insert(texCoords, vec2(1,0))
    table.insert(texCoords, vec2(0,1))
    table.insert(texCoords, vec2(0,1))
    table.insert(texCoords, vec2(1,0))
    table.insert(texCoords, vec2(1,1))
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    camera(cameraX,30,-100,lookatX,0,0)
    perspective()
    translate(translateBeforeRotation,0,0)
    rotate(rotateAngle,0,1,0)
    translate(translateAfterRotation,0,0)
    myMesh:draw()
    
end

For a dynamic scene we take a slightly different approach. I create myMesh with a single square in it. But then in the drawing loop I draw that mesh multiple times, once at each location I have defined (rectLocations array).

The key thing to notice here is that I keep my world translations as before, but for drawing each mesh I do pushMatrix(), translate to rectLocation, draw, popMatrix(). What the push and pop does is store the current state of the world with push, and then once I’ve done what I want for that single draw, pop restores the world back to what it was when I did the push.

We now have the exact same scene as the previous step, but with a different approach.

-- 3d starter

-- Use this function to perform your initial setup
function setup()
    myMesh = mesh()
    vertices = {}
    texCoords = {}

    addRectAtLocation(vec3(0,0,0))
    
    myMesh.vertices = vertices
    myMesh.texCoords = texCoords
    myMesh.texture = readImage("Cargo Bot:Crate Yellow 2")
    myMesh:setColors(color(255))
    
    rectLocations = {vec3(0,0,0), vec3(-20,0,20), vec3(20,20,10)}
    
    parameter.integer("cameraX",-40,40,0)
    parameter.integer("lookatX",-20,20,0)
    parameter.integer("translateBeforeRotation",-40,40,0)
    parameter.integer("rotateAngle", -360,360,0)
    parameter.integer("translateAfterRotation",-40,40,0)
end

function addRectAtLocation (location)
    table.insert(vertices, vec3(-10,0,0)+location)
    table.insert(vertices, vec3(10,0,0)+location)
    table.insert(vertices, vec3(-10,20,0)+location)
    table.insert(vertices, vec3(-10,20,0)+location)
    table.insert(vertices, vec3(10,0,0)+location)
    table.insert(vertices, vec3(10,20,0)+location)
    table.insert(texCoords, vec2(0,0))
    table.insert(texCoords, vec2(1,0))
    table.insert(texCoords, vec2(0,1))
    table.insert(texCoords, vec2(0,1))
    table.insert(texCoords, vec2(1,0))
    table.insert(texCoords, vec2(1,1))
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    camera(cameraX,30,-100,lookatX,0,0)
    perspective()
    translate(translateBeforeRotation,0,0)
    rotate(rotateAngle,0,1,0)
    translate(translateAfterRotation,0,0)
    for k,v in pairs(rectLocations) do
        pushMatrix()
        translate(-v.x,v.y,v.z)
        myMesh:draw()
        popMatrix()
    end
    
end

So why take the dynamic approach? Well now I can manipulate my objects independently. In this version I’ve added some wiggle and rotation to each square and they are doing this independently of each other and the world.

In reality you will probably have some things in your game that are static, like the ground or buildings, so building those meshes straight in world coordinates is good. Other things like animals or cars are better off with meshes built around the 0,0,0 and then placing them into the scene dynamically as they move around the world.

-- 3d starter

-- Use this function to perform your initial setup
function setup()
    myMesh = mesh()
    vertices = {}
    texCoords = {}

    addRectAtLocation(vec3(0,0,0))
    
    myMesh.vertices = vertices
    myMesh.texCoords = texCoords
    myMesh.texture = readImage("Cargo Bot:Crate Yellow 2")
    myMesh:setColors(color(255))
    
    rectLocations = {vec3(0,0,0), vec3(-20,0,20), vec3(20,20,10)}
    
    parameter.integer("cameraX",-40,40,0)
    parameter.integer("lookatX",-20,20,0)
    parameter.integer("translateBeforeRotation",-40,40,0)
    parameter.integer("rotateAngle", -360,360,0)
    parameter.integer("translateAfterRotation",-40,40,0)
    count = 0
    wiggle = 0
    wiggleDir = 1
end

function addRectAtLocation (location)
    table.insert(vertices, vec3(-10,0,0)+location)
    table.insert(vertices, vec3(10,0,0)+location)
    table.insert(vertices, vec3(-10,20,0)+location)
    table.insert(vertices, vec3(-10,20,0)+location)
    table.insert(vertices, vec3(10,0,0)+location)
    table.insert(vertices, vec3(10,20,0)+location)
    table.insert(texCoords, vec2(0,0))
    table.insert(texCoords, vec2(1,0))
    table.insert(texCoords, vec2(0,1))
    table.insert(texCoords, vec2(0,1))
    table.insert(texCoords, vec2(1,0))
    table.insert(texCoords, vec2(1,1))
end

-- This function gets called once every frame
function draw()
    count = count + 1
    wiggle = wiggle + wiggleDir
    if wiggle > 15 or wiggle < -15 then 
        wiggleDir = -wiggleDir
    end
    -- This sets a dark background color 
    background(40, 40, 50)

    camera(cameraX,30,-100,lookatX,0,0)
    perspective()
    translate(translateBeforeRotation,0,0)
    rotate(rotateAngle,0,1,0)
    translate(translateAfterRotation,0,0)
    for k,v in pairs(rectLocations) do
        pushMatrix()
        translate(-v.x,v.y,v.z)
        translate(wiggle/k,0,0)
        rotate(count*k,0,1,0)
        myMesh:draw()
        popMatrix()
    end
    
end

Thanks @spacemonkey !

@spacemonkey - while you’re writing about meshes, can you tell me this? If I create a mesh with 2 triangles from an image, it comes up pretty dark on my screen. If I include the line fill(255) in draw, it has the original brightness. I’m not sure why it was a problem to start with, or why this fix works (NB I thought setColours might help, but it doesn’t).

@Ignatz - sounds a bit weird, the standard shaders mix the mesh color and texture so setColors is necessary unless you are doing your own shader that ditches that element of it, so if you setColors(color(255,0,0,255) it would tint green etc. Does your image have a low alpha (transparency) perhaps? If you gave me the example image and code I could check it out.

This code shows the behaviour, the parameter toggles it on/off in draw

function setup()
    i=readImage("Small World:Icon")
    local x,y,z,a=0,0,500,0
    CreateMesh(x,y,z,a,i)
    parameter.boolean("Bright",false)
end

function CreateMesh(x,y,z,a,img)
    local w,h=img.width,img.height
    m=mesh()
    m.texture=img
    local v,t={},{}
    local xx,yy,zz=-w/2,-h/2,0
    v[#v+1]=vec3(xx,yy,zz)  t[#t+1]=vec2(0,0)
    v[#v+1]=vec3(xx+w,yy,zz)  t[#t+1]=vec2(1,0)
    v[#v+1]=vec3(xx+w,yy+h,zz)  t[#t+1]=vec2(1,1)
    v[#v+1]=vec3(xx+w,yy+h,zz)  t[#t+1]=vec2(1,1)
    v[#v+1]=vec3(xx,yy+h,zz)  t[#t+1]=vec2(0,1)
    v[#v+1]=vec3(xx,yy,zz)  t[#t+1]=vec2(0,0)
    m.vertices=v
    m.texCoords=t 
    m.pos=vec3(x,y,z)
    m.a=a
end

function draw()
    background(220)
    perspective(45,WIDTH/HEIGHT)
    pushStyle()
    if Bright==true then fill(255) end
    view=500
    camera(0,20,10,0,20,-400,0,1,0)
    pushMatrix()
    translate(m.pos.x,m.pos.y,-m.pos.z)
    m.a=m.a+5/60
    rotate(-m.a,0,1,0)
    m:draw()
    popStyle()
    popMatrix() 
end

The problem is the colors of the mesh. The standard shader mixes the color and texture. Then it layers onto your world, since your background is color(220) it darkens it in the mix.

If you tweak the setup code as below it’s full brightness on any background:

function setup()
    i=readImage("Small World:Icon")
    local x,y,z,a=0,0,500,0
    CreateMesh(x,y,z,a,i)
    m:setColors(color(255))
    parameter.boolean("Bright",false)
end

The odd thing is why fill works, and where the mesh got it’s color from by default… but I think it’s probably safest to explicitly color your mesh. I guess if it has no color it is using whatever the value of fill currently is, which I did not know.

Also note, you must create all your vertices before calling setColors because this sets the color of all current vertices in the mesh.

ah, I was doing setColors, but not after creating the vertices. There, I think, is my problem

Much appreciated! =D>

Added in a couple more sections to cover wider scene creation.