Created
July 27, 2016 02:49
-
-
Save dermotbalson/3bcd11364e830a581a055124004bdc90 to your computer and use it in GitHub Desktop.
Step by step 3D
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# Notes | |
--[[ | |
This set of demos is aimed at people who have not worked with 3D before. | |
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) is arranged like this. If you are standing looking forward.. | |
x axis is negative on the left, and positive on the right | |
y axis is negative below you,and positive above you | |
z axis is positive in front of you, and negative in front (ie "looking into the screen") | |
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. The block code is commented. | |
--]] | |
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 | |
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 | |
--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("'Billboarding' with 2D images") | |
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 | |
--spritesheet 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") | |
print("Find the boxes of treasure") | |
end | |
--# Sphere | |
-- Spheres | |
--[[ | |
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 | |
--# 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 | |
--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 | |
-- 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 | |
--# 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