Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

2D Procedural grass and dirt [update 2]

edited May 14 in Code Sharing Posts: 26

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
Sign In or Register to comment.