Last active
February 21, 2021 03:28
-
-
Save dermotbalson/05d444f4c4a948650589 to your computer and use it in GitHub Desktop.
Asteroids 3D
This file contains 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
--Three D asteroids | |
--The comments below are aimed at a wide variety of users | |
--They may be too detailed or not detailed enough, I do my best! | |
--Comments and questions to ignatz on the Codea forum | |
--There is a lot of setup work to do, but note how it's packaged into separate functions to keep tidy | |
function setup() | |
Settings() --general settings | |
ast={} --table of asteroids to show on screen | |
FPS=60 --frame rate, to check it's not too slow | |
--use a temporary draw function to show a message while loading | |
--store the address of the real draw function away safely | |
drawTemp=draw | |
--set a temporary function to use for drawing | |
draw=LoadingScreen | |
end | |
function Settings() | |
displayMode(FULLSCREEN) | |
--these next few lines are used to help calculate how far away to place asteroids when they | |
--first appear. We want to show them only when they get big enough to see, so small asteroids | |
--will appear on screen when they are closer to us than large asteroids, which we can see from | |
--further away | |
horizon=100000 --maximum viewing distance | |
sizeToDraw=4 --minimum visible size of asteroid before we need to draw it | |
--now get the perspective factors we will need later | |
--set up the 3D screen temporarily so we can capture the factors | |
perspective(45,WIDTH/HEIGHT,0.1,horizon) | |
camera(0,0,0,0,0,-horizon) | |
xFactor=projectionMatrix()[1] --these are the factors, see how they are used later | |
yFactor=projectionMatrix()[6] --ditto | |
--if we add asteroids randomly over the entire screen, most of them will fall off the sides before | |
--they get to us. If we add them more in the middle, they will appear to fan out, and fill the screen | |
--before they get to us. Scatter is what proportion of the screen we add them in, eg the midde 50% | |
scatter=0.5 | |
newAsteroidRate=2 --per second | |
vel=vec3(0,0,2000) --velocity of asteroids, make them all the same | |
timer=0 --used for deciding when to add more asteroids | |
end | |
--This function first creates an icosphere, then uses this to create a number of odd shapes by | |
--distorting vertices (MakeAsteroid) | |
function CreateAsteroidTemplates() | |
--make an icosphere with 2 subdivisions (1500 vertices) | |
--increasing the parameter by 1 makes it smoother but multiplies vertices by 5! | |
M=CreateIcosphere(2) | |
numStdAsteroids=10 --number of templates to make | |
StdAsteroids={} --table for asteroid templates | |
for i=1,numStdAsteroids do | |
StdAsteroids[i]=MakeAsteroid(M,img) | |
end | |
end | |
function LoadingScreen() | |
--draw wait screen | |
background(0) | |
fill(255) | |
fontSize(24) | |
text("Prepare to enter\nthe asteroid field, captain!",WIDTH/2,HEIGHT/2) | |
frame=(frame or 0)+1 --increment frame count | |
--don't do anything during the first frame, to allow the screen to draw the message | |
--set our time consuming asteroid creation running during the second frame | |
--the message from the first frame will still be there | |
if frame==2 then CreateAsteroidTemplates() --create some asteroid shapes to choose from | |
--when we get to the third frame, the asteroids are done, and we can use the normal draw functio | |
elseif frame==3 then draw=drawTemp end | |
end | |
function draw() | |
background(0) | |
FPS=FPS*0.9+0.1/DeltaTime --frame rate | |
--make the camera see things up to 10,000 pixels away (all the other parameters are defaults) | |
perspective(45,WIDTH/HEIGHT,0.1,horizon) | |
camera(0,0,0,0,0,-horizon) | |
--add new asteroid based on timer | |
if ElapsedTime>timer then | |
timer=ElapsedTime+1/newAsteroidRate | |
AddAsteroid() | |
end | |
local count=0 --asteroid counter | |
for i,a in pairs(ast) do --draw asteroids | |
a.pos=a.pos+a.vel*DeltaTime --move them | |
if a.pos.z>a.offscreen then a=nil --kill them if they fall off screen | |
else | |
pushMatrix() | |
translate(a.pos.x,a.pos.y,a.pos.z) | |
rotate(a.rot.x,1,0,0) rotate(a.rot.y,0,1,0) rotate(a.rot.z,0,0,1) | |
a.rot=a.rot+a.drot --adjust rotation for next time | |
a.obj.shader.mModel=modelMatrix() --used for lighting | |
a.obj:draw() | |
popMatrix() | |
count=count+1 | |
end | |
end | |
--show frame rate and number of asteroids on screen | |
ortho() --convert back to 2D first | |
viewMatrix(matrix()) | |
fill(255,255,255,100) | |
fontSize(18) | |
text("FPS: "..math.floor(FPS).." count="..count,150,50) | |
end | |
--choose one of the template asteroids, resize it | |
function AddAsteroid() | |
local a={} | |
a.size=10+500*math.random()*math.random() --random size | |
local e=StdAsteroids[math.random(1,numStdAsteroids)] --choose random asteroid from our table | |
local vv=e:buffer("position") --get vertex positions | |
local cc=e:buffer("color") | |
--resize them to be the size we want | |
local v,c={},{} | |
local L=vv[1]:len() --current size of template | |
for i=1,e.size do v[i]=vv[i]*a.size/L end --pro-rate all vectors to new size | |
for i=1,e.size do c[i]=cc[i] end | |
local m=mesh() | |
m.vertices=v | |
m:setColors(color(255)) | |
m.shader=shader(Diffuse.vertexShader,Diffuse.fragmentShader) | |
--set random shades of gray for variety | |
local r=math.random() | |
m.shader.ambientColor=color(255*(.1+.2*r)) | |
m.shader.directColor=color(255*(0.2+.6*r)) | |
m.shader.directDirection=vec4(-1,0,1,0):normalize() | |
m.shader.reflect=0.6 | |
a.obj=m | |
--now calculate distance at which we can first see this asteroid, depending on its size | |
local z=-a.size*WIDTH*xFactor/sizeToDraw | |
--and how many pixels wide and high the screen is, at that distance | |
local xx,yy=-math.floor(z/xFactor),-math.floor(z/yFactor) | |
--calculate random x,y position, applying scatter factor (limits how much of the screen we use) | |
local x,y=math.random(-xx,xx)*scatter,math.random(-yy,yy)*scatter | |
a.pos=vec3(x,y,z) | |
--calculate random rotation, and random rotation rate | |
a.rot=vec3(math.random(-180,180),math.random(-180,180),math.random(-180,180)) | |
a.drot=vec3(math.random(),math.random(),math.random())-vec3(0.5,0.5,0.5) | |
--se velocity | |
a.vel=vel | |
--using the same factors as for deciding how far away to place the asteroid, we can figure out exactly | |
--when it will fall off the edge of the screen. This makes it easy to test when to destroy it. | |
a.offscreen= | |
math.max(-a.size/2,math.min(-(math.abs(a.pos.x)-a.size)/xFactor,-(math.abs(a.pos.y)-a.size)/yFactor)) | |
table.insert(ast,a) | |
end | |
--this function creates a distored version of our icosphere as an asteroid template | |
function MakeAsteroid(master) | |
local v=master:buffer("position") --get vertex positions | |
local s=master.size | |
local g=1 | |
local vv={} | |
for i=1,s do vv[i]=v[i] end --copy vertices to new table | |
for i=1,math.random(10,40) do --stretch a number of vertices | |
--get a random direction to stretch in | |
local f=(vec3(math.random(),math.random(),math.random())-vec3(0.5,0.5,0.5)):normalize() | |
local gg=math.random()*g --gg is size of stretch | |
local sgn=1 if math.random()>0.5 then sgn=-1 end --can be outward or inward | |
--now apply the stretch to all vertices on the same side of the sphere | |
--the closer they are to the stretch direction, the greater the effect | |
--the dot function does this nicely | |
for j=1,s do | |
local hh=(vv[j]:normalize()):dot(f) | |
if hh>0 then vv[j]=vv[j]*(1+hh*gg*sgn) end | |
end | |
g=g*0.95 --reduce the size of stretch for next time, this helps give a smoothed effect | |
end | |
--stretching vertices has moved the centre of the sphere, recalculate it | |
local c=vec3(0,0,0) | |
for j=1,s do c=c+vv[j] end | |
c=c/s | |
--subtract new centre from all vertices so it will spin correctly | |
for j=1,s do vv[j]=(vv[j]-c) end | |
local a=mesh() | |
a.vertices=vv | |
return a | |
end | |
--an icosphere is a sphere made of equally sized and shaped triangles | |
--this function starts by making an icosphere with 12 vertices, then subdivides f times | |
--each subdivision makes it smoother but increases the number of vertices by 5 | |
function CreateIcosphere(f) | |
f=f or 0 | |
--make a 12 sided icosphere | |
local t=(1+math.sqrt(5))/2 | |
local v={vec3(-1,t,0),vec3(1,t,0),vec3(-1,-t,0),vec3(1,-t,0),vec3(0,-1,t),vec3(0,1,t), | |
vec3(0,-1,-t),vec3(0,1,-t),vec3(t,0,-1),vec3(t,0,1),vec3(-t,0,-1),vec3(-t,0,1)} | |
vertOrder={0,11,5, 0,5,1, 0,1,7, 0,7,10, 0,10,11, 1,5,9, 5,11,4, 11,10,2, 10,7,6, 7,1,8, | |
3,9,4, 3,4,2, 3,2,6, 3,6,8, 3,8,9, 4,9,5, 2,4,11, 6,2,10, 8,6,7, 9,8,1} | |
local verts={} | |
for i=1,#vertOrder do | |
table.insert(verts,v[vertOrder[i]+1]) | |
end | |
--now subdivide | |
for i=1,f do | |
local vv={} | |
--loop through triangles, calculate midpoint of each line, make 4 subtriangles | |
for j=1,#verts,3 do | |
local v1=GetMiddle(verts[j],verts[j+1]) | |
local v2=GetMiddle(verts[j+1],verts[j+2]) | |
local v3=GetMiddle(verts[j+2],verts[j]) | |
table.insert(verts,verts[j]) table.insert(verts,v1) table.insert(verts,v3) | |
table.insert(verts,verts[j+1]) table.insert(verts,v2) table.insert(verts,v1) | |
table.insert(verts,verts[j+2]) table.insert(verts,v3) table.insert(verts,v2) | |
table.insert(verts,v1) table.insert(verts,v2) table.insert(verts,v3) | |
end | |
end | |
local m=mesh() | |
m.vertices=verts | |
m:setColors(color(255)) | |
return m | |
end | |
--calculates middle point between two vertices | |
function GetMiddle(v1,v2) | |
local p=(v1+v2)/2 --this point is the average of v1 and v2 but is not on the surface | |
--so we need to adjust it so itsl ength is the same as the other vertices | |
return p*v1:len()/p:len() | |
end | |
--shader | |
--this shader uses very simple diffuse lighting based on normals | |
--however the normals for each pixel are based on actual vertex positions, making the interior perfectly rounded | |
--a texture is not possible (I couldn't find a way to map a texture to icosphere vertices) | |
Diffuse={ | |
vertexShader=[[ | |
uniform mat4 modelViewProjection; | |
uniform mat4 mModel; | |
attribute vec4 position; | |
attribute vec4 color; | |
varying lowp vec4 vColor; | |
varying lowp vec4 vPosition; | |
vec4 centre=mModel*vec4(0.0,0.0,0.0,1.0); //world position of centre of asteroid | |
void main() | |
{ | |
vColor = color; | |
gl_Position = modelViewProjection * position; | |
//calculate world position of vertex, subtract world position of centre | |
//this gives us the normal of the vertex | |
vPosition = mModel * position-centre; | |
} | |
]], | |
fragmentShader=[[ | |
precision highp float; | |
uniform float reflect; | |
uniform vec3 centre; | |
uniform vec4 ambientColor; | |
uniform vec4 directColor; | |
uniform vec4 directDirection; | |
varying lowp vec4 vColor; | |
varying lowp vec4 vPosition; | |
void main() | |
{ | |
vec4 pixel = vColor; | |
vec4 norm = normalize(vPosition); | |
float diffuse = max( 0.0, dot( norm, directDirection )); | |
vec4 totalColor = reflect * pixel * (ambientColor + diffuse * directColor); | |
totalColor.a=1.; | |
gl_FragColor=totalColor; | |
} | |
]] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment