Skip to content

Instantly share code, notes, and snippets.

@dermotbalson
Created October 5, 2015 02:29
Show Gist options
  • Save dermotbalson/5c0f7aa3d4c6187dfd69 to your computer and use it in GitHub Desktop.
Save dermotbalson/5c0f7aa3d4c6187dfd69 to your computer and use it in GitHub Desktop.
SBS 3D v2
--# Notes
--[[
3D is a very, very big subject, with a lot to learn, so this set of demos can only scratch the surface, and cannot possibly explain everything there is to know.
You can simply watch the demos to see what Codea can do, but ideally,you should read up on 3D before trying to understand how the demos work, and before trying your own projects.
Also, this is not a set of "Wow, look what 3D can do" demos. If you look at them all without reading the notes,
you may not be very excited. But if you want to write your own 3D programs, this is what you need to know. (If
instead we had provided Wow demos, they would have been too hard for you to understand right now, and you might
give up).
When you draw in 2D, it is like drawing on a window or on a piece of paper. You can view the picture from any angle and it looks the same.
When you draw in 3D, it is like looking THROUGH a window to a scene outside. If you move around, what you see will change, further objects will look smaller and may be hidden by closer objects, etc. Also, if you turn your head, or bend down, what you see will change. So your position, and the direction you are looking in, are important.
So there are several very important differences between 3D and 2D
1. perspective - you now have depth, a z value, and further objects will be smaller than closer objects
2. camera - the position of the camera in your scene, and where it is pointed, affect what is drawn
3. coordinates - in 2D, the bottom left corner is always 0,0, and x and y increase as you move right and up. In 3D,
there is no such thing as a left corner, just a big empty 3D space. You have to define screen positions with
(x,y,z) values, which can be anything you like.
4. axis orientation - which we'll explain in the first demo
IMPORTANT NOTE
Various useful functions are added as you work through the demos - functions for making blocks, spheres, etc.
The first time they are used, their code is included with that demo and explained. But you will appreciate that
if we included ALL this code with every demo, the code is going to get very messy.
So once we have explained one of these functions the first time, we won't include its code in any later demos that might use it. Instead, we've put all these functions in a special Utility tab toward the right, so they can be used by all the demos, and we can keep the demo code clean and neat.
This means that if you copy any code tabs out of here to play with in your own project, you may also need the Utility tab code as well.
And of course, having all this code in one tab makes it easy for you to copy it into your own 3D projects.
--]]
--# Intro
--Intro
--This is just the starting message, ignore this tab
function setup()
end
function draw()
background(0)
fill(255)
fontSize(18)
textWrapWidth(500)
txt=[[
These demos are for new 3D programmers
They are not WOW!! LOOK! WHAT! 3D! CAN! DO!! demos
(Codea does include some WOW demos, but you will probably find they are too difficult for you to understand. That's why these demos are different).
They are kept simple, to teach you what you need to know to start programming in 3D.
If you simply look at all the demos without reading the code and the notes, you will learn nothing. You need to work through the code for each demo, from left to right, and make sure you understand it before moving on.
]]
text(txt,WIDTH/2,HEIGHT/2)
end
function PrintExplanation()
output.clear()
print("After reading this, please go to the Notes tab and read it")
print("Then choose the next demo using the parameter slider above, and look at the code and notes that go with it.")
end
--# Axes
--Understanding the directions of the axes
--[[
OpenGL (the graphic system used by Apple) starts off with the three axes like this
x axis is positive on the left of (0,0,0) and negative on the right --NOTICE THIS!!!!
y axis is negative below (0,0,0) and positive above it
z axis is negative in front of (0,0,0) and positive beyond it (ie "going into the screen")
There is a problem with x. We want it to run from negative on the left of (0,0,0) to positive on the right,
otherwise we will get very confused.
We can achieve this by turning round 180 degrees (on the y axis), which reverses the x axis, keeps the y axis
the same (so down is still negative, and up is positive), and reverses the z axis, so if we are standing at
(0,0,0) looking forward, z runs from positive behind us to negative in front of us.
So we can "fix" the x axis, if we are prepared to reverse the z axis. And because we don't use a z axis much in
our lives, we can fairly easily get used to having z go more negative as we go forwards.
The demo below lets you move a picture around so you can get used to the directions in 3D.
--]]
function setup()
parameter.integer("X",-100,100,0)
parameter.integer("Y",-100,100,0)
parameter.integer("Z",-100,100,0)
end
function draw()
background(0)
perspective() --turn on 3D, we will explain this in the next demo
camera(0,0,100,0,0,0) --we will explain this later
translate(X,Y,Z)
sprite("Planet Cute:Character Pink Girl",0,0,25)
end
function PrintExplanation()
output.clear()
print("We draw a picture in front of you at (0,0,0)")
print("Use the X,Y,Z settings to move it - notice which way they move, especially Z")
print("It's very important you learn these directions to avoid being confused")
end
--# Block
--Basics of 3D
--[[
You turn on 3D drawing with the perspective() command
After that, all positions need to have a z value
And we are looking directly toward -z, with -x on our left, +x on our right, -y below us and +y above us
In 3D, Codea needs to know
1. where the camera is positioned
2. the point at which the camera is looking
and these are provided as two sets of three numbers like this, using the camera function
camera(0,0,100, 0,0,10) --camera is at (0,0,100), looking at (0,0,10)
[For those of you who understand vectors and directions, Codea calculates the direction of the camera as the
normalised value of the "look" position minus the camera position. So if the camera is at (10,20,30) and you want
to look straight left, you can just add (-1,0,0) to the camera position to get a "look" position of (9,20,30)
giving you camera settings of (10,20,30, 9,20,30) ]. NB If you don't understand this, don't worry..
This demo shows a 3D block, which you can spin with your finger
--]]
function setup()
m=MakeBlock(20,30,10,color(255),readImage("Platformer Art:Block Brick"):copy(3,3,64,64))
SetupTouches() --used to handle touches, it has nothing to do with 3D
pos=vec3(0,0,0) --starting position of block
--make block go further away and then come back, see how the size changes
tween(10, pos, {z=-300}, { easing = tween.easing.linear, loop = tween.loop.pingpong } )
end
function draw()
background(220)
perspective() --this turns 3D on, so now our screen has depth (z)
camera(-0,0,100, 0,0,0) --camera is at (0,0,100), looking at (0,0,0)
pushMatrix()
HandleTouches(pos) --handles your touches, rotates anything drawn after this
m:draw()
popMatrix()
--draw position on the screen, first convert back to 2D
ortho()
viewMatrix(matrix())
fill(0)
fontSize(18)
text("Position is ("..math.floor(pos.x)..", "..math.floor(pos.y)..", "..math.floor(pos.z)..")",WIDTH/2,100)
end
--You needn't worry about understanding everything below, but you should at least know how meshes work
function MakeBlock(w,h,d,c,tex) --width,height,depth, colour,texture
local m=mesh()
--define the 8 corners of the block ,centred on (0,0,0)
local fbl=vec3(-w/2,-h/2,d/2) --front bottom left
local fbr=vec3(w/2,-h/2,d/2) --front bottom right
local ftr=vec3(w/2,h/2,d/2) --front top right
local ftl=vec3(-w/2,h/2,d/2) --front top left
local bbl=vec3(-w/2,-h/2,-d/2) --back bottom left (as viewed from the front)
local bbr=vec3(w/2,-h/2,-d/2) --back bottom right
local btr=vec3(w/2,h/2,-d/2) --back top right
local btl=vec3(-w/2,h/2,-d/2) --back top left
--now create the 6 faces of the block, each is two triangles with 3 vertices (arranged anticlockwise)
--so that is 36 vertices
--for each face, I'm going to start at bottom left, then bottom right, then top right, and for the second
--triangle, top right, top left, then bottom left
m.vertices={
fbl,fbr,ftr, ftr,ftl,fbl, --front face
bbl,fbl,ftl, ftl,btl,bbl, --left face
fbr,bbr,btr, btr,ftr,fbr, --right face
ftl,ftr,btr, btr,btl,ftl, --top face
bbl,bbr,fbr, fbr,fbl,bbl, --bottom face
bbr,bbl,btl, btl,btr,bbr --back face
}
if tex then
--add texture positions, we will use the same image for each face so we only need 4 corner positions
local bl,br,tr,tl=vec2(0,0),vec2(1,0),vec2(1,1),vec2(0,1)
local t={}
for i=1,6 do --use a loop to add texture positions for each face, as they are the same for each face
t[#t+1],t[#t+2],t[#t+3],t[#t+4],t[#t+5],t[#t+6]=bl,br,tr,tr,tl,bl
end
m.texCoords=t
m.texture=tex
m:setColors(color(255))
else m:setColors(c)
end
return m
end
--This function allows you to rotate objects with your fingers
--It needs the function SetupTouches() to be run in setup, and HandleTouches() needs to be run in draw
--it affects anything that is drawn after it is run
--you can pass through the current position of the object as p=vec3
function HandleTouches(p)
--do rotation for touch
if CurrentTouch.state == MOVING then --only rotate while fingers are moving on the screen
currentModelMatrix=currentModelMatrix:rotate(CurrentTouch.deltaX,0,1,0)
currentModelMatrix=currentModelMatrix:rotate(CurrentTouch.deltaY,1,0,0)
end
if p then currentModelMatrix[13],currentModelMatrix[14],currentModelMatrix[15]=p.x,p.y,p.z end
modelMatrix(currentModelMatrix) --apply the stored settings
end
function SetupTouches()
currentModelMatrix = modelMatrix()
end
function PrintExplanation()
output.clear()
print("We draw a 3D block which moves forward and backwards")
print("Rotate it with your finger")
end
--# Perspective
--Understanding perspective and ortho - turning 3D on and off
--[[
TURNING 3D ON
The perspective() command turns on 3D
It has some optional parameters that you can vary. Normally you don't to change any of them.
The full command is perspective(fov,aspect,near,far), where
* "fov" is the number of degrees covered by the width of the screen. 45 is the default, but you can vary it,
and it acts like a zoom lens, as you will see in this demo. If fov is small, eg 5, you will see less of the
scene, but it will be enlarged.
* "aspect" is the ratio of screen width to height, defaulting to WIDTH/HEIGHT. Don't change this
* "near" = the closest object that will be drawn, defaults to 0.1 pixel away from the camera, don't change this
* "far" = the furthest object that will be drawn on the screen, default 2000, this demo lets you vary it
So "fov" is great for zooming in and out of a scene, and "far" is great if (say) you have fog or mist with a
radius of (say) 150 pixels. You can set "far" to 150, then Codea will not bother to draw anything further than 150
pixels from the camera, and this can speed things up.
You don't need to remember how to do these things right now, because you won't use them often, but just remember
that they are there if you need them.
TURNING 3D OFF
You don't need to turn 3D off at the end of draw, because Codea will do this for you.
However, if you want to draw some text on the screen, eg scores or health, you will find it almost impossible to draw it correctly while in 3D. You could draw the text at the beginning of draw, BEFORE you change to 3D with the perspective command. However, then your writing may be hidden by objects on the screen.
If you want to write on the screen AFTER drawing everything in 3D, you need to turn off 3D first, then you can draw text in 2D with the usual x,y positions.
You turn off 3D with TWO commands (you need both)
ortho() --gets rid of perspective, ie depth
viewMatrix(matrix()) --resets the screen settings
This is demonstrated below.
--]]
--setup is exactly the same as the previous demo except for the parameter options
function setup()
m=MakeBlock(20,30,10,color(255),readImage("Platformer Art:Block Brick"):copy(3,3,64,64))
SetupTouches() --used to handle touches, it has nothing to do with 3D
pos=vec3(0,0,0) --starting position of block
--make block go further away and then come back, see how the size changes
tween(10, pos, {z=-300}, { easing = tween.easing.linear, loop = tween.loop.pingpong } )
parameter.integer("FOV",5,75,45) --NEW
parameter.integer("Far",200,600,300) --NEW
end
--draw is the same as before, except that perspective uses your choices, and we turn off 3D to draw some text
function draw()
background(220)
perspective(FOV,WIDTH/HEIGHT,0.1,Far)
camera(-0,0,100, 0,0,0)
pushMatrix()
HandleTouches(pos)
m:draw()
popMatrix()
--go back to 2D to draw a message on the screen
ortho() --you need both these commands to turn off 3D
viewMatrix(matrix())
fill(107, 77, 50, 255)
fontSize(18)
text("The block is "..math.floor(100-pos.z).." pixels away",WIDTH/2,HEIGHT/2)
end
function PrintExplanation()
output.clear()
print("Use the FOV option to zoom in and out")
print("Use the Far option to stop drawing beyond a set distance")
print("We turn off 3D after drawing the scene, so we can write on the screen")
print("You can rotate the block with your finger")
end
--# Billboard
--Using 2D images in 3D
--[[
Some objects are very difficult to make in 3D, eg trees.
However, if you draw a tree in 2D, and if you keep it turning it to always face the camera, it can appear realistic. This works best for complex objects like trees where the viewer can't remember the details and therefore doesn't notice that the tree never changes (unless it is a strange shape of course). It doesn't work so well for objects like people or cars, where you will quickly notice if they seem to be in a fixed position.
Using a 2D image in 3D is called billboarding (named after the advertising billboards on highways), and this is how you do it. There are some important tricks you need to know, especially
* how to rotate a billboard to face the camera
* dealing with transparent pixels
* dealing with overlapping pixels
1. Rotating an image to face the camera
The LookAt function at the bottom will do this for you. Given the image position, and the position it needs to face, it translates and rotates so that all you need to do is draw the image. It may look horribly complicated, but it is explained here (https://coolcodea.wordpress.com/2015/02/12/198-looking-at-objects-in-3d/)
2. Dealing with transparent pixels
This is a real gotcha for 3D beginners using billboards. Almost all 2D images have transparent pixels around the picture in the middle. The problem is that OpenGL (the graphics package used by Apple) doesn't recognise transparent pixels, and treats them as filled.
Suppose you draw a billboard, and then you draw a block behind it. When OpenGL draws the block, it checks whether any of it is hidden by the billboard in front, and if it is, it doesn't draw that part of the block. That is what we want it to do, ie only draw what we can see. The problem is that it doesn't realise that we can see through transparent pixels, and won't draw anything behind them!
You'll see that when you run this demo. The answer is to draw billboards (and any other object with transparent pixels) from furthest to nearest, so that you never ask OpenGL to draw anything behind existing transparent pixels. This means you need to SORT your billboards by distance from the camera, each time you draw! The easiest way to do this is to put them in a table, and sort that.
3. Dealing with overlapping pixels
You may want to overlap images, eg put a poster on a wall. But if you draw the image in the same place as the wall, OpenGL gets confused because it is being asked to draw two different pixels in the same place, and it will flicker. The answer is to put the poster very slightly (eg 0.1 pixels) in front of the wall.
You will see all of these problems in this demo.
--]]
function setup()
--create a block, it will just let us see the rotation happening
block=MakeBlock(20,30,10,color(255),readImage("Platformer Art:Block Brick"):copy(3,3,64,64))
block.pos=vec3(0,0,-40)
--add a label to the side of the box, this will not rotate to face the camera because it's stuck to the box
label=mesh()
local img=readImage("Cargo Bot:Clear Button")
label:addRect(0,0,10,img.height*10/img.width) --scale image to smaller size
label.texture=img
label.pos=vec3(0,5,5) --this position assumes we have translated to the block position, ie it is relative
CreateBillboards()
parameter.boolean("ManageOverlappingPixels")
parameter.boolean("ManageTransparency",false)
parameter.boolean("RotateTowardCamera",false)
--camera position
camPos=vec3(-300,0,50)
--make camera position swing from left to right
tween(10, camPos, {x=300}, { easing = tween.easing.linear, loop = tween.loop.pingpong } )
end
function CreateBillboards()
--add a couple of 2D images we'll use as billboards, put them in their own meshes
p1=mesh()
local img=readImage("Planet Cute:Character Princess Girl")
p1:addRect(0,0,20,img.height*20/img.width) --scale image to smaller size
p1.texture=img
p1.pos=vec3(0,0,0)
p2=mesh()
local img=readImage("Planet Cute:Character Pink Girl")
p2:addRect(0,0,20,img.height*20/img.width) --scale image to smaller size
p2.texture=img
p2.pos=vec3(-10,0,-20)
--put images in a table so we can sort them by distance
billboards={p1,p2}
end
function draw()
background(220)
perspective()
camera(camPos.x,camPos.y,camPos.z,0,0,0)
--draw the block first
pushMatrix()
translate(block.pos:unpack())
block:draw()
translate(label.pos:unpack())
--to avoid flicker, always separate your objects, even by a very small amount
if ManageOverlappingPixels then translate(0,0,0.1) end
label:draw()
popMatrix()
--draw the billboards
--first sort by distance from camera, if requested
if ManageTransparency then
table.sort(billboards,function(a,b) return a.pos:dist(camPos)>b.pos:dist(camPos) end)
end
for i=1,#billboards do
pushMatrix()
if RotateTowardCamera then
LookAt(billboards[i].pos,camPos)
else
translate(billboards[i].pos:unpack())
end
billboards[i]:draw()
popMatrix()
end
end
function LookAt(source,target,up)
local Z=(source-target):normalize()
up=up or vec3(0,1,0)
local X=(up:cross(Z)):normalize()
local Y=(Z:cross(X)):normalize()
modelMatrix(matrix(X.x,X.y,X.z,0,Y.x,Y.y,Y.z,0,Z.x,Z.y,Z.z,0,source.x,source.y,source.z,1))
end
function PrintExplanation()
output.clear()
print("Can you see the three problems?\n1. Flickering\n2. The front image blocks out everything behind it\n3. The images of people don't rotate to face the camera, so they look flat")
print("Turn on each of the options one at a time to see the effect")
end
--# TableTop
--Tabletop 3D, or 2.5D
--[[
A tabletop 3D scene, where you move around on a flat surface (as in FPS games) keeps things fairly simple,
because
1. you only turn left and right (ie only on the y axis), avoiding all the difficulties of 3D rotation
2. it is easier to draw objects sitting (or moving) on a flat surface
FLOOR
We start with a floor made up of two huge triangles. We'll just give that a colour because we don't have an image that size, and if we tiled (ie repeated) a smaller image many times, we'd need a lot of triangles. (Later, you may find out how to tile an image across a surface repeatedly, using a shader).
LEVEL DESIGN
We want to make the level design simple to create, modify, and use to build our scene. A tilemap does this well.
We'll start with by drawing a map broken into tiles 10 pixels square. Our map will have a letter for each tile
that tells us what to put in that tile. This makes it easy to create and modify different levels for a game.
WALLS
The map will be used to build a mesh for the blocks which make up the walls. We'll hard code all the block positions because they won't move. This is very simple to build, but if you draw walls with blocks, you are including a lot of cube faces that will never be seen. If your scene gets bigger, you might want to "cull" (remove) all the faces that cannot be seen, to improve performance.
REWARDS
We could add the reward boxes to the same mesh because they won't move either, but the problem is that when we "open" them, they need to disappear. This is a bit tricky when they consist of 36 vertices somewhere in a big mesh. It's easier to just create a mesh with just one reward box, plus a table with a list of all the positions of reward boxes, then we can use that to draw them, and it's easy to remove them from the table. (If we add more types of reward later, our table can include the type as well as position).
MOVING AROUND
We've included a simple joystick class that lets you move left and right across a tabletop.
--]]
function setup()
SetupScene()
joy=JoyStick()
speed=0
angle=0 --in radians
end
function SetupScene()
--create the scene, using a text map below to tell us where to put everything
--this makes it easy to change things, and to create new levels
tileSize=10 --10 pixels per square on the map below
--we put a letter (or blank) in each place, the letters are:
--x = where we start
--b = a block
--a = a box containing rewards
map={
" ",
" bbb bbbbbbbbb ",
" bbb ",
" ",
" a bbbbbbbb ",
" ba b ",
" b b ",
" bbbb b ",
" b b ",
" b b ",
" b ",
" b b ",
" ba bbbb ",
" bbbb ",
" x "
}
--create the scene, using the map
--we'll put each type of object into its own mesh
--it might be better to put all the fixed (non moving) objects into one mesh, but then we'd need an image
--tilemap to hold all the images used in the mesh, and texture coordinates
--so let's keep things really simple for now
blocks=mesh() --wall blocks go in here
blocksV,blocksT={},{} --vertex and texture coords
blocks.texture=readImage("Platformer Art:Block Brick"):copy(3,3,64,64)
--first we need a floor, which will go into the scene mesh
--we calculate the size and make one huge coloured rectangle to cover it
--(later we'll show you how to tile an image texture across it)
--Where is this floor going to be, in 3D space?
--We'll make the bottom left (0,0,0), but you can make it anything you like
widthTiles,depthTiles = map[1]:len(), #map --width and depth of the whole floor in tiles
local wp,dp = widthTiles*tileSize, -depthTiles*tileSize --width and depth in pixels
--add the vertices and colours, the floor is flat, so y=0
local v={vec3(0,0,0),vec3(wp,0,0),vec3(wp,0,dp),vec3(0,0,dp)} --corners of map
--create two huge triangles to cover the floor
floor=mesh()
floor.vertices={v[1],v[2],v[3],v[3],v[4],v[1]}
floor:setColors(color(145, 87, 62, 255))--brown colour
--we're going to need some cubes for walls and rewards, so let's make one to start with
--the MakeBlock function gives us back a mesh, but also a table of vertices, texture positions and colours
--and it's those that we will copy
local m,v,t,c=MakeBlock(tileSize,tileSize,tileSize,nil,blocks.texture)
--use this to make a single reward box mesh
box=mesh()
local bv,bt={},{}
for i=1,36 do
bv[i]=v[i]/4+vec3(x,tileSize/8,z) --make box 1/4 the size of a tile
bt[i]=t[i]
end
box.vertices=bv
box.texCoords=bt
box:setColors(color(255))
box.texture=readImage("Cargo Bot:Crate Yellow 1"):copy(2,2,40,40)
boxList={} --keep a list of where reward boxes are stored
--now we'll read what is in the map
for i=1,#map do --z axis, furthest to nearest
local z=-tileSize*(depthTiles-i+0.5) --z position of centre of tile, note it is negative because
--if the bottom of the map is at (0,0,0), then everything above it must be in front of it, in negative z
for j=1,map[i]:len() do --x axis, left to right
local x=tileSize*(j-0.5) --x position of centre of tile
local c=map[i]:sub(j,j) --get the letter at this position
if c=="x" then pos=vec3(x,tileSize/2,z)
elseif c=="b" then --block, add a copy of the block vertices and texture coords to our wall mesh
for i=1,36 do
blocksV[#blocksV+1]=v[i]+vec3(x,tileSize/2,z)
blocksT[#blocksT+1]=t[i]
end
elseif c=="a" then --reward, store the location
boxList[#boxList+1]=vec3(x,0,z)
end
end
end
blocks.vertices=blocksV
blocks.texCoords=blocksT
blocks:setColors(color(255))
end
function draw()
background(149, 165, 188, 255)
perspective()
--adjust position and angle
--x movements of joystick affect angle, y movements affect speed
local v=joy:update() --gives us the x,y movement as numbers in the range -1 to +1
angle=angle+v.x/50 --divided by a large number because angle is in radians
speed=v.y/8
--calculate direction vector based on the angle, plus any panning
--this tells us how much the x and z position change for each 1 radian
local direction=vec3(math.sin(angle),0,-math.cos(angle))
--set the new position of the player, calculate change in position
local posChange=direction*speed
--check we haven't walked into a wall
if CanMove(pos+posChange) then
pos=pos+posChange
else --if we have, reset joystick to centre, and bounce back a couple of pixels, the way we came
joy:reset()
pos=pos-2*direction
end
--the camera is looking in the same direction as the player, so we add direction to pos
local look=pos+direction
camera(pos.x,pos.y,pos.z,look.x,look.y,look.z)
floor:draw()
--draw the list of reward boxes
for i=1,#boxList do
pushMatrix()
translate(boxList[i]:unpack())
box:draw()
popMatrix()
end
blocks:draw() --the walls
joy:draw() --joystick
end
function touched(t)
joy:touched(t) --update the joystick class with any touches
end
--this function stops us walking through walls
function CanMove(p)
--calculate which tile we are in
local tx,tz= math.ceil(p.x/tileSize),math.ceil(depthTiles+p.z/tileSize)
--look up what is in the tie, if it is "b", we can't move
if map[tz]:sub(tx,tx)=="b" then return false else return true end
end
function TileToPixel(v)
return vec3(v.x-0.5,0,v.y-0.5)*tileSize
end
function PrintExplanation()
output.clear()
print("Creating a tabletop game")
print("Use the joystick at lower left to move around, swipe on the screen to look left and right")
print("Find the boxes of treasure")
end
--# Sphere
-- Spheres and sky
--[[
Spheres can be used for many things in 3D apps
The first thing you need is code to make a sphere, which is pretty tricky. There are types of sphere
1. UV - the type you see with world globes, where all the lines get close together at the top and bottom. These
spheres have vertices which are further part in the middle that at top and bottom.
This type of sphere is the one for which most image maps are made. Look for pictures with width that is twice
the height.
2. icosphere - the vertices are equally distant from each other. This is better if you want to distort the sphere
(eg if you want to make lumpy asteroids) but it is difficult to set texture coordinates.
For this demo, we will make a UV sphere (UV are labels U and V for the longitude/latitude axes), and
code for doing this is provided in the Utility tab
We will show the earth and moon in space, and you can spin the earth with your finger, to see how the image
wraps around it. To start with, they will just have a red starry texture, but if you download the images from
the link below, you can get very realistic retults.
--]]
function setup()
--to see some nice pictures instead of the red stars
--download Earth.Day and Moon images from https://moon-20.googlecode.com/svn/win32/Textures/
--copy them to your dropbox and sync them
earthImg=readImage("Dropbox:EarthDay") or GetPlaceholderImage() --use a placeholder if we don't have earth img
moonImg=readImage("Dropbox:Moon") or GetPlaceholderImage()
earth=CreateSphere(200,earthImg)
moon=CreateSphere(50,moonImg)
angle=0
end
--if we don't have real earth and moon images, just make an image to wrap around the spheres
function GetPlaceholderImage()
local img=readImage("Cargo Bot:Starry Background")
local s=img.width
--make it twice as wide as high
local img2=image(s*2,s)
setContext(img2)
sprite(img,s/2,s/2)
sprite(img,s*3/2,s/2)
setContext()
return img2
end
function draw()
background(0)
perspective()
camera(0,0,1000,0,0,0)
pushMatrix()
translate(-500,500,-750)
moon:draw()
popMatrix()
rotate(angle,0,1,0) --rotate on y axis
angle=angle+0.2
earth:draw()
end
--# modelMatrix
--modelMatrix
--[[
How does Codea actually translate and rotate objects?
It uses 4x4 matrices, which look like this
x x x 0 --these three figures rotate the x value
y y y 0 --and these rotate the y value
z z z 0 --and these rotate the z value
t t t 1 --the three "t" figures are the x,y,z translation positions
The top left 3x3 part of the matrix handles rotations. If you rotate on any of the three axes, formulae using sin
and code are used to calculate these values.
The bottom row contains the translation values.
The starting matrix looks like this
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
So if you do this
translate(10,20,30)
rotate (90,0,1,0) --rotate left 90 degrees on y
the matrix looks like this - you can see it with print(modelMatrix())
0, 0, -1, 0
0, 1, 0, 0
1, 0, 0, 0
10, 20, 30, 1
If you were standing with your arms outstretched, facing -z, and turned left, your left arm (which is -x) is now
pointing toward +z, so the effect of rotation is that your x values become -z.
When you multiply this matrix by a vertex (x,y,z), you get (10+z, 20+y, 30-x)
[Note that before multiplying, Codea makes the vertex a vec4 by adding a 1, ie (x,y,z,1), which has the effect of adding the translation values at the bottom of the matrix].
The matrix that handles translations and rotations is called the model matrix.
There are two more matrices. The view matrix translates and rotates the whole scene so it is in front of the camera, and the projection matrix adds perspective (makes further away objects smaller). All three matrices are used to draw in 3D.
It may take you some time to understand how this works, but keep trying, it's worth it.
--]]
function setup()
m=MakeBlock(20,30,10,color(255),readImage("Platformer Art:Block Brick"):copy(3,3,64,64))
--SetupTouches()
pos=vec3(0,0,0)
tween(10, pos, {z=-300}, { easing = tween.easing.linear, loop = tween.loop.pingpong } )
counter=0
parameter.integer("X_Rotation",-90,90,0)
parameter.integer("Y_Rotation",-90,90,0)
parameter.integer("Z_Rotation",-90,90,0)
parameter.action("Reset_All",function() X_Rotation,Y_Rotation,Z_Rotation=0,0,0 end)
end
function draw()
background(220)
perspective()
camera(-0,0,100, 0,0,0)
pushMatrix()
translate(pos:unpack())
rotate(X_Rotation,1,0,0)
rotate(Y_Rotation,0,1,0)
rotate(Z_Rotation,0,0,1)
--HandleTouches(pos)
m:draw()
txt=tostring(modelMatrix())
popMatrix()
--print the modelMatrix values
ortho()
viewMatrix(matrix())
fill(0)
text(txt,WIDTH/2,150)
end
function PrintExplanation()
output.clear()
print("Read the notes in the code before you view this")
print("Use the parameters to rotate the block")
end
--# Utility
--Utility
--RECTANGULAR BLOCK ************************************
--You needn't worry about understanding everything below, but you should at least know how meshes work
function MakeBlock(w,h,d,c,tex) --width,height,depth, colour,texture
local m=mesh()
--define the 8 corners of the block ,centred on (0,0,0)
local fbl=vec3(-w/2,-h/2,d/2) --front bottom left
local fbr=vec3(w/2,-h/2,d/2) --front bottom right
local ftr=vec3(w/2,h/2,d/2) --front top right
local ftl=vec3(-w/2,h/2,d/2) --front top left
local bbl=vec3(-w/2,-h/2,-d/2) --back bottom left (as viewed from the front)
local bbr=vec3(w/2,-h/2,-d/2) --back bottom right
local btr=vec3(w/2,h/2,-d/2) --back top right
local btl=vec3(-w/2,h/2,-d/2) --back top left
--now create the 6 faces of the block, each is two triangles with 3 vertices (arranged anticlockwise)
--so that is 36 vertices
--for each face, I'm going to start at bottom left, then bottom right, then top right, and for the second
--triangle, top right, top left, then bottom left
local v={
fbl,fbr,ftr, ftr,ftl,fbl, --front face
bbl,fbl,ftl, ftl,btl,bbl, --left face
fbr,bbr,btr, btr,ftr,fbr, --right face
ftl,ftr,btr, btr,btl,ftl, --top face
bbl,bbr,fbr, fbr,fbl,bbl, --bottom face
bbr,bbl,btl, btl,btr,bbr --back face
}
m.vertices=v
local t={}
if tex then
--add texture positions, we will use the same image for each face so we only need 4 corner positions
local bl,br,tr,tl=vec2(0,0),vec2(1,0),vec2(1,1),vec2(0,1)
for i=1,6 do --use a loop to add texture positions for each face, as they are the same for each face
t[#t+1],t[#t+2],t[#t+3],t[#t+4],t[#t+5],t[#t+6]=bl,br,tr,tr,tl,bl
end
m.texCoords=t
m.texture=tex
m:setColors(color(255))
else m:setColors(c)
end
return m,v,t,c
end
function AddBlock(m,w,h,d,c,p) --mesh,width,height,depth, colour, texture positions (x,y,w,h)
--define the 8 corners of the block ,centred on (0,0,0)
local fbl=vec3(-w/2,-h/2,d/2) --front bottom left
local fbr=vec3(w/2,-h/2,d/2) --front bottom right
local ftr=vec3(w/2,h/2,d/2) --front top right
local ftl=vec3(-w/2,h/2,d/2) --front top left
local bbl=vec3(-w/2,-h/2,-d/2) --back bottom left (as viewed from the front)
local bbr=vec3(w/2,-h/2,-d/2) --back bottom right
local btr=vec3(w/2,h/2,-d/2) --back top right
local btl=vec3(-w/2,h/2,-d/2) --back top left
--now create the 6 faces of the block, each is two triangles with 3 vertices (arranged anticlockwise)
--so that is 36 vertices
--for each face, I'm going to start at bottom left, then bottom right, then top right, and for the second
--triangle, top right, top left, then bottom left
local v={
fbl,fbr,ftr, ftr,ftl,fbl, --front face
bbl,fbl,ftl, ftl,btl,bbl, --left face
fbr,bbr,btr, btr,ftr,fbr, --right face
ftl,ftr,btr, btr,btl,ftl, --top face
bbl,bbr,fbr, fbr,fbl,bbl, --bottom face
bbr,bbl,btl, btl,btr,bbr --back face
}
local s=m.size
m:resize(s+#v)
local vert=m:buffer("position") --get existing vertex positions
for i=1,#v do vert[s+i]=v[i] end --add the new ones
--m.vertices=vert
if p then
--add texture positions, we will use the same image for each face so we only need 4 corner positions
local bl,br,tr,tl=vec2(p[1],p[2]),vec2(p[1]+p[3],p[2]),vec2(p[1]+p[3],p[2]+p[4]),vec2(p[1],p[2]+p[4])
local t={}
for i=1,6 do --use a loop to add texture positions for each face, as they are the same for each face
t[#t+1],t[#t+2],t[#t+3],t[#t+4],t[#t+5],t[#t+6]=bl,br,tr,tr,tl,bl
end
local tex=m:buffer("texCoord") --get existing texture positions
for i=1,#t do tex[s+i]=t[i] end
--m.texCoords=t
end
local col=m:buffer("color")
c=c or color(255)
for i=1,#v do col[s+i]=c end
--m.colors=col
return m
end
--UV SPHERE ****************************************
--r=radius, tex=texture image, col=color (optional, applies if no texture provided)
function CreateSphere(r,tex,col)
local vertices,tc = Sphere_OptimMesh(40,20)
vertices = Sphere_WarpVertices(vertices)
for i=1,#vertices do vertices[i]=vertices[i]*r end
local ms = mesh()
ms.vertices=vertices
if tex then ms.texture,ms.texCoords=tex,tc end
ms:setColors(col or color(255))
return ms
end
function Sphere_OptimMesh(nx,ny)
local v,t={},{}
local k,s,x,y,x1,x2,i1,i2,sx,sy=0,1,0,0,{},{},0,0,nx/ny,1/ny
local c = vec3(1,0.5,0)
local m1,m2
for y=0,ny-1 do
local nx1 = math.floor( nx * math.abs(math.cos(( y*sy-0.5)*2 * math.pi/2)) )
if nx1<6 then nx1=6 end
local nx2 = math.floor( nx * math.abs(math.cos(((y+1)*sy-0.5)*2 * math.pi/2)) )
if nx2<6 then nx2=6 end
x1,x2 = {},{}
for i1 = 1,nx1 do x1[i1] = (i1-1)/(nx1-1)*sx end x1[nx1+1] = x1[nx1]
for i2 = 1,nx2 do x2[i2] = (i2-1)/(nx2-1)*sx end x2[nx2+1] = x2[nx2]
local i1,i2,n,nMax,continue=1,1,0,0,true
nMax = nx*2+1
while continue do
m1,m2=(x1[i1]+x1[i1+1])/2,(x2[i2]+x2[i2+1])/2
if m1<=m2 then
v[k+1],v[k+2],v[k+3]=vec3(x1[i1],sy*y,1)-c,vec3(x1[i1+1],sy*y,1)-c,vec3(x2[i2],sy*(y+1),1)-c
t[k+1],t[k+2],t[k+3]=vec2(-x1[i1]/2,sy*y) ,vec2(-x1[i1+1]/2,sy*y),vec2(-x2[i2]/2,sy*(y+1))
if i1<nx1 then i1 = i1 +1 end
else
v[k+1],v[k+2],v[k+3]=vec3(x1[i1],sy*y,1)-c,vec3(x2[i2],sy*(y+1),1)-c,vec3(x2[i2+1],sy*(y+1),1)-c
t[k+1],t[k+2],t[k+3]=vec2(-x1[i1]/2,sy*y),vec2(-x2[i2]/2,sy*(y+1)),vec2(-x2[i2+1]/2,sy*(y+1))
if i2<nx2 then i2 = i2 +1 end
end
if i1==nx1 and i2==nx2 then continue=false end
k,n=k+3,n+1
if n>nMax then continue=false end
end
end
return v,t
end
function Sphere_WarpVertices(verts)
local m = matrix(0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0)
local vx,vy,vz,vm
for i,v in ipairs(verts) do
vx,vy = v[1], v[2]
vm = m:rotate(180*vy,1,0,0):rotate(180*vx,0,1,0)
vx,vy,vz = vm[1],vm[5],vm[9]
verts[i] = vec3(vx,vy,vz)
end
return verts
end
--SKY CLASS ***********************************
Sky=class()
function Sky:init(r,tex)
if tex.width==tex.height*4 then
local img=image(tex.width,tex.width/2)
setContext(img)
sprite(tex,img.width/2,img.height*3/4)
sprite(tex,img.width/2,img.height*1/4,tex.width,tex.height)
setContext()
self.sky=CreateSphere(r,img)
else self.sky=CreateSphere(r,tex) end
end
function Sky:draw(p) pushMatrix() translate(p:unpack()) self.sky:draw() popMatrix() end
-- TOUCHES ********************************************
--This function allows you to rotate objects with your fingers
--It needs the function SetupTouches() to be run in setup, and HandleTouches() needs to be run in draw
--it affects anything that is drawn after it is run
--pass the position p through if you want to translate first
function HandleTouches(p)
--do rotation for touch
if CurrentTouch.state == MOVING then --only rotate while fingers are moving on the screen
currentModelMatrix=currentModelMatrix:rotate(CurrentTouch.deltaX,0,1,0)
currentModelMatrix=currentModelMatrix:rotate(CurrentTouch.deltaY,1,0,0)
end
--translate if required
if p then currentModelMatrix[13],currentModelMatrix[14],currentModelMatrix[15]=p.x,p.y,p.z end
modelMatrix(currentModelMatrix) --apply the stored settings
if CurrentTouch.state == MOVING then return true end --tells us if something has changed
end
function SetupTouches()
currentModelMatrix = modelMatrix()
end
-- JOYSTICK ******************************
JoyStick = class()
--Note all the options you can set below. Pass them through in a named table
function JoyStick:init(t)
t = t or {}
self.radius = t.radius or 100 --size of joystick on screen
self.stick = t.stick or 30 --size of inner circle
self.centre = t.centre or self.radius * vec2(1,1) + vec2(5,5)
self.damp=t.damp or vec2(0.2,0.2)
self.position = vec2(0,0) --initial position of inner circle
self.target = vec2(0,0) --current position of inner circle (used when we interpolate movement)
self.value = vec2(0,0)
self.delta = vec2(0,0)
self.mspeed = 30
self.moving = 0
end
function JoyStick:draw()
ortho()
viewMatrix(matrix())
pushStyle()
fill(160, 182, 191, 1)
stroke(118, 154, 195, 100) stroke(0,0,0,25)
strokeWidth(3)
ellipse(self.centre.x,self.centre.y,2*self.radius)
fill(78, 131, 153, 1)
ellipse(self.centre.x+self.position.x, self.centre.y+self.position.y, self.stick*2)
popStyle()
end
function JoyStick:touched(t)
if t.state == BEGAN then
local v = vec2(t.x,t.y)
if v:dist(self.centre)<self.radius-self.stick then
self.touch = t.id
--else return false
end
end
if t.id == self.touch then
if t.state~=ENDED then
local v = vec2(t.x,t.y)
if v:dist(self.centre)>self.radius-self.stick then
v = (v - self.centre):normalize()*(self.radius - self.stick) + self.centre
end --set x,y values for joy based on touch
self.target=v - self.centre
else --reset joystick to centre when touch ends
self.target=vec2(0,0)
self.touch = false
end
else return false
end
return true
end
function JoyStick:update()
local p = self.target - self.position
if p:len() < self.mspeed/60 then
self.position = self.target
if not self.touch then
if self.moving ~= 0 then
self.moving = self.moving - 1
end
else
self.moving = 2
end
else
self.position = self.position + p:normalize() * self.mspeed/60
self.moving = 2
end
local v=self.position/(self.radius - self.stick)
return self:Dampen(v)
end
function JoyStick:Dampen(v)
if not self.damp then return v end
if v.x>0 then v.x=math.max(0,(v.x-self.damp.x)/(1-self.damp.x))
else v.x=math.min(0,(v.x+self.damp.x)/(1-self.damp.x)) end
if v.y>0 then v.y=math.max(0,(v.y-self.damp.y)/(1-self.damp.y))
else v.y=math.min(0,(v.y+self.damp.y)/(1-self.damp.y)) end
return v
end
function JoyStick:isMoving()
return self.moving
end
function JoyStick:isTouched()
return self.touch
end
function JoyStick:reset()
self.position = vec2(0,0)
end
--SHADERS ************************************
LightingShader={
v=[[
uniform mat4 modelViewProjection;
uniform mat4 mModel;
uniform vec4 directColor;
uniform vec4 directDirection;
uniform vec4 ambientColor;
uniform float reflec;
uniform float fog;
uniform vec4 mistColor;
uniform vec3 camPos;
attribute vec4 position;
attribute vec4 color;
attribute vec3 normal;
varying lowp vec4 vColor;
void main()
{
gl_Position = modelViewProjection * position;
vec4 norm = normalize(mModel * vec4( normal, 0.0 ));
float diffuse = max( 0.0, dot( norm, directDirection ));
vec4 p=mModel*position;
float f = clamp(distance(p.xyz,camPos)/fog,0.0,1.0);
vColor = mix(reflec * color * ( diffuse * directColor + ambientColor ),mistColor,f);
vColor.a=1.0;
}
]],
f=[[
precision highp float;
varying lowp vec4 vColor;
void main()
{
gl_FragColor=vColor;
}
]]}
--Tile
TileShader = {
v = [[
uniform mat4 modelViewProjection;
uniform mat4 mModel;
attribute vec4 position;
attribute vec2 texCoord;
varying highp vec2 vTexCoord;
varying highp vec4 vPosition;
void main()
{
vTexCoord = texCoord;
vPosition=mModel*position;
gl_Position = modelViewProjection * position;
}
]],
f = [[
precision highp float;
uniform lowp sampler2D texture;
uniform float fog;
uniform vec4 mistColor;
uniform vec3 camPos;
varying highp vec2 vTexCoord;
varying highp vec4 vPosition;
void main()
{
lowp vec4 col = texture2D(texture, vec2(mod(vTexCoord.x,1.0),mod(vTexCoord.y,1.0)));
float f = clamp(distance(vPosition.xyz,camPos)/fog,0.0,1.0);
gl_FragColor = mix(col,mistColor,f);
}
]]}
--# Main
-- MultiStep
function setup()
demos = listProjectTabs()
for i=#demos,1,-1 do
if demos[i]=="Notes" or demos[i]=="Utility" then table.remove(demos,i) end
end
table.remove(demos) --remove the last tab
startDemo()
global = "select a step"
end
function showList()
output.clear()
for i=1,#demos do print(i,demos[i]) end
end
function startDemo()
if cleanup then cleanup() end
setup,draw,touched,collide,PrintExplanation=nil,nil,nil,nil,nil
lastDemo=Demo or readProjectData("lastDemo") or 1
lastDemo=math.min(lastDemo,#demos)
saveProjectData("lastDemo",lastDemo)
parameter.clear()
parameter.integer("Demo", 1, #demos, lastDemo,showList)
parameter.action("Run", startDemo)
loadstring(readProjectTab(demos[Demo]))()
if PrintExplanation then PrintExplanation() end
setup()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment