--# 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. 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 --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") 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 i1nMax 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 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