<linkrel="alternate"lang="fr"xml:lang="fr"title="Un example progressif avec Haskell"type="text/html"hreflang="fr"href="/Scratch/fr/blog/Haskell-OpenGL-Mandelbrot/"/>
<p><imgalt="The B in Benoît B. Mandelbrot stand for Benoît B. Mandelbrot"src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/BenoitBMandelbrot.jpg"/></p>
<divclass="intro">
<p><spanclass="sc"><abbrtitle="Too long; didn't read">tl;dr</abbr>: </span> A progressive Haskell example.
A Mandelbrot set extended in 3D, rendered using OpenGL and coded with Haskell.
In the end the code will be very clean.
The significant stuff will be in a pure functional bubble.
The display details will be put in an external module playing the role of a wrapper.
Imperative language could also benefit from this functional organization.</p>
<blockquote>
<center><hrstyle="width:30%;float:left;border-color:#CCCCD0;margin-top:1em"/><spanclass="sc"><b>Table of Content</b></span><hrstyle="width:30%;float:right;border-color:#CCCCD0;margin-top:1em"/></center>
<ulid="markdown-toc">
<li><ahref="#introduction">Introduction</a></li>
<li><ahref="#first-version">First version</a><ul>
<li><ahref="#lets-play-the-song-of-our-people">Let’s play the song of our people</a></li>
<li><ahref="#let-us-start">Let us start</a></li>
</ul>
</li>
<li><ahref="#only-the-edges">Only the edges</a></li>
<li><ahref="#d-mandelbrot">3D Mandelbrot?</a><ul>
<li><ahref="#from-2d-to-3d">From 2D to 3D</a></li>
<li><ahref="#the-3d-mandelbrot">The 3D Mandelbrot</a></li>
<figure><imgalt="Another detail of the Mandelbulb"src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/3DMandelbulbDetail2.png"/><figcaption>Another detail of the Mandelbulb</figcaption></figure>
<p>And you can see the intermediate steps to reach this goal:</p>
<p><imgalt="The parts of the article"src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/HGL_Plan.png"/></p>
<p>From the 2<sup>nd</sup> section to the 4<sup>th</sup> it will be <em>dirtier</em> and <em>dirtier</em>.
We start cleaning the code at the 5<sup>th</sup> section.</p>
<hr/>
<p><ahref="code/01_Introduction/hglmandel.lhs"class="cut">Download the source code of this section → 01_Introduction/<strong>hglmandel.lhs</strong></a></p>
<h2id="first-version">First version</h2>
<p>We can consider two parts.
The first being mostly some boilerplate<supid="fnref:011"><ahref="#fn:011"rel="footnote">2</a></sup>.
And the second part more focused on OpenGL and content.</p>
<h3id="lets-play-the-song-of-our-people">Let’s play the song of our people</h3>
<p>Of course, instead of trying to test the real limit, we just make a test after a finite number of occurrences.</p>
<divclass="codehighlight">
<pre><codeclass="haskell">f :: Complex -> Complex -> Int -> Int
f c z 0 = 0
f c z n = if (magnitude z > 2 )
then n
else f c ((z*z)+c) (n-1)
</code></pre>
</div>
<p>Well, if you download this file (look at the bottom of this section), compile it and run it this is the result:</p>
<p><imgalt="The mandelbrot set version 1"src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/hglmandel_v01.png"/></p>
<p>A first very interesting property of this program is that the computation for all the points is done only once.
It is a bit long before the first image appears, but if you resize the window, it updates instantaneously.
This property is a direct consequence of purity.
If you look closely, you see that <code>allPoints</code> is a pure list.
Therefore, calling <code>allPoints</code> will always render the same result and Haskell is clever enough to use this property.
While Haskell doesn’t garbage collect <code>allPoints</code> the result is reused for free.
We did not specified this value should be saved for later use.
It is saved for us.</p>
<p>See what occurs if we make the window bigger:</p>
<p><imgalt="The mandelbrot too wide, black lines and columns"src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/hglmandel_v01_too_wide.png"/></p>
<p>We see some black lines because we have drawn less point than there is on the surface.
We can repair this by drawing little squares instead of just points.
But, instead we will do something a bit different and unusual.</p>
<p><ahref="code/01_Introduction/hglmandel.lhs"class="cut">Download the source code of this section → 01_Introduction/<strong>hglmandel.lhs</strong></a></p>
<hr/>
<p><ahref="code/02_Edges/HGLMandelEdge.lhs"class="cut">Download the source code of this section → 02_Edges/<strong>HGLMandelEdge.lhs</strong></a></p>
<p>No rocket science here. See the result now:</p>
<p><imgalt="The edges of the mandelbrot set"src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/HGLMandelEdges.png"/></p>
<divstyle="display:none">
<divclass="codehighlight">
<pre><codeclass="haskell">colorFromValue n =
let
t :: Int -> GLfloat
t i = 0.5 + 0.5*cos( fromIntegral i / 10 )
in
Color3 (t n) (t (n+5)) (t (n+10))
</code></pre>
</div>
<divclass="codehighlight">
<pre><codeclass="haskell">mandel x y =
let r = 2.0 * x / width
i = 2.0 * y / height
in
f (complex r i) 0 64
</code></pre>
</div>
<divclass="codehighlight">
<pre><codeclass="haskell">f :: Complex -> Complex -> Int -> Int
f c z 0 = 0
f c z n = if (magnitude z > 2 )
then n
else f c ((z*z)+c) (n-1)
</code></pre>
</div>
</div>
<p><ahref="code/02_Edges/HGLMandelEdge.lhs"class="cut">Download the source code of this section → 02_Edges/<strong>HGLMandelEdge.lhs</strong></a></p>
<hr/>
<p><ahref="code/03_Mandelbulb/Mandelbulb.lhs"class="cut">Download the source code of this section → 03_Mandelbulb/<strong>Mandelbulb.lhs</strong></a></p>
<h2id="d-mandelbrot">3D Mandelbrot?</h2>
<p>Now we will we extend to a third dimension.
But, there is no 3D equivalent to complex.
In fact, the only extension known are quaternions (in 4D).
As I know almost nothing about quaternions, I will use some extended complex,
instead of using a 3D projection of quaternions.
I am pretty sure this construction is not useful for numbers.
But it will be enough for us to create something that look nice.</p>
<p>This section is quite long, but don’t be afraid,
most of the code is some OpenGL boilerplate.
If you just want to skim this section,
here is a high level representation:</p>
<blockquote>
<ul>
<li>
<p>OpenGL Boilerplate</p>
<ul>
<li>set some IORef (understand variables) for states </li>
We only changed from `Complex` to `ExtComplex` of the main `f` function.
<divclass="codehighlight">
<pre><codeclass="haskell">f :: ExtComplex -> ExtComplex -> Int -> Int
f c z 0 = 0
f c z n = if (magnitude z > 2 )
then n
else f c ((z*z)+c) (n-1)
</code></pre>
</div>
</div>
<p>We simply add a new dimension to the <code>mandel</code> function
and change the type signature of <code>f</code> from <code>Complex</code> to <code>ExtComplex</code>.</p>
<divclass="codehighlight">
<pre><codeclass="haskell">mandel x y z =
let r = 2.0 * x / width
i = 2.0 * y / height
s = 2.0 * z / deep
in
f (extcomplex r i s) 0 64
</code></pre>
</div>
<p>Here is the result:</p>
<p><imgalt="A 3D mandelbrot like"src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/mandelbrot_3D.png"/></p>
<p><ahref="code/03_Mandelbulb/Mandelbulb.lhs"class="cut">Download the source code of this section → 03_Mandelbulb/<strong>Mandelbulb.lhs</strong></a></p>
<hr/>
<p><ahref="code/04_Mandelbulb/Mandelbulb.lhs"class="cut">Download the source code of this section → 04_Mandelbulb/<strong>Mandelbulb.lhs</strong></a></p>
ymandel x y z = mandel (2*x/width) (2*y/height) (2*z/deep) 64
</code></pre>
</div>
<p>This code is cleaner but many things doesn’t feel right.
First, all the user interaction code is outside our main file.
I feel it is okay to hide the detail for the rendering.
But I would have preferred to control the user actions.</p>
<p>On the other hand, we continue to handle a lot rendering details.
For example, we provide ordered vertices.</p>
<p><ahref="code/04_Mandelbulb/Mandelbulb.lhs"class="cut">Download the source code of this section → 04_Mandelbulb/<strong>Mandelbulb.lhs</strong></a></p>
<hr/>
<p><ahref="code/05_Mandelbulb/Mandelbulb.lhs"class="cut">Download the source code of this section → 05_Mandelbulb/<strong>Mandelbulb.lhs</strong></a></p>
In particular the <code>mainLoop</code> function is a direct link to the C library (FFI).
This function is clearly far from the functional paradigm.
Could we make this better?
We will have two choices: </p>
<ul>
<li>create our own <code>mainLoop</code> function to make it more functional.</li>
<li>deal with the imperative nature of the GLUT <code>mainLoop</code> function.</li>
</ul>
<p>As one of the goal of this article is to understand how to deal with existing libraries and particularly the one coming from imperative languages, we will continue to use the <code>mainLoop</code> function.</p>
</li>
<li>
<p>Our main problem come from user interaction.
If you ask “the Internet”,
about how to deal with user interaction with a functional paradigm,
the main answer is to use <em>functional reactive programming</em> (FRP).
I won’t use FRP in this article.
Instead, I’ll use a simpler while less effective way to deal with user interaction.
But The method I’ll use will be as pure and functional as possible.</p>
</li>
</ol>
<p>Here is how I imagine things should go.
First, what the main loop should look like if we could make our own:</p>
ymandel :: Point -> Point -> Point -> Point
ymandel x y z = fromIntegral (mandel x y z 64) / 64
</code></pre>
</div>
<p>I won’t explain how the magic occurs here.
If you are interested, just read the file <ahref="code/05_Mandelbulb/YGL.hs"><code>YGL.hs</code></a>.
It is commented a lot.</p>
<ul>
<li><ahref="code/05_Mandelbulb/YGL.hs"><code>YGL.hs</code></a>, the 3D rendering framework</li>
<li><ahref="code/05_Mandelbulb/Mandel.hs"><code>Mandel</code></a>, the mandel function</li>
<li><ahref="code/05_Mandelbulb/ExtComplex.hs"><code>ExtComplex</code></a>, the extended complexes</li>
</ul>
<p><ahref="code/05_Mandelbulb/Mandelbulb.lhs"class="cut">Download the source code of this section → 05_Mandelbulb/<strong>Mandelbulb.lhs</strong></a></p>
<hr/>
<p><ahref="code/06_Mandelbulb/Mandelbulb.lhs"class="cut">Download the source code of this section → 06_Mandelbulb/<strong>Mandelbulb.lhs</strong></a></p>
<h2id="optimization">Optimization</h2>
<p>Our code architecture feel very clean.
All the meaningful code is in our main file and all display details are
externalized.
If you read the code of <code>YGL.hs</code>, you’ll see I didn’t made everything perfect.
For example, I didn’t finished the code of the lights.
But I believe it is a good first step and it will be easy to go further.
Unfortunately the program of the preceding session is extremely slow.
We compute the Mandelbulb for each frame now.</p>
<p>Before our program structure was:</p>
<pre><codeclass="no-highlight">Constant Function -> Constant List of Triangles -> Display
</code></pre>
<p>Now we have </p>
<pre><codeclass="no-highlight">Main loop -> World -> Function -> List of Objects -> Atoms -> Display
</code></pre>
<p>The World state could change.
The compiler can no more optimize the computation for us.
We have to manually explain when to redraw the shape.</p>
<p>To optimize we must do some things in a lower level.
Mostly the program remains the same,
but it will provide the list of atoms directly.</p>
<divstyle="display:none">
<divclass="codehighlight">
<pre><codeclass="haskell">import YGL -- Most the OpenGL Boilerplate
import Mandel -- The 3D Mandelbrot maths
-- Centralize all user input interaction
inputActionMap :: InputMap World
inputActionMap = inputMapFromList [
(Press ' ' , switchRotation)
,(Press 'k' , rotate xdir 5)
,(Press 'i' , rotate xdir (-5))
,(Press 'j' , rotate ydir 5)
,(Press 'l' , rotate ydir (-5))
,(Press 'o' , rotate zdir 5)
,(Press 'u' , rotate zdir (-5))
,(Press 'f' , translate xdir 0.1)
,(Press 's' , translate xdir (-0.1))
,(Press 'e' , translate ydir 0.1)
,(Press 'd' , translate ydir (-0.1))
,(Press 'z' , translate zdir 0.1)
,(Press 'r' , translate zdir (-0.1))
,(Press '+' , zoom 1.1)
,(Press '-' , zoom (1/1.1))
,(Press 'h' , resize 2.0)
,(Press 'g' , resize (1/2.0))
]
</code></pre>
</div>
</div>
<divclass="codehighlight">
<pre><codeclass="haskell">data World = World {
angle :: Point3D
, anglePerSec :: Scalar
, scale :: Scalar
, position :: Point3D
, box :: Box3D
, told :: Time
-- We replace shape by cache
, cache :: [YObject]
}
</code></pre>
</div>
<divclass="codehighlight">
<pre><codeclass="haskell">instance DisplayableWorld World where
winTitle _ = "The YGL Mandelbulb"
camera w = Camera {
camPos = position w,
camDir = angle w,
camZoom = scale w }
-- We update our objects instanciation
objects = cache
</code></pre>
</div>
<divstyle="display:none">
<divclass="codehighlight">
<pre><codeclass="haskell">xdir :: Point3D
xdir = makePoint3D (1,0,0)
ydir :: Point3D
ydir = makePoint3D (0,1,0)
zdir :: Point3D
zdir = makePoint3D (0,0,1)
rotate :: Point3D -> Scalar -> World -> World
rotate dir angleValue world =
world {
angle = angle world + (angleValue -*< dir) }
switchRotation :: World -> World
switchRotation world =
world {
anglePerSec = if anglePerSec world > 0 then 0 else 5.0 }
translate :: Point3D -> Scalar -> World -> World
translate dir len world =
world {
position = position world + (len -*< dir) }
zoom :: Scalar -> World -> World
zoom z world = world {
scale = z * scale world }
</code></pre>
</div>
<divclass="codehighlight">
<pre><codeclass="haskell">main :: IO ()
main = yMainLoop inputActionMap idleAction initialWorld
</code></pre>
</div>
</div>
<p>Our initial world state is slightly changed:</p>
<divclass="codehighlight">
<pre><codeclass="haskell">-- We initialize the world state
ymandel :: Point -> Point -> Point -> Point
ymandel x y z = fromIntegral (mandel x y z 64) / 64
</code></pre>
</div>
</div>
<p>And you can also consider minor changes in the <code>YGL.hs</code> source file.</p>
<ul>
<li><ahref="code/06_Mandelbulb/YGL.hs"><code>YGL.hs</code></a>, the 3D rendering framework</li>
<li><ahref="code/06_Mandelbulb/Mandel.hs"><code>Mandel</code></a>, the mandel function</li>
<li><ahref="code/06_Mandelbulb/ExtComplex.hs"><code>ExtComplex</code></a>, the extended complexes</li>
</ul>
<p><ahref="code/06_Mandelbulb/Mandelbulb.lhs"class="cut">Download the source code of this section → 06_Mandelbulb/<strong>Mandelbulb.lhs</strong></a></p>
<h2id="conclusion">Conclusion</h2>
<p>As we can use imperative style in a functional language,
know you can use functional style in imperative languages.
This article exposed a way to organize some code in a functional way.
I’d like to stress the usage of Haskell made it very simple to achieve this.</p>
<p>Once you are used to pure functional style,
it is hard not to see all advantages it offers.</p>
<p>The code in the two last sections is completely pure and functional.
Furthermore I don’t use <code>GLfloat</code>, <code>Color3</code> or any other OpenGL type.
If I want to use another library in the future,
I would be able to keep all the pure code and simply update the YGL module.</p>
<p>The <code>YGL</code> module can be seen as a “wrapper” around 3D display and user interaction.
It is a clean separator between the imperative paradigm and functional paradigm.</p>
<p>If you want to go further, it shouldn’t be hard to add parallelism.
This should be easy mainly because most of the visible code is pure.
Such an optimization would have been harder by using directly the OpenGL library.</p>
<p>You should also want to make a more precise object. Because, the Mandelbulb is
clearly not convex. But a precise rendering might be very long from
O(n².log(n)) to O(n³).</p>
<hr/><divclass="footnotes">
<ol>
<liid="fn:001">
<p>Unfortunately, I couldn’t make this program to work on my Mac. More precisely, I couldn’t make the <ahref="http://openil.sourceforge.net/">DevIL</a> library work on Mac to output the image. Yes I have done a <code>brew install libdevil</code>. But even a minimal program who simply write some <code>jpg</code> didn’t worked. I tried both with <code>Haskell</code> and <code>C</code>.<ahref="#fnref:001"rel="reference">↩</a></p>
</li>
<liid="fn:011">
<p>Generally in Haskell you need to declare a lot of import lines.
This is something I find annoying.
In particular, it should be possible to create a special file, Import.hs
which make all the necessary import for you, as you generally need them all.
I understand why this is cleaner to force the programmer not to do so,
but, each time I do a copy/paste, I feel something is wrong.
I believe this concern can be generalized to the lack of namespace in Haskell.<ahref="#fnref:011"rel="reference">↩</a></p>