# 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

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

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 = {}

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

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 = {}

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

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