Plotting Fractals in WebAssembly
Plotting Fractals in WebAssembly
| Previous | Top | Next |
|---|---|---|
| 2: Initial Implementation | 3: Basic WAT Implementation | 4: Optimised WAT Implementation |
| 3.2: Create the WebAssembly Module | 3.3: Generate the Colour Palette | 3.4: Escape-Time Algorithm |
3.3: Generate the Colour Palette
Since we are interested in runtime performance, we will precalculate all the possible 4-byte colour values that could be obtained from the range of iteration values between 0 and max_iters. This will then become our colour palette lookup table.
Note on Coding Style
You are free to lay out the source code of a WebAssembly Text program any way you like, but I have found the following conventions to be helpful:
-
Since WebAssembly Text programs can become deeply nested, an indentation level of 2 spaces helps avoid source code becoming unnecessarily wide due to indentation
-
Function signatures have a variety of optional statements that, if used, must immediately follow the
funckeyword. Since the open parentheses plus the wordfuncplus a space occupies 6 characters, I find it helpful to indent function signature statements by 6 characters to provide a visual cue that they are not part of the function body.
Transform an Iteration Value into an RGBA1 Colour Value
The coding that generates the colour palette does not need to be described in detail, suffice it to say that a single iteration value can be translated into the red, green and blue colour components by multiplying it by 4 (implemented as a shift left instruction), then passing it through an algorithm that derives an 8-bit value for each colour component using fixed thresholds.2
The $8_bit_clamp function shown below lives within the module definition in file mandel_plot.wat
;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;; Derive a single colour component from the iteration and colour threshold values
(func $8_bit_clamp
(param $n i32)
(param $threshold i32)
(result i32)
(local $temp i32)
;; Add a colour-specific threshold, then mask out all but the junior 10 bits
;; $temp = ($n + $threshold) & 1023
(local.set $temp
(i32.and (i32.add (local.get $n) (local.get $threshold))
(i32.const 1023)
)
)
;; How many bits does $temp use?
(if (result i32)
;; 9 or more bits?
(i32.ge_u (local.get $temp) (i32.const 256))
(then
;; If bit 10 is switched off invert value, else return zero
(if (result i32)
(i32.lt_u (local.get $temp) (i32.const 512))
(then (i32.sub (i32.const 510) (local.get $temp)))
(else (i32.const 0))
)
)
(else (local.get $temp))
)
)
With the $8_bit_clamp function in place, we can now create three colour functions in which the hard-coded colour thresholds are defined.
Stylistic Note
Since these functions are very small, their definitions can be compressed into a single line:
;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;; Generate colour components
(func $red (param $iter i32) (result i32) (call $8_bit_clamp (local.get $iter) (i32.const 0)))
(func $green (param $iter i32) (result i32) (call $8_bit_clamp (local.get $iter) (i32.const 128)))
(func $blue (param $iter i32) (result i32) (call $8_bit_clamp (local.get $iter) (i32.const 356)))
Finally, we take each of the colour component values, shift them left by the appropriate number of bits, then OR them all together to form the 32-bit colour value.
IMPORTANT
Due to the fact that all modern processors are little-endian, we must assemble the RGBA values in reverse order.
;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;; Transform an iteration value to an ABGR colour
(func $colour
(param $iter i32)
(result i32)
(local $iter4 i32)
(local.set $iter4 (i32.shl (local.get $iter) (i32.const 2)))
;; Little-endian processors require the colour component values in ABGR order, not RGBA
(i32.or
(i32.or
(i32.const 0xFF000000) ;; Fully opaque
(i32.shl (call $blue (local.get $iter4)) (i32.const 16))
)
(i32.or
(i32.shl (call $green (local.get $iter4)) (i32.const 8))
(call $red (local.get $iter4))
)
)
)
Generate the Entire Colour Palette
This particular palette generation algorithm produces colours that are distributed evenly across their range between 0 and max_iters. However, since we need to generate a static lookup table, should max_iters ever change,3 then this table will need to be regenerated.
;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;; Generate entire colour palette
(func (export "gen_palette")
(param $max_iters i32)
(local $idx i32)
(loop $next
(if (i32.gt_u (local.get $max_iters) (local.get $idx))
(then
(i32.store
(i32.add (global.get $palette_offset) (i32.shl (local.get $idx) (i32.const 2)))
(call $colour (local.get $idx))
)
(local.set $idx (i32.add (local.get $idx) (i32.const 1)))
(br $next)
)
)
)
)
There are several things to notice about this function:
- Since know that this function will only be invoked from the host environment and never from another WebAssembly function, there is no need to give this function an internal name.
- This function does not return a specific value, it writes to shared memory; therefore, it has no
resultstatement. - In §3.2, at the start of the module, we defined a global constant called
$palette_offsetwhose value is imported from the host environment as propertyjs.palette_offset. This value acts as the starting point for calculating where the nexti32colour value will be written in memory - The loop labeled
$nextcontinues until our index counter$idxexceeds the supplied value ofmax_iters - The
i32.storeinstruction takes the topi32value off the stack and writes it to memory. The first argument is the memory offset and the second is the value being stored. So we call function$colourto transform the value of$idxinto a colour, then store that value at the memory location calculated from$palette_offset + ($idx * 4)4 - When this function exits, the block of shared memory starting at the offset defined in
$palette_offsetwill contain the colours for all iteration values from 0 tomax_iters
-
RGBA stands for the four values needed to fully define a pixel’s colour: Red, Green, Blue and Alpha (opacity) ↩
-
0 for red, 128 for green, and 356 for blue ↩
-
This functionality will not be implemented until the very end of this development process ↩
-
The cheapest way to implement multiplication by a power of 2 is to perform an
i32.shl(shift left) instruction by the relevant number of binary places ↩