- Recursive Subdivision Of Curves
- The Algorithm in 1-D
- The fBm Curve
- Displaying the Curve
- main.lua
- conf.lua
| Version | Date | Comments | 
|---|---|---|
| 0.1 | 25/11/2024 | Initial version | 
Recursive Subdivision Of Curves
This is a literate program (written in litpd 1) that demonstrates creating fractal paths/curves starting with a simple curve with a few line segments. The algorithm is from the classic paper on the topic of such methods 2. The paper discusses the use of recursive subdivision methods to create fractal curves and surfaces. This method and its variations have been frequently used as they produce decent results and the 1-D version that we use here has linear complexity.
This article is divided into two sections:
- Algorithm: We discuss the recursive subdivision algoritm and its properties in this section.
- Program: We develop a love2d simulation to demonstrate the use of this algorithm in various scenarios and with varying parameters.
Here is a youtube video which shows the simulation in action:
The Algorithm in 1-D
The paper mentioned in the introduction 3 describes the algorithm we are about to discuss as "... a recursive algorithm for generating approximations to the sample paths of one dimensional fBm" (fBm stands for fractional Brownian motion). Thus the algorithm generates in every run a sample Brownian motion path between the input end-points.
Since this algorithm is recursive in nature it can be repeated to any level of detail. The detail in the curve increases with the recursive depth of the program. This recursive depth should be used to produce a curve that retains sufficient detail at the highest zoom level used in the graphical application.
Now, let's jump into the algorithm. In the original paper the algorithm is written in Pascal, however here I present it informally as we will later develop the program in lua.
- 
Step 1: End-points of the Curve: First we must established the start and end-point of the curve. These will serve as the starting input to the recursive subdivision. These points on 1-d are simple real values which can be given as user input or assigned to random values. 
- 
Step 2: Establish the number of points: The paper provides states that for detail level N, the number of points in the curve should be2^N. The maxlevelNcan be a user input.
- 
Step 3: Recursive Subdivision: In the third step we take a segment and find its midpoint. Then we perturb the midpoint by a random amount. This gives us two new segments. We run the process till the segments are of zero lengths and cannot be subdivided. Each recursive step doubles the number of segments to be processed in the next round. The entire process is started with the first and last points decided in the Step 1. 
The amound of perturbation at each level of recursion decreases as we subdivide smaller and smaller segments. The amount of randomness added is the Hurst Exponent - which is related to the shape of the fBm. The value of the Hurst Exponent decides the roughness/smoothness of the generated curve.
Building and Running the Program
See the Makefile in the current directory to see how to build and run the program.
The fBm Curve
The fbmcurve.lua file defines the FBMCurve class. It represents a single curve defined using recursive subdivision. The class is defined using the middleclass library.
id: fbmdeclaration
local Class = require "middleclass"
local FBMCurve = Class("FBMCurve")
Constructor
- The class constructor accepts four optional arguments seed,num_levels,max_height, andh.
- The seedargument is an input to the random number generator. The random number generator is used to create the perturbation in the midpoint when subdividing a line segment. The default value ofseedis1.
- The num_levelsargument controls how many points will be generated in the curve. It represents the amount of detail required in the curve. The higher the value, the greater the number of points in the curve. The default value fornum_levelsis9.
- The max_heightis the maximum value of the curve generated. It is used to generate the first and last points of the curve. The x-values of each point in the curve is the index in the sequence of points.
- his a parameter which controls roughness/smoothness of the generated curve. It's default is- 0.01.
- The constructor uses the inputs to initialize some state for the curve.
- The number of points num_pointsin the curve is first set equal to2^num_levels + 1.
- A new table pointsis created to store the points of the curve.
- The first and last points of the curve are created using the random number generator.
- A default initial value is assinged to all the other points of the curve.
- The hvalue refers to theHurst Exponentand is used to calculate the ratio of dampening of the perturbation at each recursive depth. The dampening increases as we descend into sub-dividing smaller and smaller line segments. The value ofhaffects the roughness/smoothness of the generated curve.
- The ratioof dampening is defined as2^-h.
id: fbmconstructor
--- Create a new FBMCurve object
-- @param seed (number) The seed for the random number generator (default 1)
-- @param num_levels (number) The number of levels of the curve (default 9)
-- @param max_height (number) The max value of y-axis (default window height)
-- @prama h (number) Hurst Expoonent (default 0.01)
function FBMCurve:initialize(seed, num_levels, max_height, h)
    self.seed = seed or 1
    self.num_levels = num_levels or 9
    self.max_height = max_height or love.graphics.getHeight()
    self.h = 0.01
    self:generate()
end
Gaussian Random Number Generator
- The gaussfunction is a utility function to generate random numbers using the inbuilt pseudo-random generator with a mean of0and a standard deviation of1.
- The seed of the random generator is based on the input index and the seed provided by user input. This allows us to create random number sequences which can be replicated if the same input is provided again.
- The function is used in the subdivision process.
id: fbmgauss
-- Generate a Gaussian (normal) distributed random number
function FBMCurve:gauss(index)
    -- Seed the RNG uniquely for deterministic results
    local combined_seed = self.seed + index
    math.randomseed(combined_seed)
    -- Generate two uniform random numbers (0, 1)
    local u1 = math.random()
    local u2 = math.random()
    -- Box-Muller transform
    local z0 = math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2)
    -- Return a Gaussian random number with mean 0 and standard deviation 1
    return z0 
end
Subdivision Implemenation
- In the subdivision implementation we define two methods viz. generateandsubdivide.
- The generatemethod sets up the parameters for the subdivision using the user provided input. The length of the curve is decided, the array of points are created to match the length, and the first and last points are assigned random y-values based on the maximum y-value of the display. The other values in the array are given initial values which are later changed by thesubdividemethod.
- Lastly, the generatemethod calls thesubdividemethod with the first and last counts of the points array, and a measure of standard division which is based on the input Hurst Expoonent.
- The subdividemethod is a recursive function to create a new midpoint for the given range, until we are down to a range with width1.
- Each midpoint is the mean of the left and right values of the range added to a random value generated using the gaussfunction defined earlier multiplied with the given standard deviation. (Note the standard deviation of the value returned fromgaussis 1). The scaling of the random adjustment reduces with the depth of recursion.
id: fbmsubdivision
function FBMCurve:generate()
    -- use the seed to create a new random number generator
    math.randomseed(self.seed)
    -- the number of points is a function of num_levels
    self.num_points = (2 ^ self.num_levels) + 1
    -- initialize the points array
    self.points = {}
    -- random start and end points using the given seed
    -- in the range [0, num_levels]
    self.points[1] = love.math.random(1, self.max_height)
    self.points[self.num_points] = love.math.random(1, self.max_height)
    -- fill the rest of the points with 0
    for i = 2, self.num_points - 1, 1 do
        self.points[i] = 10
    end
    -- the ratio of the perturbation is a function of the Hurst Exponent
    self.ratio = 2 ^ (-self.h)
    -- define the standard deviation
    local std = self.ratio * self.num_levels
    -- start with the subdivision of the outermost segment
    self:subdivide(1, self.num_points, std)
end
function FBMCurve:subdivide(left, right, std)
    -- get the midpoint of the segment
    local mid = math.floor((left + right) / 2)
    -- only proceed if the midpoint is distinct from the left and right
    if mid ~= left and mid ~= right then
        -- the y-value at the midpoint is the mean of the left and right points
        -- with an additional random compnent multiplied with the standard
        -- deviation
        self.points[mid] = (self.points[left] + self.points[right]) / 2.0 + self:gauss(mid) * std
        -- define the standard deviation for the next level of recursion,
        -- by multiplying with self.ratio
        local stdmid = std * self.ratio
        -- subdivide the left and right segments
        self:subdivide(left, mid, stdmid)
        self:subdivide(mid, right, stdmid)
    end
end
Class definition
In this section we bring all the parts of the FBMCurve class together, and generate the program.
file: fbmcurve.lua
@<fbmdeclaration@>
@<fbmconstructor@>
@<fbmgauss@>
@<fbmsubdivision@>
return FBMCurve
Displaying the Curve
- In the final simulation we will see the curve drawn next to a bunch of slider controls which allow us to tweak some of the parameters.
- To this end, I will use the ne0luv library (described elsewhere on my website) which contains some utility UI classes.
- We will create a new panel called CurvePanelwhose only task it to create a new instance of the FBMCurve and display it.
- I will not describe this in detail as the code below is self-explanatory.
file: curvepanel.lua
local Class = require('middleclass')
local nl = require('ne0luv')
local FBMCurve = require"fbmcurve"
local CurvePanel = Class('CurvePanel', nl.Panel)
function CurvePanel:initialize(bounds)
    -- call the parent class constructor
    nl.Panel.initialize(self, bounds)
    self.curve = FBMCurve(1, 9, self:getHeight())
end
function CurvePanel:_draw()
    -- draw a warm gray background
    love.graphics.setColor(0.15, 0.1, 0.1)
    love.graphics.rectangle('fill', 0, 0, self:getWidth(), self:getHeight())
    -- draw the curve in yellow
    love.graphics.setColor(1.0, 1.0, 0.0)
    -- delta is window width divided number of points
    local delta = self:getWidth() / self.curve.num_points
    -- draw the curve
    for i = 1, self.curve.num_points - 1 do
        love.graphics.line(i * delta,
            self.curve.points[i], (i + 1) * delta, self.curve.points[i + 1])
    end
end
return CurvePanel
main.lua
We bring together the CurvePanel and a couple of slider controls to allow the user to change the values of h and num_levels input parameters.
Again the code is documented below and therefore self-explanatory.
Module Imports & Variables
id: moduleglobal
local nl = require('ne0luv')
local CurvePanel = require"curvepanel"
local layout
local controlLayout
local curvePanel
local hText
local hSlider
local levelText
local levelSlider
love.load - Initialization
id: loveload
--- love.load: Called once at the start of the simulation
function love.load()
    -- get the canvas size
    local cw = love.graphics.getWidth()
    local ch = love.graphics.getHeight()
    -- width of the control panel
    local controlPanelWidth = 250
    -- create a layout panel
    layout = nl.Layout(nl.Rect(0, 0, cw, ch), {
        layout = 'row',
    })
    -- create a curve panel
    curvePanel = CurvePanel(nl.Rect(0, 0, cw - controlPanelWidth, ch))
    -- create a control panel
    controlLayout = nl.Layout(nl.Rect(0, 0, controlPanelWidth, ch), {
        layout = 'column',
    })
    -- Hurst Exponent text and slider
    hText = nl.Text(nl.Rect(0, 0, controlPanelWidth, 20), {
        text = "Hurst Exponent(h): " .. curvePanel.curve.h,
        align = 'center',
    })
    hSlider = nl.Slider(nl.Rect(0, 0, controlPanelWidth, 20), {
        minValue = 1,
        maxValue = 500,
        currentValue = curvePanel.curve.h * 1000,
    })
    hSlider:addChangeHandler(function(slider)
        local val = hSlider.currentValue / 1000
        -- round to 2 decimal places
        curvePanel.curve.h = math.floor(val * 100) / 100
        curvePanel.curve:generate()
        hText:setText("Hurst Exponent(h): " .. curvePanel.curve.h)
    end)
    -- Number of levels text and slider
    levelText = nl.Text(nl.Rect(0, 0, controlPanelWidth, 20), {
        text = "Number of Levels: " .. curvePanel.curve.num_levels,
        align = 'center',
    })
    levelSlider = nl.Slider(nl.Rect(0, 0, controlPanelWidth, 20), {
        minValue = 5,
        maxValue = 11,
        currentValue = curvePanel.curve.num_levels,
    })
    levelSlider:addChangeHandler(function(slider)
        curvePanel.curve.num_levels = math.floor(levelSlider.currentValue)
        curvePanel.curve:generate()
        levelText:setText("Number of Levels: " .. curvePanel.curve.num_levels)
    end)
    -- add the curve panel and control panel to the layout
    layout:addChild(curvePanel)
    -- add a text panel saying "FBM Curve Parameter"
    controlLayout:addChild(nl.Text(nl.Rect(0, 0, controlPanelWidth, 30), {
        font = love.graphics.newFont(20),
        text = "Curve Parameters",
        fgColor = {0.1, 0.9, 0.2},
        align = 'center',
    }))
    -- add some empty space at the top with an empty panel
    controlLayout:addChild(nl.Panel(nl.Rect(0, 0, controlPanelWidth, 10)))
    controlLayout:addChild(hText)
    controlLayout:addChild(hSlider)
    -- add some empty space at the top with an empty panel
    controlLayout:addChild(nl.Panel(nl.Rect(0, 0, controlPanelWidth, 10)))
    controlLayout:addChild(levelText)
    controlLayout:addChild(levelSlider)
    layout:addChild(controlLayout)
end
love.update - Update the Simulation
id: loveupdate
--- love.update: Called every frame, updates the simulation
function love.update(dt)
    layout:update(dt)
end
love.draw - Draw the Simulation
id: lovedraw
--- love.draw: Called every frame, draws the simulation
function love.draw()
    layout:draw()
end
Handle Keyboard/Mouse Events
id: lovekeypressed
-- escape to exit
function love.keypressed(key)
    if key == "escape" then
        love.event.quit()
    end
end
function love.mousepressed(x, y, button)
    layout:mousepressed(x, y, button)
end
function love.mousereleased(x, y, button)
    layout:mousereleased(x, y, button)
end
function love.mousemoved(x, y, dx, dy)
    layout:mousemoved(x, y, dx, dy)
end
file: main.lua
--- main.lua: <Empty> Simulation in LÖVE
-- date: 4/3/2024
-- author: Abhishek Mishra
@<moduleglobal@>
@<loveload@>
@<loveupdate@>
@<lovedraw@>
@<lovekeypressed@>
conf.lua
file: conf.lua
--- conf.lua: Config for the love2d game.
--
-- date: 4/3/2024
-- author: Abhishek Mishra
-- canvas size
local canvasWidth = 800
local canvasHeight = 600
function love.conf(t)
    -- set the window title
    t.window.title = "Recursive Subdivision Of Curves"
    -- set the window size
    t.window.width = canvasWidth
    t.window.height = canvasHeight
    -- disable unused modules for performance
    t.modules.joystick = false
    t.modules.physics = false
    t.modules.touch = false
    -- enable console
    -- TODO: turning on console crashes Love2D on Windows,
    -- so it's disabled for now
    -- t.console = true
end
- 
https://neolateral.in/litpd-literate-programming-for-pandoc-markdown ↩ 
- 
https://doi.org/10.1145/358523.358553 "Alain Fournier, Don Fussell, and Loren Carpenter. 1982. Computer rendering of stochastic models. Commun. ACM 25, 6 (June 1982), 371--384." ↩ 
- 
https://doi.org/10.1145/358523.358553 "Alain Fournier, Don Fussell, and Loren Carpenter. 1982. Computer rendering of stochastic models. Commun. ACM 25, 6 (June 1982), 371--384." ↩ 
