Skip to content

Instantly share code, notes, and snippets.

@dermotbalson
Last active February 21, 2021 03:28
Show Gist options
  • Save dermotbalson/05d444f4c4a948650589 to your computer and use it in GitHub Desktop.
Save dermotbalson/05d444f4c4a948650589 to your computer and use it in GitHub Desktop.
Asteroids 3D
--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