Skip to content

Instantly share code, notes, and snippets.

@dermotbalson
Created October 21, 2013 06:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dermotbalson/7079524 to your computer and use it in GitHub Desktop.
Save dermotbalson/7079524 to your computer and use it in GitHub Desktop.
Lunar landing 3a
--# Main
--The name of the project must match your Codea project name if dependencies are used.
--Project: 3D Lunar Landing
--Version: Alpha 1.12
--Comments:
--Main
states={Intro=0,Loading=1,Flying=2,Crashed=3,Landed=4,Walking=5}
--this loads the images before doing anything else
function setup()
displayMode(FULLSCREEN)
imageStatus="Ready" --tells draw it's ok to draw the scene (will be turned off if we have to download images)
--pass through Codea name of image and internet url
--if not in Codea, will be downloaded and saved
imgMoon=LoadImage("Dropbox:Moon",
"http://i1303.photobucket.com/albums/ag142/ignatz_mouse/Surface_zps8a08cf9b.png")
if imageStatus=="Ready" then setup2() end
end
function setup2()
saveProjectInfo("Description","3D Moon landing")
saveProjectInfo("Author","ignatz")
output.clear()
state=states.Loading
rad=math.pi/180
d,da=0,0 --navigation
Map={}
HM={} --height maps
B={} --buildings table
p=Map4
p:SetupMap()
lookY=15
Sky=color(0)
FPS=60
--parameter.integer("FPS",0,60,60)
textMode(CORNER)
textColour=color(156, 202, 150, 255)
fill(textColour)
font("Inconsolata")
fontSize(18)
CreateControlPanel()
spriteMode(CORNER)
state=states.Intro
end
function CreateControlPanel()
pushStyle()
powerBar={width=300,height=50,x=WIDTH-400,y=25}
leftThrust={width=75,height=75,x=powerBar.x-105,y=powerBar.y-25}
rightThrust={width=75,height=75,x=powerBar.x+powerBar.width+40,y=powerBar.y-25}
--draw power bar
local p=image(powerBar.width,powerBar.height)
setContext(p)
strokeWidth(50)
stroke(textColour)
line(-powerBar.width,-powerBar.height*1.5,2*powerBar.width,powerBar.height*1.5)
setContext()
--create side thruster images
lineCapMode(SQUARE)
--left
local pL=image(leftThrust.width,leftThrust.height)
setContext(pL)
strokeWidth(leftThrust.height)
line(2*leftThrust.width,0,leftThrust.width,leftThrust.height)
setContext()
--right
local pR=image(rightThrust.width,rightThrust.height)
setContext(pR)
strokeWidth(rightThrust.height)
line(-rightThrust.width,0,0,rightThrust.height)
setContext()
--draw main panel
imgPanel=image(WIDTH,100)
setContext(imgPanel)
background(44, 58, 46, 255)
strokeWidth(2)
stroke(122, 143, 122, 255)
line(0,imgPanel.height-1,imgPanel.width,imgPanel.height-1)
spriteMode(CORNER)
sprite(p,powerBar.x,powerBar.y)
sprite(pL,leftThrust.x,leftThrust.y)
sprite(pR,rightThrust.x,rightThrust.y)
setContext()
popStyle()
thrustPos=powerBar.x+thrustLevel*powerBar.width
end
function draw()
if imageStatus~="Ready" then return end
background(0)
if state==states.Intro then DrawIntro()
elseif state==states.Loading then DrawLoading()
elseif state==states.Flying then DrawFlying()
elseif state==states.Crashed then DrawCrash()
elseif state==states.Landed then DrawLanding()
elseif state==states.Walking then DrawWalking()
end
if LL.ResetAlerts then LL.ResetAlerts() end
end
function DrawIntro()
pushStyle()
fontSize(60)
fill(255,255,255,150)
textMode(CENTER)
local h,s=HEIGHT*2/3,25
text("Lunar Landing 3D",WIDTH/2,h)
fontSize(18)
h=h-s*3 text("Your mission is off course and you must land soon",WIDTH/2,h)
h=h-s text("Yellow markers (if any - it's random) show where it's flat enough to land",WIDTH/2,h)
h=h-s*2 text("Use the main engine (slider bar below) and side thrusters to land",WIDTH/2,h)
h=h-s text("near a yellow marker, dropping at less than 50 pixels/sec",WIDTH/2,h)
h=h-s text("(and keep an eye on your fuel, too)",WIDTH/2,h)
fill(255,255,0,200)
h=h-s*1.5 text("Touch the screen to start",WIDTH/2,h)
popStyle()
DrawFlying()
end
function DrawFlying()
if state~=states.Intro then
fallSpeed=fallSpeed +(5-thrustLevel*maxThrustMain)*DeltaTime
Fuel=Fuel-thrustLevel*maxFuelUsed
if Fuel<0 then state=states.Crashed return end
Elevation=Elevation-fallSpeed*DeltaTime
else Elevation=1000
end
posY=Elevation
local f=forwardSpeed*DeltaTime
local mx,mz=math.sin(angle),math.cos(angle)
posX,posZ=posX+f*mx,posZ+f*mz
if posZ>mapDepth then state=states.Crashed return end
clearance=Elevation-Scene:HeightAtPos(posX,posZ)
if clearance<landerSize/3 then
if fallSpeed>maxLandSpeed then state=states.Crashed return end
for i=1,#sites do
local x,y=sites[i].x,sites[i].y
if vec3(x,Scene:HeightAtPos(x,y),y):dist(vec3(posX,posY,posZ))<landerSize*2 then
state=states.Landed
break
end
end
if state==states.Flying then state=states.Crashed end
return
end
DrawControlPanel()
perspective(30, WIDTH/HEIGHT)
--look in the same direction as we are facing
lookX,lookY,lookZ=posX+1000*mx,200,-posZ-1000*mz
camera(posX,posY,-posZ,lookX,lookY,lookZ, 0,1,0)
fill(255)
for i,mmmm in pairs(Mesh) do
for j,mm in pairs(Mesh[i]) do
LL.setEyePosition(vec3(posX,posY,-posZ))
LL.draw(mm.m)
end
end
fill(255,255,0,50)
for i=#sites,1,-1 do
local s=sites[i]
pushMatrix()
translate(s.x,Scene:HeightAtPos(s.x,s.y)+5,-s.y)
rect(0,0,5,5)
popMatrix()
end
FPS=FPS*0.9+.1/DeltaTime
end
function DrawWalking()
pushStyle()
fill(255)
fontSize(18)
text("Touch left/right/top/bottom of screen to turn/move, middle of screen to stop",75,HEIGHT-50)
popStyle()
perspective(60, WIDTH/HEIGHT)
--navigation
angle=angle+da*DeltaTime --navigation
local dd=d*DeltaTime --navigation
local h0=Scene:HeightAtPos(posX,posZ)
posX,posZ=posX+dd*math.sin(angle*rad),posZ+dd*math.cos(angle*rad)
local h1=Scene:HeightAtPos(posX,posZ)
posY=math.max(Elevation,h1+10)
if dd>0 then lookY=(h1-h0)*100/dd+150 end
--end of navigation
--look in the same direction as we are facing
lookX,lookZ=posX+1000*math.sin(angle*rad),-posZ-1000*math.cos(angle*rad)
camera(posX,posY,-posZ,lookX,lookY,lookZ, 0,1,0)
fill(255)
for i,mmmm in pairs(Mesh) do
for j,mm in pairs(Mesh[i]) do
LL.setEyePosition(vec3(posX,posY,-posZ))
LL.draw(mm.m)
end
end
pushMatrix()
fill(255,255,0,100)
strokeWidth(0)
translate(Land.x,Land.y,-Land.z)
rect(0,0,5,15)
popMatrix()
popStyle()
FPS=FPS*.9+(1/DeltaTime)*.1
if LL.ResetAlerts then LL.ResetAlerts() end
end
function DrawCrash()
pushStyle()
fontSize(60)
fill(255,0,0)
textMode(CENTER)
local h,s=HEIGHT*2/3,25
text("CRASH !",WIDTH/2,h)
fontSize(18)
fill(255)
h=h-s*3 text("Watch the down speed (and time until landing), don't let it get extreme",WIDTH/2,h)
h=h-s*2 text("Keep your height 200-800, as you can easily fall out of the sky",WIDTH/2,h)
h=h-s text("Markers are further than they may appear",WIDTH/2,h)
fill(255,255,0,200)
h=h-s*2 text("Touch the screen to try again",WIDTH/2,h)
popStyle()
end
function DrawLanding()
pushStyle()
fontSize(60)
fill(0,100,255,255)
textMode(CENTER)
local h,s=HEIGHT*2/3,25
text("LANDED !",WIDTH/2,h)
fontSize(18)
fill(255)
h=h-s*3 text("Now you can explore the moon on foot",WIDTH/2,h)
h=h-s text("Touch the left and right of the screen to turn",WIDTH/2,h)
h=h-s*2 text("Touch the top and bottom of the screen to go forward and back",WIDTH/2,h)
h=h-s text("Touch the middle of the screen to stop",WIDTH/2,h)
h=h-s*2 text("The yellow marker behind you is your spaceship",WIDTH/2,h)
h=h-s text("use your imagination, they're kinda hard to draw in 3D",WIDTH/2,h)
fill(255,255,0,200)
h=h-s*2 text("Touch the screen to start walking",WIDTH/2,h)
popStyle()
end
function DrawLoading()
pushStyle()
fontSize(60)
fill(255,255,0)
textMode(CENTER)
text("Loading...",WIDTH/2,HEIGHT/2)
popStyle()
end
function DrawControlPanel()
sprite(imgPanel,0,0)
stroke(255)
strokeWidth(1)
pushStyle()
strokeWidth(2)
stroke(255)
fill(255)
line(thrustPos,powerBar.y,thrustPos,powerBar.y+powerBar.height)
local x,y=WIDTH/2,HEIGHT/2+50
line(x-20,y,x+20,y) line(x,y-20,x,y+20)
strokeWidth(0)
local x,y=WIDTH/2,HEIGHT/2+50
line(x-20,y,x+20,y) line(x,y-20,x,y+20)
text("Height: "..math.floor(clearance or 0),330,75)
text("Down: "..math.floor(fallSpeed),330,50)
local s=math.min(1000,math.floor(clearance/math.max(fallSpeed,0.001))) --limit to 1000
text("Touchdown in: "..s.. " secs",330,25)
text("Fuel: "..math.floor(Fuel),330,5)
text("FPS: "..math.floor(FPS+.5),50,75)
WarningLight(clearance,250,300,85)
WarningLight(s,25,300,35)
WarningLight(Fuel,400,300,15)
popStyle()
end
function WarningLight(state,max,x,y)
local s=state/max*255
s=math.max(0,math.min(255,s))
fill(255-s,s,0,150)
ellipse(x,y,20)
end
--basic navigation, see Notes tab
function touched(touch)
if state==states.Walking and touch.state~=ENDED then
local center=true
if touch.x<WIDTH/4 then
da=da-2
center=false
elseif touch.x>WIDTH*3/4 then
da=da+2
center=false
end
if touch.y<HEIGHT/4 then
d=d-3
da=0
center=false
elseif touch.y>HEIGHT*3/4 then
d=d+3
da=0
center=false
end
if center then d=0 da=0 end
elseif touch.state~=BEGAN then
if state==states.Intro then
state=states.Flying
elseif state==states.Crashed then
setup2()
elseif state==states.Flying then
if DidTouchPanel(powerBar,touch) then
thrustLevel=(touch.x-powerBar.x)/powerBar.width
thrustPos=powerBar.x+thrustLevel*powerBar.width
return
end
if DidTouchPanel(leftThrust,touch) then
angle=angle-math.rad(sideThrusterEffect)
Fuel=Fuel-sideThrusterFuel
return
end
if DidTouchPanel(rightThrust,touch) then
angle=angle+math.rad(sideThrusterEffect)
Fuel=Fuel-sideThrusterFuel
return
end
elseif state==states.Landed then
SetupWalking()
end
end
end
function SetupWalking()
state=states.Walking
angle,d,dd,da=0,0,0,0
Land=vec3(posX,Scene:HeightAtPos(posX,posZ-5),posZ-5)
end
function DidTouchPanel(p,t)
if t.x>=p.x and t.x<=p.x+p.width and t.y>=p.y and t.y<=p.y+p.height then return true else return false end
end
--downloads images one by one
function LoadImage(fileName,url)
local i=readImage(fileName)
if i~=nil then return i end
--not found, we need to download, add to queue (ie table)
if imageTable==nil then imageTable={} end
imageTable[#imageTable+1]={name=fileName,url=url}
--print('Queueing',fileName)
imageStatus='Loading'
--if the first one, go ahead and download
if #imageTable==1 then
http.request(imageTable[1].url,ImageDownloaded)
print('Downloading: ',imageTable[1].name)
end
end
--saves downloaded images
function ImageDownloaded(img)
--print(imageTable[1].name,'loaded')
saveImage(imageTable[1].name,img) --save
table.remove(imageTable,1)
--load next one if we have any more to do
if #imageTable>0 then
http.request(imageTable[1].url,ImageDownloaded)
print('Downloading',imageTable[1].name)
else
setup()
end
end
--# Map4
--Map4 Everything on this tab is a user setting. Just follow the instructions
Map4={}
function Map4:SetupMap()
imageStatus="Ready"
--initialise scene, get back mesh
Mesh=Scene:init()
landerSize=50
tolerance=10
--initial settings
Elevation=1000 --pixels
forwardSpeed=50 --pixels/sec
fallSpeed=10 --pixels/sec
maxLandSpeed=50
angle=0
--engine settings
Fuel=10000
maxThrustMain,maxFuelUsed=10,10
thrustLevel=0.5
sideThrusterEffect,sideThrusterFuel=1,3
frontThrusterEffect,frontThrusterFuel=1,3
imgMoon0=image(10,10)
setContext(imgMoon0)
background(200)
setContext()
--imgMoon=readImage("Dropbox:Surface")
mapWidth,mapDepth=3000,6000 --map width/height in pixels
--x is width, y is depth, z is vertical height
posX,posZ=mapWidth/2,0 --starting position
angle=0 --initial angle, 0 faces forward
local s=100
--now the terrain
ht=hmap.create(mapWidth/s,mapDepth/s,0,500,.8)
Scene:AddTerrain({x=0,y=.4,z=0,width=mapWidth,depth=mapDepth,heightTable=ht,
tileSize=s,minY=.4,maxY=75,noiseLevel=.12,img=imgMoon,scaling=.15,heightMaps=HM,noiseStep=.2})
for i,mmmm in pairs(Mesh) do
for j,mm in pairs(Mesh[i]) do
LL.addMesh({mesh=mm.m,reflect=1,shine=0,averageNormals=true})
end
end
self:FindLandingSites(landerSize,tolerance)
LL.setAmbient({strength=0.15,color=color(150)})
LL.setDirectional({strength=.6,direction=vec3(-500,200,100),color=color(220)})
end
function Map4:FindLandingSites(s,tol)
sites={}
local w=s*2
for i=s*3, mapWidth-s*3,10 do
for j=s*3, mapDepth-s*3, 10 do
local min,max=9999,-9999
for k=-w,w,w do
for m=-w,w,w do
local h=Scene:HeightAtPos(i+k,j+m)
if h<min then min=h end
if h>max then max=h end
end
end
if max-min<=tol then
table.insert(sites,vec2(i,j))
end
end
end
--print(#sites)
end
function Map4:CreateFlag()
local m=mesh()
--m:addRect
end
--# Scene
Scene = class()
function Scene:init()
Scene.MeshTable={}
Scene.MeshTable[1],Scene.MeshTable[2]={},{}
Scene.ObjectMaps={row={},col={}}
return Scene.MeshTable
end
--adds a wall/cuts a hole in a wall of our internal collision avoidance map
--walls must be parallel to x or z axis, ie either w or d must be nil
--x,z are start position of line, w,d are width/depth
--o is true if line cannot be crossed, otherwise false
function Scene:AddWallToMap(...)
local x,z,w,d,o=Scene:Params({"x","z","width","depth","obstacle"},arg)
if o==nil then return end
xx,zz,ww,dd=math.floor(x+.5),math.floor(z+.5),math.floor(w+.5),math.floor(d+.5)
local i1,i2=0,0
if o==false then i1,i2=-1,1 end
if dd>0 then
for i=xx+i1,xx+i2 do
if Scene.ObjectMaps.col[i]==nil then Scene.ObjectMaps.col[i]={} end
table.insert(Scene.ObjectMaps.col[i],{blocked=o,z1=zz,z2=zz+dd})
end
else
for i=zz+i1,zz+i2 do
if Scene.ObjectMaps.row[i]==nil then Scene.ObjectMaps.row[i]={} end
table.insert(Scene.ObjectMaps.row[i],{blocked=o,x1=xx,x2=xx+ww})
end
end
end
--adds an image to our internal collision avoidance map
--images must be parallel to x axis, x is assumed to be centre of image
function Scene:AddImageToMap(...)
local x,z,w,r=Scene:Params({"x","z","width","rotate"},arg)
for ww=-w/2,w/2,w do
local zz=math.floor(z+ww)
if Scene.ObjectMaps.row[zz]==nil then Scene.ObjectMaps.row[zz]={} end
table.insert(Scene.ObjectMaps.row[zz],{blocked=true,x1=x-w/2,x2=x+w/2})
local xx=math.floor(x+ww)
if Scene.ObjectMaps.col[xx]==nil then Scene.ObjectMaps.col[xx]={} end
table.insert(Scene.ObjectMaps.col[xx],{blocked=true,z1=z-w/2,z2=z+w/2})
end
end
--test whether we can make the desired move without colliding with something
--information about map objects is stored in ObjectMaps
--all objects are parallel to x or z axis and stored in a row or col table accordingly
--so
--(a) if we aren't moving, there is nothing to do, return
--(b) calculate the target x,z square = xx,zz, allowing for the move
--(this will never be more than 1 pixel away)
--(c) if there is data in the xx ObjectMaps column, check if our zz value is between the ends of any
-- objects stored in there
--(d) if it is between the ends of an object, then if it is a gap, ie blocked=false, we don't need
-- to go any further, and can return the desired move
--(e) otherwise we keep looking, and set the blocked marker if we find a blocking object
--(f) at the end, if we have been blocked, we return the original position and nil speed
--(the reason we keep looking after being blocked is that we might find a gap later)
-- x,z = current position
-- dx,dz = movement in x and y axes
-- current speed (will be left as is or made 0)
function Scene:Move(...)
local x,z,dx,dz,speed=Scene:Params({"x","z","deltaX","deltaZ","speed"},arg)
local backpedal=2*60 --will bounce back by this many pixels,
--(prevents being right up against obstacle and seeing nothing)
if speed==0 then return x+dx,z+dz,speed end
local x0,z0=math.floor(x+.5),math.floor(z+.5)
local xx,zz=math.floor(x+dx+.5),math.floor(z+dz+.5)
if xx==x0 and zz==z0 then return x+dx,z+dz,speed end
local blocked=false
if Scene.ObjectMaps.col[xx]~=nil then
for i,m in pairs(Scene.ObjectMaps.col[xx]) do
if zz>=m.z1 and zz<=m.z2 then
if m.blocked==true then blocked=true
else return x+dx,z+dz,speed end --gap
end
end
if blocked==true then return x-dx*backpedal/speed,z-dz*backpedal/speed,0 end
end
if Scene.ObjectMaps.row[zz]~=nil then
for i,m in pairs(Scene.ObjectMaps.row[zz]) do
if xx>=m.x1 and xx<=m.x2 then
if m.blocked==true then blocked=true
else return x+dx,z+dz,speed end --gap
end
end
if blocked==true then return x-dx*backpedal/speed,z-dz*backpedal/speed,0 end
end
return x+dx,z+dz,speed
end
function Scene:Level(...)
local x,y,z,w,d,img,sc=Scene:Params({"x","y","z","width","depth","img","scaling"},arg)
sc=sc or 1
local x1,y1,z1,x2,y2,z2=x,y,z,x+w,y,z+d
local res1,res2=(x2-x1)/img.width/sc,(z2-z1)/img.height/sc
v,t={},{}
v = {vec3(x1,y1,-z1), vec3(x2,y1,-z1), vec3(x1,y1,-z2),
vec3(x1,y1,-z2), vec3(x2,y1,-z1), vec3(x2,y1,-z2)}
t = {vec2(0,0), vec2(res1,0), vec2(0,res2),
vec2(0,res2), vec2(res1,0), vec2(res1,res2)}
Scene:AddToMeshShader({tableIndex=1,Vertices=v,Texture=t,Image=img,x=x,z=z,w=w})
end
-- x,z = bottom left corner
-- y = height around the edges
-- w,d = width and depth of area to be terrained (pixels) - MUST BE MULTIPLE OF p
-- p = pixels per tile
-- f = minimum height (pixels), can be lower than y
-- h = max height (pixels)
-- r = whether to randomise terrain each run (true/false)
-- n = noise level (bigger numbers result in bumpy terrain, smaller gives smoother results)
-- tbl = table of heights, if provided
-- img = image or string name of image
-- sc = image scaling factor (see above) 0-1
-- hm = height map storing heights of each point in
-- ns = the fraction by which noise level increases for each tile as we go deeper into the terrain
-- eg a value of 0.3 means that the outside ring of tiles has nil noise (as always, so these tiles
-- exactly meet the surface around), the next ring of tiles has noise of 0.3 of the full level,
-- the third ring of tiles has noise of 0.6, and so on, up to a maximum of 1.0. The bigger ns, the
-- the more you will get cliffs at the edges of the terrain
function Scene:AddTerrain(...)
local x,y,z,w,d,p,f,h,r,n,tbl2,img,sc,hm,ns=Scene:Params(
{"x","y","z","width","depth","tileSize","minY","maxY","random","noiseLevel",
"heightTable","img","scaling","heightMaps","noiseStep"},arg)
local nw,nd=math.floor(w/p),math.floor(d/p) --number of tiles in each direction
--if no table of heights provided, generate one with noise
if tbl2==nil then
n=n or 0.2 --default noise level
tbl1,tbl2={},{}
--noise starts at 0 on the outside, increases by the step as we move inward, max of 1
--noise is nil at edge, increases by 30% per tile inwards, to 100%
local min,max=9999,0
--if user wants a random result each time. add a random number to the noise results
if r==true then rnd=math.random(1,10000) else rnd=0 end
for i=1,nw+1 do
tbl1[i],tbl2[i]={},{}
for j=1,nd+1 do
--noise fraction for this tile, based on how close to the edge it is
--this formula counts how many rows in from the edge this tile is
tbl1[i][j]=math.min(1,math.min(i-1,j-1,nw+1-i,nd+1-j)*ns)
--the noise itself
tbl2[i][j]=noise(rnd+i*n, rnd+j*n)
--keep track of min and max values of noise
if tbl2[i][j]<min then min=tbl2[i][j] end
if tbl2[i][j]>max then max=tbl2[i][j] end
end
end
--now go back through the whole array and scale it
--we know the user wants values between f and h
--we know our noise varies between min and max
--so now we pro rate all our values so they vary between f and h, based on the noise values
for i=1,nw+1 do
for j=1,nd+1 do
local f1=y
--pro rate
local f2=f+(h-f)*(tbl2[i][j]-min)/(max-min)
--now apply noise fraction, to reduce noise around the edges
tbl2[i][j]=f1*(1-tbl1[i][j])+f2*tbl1[i][j]
end
end
end
--store details for later use in determining height
table.insert(hm,{x1=x,z1=z,x2=x+w,z2=z+d,p=p,t=tbl2})
--create the vectors and mesh
local wx,wz=img.width*sc,img.height*sc
v,t={},{}
for i=1,nw do
for j=1,nd do
local x1,x2=x+(i-1)*p,x+i*p local x3,x4=x2,x1
local y1,y2,y3,y4=tbl2[i][j],tbl2[i+1][j],tbl2[i+1][j+1],tbl2[i][j+1]
local z1,z3=z+(j-1)*p,z+j*p local z2,z4=z1,z3
v[#v+1]=vec3(x1,y1,-z1) t[#t+1]=vec2(x1/wx,z1/wz) --bottom left
v[#v+1]=vec3(x2,y2,-z2) t[#t+1]=vec2(x2/wx,z2/wz) --bottom right
v[#v+1]=vec3(x3,y3,-z3) t[#t+1]=vec2(x3/wx,z3/wz) --top right
v[#v+1]=vec3(x3,y3,-z3) t[#t+1]=vec2(x3/wx,z3/wz) --top right
v[#v+1]=vec3(x4,y4,-z4) t[#t+1]=vec2(x4/wx,z4/wz) --top left
v[#v+1]=vec3(x1,y1,-z1) t[#t+1]=vec2(x1/wx,z1/wz) --bottom left
end
end
Scene:AddToMeshShader({tableIndex=1,Vertices=v,Texture=t,Image=img})
end
--calculates height at x,z
function Scene:HeightAtPos(x,z)
--identify the location and calculate height
local h=0 --set default as 0
for i,v in pairs(HM) do --look in each terrain area
if x>=v.x1 and x<=v.x2 and z>=v.z1 and z<=v.z2 then --if in this area...
--calc which square we are in
local mx,mz=1+math.floor((x-v.x1)/v.p),1+math.floor((z-v.z1)/v.p)
--use bilinear interpolation (most common method) for interpolating 4 corner values
--formula taken from: http://en.wikipedia.org/wiki/Bilinear_interpolation
local Q11,Q21,Q12,Q22=v.t[mx][mz],v.t[mx+1][mz],v.t[mx][mz+1],v.t[mx+1][mz+1]
local x1,x2,y1,y2,x,y=mx,mx+1,mz,mz+1,1+(x-v.x1)/v.p,1+(z-v.z1)/v.p
h=(Q11*(x2-x)*(y2-y)+Q21*(x-x1)*(y2-y)+Q12*(x2-x)*(y-y1)+Q22*(x-x1)*(y-y1))/(x2-x1)/(y2-y1)
break
end
end
return h
end
--draw a basic box with a given texture
--a box consists of 24 triangles, made up of the 8 corner vertices
--x,z is bottom left corner (looking at map from starting position, and 0,0 is bottom left of map)
--y is height above ground
--w = width along x axis (pixels)
--d = depth along z axis (pixels)
--h = height of wall along y axis (pixels)
--i = image or string name of image (do not include library name)
--sc = image scaling factor (see above) 0-1
--r = the roof pitch in degrees, defaults to 15 (normal house is 10-15)
--b = ID of existing building, creates new one if none supplied
function Scene:BuildingWalls(...)
local x,y,z,w,d,h,img,sc,r,b=Scene:Params({"x","y","z","width","depth","height",
"img","scaling","pitch","ID"},arg)
local o
if b==nil then
table.insert(B,{x=(x+w)/2,z=(z+d)/2,x1=x,y1=y,z1=z,x2=x+w,y2=y+h,z2=z+d,w=w,d=d,h=h,pitch=r,img=img,sc=sc})
b=#B
o=true --walls are only an obstacle if they they are the initial ones
end
local colr
--now create rectangular box
local wm={}
--front wall
wm[1]=Scene:AddWall({x1=x,y1=y,z1=z,x2=x+w,y2=y+h,z2=z,img=img,scaling=sc,obstacle=o})
--left side
wm[2]=Scene:AddWall({x1=x,y1=y,z1=z,x2=x,y2=y+h,z2=z+d,img=img,scaling=sc,obstacle=o})
--right side
wm[3]=Scene:AddWall({x1=x+w,y1=y,z1=z,x2=x+w,y2=y+h,z2=z+d,img=img,scaling=sc,obstacle=o})
--back side
wm[4]=Scene:AddWall({x1=x,y1=y,z1=z+d,x2=x+w,y2=y+h,z2=z+d,img=img,scaling=sc,obstacle=o})
--add ends
if r then Scene:AddRoofEnds({ID=b,img=img,scaling=sc}) end
B[b].wm=wm
--add to mesh
--Scene:AddToMeshShader(1,v,t,img,colr)
--store data unless s set
return b
end
--Draw interior walls and ceiling
--i = image or string name of image (do not include library name)
--sc = image scaling factor (see above) 0-1
--c = whether to draw a ceiling as well (omit if not required, else true)
function Scene:BuildingInteriorWalls(...)
local b,img,sc,c=Scene:Params({"ID","img","scaling","ceiling"},arg)
local margin=0.25
local x,y,z=B[b].x1+margin,B[b].y1,B[b].z1+margin
local w,d,h=B[b].w-margin*2,B[b].d-margin*2,B[b].h
Scene:BuildingWalls({x=x,y=y,z=z,width=w,depth=d,height=h,img=img,scaling=sc,ID=b})
--last param is true so we don't store bldg coords
if c then Scene:Level({x=x,y=y+h,z=z,width=w,depth=d,img=img,scaling=sc}) end --ceiling if requested
end
function Scene:AddWall(...)
local x1,y1,z1,x2,y2,z2,img,sc,o=Scene:Params({"x1","y1","z1","x2","y2","z2",
"img","scaling","obstacle","hole"},arg)
sc=sc or 1
print(x1,y1,z1,x2,y2,z2)
local imgW,imgH=img.width*sc,img.height*sc
local nX,nY,nZ=math.abs(x2-x1)/imgW,math.abs(y2-y1)/imgH,
math.abs(z2-z1)/imgW --number of tiles required in x and y axes
v,t={},{}
v[#v+1]=vec3(x1,y1,-z1) t[#t+1]=vec2(0,0)
v[#v+1]=vec3(x2,y1,-z2) t[#t+1]=vec2(nX+nZ,0) --nX and nZ added because only one can be nonzero
v[#v+1]=vec3(x1,y2,-z1) t[#t+1]=vec2(0,nY)
v[#v+1]=vec3(x1,y2,-z1) t[#t+1]=vec2(0,nY)
v[#v+1]=vec3(x2,y2,-z2) t[#t+1]=vec2(nX+nZ,nY)
v[#v+1]=vec3(x2,y1,-z2) t[#t+1]=vec2(nX+nZ,0)
Scene:AddWallToMap({x=x1,z=z1,width=x2-x1,depth=z2-z1,obstacle=o})
local m= Scene:AddToMeshShader({tableIndex=2,Vertices=v,Texture=t,Image=img,
x=(x1+x2)/2,z=(z1+z2)/2})
m.nx,m.ny,m.nz=nX,nY,nZ
m.w,m.h=x2-x1+z2-z1,y2-y1
return m
end
--adds tint to outside of building
function Scene:AddTint(...)
local b,c=Scene:Params({"ID","colour"},arg)
local e=image(10,10)
local margin=0.2
pushStyle()
setContext(e)
strokeWidth(0)
fill(c)
rect(-1,-1,e.width+2,e.height+2)
setContext()
popStyle()
Scene:BuildingWalls({x=B[b].x1-margin,y=B[b].y1,z=B[b].z1-margin,width=B[b].w+margin*2,
depth=B[b].d+margin*2,height=B[b].h,img=e,scaling=1,pitch=15,ID=b})
end
--draws an interior floor surface
--i = image or string name of image (do not include library name)
--sc = image scaling factor (see above) 0-1
function Scene:Floor(...)
local b,img,sc=Scene:Params({"ID","img","scaling"},arg)
local margin=0.05
local x,y,z,w,d=B[b].x1+margin,B[b].y1+margin,B[b].z1+margin,B[b].w-margin*2,B[b].d-margin*2
Scene:Level(x,y,z,w,d,img,sc)
end
--adds roof
--o = overhang, number of pixels by which roof is wider than walls
--i = image or string name of image (do not include library name)
--sc = image scaling factor (see above) 0-1
function Scene:Roof(...)
local b,o,img,s=Scene:Params({"ID","overhang","img","scaling"},arg)
local p=B[b].pitch*math.pi/180 --15 degree roof pitch
o=o or 3
s=s or 1
local iw,id,ih
local iw=math.max(B[b].w,B[b].d)+o*2 --greater of width and depth of building
local id=(iw/2)/math.cos(p) --depth of pitched roof (at an angle, so > d)
local ih=(iw/2)*math.tan(p) --height of roof at middle of house
local nw,nd=iw/img.width/s,id/img.height/s
local sw,sd,x1,x2,z1,z2
local y1,y2=B[b].h,B[b].h+ih
local v,t={},{}
if B[b].w>B[b].d then
x1,x2=B[b].x1-o,B[b].x2+o
z1,z2=-(B[b].z1-o),-(B[b].z1+B[b].d/2)
v[1]=vec3(x1,y1,z1) t[1]=vec2(0,0)
v[2]=vec3(x1,y2,z2) t[2]=vec2(0,nd)
v[3]=vec3(x2,y1,z1) t[3]=vec2(nw,0)
v[4]=vec3(x2,y1,z1) t[4]=vec2(nw,0)
v[5]=vec3(x2,y2,z2) t[5]=vec2(nw,nd)
v[6]=vec3(x1,y2,z2) t[6]=vec2(0,nd)
z1,z2=-(B[b].z2+o),-(B[b].z2-B[b].d/2)
v[7]= vec3(x1,y1,z1) t[7]=vec2(0,0)
v[8]= vec3(x1,y2,z2) t[8]=vec2(0,nd)
v[9]= vec3(x2,y1,z1) t[9]=vec2(nw,0)
v[10]=vec3(x2,y1,z1) t[10]=vec2(nw,0)
v[11]=vec3(x2,y2,z2) t[11]=vec2(nw,nd)
v[12]=vec3(x1,y2,z2) t[12]=vec2(0,nd)
else
x1,x2=B[b].x1-o,B[b].x1+B[b].w/2
z1,z2=-(B[b].z1-o),-(B[b].z2+o)
v[1]=vec3(x1,y1,z1) t[1]=vec2(0,0)
v[2]=vec3(x2,y2,z1) t[2]=vec2(0,nd)
v[3]=vec3(x1,y1,z2) t[3]=vec2(nw,0)
v[4]=vec3(x1,y1,z2) t[4]=vec2(nw,0)
v[5]=vec3(x2,y2,z2) t[5]=vec2(nw,nd)
v[6]=vec3(x2,y2,z1) t[6]=vec2(0,nd)
x1,x2=B[b].x2+o,B[b].x2-B[b].w/2
z1,z2=-(B[b].z1-o),-(B[b].z2+o)
v[7]=vec3(x1,y1,z1) t[7]=vec2(0,0)
v[8]=vec3(x2,y2,z1) t[8]=vec2(0,nd)
v[9]=vec3(x1,y1,z2) t[9]=vec2(nw,0)
v[10]=vec3(x1,y1,z2) t[10]=vec2(nw,0)
v[11]=vec3(x2,y2,z2) t[11]=vec2(nw,nd)
v[12]=vec3(x2,y2,z1) t[12]=vec2(0,nd)
end
--add to mes
Scene:AddToMeshShader({tableIndex=2,Vertices=v,Texture=t,Image=img,
x=(x1+x2)/2,z=(z1+z2)/2})
end
function Scene:AddRoofEnds(...)
local b,img,sc=Scene:Params({"ID","img","scaling"},arg)
local v,t={},{} --vertices and texture mappings
local p=B[b].pitch*math.pi/180 --15 degree roof pitch
local imax,imin,iw,ih
imax=math.max(B[b].w,B[b].d)
imin=math.min(B[b].w,B[b].d)
ih=(imax/2)*math.tan(p)
local nw,nh=imin/sc/img.width,ih/sc/img.height
if B[b].w>B[b].d then
v[1]=vec3(B[b].x1,B[b].h,-B[b].z1) t[1]=vec2(0,0)
v[2]=vec3(B[b].x1,B[b].h+ih,-(B[b].z1+B[b].d/2)) t[2]=vec2(nw*.5,nh)
v[3]=vec3(B[b].x1,B[b].h,-B[b].z2) t[3]=vec2(nw,0)
v[4]=vec3(B[b].x2,B[b].h,-B[b].z1) t[4]=vec2(0,0)
v[5]=vec3(B[b].x2,B[b].h+ih,-(B[b].z1+B[b].d/2)) t[5]=vec2(nw*.5,nh)
v[6]=vec3(B[b].x2,B[b].h,-B[b].z2) t[6]=vec2(nw,0)
else
v[1]=vec3(B[b].x1,B[b].h,-B[b].z1) t[1]=vec2(0,0)
v[2]=vec3(B[b].x1+B[b].w/2,B[b].h+ih,-(B[b].z1)) t[2]=vec2(nw*.5,nh)
v[3]=vec3(B[b].x2,B[b].h,-B[b].z1) t[3]=vec2(nw,0)
v[4]=vec3(B[b].x1,B[b].h,-B[b].z2) t[4]=vec2(0,0)
v[5]=vec3(B[b].x1+B[b].w/2,B[b].h+ih,-(B[b].z2)) t[5]=vec2(nw*.5,nh)
v[6]=vec3(B[b].x2,B[b].h,-B[b].z2) t[6]=vec2(nw,0)
end
--add to mesh
Scene:AddToMeshShader({tableIndex=2,Vertices=v,Texture=t,Image=img,
x=B[b].x,z=B[b].z})
end
--ID is ID of building
--wallID =1 front wall, 2=left, 3=right, 4=back, viewed from front perspective
--x,y = position of bottom left corner of feature on the wall, as seen looking at it from the outside
-- (so if you want a window 20 pixels from the left hand edge, 4 up from the ground, then x=20, y=4
--w = width
--img is image
--d is whether image shows on the inside as well-if true means image is mirrored on the inside
--o is false if this creates a gap you can walk through, like a door
--l is lighting factor
--t is whether to make a hole (transparent)
function Scene:AddFeature(...)
local b,n,x,y,w,img,d,o,l,hole=Scene:Params({"ID","wallID","x","y","w",
"img","inside","obstacle","lighting","hole"},arg)
sc=w/img.width
h=sc*img.height
local v,t={},{}
local margin
if hole then margin=0.1 else margin=0.8 end
local mm
if n==1 then --front
local zz=B[b].z1-margin
mm=Scene:AddWall({x1=B[b].x1+x,y1=y,z1=zz,x2=B[b].x1+x+w,y2=y+h,z2=zz,img=img,scaling=sc,obstacle=o})
if d then
Scene:AddWall({x1=B[b].x1+x,y1=y,z1=B[b].z1+margin,x2=B[b].x1+x+w,y2=y+h,z2=B[b].z1+margin,
img=img,scaling=sc})
end
elseif n==2 then --left
local xx=B[b].x1-margin
mm=Scene:AddWall({x1=xx,y1=y,z1=B[b].z2-x,x2=xx,y2=y+h,z2=B[b].z2-x-w,img=img,scaling=sc,obstacle=o})
if d then
Scene:AddWall({x1=B[b].x1+margin,y1=y,z1=B[b].z2-x-w,x2=B[b].x1+margin,y2=y+h,z2=B[b].z2-x,
img=img,scaling=sc})
end
elseif n==3 then --right
local xx=B[b].x2+margin
mm=Scene:AddWall({x1=xx,y1=y,z1=B[b].z1+x,x2=xx,y2=y+h,z2=B[b].z1+x+w,img=img,scaling=sc,obstacle=o})
if d then
Scene:AddWall({x1=B[b].x2-margin,y1=y,z1=B[b].z1+x,x2=B[b].x2-margin,y2=y+h,z2=B[b].z1+x+w,
img=img,scaling=sc})
end
elseif n==4 then --back
local zz=B[b].z2+margin
mm=Scene:AddWall({x1=B[b].x2-x,y1=y,z1=zz,x2=B[b].x2-x-w,y2=y+h,z2=zz,img=img,scaling=sc,obstacle=o})
if d then
Scene:AddWall({x1=B[b].x2-x,y1=y,z1=B[b].z2-margin,x2=B[b].x2-x-w,y2=y+h,z2=B[b].z2-margin,
img=img,scaling=sc})
end
end
--add to mesh
mm.light=l
if hole then Scene:MakeHole({ID=b,wallNo=n,x=x,y=y,width=w,height=h}) end
end
function Scene:MakeHole(...)
local b,n,x,y,w,h=Scene:Params({"ID","wallNo","x","y","width","height"},arg)
local mm=B[b].wm[n]
mm.m.shader = shader(autoTilerHoleShader.vertexShader, autoTilerHoleShader.fragmentShader)
if B[b].nHoles==nil then
B[b].nHoles=0
mm.m.shader.hole1=vec4(0,0,0,0)
mm.m.shader.hole2=vec4(0,0,0,0)
mm.m.shader.hole3=vec4(0,0,0,0)
end
local h,shole
if B[b].nHoles==3 then return else
B[b].nHoles=B[b].nHoles+1 h=B[b].nHoles
end
local v
local fx,fy,fz=mm.nx/mm.w,mm.ny/mm.h,mm.nz/mm.w
if n==1 then
v=vec4(x*fx,y*fy,(x+w)*fx,(y+h)*fy)
elseif n==2 then
v=1
elseif n==3 then
v=1
elseif n==4 then
v=vec4((B[b].w-x-w)*fx,y*fy,(B[b].w-x)*fx,(y+h)*fy)
end
if h==1 then mm.m.shader.hole1=v elseif h==2 then mm.m.shader.hole2=v else mm.m.shader.hole3=v end
end
--adds stand alone image
--x,z is bottom left corner (looking at map from starting position, and 0,0 is bottom left of map)
--w = width of object (pixels)
--i = image or string name of image (do not include library name)
--r = true if image should always rotate to face the user
--ow = area you cannot walk through, in pixels, centred on xx
--l = lighting
function Scene:AddImage(...)
local xx,zz,w,img,r,ow,l=Scene:Params({"x","z","w","img","rotate","obstacleWidth","lighting"},arg)
local h=w*img.height/img.width
local v,t={},{}
local x,z
if r then x,z=-w/2,0 else x,z=xx-w/2,zz end
--identify the location and calculate height
local y=Scene:HeightAtPos(xx,zz)
if y>0 then y=y-1 end
v[1]=vec3(x,y,-z) t[1]=vec2(0,0)
v[2]=vec3(x,y+h,-z) t[2]=vec2(0,1)
v[3]=vec3(x+w,y,-z) t[3]=vec2(1,0)
v[4]=vec3(x+w,y,-z) t[4]=vec2(1,0)
v[5]=vec3(x+w,y+h,-z) t[5]=vec2(1,1)
v[6]=vec3(x,y+h,-z) t[6]=vec2(0,1)
--prevent user walking through
if ow~=nil then
Scene:AddImageToMap({x=xx,z=zz,width=w,rotate=r})
end
if r then
Scene:AddToMeshShader({tableIndex=2,Vertices=v,Texture=t,Image=img,
x=xx,z=zz,w=w,rotate=true,lighting=l})
else
Scene:AddToMeshShader({tableIndex=2,Vertices=v,Texture=t,Image=img,
w=w,lighting=l})
end
collectgarbage()
end
function Scene:AddToMeshShader(...)
local n,v,t,img,c,x,y,z,w,l,sh,r=Scene:Params({"tableIndex","Vertices","Texture","Image","Colour",
"x","y","z","w","lighting","holeShader","rotate"},arg)
local mm={}
mm.m=mesh()
local colr=c or color(255)
mm.m.vertices=v
mm.m.texCoords=t
mm.m:setColors(colr)
mm.m.texture=img
--mm.x,mm.y,mm.z,mm.w,mm.r=x,y,z,w,r
--mm.light=l
--if sh ==nil then sh=autoTilerShader end
--if Shadow then
-- mm.m.shader=shader(ADSLighting.vertexShader, ADSLighting.fragmentShader)
-- local normals={}
-- for i=1,#v,3 do --determine the surfacenormal for the triangle
-- local n = (v[i+1] - v[i]):cross(v[i+2] - v[i])
-- normals[i] = n
-- normals[i+1] = n
-- normals[i+2] = n
-- end
-- mm.m.normals=normals
-- mm.m.shader.vLightPosition = vec4(0,400,200,1)
-- mm.m.shader.useTexture = true
-- mm.m.shader.useBumpMap = false
-- --else
-- --mm.m.shader = shader(sh.vertexShader, sh.fragmentShader)
-- --end
table.insert(Scene.MeshTable[n],mm)
collectgarbage()
--return mm
end
--
function Scene:Params(list,tbl) --tbl is params
if type(tbl[1])=="table" then --was named table
local t={} --match the named params with the list provided
local www
for n,v in pairs(tbl[1]) do
for i=1,#list do
if n==list[i] then
t[i]=v
break
end
end
end
return unpack(t,1,table.maxn(t))
else --was an unnamed list, return it as is
return unpack(tbl)
end
end
--# HeightMap
-- Heightmap module
-- Copyright (C) 2011 Marc Lepage
hmap={}
-- Find power of two sufficient for size
function hmap.pot(size)
local pot = 2
while true do
if size <= pot then return pot end
pot = 2*pot
end
end
-- Create a table with 0 to n zero values
function hmap.tcreate(n)
local t = {}
for i = 0, n do t[i] = 0 end
return t
end
-- Square step
-- Sets map[x][y] from square of radius d using height function f
function hmap.square(map, x, y, d, f)
local sum, num = 0, 0
if 0 <= x-d then
if 0 <= y-d then sum, num = sum + map[x-d][y-d], num + 1 end
if y+d <= map.h then sum, num = sum + map[x-d][y+d], num + 1 end
end
if x+d <= map.w then
if 0 <= y-d then sum, num = sum + map[x+d][y-d], num + 1 end
if y+d <= map.h then sum, num = sum + map[x+d][y+d], num + 1 end
end
map[x][y] = f(map, x, y, d, sum/num)
end
-- Diamond step
-- Sets map[x][y] from diamond of radius d using height function f
function hmap.diamond(map, x, y, d, f)
local sum, num = 0, 0
if 0 <= x-d then sum, num = sum + map[x-d][y], num + 1 end
if x+d <= map.w then sum, num = sum + map[x+d][y], num + 1 end
if 0 <= y-d then sum, num = sum + map[x][y-d], num + 1 end
if y+d <= map.h then sum, num = sum + map[x][y+d], num + 1 end
map[x][y] = f(map, x, y, d, sum/num)
end
-- Diamond square algorithm generates cloud/plasma fractal heightmap
-- http://en.wikipedia.org/wiki/Diamond-square_algorithm
-- Size must be power of two
-- Height function f must look like f(map, x, y, d, h) and return h'
function hmap.diamondsquare(size, f)
-- create map
local map = { w = size, h = size }
for c = 0, size do map[c] = hmap.tcreate(size) end
-- seed four corners
local d = size
dd=2^-hmap.H
D=dd
local dx=(math.random()-0.5)*2
map[0][0] = dx --f(map, 0, 0, d, 0)
map[0][d] = dx --f(map, 0, d, d, 0)
map[d][0] = dx --f(map, d, 0, d, 0)
map[d][d] = dx --f(map, d, d, d, 0)
d = d/2 D=D*dd
-- perform square and diamond steps
while 1 <= d do
for x = d, map.w-1, 2*d do
for y = d, map.h-1, 2*d do
hmap.square(map, x, y, d, f)
end
end
for x = d, map.w-1, 2*d do
for y = 0, map.h, 2*d do
hmap.diamond(map, x, y, d, f)
end
end
for x = 0, map.w, 2*d do
for y = d, map.h-1, 2*d do
hmap.diamond(map, x, y, d, f)
end
end
d = d/2 D=D*dd
end
return map
end
-- Default height function
-- d is depth (from size to 1 by powers of two)
-- h is mean height at map[x][y] (from square/diamond of radius d)
-- returns h' which is used to set map[x][y]
function hmap.defaultf(map, x, y, d, h)
return h + (math.random()-0.5)*D
end
-- Create a heightmap using the specified height function (or default)
-- map[x][y] where x from 0 to map.w and y from 0 to map.h
function hmap.create(width, height, s0,s1,h)
f = hmap.defaultf
hmap.H=h
-- make heightmap
local map = hmap.diamondsquare(hmap.pot(math.max(width, height)), f)
-- clip heightmap to desired size
for x = 0, map.w do for y = height+1, map.h do map[x][y] = nil end end
for x = width+1, map.w do map[x] = nil end
smap=hmap.rescale(map,s0,s1)
smap.w, smap.h = width, height
return smap
end
function hmap.rescale(m,s0,s1)
local min,max=9999,-9999
for i=0,#m do
for j=0,#m[1] do
if m[i][j]<min then min=m[i][j] end
if m[i][j]>max then max=m[i][j] end
end
end
mm={}
for i=0,#m do
mm[i+1]={}
for j=0,#m[1] do
mm[i+1][j+1]=s0 + s1*(m[i][j]-min)/(max-min)
end
end
return mm
end
--# Lighting
--Ambient/diffuse/Specular lighting library
--ver 1.04 point lights adjustable
LL={}
-- === LIGHT SETTINGS ====
--setAmbient, setDirectional and setPoint1 all accept a table of named parameters
--this means you need only set the ones you want, and can change any of them easily at any time
function LL.ClearLights()
LL.ambient=nil
LL.directional=nil
LL.point1=nil
LL.point2=nil
end
--general background light
--parameters = strength (0-1), color
function LL.setAmbient(p)
if LL.ambient==nil then LL.ambient={} end
LL.fill(LL.ambient,p)
LL.changeAmbient=true
end
--directional light
--parameters = strength (0-1), direction (vec3), color
function LL.setDirectional(p)
if LL.directional==nil then LL.directional={} end
LL.fill(LL.directional,p)
LL.changeDirectional=true
end
--point light
--parameters = strength (0-1), source (vec3), range (pixels), color
function LL.setPoint1(p)
if LL.point1==nil then LL.point1={} end
LL.fill(LL.point1,p)
LL.changePoint1=true
end
function LL.setPoint2(p)
if LL.point2==nil then LL.point2={} end
LL.fill(LL.point2,p)
LL.changePoint2=true
end
--specular reflection
function LL.setEyePosition(v)
LL.eyePosition=v
LL.changeEyePosition=true
end
--for each mesh
--parameters = mesh, reflect, shine, averageNormals
function LL.addMesh(...)
local m,reflect,shine,norm,ave=LL.Params({"mesh","reflect","shine","normals","averageNormals"},arg)
m.shader=shader(LL.Lighting.vertexShader,LL.Lighting.fragmentShader)
m.normals=norm or LL.CalculateNormals(m.vertices,ave)
m.shader.hasTexture=(m.texture~=nil)
m.shader.hasSpecular=(shine>0)
m.shader.reflectivity=reflect
m.shader.specularPower=32 --hard coded for now
m.shader.specularStrength=shine
m.shader.specularColor=color(255,255,255,255)
end
-- === RUNTIME FUNCTIONS ====
function TransformLights(objectPos,lightPos,rot,light)
pushMatrix()
translate(objectPos.x,objectPos.y,objectPos.z)
rotate(rot.x,1,0,0)
rotate(rot.y,0,1,0)
rotate(rot.z,0,0,1)
if light==1 then
m.shader.point1Source=ObjectToWorld(lightPos)
if LL.point1["direction"] then
m.shader.point1Direction=(ObjectToWorld(LL.point1["direction"])-ObjectToWorld(vec3(0,0,0))):normalize()
end
else
m.shader.point2Source=ObjectToWorld(lightPos)
if LL.point2["direction"] then
m.shader.point2Direction=(ObjectToWorld(LL.point2["direction"])-ObjectToWorld(vec3(0,0,0))):normalize()
end
end
popMatrix()
end
--this runs at each draw and checks for any user changes or settings
function LL.UpdateLights(m)
--ambient
if LL.changeAmbient then
m.shader.ambientStrength=LL.ambient["strength"] or 0
m.shader.ambientColor=LL.ambient["color"] or color(255)
end
--directional
if LL.changeDirectional then
m.shader.directionalStrength=LL.directional["strength"] or 0
m.shader.directionalDirection=Vec3To4(LL.directional["direction"],0)
m.shader.directionalColor=LL.directional["color"] or color(255)
m.shader.hasDirectional=true
end
if m.shader.directionalStrength==nil then
m.shader.hasDirectional=false
end
--point light
if LL.changePoint1 then
m.shader.point1Strength=LL.point1["strength"] or 0
m.shader.point1Source=Vec3To4(LL.point1["source"],1)
m.shader.point1Range= LL.point1["range"] or 0
m.shader.point1Color=LL.point1["color"] or color(255)
m.shader.hasPoint1=(m.shader.point1Strength>0)
--spotlight has direction as well
if LL.point1["direction"] then
m.shader.point1IsSpot=true
m.shader.point1Direction=Vec3To4(LL.point1["direction"]:normalize(),0)
m.shader.point1Cos=math.cos(math.rad(LL.point1["radius"] or 15))
else m.shader.point1IsSpot=false end
end
if LL.changePoint2 then
m.shader.point2Strength=LL.point2["strength"] or 0
m.shader.point2Source=Vec3To4(LL.point2["source"],1)
m.shader.point2Range= LL.point2["range"] or 0
m.shader.point2Color=LL.point2["color"] or color(255)
m.shader.hasPoint2=(m.shader.point2Strength>0)
--spotlight has direction as well
if LL.point2["direction"] then
m.shader.point2IsSpot=true
m.shader.point2Direction=Vec3To4(LL.point2["direction"]:normalize(),0) --XXXX
m.shader.point2Cos=math.cos(math.rad(LL.point2["radius"] or 15))
else m.shader.point2IsSpot=false end
end
--eye position
if LL.changeEyePosition then m.shader.eyePosition=Vec3To4(LL.eyePosition,1) end
m.shader.hasEmissive=false
m.shader.hasTexture=(m.texture~=nil) --TEMPORARY, needed while user can change textures
end
function LL.draw(m,p)
LL.UpdateLights(m)
--next line is needed in shader to convert normals to world space
--m.shader.mInvModel = modelMatrix():inverse():transpose()
m.shader.mModel = modelMatrix()
m:draw()
end
function LL.ResetAlerts() --reset event notifications
LL.changeAmbient,LL.changeDirectional,LL.changePoint1,LL.changePoint2,LL.changeEyePosition=
false,false,false,false,false end
-- ==== UTILITY FUNCTIONS ====
--reads parameters for colour settings into their tables
function LL.fill(t,p)
if t==nil then t={} end
for a,b in pairs(p) do
t[a]=b
end
end
function Vec3To4(v,n)
if n==1 then
return vec4(v.x,v.y,v.z,n)
else
local nv=v:normalize()
return vec4(nv.x,nv.y,nv.z,n)
end
end
function ObjectToWorld(v) --returns vec4 position
local mv=modelMatrix():translate(v.x,v.y,v.z)
return vec4(mv[13],mv[14],mv[15],1)
end
function VectorToEyeSpace(v,w) --returns vec4 direction
local mv=viewMatrix():inverse():translate(v.x,v.y,v.z)
return vec4(mv[13],mv[14],mv[15],w or 0)
end
function LL.CalculateNormals(vertices,average)
if average then
--average normals at each vertex
--first get a list of unique vertices, concatenate the x,y,z values as a key
local norm,unique= {},{}
for i=1, #vertices do
unique[vertices[i].x ..vertices[i].y..vertices[i].z]=vec3(0,0,0)
end
--calculate normals, add them up for each vertex and keep count
for i=1, #vertices,3 do --calculate normal for each set of 3 vertices
local n = (vertices[i+1] - vertices[i]):cross(vertices[i+2] - vertices[i])
for j=0,2 do
local v=vertices[i+j].x ..vertices[i+j].y..vertices[i+j].z
unique[v]=unique[v]+n
end
end
--calculate average for each unique vertex
for i=1,#unique do
unique[i] = unique[i]:normalize()
end
--now apply averages to list of vertices
for i=1, #vertices do --calculate average
norm[i] = unique[vertices[i].x ..vertices[i].y..vertices[i].z]
end
return norm
else
--this assumes flat surfaces, and hard edges between triangles
local norm = {}
for i=1, #vertices,3 do --calculate normal for each set of 3 vertices
local n = ((vertices[i+1] - vertices[i]):cross(vertices[i+2] - vertices[i])):normalize()
norm[i] = n --then apply it to all 3
norm[i+1] = n
norm[i+2] = n
end
return norm
end
end
--read table of parameters into a list
function LL.Params(list,tbl) --tbl is params
if type(tbl[1])=="table" then --was named table
local t={} --match the named params with the list provided
local www
for n,v in pairs(tbl[1]) do
for i=1,#list do
if n==list[i] then
t[i]=v
break
end
end
end
return unpack(t,1,table.maxn(t))
else --was an unnamed list, return it as is
return unpack(tbl)
end
end
-- ==== SHADER ==========
--cred to spacemonkey for his great code!
LL.Lighting = {
vertexShader = [[
uniform mat4 modelViewProjection;
uniform mat4 mInvModel;
uniform mat4 mModel;
// --directional light
uniform bool hasDirectional;
uniform float directionalStrength;
uniform vec4 directionalDirection;
uniform vec4 directionalColor;
// --point light
uniform bool hasPoint1;
uniform vec4 point1Source;
uniform vec4 point1Color;
uniform float point1Strength;
uniform bool hasPoint2;
uniform vec4 point2Source;
uniform vec4 point2Color;
uniform float point2Strength;
uniform bool hasSpecular;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
attribute vec3 normal;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying highp vec4 vNormal;
varying highp vec4 vPosition;
varying highp vec4 vDirectionalDiffuse;
varying highp vec4 vPoint1Diffuse;
varying highp vec4 vPoint2Diffuse;
void main()
{
vColor = color;
vTexCoord = texCoord;
//--vNormal = mInvModel * vec4( normal, 0.0 ); //--convert to world space
vNormal = mModel * vec4( normal, 0.0 );
vec4 norm = normalize(vNormal);
vPosition = mModel * position; //-- convert to world space
gl_Position = modelViewProjection * position;
if ( hasDirectional ) {
vDirectionalDiffuse = directionalColor * directionalStrength *
max( 0.0, dot( norm, directionalDirection ));
}
if ( hasPoint1 ) {
vec4 d = normalize( point1Source - vPosition );
vPoint1Diffuse = point1Color * point1Strength * max( 0.0, dot( norm, d ));
}
if ( hasPoint2 ) {
vec4 d2 = normalize( point2Source - vPosition );
vPoint2Diffuse = point2Color * point2Strength * max( 0.0, dot( norm, d2 ));
}
}
]],
fragmentShader = [[
precision highp float;
uniform lowp sampler2D texture;
// --ambient light
uniform float ambientStrength;
uniform vec4 ambientColor;
// --directional light
uniform float directionalStrength;
uniform vec4 directionalDirection;
uniform vec4 directionalColor;
// -- point 1 light
uniform float point1Strength;
uniform vec4 point1Source;
uniform float point1Range;
uniform vec4 point1Color;
uniform bool point1IsSpot;
uniform vec4 point1Direction;
uniform float point1Cos;
// -- point 2 light
uniform float point2Strength;
uniform vec4 point2Source;
uniform float point2Range;
uniform vec4 point2Color;
uniform bool point2IsSpot;
uniform vec4 point2Direction;
uniform float point2Cos;
uniform vec4 eyePosition;
// --material properties
uniform float reflectivity;
uniform float specularPower;
uniform float specularStrength;
uniform vec4 specularColor;
// --switches to reduce unnecessary calculation
uniform bool hasTexture;
uniform bool hasDirectional;
uniform bool hasPoint1;
uniform bool hasPoint2;
uniform bool hasSpecular;
uniform bool hasEmissive;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying highp vec4 vNormal;
varying highp vec4 vPosition;
varying highp vec4 vDirectionalDiffuse;
varying highp vec4 vPoint1Diffuse;
varying highp vec4 vPoint2Diffuse;
vec4 normalizedNormal = normalize(vNormal);
// -- specular light, uses Blinn-Phong half vector approach, faster than Phong
vec4 GetSpecularColor(vec4 lightPosition, vec4 lightColor, bool isDirectional)
{
vec4 lightDirection;
if ( isDirectional ) lightDirection = lightPosition;
else lightDirection = vec4( normalize( lightPosition - vPosition ));
vec4 cameraDirection = normalize( eyePosition - vPosition );
vec4 halfAngle = normalize( cameraDirection + lightDirection );
vec4 specularColor = min(lightColor + 0.5, 1.0);
float spec = pow( max( 0.0, dot( normalizedNormal, halfAngle)), specularPower );
return specularColor * spec * specularStrength;
}
// --attentuation over the range provided, stays at full for first 5% of range, then degrades linearly
float GetAttenuation(vec4 source,float range)
{
float d = length( source.xyz - vPosition.xyz ) / range;
if (d < 0.02) return 1.0;
else return max( 0.0, (1.0 - d) / 0.98 );
}
void main()
{
lowp vec4 pixel=vColor;
if (hasTexture) pixel = texture2D( texture, vec2(mod(vTexCoord.x,1.0), mod(vTexCoord.y,1.0))) ;
// -- ambient
vec4 ambient = pixel * ambientStrength * ambientColor;
// --default
vec4 specular = vec4(0.,0.,0.,0.);
// -- directional light
vec4 directional=vec4(0.,0.,0.,0.);
if (hasDirectional) {
directional = pixel * reflectivity * vDirectionalDiffuse;
if (hasSpecular) {
specular=specular + directionalStrength * GetSpecularColor(directionalDirection, specularColor, true);
}
}
// -- point light 1
vec4 point1Light=vec4(0.,0.,0.,0.);
if (hasPoint1) {
float fracSpot=1.0;
if (point1IsSpot) {
//--get vector from light to current pixel
vec4 lightToPixel = normalize( vPosition - point1Source );
//--calculate cos of angle
float cos = dot( point1Direction, lightToPixel );
//---compare with max cos of spotlight, interpolate if within its beam
if ( cos >= point1Cos && cos <= 1.0 ) {
fracSpot = max( 0.0, 1.0 - ( 1.0 - cos ) / ( 1.0 - point1Cos ));
}
else fracSpot = 0.0;
}
float attenuation1 = GetAttenuation( point1Source, point1Range );
point1Light = pixel * reflectivity * fracSpot * attenuation1 * vPoint1Diffuse;
if (hasSpecular) {
specular=specular + fracSpot * attenuation1 * point1Strength *
GetSpecularColor(point1Source, specularColor, false);
}
}
// -- point light 2
vec4 point2Light=vec4(0.,0.,0.,0.);
if (hasPoint2) {
float fracSpot2=1.0;
if (point2IsSpot) {
//--get vector from light to current pixel
vec4 lightToPixel2 = normalize( vPosition - point2Source );
//--calculate cos of angle
float cos2 = dot( point2Direction, lightToPixel2 );
//---compare with max cos of spotlight, interpolate if within its beam
if ( cos2 >= point2Cos && cos2 <= 1.0 ) {
fracSpot2 = max( 0.0, 1.0 - ( 1.0 - cos2 ) / ( 1.0 - point2Cos ));
}
else fracSpot2 = 0.0;
}
float attenuation2 = GetAttenuation( point2Source, point2Range );
point2Light = pixel * reflectivity * fracSpot2 * attenuation2 * vPoint2Diffuse;
if (hasSpecular) {
specular=specular + fracSpot2 * attenuation2 * point2Strength *
GetSpecularColor(point2Source, specularColor, false);
}
}
vec4 totalColor = clamp(ambient + directional + point1Light + point2Light + specular,0.,1.);
totalColor.a=1.;
gl_FragColor=totalColor;
}
]]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment