This is yet another episode in my attempt to write a (better) fractal-generating program in Elixir. The series so far:
I implemented a couple more coloring schemes. They’re a little more interesting that the black-on-white and white-on-black schemes from last week.
As always, my code is in my mandelbrot
repo on Github tagged blog_2016_06_26
.
Last week we saw pictures like these:
I now implemented a “gray” color scheme:
I really like the effect that it produces. You can now see the connection to the little mini-Mandelbrot off to the left. The tendrils sticking out of the main set also stand out.
It takes just a little math1:
def gray(iterations) when escaped?(iterations), do: PPM.black
def gray(iterations) do
factor = :math.sqrt(iterations / @maximum_iterations)
intensity = round(@maximum_intensity * factor)
PPM.ppm(intensity, intensity, intensity)
end
The inside of the Mandelbrot set is always colored black. The outside of the set gets a scaled gray color. factor
scales the intensity so that points that escape fast (with a low number of iterations) are close to black. Points that take a while to escape (close to the set) are lighter colored.
Colors are weird, and our brains are weirder. A simple linear scale (without the square root) doesn’t work for our eyes and brain. It looks rather boring:
We need the intensity to get lighter (towards white) faster. Numbers really close to zero grow much larger when you take the square root:
That’s iterations versus intensity.
“Warp” is a user on the povusers.org website2. Unfortunately, he (I think) doesn’t include any personal information (like his name), so I can’t give him proper credit. But he suggests a coloring scheme on his Mandelbrot set page.
This “Warp POV” coloring scheme uses two linear scales. For points that escape quickly, the scale is from black to bright red (or green or blue). For points that escape more slowly, the scale is from bright red (or green or blue) to white.
It works pretty well. Red:
Green:
Blue:
To my eye, I get more detail from the gray-scale. These Warp-POV coloring schemes work very well with the burning ship fractal which we’ll be seeing pretty soon.
The coloring is based on computing two intensities:
def intensities(iterations) when escaped?(iterations), do: {0 , 0}
def intensities(iterations) do
half_iterations = @maximum_iterations/2-1
if iterations <= half_iterations do
{scale(max(1, iterations)), 0}
else
{@maximum_intensity, scale(iterations - half_iterations)}
end
end
def scale(i) do
round(2.0 * (i - 1) / @maximum_iterations * @maximum_intensity)
end
The primary intensity is used for the color of the color scheme: red, green, or blue. The second intensity is for the other two color components. So for the red color scheme the intensities {128, 0}
are turned into PPM.ppm(128, 0, 0)
. If it were the blue color scheme, the same intensities are turned into PPM.ppm(0, 0, 128)
.
The challenge with a random coloring scheme is to keep the coloring consistent for one image. If two points escape after 58 iterations, they both need the same color. The colors can vary from run to run, but for one run they need to be consistent.
So somewhere, somehow, you have to keep state. My state is a list of random colors, and I keep it in a process because this is Elixir, of course.
Fractals.Colorizer.Random
is a GenServer
which is supervised by Fractals.ColorizerSupervisor
. The supervisor always starts the Random
server, even if another color scheme is selected. Since the process just holds 256 random colors, it doesn’t seem like something that really needs to be optimized.
When the server starts, Random
generates a list of random colors:
defp make_colors do
Enum.map(0..255, &random_color/1)
end
defp random_color(_) do
PPM.ppm(random_hue, random_hue, random_hue)
end
defp random_hue do
:random.uniform(255)
end
The result of make_colors
is the state for the GenServer
. A Random.at
function returns the color at a particular index.
The one drawback to using a GenServer
is that it’s a bottleneck. ColorizerWorker
spawns Task
s to get chunks of the image colored, and each Task
concurrently asks Random
for colors. Nevertheless, as far as I can tell, the random coloring doesn’t run appreciable slower than another other scheme.
One of my concerns with my old Elixir app was the way I dispatched on the different fractal options. I thought the way I handled the color schemes was okay:
# OLD VERSION!
def color_function(options) do
case options.color do
:black_on_white -> &Simple.black_on_white/1
:white_on_black -> &Simple.white_on_black/1
:gray -> &Simple.gray/1
:blue -> &WarpPov.blue/1
:green -> &WarpPov.green/1
:red -> &WarpPov.red/1
:random -> Random.build_random
end
end
The result of color_function
was stored as part of the fractal options, before the actual processing of the fractal even got started.
In my reboot, the coloring scheme is picked as late as possible:
def color_point({_zn, iterations}, %Options{color: color}) do
case color do
:black_on_white -> black_on_white(iterations)
:white_on_black -> white_on_black(iterations)
:gray -> gray(iterations)
:red -> WarpPov.red(iterations)
:green -> WarpPov.green(iterations)
:blue -> WarpPov.blue(iterations)
:random -> Random.at(Random, iterations)
end
end
I like how this code is explicit about what and how it’s calling the functions for the different coloring schemes. I don’t have to come up with abstractions like I did for color_function
.
The dispatch to the different color scheme looks the same, done through a case
expression. The difference is that color_point
is given the result of the escape-time algorithm along with the fractal options, after the escape-time algorithm has been run. While color_function
is called once, color_point
is called on every grid point.
I’m having a hard time getting over the extra cost this incurs. Pick the function for the color scheme for every point? Why not pick it once and be done with it? And yet, as I pointed out in the original “Rebooted” article, if I were writing this in an Object-Oriented language like Ruby, I’d use a class for each color scheme. There would be a color_point
method with a common interface (can’t get around this in an OO language), and the method would have to be looked up on the color-scheme object for every point. I don’t even flinch at that scenario because that’s OOP. I’m not sure why the analogous thing in FP makes me anxious.
The Warp-POV color scheme does not have to stick to the three primary colors. With a bit of tweaking, yellow, magenta, and cyan shouldn’t be too hard to do since they’re the equal blending of two primary colors. The real accomplishment would be to specify any color to put at the halfway point in the color scheme.
I recently discovered a continuous, smooth scaling (wikipedia, stackoverflow, “Renormalizing the Mandelbrot Escape”). I’ll have to spend some time absorbing the math a bit first.
The random coloring scheme needs to be consist for one image. If I tweak my app to generate multiple images at the same time, I might want different random colorings for different images. My current architecture works on only one image, so it’s premature to work on this any time soon.
I’d also like to run some timing experiments. What is the overhead of picking the function for the color scheme on every point? Just how much does the bottleneck of Random
affect performance?
I also created a Mix task for generating images. I had used a Rakefile
for the previous Elixir attempt since I could crib it from the one I had written for my Haskell version.
I decided that I should learn a bit more about Mix, and so I wrote this Mix task.
I’ll talk about about this task in a future article once I’ve worked on it a bit more. Right now it works pretty well, but it’s a bit hacky (e.g., it invokes the fractals
executable through the shell).
I think I’ll tackle the other fractals this week and report on them next time. I already worked out the basic algorithms in my previous Elixir app, so it should just be a matter of adapting them for my new system. Just.
As I mentioned in a previous episode, I write the output image in the PPM format. It’s a simple RGB format written in ASCII. See my PPM
module and its specs for more details. ↩
POV—the Persistence of Vision Raytracer—is a pretty impressive, open source raytracer. Or at least it was. It’s been a while since I played with it. ↩