I wanted to find out how 2D terrain generation works so I made this. This uses no assets and is completely procedural. I love how it looks so I decided to share it on this forum. The colors are completely customizable, as well as the amount of effects. This is not finished and doesn’t have smoothing, or different channels of noise, as well as frequency control. I also don’t know how the built in noise function works, so I didn’t use it. The code for the terrain is not on the main script because I hope this will become something bigger. I hope to make it infinitely scrollable, and to add different types of terrain. This is accompanied by a procedural sun. I hope you like it.
Update: Added ability to set the seed for the terrain. Added zoom. Previous terrain clears itself for new terrain when regenerating.
Update 2: Terrain frequency is shared over multiple chunks. Larger hills. Bigger Image. Rendering is a bit more efficient.
Next update: infinite terrain.
Main Script
-- Terrain Generation
-- Use this function to perform your initial setup
function setup()
-- go into fullscreen
displayMode(FULLSCREEN)
-- initiallize terrain
Terrain:init()
end
function Sun(light,x,y,w,h,d,s,c)
-- this simply makes a n-gon with the center vertices being a color, and outer vertices being transparent
-- these are temporary tables to hold all the vertices and colors
local lphv = {}
local lphc = {}
if #light.vertices <= s*3 then
for i = 1,d,d/s do
-- this sorts the vertices in the correct order
local v = i + (d/s)
table.insert(lphv, vec2(math.cos(math.rad(i))*(w/2)+x, math.sin(math.rad(i))*(h/2)+y))
table.insert(lphc,color(c.r,c.g,c.b,0))
table.insert(lphv, vec2(math.cos(math.rad(v))*(w/2)+x, math.sin(math.rad(v))*(h/2)+y))
table.insert(lphc,color(c.r,c.g,c.b,0))
table.insert(lphv, vec2(x,y))
table.insert(lphc, c)
end
end
-- this draws the mesh
light.vertices = lphv
light.colors = lphc
pushMatrix()
light:draw()
popMatrix()
end
-- this makes a button in the location specified
function button(actX,actY,x,y,w,h,c)
fill(c)
ellipse(x,y,w,h)
noFill()
if (x + w/2) > actX
and actX > (x - w/2)
and (y + h/2) > actY
and actY > (y - h/2) then
if actX == CurrentTouch.x
or actY == CurrentTouch.y then
if CurrentTouch.state == ENDED then
return false
end
end
return true
else
return false
end
end
function draw()
-- draw everything
background(0, 233, 255, 255)
local a = mesh()
Sun(a,200,600,300,300,360,64,color(255, 245, 175, 255))
Terrain:draw()
end
Terrain Script
Terrain = class()
function Terrain:init()
-- resolution (usually 1)
self.density = 1
-- the image
self.w = 4096
self.h = HEIGHT*2
self.tiles = image(self.w/self.density,self.h/self.density)
-- empty image for quick clearing
-- image x
self.x = WIDTH/2
-- image y
self.y = (self.tiles.height/2)
-- seed for generation
self.seed = 0
-- frequency
self.intFreq = 16
-- amplitude
self.intAmp = 800
-- the maximum height
self.maxheight = (HEIGHT/2)+(self.intAmp/2)+100
-- the minimum height
self.minheight = (HEIGHT/2)-(self.intAmp/2)+100
math.randomseed(self.seed)
-- the height to generate each column a chunk
self.theight = math.random(self.minheight,self.maxheight)
-- how tall the grass is
self.grassthickness = 32
-- the base color of the grass
self.grass = color(180, 220, 54, 255)
-- the base color of the dirt
self.dirt = color(143, 90, 67, 255)
-- true if finished rendering the image
self.initrendone = false
-- number of chunks the image is split into
self.nChunks = 128
-- the chunk that is currently being rendered
self.cChunk = 0
-- Shift the chunk values up or down
self.chunkShift = 0
-- progress of the interpolation
self.intProg = 1
-- zoom level
self.zoom = 0
-- interpolation point left
self.l = 0
-- interpolation point right
self.r = 0
parameter.text("seed","0")
parameter.watch("Terrain.seed")
parameter.number("zoom",-16,16,0)
end
function Terrain:cosInt(a,b,x)
-- cosine interpolation method
local c = x*math.pi
local d = (1-math.cos(c))/2
return a * (1 - d) + (b*d)
end
function Terrain:intAve(tx,cs,o)
local e = 0
for i = 1,(o) do
e = e + Terrain:cosInt(self.l,self.r,f)
end
return e/o
end
function Terrain:shift(dir)
local cs = (self.tiles.width/self.nChunks)
local blank = image(self.w/self.density,self.h/self.density)
local i = self.tiles:copy()
local sm = spriteMode()
spriteMode(CORNER)
if dir == "right" then
setContext(blank)
pushMatrix()
translate(cs,0)
sprite(i)
popMatrix()
setContext()
end
if dir == "left" then
setContext(blank)
pushMatrix()
translate(-cs,0)
sprite(i)
popMatrix()
setContext()
end
spriteMode(sm)
self.tiles = blank
collectgarbage()
end
function Terrain:drawChunk()
end
function Terrain:initrender()
-- width of a chunk
local chunk = (self.tiles.width/self.nChunks)
self.zoom = zoom
self.seed = seed
-- when pressed, regenerate the terrain
if button(CurrentTouch.x,CurrentTouch.y,100,HEIGHT/2,50,50,color(127,127,127,127)) then
sunx = math.random(200,800)
suny = math.random(100,500)
math.randomseed(self.seed)
self.theight = math.random(self.minheight,self.maxheight)
self.l = 0
self.r = 0
self.cChunk = 0
self.initrendone = false
end
-- if done generating then stop
if self.initrendone == false then
if self.cChunk%(self.intFreq) == 0 then
-- set self.l to the height of the terrain
self.l = self.theight
-- set self.r to the next random height between min and max
self.r = math.random(self.minheight,self.maxheight)
end
-- for columns in chunk
for i = (chunk*(self.cChunk))+1, chunk*(self.cChunk+1) do
if self.intProg == 0 then
math.randomseed(self.seed+i)
end
self.intProg = (((i-(chunk)*(self.cChunk)))/(chunk)+((self.cChunk)%(self.intFreq)))/self.intFreq
-- interpolate between values
self.theight = Terrain:cosInt(self.l,self.r,self.intProg)
-- random grass height
local grand = ((math.random((self.grassthickness/2),self.grassthickness)-(self.grassthickness/4)))
for v = 0,(self.theight + grand) do
-- this determines whether dirt or grass
if v > (self.theight-self.grassthickness)-grand/2 then
-- color grass lighter at the top and darker at the bottom
local gUp = (self.theight-v)
-- add depth by making bigger grass darker, and smaller grass lighter
local effect = (3*gUp)+(3*grand)
-- apply effect to base color
local grass = color(self.grass.r-effect,self.grass.g-effect,self.grass.b-effect)
-- set the color
self.tiles:set(i,v,grass)
else
-- grainy effect
local grain = math.random(-10,30)
-- fade to darkness further from the surface
local dgrad = (self.theight-v)*1.25
-- grass shadows
if v > ((self.theight-(self.grassthickness*1.2))-grand/2) then
dgrad = (self.grassthickness*2)+((grand*3)/2)
end
-- apply effect
local dirt = color((self.dirt.r+grain)-dgrad,(self.dirt.g+grain)-dgrad,(self.dirt.b+grain)-dgrad)
-- set effect
self.tiles:set(i,v,dirt)
end
end
if i >= self.tiles.width then
self.initrendone = true
end
end
-- generate the next chunk
self.cChunk = self.cChunk + 1
end
-- stop if all chunks are generated
end
function Terrain:draw()
-- render the image
Terrain:initrender()
-- move image
if CurrentTouch.state == MOVING then
self.x = self.x + CurrentTouch.deltaX
if self.y < 0 then
self.y = self.y + CurrentTouch.deltaY
end
if self.y > -self.tiles.height/2 then
self.y = self.y + CurrentTouch.deltaY
end
end
-- draw the image
pushMatrix()
translate(self.x,self.y)
scale(self.density+self.zoom)
noSmooth()
sprite(self.tiles)
popMatrix()
end