Howdy, Stranger!

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

Bloom: a first experiment with procedural flowers

edited October 20 in Code Sharing Posts: 489

The code below (2017 update: updated for the introduction of integers in Lua 5.3) builds on my recent experiments with pastel colours and the golden angle to generate procedural flowers. The petals are drawn with a mesh using quadratic Bézier curves. The flowers, once initialised, draw themselves as a sprite. (Update) An example of its output is below:
image

--
-- Bloom
--

supportedOrientations(LANDSCAPE_ANY)
function setup()
    w2, h2 = WIDTH / 2, HEIGHT / 2
    bImg = image(WIDTH, HEIGHT)
    setContext(bImg)
    background(100, 175, 60)
    rectMode(CENTER)
    for i = 1, 1000 do
        resetMatrix()
        rotate(math.random(-10, 10))
        fill(math.random(90, 110),
        math.random(150, 200),
        math.random(50, 70))
        rect(math.random(WIDTH), math.random(HEIGHT),
            math.random(5, 10), 100)
    end
    setContext()
    f = {}
    print("Touch, hold and release viewer to grow a bloom.")
end

function draw()
    sprite(bImg, w2, h2)
    for i = 1, #f do
        f[i]:draw()
    end
end

function touched(touch)
    local tx, ty, state = touch.x, touch.y, touch.state
    if state == BEGAN and not id then
        touchTime = ElapsedTime
        id = touch.id
    elseif state == ENDED and touch.id == id then
        local size = math.min(50 + (ElapsedTime - touchTime) * 50, 100)
        table.insert(f, Flower(size, 21, tx, ty, math.random()))
        id = nil
    end
end

-- A function to generate a spectrum of pastel colours
-- h is [0, 1); strength is [0, 1]
function pastelH2RGB(h, strength)
    local s = strength / 2 + 0.25
    local r, g, b = 1, 1, 1
    local i = h * 3
    local x = (i % 1) * (1 - s)
    if i < 1 then r, g = 1 - x, s + x
    elseif i < 2 then r, b = s + x, 1 - x
    else g, b = 1 - x, s + x end
    return color(255 * r, 255 * g, 255 * b)
end

B2 = class()

function B2:init(p)
    self.p = p
end

function B2:point(t)
    local p = self.p
    local t1 = 1 - t
    return t1 * t1 * p[1] + 2 * t * t1 * p[2] + t * t * p[3]
end

Petal = class()

function Petal:init(size, a, b)
    local as, bs = a * size, b * size
    local n = 20
    self.mesh = mesh()
    local b1 = B2({vec2(0, 0), vec2(-as , bs), vec2(0, size)})
    local b2 = B2({vec2(0, 0), vec2(as, bs), vec2(0, size)})
    local v = {
        b1:point(0), b2:point(1/n), b1:point(1/n),
        b1:point(1), b1:point(1 - 1/n), b2:point(1 - 1/n)}
    for i = 2, n - 1 do
        local t1 = (i - 1) / n
        local t2 = i / n
        table.append(v,
            triangulate({b1:point(t1), b1:point(t2),
            b2:point(t2), b2:point(t1)}))
    end
    self.mesh.vertices = v
end

function Petal:setColor(col)
    self.color = col
end

function Petal:draw()
    pushMatrix()
    translate(self.x - 2, self.y - 2)
    rotate(self.angle)
    self.mesh:setColors(0, 0, 0, 16)
    self.mesh:draw()
    popMatrix()
    pushMatrix()
    translate(self.x, self.y)
    rotate(self.angle)
    self.mesh:setColors(self.color)
    self.mesh:draw()
    popMatrix()   
end

-- Helper function
function table.append(t1, t2)
    local n = #t1
    for i = 1, #t2 do
        t1[n + i] = t2[i]
    end
end

Flower = class()

function Flower:init(size, n, x, y, hue)
    self.x, self.y = x, y
    self.img = image(size * 2.1, size * 2.1)
    local petals = {}
    local a = math.random(360)
    local pa = 0.3 + 0.3 * math.random()
    local pb = 0.2 + 0.6 * math.random()
    local phi = (1 + math.sqrt(5)) / 2 -- Golden ratio
    local ga = 360 / phi ^ 2           -- Golden angle (radians)
    for i = 1, n do
        local p = Petal(math.random((size * 0.95) // 1, (size * 1.05) // 1), pa, pb)
        p.x, p.y = 0, 0
        p.angle = a
        local col = (hue + math.random() / 10 - 0.05) % 1
        p:setColor(pastelH2RGB(col, 0.5))
        table.insert(petals, p)
        a = a + ga
    end
    setContext(self.img)
    translate(size * 1.05, size * 1.05)
    for i = 1, #petals do
        petals[i]:draw()
    end
    a = 0
    ga = math.rad(ga)
    for i = 1, 20 do
        local r = math.sqrt(i) * size / 20
        local x, y = r * math.cos(a), r * math.sin(a)
        fill(245, 200, 0)
        ellipse(x - 1, y - 1, 7)
        fill(250, 250, 0)
        ellipse(x, y, 7)
        a = a + ga
    end
    setContext()
end

function Flower:draw()
    sprite(self.img, self.x, self.y)
end

Comments

  • Posts: 666

    Man, that is some beautiful work! Very pretty pastels and 'shadowing' effect! Fast, too!

  • Posts: 666

    Any thoughts on how to make this into trees? I would love to touch-drag and create different trees by dragging up for heat and seeing growth as it gets taller.

    Seems possible, but I don't yet understand what in the world this code is doing to make the mesh! :)

  • SimeonSimeon Admin Mod
    Posts: 4,459

    This is a really beautiful demo. Great colour choices.

  • Posts: 489

    Thank you. @aciolino, the petal boundary is formed by two quadratic Bézier curves that are mirror images of each other, each curve being defined by its three control points and parameterized by a parameter that runs from 0 to 1. Where the two curves meet forms a triangle, at the top and bottom of the petal. The rest of the petal can be approximated by a series of quads with vertices on the boundary curves. I divide the petal into 20 pieces in this way.

  • Posts: 666

    So, what are these classes? How / what does the bezier calculation? I'm really interested in how this code works. :)

  • Posts: 489

    Hello @aciolino. I hope the following helps.

    Flowers (Codea class: Flower) are made up of petals (class: Petal) which are defined by two 2nd-order (that is, quadratic) Bezier curves (class: B2).

    Taking a couple of steps back: What, in general, are Bezier curves? What do they look like?

    An answer to the first question is provided by this Wikipedia article.

    An answer to the second is provided by @Codeslinger's Interactive Bezier Curve Animation. The default for the animation is four control points (a cubic Bezier curve); you need to delete a control point to see a 2nd-order curve.

    Class B2 is simple. An array referred to by field p holds the three control points (as vec2 userdata values) and a function point(t) returns the point parameterised by variable t:

    B2 = class()
    
    function B2:init(p)
        self.p = p
    end
    
    function B2:point(t)
        local p = self.p
        local t1 = 1 - t
        return t1 * t1 * p[1] + 2 * t * t1 * p[2] + t * t * p[3]
    end
    

    Class Petal is also simple. A mesh userdata value referred to by field mesh holds the mesh for the petal. Local variables b1 and b2 refer to the two Bezier curves.

    The two curves both start at 0, 0 and end at 0, size. The middle control points are placed symmetrically at -as, bs and +as, bs. If b is 0.5, then the petal will be symmetical. If a is large, then the petal will be fat.

    Local variable v refers to an array that holds the vertices for the triangles of the mesh. It is initialised with the top and bottom triangles and then helper function table.append(t1, t2) is used to add more triangles.

    The Codea API function triangulate is used to triangulate each quad before its triangles are added to v. The top vertices of each quad lie at point t2 on the curves and the bottom vertices of each quad lie at point t1 on the curves.

    Petal = class()
    
    function Petal:init(size, a, b)
        -- Scale up a and b for size
        local as, bs = a * size, b * size
        -- Set number of segments for petal
        local n = 20
        -- Establish the mesh
        self.mesh = mesh()
        -- Establish the two Bezier curves
        local b1 = B2({vec2(0, 0), vec2(-as , bs), vec2(0, size)})
        local b2 = B2({vec2(0, 0), vec2(as, bs), vec2(0, size)})
        -- Establish the top and bottom triangles
        local v = {
            b1:point(0), b2:point(1/n), b1:point(1/n),
            b1:point(1), b1:point(1 - 1/n), b2:point(1 - 1/n)}
        -- Work through the remaining quads
        for i = 2, n - 1 do
            local t1 = (i - 1) / n
            local t2 = i / n
            -- Add the triangulated quad
            table.append(v,
                triangulate({b1:point(t1), b1:point(t2),
                b2:point(t2), b2:point(t1)}))
        end
        -- Set the vertices of the mesh
        self.mesh.vertices = v
    end
    

    This mesh has a particular location (the origin) and orientation (vertical). The petal also records its location (fields x and y) and orientation (field angle) and the mesh is translated and rotated before it is drawn, accordingly. The petal also records its colour (field color).

  • Posts: 115

    Thank you for taking the time to share. It really helps out newbs such as myself.

  • Posts: 666

    Great explanation! Nice post, too.

  • Posts: 489

    The shadow effect mentioned by @aciolino above (see also the screen shot in the updated original post) dramatically improved the quality of the output. It built on an idea for improved contrast here by @KalimMalik.

    Each petal is drawn twice, first in transparent black offset a couple of pixels below and to the left (the shadow) and then in the colour of the petal.

    function Petal:draw()
        pushMatrix()
        translate(self.x - 2, self.y - 2) -- offset a couple of pixels
        rotate(self.angle)
        self.mesh:setColors(0, 0, 0, 16)  -- transparent black
        self.mesh:draw()
        popMatrix()
        pushMatrix()
        translate(self.x, self.y)       -- set location of petal origin on the viewer
        rotate(self.angle)              -- rotate the petal about its origin
        self.mesh:setColors(self.color) -- the colour of the petal
        self.mesh:draw()
        popMatrix()   
    end
    
  • Posts: 553

    If anyone else is interested in running this, they will find that it stops due to errors when you touch the screen. This is apparently because it was written before Lua had integers, at least that's my best guess, so math.random doesn't know what to do with some decimals that get passed to it.

    I tried to account for that by mildly adjusting the code in that section, and I think now it runs like it was meant to. I know this is a very old post but it is still kind of cool.

    --
    -- Bloom
    --
    
    supportedOrientations(LANDSCAPE_ANY)
    function setup()
        w2, h2 = WIDTH / 2, HEIGHT / 2
        bImg = image(WIDTH, HEIGHT)
        setContext(bImg)
        background(100, 175, 60)
        rectMode(CENTER)
        for i = 1, 1000 do
            resetMatrix()
            rotate(math.random(-10, 10))
            fill(math.random(90, 110),
            math.random(150, 200),
            math.random(50, 70))
            rect(math.random(WIDTH), math.random(HEIGHT),
                math.random(5, 10), 100)
        end
        setContext()
        f = {}
        print("Touch, hold and release viewer to grow a bloom.")
    end
    
    function draw()
        sprite(bImg, w2, h2)
        for i = 1, #f do
            f[i]:draw()
        end
    end
    
    function touched(touch)
        local tx, ty, state = touch.x, touch.y, touch.state
        if state == BEGAN and not id then
            touchTime = ElapsedTime
            id = touch.id
        elseif state == ENDED and touch.id == id then
            local size = math.min(50 + (ElapsedTime - touchTime) * 50, 100)
            table.insert(f, Flower(size, 21, tx, ty, math.random()))
            id = nil
        end
    end
    
    -- A function to generate a spectrum of pastel colours
    -- h is [0, 1); strength is [0, 1]
    function pastelH2RGB(h, strength)
        local s = strength / 2 + 0.25
        local r, g, b = 1, 1, 1
        local i = h * 3
        local x = (i % 1) * (1 - s)
        if i < 1 then r, g = 1 - x, s + x
        elseif i < 2 then r, b = s + x, 1 - x
        else g, b = 1 - x, s + x end
        return color(255 * r, 255 * g, 255 * b)
    end
    
    B2 = class()
    
    function B2:init(p)
        self.p = p
    end
    
    function B2:point(t)
        local p = self.p
        local t1 = 1 - t
        return t1 * t1 * p[1] + 2 * t * t1 * p[2] + t * t * p[3]
    end
    
    Petal = class()
    
    function Petal:init(size, a, b)
        local as, bs = a * size, b * size
        local n = 20
        self.mesh = mesh()
        local b1 = B2({vec2(0, 0), vec2(-as , bs), vec2(0, size)})
        local b2 = B2({vec2(0, 0), vec2(as, bs), vec2(0, size)})
        local v = {
            b1:point(0), b2:point(1/n), b1:point(1/n),
            b1:point(1), b1:point(1 - 1/n), b2:point(1 - 1/n)}
        for i = 2, n - 1 do
            local t1 = (i - 1) / n
            local t2 = i / n
            table.append(v,
                triangulate({b1:point(t1), b1:point(t2),
                b2:point(t2), b2:point(t1)}))
        end
        self.mesh.vertices = v
    end
    
    function Petal:setColor(col)
        self.color = col
    end
    
    function Petal:draw()
        pushMatrix()
        translate(self.x - 2, self.y - 2)
        rotate(self.angle)
        self.mesh:setColors(0, 0, 0, 16)
        self.mesh:draw()
        popMatrix()
        pushMatrix()
        translate(self.x, self.y)
        rotate(self.angle)
        self.mesh:setColors(self.color)
        self.mesh:draw()
        popMatrix()   
    end
    
    -- Helper function
    function table.append(t1, t2)
        local n = #t1
        for i = 1, #t2 do
            t1[n + i] = t2[i]
        end
    end
    
    Flower = class()
    
    function Flower:init(size, n, x, y, hue)
        self.x, self.y = x, y
        self.img = image(size * 2.1, size * 2.1)
        local petals = {}
        local a = math.random(360)
        local pa = 0.3 + 0.3 * math.random()
        local pb = 0.2 + 0.6 * math.random()
        local phi = (1 + math.sqrt(5)) / 2 -- Golden ratio
        local ga = 360 / phi ^ 2           -- Golden angle (radians)
        for i = 1, n do
            local rParam1 = math.floor(size * 0.95 * 100000)
            local rParam2 = math.floor(size * 1.05 * 100000)
            local random = math.random(rParam1, rParam2) * 0.00001
            local p = Petal(random, pa, pb)
            p.x, p.y = 0, 0
            p.angle = a
            local col = (hue + math.random() / 10 - 0.05) % 1
            p:setColor(pastelH2RGB(col, 0.5))
            table.insert(petals, p)
            a = a + ga
        end
        setContext(self.img)
        translate(size * 1.05, size * 1.05)
        for i = 1, #petals do
            petals[i]:draw()
        end
        a = 0
        ga = math.rad(ga)
        for i = 1, 20 do
            local r = math.sqrt(i) * size / 20
            local x, y = r * math.cos(a), r * math.sin(a)
            fill(245, 200, 0)
            ellipse(x - 1, y - 1, 7)
            fill(250, 250, 0)
            ellipse(x, y, 7)
            a = a + ga
        end
        setContext()
    end
    
    function Flower:draw()
        sprite(self.img, self.x, self.y)
    end
    
  • dave1707dave1707 Mod
    Posts: 6,325

    @UberGoober The line in the original code just needed to be change to what I show.

    Original code

     local p = Petal(math.random(size * 0.95, size * 1.05), pa, pb)
    

    Change code

     local p = Petal(math.random((size * 0.95)//1, (size * 1.05)//1), pa, pb)
    
  • edited October 10 Posts: 553

    What does // do?

  • em2em2
    edited October 10 Posts: 167

    It rounds the result of division. math.floor(20/11) == 20//11

  • edited October 20 Posts: 489

    I’ve restored the example image (Dropbox no longer works) and applied @dave1707's fix to update the original code for Lua 5.3.

Sign In or Register to comment.