Modified .gitignore (shoud work out of heroku)

This commit is contained in:
Yann Esposito (Yogsototh) 2012-06-12 16:17:25 +02:00
parent cf1b30a2d4
commit cc0ff59931
38 changed files with 3708 additions and 1387 deletions

8
.gitignore vendored
View file

@ -7,3 +7,11 @@ tmp/
recupen.pl recupen.pl
recupfr.pl recupfr.pl
.sass-cache .sass-cache
# output/Scratch/**/index.html
output/Scratch/*/index.html
output/Scratch/*/*/index.html
output/Scratch/*/*/*/index.html
output/Scratch/*/*/*/*/index.html
output/Scratch/*/*/*/*/*/index.html
output/Scratch/*/*/*/*/*/*/index.html

View file

@ -13,11 +13,11 @@ tags:
- functional - functional
- tutorial - tutorial
----- -----
blogimage("HGL_Plan.png","The plan in image") blogimage("BenoitBMandelbrot.jpg","The B in Benoît B. Mandelbrot stand for Benoît B. Mandelbrot")
begindiv(intro) begindiv(intro)
%tldr A progressive real world example. %tldr You will see how to go from theory to a real application using Haskell.
> <center><hr style="width:30%;float:left;border-color:#CCCCD0;margin-top:1em"/><span class="sc"><b>Table of Content</b></span><hr style="width:30%;float:right;border-color:#CCCCD0;margin-top:1em"/></center> > <center><hr style="width:30%;float:left;border-color:#CCCCD0;margin-top:1em"/><span class="sc"><b>Table of Content</b></span><hr style="width:30%;float:right;border-color:#CCCCD0;margin-top:1em"/></center>
@ -30,46 +30,63 @@ enddiv
## Introduction ## Introduction
TODO: write something nice after reading. I wanted to go further than my
[preceding article](/Scratch/en/blog/Haskell-the-Hard-Way/) in which I introduced Haskell.
Steps: Instead of arguing that Haskell is better, because it is functional and "Functional Programming! Yeah!", I'll give an example of what benefit
functional programming can provide.
This article is more about functional paradigm than functional language.
The code organization can be used in most imperative language.
As Haskell is designed for functional paradigm, it is easier to talk about functional paradigm using it.
In reality, in the firsts sections I use an imperative paradigm.
As you can use functional paradigm in imperative language,
you can also use imperative paradigm in functional languages.
1. Mandelbrot set with Haskell OpenGL This article is about creating a useful program.
2. Mandelbrot edges It can interact with the user in real time.
3. 3D Mandelbrot because its fun It uses OpenGL, a library with imperative programming foundations.
4. Clean the code from full impure and imperative to purer and purer. But the final code will be quite clean.
5. Refactor the code to separate nicely important parts Most of the code will remain in the pure part (no `IO`).
6. Improve efficiency
I believe the main audience for this article are:
- Haskell programmer looking for an OpengGL tutorial.
- People interested in program organization (programming language agnostic).
- Fractal lovers and in particular 3D fractal.
- Game programmers (any language)
I wanted to talk about something cool.
For example I always wanted to make a Mandelbrot set explorer.
I had written a [command line Mandelbrot set generator in Haskell](http://github.com/yogsototh/mandelbrot.git).
The cool part of this utility is that it use all the cores to make the computation (it uses the `repa` package)[^001].
[^001]: Unfortunately, I couldn't make this program to work on my Mac. More precisely, I couldn't make the [DevIL](http://openil.sourceforge.net/) library work on Mac to output the image. Yes I have done a `brew install libdevil`. But even a minimal program who simply write some `jpg` didn't worked.
This time, we will display the Mandelbrot set extended in 3D using OpenGL and Haskell.
You will be able to move it using your keyboard.
This object is a Mandelbrot set in the plan (z=0),
and something nice to see in 3D.
Here is what you'll end with:
blogimage("GoldenMandelbulb.png","A golden mandelbulb")
And here are the intermediate steps:
blogimage("HGL_Plan.png","The parts of the article")
From 1 to 3 it will be _dirtier_ and _dirtier_. From 1 to 3 it will be _dirtier_ and _dirtier_.
At 4, we will make some order in this mess! We start cleaning everything at the 4th part.
Hopefuly for the best!
One of the goal of this article is to show some good properties of Haskell. <hr/><a href="code/01_Introduction/hglmandel.lhs" class="cut">Download the source code of this section → 01_Introduction/<strong>hglmandel.lhs</strong></a>
In particular, how to make some real world application with a pure functional language.
I know drawing a simple mandelbrot set isn't a "real world" application.
But the idea is not to show you a real world application which would be hard to follows, but to give you a way to pass from the pure mindset to some real world application.
To this, I will show you how should progress an application.
It is not something easy to show.
This is why, I preferred work with a program that generate some image.
In a real world application, the first constraint would be to work with some framework.
And generally an imperative one.
Also, the imperative nature of OpenGL make it the perfect choice for an example.
<hr/><a href="code/01_Introduction/hglmandel.lhs" class="cut">01_Introduction/<strong>hglmandel.lhs</strong></a>
## First version ## First version
We can consider two parts. We can consider two parts.
The first being mostly some boilerplate[^1]. The first being mostly some boilerplate[^011].
The second part, contain more interesting stuff. And the second part more focused on OpenGL and content.
Even in this part, there are some necessary boilerplate.
But it is due to the OpenGL library this time.
[^1]: Generally in Haskell you need to declare a lot of import lines. [^011]: Generally in Haskell you need to declare a lot of import lines.
This is something I find annoying. This is something I find annoying.
In particular, it should be possible to create a special file, Import.hs 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. which make all the necessary import for you, as you generally need them all.
@ -126,9 +143,6 @@ magnitude = real.abs
### Let us start ### Let us start
Well, up until here we didn't made something useful.
Just a lot of boilerplate and default value.
Sorry but it is not completely the end.
We start by giving the main architecture of our program: We start by giving the main architecture of our program:
<div class="codehighlight"> <div class="codehighlight">
@ -149,7 +163,8 @@ main = do
</code> </code>
</div> </div>
The only interesting part is we declared that the function `display` will be used to render the graphics: Mainly, we initialize our OpenGL application.
We declared that the function `display` will be used to render the graphics:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -161,12 +176,12 @@ display = do
</code> </code>
</div> </div>
Also here, there is only one interesting part, Also here, there is only one interesting line;
the draw will occurs in the function `drawMandelbrot`. the draw will occur in the function `drawMandelbrot`.
Now we must speak a bit about how OpenGL works. This function will provide a list of draw actions.
We said that OpenGL is imperative by design. Remember that OpenGL is imperative by design.
In fact, you must write the list of actions in the right order. Then, one of the consequence is you must write the actions in the right order.
No easy parallel drawing here. No easy parallel drawing here.
Here is the function which will render something on the screen: Here is the function which will render something on the screen:
@ -199,8 +214,8 @@ drawMandelbrot =
~~~ ~~~
We also need some kind of global variables. We also need some kind of global variables.
In fact, global variable are a proof of some bad design. In fact, global variable are a proof of a design problem.
But remember it is our first try: We will get rid of them later.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -235,7 +250,7 @@ colorFromValue n =
</code> </code>
</div> </div>
And now the mandel function. And now the `mandel` function.
Given two coordinates in pixels, it returns some integer value: Given two coordinates in pixels, it returns some integer value:
<div class="codehighlight"> <div class="codehighlight">
@ -248,8 +263,8 @@ mandel x y =
</code> </code>
</div> </div>
It uses the main mandelbrot function for each complex \\(c\\). It uses the main Mandelbrot function for each complex \\(c\\).
The mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity. The Mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity.
Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\) Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\)
@ -271,15 +286,15 @@ f c z n = if (magnitude z > 2 )
</code> </code>
</div> </div>
Well, if you download this lhs file, compile it and run it this is the result: Well, if you download this file (look at the bottom of this section), compile it and run it this is the result:
blogimage("hglmandel_v01.png","The mandelbrot set version 1") blogimage("hglmandel_v01.png","The mandelbrot set version 1")
A first very interesting property of this program is that the computation for all the points is done only once. A first very interesting property of this program is that the computation for all the points is done only once.
The proof is that it might be a bit long before a first image appears, but if you resize the window, it updates instantaneously. 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. This property is a direct consequence of purity.
If you look closely, you see that `allPoints` is a pure list. If you look closely, you see that `allPoints` is a pure list.
Therefore, calling `allPoints` will always render the same result. Therefore, calling `allPoints` will always render the same result and Haskell is clever enough to use this property.
While Haskell doesn't garbage collect `allPoints` the result is reused for free. While Haskell doesn't garbage collect `allPoints` the result is reused for free.
We didn't specified this value should be saved for later use. We didn't specified this value should be saved for later use.
It is saved for us. It is saved for us.
@ -288,14 +303,13 @@ See what occurs if we make the window bigger:
blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns") blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns")
Yep, we see some black lines. We see some black lines because we drawn less point than there is on the surface.
Why? Simply because we drawn less point than there is on the surface.
We can repair this by drawing little squares instead of just points. We can repair this by drawing little squares instead of just points.
But, instead we will do something a bit different and unusual. But, instead we will do something a bit different and unusual.
<a href="code/01_Introduction/hglmandel.lhs" class="cut">01_Introduction/<strong>hglmandel.lhs</strong> </a> <a href="code/01_Introduction/hglmandel.lhs" class="cut">Download the source code of this section → 01_Introduction/<strong>hglmandel.lhs</strong> </a>
<hr/><a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">02_Edges/<strong>HGLMandelEdge.lhs</strong></a> <hr/><a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">Download the source code of this section → 02_Edges/<strong>HGLMandelEdge.lhs</strong></a>
## Only the edges ## Only the edges
@ -353,6 +367,10 @@ height = 320 :: GLfloat
</div> </div>
This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set. This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set.
The method I use is a rough approximation.
I consider the Mandelbrot set to be almost convex.
The result will be good enough.
We change slightly the drawMandelbrot function. We change slightly the drawMandelbrot function.
We replace the `Points` by `LineLoop` We replace the `Points` by `LineLoop`
@ -383,21 +401,23 @@ allPoints = positivePoints ++
</div> </div>
We only need to compute the positive point. We only need to compute the positive point.
The mandelbrot set is symetric on the abscisse axis. The Mandelbrot set is symmetric on the abscisse axis.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)] positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)]
positivePoints = do positivePoints = do
x <- [-width..width] x <- [-width..width]
let y = findMaxOrdFor (mandel x) 0 height 10 -- log height let y = findMaxOrdFor (mandel x) 0 height (log2 height)
if y < 1 -- We don't draw point in the absciss if y < 1 -- We don't draw point in the absciss
then [] then []
else return (x/width,y/height,colorFromValue $ mandel x y) else return (x/width,y/height,colorFromValue $ mandel x y)
where
log2 n = floor ((log n) / log 2)
</code> </code>
</div> </div>
This function is interresting. This function is interesting.
For those not used to the list monad here is a natural language version of this function: For those not used to the list monad here is a natural language version of this function:
~~~ ~~~
@ -409,7 +429,7 @@ positivePoints =
~~~ ~~~
In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically. In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically.
To find the smallest number such that mandel x y > 0 we create a simple dichotomic search: To find the smallest number such that `mandel x y > 0` we use a simple dichotomy:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -422,9 +442,7 @@ findMaxOrdFor func minval maxval n =
</code> </code>
</div> </div>
No rocket science here. No rocket science here. See the result now:
I know, due to the fact the mandelbrot set is not convex this approach does some errors. But the approximation will be good enough.
See the result now:
blogimage("HGLMandelEdges.png","The edges of the mandelbrot set") blogimage("HGLMandelEdges.png","The edges of the mandelbrot set")
@ -463,27 +481,28 @@ f c z n = if (magnitude z > 2 )
</div> </div>
<a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">02_Edges/<strong>HGLMandelEdge.lhs</strong> </a> <a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">Download the source code of this section → 02_Edges/<strong>HGLMandelEdge.lhs</strong> </a>
<hr/><a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">03_Mandelbulb/<strong>Mandelbulb.lhs</strong></a> <hr/><a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 03_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
## 3D Mandelbrot? ## 3D Mandelbrot?
Why only draw the edge? Now we will we extend to a third dimension.
It is clearly not as nice as drawing the complete surface. But, there is no 3D equivalent to complex.
Yeah, I know, but, as we use OpenGL, why not show something in 3D. In fact, the only extension known are quaternions (in 4D).
As I know almost nothing about quaternions, I will use some extended complex,
But, complex number are only in 2D and there is no 3D equivalent to complex. instead of using a 3D projection of quaternions.
In fact, the only extension known are quaternions, 4D.
As I know almost nothing about quaternions, I will use some extended complex.
I am pretty sure this construction is not useful for numbers. I am pretty sure this construction is not useful for numbers.
But it will be enough for us to create something nice. But it will be enough for us to create something that look nice.
As there is a lot of code, I'll give a high level view to what occurs: This section is quite long, but don't be afraid,
most of the code is some OpenGL boilerplate.
For those you want to skim,
here is a high level representation:
> - OpenGL Boilerplate > - OpenGL Boilerplate
> >
> - set some IORef for states > - set some IORef (understand variables) for states
> - Drawing: > - Drawing:
> >
> - set doubleBuffer, handle depth, window size... > - set doubleBuffer, handle depth, window size...
@ -520,8 +539,8 @@ type ColoredPoint = (GLfloat,GLfloat,GLfloat,Color3 GLfloat)
</div> </div>
We declare a new type `ExtComplex` (for exttended complex). We declare a new type `ExtComplex` (for extended complex).
An extension of complex numbers: An extension of complex numbers with a third component:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -542,7 +561,17 @@ instance Num ExtComplex where
</div> </div>
The most important part is the new multiplication instance. The most important part is the new multiplication instance.
Modifying this formula will change radically the shape of this somehow 3D mandelbrot. Modifying this formula will change radically the shape of the result.
Here is the formula written in a more mathematical notation.
I called the third component of these extended complex _strange_.
$$ \mathrm{real} ((x,y,z) * (x',y',z')) = xx' - yy' - zz' $$
$$ \mathrm{im} ((x,y,z) * (x',y',z')) = xy' - yx' + zz' $$
$$ \mathrm{strange} ((x,y,z) * (x',y',z')) = xz' + zx' $$
Note how if `z=z'=0` then the multiplication is the same to the complex one.
<div style="display:none"> <div style="display:none">
@ -585,15 +614,14 @@ main = do
createWindow "3D HOpengGL Mandelbrot" createWindow "3D HOpengGL Mandelbrot"
-- We add some directives -- We add some directives
depthFunc $= Just Less depthFunc $= Just Less
-- matrixMode $= Projection
windowSize $= Size 500 500 windowSize $= Size 500 500
-- Some state variables (I know it feels BAD) -- Some state variables (I know it feels BAD)
angle <- newIORef ((35,0)::(GLfloat,GLfloat)) angle <- newIORef ((35,0)::(GLfloat,GLfloat))
zoom <- newIORef (2::GLfloat) zoom <- newIORef (2::GLfloat)
campos <- newIORef ((0.7,0)::(GLfloat,GLfloat)) campos <- newIORef ((0.7,0)::(GLfloat,GLfloat))
-- Action to call when waiting -- Function to call each frame
idleCallback $= Just idle idleCallback $= Just idle
-- We will use the keyboard -- Function to call when keyboard or mouse is used
keyboardMouseCallback $= keyboardMouseCallback $=
Just (keyboardMouse angle zoom campos) Just (keyboardMouse angle zoom campos)
-- Each time we will need to update the display -- Each time we will need to update the display
@ -605,7 +633,8 @@ main = do
</code> </code>
</div> </div>
The `idle` function necessary for animation. The `idle` is here to change the states.
There should never be any modification done in the `display` function.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -615,6 +644,9 @@ idle = postRedisplay Nothing
We introduce some helper function to manipulate We introduce some helper function to manipulate
standard `IORef`. standard `IORef`.
Mainly `modVar x f` is equivalent to the imperative `x:=f(x)`,
`modFst (x,y) (+1)` is equivalent to `(x,y) := (x+1,y)`
and `modSnd (x,y) (+1)` is equivalent to `(x,y) := (x,y+1)`
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -630,25 +662,29 @@ And we use them to code the function handling keyboard.
We will use the keys `hjkl` to rotate, We will use the keys `hjkl` to rotate,
`oi` to zoom and `sedf` to move. `oi` to zoom and `sedf` to move.
Also, hitting space will reset the view. Also, hitting space will reset the view.
Remember that `angle` and `campos` are pairs and `zoom` is a scalar.
Also note `(+0.5)` is the function `\x->x+0.5`
and `(-0.5)` is the number `-0.5` (yes I share your pain).
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
keyboardMouse angle zoom pos key state modifiers position = keyboardMouse angle zoom campos key state modifiers position =
kact angle zoom pos key state -- We won't use modifiers nor position
kact angle zoom campos key state
where where
-- reset view when hitting space -- reset view when hitting space
kact a z p (Char ' ') Down = do kact a z p (Char ' ') Down = do
a $= (0,0) a $= (0,0) -- angle
z $= 1 z $= 1 -- zoom
p $= (0,0) p $= (0,0) -- camera position
-- use of hjkl to rotate -- use of hjkl to rotate
kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5)) kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5))
kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5))) kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5)))
kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5)) kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5))
kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5))) kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5)))
-- use o and i to zoom -- use o and i to zoom
kact _ s _ (Char 'o') Down = modVar s (*1.1) kact _ z _ (Char 'o') Down = modVar z (*1.1)
kact _ s _ (Char 'i') Down = modVar s (*0.9) kact _ z _ (Char 'i') Down = modVar z (*0.9)
-- use sdfe to move the camera -- use sdfe to move the camera
kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1)) kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1))
kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1))) kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1)))
@ -659,9 +695,8 @@ keyboardMouse angle zoom pos key state modifiers position =
</code> </code>
</div> </div>
Now, we will show the object using the display function. Note `display` take some parameters this time.
Note, this time, display take some parameters. This function if full of boilerplate:
Mainly, this function if full of boilerplate:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -681,9 +716,11 @@ display angle zoom position = do
(xangle,yangle) <- get angle (xangle,yangle) <- get angle
rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat) rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat)
rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat) rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat)
-- Now that all transformation were made -- Now that all transformation were made
-- We create the object(s) -- We create the object(s)
preservingMatrix drawMandelbrot preservingMatrix drawMandelbrot
swapBuffers -- refresh screen swapBuffers -- refresh screen
</code> </code>
</div> </div>
@ -693,9 +730,9 @@ Mainly there are two parts: apply some transformations, draw the object.
### The 3D Mandelbrot ### The 3D Mandelbrot
Now, that we talked about the OpenGL part, let's talk about how we We have finished with the OpenGL section, let's talk about how we
generate the 3D points and colors. generate the 3D points and colors.
First, we will set the number of detatils to 180 pixels in the three dimensions. First, we will set the number of details to 200 pixels in the three dimensions.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -708,7 +745,8 @@ deep = nbDetails
This time, instead of just drawing some line or some group of points, This time, instead of just drawing some line or some group of points,
we will show triangles. we will show triangles.
The idea is that we should provide points three by three. The function `allPoints` will provide a multiple of three points.
Each three successive point representing the coordinate of each vertex of a triangle.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -723,14 +761,13 @@ drawMandelbrot = do
</code> </code>
</div> </div>
Now instead of providing only one point at a time, we will provide six ordered points. In fact, we will provide six ordered points.
These points will be used to draw two triangles. These points will be used to draw two triangles.
blogimage("triangles.png","Explain triangles") blogimage("triangles.png","Explain triangles")
Note in 3D the depth of the point is generally different.
The next function is a bit long. The next function is a bit long.
An approximative English version is: Here is an approximative English version:
~~~ ~~~
forall x from -width to width forall x from -width to width
@ -750,7 +787,8 @@ depthPoints = do
x <- [-width..width] x <- [-width..width]
y <- [-height..height] y <- [-height..height]
let let
depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep 7 depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep logdeep
logdeep = floor ((log deep) / log 2)
z1 = depthOf x y z1 = depthOf x y
z2 = depthOf (x+1) y z2 = depthOf (x+1) y
z3 = depthOf (x+1) (y+1) z3 = depthOf (x+1) (y+1)
@ -770,17 +808,18 @@ depthPoints = do
If you look at the function above, you see a lot of common patterns. If you look at the function above, you see a lot of common patterns.
Haskell is very efficient to make this better. Haskell is very efficient to make this better.
Here is a somehow less readable but more generic refactored function: Here is a harder to read but shorter and more generic rewritten function:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
depthPoints :: [ColoredPoint] depthPoints :: [ColoredPoint]
depthPoints = do depthPoints = do
x <- [-width..width] x <- [-width..width]
y <- [0..height] y <- [-height..height]
let let
neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)] neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)]
depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep 7 depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep logdeep
logdeep = floor ((log deep) / log 2)
-- zs are 3D points with found depth -- zs are 3D points with found depth
zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors
-- ts are 3D pixels + mandel value -- ts are 3D pixels + mandel value
@ -799,26 +838,21 @@ depthPoints = do
If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example. If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example.
Also, we didn't searched for negative values. Also, we didn't searched for negative values.
For simplicity, I mirror these values. This modified Mandelbrot is no more symmetric relatively to the plan `y=0`.
I haven't even tested if this modified mandelbrot is symetric relatively to the plan {(x,y,z)|z=0}. But it is symmetric relatively to the plan `z=0`.
Then I mirror these values.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
allPoints :: [ColoredPoint] allPoints :: [ColoredPoint]
allPoints = planPoints ++ map inverseDepth planPoints allPoints = planPoints ++ map inverseDepth planPoints
where where
planPoints = depthPoints ++ map inverseHeight depthPoints planPoints = depthPoints
inverseHeight (x,y,z,c) = (x,-y,z,c)
inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c) inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c)
</code> </code>
</div> </div>
I cheat by making these symmetry. The rest of the program is very close to the preceding one.
But it is faster and render a nice form.
For this tutorial it will be good enough.
Also, the dichotomic method I use is mostly right but false for some cases.
The rest of the program is very close to the preceeding one.
<div style="display:none"> <div style="display:none">
@ -860,7 +894,8 @@ f c z n = if (magnitude z > 2 )
</div> </div>
We simply add a new dimenstion to the mandel function. Also we simply need to change the type signature of the function `f` from `Complex` to `ExtComplex`. We simply add a new dimension to the `mandel` function
and change the type signature of `f` from `Complex` to `ExtComplex`.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -873,21 +908,19 @@ mandel x y z =
</code> </code>
</div> </div>
And here is the result (if you use 500 for `nbDetails`): Here is the result:
blogimage("mandelbrot_3D.png","A 3D mandelbrot like") blogimage("mandelbrot_3D.png","A 3D mandelbrot like")
This image is quite nice. <a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 03_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>
<a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">03_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <hr/><a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 04_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
<hr/><a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">04_Mandelbulb/<strong>Mandelbulb.lhs</strong></a> ## Naïve code cleaning
## Cleaning the code
The first thing to do is to separate the GLUT/OpenGL The first thing to do is to separate the GLUT/OpenGL
part from the computation of the shape. part from the computation of the shape.
Here is the cleaned version of the preceeding section. Here is the cleaned version of the preceding section.
Most boilerplate was put in external files. Most boilerplate was put in external files.
- [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering - [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering
@ -984,13 +1017,10 @@ But I would have preferred to control the user actions.
On the other hand, we continue to handle a lot rendering details. On the other hand, we continue to handle a lot rendering details.
For example, we provide ordered vertices. For example, we provide ordered vertices.
I feel, this should be externalized.
I would have preferred to make things a bit more general. <a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 04_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>
<a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">04_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <hr/><a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 05_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
<hr/><a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">05_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
## Functional organization? ## Functional organization?
@ -1012,7 +1042,7 @@ Some points:
Then here is how I imagine things should go. Then here is how I imagine things should go.
First, what the main loop should look like: First, what the main loop should look like:
<code class="haskell"> <code class="no-highlight">
functionalMainLoop = functionalMainLoop =
Read user inputs and provide a list of actions Read user inputs and provide a list of actions
Apply all actions to the World Apply all actions to the World
@ -1269,16 +1299,16 @@ This file is commented a lot.
- [`Mandel`](code/05_Mandelbulb/Mandel.hs), the mandel function - [`Mandel`](code/05_Mandelbulb/Mandel.hs), the mandel function
- [`ExtComplex`](code/05_Mandelbulb/ExtComplex.hs), the extended complexes - [`ExtComplex`](code/05_Mandelbulb/ExtComplex.hs), the extended complexes
<a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">05_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 05_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>
<hr/><a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">06_Mandelbulb/<strong>Mandelbulb.lhs</strong></a> <hr/><a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 06_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
## Optimization ## Optimization
All feel good from the architecture point of vue. All feel good from the architecture point of vue.
More precisely, the separation between rendering and world behavior is clear. More precisely, the separation between rendering and world behavior is clear.
But this is extremely slow now. But this is extremely slow now.
Because we compute the mandelbulb for each frame now. Because we compute the Mandelbulb for each frame now.
Before we had Before we had
@ -1308,7 +1338,8 @@ import Mandel -- The 3D Mandelbrot maths
-- Centralize all user input interaction -- Centralize all user input interaction
inputActionMap :: InputMap World inputActionMap :: InputMap World
inputActionMap = inputMapFromList [ inputActionMap = inputMapFromList [
(Press 'k' , rotate xdir 5) (Press ' ' , switch_rotation)
,(Press 'k' , rotate xdir 5)
,(Press 'i' , rotate xdir (-5)) ,(Press 'i' , rotate xdir (-5))
,(Press 'j' , rotate ydir 5) ,(Press 'j' , rotate ydir 5)
,(Press 'l' , rotate ydir (-5)) ,(Press 'l' , rotate ydir (-5))
@ -1322,8 +1353,8 @@ inputActionMap = inputMapFromList [
,(Press 'r' , translate zdir (-0.1)) ,(Press 'r' , translate zdir (-0.1))
,(Press '+' , zoom 1.1) ,(Press '+' , zoom 1.1)
,(Press '-' , zoom (1/1.1)) ,(Press '-' , zoom (1/1.1))
,(Press 'h' , resize 1.2) ,(Press 'h' , resize 2.0)
,(Press 'g' , resize (1/1.2)) ,(Press 'g' , resize (1/2.0))
] ]
</code> </code>
</div> </div>
@ -1334,6 +1365,7 @@ inputActionMap = inputMapFromList [
<code class="haskell"> <code class="haskell">
data World = World { data World = World {
angle :: Point3D angle :: Point3D
, anglePerSec :: Scalar
, scale :: Scalar , scale :: Scalar
, position :: Point3D , position :: Point3D
, box :: Box3D , box :: Box3D
@ -1373,6 +1405,11 @@ rotate dir angleValue world =
world { world {
angle = (angle world) + (angleValue -*< dir) } angle = (angle world) + (angleValue -*< dir) }
switch_rotation :: World -> World
switch_rotation world =
world {
anglePerSec = if anglePerSec world > 0 then 0 else 5.0 }
translate :: Point3D -> Scalar -> World -> World translate :: Point3D -> Scalar -> World -> World
translate dir len world = translate dir len world =
world { world {
@ -1403,11 +1440,12 @@ Our initial world state is slightly changed:
initialWorld :: World initialWorld :: World
initialWorld = World { initialWorld = World {
angle = makePoint3D (30,30,0) angle = makePoint3D (30,30,0)
, anglePerSec = 5.0
, position = makePoint3D (0,0,0) , position = makePoint3D (0,0,0)
, scale = 1.0 , scale = 1.0
, box = Box3D { minPoint = makePoint3D (-2,-2,-2) , box = Box3D { minPoint = makePoint3D (-2,-2,-2)
, maxPoint = makePoint3D (2,2,2) , maxPoint = makePoint3D (2,2,2)
, resolution = 0.02 } , resolution = 0.03 }
, told = 0 , told = 0
-- We declare cache directly this time -- We declare cache directly this time
, cache = objectFunctionFromWorld initialWorld , cache = objectFunctionFromWorld initialWorld
@ -1423,11 +1461,12 @@ This way instead of providing `XYFunc`, we provide directly a list of Atoms.
objectFunctionFromWorld :: World -> [YObject] objectFunctionFromWorld :: World -> [YObject]
objectFunctionFromWorld w = [Atoms atomList] objectFunctionFromWorld w = [Atoms atomList]
where atomListPositive = where atomListPositive =
getObject3DFromShapeFunction (shapeFunc (resolution (box w))) (box w) getObject3DFromShapeFunction
(shapeFunc (resolution (box w))) (box w)
atomList = atomListPositive ++ atomList = atomListPositive ++
map negativeTriangle atomListPositive map negativeTriangle atomListPositive
negativeTriangle (ColoredTriangle (p1,p2,p3,c)) = negativeTriangle (ColoredTriangle (p1,p2,p3,c)) =
ColoredTriangle (negz p1,negz p2,negz p3,c) ColoredTriangle (negz p1,negz p3,negz p2,c)
where negz (P (x,y,z)) = P (x,y,-z) where negz (P (x,y,z)) = P (x,y,-z)
</code> </code>
</div> </div>
@ -1460,10 +1499,18 @@ idleAction tnew world =
, told = tnew , told = tnew
} }
where where
anglePerSec = 5.0 delta = anglePerSec world * elapsed / 1000.0
delta = anglePerSec * elapsed / 1000.0
elapsed = fromIntegral (tnew - (told world)) elapsed = fromIntegral (tnew - (told world))
shapeFunc' :: Scalar -> Function3D
shapeFunc' res x y = if or [tmp u v>=0 | u<-[x,x+res], v<-[y,y+res]]
then Just (z,hexColor "#AD4")
else Nothing
where tmp x y = (x**2 + y**2)
protectSqrt t = if t<0 then 0 else sqrt t
z = sqrt (a**2 - (c - protectSqrt(tmp x y))**2)
a = 0.2
c = 0.5
shapeFunc :: Scalar -> Function3D shapeFunc :: Scalar -> Function3D
shapeFunc res x y = shapeFunc res x y =
let let
@ -1472,13 +1519,13 @@ shapeFunc res x y =
if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 | if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 |
val <- [res], xeps <- [-val,val], yeps<-[-val,val]] val <- [res], xeps <- [-val,val], yeps<-[-val,val]]
then Nothing then Nothing
else Just (z,colorFromValue ((ymandel x y z) * 64)) else Just (z,colorFromValue 0)
colorFromValue :: Point -> Color colorFromValue :: Point -> Color
colorFromValue n = colorFromValue n =
let let
t :: Point -> Scalar t :: Point -> Scalar
t i = 0.7 + 0.3*cos( i / 10 ) t i = 0.0 + 0.5*cos( i /10 )
in in
makeColor (t n) (t (n+5)) (t (n+10)) makeColor (t n) (t (n+5)) (t (n+10))
@ -1502,5 +1549,5 @@ ymandel x y z = fromIntegral (mandel x y z 64) / 64
- [`Mandel`](code/06_Mandelbulb/Mandel.hs), the mandel function - [`Mandel`](code/06_Mandelbulb/Mandel.hs), the mandel function
- [`ExtComplex`](code/06_Mandelbulb/ExtComplex.hs), the extended complexes - [`ExtComplex`](code/06_Mandelbulb/ExtComplex.hs), the extended complexes
<a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">06_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 06_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>

View file

@ -13,11 +13,11 @@ tags:
- functional - functional
- tutorial - tutorial
----- -----
blogimage("HGL_Plan.png","The plan in image") blogimage("BenoitBMandelbrot.jpg","The B in Benoît B. Mandelbrot stand for Benoît B. Mandelbrot")
begindiv(intro) begindiv(intro)
%tlal Un exemple progressif de programmation avec Haskell. %tlal Un exemple progressif d'utilisation d'Haskell.
> <center><hr style="width:30%;float:left;border-color:#CCCCD0;margin-top:1em"/><span class="sc"><b>Table of Content</b></span><hr style="width:30%;float:right;border-color:#CCCCD0;margin-top:1em"/></center> > <center><hr style="width:30%;float:left;border-color:#CCCCD0;margin-top:1em"/><span class="sc"><b>Table of Content</b></span><hr style="width:30%;float:right;border-color:#CCCCD0;margin-top:1em"/></center>
@ -30,46 +30,63 @@ enddiv
## Introduction ## Introduction
TODO: write something nice after reading. I wanted to go further than my
[preceding article](/Scratch/en/blog/Haskell-the-Hard-Way/) in which I introduced Haskell.
Steps: Instead of arguing that Haskell is better, because it is functional and "Functional Programming! Yeah!", I'll give an example of what benefit
functional programming can provide.
This article is more about functional paradigm than functional language.
The code organization can be used in most imperative language.
As Haskell is designed for functional paradigm, it is easier to talk about functional paradigm using it.
In reality, in the firsts sections I use an imperative paradigm.
As you can use functional paradigm in imperative language,
you can also use imperative paradigm in functional languages.
1. Mandelbrot set with Haskell OpenGL This article is about creating a useful program.
2. Mandelbrot edges It can interact with the user in real time.
3. 3D Mandelbrot because its fun It uses OpenGL, a library with imperative programming foundations.
4. Clean the code from full impure and imperative to purer and purer. But the final code will be quite clean.
5. Refactor the code to separate nicely important parts Most of the code will remain in the pure part (no `IO`).
6. Improve efficiency
I believe the main audience for this article are:
- Haskell programmer looking for an OpengGL tutorial.
- People interested in program organization (programming language agnostic).
- Fractal lovers and in particular 3D fractal.
- Game programmers (any language)
I wanted to talk about something cool.
For example I always wanted to make a Mandelbrot set explorer.
I had written a [command line Mandelbrot set generator in Haskell](http://github.com/yogsototh/mandelbrot.git).
The cool part of this utility is that it use all the cores to make the computation (it uses the `repa` package)[^001].
[^001]: Unfortunately, I couldn't make this program to work on my Mac. More precisely, I couldn't make the [DevIL](http://openil.sourceforge.net/) library work on Mac to output the image. Yes I have done a `brew install libdevil`. But even a minimal program who simply write some `jpg` didn't worked.
This time, we will display the Mandelbrot set extended in 3D using OpenGL and Haskell.
You will be able to move it using your keyboard.
This object is a Mandelbrot set in the plan (z=0),
and something nice to see in 3D.
Here is what you'll end with:
blogimage("GoldenMandelbulb.png","A golden mandelbulb")
And here are the intermediate steps:
blogimage("HGL_Plan.png","The parts of the article")
From 1 to 3 it will be _dirtier_ and _dirtier_. From 1 to 3 it will be _dirtier_ and _dirtier_.
At 4, we will make some order in this mess! We start cleaning everything at the 4th part.
Hopefuly for the best!
One of the goal of this article is to show some good properties of Haskell. <hr/><a href="code/01_Introduction/hglmandel.lhs" class="cut">Download the source code of this section → 01_Introduction/<strong>hglmandel.lhs</strong></a>
In particular, how to make some real world application with a pure functional language.
I know drawing a simple mandelbrot set isn't a "real world" application.
But the idea is not to show you a real world application which would be hard to follows, but to give you a way to pass from the pure mindset to some real world application.
To this, I will show you how should progress an application.
It is not something easy to show.
This is why, I preferred work with a program that generate some image.
In a real world application, the first constraint would be to work with some framework.
And generally an imperative one.
Also, the imperative nature of OpenGL make it the perfect choice for an example.
<hr/><a href="code/01_Introduction/hglmandel.lhs" class="cut">01_Introduction/<strong>hglmandel.lhs</strong></a>
## First version ## First version
We can consider two parts. We can consider two parts.
The first being mostly some boilerplate[^1]. The first being mostly some boilerplate[^011].
The second part, contain more interesting stuff. And the second part more focused on OpenGL and content.
Even in this part, there are some necessary boilerplate.
But it is due to the OpenGL library this time.
[^1]: Generally in Haskell you need to declare a lot of import lines. [^011]: Generally in Haskell you need to declare a lot of import lines.
This is something I find annoying. This is something I find annoying.
In particular, it should be possible to create a special file, Import.hs 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. which make all the necessary import for you, as you generally need them all.
@ -126,9 +143,6 @@ magnitude = real.abs
### Let us start ### Let us start
Well, up until here we didn't made something useful.
Just a lot of boilerplate and default value.
Sorry but it is not completely the end.
We start by giving the main architecture of our program: We start by giving the main architecture of our program:
<div class="codehighlight"> <div class="codehighlight">
@ -149,7 +163,8 @@ main = do
</code> </code>
</div> </div>
The only interesting part is we declared that the function `display` will be used to render the graphics: Mainly, we initialize our OpenGL application.
We declared that the function `display` will be used to render the graphics:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -161,12 +176,12 @@ display = do
</code> </code>
</div> </div>
Also here, there is only one interesting part, Also here, there is only one interesting line;
the draw will occurs in the function `drawMandelbrot`. the draw will occur in the function `drawMandelbrot`.
Now we must speak a bit about how OpenGL works. This function will provide a list of draw actions.
We said that OpenGL is imperative by design. Remember that OpenGL is imperative by design.
In fact, you must write the list of actions in the right order. Then, one of the consequence is you must write the actions in the right order.
No easy parallel drawing here. No easy parallel drawing here.
Here is the function which will render something on the screen: Here is the function which will render something on the screen:
@ -199,8 +214,8 @@ drawMandelbrot =
~~~ ~~~
We also need some kind of global variables. We also need some kind of global variables.
In fact, global variable are a proof of some bad design. In fact, global variable are a proof of a design problem.
But remember it is our first try: We will get rid of them later.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -235,7 +250,7 @@ colorFromValue n =
</code> </code>
</div> </div>
And now the mandel function. And now the `mandel` function.
Given two coordinates in pixels, it returns some integer value: Given two coordinates in pixels, it returns some integer value:
<div class="codehighlight"> <div class="codehighlight">
@ -248,8 +263,8 @@ mandel x y =
</code> </code>
</div> </div>
It uses the main mandelbrot function for each complex \\(c\\). It uses the main Mandelbrot function for each complex \\(c\\).
The mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity. The Mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity.
Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\) Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\)
@ -271,15 +286,15 @@ f c z n = if (magnitude z > 2 )
</code> </code>
</div> </div>
Well, if you download this lhs file, compile it and run it this is the result: Well, if you download this file (look at the bottom of this section), compile it and run it this is the result:
blogimage("hglmandel_v01.png","The mandelbrot set version 1") blogimage("hglmandel_v01.png","The mandelbrot set version 1")
A first very interesting property of this program is that the computation for all the points is done only once. A first very interesting property of this program is that the computation for all the points is done only once.
The proof is that it might be a bit long before a first image appears, but if you resize the window, it updates instantaneously. 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. This property is a direct consequence of purity.
If you look closely, you see that `allPoints` is a pure list. If you look closely, you see that `allPoints` is a pure list.
Therefore, calling `allPoints` will always render the same result. Therefore, calling `allPoints` will always render the same result and Haskell is clever enough to use this property.
While Haskell doesn't garbage collect `allPoints` the result is reused for free. While Haskell doesn't garbage collect `allPoints` the result is reused for free.
We didn't specified this value should be saved for later use. We didn't specified this value should be saved for later use.
It is saved for us. It is saved for us.
@ -288,14 +303,13 @@ See what occurs if we make the window bigger:
blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns") blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns")
Yep, we see some black lines. We see some black lines because we drawn less point than there is on the surface.
Why? Simply because we drawn less point than there is on the surface.
We can repair this by drawing little squares instead of just points. We can repair this by drawing little squares instead of just points.
But, instead we will do something a bit different and unusual. But, instead we will do something a bit different and unusual.
<a href="code/01_Introduction/hglmandel.lhs" class="cut">01_Introduction/<strong>hglmandel.lhs</strong> </a> <a href="code/01_Introduction/hglmandel.lhs" class="cut">Download the source code of this section → 01_Introduction/<strong>hglmandel.lhs</strong> </a>
<hr/><a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">02_Edges/<strong>HGLMandelEdge.lhs</strong></a> <hr/><a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">Download the source code of this section → 02_Edges/<strong>HGLMandelEdge.lhs</strong></a>
## Only the edges ## Only the edges
@ -353,6 +367,10 @@ height = 320 :: GLfloat
</div> </div>
This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set. This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set.
The method I use is a rough approximation.
I consider the Mandelbrot set to be almost convex.
The result will be good enough.
We change slightly the drawMandelbrot function. We change slightly the drawMandelbrot function.
We replace the `Points` by `LineLoop` We replace the `Points` by `LineLoop`
@ -383,21 +401,23 @@ allPoints = positivePoints ++
</div> </div>
We only need to compute the positive point. We only need to compute the positive point.
The mandelbrot set is symetric on the abscisse axis. The Mandelbrot set is symmetric on the abscisse axis.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)] positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)]
positivePoints = do positivePoints = do
x <- [-width..width] x <- [-width..width]
let y = findMaxOrdFor (mandel x) 0 height 10 -- log height let y = findMaxOrdFor (mandel x) 0 height (log2 height)
if y < 1 -- We don't draw point in the absciss if y < 1 -- We don't draw point in the absciss
then [] then []
else return (x/width,y/height,colorFromValue $ mandel x y) else return (x/width,y/height,colorFromValue $ mandel x y)
where
log2 n = floor ((log n) / log 2)
</code> </code>
</div> </div>
This function is interresting. This function is interesting.
For those not used to the list monad here is a natural language version of this function: For those not used to the list monad here is a natural language version of this function:
~~~ ~~~
@ -409,7 +429,7 @@ positivePoints =
~~~ ~~~
In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically. In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically.
To find the smallest number such that mandel x y > 0 we create a simple dichotomic search: To find the smallest number such that `mandel x y > 0` we use a simple dichotomy:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -422,9 +442,7 @@ findMaxOrdFor func minval maxval n =
</code> </code>
</div> </div>
No rocket science here. No rocket science here. See the result now:
I know, due to the fact the mandelbrot set is not convex this approach does some errors. But the approximation will be good enough.
See the result now:
blogimage("HGLMandelEdges.png","The edges of the mandelbrot set") blogimage("HGLMandelEdges.png","The edges of the mandelbrot set")
@ -463,27 +481,28 @@ f c z n = if (magnitude z > 2 )
</div> </div>
<a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">02_Edges/<strong>HGLMandelEdge.lhs</strong> </a> <a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">Download the source code of this section → 02_Edges/<strong>HGLMandelEdge.lhs</strong> </a>
<hr/><a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">03_Mandelbulb/<strong>Mandelbulb.lhs</strong></a> <hr/><a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 03_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
## 3D Mandelbrot? ## 3D Mandelbrot?
Why only draw the edge? Now we will we extend to a third dimension.
It is clearly not as nice as drawing the complete surface. But, there is no 3D equivalent to complex.
Yeah, I know, but, as we use OpenGL, why not show something in 3D. In fact, the only extension known are quaternions (in 4D).
As I know almost nothing about quaternions, I will use some extended complex,
But, complex number are only in 2D and there is no 3D equivalent to complex. instead of using a 3D projection of quaternions.
In fact, the only extension known are quaternions, 4D.
As I know almost nothing about quaternions, I will use some extended complex.
I am pretty sure this construction is not useful for numbers. I am pretty sure this construction is not useful for numbers.
But it will be enough for us to create something nice. But it will be enough for us to create something that look nice.
As there is a lot of code, I'll give a high level view to what occurs: This section is quite long, but don't be afraid,
most of the code is some OpenGL boilerplate.
For those you want to skim,
here is a high level representation:
> - OpenGL Boilerplate > - OpenGL Boilerplate
> >
> - set some IORef for states > - set some IORef (understand variables) for states
> - Drawing: > - Drawing:
> >
> - set doubleBuffer, handle depth, window size... > - set doubleBuffer, handle depth, window size...
@ -520,8 +539,8 @@ type ColoredPoint = (GLfloat,GLfloat,GLfloat,Color3 GLfloat)
</div> </div>
We declare a new type `ExtComplex` (for exttended complex). We declare a new type `ExtComplex` (for extended complex).
An extension of complex numbers: An extension of complex numbers with a third component:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -542,7 +561,17 @@ instance Num ExtComplex where
</div> </div>
The most important part is the new multiplication instance. The most important part is the new multiplication instance.
Modifying this formula will change radically the shape of this somehow 3D mandelbrot. Modifying this formula will change radically the shape of the result.
Here is the formula written in a more mathematical notation.
I called the third component of these extended complex _strange_.
$$ \mathrm{real} ((x,y,z) * (x',y',z')) = xx' - yy' - zz' $$
$$ \mathrm{im} ((x,y,z) * (x',y',z')) = xy' - yx' + zz' $$
$$ \mathrm{strange} ((x,y,z) * (x',y',z')) = xz' + zx' $$
Note how if `z=z'=0` then the multiplication is the same to the complex one.
<div style="display:none"> <div style="display:none">
@ -585,15 +614,14 @@ main = do
createWindow "3D HOpengGL Mandelbrot" createWindow "3D HOpengGL Mandelbrot"
-- We add some directives -- We add some directives
depthFunc $= Just Less depthFunc $= Just Less
-- matrixMode $= Projection
windowSize $= Size 500 500 windowSize $= Size 500 500
-- Some state variables (I know it feels BAD) -- Some state variables (I know it feels BAD)
angle <- newIORef ((35,0)::(GLfloat,GLfloat)) angle <- newIORef ((35,0)::(GLfloat,GLfloat))
zoom <- newIORef (2::GLfloat) zoom <- newIORef (2::GLfloat)
campos <- newIORef ((0.7,0)::(GLfloat,GLfloat)) campos <- newIORef ((0.7,0)::(GLfloat,GLfloat))
-- Action to call when waiting -- Function to call each frame
idleCallback $= Just idle idleCallback $= Just idle
-- We will use the keyboard -- Function to call when keyboard or mouse is used
keyboardMouseCallback $= keyboardMouseCallback $=
Just (keyboardMouse angle zoom campos) Just (keyboardMouse angle zoom campos)
-- Each time we will need to update the display -- Each time we will need to update the display
@ -605,7 +633,8 @@ main = do
</code> </code>
</div> </div>
The `idle` function necessary for animation. The `idle` is here to change the states.
There should never be any modification done in the `display` function.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -615,6 +644,9 @@ idle = postRedisplay Nothing
We introduce some helper function to manipulate We introduce some helper function to manipulate
standard `IORef`. standard `IORef`.
Mainly `modVar x f` is equivalent to the imperative `x:=f(x)`,
`modFst (x,y) (+1)` is equivalent to `(x,y) := (x+1,y)`
and `modSnd (x,y) (+1)` is equivalent to `(x,y) := (x,y+1)`
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -630,25 +662,29 @@ And we use them to code the function handling keyboard.
We will use the keys `hjkl` to rotate, We will use the keys `hjkl` to rotate,
`oi` to zoom and `sedf` to move. `oi` to zoom and `sedf` to move.
Also, hitting space will reset the view. Also, hitting space will reset the view.
Remember that `angle` and `campos` are pairs and `zoom` is a scalar.
Also note `(+0.5)` is the function `\x->x+0.5`
and `(-0.5)` is the number `-0.5` (yes I share your pain).
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
keyboardMouse angle zoom pos key state modifiers position = keyboardMouse angle zoom campos key state modifiers position =
kact angle zoom pos key state -- We won't use modifiers nor position
kact angle zoom campos key state
where where
-- reset view when hitting space -- reset view when hitting space
kact a z p (Char ' ') Down = do kact a z p (Char ' ') Down = do
a $= (0,0) a $= (0,0) -- angle
z $= 1 z $= 1 -- zoom
p $= (0,0) p $= (0,0) -- camera position
-- use of hjkl to rotate -- use of hjkl to rotate
kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5)) kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5))
kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5))) kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5)))
kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5)) kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5))
kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5))) kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5)))
-- use o and i to zoom -- use o and i to zoom
kact _ s _ (Char 'o') Down = modVar s (*1.1) kact _ z _ (Char 'o') Down = modVar z (*1.1)
kact _ s _ (Char 'i') Down = modVar s (*0.9) kact _ z _ (Char 'i') Down = modVar z (*0.9)
-- use sdfe to move the camera -- use sdfe to move the camera
kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1)) kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1))
kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1))) kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1)))
@ -659,9 +695,8 @@ keyboardMouse angle zoom pos key state modifiers position =
</code> </code>
</div> </div>
Now, we will show the object using the display function. Note `display` take some parameters this time.
Note, this time, display take some parameters. This function if full of boilerplate:
Mainly, this function if full of boilerplate:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -681,9 +716,11 @@ display angle zoom position = do
(xangle,yangle) <- get angle (xangle,yangle) <- get angle
rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat) rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat)
rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat) rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat)
-- Now that all transformation were made -- Now that all transformation were made
-- We create the object(s) -- We create the object(s)
preservingMatrix drawMandelbrot preservingMatrix drawMandelbrot
swapBuffers -- refresh screen swapBuffers -- refresh screen
</code> </code>
</div> </div>
@ -693,9 +730,9 @@ Mainly there are two parts: apply some transformations, draw the object.
### The 3D Mandelbrot ### The 3D Mandelbrot
Now, that we talked about the OpenGL part, let's talk about how we We have finished with the OpenGL section, let's talk about how we
generate the 3D points and colors. generate the 3D points and colors.
First, we will set the number of detatils to 180 pixels in the three dimensions. First, we will set the number of details to 200 pixels in the three dimensions.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -708,7 +745,8 @@ deep = nbDetails
This time, instead of just drawing some line or some group of points, This time, instead of just drawing some line or some group of points,
we will show triangles. we will show triangles.
The idea is that we should provide points three by three. The function `allPoints` will provide a multiple of three points.
Each three successive point representing the coordinate of each vertex of a triangle.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -723,14 +761,13 @@ drawMandelbrot = do
</code> </code>
</div> </div>
Now instead of providing only one point at a time, we will provide six ordered points. In fact, we will provide six ordered points.
These points will be used to draw two triangles. These points will be used to draw two triangles.
blogimage("triangles.png","Explain triangles") blogimage("triangles.png","Explain triangles")
Note in 3D the depth of the point is generally different.
The next function is a bit long. The next function is a bit long.
An approximative English version is: Here is an approximative English version:
~~~ ~~~
forall x from -width to width forall x from -width to width
@ -750,7 +787,8 @@ depthPoints = do
x <- [-width..width] x <- [-width..width]
y <- [-height..height] y <- [-height..height]
let let
depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep 7 depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep logdeep
logdeep = floor ((log deep) / log 2)
z1 = depthOf x y z1 = depthOf x y
z2 = depthOf (x+1) y z2 = depthOf (x+1) y
z3 = depthOf (x+1) (y+1) z3 = depthOf (x+1) (y+1)
@ -770,17 +808,18 @@ depthPoints = do
If you look at the function above, you see a lot of common patterns. If you look at the function above, you see a lot of common patterns.
Haskell is very efficient to make this better. Haskell is very efficient to make this better.
Here is a somehow less readable but more generic refactored function: Here is a harder to read but shorter and more generic rewritten function:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
depthPoints :: [ColoredPoint] depthPoints :: [ColoredPoint]
depthPoints = do depthPoints = do
x <- [-width..width] x <- [-width..width]
y <- [0..height] y <- [-height..height]
let let
neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)] neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)]
depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep 7 depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep logdeep
logdeep = floor ((log deep) / log 2)
-- zs are 3D points with found depth -- zs are 3D points with found depth
zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors
-- ts are 3D pixels + mandel value -- ts are 3D pixels + mandel value
@ -799,26 +838,21 @@ depthPoints = do
If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example. If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example.
Also, we didn't searched for negative values. Also, we didn't searched for negative values.
For simplicity, I mirror these values. This modified Mandelbrot is no more symmetric relatively to the plan `y=0`.
I haven't even tested if this modified mandelbrot is symetric relatively to the plan {(x,y,z)|z=0}. But it is symmetric relatively to the plan `z=0`.
Then I mirror these values.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
allPoints :: [ColoredPoint] allPoints :: [ColoredPoint]
allPoints = planPoints ++ map inverseDepth planPoints allPoints = planPoints ++ map inverseDepth planPoints
where where
planPoints = depthPoints ++ map inverseHeight depthPoints planPoints = depthPoints
inverseHeight (x,y,z,c) = (x,-y,z,c)
inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c) inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c)
</code> </code>
</div> </div>
I cheat by making these symmetry. The rest of the program is very close to the preceding one.
But it is faster and render a nice form.
For this tutorial it will be good enough.
Also, the dichotomic method I use is mostly right but false for some cases.
The rest of the program is very close to the preceeding one.
<div style="display:none"> <div style="display:none">
@ -860,7 +894,8 @@ f c z n = if (magnitude z > 2 )
</div> </div>
We simply add a new dimenstion to the mandel function. Also we simply need to change the type signature of the function `f` from `Complex` to `ExtComplex`. We simply add a new dimension to the `mandel` function
and change the type signature of `f` from `Complex` to `ExtComplex`.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -873,21 +908,19 @@ mandel x y z =
</code> </code>
</div> </div>
And here is the result (if you use 500 for `nbDetails`): Here is the result:
blogimage("mandelbrot_3D.png","A 3D mandelbrot like") blogimage("mandelbrot_3D.png","A 3D mandelbrot like")
This image is quite nice. <a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 03_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>
<a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">03_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <hr/><a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 04_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
<hr/><a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">04_Mandelbulb/<strong>Mandelbulb.lhs</strong></a> ## Naïve code cleaning
## Cleaning the code
The first thing to do is to separate the GLUT/OpenGL The first thing to do is to separate the GLUT/OpenGL
part from the computation of the shape. part from the computation of the shape.
Here is the cleaned version of the preceeding section. Here is the cleaned version of the preceding section.
Most boilerplate was put in external files. Most boilerplate was put in external files.
- [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering - [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering
@ -984,13 +1017,10 @@ But I would have preferred to control the user actions.
On the other hand, we continue to handle a lot rendering details. On the other hand, we continue to handle a lot rendering details.
For example, we provide ordered vertices. For example, we provide ordered vertices.
I feel, this should be externalized.
I would have preferred to make things a bit more general. <a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 04_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>
<a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">04_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <hr/><a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 05_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
<hr/><a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">05_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
## Functional organization? ## Functional organization?
@ -1012,7 +1042,7 @@ Some points:
Then here is how I imagine things should go. Then here is how I imagine things should go.
First, what the main loop should look like: First, what the main loop should look like:
<code class="haskell"> <code class="no-highlight">
functionalMainLoop = functionalMainLoop =
Read user inputs and provide a list of actions Read user inputs and provide a list of actions
Apply all actions to the World Apply all actions to the World
@ -1269,16 +1299,16 @@ This file is commented a lot.
- [`Mandel`](code/05_Mandelbulb/Mandel.hs), the mandel function - [`Mandel`](code/05_Mandelbulb/Mandel.hs), the mandel function
- [`ExtComplex`](code/05_Mandelbulb/ExtComplex.hs), the extended complexes - [`ExtComplex`](code/05_Mandelbulb/ExtComplex.hs), the extended complexes
<a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">05_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 05_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>
<hr/><a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">06_Mandelbulb/<strong>Mandelbulb.lhs</strong></a> <hr/><a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 06_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
## Optimization ## Optimization
All feel good from the architecture point of vue. All feel good from the architecture point of vue.
More precisely, the separation between rendering and world behavior is clear. More precisely, the separation between rendering and world behavior is clear.
But this is extremely slow now. But this is extremely slow now.
Because we compute the mandelbulb for each frame now. Because we compute the Mandelbulb for each frame now.
Before we had Before we had
@ -1308,7 +1338,8 @@ import Mandel -- The 3D Mandelbrot maths
-- Centralize all user input interaction -- Centralize all user input interaction
inputActionMap :: InputMap World inputActionMap :: InputMap World
inputActionMap = inputMapFromList [ inputActionMap = inputMapFromList [
(Press 'k' , rotate xdir 5) (Press ' ' , switch_rotation)
,(Press 'k' , rotate xdir 5)
,(Press 'i' , rotate xdir (-5)) ,(Press 'i' , rotate xdir (-5))
,(Press 'j' , rotate ydir 5) ,(Press 'j' , rotate ydir 5)
,(Press 'l' , rotate ydir (-5)) ,(Press 'l' , rotate ydir (-5))
@ -1322,8 +1353,8 @@ inputActionMap = inputMapFromList [
,(Press 'r' , translate zdir (-0.1)) ,(Press 'r' , translate zdir (-0.1))
,(Press '+' , zoom 1.1) ,(Press '+' , zoom 1.1)
,(Press '-' , zoom (1/1.1)) ,(Press '-' , zoom (1/1.1))
,(Press 'h' , resize 1.2) ,(Press 'h' , resize 2.0)
,(Press 'g' , resize (1/1.2)) ,(Press 'g' , resize (1/2.0))
] ]
</code> </code>
</div> </div>
@ -1334,6 +1365,7 @@ inputActionMap = inputMapFromList [
<code class="haskell"> <code class="haskell">
data World = World { data World = World {
angle :: Point3D angle :: Point3D
, anglePerSec :: Scalar
, scale :: Scalar , scale :: Scalar
, position :: Point3D , position :: Point3D
, box :: Box3D , box :: Box3D
@ -1373,6 +1405,11 @@ rotate dir angleValue world =
world { world {
angle = (angle world) + (angleValue -*< dir) } angle = (angle world) + (angleValue -*< dir) }
switch_rotation :: World -> World
switch_rotation world =
world {
anglePerSec = if anglePerSec world > 0 then 0 else 5.0 }
translate :: Point3D -> Scalar -> World -> World translate :: Point3D -> Scalar -> World -> World
translate dir len world = translate dir len world =
world { world {
@ -1403,11 +1440,12 @@ Our initial world state is slightly changed:
initialWorld :: World initialWorld :: World
initialWorld = World { initialWorld = World {
angle = makePoint3D (30,30,0) angle = makePoint3D (30,30,0)
, anglePerSec = 5.0
, position = makePoint3D (0,0,0) , position = makePoint3D (0,0,0)
, scale = 1.0 , scale = 1.0
, box = Box3D { minPoint = makePoint3D (-2,-2,-2) , box = Box3D { minPoint = makePoint3D (-2,-2,-2)
, maxPoint = makePoint3D (2,2,2) , maxPoint = makePoint3D (2,2,2)
, resolution = 0.02 } , resolution = 0.03 }
, told = 0 , told = 0
-- We declare cache directly this time -- We declare cache directly this time
, cache = objectFunctionFromWorld initialWorld , cache = objectFunctionFromWorld initialWorld
@ -1423,11 +1461,12 @@ This way instead of providing `XYFunc`, we provide directly a list of Atoms.
objectFunctionFromWorld :: World -> [YObject] objectFunctionFromWorld :: World -> [YObject]
objectFunctionFromWorld w = [Atoms atomList] objectFunctionFromWorld w = [Atoms atomList]
where atomListPositive = where atomListPositive =
getObject3DFromShapeFunction (shapeFunc (resolution (box w))) (box w) getObject3DFromShapeFunction
(shapeFunc (resolution (box w))) (box w)
atomList = atomListPositive ++ atomList = atomListPositive ++
map negativeTriangle atomListPositive map negativeTriangle atomListPositive
negativeTriangle (ColoredTriangle (p1,p2,p3,c)) = negativeTriangle (ColoredTriangle (p1,p2,p3,c)) =
ColoredTriangle (negz p1,negz p2,negz p3,c) ColoredTriangle (negz p1,negz p3,negz p2,c)
where negz (P (x,y,z)) = P (x,y,-z) where negz (P (x,y,z)) = P (x,y,-z)
</code> </code>
</div> </div>
@ -1460,10 +1499,18 @@ idleAction tnew world =
, told = tnew , told = tnew
} }
where where
anglePerSec = 5.0 delta = anglePerSec world * elapsed / 1000.0
delta = anglePerSec * elapsed / 1000.0
elapsed = fromIntegral (tnew - (told world)) elapsed = fromIntegral (tnew - (told world))
shapeFunc' :: Scalar -> Function3D
shapeFunc' res x y = if or [tmp u v>=0 | u<-[x,x+res], v<-[y,y+res]]
then Just (z,hexColor "#AD4")
else Nothing
where tmp x y = (x**2 + y**2)
protectSqrt t = if t<0 then 0 else sqrt t
z = sqrt (a**2 - (c - protectSqrt(tmp x y))**2)
a = 0.2
c = 0.5
shapeFunc :: Scalar -> Function3D shapeFunc :: Scalar -> Function3D
shapeFunc res x y = shapeFunc res x y =
let let
@ -1472,13 +1519,13 @@ shapeFunc res x y =
if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 | if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 |
val <- [res], xeps <- [-val,val], yeps<-[-val,val]] val <- [res], xeps <- [-val,val], yeps<-[-val,val]]
then Nothing then Nothing
else Just (z,colorFromValue ((ymandel x y z) * 64)) else Just (z,colorFromValue 0)
colorFromValue :: Point -> Color colorFromValue :: Point -> Color
colorFromValue n = colorFromValue n =
let let
t :: Point -> Scalar t :: Point -> Scalar
t i = 0.7 + 0.3*cos( i / 10 ) t i = 0.0 + 0.5*cos( i /10 )
in in
makeColor (t n) (t (n+5)) (t (n+10)) makeColor (t n) (t (n+5)) (t (n+10))
@ -1502,5 +1549,5 @@ ymandel x y z = fromIntegral (mandel x y z 64) / 64
- [`Mandel`](code/06_Mandelbulb/Mandel.hs), the mandel function - [`Mandel`](code/06_Mandelbulb/Mandel.hs), the mandel function
- [`ExtComplex`](code/06_Mandelbulb/ExtComplex.hs), the extended complexes - [`ExtComplex`](code/06_Mandelbulb/ExtComplex.hs), the extended complexes
<a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">06_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 06_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>

View file

@ -15,12 +15,12 @@ tags:
- functional - functional
- tutorial - tutorial
----- -----
blogimage("HGL_Plan.png","The plan in image") blogimage("BenoitBMandelbrot.jpg","The B in Benoît B. Mandelbrot stand for Benoît B. Mandelbrot")
begindiv(intro) begindiv(intro)
en: %tldr A progressive real world example. en: %tldr You will see how to go from theory to a real application using Haskell.
fr: %tlal Un exemple progressif de programmation avec Haskell. fr: %tlal Un exemple progressif d'utilisation d'Haskell.
> <center><hr style="width:30%;float:left;border-color:#CCCCD0;margin-top:1em"/><span class="sc"><b>Table of Content</b></span><hr style="width:30%;float:right;border-color:#CCCCD0;margin-top:1em"/></center> > <center><hr style="width:30%;float:left;border-color:#CCCCD0;margin-top:1em"/><span class="sc"><b>Table of Content</b></span><hr style="width:30%;float:right;border-color:#CCCCD0;margin-top:1em"/></center>
@ -33,46 +33,63 @@ enddiv
## Introduction ## Introduction
TODO: write something nice after reading. I wanted to go further than my
[preceding article](/Scratch/en/blog/Haskell-the-Hard-Way/) in which I introduced Haskell.
Steps: Instead of arguing that Haskell is better, because it is functional and "Functional Programming! Yeah!", I'll give an example of what benefit
functional programming can provide.
This article is more about functional paradigm than functional language.
The code organization can be used in most imperative language.
As Haskell is designed for functional paradigm, it is easier to talk about functional paradigm using it.
In reality, in the firsts sections I use an imperative paradigm.
As you can use functional paradigm in imperative language,
you can also use imperative paradigm in functional languages.
1. Mandelbrot set with Haskell OpenGL This article is about creating a useful program.
2. Mandelbrot edges It can interact with the user in real time.
3. 3D Mandelbrot because its fun It uses OpenGL, a library with imperative programming foundations.
4. Clean the code from full impure and imperative to purer and purer. But the final code will be quite clean.
5. Refactor the code to separate nicely important parts Most of the code will remain in the pure part (no `IO`).
6. Improve efficiency
I believe the main audience for this article are:
- Haskell programmer looking for an OpengGL tutorial.
- People interested in program organization (programming language agnostic).
- Fractal lovers and in particular 3D fractal.
- Game programmers (any language)
I wanted to talk about something cool.
For example I always wanted to make a Mandelbrot set explorer.
I had written a [command line Mandelbrot set generator in Haskell](http://github.com/yogsototh/mandelbrot.git).
The cool part of this utility is that it use all the cores to make the computation (it uses the `repa` package)[^001].
[^001]: Unfortunately, I couldn't make this program to work on my Mac. More precisely, I couldn't make the [DevIL](http://openil.sourceforge.net/) library work on Mac to output the image. Yes I have done a `brew install libdevil`. But even a minimal program who simply write some `jpg` didn't worked.
This time, we will display the Mandelbrot set extended in 3D using OpenGL and Haskell.
You will be able to move it using your keyboard.
This object is a Mandelbrot set in the plan (z=0),
and something nice to see in 3D.
Here is what you'll end with:
blogimage("GoldenMandelbulb.png","A golden mandelbulb")
And here are the intermediate steps:
blogimage("HGL_Plan.png","The parts of the article")
From 1 to 3 it will be _dirtier_ and _dirtier_. From 1 to 3 it will be _dirtier_ and _dirtier_.
At 4, we will make some order in this mess! We start cleaning everything at the 4th part.
Hopefuly for the best!
One of the goal of this article is to show some good properties of Haskell. <hr/><a href="code/01_Introduction/hglmandel.lhs" class="cut">Download the source code of this section → 01_Introduction/<strong>hglmandel.lhs</strong></a>
In particular, how to make some real world application with a pure functional language.
I know drawing a simple mandelbrot set isn't a "real world" application.
But the idea is not to show you a real world application which would be hard to follows, but to give you a way to pass from the pure mindset to some real world application.
To this, I will show you how should progress an application.
It is not something easy to show.
This is why, I preferred work with a program that generate some image.
In a real world application, the first constraint would be to work with some framework.
And generally an imperative one.
Also, the imperative nature of OpenGL make it the perfect choice for an example.
<hr/><a href="code/01_Introduction/hglmandel.lhs" class="cut">01_Introduction/<strong>hglmandel.lhs</strong></a>
## First version ## First version
We can consider two parts. We can consider two parts.
The first being mostly some boilerplate[^1]. The first being mostly some boilerplate[^011].
The second part, contain more interesting stuff. And the second part more focused on OpenGL and content.
Even in this part, there are some necessary boilerplate.
But it is due to the OpenGL library this time.
[^1]: Generally in Haskell you need to declare a lot of import lines. [^011]: Generally in Haskell you need to declare a lot of import lines.
This is something I find annoying. This is something I find annoying.
In particular, it should be possible to create a special file, Import.hs 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. which make all the necessary import for you, as you generally need them all.
@ -129,9 +146,6 @@ magnitude = real.abs
### Let us start ### Let us start
Well, up until here we didn't made something useful.
Just a lot of boilerplate and default value.
Sorry but it is not completely the end.
We start by giving the main architecture of our program: We start by giving the main architecture of our program:
<div class="codehighlight"> <div class="codehighlight">
@ -152,7 +166,8 @@ main = do
</code> </code>
</div> </div>
The only interesting part is we declared that the function `display` will be used to render the graphics: Mainly, we initialize our OpenGL application.
We declared that the function `display` will be used to render the graphics:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -164,12 +179,12 @@ display = do
</code> </code>
</div> </div>
Also here, there is only one interesting part, Also here, there is only one interesting line;
the draw will occurs in the function `drawMandelbrot`. the draw will occur in the function `drawMandelbrot`.
Now we must speak a bit about how OpenGL works. This function will provide a list of draw actions.
We said that OpenGL is imperative by design. Remember that OpenGL is imperative by design.
In fact, you must write the list of actions in the right order. Then, one of the consequence is you must write the actions in the right order.
No easy parallel drawing here. No easy parallel drawing here.
Here is the function which will render something on the screen: Here is the function which will render something on the screen:
@ -202,8 +217,8 @@ drawMandelbrot =
~~~ ~~~
We also need some kind of global variables. We also need some kind of global variables.
In fact, global variable are a proof of some bad design. In fact, global variable are a proof of a design problem.
But remember it is our first try: We will get rid of them later.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -238,7 +253,7 @@ colorFromValue n =
</code> </code>
</div> </div>
And now the mandel function. And now the `mandel` function.
Given two coordinates in pixels, it returns some integer value: Given two coordinates in pixels, it returns some integer value:
<div class="codehighlight"> <div class="codehighlight">
@ -251,8 +266,8 @@ mandel x y =
</code> </code>
</div> </div>
It uses the main mandelbrot function for each complex \\(c\\). It uses the main Mandelbrot function for each complex \\(c\\).
The mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity. The Mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity.
Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\) Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\)
@ -274,15 +289,15 @@ f c z n = if (magnitude z > 2 )
</code> </code>
</div> </div>
Well, if you download this lhs file, compile it and run it this is the result: Well, if you download this file (look at the bottom of this section), compile it and run it this is the result:
blogimage("hglmandel_v01.png","The mandelbrot set version 1") blogimage("hglmandel_v01.png","The mandelbrot set version 1")
A first very interesting property of this program is that the computation for all the points is done only once. A first very interesting property of this program is that the computation for all the points is done only once.
The proof is that it might be a bit long before a first image appears, but if you resize the window, it updates instantaneously. 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. This property is a direct consequence of purity.
If you look closely, you see that `allPoints` is a pure list. If you look closely, you see that `allPoints` is a pure list.
Therefore, calling `allPoints` will always render the same result. Therefore, calling `allPoints` will always render the same result and Haskell is clever enough to use this property.
While Haskell doesn't garbage collect `allPoints` the result is reused for free. While Haskell doesn't garbage collect `allPoints` the result is reused for free.
We didn't specified this value should be saved for later use. We didn't specified this value should be saved for later use.
It is saved for us. It is saved for us.
@ -291,14 +306,13 @@ See what occurs if we make the window bigger:
blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns") blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns")
Yep, we see some black lines. We see some black lines because we drawn less point than there is on the surface.
Why? Simply because we drawn less point than there is on the surface.
We can repair this by drawing little squares instead of just points. We can repair this by drawing little squares instead of just points.
But, instead we will do something a bit different and unusual. But, instead we will do something a bit different and unusual.
<a href="code/01_Introduction/hglmandel.lhs" class="cut">01_Introduction/<strong>hglmandel.lhs</strong> </a> <a href="code/01_Introduction/hglmandel.lhs" class="cut">Download the source code of this section → 01_Introduction/<strong>hglmandel.lhs</strong> </a>
<hr/><a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">02_Edges/<strong>HGLMandelEdge.lhs</strong></a> <hr/><a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">Download the source code of this section → 02_Edges/<strong>HGLMandelEdge.lhs</strong></a>
## Only the edges ## Only the edges
@ -356,6 +370,10 @@ height = 320 :: GLfloat
</div> </div>
This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set. This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set.
The method I use is a rough approximation.
I consider the Mandelbrot set to be almost convex.
The result will be good enough.
We change slightly the drawMandelbrot function. We change slightly the drawMandelbrot function.
We replace the `Points` by `LineLoop` We replace the `Points` by `LineLoop`
@ -386,21 +404,23 @@ allPoints = positivePoints ++
</div> </div>
We only need to compute the positive point. We only need to compute the positive point.
The mandelbrot set is symetric on the abscisse axis. The Mandelbrot set is symmetric on the abscisse axis.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)] positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)]
positivePoints = do positivePoints = do
x <- [-width..width] x <- [-width..width]
let y = findMaxOrdFor (mandel x) 0 height 10 -- log height let y = findMaxOrdFor (mandel x) 0 height (log2 height)
if y < 1 -- We don't draw point in the absciss if y < 1 -- We don't draw point in the absciss
then [] then []
else return (x/width,y/height,colorFromValue $ mandel x y) else return (x/width,y/height,colorFromValue $ mandel x y)
where
log2 n = floor ((log n) / log 2)
</code> </code>
</div> </div>
This function is interresting. This function is interesting.
For those not used to the list monad here is a natural language version of this function: For those not used to the list monad here is a natural language version of this function:
~~~ ~~~
@ -412,7 +432,7 @@ positivePoints =
~~~ ~~~
In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically. In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically.
To find the smallest number such that mandel x y > 0 we create a simple dichotomic search: To find the smallest number such that `mandel x y > 0` we use a simple dichotomy:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -425,9 +445,7 @@ findMaxOrdFor func minval maxval n =
</code> </code>
</div> </div>
No rocket science here. No rocket science here. See the result now:
I know, due to the fact the mandelbrot set is not convex this approach does some errors. But the approximation will be good enough.
See the result now:
blogimage("HGLMandelEdges.png","The edges of the mandelbrot set") blogimage("HGLMandelEdges.png","The edges of the mandelbrot set")
@ -466,27 +484,28 @@ f c z n = if (magnitude z > 2 )
</div> </div>
<a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">02_Edges/<strong>HGLMandelEdge.lhs</strong> </a> <a href="code/02_Edges/HGLMandelEdge.lhs" class="cut">Download the source code of this section → 02_Edges/<strong>HGLMandelEdge.lhs</strong> </a>
<hr/><a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">03_Mandelbulb/<strong>Mandelbulb.lhs</strong></a> <hr/><a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 03_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
## 3D Mandelbrot? ## 3D Mandelbrot?
Why only draw the edge? Now we will we extend to a third dimension.
It is clearly not as nice as drawing the complete surface. But, there is no 3D equivalent to complex.
Yeah, I know, but, as we use OpenGL, why not show something in 3D. In fact, the only extension known are quaternions (in 4D).
As I know almost nothing about quaternions, I will use some extended complex,
But, complex number are only in 2D and there is no 3D equivalent to complex. instead of using a 3D projection of quaternions.
In fact, the only extension known are quaternions, 4D.
As I know almost nothing about quaternions, I will use some extended complex.
I am pretty sure this construction is not useful for numbers. I am pretty sure this construction is not useful for numbers.
But it will be enough for us to create something nice. But it will be enough for us to create something that look nice.
As there is a lot of code, I'll give a high level view to what occurs: This section is quite long, but don't be afraid,
most of the code is some OpenGL boilerplate.
For those you want to skim,
here is a high level representation:
> - OpenGL Boilerplate > - OpenGL Boilerplate
> >
> - set some IORef for states > - set some IORef (understand variables) for states
> - Drawing: > - Drawing:
> >
> - set doubleBuffer, handle depth, window size... > - set doubleBuffer, handle depth, window size...
@ -523,8 +542,8 @@ type ColoredPoint = (GLfloat,GLfloat,GLfloat,Color3 GLfloat)
</div> </div>
We declare a new type `ExtComplex` (for exttended complex). We declare a new type `ExtComplex` (for extended complex).
An extension of complex numbers: An extension of complex numbers with a third component:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -545,7 +564,17 @@ instance Num ExtComplex where
</div> </div>
The most important part is the new multiplication instance. The most important part is the new multiplication instance.
Modifying this formula will change radically the shape of this somehow 3D mandelbrot. Modifying this formula will change radically the shape of the result.
Here is the formula written in a more mathematical notation.
I called the third component of these extended complex _strange_.
$$ \mathrm{real} ((x,y,z) * (x',y',z')) = xx' - yy' - zz' $$
$$ \mathrm{im} ((x,y,z) * (x',y',z')) = xy' - yx' + zz' $$
$$ \mathrm{strange} ((x,y,z) * (x',y',z')) = xz' + zx' $$
Note how if `z=z'=0` then the multiplication is the same to the complex one.
<div style="display:none"> <div style="display:none">
@ -588,15 +617,14 @@ main = do
createWindow "3D HOpengGL Mandelbrot" createWindow "3D HOpengGL Mandelbrot"
-- We add some directives -- We add some directives
depthFunc $= Just Less depthFunc $= Just Less
-- matrixMode $= Projection
windowSize $= Size 500 500 windowSize $= Size 500 500
-- Some state variables (I know it feels BAD) -- Some state variables (I know it feels BAD)
angle <- newIORef ((35,0)::(GLfloat,GLfloat)) angle <- newIORef ((35,0)::(GLfloat,GLfloat))
zoom <- newIORef (2::GLfloat) zoom <- newIORef (2::GLfloat)
campos <- newIORef ((0.7,0)::(GLfloat,GLfloat)) campos <- newIORef ((0.7,0)::(GLfloat,GLfloat))
-- Action to call when waiting -- Function to call each frame
idleCallback $= Just idle idleCallback $= Just idle
-- We will use the keyboard -- Function to call when keyboard or mouse is used
keyboardMouseCallback $= keyboardMouseCallback $=
Just (keyboardMouse angle zoom campos) Just (keyboardMouse angle zoom campos)
-- Each time we will need to update the display -- Each time we will need to update the display
@ -608,7 +636,8 @@ main = do
</code> </code>
</div> </div>
The `idle` function necessary for animation. The `idle` is here to change the states.
There should never be any modification done in the `display` function.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -618,6 +647,9 @@ idle = postRedisplay Nothing
We introduce some helper function to manipulate We introduce some helper function to manipulate
standard `IORef`. standard `IORef`.
Mainly `modVar x f` is equivalent to the imperative `x:=f(x)`,
`modFst (x,y) (+1)` is equivalent to `(x,y) := (x+1,y)`
and `modSnd (x,y) (+1)` is equivalent to `(x,y) := (x,y+1)`
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -633,25 +665,29 @@ And we use them to code the function handling keyboard.
We will use the keys `hjkl` to rotate, We will use the keys `hjkl` to rotate,
`oi` to zoom and `sedf` to move. `oi` to zoom and `sedf` to move.
Also, hitting space will reset the view. Also, hitting space will reset the view.
Remember that `angle` and `campos` are pairs and `zoom` is a scalar.
Also note `(+0.5)` is the function `\x->x+0.5`
and `(-0.5)` is the number `-0.5` (yes I share your pain).
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
keyboardMouse angle zoom pos key state modifiers position = keyboardMouse angle zoom campos key state modifiers position =
kact angle zoom pos key state -- We won't use modifiers nor position
kact angle zoom campos key state
where where
-- reset view when hitting space -- reset view when hitting space
kact a z p (Char ' ') Down = do kact a z p (Char ' ') Down = do
a $= (0,0) a $= (0,0) -- angle
z $= 1 z $= 1 -- zoom
p $= (0,0) p $= (0,0) -- camera position
-- use of hjkl to rotate -- use of hjkl to rotate
kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5)) kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5))
kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5))) kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5)))
kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5)) kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5))
kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5))) kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5)))
-- use o and i to zoom -- use o and i to zoom
kact _ s _ (Char 'o') Down = modVar s (*1.1) kact _ z _ (Char 'o') Down = modVar z (*1.1)
kact _ s _ (Char 'i') Down = modVar s (*0.9) kact _ z _ (Char 'i') Down = modVar z (*0.9)
-- use sdfe to move the camera -- use sdfe to move the camera
kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1)) kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1))
kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1))) kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1)))
@ -662,9 +698,8 @@ keyboardMouse angle zoom pos key state modifiers position =
</code> </code>
</div> </div>
Now, we will show the object using the display function. Note `display` take some parameters this time.
Note, this time, display take some parameters. This function if full of boilerplate:
Mainly, this function if full of boilerplate:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -684,9 +719,11 @@ display angle zoom position = do
(xangle,yangle) <- get angle (xangle,yangle) <- get angle
rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat) rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat)
rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat) rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat)
-- Now that all transformation were made -- Now that all transformation were made
-- We create the object(s) -- We create the object(s)
preservingMatrix drawMandelbrot preservingMatrix drawMandelbrot
swapBuffers -- refresh screen swapBuffers -- refresh screen
</code> </code>
</div> </div>
@ -696,9 +733,9 @@ Mainly there are two parts: apply some transformations, draw the object.
### The 3D Mandelbrot ### The 3D Mandelbrot
Now, that we talked about the OpenGL part, let's talk about how we We have finished with the OpenGL section, let's talk about how we
generate the 3D points and colors. generate the 3D points and colors.
First, we will set the number of detatils to 180 pixels in the three dimensions. First, we will set the number of details to 200 pixels in the three dimensions.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -711,7 +748,8 @@ deep = nbDetails
This time, instead of just drawing some line or some group of points, This time, instead of just drawing some line or some group of points,
we will show triangles. we will show triangles.
The idea is that we should provide points three by three. The function `allPoints` will provide a multiple of three points.
Each three successive point representing the coordinate of each vertex of a triangle.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -726,14 +764,13 @@ drawMandelbrot = do
</code> </code>
</div> </div>
Now instead of providing only one point at a time, we will provide six ordered points. In fact, we will provide six ordered points.
These points will be used to draw two triangles. These points will be used to draw two triangles.
blogimage("triangles.png","Explain triangles") blogimage("triangles.png","Explain triangles")
Note in 3D the depth of the point is generally different.
The next function is a bit long. The next function is a bit long.
An approximative English version is: Here is an approximative English version:
~~~ ~~~
forall x from -width to width forall x from -width to width
@ -753,7 +790,8 @@ depthPoints = do
x <- [-width..width] x <- [-width..width]
y <- [-height..height] y <- [-height..height]
let let
depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep 7 depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep logdeep
logdeep = floor ((log deep) / log 2)
z1 = depthOf x y z1 = depthOf x y
z2 = depthOf (x+1) y z2 = depthOf (x+1) y
z3 = depthOf (x+1) (y+1) z3 = depthOf (x+1) (y+1)
@ -773,17 +811,18 @@ depthPoints = do
If you look at the function above, you see a lot of common patterns. If you look at the function above, you see a lot of common patterns.
Haskell is very efficient to make this better. Haskell is very efficient to make this better.
Here is a somehow less readable but more generic refactored function: Here is a harder to read but shorter and more generic rewritten function:
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
depthPoints :: [ColoredPoint] depthPoints :: [ColoredPoint]
depthPoints = do depthPoints = do
x <- [-width..width] x <- [-width..width]
y <- [0..height] y <- [-height..height]
let let
neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)] neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)]
depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep 7 depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep logdeep
logdeep = floor ((log deep) / log 2)
-- zs are 3D points with found depth -- zs are 3D points with found depth
zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors
-- ts are 3D pixels + mandel value -- ts are 3D pixels + mandel value
@ -802,26 +841,21 @@ depthPoints = do
If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example. If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example.
Also, we didn't searched for negative values. Also, we didn't searched for negative values.
For simplicity, I mirror these values. This modified Mandelbrot is no more symmetric relatively to the plan `y=0`.
I haven't even tested if this modified mandelbrot is symetric relatively to the plan {(x,y,z)|z=0}. But it is symmetric relatively to the plan `z=0`.
Then I mirror these values.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
allPoints :: [ColoredPoint] allPoints :: [ColoredPoint]
allPoints = planPoints ++ map inverseDepth planPoints allPoints = planPoints ++ map inverseDepth planPoints
where where
planPoints = depthPoints ++ map inverseHeight depthPoints planPoints = depthPoints
inverseHeight (x,y,z,c) = (x,-y,z,c)
inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c) inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c)
</code> </code>
</div> </div>
I cheat by making these symmetry. The rest of the program is very close to the preceding one.
But it is faster and render a nice form.
For this tutorial it will be good enough.
Also, the dichotomic method I use is mostly right but false for some cases.
The rest of the program is very close to the preceeding one.
<div style="display:none"> <div style="display:none">
@ -863,7 +897,8 @@ f c z n = if (magnitude z > 2 )
</div> </div>
We simply add a new dimenstion to the mandel function. Also we simply need to change the type signature of the function `f` from `Complex` to `ExtComplex`. We simply add a new dimension to the `mandel` function
and change the type signature of `f` from `Complex` to `ExtComplex`.
<div class="codehighlight"> <div class="codehighlight">
<code class="haskell"> <code class="haskell">
@ -876,21 +911,19 @@ mandel x y z =
</code> </code>
</div> </div>
And here is the result (if you use 500 for `nbDetails`): Here is the result:
blogimage("mandelbrot_3D.png","A 3D mandelbrot like") blogimage("mandelbrot_3D.png","A 3D mandelbrot like")
This image is quite nice. <a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 03_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>
<a href="code/03_Mandelbulb/Mandelbulb.lhs" class="cut">03_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <hr/><a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 04_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
<hr/><a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">04_Mandelbulb/<strong>Mandelbulb.lhs</strong></a> ## Naïve code cleaning
## Cleaning the code
The first thing to do is to separate the GLUT/OpenGL The first thing to do is to separate the GLUT/OpenGL
part from the computation of the shape. part from the computation of the shape.
Here is the cleaned version of the preceeding section. Here is the cleaned version of the preceding section.
Most boilerplate was put in external files. Most boilerplate was put in external files.
- [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering - [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering
@ -987,13 +1020,10 @@ But I would have preferred to control the user actions.
On the other hand, we continue to handle a lot rendering details. On the other hand, we continue to handle a lot rendering details.
For example, we provide ordered vertices. For example, we provide ordered vertices.
I feel, this should be externalized.
I would have preferred to make things a bit more general. <a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 04_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>
<a href="code/04_Mandelbulb/Mandelbulb.lhs" class="cut">04_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <hr/><a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 05_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
<hr/><a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">05_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
## Functional organization? ## Functional organization?
@ -1015,7 +1045,7 @@ Some points:
Then here is how I imagine things should go. Then here is how I imagine things should go.
First, what the main loop should look like: First, what the main loop should look like:
<code class="haskell"> <code class="no-highlight">
functionalMainLoop = functionalMainLoop =
Read user inputs and provide a list of actions Read user inputs and provide a list of actions
Apply all actions to the World Apply all actions to the World
@ -1272,16 +1302,16 @@ This file is commented a lot.
- [`Mandel`](code/05_Mandelbulb/Mandel.hs), the mandel function - [`Mandel`](code/05_Mandelbulb/Mandel.hs), the mandel function
- [`ExtComplex`](code/05_Mandelbulb/ExtComplex.hs), the extended complexes - [`ExtComplex`](code/05_Mandelbulb/ExtComplex.hs), the extended complexes
<a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">05_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <a href="code/05_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 05_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>
<hr/><a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">06_Mandelbulb/<strong>Mandelbulb.lhs</strong></a> <hr/><a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 06_Mandelbulb/<strong>Mandelbulb.lhs</strong></a>
## Optimization ## Optimization
All feel good from the architecture point of vue. All feel good from the architecture point of vue.
More precisely, the separation between rendering and world behavior is clear. More precisely, the separation between rendering and world behavior is clear.
But this is extremely slow now. But this is extremely slow now.
Because we compute the mandelbulb for each frame now. Because we compute the Mandelbulb for each frame now.
Before we had Before we had
@ -1311,7 +1341,8 @@ import Mandel -- The 3D Mandelbrot maths
-- Centralize all user input interaction -- Centralize all user input interaction
inputActionMap :: InputMap World inputActionMap :: InputMap World
inputActionMap = inputMapFromList [ inputActionMap = inputMapFromList [
(Press 'k' , rotate xdir 5) (Press ' ' , switch_rotation)
,(Press 'k' , rotate xdir 5)
,(Press 'i' , rotate xdir (-5)) ,(Press 'i' , rotate xdir (-5))
,(Press 'j' , rotate ydir 5) ,(Press 'j' , rotate ydir 5)
,(Press 'l' , rotate ydir (-5)) ,(Press 'l' , rotate ydir (-5))
@ -1325,8 +1356,8 @@ inputActionMap = inputMapFromList [
,(Press 'r' , translate zdir (-0.1)) ,(Press 'r' , translate zdir (-0.1))
,(Press '+' , zoom 1.1) ,(Press '+' , zoom 1.1)
,(Press '-' , zoom (1/1.1)) ,(Press '-' , zoom (1/1.1))
,(Press 'h' , resize 1.2) ,(Press 'h' , resize 2.0)
,(Press 'g' , resize (1/1.2)) ,(Press 'g' , resize (1/2.0))
] ]
</code> </code>
</div> </div>
@ -1337,6 +1368,7 @@ inputActionMap = inputMapFromList [
<code class="haskell"> <code class="haskell">
data World = World { data World = World {
angle :: Point3D angle :: Point3D
, anglePerSec :: Scalar
, scale :: Scalar , scale :: Scalar
, position :: Point3D , position :: Point3D
, box :: Box3D , box :: Box3D
@ -1376,6 +1408,11 @@ rotate dir angleValue world =
world { world {
angle = (angle world) + (angleValue -*< dir) } angle = (angle world) + (angleValue -*< dir) }
switch_rotation :: World -> World
switch_rotation world =
world {
anglePerSec = if anglePerSec world > 0 then 0 else 5.0 }
translate :: Point3D -> Scalar -> World -> World translate :: Point3D -> Scalar -> World -> World
translate dir len world = translate dir len world =
world { world {
@ -1406,11 +1443,12 @@ Our initial world state is slightly changed:
initialWorld :: World initialWorld :: World
initialWorld = World { initialWorld = World {
angle = makePoint3D (30,30,0) angle = makePoint3D (30,30,0)
, anglePerSec = 5.0
, position = makePoint3D (0,0,0) , position = makePoint3D (0,0,0)
, scale = 1.0 , scale = 1.0
, box = Box3D { minPoint = makePoint3D (-2,-2,-2) , box = Box3D { minPoint = makePoint3D (-2,-2,-2)
, maxPoint = makePoint3D (2,2,2) , maxPoint = makePoint3D (2,2,2)
, resolution = 0.02 } , resolution = 0.03 }
, told = 0 , told = 0
-- We declare cache directly this time -- We declare cache directly this time
, cache = objectFunctionFromWorld initialWorld , cache = objectFunctionFromWorld initialWorld
@ -1426,11 +1464,12 @@ This way instead of providing `XYFunc`, we provide directly a list of Atoms.
objectFunctionFromWorld :: World -> [YObject] objectFunctionFromWorld :: World -> [YObject]
objectFunctionFromWorld w = [Atoms atomList] objectFunctionFromWorld w = [Atoms atomList]
where atomListPositive = where atomListPositive =
getObject3DFromShapeFunction (shapeFunc (resolution (box w))) (box w) getObject3DFromShapeFunction
(shapeFunc (resolution (box w))) (box w)
atomList = atomListPositive ++ atomList = atomListPositive ++
map negativeTriangle atomListPositive map negativeTriangle atomListPositive
negativeTriangle (ColoredTriangle (p1,p2,p3,c)) = negativeTriangle (ColoredTriangle (p1,p2,p3,c)) =
ColoredTriangle (negz p1,negz p2,negz p3,c) ColoredTriangle (negz p1,negz p3,negz p2,c)
where negz (P (x,y,z)) = P (x,y,-z) where negz (P (x,y,z)) = P (x,y,-z)
</code> </code>
</div> </div>
@ -1463,10 +1502,18 @@ idleAction tnew world =
, told = tnew , told = tnew
} }
where where
anglePerSec = 5.0 delta = anglePerSec world * elapsed / 1000.0
delta = anglePerSec * elapsed / 1000.0
elapsed = fromIntegral (tnew - (told world)) elapsed = fromIntegral (tnew - (told world))
shapeFunc' :: Scalar -> Function3D
shapeFunc' res x y = if or [tmp u v>=0 | u<-[x,x+res], v<-[y,y+res]]
then Just (z,hexColor "#AD4")
else Nothing
where tmp x y = (x**2 + y**2)
protectSqrt t = if t<0 then 0 else sqrt t
z = sqrt (a**2 - (c - protectSqrt(tmp x y))**2)
a = 0.2
c = 0.5
shapeFunc :: Scalar -> Function3D shapeFunc :: Scalar -> Function3D
shapeFunc res x y = shapeFunc res x y =
let let
@ -1475,13 +1522,13 @@ shapeFunc res x y =
if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 | if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 |
val <- [res], xeps <- [-val,val], yeps<-[-val,val]] val <- [res], xeps <- [-val,val], yeps<-[-val,val]]
then Nothing then Nothing
else Just (z,colorFromValue ((ymandel x y z) * 64)) else Just (z,colorFromValue 0)
colorFromValue :: Point -> Color colorFromValue :: Point -> Color
colorFromValue n = colorFromValue n =
let let
t :: Point -> Scalar t :: Point -> Scalar
t i = 0.7 + 0.3*cos( i / 10 ) t i = 0.0 + 0.5*cos( i /10 )
in in
makeColor (t n) (t (n+5)) (t (n+10)) makeColor (t n) (t (n+5)) (t (n+10))
@ -1505,5 +1552,5 @@ ymandel x y z = fromIntegral (mandel x y z 64) / 64
- [`Mandel`](code/06_Mandelbulb/Mandel.hs), the mandel function - [`Mandel`](code/06_Mandelbulb/Mandel.hs), the mandel function
- [`ExtComplex`](code/06_Mandelbulb/ExtComplex.hs), the extended complexes - [`ExtComplex`](code/06_Mandelbulb/ExtComplex.hs), the extended complexes
<a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">06_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a> <a href="code/06_Mandelbulb/Mandelbulb.lhs" class="cut">Download the source code of this section → 06_Mandelbulb/<strong>Mandelbulb.lhs</strong> </a>

View file

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="fr" xml:lang="fr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="jquery.cookie.js"></script>
<script>
$(document).ready(function(){
$.cookie('admin',1);
$('#info').html('Analytics can no more see you.')
});
</script>
<title>Hide to analytics</title>
</head>
<body>
<div id="info"></div>
</body>
</html>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="fr" xml:lang="fr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="jquery.cookie.js"></script>
<script>
$(document).ready(function(){
$.cookie('admin',null);
$('#info').html('Analytics can see you.')
});
</script>
<title>Hide to analytics</title>
</head>
<body>
<div id="info"></div>
</body>
</html>

View file

@ -1,31 +1,49 @@
## Introduction ## Introduction
TODO: write something nice after reading. I wanted to go further than my
[preceding article](/Scratch/en/blog/Haskell-the-Hard-Way/) in which I introduced Haskell.
Steps: Instead of arguing that Haskell is better, because it is functional and "Functional Programming! Yeah!", I'll give an example of what benefit
functional programming can provide.
This article is more about functional paradigm than functional language.
The code organization can be used in most imperative language.
As Haskell is designed for functional paradigm, it is easier to talk about functional paradigm using it.
In reality, in the firsts sections I use an imperative paradigm.
As you can use functional paradigm in imperative language,
you can also use imperative paradigm in functional languages.
1. Mandelbrot set with Haskell OpenGL This article is about creating a useful program.
2. Mandelbrot edges It can interact with the user in real time.
3. 3D Mandelbrot because its fun It uses OpenGL, a library with imperative programming foundations.
4. Clean the code from full impure and imperative to purer and purer. But the final code will be quite clean.
5. Refactor the code to separate nicely important parts Most of the code will remain in the pure part (no `IO`).
6. Improve efficiency
I believe the main audience for this article are:
- Haskell programmer looking for an OpengGL tutorial.
- People interested in program organization (programming language agnostic).
- Fractal lovers and in particular 3D fractal.
- Game programmers (any language)
I wanted to talk about something cool.
For example I always wanted to make a Mandelbrot set explorer.
I had written a [command line Mandelbrot set generator in Haskell](http://github.com/yogsototh/mandelbrot.git).
The cool part of this utility is that it use all the cores to make the computation (it uses the `repa` package)[^001].
[^001]: Unfortunately, I couldn't make this program to work on my Mac. More precisely, I couldn't make the [DevIL](http://openil.sourceforge.net/) library work on Mac to output the image. Yes I have done a `brew install libdevil`. But even a minimal program who simply write some `jpg` didn't worked.
This time, we will display the Mandelbrot set extended in 3D using OpenGL and Haskell.
You will be able to move it using your keyboard.
This object is a Mandelbrot set in the plan (z=0),
and something nice to see in 3D.
Here is what you'll end with:
blogimage("GoldenMandelbulb.png","A golden mandelbulb")
And here are the intermediate steps:
blogimage("HGL_Plan.png","The parts of the article")
From 1 to 3 it will be _dirtier_ and _dirtier_. From 1 to 3 it will be _dirtier_ and _dirtier_.
At 4, we will make some order in this mess! We start cleaning everything at the 4th part.
Hopefuly for the best!
One of the goal of this article is to show some good properties of Haskell.
In particular, how to make some real world application with a pure functional language.
I know drawing a simple mandelbrot set isn't a "real world" application.
But the idea is not to show you a real world application which would be hard to follows, but to give you a way to pass from the pure mindset to some real world application.
To this, I will show you how should progress an application.
It is not something easy to show.
This is why, I preferred work with a program that generate some image.
In a real world application, the first constraint would be to work with some framework.
And generally an imperative one.
Also, the imperative nature of OpenGL make it the perfect choice for an example.

View file

@ -1,13 +1,10 @@
## First version ## First version
We can consider two parts. We can consider two parts.
The first being mostly some boilerplate[^1]. The first being mostly some boilerplate[^011].
The second part, contain more interesting stuff. And the second part more focused on OpenGL and content.
Even in this part, there are some necessary boilerplate.
But it is due to the OpenGL library this time.
[^011]: Generally in Haskell you need to declare a lot of import lines.
[^1]: Generally in Haskell you need to declare a lot of import lines.
This is something I find annoying. This is something I find annoying.
In particular, it should be possible to create a special file, Import.hs 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. which make all the necessary import for you, as you generally need them all.
@ -50,9 +47,6 @@ We declare some useful functions for manipulating complex numbers:
### Let us start ### Let us start
Well, up until here we didn't made something useful.
Just a lot of boilerplate and default value.
Sorry but it is not completely the end.
We start by giving the main architecture of our program: We start by giving the main architecture of our program:
> main :: IO () > main :: IO ()
@ -69,7 +63,8 @@ We start by giving the main architecture of our program:
> -- We enter the main loop > -- We enter the main loop
> mainLoop > mainLoop
The only interesting part is we declared that the function `display` will be used to render the graphics: Mainly, we initialize our OpenGL application.
We declared that the function `display` will be used to render the graphics:
> display = do > display = do
> clear [ColorBuffer] -- make the window black > clear [ColorBuffer] -- make the window black
@ -77,12 +72,12 @@ The only interesting part is we declared that the function `display` will be use
> preservingMatrix drawMandelbrot > preservingMatrix drawMandelbrot
> swapBuffers -- refresh screen > swapBuffers -- refresh screen
Also here, there is only one interesting part, Also here, there is only one interesting line;
the draw will occurs in the function `drawMandelbrot`. the draw will occur in the function `drawMandelbrot`.
Now we must speak a bit about how OpenGL works. This function will provide a list of draw actions.
We said that OpenGL is imperative by design. Remember that OpenGL is imperative by design.
In fact, you must write the list of actions in the right order. Then, one of the consequence is you must write the actions in the right order.
No easy parallel drawing here. No easy parallel drawing here.
Here is the function which will render something on the screen: Here is the function which will render something on the screen:
@ -111,8 +106,8 @@ drawMandelbrot =
~~~ ~~~
We also need some kind of global variables. We also need some kind of global variables.
In fact, global variable are a proof of some bad design. In fact, global variable are a proof of a design problem.
But remember it is our first try: We will get rid of them later.
> width = 320 :: GLfloat > width = 320 :: GLfloat
> height = 320 :: GLfloat > height = 320 :: GLfloat
@ -135,7 +130,7 @@ We need a function which transform an integer value to some color:
> in > in
> Color3 (t n) (t (n+5)) (t (n+10)) > Color3 (t n) (t (n+5)) (t (n+10))
And now the mandel function. And now the `mandel` function.
Given two coordinates in pixels, it returns some integer value: Given two coordinates in pixels, it returns some integer value:
> mandel x y = > mandel x y =
@ -144,8 +139,8 @@ Given two coordinates in pixels, it returns some integer value:
> in > in
> f (complex r i) 0 64 > f (complex r i) 0 64
It uses the main mandelbrot function for each complex \\(c\\). It uses the main Mandelbrot function for each complex \\(c\\).
The mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity. The Mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity.
Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\) Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\)
@ -163,15 +158,15 @@ Of course, instead of trying to test the real limit, we just make a test after a
> then n > then n
> else f c ((z*z)+c) (n-1) > else f c ((z*z)+c) (n-1)
Well, if you download this lhs file, compile it and run it this is the result: Well, if you download this file (look at the bottom of this section), compile it and run it this is the result:
blogimage("hglmandel_v01.png","The mandelbrot set version 1") blogimage("hglmandel_v01.png","The mandelbrot set version 1")
A first very interesting property of this program is that the computation for all the points is done only once. A first very interesting property of this program is that the computation for all the points is done only once.
The proof is that it might be a bit long before a first image appears, but if you resize the window, it updates instantaneously. 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. This property is a direct consequence of purity.
If you look closely, you see that `allPoints` is a pure list. If you look closely, you see that `allPoints` is a pure list.
Therefore, calling `allPoints` will always render the same result. Therefore, calling `allPoints` will always render the same result and Haskell is clever enough to use this property.
While Haskell doesn't garbage collect `allPoints` the result is reused for free. While Haskell doesn't garbage collect `allPoints` the result is reused for free.
We didn't specified this value should be saved for later use. We didn't specified this value should be saved for later use.
It is saved for us. It is saved for us.
@ -180,7 +175,6 @@ See what occurs if we make the window bigger:
blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns") blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns")
Yep, we see some black lines. We see some black lines because we drawn less point than there is on the surface.
Why? Simply because we drawn less point than there is on the surface.
We can repair this by drawing little squares instead of just points. We can repair this by drawing little squares instead of just points.
But, instead we will do something a bit different and unusual. But, instead we will do something a bit different and unusual.

View file

@ -51,6 +51,10 @@
</div> </div>
This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set. This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set.
The method I use is a rough approximation.
I consider the Mandelbrot set to be almost convex.
The result will be good enough.
We change slightly the drawMandelbrot function. We change slightly the drawMandelbrot function.
We replace the `Points` by `LineLoop` We replace the `Points` by `LineLoop`
@ -73,18 +77,19 @@ we will choose only point on the surface.
> map (\(x,y,c) -> (x,-y,c)) (reverse positivePoints) > map (\(x,y,c) -> (x,-y,c)) (reverse positivePoints)
We only need to compute the positive point. We only need to compute the positive point.
The mandelbrot set is symetric on the abscisse axis. The Mandelbrot set is symmetric on the abscisse axis.
> positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)] > positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)]
> positivePoints = do > positivePoints = do
> x <- [-width..width] > x <- [-width..width]
> let y = findMaxOrdFor (mandel x) 0 height 10 -- log height > let y = findMaxOrdFor (mandel x) 0 height (log2 height)
> if y < 1 -- We don't draw point in the absciss > if y < 1 -- We don't draw point in the absciss
> then [] > then []
> else return (x/width,y/height,colorFromValue $ mandel x y) > else return (x/width,y/height,colorFromValue $ mandel x y)
> where
> log2 n = floor ((log n) / log 2)
This function is interesting.
This function is interresting.
For those not used to the list monad here is a natural language version of this function: For those not used to the list monad here is a natural language version of this function:
~~~ ~~~
@ -96,7 +101,7 @@ positivePoints =
~~~ ~~~
In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically. In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically.
To find the smallest number such that mandel x y > 0 we create a simple dichotomic search: To find the smallest number such that `mandel x y > 0` we use a simple dichotomy:
> findMaxOrdFor func minval maxval 0 = (minval+maxval)/2 > findMaxOrdFor func minval maxval 0 = (minval+maxval)/2
> findMaxOrdFor func minval maxval n = > findMaxOrdFor func minval maxval n =
@ -105,9 +110,7 @@ To find the smallest number such that mandel x y > 0 we create a simple dichotom
> else findMaxOrdFor func medpoint maxval (n-1) > else findMaxOrdFor func medpoint maxval (n-1)
> where medpoint = (minval+maxval)/2 > where medpoint = (minval+maxval)/2
No rocket science here. No rocket science here. See the result now:
I know, due to the fact the mandelbrot set is not convex this approach does some errors. But the approximation will be good enough.
See the result now:
blogimage("HGLMandelEdges.png","The edges of the mandelbrot set") blogimage("HGLMandelEdges.png","The edges of the mandelbrot set")

View file

@ -1,20 +1,21 @@
## 3D Mandelbrot? ## 3D Mandelbrot?
Why only draw the edge? Now we will we extend to a third dimension.
It is clearly not as nice as drawing the complete surface. But, there is no 3D equivalent to complex.
Yeah, I know, but, as we use OpenGL, why not show something in 3D. In fact, the only extension known are quaternions (in 4D).
As I know almost nothing about quaternions, I will use some extended complex,
But, complex number are only in 2D and there is no 3D equivalent to complex. instead of using a 3D projection of quaternions.
In fact, the only extension known are quaternions, 4D.
As I know almost nothing about quaternions, I will use some extended complex.
I am pretty sure this construction is not useful for numbers. I am pretty sure this construction is not useful for numbers.
But it will be enough for us to create something nice. But it will be enough for us to create something that look nice.
As there is a lot of code, I'll give a high level view to what occurs: This section is quite long, but don't be afraid,
most of the code is some OpenGL boilerplate.
For those you want to skim,
here is a high level representation:
> - OpenGL Boilerplate > - OpenGL Boilerplate
> >
> - set some IORef for states > - set some IORef (understand variables) for states
> - Drawing: > - Drawing:
> >
> - set doubleBuffer, handle depth, window size... > - set doubleBuffer, handle depth, window size...
@ -49,8 +50,8 @@ As there is a lot of code, I'll give a high level view to what occurs:
</div> </div>
We declare a new type `ExtComplex` (for exttended complex). We declare a new type `ExtComplex` (for extended complex).
An extension of complex numbers: An extension of complex numbers with a third component:
> data ExtComplex = C (GLfloat,GLfloat,GLfloat) > data ExtComplex = C (GLfloat,GLfloat,GLfloat)
> deriving (Show,Eq) > deriving (Show,Eq)
@ -67,7 +68,17 @@ An extension of complex numbers:
> signum (C (x,y,z)) = C (signum x, 0, 0) > signum (C (x,y,z)) = C (signum x, 0, 0)
The most important part is the new multiplication instance. The most important part is the new multiplication instance.
Modifying this formula will change radically the shape of this somehow 3D mandelbrot. Modifying this formula will change radically the shape of the result.
Here is the formula written in a more mathematical notation.
I called the third component of these extended complex _strange_.
$$ \mathrm{real} ((x,y,z) * (x',y',z')) = xx' - yy' - zz' $$
$$ \mathrm{im} ((x,y,z) * (x',y',z')) = xy' - yx' + zz' $$
$$ \mathrm{strange} ((x,y,z) * (x',y',z')) = xz' + zx' $$
Note how if `z=z'=0` then the multiplication is the same to the complex one.
<div style="display:none"> <div style="display:none">
@ -104,15 +115,14 @@ And also we will listen the keyboard.
> createWindow "3D HOpengGL Mandelbrot" > createWindow "3D HOpengGL Mandelbrot"
> -- We add some directives > -- We add some directives
> depthFunc $= Just Less > depthFunc $= Just Less
> -- matrixMode $= Projection
> windowSize $= Size 500 500 > windowSize $= Size 500 500
> -- Some state variables (I know it feels BAD) > -- Some state variables (I know it feels BAD)
> angle <- newIORef ((35,0)::(GLfloat,GLfloat)) > angle <- newIORef ((35,0)::(GLfloat,GLfloat))
> zoom <- newIORef (2::GLfloat) > zoom <- newIORef (2::GLfloat)
> campos <- newIORef ((0.7,0)::(GLfloat,GLfloat)) > campos <- newIORef ((0.7,0)::(GLfloat,GLfloat))
> -- Action to call when waiting > -- Function to call each frame
> idleCallback $= Just idle > idleCallback $= Just idle
> -- We will use the keyboard > -- Function to call when keyboard or mouse is used
> keyboardMouseCallback $= > keyboardMouseCallback $=
> Just (keyboardMouse angle zoom campos) > Just (keyboardMouse angle zoom campos)
> -- Each time we will need to update the display > -- Each time we will need to update the display
@ -122,12 +132,16 @@ And also we will listen the keyboard.
> -- We enter the main loop > -- We enter the main loop
> mainLoop > mainLoop
The `idle` function necessary for animation. The `idle` is here to change the states.
There should never be any modification done in the `display` function.
> idle = postRedisplay Nothing > idle = postRedisplay Nothing
We introduce some helper function to manipulate We introduce some helper function to manipulate
standard `IORef`. standard `IORef`.
Mainly `modVar x f` is equivalent to the imperative `x:=f(x)`,
`modFst (x,y) (+1)` is equivalent to `(x,y) := (x+1,y)`
and `modSnd (x,y) (+1)` is equivalent to `(x,y) := (x,y+1)`
> modVar v f = do > modVar v f = do
> v' <- get v > v' <- get v
@ -139,23 +153,27 @@ And we use them to code the function handling keyboard.
We will use the keys `hjkl` to rotate, We will use the keys `hjkl` to rotate,
`oi` to zoom and `sedf` to move. `oi` to zoom and `sedf` to move.
Also, hitting space will reset the view. Also, hitting space will reset the view.
Remember that `angle` and `campos` are pairs and `zoom` is a scalar.
Also note `(+0.5)` is the function `\x->x+0.5`
and `(-0.5)` is the number `-0.5` (yes I share your pain).
> keyboardMouse angle zoom pos key state modifiers position = > keyboardMouse angle zoom campos key state modifiers position =
> kact angle zoom pos key state > -- We won't use modifiers nor position
> kact angle zoom campos key state
> where > where
> -- reset view when hitting space > -- reset view when hitting space
> kact a z p (Char ' ') Down = do > kact a z p (Char ' ') Down = do
> a $= (0,0) > a $= (0,0) -- angle
> z $= 1 > z $= 1 -- zoom
> p $= (0,0) > p $= (0,0) -- camera position
> -- use of hjkl to rotate > -- use of hjkl to rotate
> kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5)) > kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5))
> kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5))) > kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5)))
> kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5)) > kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5))
> kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5))) > kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5)))
> -- use o and i to zoom > -- use o and i to zoom
> kact _ s _ (Char 'o') Down = modVar s (*1.1) > kact _ z _ (Char 'o') Down = modVar z (*1.1)
> kact _ s _ (Char 'i') Down = modVar s (*0.9) > kact _ z _ (Char 'i') Down = modVar z (*0.9)
> -- use sdfe to move the camera > -- use sdfe to move the camera
> kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1)) > kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1))
> kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1))) > kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1)))
@ -164,9 +182,8 @@ Also, hitting space will reset the view.
> -- any other keys does nothing > -- any other keys does nothing
> kact _ _ _ _ _ = return () > kact _ _ _ _ _ = return ()
Now, we will show the object using the display function. Note `display` take some parameters this time.
Note, this time, display take some parameters. This function if full of boilerplate:
Mainly, this function if full of boilerplate:
> display angle zoom position = do > display angle zoom position = do
> -- set the background color (dark solarized theme) > -- set the background color (dark solarized theme)
@ -184,9 +201,11 @@ Mainly, this function if full of boilerplate:
> (xangle,yangle) <- get angle > (xangle,yangle) <- get angle
> rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat) > rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat)
> rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat) > rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat)
>
> -- Now that all transformation were made > -- Now that all transformation were made
> -- We create the object(s) > -- We create the object(s)
> preservingMatrix drawMandelbrot > preservingMatrix drawMandelbrot
>
> swapBuffers -- refresh screen > swapBuffers -- refresh screen
Not much to say about this function. Not much to say about this function.
@ -194,9 +213,9 @@ Mainly there are two parts: apply some transformations, draw the object.
### The 3D Mandelbrot ### The 3D Mandelbrot
Now, that we talked about the OpenGL part, let's talk about how we We have finished with the OpenGL section, let's talk about how we
generate the 3D points and colors. generate the 3D points and colors.
First, we will set the number of detatils to 180 pixels in the three dimensions. First, we will set the number of details to 200 pixels in the three dimensions.
> nbDetails = 200 :: GLfloat > nbDetails = 200 :: GLfloat
> width = nbDetails > width = nbDetails
@ -205,7 +224,9 @@ First, we will set the number of detatils to 180 pixels in the three dimensions.
This time, instead of just drawing some line or some group of points, This time, instead of just drawing some line or some group of points,
we will show triangles. we will show triangles.
The idea is that we should provide points three by three. The function `allPoints` will provide a multiple of three points.
Each three successive point representing the coordinate of each vertex of a triangle.
> drawMandelbrot = do > drawMandelbrot = do
> -- We will print Points (not triangles for example) > -- We will print Points (not triangles for example)
@ -216,14 +237,13 @@ The idea is that we should provide points three by three.
> color c > color c
> vertex $ Vertex3 x y z > vertex $ Vertex3 x y z
Now instead of providing only one point at a time, we will provide six ordered points. In fact, we will provide six ordered points.
These points will be used to draw two triangles. These points will be used to draw two triangles.
blogimage("triangles.png","Explain triangles") blogimage("triangles.png","Explain triangles")
Note in 3D the depth of the point is generally different.
The next function is a bit long. The next function is a bit long.
An approximative English version is: Here is an approximative English version:
~~~ ~~~
forall x from -width to width forall x from -width to width
@ -243,7 +263,8 @@ depthPoints = do
x <- [-width..width] x <- [-width..width]
y <- [-height..height] y <- [-height..height]
let let
depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep 7 depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep logdeep
logdeep = floor ((log deep) / log 2)
z1 = depthOf x y z1 = depthOf x y
z2 = depthOf (x+1) y z2 = depthOf (x+1) y
z3 = depthOf (x+1) (y+1) z3 = depthOf (x+1) (y+1)
@ -263,15 +284,16 @@ depthPoints = do
If you look at the function above, you see a lot of common patterns. If you look at the function above, you see a lot of common patterns.
Haskell is very efficient to make this better. Haskell is very efficient to make this better.
Here is a somehow less readable but more generic refactored function: Here is a harder to read but shorter and more generic rewritten function:
> depthPoints :: [ColoredPoint] > depthPoints :: [ColoredPoint]
> depthPoints = do > depthPoints = do
> x <- [-width..width] > x <- [-width..width]
> y <- [0..height] > y <- [-height..height]
> let > let
> neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)] > neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)]
> depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep 7 > depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep logdeep
> logdeep = floor ((log deep) / log 2)
> -- zs are 3D points with found depth > -- zs are 3D points with found depth
> zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors > zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors
> -- ts are 3D pixels + mandel value > -- ts are 3D pixels + mandel value
@ -288,22 +310,17 @@ Here is a somehow less readable but more generic refactored function:
If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example. If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example.
Also, we didn't searched for negative values. Also, we didn't searched for negative values.
For simplicity, I mirror these values. This modified Mandelbrot is no more symmetric relatively to the plan `y=0`.
I haven't even tested if this modified mandelbrot is symetric relatively to the plan {(x,y,z)|z=0}. But it is symmetric relatively to the plan `z=0`.
Then I mirror these values.
> allPoints :: [ColoredPoint] > allPoints :: [ColoredPoint]
> allPoints = planPoints ++ map inverseDepth planPoints > allPoints = planPoints ++ map inverseDepth planPoints
> where > where
> planPoints = depthPoints ++ map inverseHeight depthPoints > planPoints = depthPoints
> inverseHeight (x,y,z,c) = (x,-y,z,c)
> inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c) > inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c)
I cheat by making these symmetry. The rest of the program is very close to the preceding one.
But it is faster and render a nice form.
For this tutorial it will be good enough.
Also, the dichotomic method I use is mostly right but false for some cases.
The rest of the program is very close to the preceeding one.
<div style="display:none"> <div style="display:none">
@ -333,7 +350,8 @@ We only changed from `Complex` to `ExtComplex` of the main `f` function.
</div> </div>
We simply add a new dimenstion to the mandel function. Also we simply need to change the type signature of the function `f` from `Complex` to `ExtComplex`. We simply add a new dimension to the `mandel` function
and change the type signature of `f` from `Complex` to `ExtComplex`.
> mandel x y z = > mandel x y z =
> let r = 2.0 * x / width > let r = 2.0 * x / width
@ -343,9 +361,6 @@ We simply add a new dimenstion to the mandel function. Also we simply need to ch
> f (extcomplex r i s) 0 64 > f (extcomplex r i s) 0 64
And here is the result (if you use 500 for `nbDetails`): Here is the result:
blogimage("mandelbrot_3D.png","A 3D mandelbrot like") blogimage("mandelbrot_3D.png","A 3D mandelbrot like")
This image is quite nice.

View file

@ -1,8 +1,8 @@
## Cleaning the code ## Naïve code cleaning
The first thing to do is to separate the GLUT/OpenGL The first thing to do is to separate the GLUT/OpenGL
part from the computation of the shape. part from the computation of the shape.
Here is the cleaned version of the preceeding section. Here is the cleaned version of the preceding section.
Most boilerplate was put in external files. Most boilerplate was put in external files.
- [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering - [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering
@ -79,6 +79,3 @@ But I would have preferred to control the user actions.
On the other hand, we continue to handle a lot rendering details. On the other hand, we continue to handle a lot rendering details.
For example, we provide ordered vertices. For example, we provide ordered vertices.
I feel, this should be externalized.
I would have preferred to make things a bit more general.

View file

@ -18,7 +18,7 @@ Some points:
Then here is how I imagine things should go. Then here is how I imagine things should go.
First, what the main loop should look like: First, what the main loop should look like:
<code class="haskell"> <code class="no-highlight">
functionalMainLoop = functionalMainLoop =
Read user inputs and provide a list of actions Read user inputs and provide a list of actions
Apply all actions to the World Apply all actions to the World

View file

@ -227,10 +227,11 @@ yMainLoop inputActionMap
Just (keyboardMouse inputActionMap worldRef) Just (keyboardMouse inputActionMap worldRef)
-- We generate one frame using the callback -- We generate one frame using the callback
displayCallback $= display worldRef displayCallback $= display worldRef
normalize $= Enabled
-- Lights -- Lights
lighting $= Enabled lighting $= Enabled
ambient (Light 0) $= Color4 0 0 0 1 ambient (Light 0) $= Color4 0 0 0 1
diffuse (Light 0) $= Color4 1 1 1 1 diffuse (Light 0) $= Color4 0.5 0.5 0.5 1
specular (Light 0) $= Color4 1 1 1 1 specular (Light 0) $= Color4 1 1 1 1
position (Light 0) $= Vertex4 1 1 0 1 position (Light 0) $= Vertex4 1 1 0 1
light (Light 0) $= Enabled light (Light 0) $= Enabled
@ -239,7 +240,7 @@ yMainLoop inputActionMap
materialAmbient Front $= Color4 0.5 0.5 0.5 1 materialAmbient Front $= Color4 0.5 0.5 0.5 1
materialSpecular Front $= Color4 0.2 0.2 0.2 1 materialSpecular Front $= Color4 0.2 0.2 0.2 1
materialEmission Front $= Color4 0.3 0.3 0.3 1 materialEmission Front $= Color4 0.3 0.3 0.3 1
materialShininess Front $= 50.0 materialShininess Front $= 90.0
-- We enter the main loop -- We enter the main loop
mainLoop mainLoop

View file

@ -21,13 +21,13 @@ extcomplex :: GLfloat -> GLfloat -> GLfloat -> ExtComplex
extcomplex x y z = C (x,y,z) extcomplex x y z = C (x,y,z)
real :: ExtComplex -> GLfloat real :: ExtComplex -> GLfloat
real (C (x,y,z)) = x real (C (x,_,_)) = x
im :: ExtComplex -> GLfloat im :: ExtComplex -> GLfloat
im (C (x,y,z)) = y im (C (_,y,_)) = y
strange :: ExtComplex -> GLfloat strange :: ExtComplex -> GLfloat
strange (C (x,y,z)) = z strange (C (_,_,z)) = z
magnitude :: ExtComplex -> GLfloat magnitude :: ExtComplex -> GLfloat
magnitude = real.abs magnitude = real.abs

View file

@ -7,7 +7,7 @@ mandel r i s nbIterations =
f (extcomplex r i s) 0 nbIterations f (extcomplex r i s) 0 nbIterations
where where
f :: ExtComplex -> ExtComplex -> Int -> Int f :: ExtComplex -> ExtComplex -> Int -> Int
f c z 0 = 0 f _ _ 0 = 0
f c z n = if (magnitude z > 2 ) f c z n = if (magnitude z > 2 )
then n then n
else f c ((z*z)+c) (n-1) else f c ((z*z)+c) (n-1)

View file

@ -3,7 +3,7 @@
All feel good from the architecture point of vue. All feel good from the architecture point of vue.
More precisely, the separation between rendering and world behavior is clear. More precisely, the separation between rendering and world behavior is clear.
But this is extremely slow now. But this is extremely slow now.
Because we compute the mandelbulb for each frame now. Because we compute the Mandelbulb for each frame now.
Before we had Before we had
@ -31,7 +31,8 @@ function, we will provide the list of atoms directly.
> -- Centralize all user input interaction > -- Centralize all user input interaction
> inputActionMap :: InputMap World > inputActionMap :: InputMap World
> inputActionMap = inputMapFromList [ > inputActionMap = inputMapFromList [
> (Press 'k' , rotate xdir 5) > (Press ' ' , switch_rotation)
> ,(Press 'k' , rotate xdir 5)
> ,(Press 'i' , rotate xdir (-5)) > ,(Press 'i' , rotate xdir (-5))
> ,(Press 'j' , rotate ydir 5) > ,(Press 'j' , rotate ydir 5)
> ,(Press 'l' , rotate ydir (-5)) > ,(Press 'l' , rotate ydir (-5))
@ -45,14 +46,15 @@ function, we will provide the list of atoms directly.
> ,(Press 'r' , translate zdir (-0.1)) > ,(Press 'r' , translate zdir (-0.1))
> ,(Press '+' , zoom 1.1) > ,(Press '+' , zoom 1.1)
> ,(Press '-' , zoom (1/1.1)) > ,(Press '-' , zoom (1/1.1))
> ,(Press 'h' , resize 1.2) > ,(Press 'h' , resize 2.0)
> ,(Press 'g' , resize (1/1.2)) > ,(Press 'g' , resize (1/2.0))
> ] > ]
</div> </div>
> data World = World { > data World = World {
> angle :: Point3D > angle :: Point3D
> , anglePerSec :: Scalar
> , scale :: Scalar > , scale :: Scalar
> , position :: Point3D > , position :: Point3D
> , box :: Box3D > , box :: Box3D
@ -85,6 +87,11 @@ function, we will provide the list of atoms directly.
> world { > world {
> angle = (angle world) + (angleValue -*< dir) } > angle = (angle world) + (angleValue -*< dir) }
> >
> switch_rotation :: World -> World
> switch_rotation world =
> world {
> anglePerSec = if anglePerSec world > 0 then 0 else 5.0 }
>
> translate :: Point3D -> Scalar -> World -> World > translate :: Point3D -> Scalar -> World -> World
> translate dir len world = > translate dir len world =
> world { > world {
@ -108,11 +115,12 @@ Our initial world state is slightly changed:
> initialWorld :: World > initialWorld :: World
> initialWorld = World { > initialWorld = World {
> angle = makePoint3D (30,30,0) > angle = makePoint3D (30,30,0)
> , anglePerSec = 5.0
> , position = makePoint3D (0,0,0) > , position = makePoint3D (0,0,0)
> , scale = 1.0 > , scale = 1.0
> , box = Box3D { minPoint = makePoint3D (-2,-2,-2) > , box = Box3D { minPoint = makePoint3D (-2,-2,-2)
> , maxPoint = makePoint3D (2,2,2) > , maxPoint = makePoint3D (2,2,2)
> , resolution = 0.02 } > , resolution = 0.03 }
> , told = 0 > , told = 0
> -- We declare cache directly this time > -- We declare cache directly this time
> , cache = objectFunctionFromWorld initialWorld > , cache = objectFunctionFromWorld initialWorld
@ -124,11 +132,12 @@ This way instead of providing `XYFunc`, we provide directly a list of Atoms.
> objectFunctionFromWorld :: World -> [YObject] > objectFunctionFromWorld :: World -> [YObject]
> objectFunctionFromWorld w = [Atoms atomList] > objectFunctionFromWorld w = [Atoms atomList]
> where atomListPositive = > where atomListPositive =
> getObject3DFromShapeFunction (shapeFunc (resolution (box w))) (box w) > getObject3DFromShapeFunction
> (shapeFunc (resolution (box w))) (box w)
> atomList = atomListPositive ++ > atomList = atomListPositive ++
> map negativeTriangle atomListPositive > map negativeTriangle atomListPositive
> negativeTriangle (ColoredTriangle (p1,p2,p3,c)) = > negativeTriangle (ColoredTriangle (p1,p2,p3,c)) =
> ColoredTriangle (negz p1,negz p2,negz p3,c) > ColoredTriangle (negz p1,negz p3,negz p2,c)
> where negz (P (x,y,z)) = P (x,y,-z) > where negz (P (x,y,z)) = P (x,y,-z)
We know that resize is the only world change that necessitate to We know that resize is the only world change that necessitate to
@ -153,10 +162,18 @@ All the rest is exactly the same.
> , told = tnew > , told = tnew
> } > }
> where > where
> anglePerSec = 5.0 > delta = anglePerSec world * elapsed / 1000.0
> delta = anglePerSec * elapsed / 1000.0
> elapsed = fromIntegral (tnew - (told world)) > elapsed = fromIntegral (tnew - (told world))
> >
> shapeFunc' :: Scalar -> Function3D
> shapeFunc' res x y = if or [tmp u v>=0 | u<-[x,x+res], v<-[y,y+res]]
> then Just (z,hexColor "#AD4")
> else Nothing
> where tmp x y = (x**2 + y**2)
> protectSqrt t = if t<0 then 0 else sqrt t
> z = sqrt (a**2 - (c - protectSqrt(tmp x y))**2)
> a = 0.2
> c = 0.5
> shapeFunc :: Scalar -> Function3D > shapeFunc :: Scalar -> Function3D
> shapeFunc res x y = > shapeFunc res x y =
> let > let
@ -165,13 +182,13 @@ All the rest is exactly the same.
> if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 | > if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 |
> val <- [res], xeps <- [-val,val], yeps<-[-val,val]] > val <- [res], xeps <- [-val,val], yeps<-[-val,val]]
> then Nothing > then Nothing
> else Just (z,colorFromValue ((ymandel x y z) * 64)) > else Just (z,colorFromValue 0)
> >
> colorFromValue :: Point -> Color > colorFromValue :: Point -> Color
> colorFromValue n = > colorFromValue n =
> let > let
> t :: Point -> Scalar > t :: Point -> Scalar
> t i = 0.7 + 0.3*cos( i / 10 ) > t i = 0.0 + 0.5*cos( i /10 )
> in > in
> makeColor (t n) (t (n+5)) (t (n+10)) > makeColor (t n) (t (n+5)) (t (n+10))
> >

View file

@ -70,8 +70,7 @@ zpoint :: Point3D -> Point
zpoint (P (_,_,z)) = z zpoint (P (_,_,z)) = z
makePoint3D :: (Point,Point,Point) -> Point3D makePoint3D :: (Point,Point,Point) -> Point3D
makePoint3D p = P p makePoint3D = P
instance Num Point3D where instance Num Point3D where
(+) (P (ax,ay,az)) (P (bx,by,bz)) = P (ax+bx,ay+by,az+bz) (+) (P (ax,ay,az)) (P (bx,by,bz)) = P (ax+bx,ay+by,az+bz)
@ -230,26 +229,24 @@ yMainLoop inputActionMap
Just (keyboardMouse inputActionMap worldRef) Just (keyboardMouse inputActionMap worldRef)
-- We generate one frame using the callback -- We generate one frame using the callback
displayCallback $= display worldRef displayCallback $= display worldRef
normalize $= Enabled -- let OpenGL resize normal vectors to unity
shadeModel $= Smooth
-- Lights -- Lights
lighting $= Enabled lighting $= Enabled
ambient (Light 0) $= Color4 0 0 0 1 ambient (Light 0) $= Color4 0.5 0.5 0.5 1
diffuse (Light 0) $= Color4 1 1 1 1 diffuse (Light 0) $= Color4 1 1 1 1
specular (Light 0) $= Color4 1 1 1 1 -- specular (Light 0) $= Color4 1 1 1 1
position (Light 0) $= Vertex4 1 1 0 1 -- position (Light 0) $= Vertex4 (-5) 5 10 0
light (Light 0) $= Enabled light (Light 0) $= Enabled
ambient (Light 1) $= Color4 0 0 0 1 pointSmooth $= Enabled
diffuse (Light 1) $= Color4 1 0.9 0.0 1
specular (Light 1) $= Color4 1 1 1 1
position (Light 1) $= Vertex4 0 0 1 1
light (Light 1) $= Enabled
colorMaterial $= Just (Front,AmbientAndDiffuse) colorMaterial $= Just (Front,AmbientAndDiffuse)
-- materialDiffuse Front $= Color4 0.5 0.5 0.5 1 materialAmbient Front $= Color4 0.0 0.0 0.0 1
materialDiffuse Front $= Color4 0.5 0.5 0.5 1 materialDiffuse Front $= Color4 0.0 0.0 0.0 1
materialAmbient Front $= Color4 0.5 0.5 0.5 1 materialSpecular Front $= Color4 1 1 1 1
materialSpecular Front $= Color4 0.2 0.2 0.2 1 materialEmission Front $= Color4 0.0 0.0 0.0 1
materialEmission Front $= Color4 0.3 0.3 0.3 1
materialShininess Front $= 1.0
-- We enter the main loop -- We enter the main loop
materialShininess Front $= 96
mainLoop mainLoop
-- When no user input entered do nothing -- When no user input entered do nothing
@ -317,24 +314,20 @@ display worldRef = do
scalarFromHex :: String -> Scalar scalarFromHex :: String -> Scalar
scalarFromHex = (/256) . fst . head . readHex scalarFromHex = (/256) . fst . head . readHex
hexColor :: [Char] -> Color hexColor :: String -> Color
hexColor ('#':rd:ru:gd:gu:bd:bu:[]) = Color3 (scalarFromHex (rd:ru:[])) hexColor ('#':rd:ru:gd:gu:bd:bu:[]) = Color3 (scalarFromHex [rd,ru])
(scalarFromHex (gd:gu:[])) (scalarFromHex [gd,gu])
(scalarFromHex (bd:bu:[])) (scalarFromHex [bd,bu])
hexColor ('#':r:g:b:[]) = hexColor ('#':r:r:g:g:b:b:[]) hexColor ('#':r:g:b:[]) = hexColor ['#',r,r,g,g,b,b]
hexColor _ = error "Bad color!!!!" hexColor _ = error "Bad color!!!!"
makeColor :: Scalar -> Scalar -> Scalar -> Color makeColor :: Scalar -> Scalar -> Scalar -> Color
makeColor x y z = Color3 x y z makeColor = Color3
--- ---
-- drawObject :: (YObject obj) => obj -> IO() -- drawObject :: (YObject obj) => obj -> IO()
drawObject :: YObject -> IO() drawObject :: YObject -> IO()
drawObject shape = do drawObject shape = renderPrimitive Triangles $
-- We will print only Triangles
renderPrimitive Triangles $ do
-- solarized base3 color
-- color $ hexColor "#fdf603"
mapM_ drawAtom (atoms shape) mapM_ drawAtom (atoms shape)
-- simply draw an Atom -- simply draw an Atom

View file

@ -19,13 +19,13 @@
<uri>yannesposito.com</uri> <uri>yannesposito.com</uri>
</author> </author>
<link rel="alternate" href="http://yannesposito.com/Scratch/en/blog/Haskell-OpenGL-Mandelbrot/"/> <link rel="alternate" href="http://yannesposito.com/Scratch/en/blog/Haskell-OpenGL-Mandelbrot/"/>
<content type="html">&lt;p&gt;&lt;img alt="The plan in image" src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/HGL_Plan.png" /&gt;&lt;/p&gt; <content type="html">&lt;p&gt;&lt;img alt="The B in Beno&#238;t B. Mandelbrot stand for Beno&#238;t B. Mandelbrot" src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/BenoitBMandelbrot.jpg" /&gt;&lt;/p&gt;
&lt;div class="intro"&gt; &lt;div class="intro"&gt;
&lt;p&gt;&lt;span class="sc"&gt;&lt;abbr title="Too long; didn't read"&gt;tl;dr&lt;/abbr&gt;: &lt;/span&gt; A progressive real world example.&lt;/p&gt; &lt;p&gt;&lt;span class="sc"&gt;&lt;abbr title="Too long; didn't read"&gt;tl;dr&lt;/abbr&gt;: &lt;/span&gt; You will see how to go from theory to a real application using Haskell.&lt;/p&gt;
&lt;blockquote&gt; &lt;blockquote&gt;
&lt;center&gt;&lt;hr style="width:30%;float:left;border-color:#CCCCD0;margin-top:1em" /&gt;&lt;span class="sc"&gt;&lt;b&gt;Table of Content&lt;/b&gt;&lt;/span&gt;&lt;hr style="width:30%;float:right;border-color:#CCCCD0;margin-top:1em" /&gt;&lt;/center&gt; &lt;center&gt;&lt;hr style="width:30%;float:left;border-color:#CCCCD0;margin-top:1em" /&gt;&lt;span class="sc"&gt;&lt;b&gt;Table of Content&lt;/b&gt;&lt;/span&gt;&lt;hr style="width:30%;float:right;border-color:#CCCCD0;margin-top:1em" /&gt;&lt;/center&gt;
@ -34,10 +34,7 @@
&lt;li&gt;&lt;a href="#introduction"&gt;Introduction&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#introduction"&gt;Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#first-version"&gt;First version&lt;/a&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="#first-version"&gt;First version&lt;/a&gt; &lt;ul&gt;
&lt;li&gt;&lt;a href="#lets-play-the-song-of-our-people"&gt;Let&amp;rsquo;s play the song of our people&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#lets-play-the-song-of-our-people"&gt;Let&amp;rsquo;s play the song of our people&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#let-us-start"&gt;Let us start&lt;/a&gt;&lt;/li&gt; ...&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/hr&gt;&lt;/center&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/p&gt;</content>
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/hr&gt;&lt;/center&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/p&gt;</content>
</entry> </entry>
<entry> <entry>
<id>tag:yannesposito.com,2012-02-08:/Scratch/en/blog/Haskell-the-Hard-Way/</id> <id>tag:yannesposito.com,2012-02-08:/Scratch/en/blog/Haskell-the-Hard-Way/</id>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="fr" xml:lang="fr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="jquery.cookie.js"></script>
<script>
$(document).ready(function(){
$.cookie('admin',1);
$('#info').html('Analytics can no more see you.')
});
</script>
<title>Hide to analytics</title>
</head>
<body>
<div id="info"></div>
</body>
</html>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="fr" xml:lang="fr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="jquery.cookie.js"></script>
<script>
$(document).ready(function(){
$.cookie('admin',null);
$('#info').html('Analytics can see you.')
});
</script>
<title>Hide to analytics</title>
</head>
<body>
<div id="info"></div>
</body>
</html>

View file

@ -1,31 +1,49 @@
## Introduction ## Introduction
TODO: write something nice after reading. I wanted to go further than my
[preceding article](/Scratch/en/blog/Haskell-the-Hard-Way/) in which I introduced Haskell.
Steps: Instead of arguing that Haskell is better, because it is functional and "Functional Programming! Yeah!", I'll give an example of what benefit
functional programming can provide.
This article is more about functional paradigm than functional language.
The code organization can be used in most imperative language.
As Haskell is designed for functional paradigm, it is easier to talk about functional paradigm using it.
In reality, in the firsts sections I use an imperative paradigm.
As you can use functional paradigm in imperative language,
you can also use imperative paradigm in functional languages.
1. Mandelbrot set with Haskell OpenGL This article is about creating a useful program.
2. Mandelbrot edges It can interact with the user in real time.
3. 3D Mandelbrot because its fun It uses OpenGL, a library with imperative programming foundations.
4. Clean the code from full impure and imperative to purer and purer. But the final code will be quite clean.
5. Refactor the code to separate nicely important parts Most of the code will remain in the pure part (no `IO`).
6. Improve efficiency
I believe the main audience for this article are:
- Haskell programmer looking for an OpengGL tutorial.
- People interested in program organization (programming language agnostic).
- Fractal lovers and in particular 3D fractal.
- Game programmers (any language)
I wanted to talk about something cool.
For example I always wanted to make a Mandelbrot set explorer.
I had written a [command line Mandelbrot set generator in Haskell](http://github.com/yogsototh/mandelbrot.git).
The cool part of this utility is that it use all the cores to make the computation (it uses the `repa` package)[^001].
[^001]: Unfortunately, I couldn't make this program to work on my Mac. More precisely, I couldn't make the [DevIL](http://openil.sourceforge.net/) library work on Mac to output the image. Yes I have done a `brew install libdevil`. But even a minimal program who simply write some `jpg` didn't worked.
This time, we will display the Mandelbrot set extended in 3D using OpenGL and Haskell.
You will be able to move it using your keyboard.
This object is a Mandelbrot set in the plan (z=0),
and something nice to see in 3D.
Here is what you'll end with:
blogimage("GoldenMandelbulb.png","A golden mandelbulb")
And here are the intermediate steps:
blogimage("HGL_Plan.png","The parts of the article")
From 1 to 3 it will be _dirtier_ and _dirtier_. From 1 to 3 it will be _dirtier_ and _dirtier_.
At 4, we will make some order in this mess! We start cleaning everything at the 4th part.
Hopefuly for the best!
One of the goal of this article is to show some good properties of Haskell.
In particular, how to make some real world application with a pure functional language.
I know drawing a simple mandelbrot set isn't a "real world" application.
But the idea is not to show you a real world application which would be hard to follows, but to give you a way to pass from the pure mindset to some real world application.
To this, I will show you how should progress an application.
It is not something easy to show.
This is why, I preferred work with a program that generate some image.
In a real world application, the first constraint would be to work with some framework.
And generally an imperative one.
Also, the imperative nature of OpenGL make it the perfect choice for an example.

View file

@ -1,13 +1,10 @@
## First version ## First version
We can consider two parts. We can consider two parts.
The first being mostly some boilerplate[^1]. The first being mostly some boilerplate[^011].
The second part, contain more interesting stuff. And the second part more focused on OpenGL and content.
Even in this part, there are some necessary boilerplate.
But it is due to the OpenGL library this time.
[^011]: Generally in Haskell you need to declare a lot of import lines.
[^1]: Generally in Haskell you need to declare a lot of import lines.
This is something I find annoying. This is something I find annoying.
In particular, it should be possible to create a special file, Import.hs 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. which make all the necessary import for you, as you generally need them all.
@ -50,9 +47,6 @@ We declare some useful functions for manipulating complex numbers:
### Let us start ### Let us start
Well, up until here we didn't made something useful.
Just a lot of boilerplate and default value.
Sorry but it is not completely the end.
We start by giving the main architecture of our program: We start by giving the main architecture of our program:
> main :: IO () > main :: IO ()
@ -69,7 +63,8 @@ We start by giving the main architecture of our program:
> -- We enter the main loop > -- We enter the main loop
> mainLoop > mainLoop
The only interesting part is we declared that the function `display` will be used to render the graphics: Mainly, we initialize our OpenGL application.
We declared that the function `display` will be used to render the graphics:
> display = do > display = do
> clear [ColorBuffer] -- make the window black > clear [ColorBuffer] -- make the window black
@ -77,12 +72,12 @@ The only interesting part is we declared that the function `display` will be use
> preservingMatrix drawMandelbrot > preservingMatrix drawMandelbrot
> swapBuffers -- refresh screen > swapBuffers -- refresh screen
Also here, there is only one interesting part, Also here, there is only one interesting line;
the draw will occurs in the function `drawMandelbrot`. the draw will occur in the function `drawMandelbrot`.
Now we must speak a bit about how OpenGL works. This function will provide a list of draw actions.
We said that OpenGL is imperative by design. Remember that OpenGL is imperative by design.
In fact, you must write the list of actions in the right order. Then, one of the consequence is you must write the actions in the right order.
No easy parallel drawing here. No easy parallel drawing here.
Here is the function which will render something on the screen: Here is the function which will render something on the screen:
@ -111,8 +106,8 @@ drawMandelbrot =
~~~ ~~~
We also need some kind of global variables. We also need some kind of global variables.
In fact, global variable are a proof of some bad design. In fact, global variable are a proof of a design problem.
But remember it is our first try: We will get rid of them later.
> width = 320 :: GLfloat > width = 320 :: GLfloat
> height = 320 :: GLfloat > height = 320 :: GLfloat
@ -135,7 +130,7 @@ We need a function which transform an integer value to some color:
> in > in
> Color3 (t n) (t (n+5)) (t (n+10)) > Color3 (t n) (t (n+5)) (t (n+10))
And now the mandel function. And now the `mandel` function.
Given two coordinates in pixels, it returns some integer value: Given two coordinates in pixels, it returns some integer value:
> mandel x y = > mandel x y =
@ -144,8 +139,8 @@ Given two coordinates in pixels, it returns some integer value:
> in > in
> f (complex r i) 0 64 > f (complex r i) 0 64
It uses the main mandelbrot function for each complex \\(c\\). It uses the main Mandelbrot function for each complex \\(c\\).
The mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity. The Mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity.
Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\) Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\)
@ -163,15 +158,15 @@ Of course, instead of trying to test the real limit, we just make a test after a
> then n > then n
> else f c ((z*z)+c) (n-1) > else f c ((z*z)+c) (n-1)
Well, if you download this lhs file, compile it and run it this is the result: Well, if you download this file (look at the bottom of this section), compile it and run it this is the result:
blogimage("hglmandel_v01.png","The mandelbrot set version 1") blogimage("hglmandel_v01.png","The mandelbrot set version 1")
A first very interesting property of this program is that the computation for all the points is done only once. A first very interesting property of this program is that the computation for all the points is done only once.
The proof is that it might be a bit long before a first image appears, but if you resize the window, it updates instantaneously. 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. This property is a direct consequence of purity.
If you look closely, you see that `allPoints` is a pure list. If you look closely, you see that `allPoints` is a pure list.
Therefore, calling `allPoints` will always render the same result. Therefore, calling `allPoints` will always render the same result and Haskell is clever enough to use this property.
While Haskell doesn't garbage collect `allPoints` the result is reused for free. While Haskell doesn't garbage collect `allPoints` the result is reused for free.
We didn't specified this value should be saved for later use. We didn't specified this value should be saved for later use.
It is saved for us. It is saved for us.
@ -180,7 +175,6 @@ See what occurs if we make the window bigger:
blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns") blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns")
Yep, we see some black lines. We see some black lines because we drawn less point than there is on the surface.
Why? Simply because we drawn less point than there is on the surface.
We can repair this by drawing little squares instead of just points. We can repair this by drawing little squares instead of just points.
But, instead we will do something a bit different and unusual. But, instead we will do something a bit different and unusual.

View file

@ -51,6 +51,10 @@
</div> </div>
This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set. This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set.
The method I use is a rough approximation.
I consider the Mandelbrot set to be almost convex.
The result will be good enough.
We change slightly the drawMandelbrot function. We change slightly the drawMandelbrot function.
We replace the `Points` by `LineLoop` We replace the `Points` by `LineLoop`
@ -73,18 +77,19 @@ we will choose only point on the surface.
> map (\(x,y,c) -> (x,-y,c)) (reverse positivePoints) > map (\(x,y,c) -> (x,-y,c)) (reverse positivePoints)
We only need to compute the positive point. We only need to compute the positive point.
The mandelbrot set is symetric on the abscisse axis. The Mandelbrot set is symmetric on the abscisse axis.
> positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)] > positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)]
> positivePoints = do > positivePoints = do
> x <- [-width..width] > x <- [-width..width]
> let y = findMaxOrdFor (mandel x) 0 height 10 -- log height > let y = findMaxOrdFor (mandel x) 0 height (log2 height)
> if y < 1 -- We don't draw point in the absciss > if y < 1 -- We don't draw point in the absciss
> then [] > then []
> else return (x/width,y/height,colorFromValue $ mandel x y) > else return (x/width,y/height,colorFromValue $ mandel x y)
> where
> log2 n = floor ((log n) / log 2)
This function is interesting.
This function is interresting.
For those not used to the list monad here is a natural language version of this function: For those not used to the list monad here is a natural language version of this function:
~~~ ~~~
@ -96,7 +101,7 @@ positivePoints =
~~~ ~~~
In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically. In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically.
To find the smallest number such that mandel x y > 0 we create a simple dichotomic search: To find the smallest number such that `mandel x y > 0` we use a simple dichotomy:
> findMaxOrdFor func minval maxval 0 = (minval+maxval)/2 > findMaxOrdFor func minval maxval 0 = (minval+maxval)/2
> findMaxOrdFor func minval maxval n = > findMaxOrdFor func minval maxval n =
@ -105,9 +110,7 @@ To find the smallest number such that mandel x y > 0 we create a simple dichotom
> else findMaxOrdFor func medpoint maxval (n-1) > else findMaxOrdFor func medpoint maxval (n-1)
> where medpoint = (minval+maxval)/2 > where medpoint = (minval+maxval)/2
No rocket science here. No rocket science here. See the result now:
I know, due to the fact the mandelbrot set is not convex this approach does some errors. But the approximation will be good enough.
See the result now:
blogimage("HGLMandelEdges.png","The edges of the mandelbrot set") blogimage("HGLMandelEdges.png","The edges of the mandelbrot set")

View file

@ -1,20 +1,21 @@
## 3D Mandelbrot? ## 3D Mandelbrot?
Why only draw the edge? Now we will we extend to a third dimension.
It is clearly not as nice as drawing the complete surface. But, there is no 3D equivalent to complex.
Yeah, I know, but, as we use OpenGL, why not show something in 3D. In fact, the only extension known are quaternions (in 4D).
As I know almost nothing about quaternions, I will use some extended complex,
But, complex number are only in 2D and there is no 3D equivalent to complex. instead of using a 3D projection of quaternions.
In fact, the only extension known are quaternions, 4D.
As I know almost nothing about quaternions, I will use some extended complex.
I am pretty sure this construction is not useful for numbers. I am pretty sure this construction is not useful for numbers.
But it will be enough for us to create something nice. But it will be enough for us to create something that look nice.
As there is a lot of code, I'll give a high level view to what occurs: This section is quite long, but don't be afraid,
most of the code is some OpenGL boilerplate.
For those you want to skim,
here is a high level representation:
> - OpenGL Boilerplate > - OpenGL Boilerplate
> >
> - set some IORef for states > - set some IORef (understand variables) for states
> - Drawing: > - Drawing:
> >
> - set doubleBuffer, handle depth, window size... > - set doubleBuffer, handle depth, window size...
@ -49,8 +50,8 @@ As there is a lot of code, I'll give a high level view to what occurs:
</div> </div>
We declare a new type `ExtComplex` (for exttended complex). We declare a new type `ExtComplex` (for extended complex).
An extension of complex numbers: An extension of complex numbers with a third component:
> data ExtComplex = C (GLfloat,GLfloat,GLfloat) > data ExtComplex = C (GLfloat,GLfloat,GLfloat)
> deriving (Show,Eq) > deriving (Show,Eq)
@ -67,7 +68,17 @@ An extension of complex numbers:
> signum (C (x,y,z)) = C (signum x, 0, 0) > signum (C (x,y,z)) = C (signum x, 0, 0)
The most important part is the new multiplication instance. The most important part is the new multiplication instance.
Modifying this formula will change radically the shape of this somehow 3D mandelbrot. Modifying this formula will change radically the shape of the result.
Here is the formula written in a more mathematical notation.
I called the third component of these extended complex _strange_.
$$ \mathrm{real} ((x,y,z) * (x',y',z')) = xx' - yy' - zz' $$
$$ \mathrm{im} ((x,y,z) * (x',y',z')) = xy' - yx' + zz' $$
$$ \mathrm{strange} ((x,y,z) * (x',y',z')) = xz' + zx' $$
Note how if `z=z'=0` then the multiplication is the same to the complex one.
<div style="display:none"> <div style="display:none">
@ -104,15 +115,14 @@ And also we will listen the keyboard.
> createWindow "3D HOpengGL Mandelbrot" > createWindow "3D HOpengGL Mandelbrot"
> -- We add some directives > -- We add some directives
> depthFunc $= Just Less > depthFunc $= Just Less
> -- matrixMode $= Projection
> windowSize $= Size 500 500 > windowSize $= Size 500 500
> -- Some state variables (I know it feels BAD) > -- Some state variables (I know it feels BAD)
> angle <- newIORef ((35,0)::(GLfloat,GLfloat)) > angle <- newIORef ((35,0)::(GLfloat,GLfloat))
> zoom <- newIORef (2::GLfloat) > zoom <- newIORef (2::GLfloat)
> campos <- newIORef ((0.7,0)::(GLfloat,GLfloat)) > campos <- newIORef ((0.7,0)::(GLfloat,GLfloat))
> -- Action to call when waiting > -- Function to call each frame
> idleCallback $= Just idle > idleCallback $= Just idle
> -- We will use the keyboard > -- Function to call when keyboard or mouse is used
> keyboardMouseCallback $= > keyboardMouseCallback $=
> Just (keyboardMouse angle zoom campos) > Just (keyboardMouse angle zoom campos)
> -- Each time we will need to update the display > -- Each time we will need to update the display
@ -122,12 +132,16 @@ And also we will listen the keyboard.
> -- We enter the main loop > -- We enter the main loop
> mainLoop > mainLoop
The `idle` function necessary for animation. The `idle` is here to change the states.
There should never be any modification done in the `display` function.
> idle = postRedisplay Nothing > idle = postRedisplay Nothing
We introduce some helper function to manipulate We introduce some helper function to manipulate
standard `IORef`. standard `IORef`.
Mainly `modVar x f` is equivalent to the imperative `x:=f(x)`,
`modFst (x,y) (+1)` is equivalent to `(x,y) := (x+1,y)`
and `modSnd (x,y) (+1)` is equivalent to `(x,y) := (x,y+1)`
> modVar v f = do > modVar v f = do
> v' <- get v > v' <- get v
@ -139,23 +153,27 @@ And we use them to code the function handling keyboard.
We will use the keys `hjkl` to rotate, We will use the keys `hjkl` to rotate,
`oi` to zoom and `sedf` to move. `oi` to zoom and `sedf` to move.
Also, hitting space will reset the view. Also, hitting space will reset the view.
Remember that `angle` and `campos` are pairs and `zoom` is a scalar.
Also note `(+0.5)` is the function `\x->x+0.5`
and `(-0.5)` is the number `-0.5` (yes I share your pain).
> keyboardMouse angle zoom pos key state modifiers position = > keyboardMouse angle zoom campos key state modifiers position =
> kact angle zoom pos key state > -- We won't use modifiers nor position
> kact angle zoom campos key state
> where > where
> -- reset view when hitting space > -- reset view when hitting space
> kact a z p (Char ' ') Down = do > kact a z p (Char ' ') Down = do
> a $= (0,0) > a $= (0,0) -- angle
> z $= 1 > z $= 1 -- zoom
> p $= (0,0) > p $= (0,0) -- camera position
> -- use of hjkl to rotate > -- use of hjkl to rotate
> kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5)) > kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5))
> kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5))) > kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5)))
> kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5)) > kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5))
> kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5))) > kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5)))
> -- use o and i to zoom > -- use o and i to zoom
> kact _ s _ (Char 'o') Down = modVar s (*1.1) > kact _ z _ (Char 'o') Down = modVar z (*1.1)
> kact _ s _ (Char 'i') Down = modVar s (*0.9) > kact _ z _ (Char 'i') Down = modVar z (*0.9)
> -- use sdfe to move the camera > -- use sdfe to move the camera
> kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1)) > kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1))
> kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1))) > kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1)))
@ -164,9 +182,8 @@ Also, hitting space will reset the view.
> -- any other keys does nothing > -- any other keys does nothing
> kact _ _ _ _ _ = return () > kact _ _ _ _ _ = return ()
Now, we will show the object using the display function. Note `display` take some parameters this time.
Note, this time, display take some parameters. This function if full of boilerplate:
Mainly, this function if full of boilerplate:
> display angle zoom position = do > display angle zoom position = do
> -- set the background color (dark solarized theme) > -- set the background color (dark solarized theme)
@ -184,9 +201,11 @@ Mainly, this function if full of boilerplate:
> (xangle,yangle) <- get angle > (xangle,yangle) <- get angle
> rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat) > rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat)
> rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat) > rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat)
>
> -- Now that all transformation were made > -- Now that all transformation were made
> -- We create the object(s) > -- We create the object(s)
> preservingMatrix drawMandelbrot > preservingMatrix drawMandelbrot
>
> swapBuffers -- refresh screen > swapBuffers -- refresh screen
Not much to say about this function. Not much to say about this function.
@ -194,9 +213,9 @@ Mainly there are two parts: apply some transformations, draw the object.
### The 3D Mandelbrot ### The 3D Mandelbrot
Now, that we talked about the OpenGL part, let's talk about how we We have finished with the OpenGL section, let's talk about how we
generate the 3D points and colors. generate the 3D points and colors.
First, we will set the number of detatils to 180 pixels in the three dimensions. First, we will set the number of details to 200 pixels in the three dimensions.
> nbDetails = 200 :: GLfloat > nbDetails = 200 :: GLfloat
> width = nbDetails > width = nbDetails
@ -205,7 +224,9 @@ First, we will set the number of detatils to 180 pixels in the three dimensions.
This time, instead of just drawing some line or some group of points, This time, instead of just drawing some line or some group of points,
we will show triangles. we will show triangles.
The idea is that we should provide points three by three. The function `allPoints` will provide a multiple of three points.
Each three successive point representing the coordinate of each vertex of a triangle.
> drawMandelbrot = do > drawMandelbrot = do
> -- We will print Points (not triangles for example) > -- We will print Points (not triangles for example)
@ -216,14 +237,13 @@ The idea is that we should provide points three by three.
> color c > color c
> vertex $ Vertex3 x y z > vertex $ Vertex3 x y z
Now instead of providing only one point at a time, we will provide six ordered points. In fact, we will provide six ordered points.
These points will be used to draw two triangles. These points will be used to draw two triangles.
blogimage("triangles.png","Explain triangles") blogimage("triangles.png","Explain triangles")
Note in 3D the depth of the point is generally different.
The next function is a bit long. The next function is a bit long.
An approximative English version is: Here is an approximative English version:
~~~ ~~~
forall x from -width to width forall x from -width to width
@ -243,7 +263,8 @@ depthPoints = do
x <- [-width..width] x <- [-width..width]
y <- [-height..height] y <- [-height..height]
let let
depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep 7 depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep logdeep
logdeep = floor ((log deep) / log 2)
z1 = depthOf x y z1 = depthOf x y
z2 = depthOf (x+1) y z2 = depthOf (x+1) y
z3 = depthOf (x+1) (y+1) z3 = depthOf (x+1) (y+1)
@ -263,15 +284,16 @@ depthPoints = do
If you look at the function above, you see a lot of common patterns. If you look at the function above, you see a lot of common patterns.
Haskell is very efficient to make this better. Haskell is very efficient to make this better.
Here is a somehow less readable but more generic refactored function: Here is a harder to read but shorter and more generic rewritten function:
> depthPoints :: [ColoredPoint] > depthPoints :: [ColoredPoint]
> depthPoints = do > depthPoints = do
> x <- [-width..width] > x <- [-width..width]
> y <- [0..height] > y <- [-height..height]
> let > let
> neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)] > neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)]
> depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep 7 > depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep logdeep
> logdeep = floor ((log deep) / log 2)
> -- zs are 3D points with found depth > -- zs are 3D points with found depth
> zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors > zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors
> -- ts are 3D pixels + mandel value > -- ts are 3D pixels + mandel value
@ -288,22 +310,17 @@ Here is a somehow less readable but more generic refactored function:
If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example. If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example.
Also, we didn't searched for negative values. Also, we didn't searched for negative values.
For simplicity, I mirror these values. This modified Mandelbrot is no more symmetric relatively to the plan `y=0`.
I haven't even tested if this modified mandelbrot is symetric relatively to the plan {(x,y,z)|z=0}. But it is symmetric relatively to the plan `z=0`.
Then I mirror these values.
> allPoints :: [ColoredPoint] > allPoints :: [ColoredPoint]
> allPoints = planPoints ++ map inverseDepth planPoints > allPoints = planPoints ++ map inverseDepth planPoints
> where > where
> planPoints = depthPoints ++ map inverseHeight depthPoints > planPoints = depthPoints
> inverseHeight (x,y,z,c) = (x,-y,z,c)
> inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c) > inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c)
I cheat by making these symmetry. The rest of the program is very close to the preceding one.
But it is faster and render a nice form.
For this tutorial it will be good enough.
Also, the dichotomic method I use is mostly right but false for some cases.
The rest of the program is very close to the preceeding one.
<div style="display:none"> <div style="display:none">
@ -333,7 +350,8 @@ We only changed from `Complex` to `ExtComplex` of the main `f` function.
</div> </div>
We simply add a new dimenstion to the mandel function. Also we simply need to change the type signature of the function `f` from `Complex` to `ExtComplex`. We simply add a new dimension to the `mandel` function
and change the type signature of `f` from `Complex` to `ExtComplex`.
> mandel x y z = > mandel x y z =
> let r = 2.0 * x / width > let r = 2.0 * x / width
@ -343,9 +361,6 @@ We simply add a new dimenstion to the mandel function. Also we simply need to ch
> f (extcomplex r i s) 0 64 > f (extcomplex r i s) 0 64
And here is the result (if you use 500 for `nbDetails`): Here is the result:
blogimage("mandelbrot_3D.png","A 3D mandelbrot like") blogimage("mandelbrot_3D.png","A 3D mandelbrot like")
This image is quite nice.

View file

@ -1,8 +1,8 @@
## Cleaning the code ## Naïve code cleaning
The first thing to do is to separate the GLUT/OpenGL The first thing to do is to separate the GLUT/OpenGL
part from the computation of the shape. part from the computation of the shape.
Here is the cleaned version of the preceeding section. Here is the cleaned version of the preceding section.
Most boilerplate was put in external files. Most boilerplate was put in external files.
- [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering - [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering
@ -79,6 +79,3 @@ But I would have preferred to control the user actions.
On the other hand, we continue to handle a lot rendering details. On the other hand, we continue to handle a lot rendering details.
For example, we provide ordered vertices. For example, we provide ordered vertices.
I feel, this should be externalized.
I would have preferred to make things a bit more general.

View file

@ -18,7 +18,7 @@ Some points:
Then here is how I imagine things should go. Then here is how I imagine things should go.
First, what the main loop should look like: First, what the main loop should look like:
<code class="haskell"> <code class="no-highlight">
functionalMainLoop = functionalMainLoop =
Read user inputs and provide a list of actions Read user inputs and provide a list of actions
Apply all actions to the World Apply all actions to the World

View file

@ -227,10 +227,11 @@ yMainLoop inputActionMap
Just (keyboardMouse inputActionMap worldRef) Just (keyboardMouse inputActionMap worldRef)
-- We generate one frame using the callback -- We generate one frame using the callback
displayCallback $= display worldRef displayCallback $= display worldRef
normalize $= Enabled
-- Lights -- Lights
lighting $= Enabled lighting $= Enabled
ambient (Light 0) $= Color4 0 0 0 1 ambient (Light 0) $= Color4 0 0 0 1
diffuse (Light 0) $= Color4 1 1 1 1 diffuse (Light 0) $= Color4 0.5 0.5 0.5 1
specular (Light 0) $= Color4 1 1 1 1 specular (Light 0) $= Color4 1 1 1 1
position (Light 0) $= Vertex4 1 1 0 1 position (Light 0) $= Vertex4 1 1 0 1
light (Light 0) $= Enabled light (Light 0) $= Enabled
@ -239,7 +240,7 @@ yMainLoop inputActionMap
materialAmbient Front $= Color4 0.5 0.5 0.5 1 materialAmbient Front $= Color4 0.5 0.5 0.5 1
materialSpecular Front $= Color4 0.2 0.2 0.2 1 materialSpecular Front $= Color4 0.2 0.2 0.2 1
materialEmission Front $= Color4 0.3 0.3 0.3 1 materialEmission Front $= Color4 0.3 0.3 0.3 1
materialShininess Front $= 50.0 materialShininess Front $= 90.0
-- We enter the main loop -- We enter the main loop
mainLoop mainLoop

View file

@ -21,13 +21,13 @@ extcomplex :: GLfloat -> GLfloat -> GLfloat -> ExtComplex
extcomplex x y z = C (x,y,z) extcomplex x y z = C (x,y,z)
real :: ExtComplex -> GLfloat real :: ExtComplex -> GLfloat
real (C (x,y,z)) = x real (C (x,_,_)) = x
im :: ExtComplex -> GLfloat im :: ExtComplex -> GLfloat
im (C (x,y,z)) = y im (C (_,y,_)) = y
strange :: ExtComplex -> GLfloat strange :: ExtComplex -> GLfloat
strange (C (x,y,z)) = z strange (C (_,_,z)) = z
magnitude :: ExtComplex -> GLfloat magnitude :: ExtComplex -> GLfloat
magnitude = real.abs magnitude = real.abs

View file

@ -7,7 +7,7 @@ mandel r i s nbIterations =
f (extcomplex r i s) 0 nbIterations f (extcomplex r i s) 0 nbIterations
where where
f :: ExtComplex -> ExtComplex -> Int -> Int f :: ExtComplex -> ExtComplex -> Int -> Int
f c z 0 = 0 f _ _ 0 = 0
f c z n = if (magnitude z > 2 ) f c z n = if (magnitude z > 2 )
then n then n
else f c ((z*z)+c) (n-1) else f c ((z*z)+c) (n-1)

View file

@ -3,7 +3,7 @@
All feel good from the architecture point of vue. All feel good from the architecture point of vue.
More precisely, the separation between rendering and world behavior is clear. More precisely, the separation between rendering and world behavior is clear.
But this is extremely slow now. But this is extremely slow now.
Because we compute the mandelbulb for each frame now. Because we compute the Mandelbulb for each frame now.
Before we had Before we had
@ -31,7 +31,8 @@ function, we will provide the list of atoms directly.
> -- Centralize all user input interaction > -- Centralize all user input interaction
> inputActionMap :: InputMap World > inputActionMap :: InputMap World
> inputActionMap = inputMapFromList [ > inputActionMap = inputMapFromList [
> (Press 'k' , rotate xdir 5) > (Press ' ' , switch_rotation)
> ,(Press 'k' , rotate xdir 5)
> ,(Press 'i' , rotate xdir (-5)) > ,(Press 'i' , rotate xdir (-5))
> ,(Press 'j' , rotate ydir 5) > ,(Press 'j' , rotate ydir 5)
> ,(Press 'l' , rotate ydir (-5)) > ,(Press 'l' , rotate ydir (-5))
@ -45,14 +46,15 @@ function, we will provide the list of atoms directly.
> ,(Press 'r' , translate zdir (-0.1)) > ,(Press 'r' , translate zdir (-0.1))
> ,(Press '+' , zoom 1.1) > ,(Press '+' , zoom 1.1)
> ,(Press '-' , zoom (1/1.1)) > ,(Press '-' , zoom (1/1.1))
> ,(Press 'h' , resize 1.2) > ,(Press 'h' , resize 2.0)
> ,(Press 'g' , resize (1/1.2)) > ,(Press 'g' , resize (1/2.0))
> ] > ]
</div> </div>
> data World = World { > data World = World {
> angle :: Point3D > angle :: Point3D
> , anglePerSec :: Scalar
> , scale :: Scalar > , scale :: Scalar
> , position :: Point3D > , position :: Point3D
> , box :: Box3D > , box :: Box3D
@ -85,6 +87,11 @@ function, we will provide the list of atoms directly.
> world { > world {
> angle = (angle world) + (angleValue -*< dir) } > angle = (angle world) + (angleValue -*< dir) }
> >
> switch_rotation :: World -> World
> switch_rotation world =
> world {
> anglePerSec = if anglePerSec world > 0 then 0 else 5.0 }
>
> translate :: Point3D -> Scalar -> World -> World > translate :: Point3D -> Scalar -> World -> World
> translate dir len world = > translate dir len world =
> world { > world {
@ -108,11 +115,12 @@ Our initial world state is slightly changed:
> initialWorld :: World > initialWorld :: World
> initialWorld = World { > initialWorld = World {
> angle = makePoint3D (30,30,0) > angle = makePoint3D (30,30,0)
> , anglePerSec = 5.0
> , position = makePoint3D (0,0,0) > , position = makePoint3D (0,0,0)
> , scale = 1.0 > , scale = 1.0
> , box = Box3D { minPoint = makePoint3D (-2,-2,-2) > , box = Box3D { minPoint = makePoint3D (-2,-2,-2)
> , maxPoint = makePoint3D (2,2,2) > , maxPoint = makePoint3D (2,2,2)
> , resolution = 0.02 } > , resolution = 0.03 }
> , told = 0 > , told = 0
> -- We declare cache directly this time > -- We declare cache directly this time
> , cache = objectFunctionFromWorld initialWorld > , cache = objectFunctionFromWorld initialWorld
@ -124,11 +132,12 @@ This way instead of providing `XYFunc`, we provide directly a list of Atoms.
> objectFunctionFromWorld :: World -> [YObject] > objectFunctionFromWorld :: World -> [YObject]
> objectFunctionFromWorld w = [Atoms atomList] > objectFunctionFromWorld w = [Atoms atomList]
> where atomListPositive = > where atomListPositive =
> getObject3DFromShapeFunction (shapeFunc (resolution (box w))) (box w) > getObject3DFromShapeFunction
> (shapeFunc (resolution (box w))) (box w)
> atomList = atomListPositive ++ > atomList = atomListPositive ++
> map negativeTriangle atomListPositive > map negativeTriangle atomListPositive
> negativeTriangle (ColoredTriangle (p1,p2,p3,c)) = > negativeTriangle (ColoredTriangle (p1,p2,p3,c)) =
> ColoredTriangle (negz p1,negz p2,negz p3,c) > ColoredTriangle (negz p1,negz p3,negz p2,c)
> where negz (P (x,y,z)) = P (x,y,-z) > where negz (P (x,y,z)) = P (x,y,-z)
We know that resize is the only world change that necessitate to We know that resize is the only world change that necessitate to
@ -153,10 +162,18 @@ All the rest is exactly the same.
> , told = tnew > , told = tnew
> } > }
> where > where
> anglePerSec = 5.0 > delta = anglePerSec world * elapsed / 1000.0
> delta = anglePerSec * elapsed / 1000.0
> elapsed = fromIntegral (tnew - (told world)) > elapsed = fromIntegral (tnew - (told world))
> >
> shapeFunc' :: Scalar -> Function3D
> shapeFunc' res x y = if or [tmp u v>=0 | u<-[x,x+res], v<-[y,y+res]]
> then Just (z,hexColor "#AD4")
> else Nothing
> where tmp x y = (x**2 + y**2)
> protectSqrt t = if t<0 then 0 else sqrt t
> z = sqrt (a**2 - (c - protectSqrt(tmp x y))**2)
> a = 0.2
> c = 0.5
> shapeFunc :: Scalar -> Function3D > shapeFunc :: Scalar -> Function3D
> shapeFunc res x y = > shapeFunc res x y =
> let > let
@ -165,13 +182,13 @@ All the rest is exactly the same.
> if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 | > if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 |
> val <- [res], xeps <- [-val,val], yeps<-[-val,val]] > val <- [res], xeps <- [-val,val], yeps<-[-val,val]]
> then Nothing > then Nothing
> else Just (z,colorFromValue ((ymandel x y z) * 64)) > else Just (z,colorFromValue 0)
> >
> colorFromValue :: Point -> Color > colorFromValue :: Point -> Color
> colorFromValue n = > colorFromValue n =
> let > let
> t :: Point -> Scalar > t :: Point -> Scalar
> t i = 0.7 + 0.3*cos( i / 10 ) > t i = 0.0 + 0.5*cos( i /10 )
> in > in
> makeColor (t n) (t (n+5)) (t (n+10)) > makeColor (t n) (t (n+5)) (t (n+10))
> >

View file

@ -70,8 +70,7 @@ zpoint :: Point3D -> Point
zpoint (P (_,_,z)) = z zpoint (P (_,_,z)) = z
makePoint3D :: (Point,Point,Point) -> Point3D makePoint3D :: (Point,Point,Point) -> Point3D
makePoint3D p = P p makePoint3D = P
instance Num Point3D where instance Num Point3D where
(+) (P (ax,ay,az)) (P (bx,by,bz)) = P (ax+bx,ay+by,az+bz) (+) (P (ax,ay,az)) (P (bx,by,bz)) = P (ax+bx,ay+by,az+bz)
@ -230,26 +229,24 @@ yMainLoop inputActionMap
Just (keyboardMouse inputActionMap worldRef) Just (keyboardMouse inputActionMap worldRef)
-- We generate one frame using the callback -- We generate one frame using the callback
displayCallback $= display worldRef displayCallback $= display worldRef
normalize $= Enabled -- let OpenGL resize normal vectors to unity
shadeModel $= Smooth
-- Lights -- Lights
lighting $= Enabled lighting $= Enabled
ambient (Light 0) $= Color4 0 0 0 1 ambient (Light 0) $= Color4 0.5 0.5 0.5 1
diffuse (Light 0) $= Color4 1 1 1 1 diffuse (Light 0) $= Color4 1 1 1 1
specular (Light 0) $= Color4 1 1 1 1 -- specular (Light 0) $= Color4 1 1 1 1
position (Light 0) $= Vertex4 1 1 0 1 -- position (Light 0) $= Vertex4 (-5) 5 10 0
light (Light 0) $= Enabled light (Light 0) $= Enabled
ambient (Light 1) $= Color4 0 0 0 1 pointSmooth $= Enabled
diffuse (Light 1) $= Color4 1 0.9 0.0 1
specular (Light 1) $= Color4 1 1 1 1
position (Light 1) $= Vertex4 0 0 1 1
light (Light 1) $= Enabled
colorMaterial $= Just (Front,AmbientAndDiffuse) colorMaterial $= Just (Front,AmbientAndDiffuse)
-- materialDiffuse Front $= Color4 0.5 0.5 0.5 1 materialAmbient Front $= Color4 0.0 0.0 0.0 1
materialDiffuse Front $= Color4 0.5 0.5 0.5 1 materialDiffuse Front $= Color4 0.0 0.0 0.0 1
materialAmbient Front $= Color4 0.5 0.5 0.5 1 materialSpecular Front $= Color4 1 1 1 1
materialSpecular Front $= Color4 0.2 0.2 0.2 1 materialEmission Front $= Color4 0.0 0.0 0.0 1
materialEmission Front $= Color4 0.3 0.3 0.3 1
materialShininess Front $= 1.0
-- We enter the main loop -- We enter the main loop
materialShininess Front $= 96
mainLoop mainLoop
-- When no user input entered do nothing -- When no user input entered do nothing
@ -317,24 +314,20 @@ display worldRef = do
scalarFromHex :: String -> Scalar scalarFromHex :: String -> Scalar
scalarFromHex = (/256) . fst . head . readHex scalarFromHex = (/256) . fst . head . readHex
hexColor :: [Char] -> Color hexColor :: String -> Color
hexColor ('#':rd:ru:gd:gu:bd:bu:[]) = Color3 (scalarFromHex (rd:ru:[])) hexColor ('#':rd:ru:gd:gu:bd:bu:[]) = Color3 (scalarFromHex [rd,ru])
(scalarFromHex (gd:gu:[])) (scalarFromHex [gd,gu])
(scalarFromHex (bd:bu:[])) (scalarFromHex [bd,bu])
hexColor ('#':r:g:b:[]) = hexColor ('#':r:r:g:g:b:b:[]) hexColor ('#':r:g:b:[]) = hexColor ['#',r,r,g,g,b,b]
hexColor _ = error "Bad color!!!!" hexColor _ = error "Bad color!!!!"
makeColor :: Scalar -> Scalar -> Scalar -> Color makeColor :: Scalar -> Scalar -> Scalar -> Color
makeColor x y z = Color3 x y z makeColor = Color3
--- ---
-- drawObject :: (YObject obj) => obj -> IO() -- drawObject :: (YObject obj) => obj -> IO()
drawObject :: YObject -> IO() drawObject :: YObject -> IO()
drawObject shape = do drawObject shape = renderPrimitive Triangles $
-- We will print only Triangles
renderPrimitive Triangles $ do
-- solarized base3 color
-- color $ hexColor "#fdf603"
mapM_ drawAtom (atoms shape) mapM_ drawAtom (atoms shape)
-- simply draw an Atom -- simply draw an Atom

View file

@ -19,13 +19,13 @@
<uri>yannesposito.com</uri> <uri>yannesposito.com</uri>
</author> </author>
<link rel="alternate" href="http://yannesposito.com/Scratch/fr/blog/Haskell-OpenGL-Mandelbrot/"/> <link rel="alternate" href="http://yannesposito.com/Scratch/fr/blog/Haskell-OpenGL-Mandelbrot/"/>
<content type="html">&lt;p&gt;&lt;img alt="The plan in image" src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/HGL_Plan.png" /&gt;&lt;/p&gt; <content type="html">&lt;p&gt;&lt;img alt="The B in Beno&#238;t B. Mandelbrot stand for Beno&#238;t B. Mandelbrot" src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/BenoitBMandelbrot.jpg" /&gt;&lt;/p&gt;
&lt;div class="intro"&gt; &lt;div class="intro"&gt;
&lt;p&gt;&lt;span class="sc"&gt;&lt;abbr title="Trop long &#224; lire"&gt;tl&#224;l&lt;/abbr&gt;&amp;nbsp;: &lt;/span&gt; Un exemple progressif de programmation avec Haskell.&lt;/p&gt; &lt;p&gt;&lt;span class="sc"&gt;&lt;abbr title="Trop long &#224; lire"&gt;tl&#224;l&lt;/abbr&gt;&amp;nbsp;: &lt;/span&gt; Un exemple progressif d&amp;rsquo;utilisation d&amp;rsquo;Haskell.&lt;/p&gt;
&lt;blockquote&gt; &lt;blockquote&gt;
&lt;center&gt;&lt;hr style="width:30%;float:left;border-color:#CCCCD0;margin-top:1em" /&gt;&lt;span class="sc"&gt;&lt;b&gt;Table of Content&lt;/b&gt;&lt;/span&gt;&lt;hr style="width:30%;float:right;border-color:#CCCCD0;margin-top:1em" /&gt;&lt;/center&gt; &lt;center&gt;&lt;hr style="width:30%;float:left;border-color:#CCCCD0;margin-top:1em" /&gt;&lt;span class="sc"&gt;&lt;b&gt;Table of Content&lt;/b&gt;&lt;/span&gt;&lt;hr style="width:30%;float:right;border-color:#CCCCD0;margin-top:1em" /&gt;&lt;/center&gt;
@ -34,10 +34,7 @@
&lt;li&gt;&lt;a href="#introduction"&gt;Introduction&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#introduction"&gt;Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#first-version"&gt;First version&lt;/a&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="#first-version"&gt;First version&lt;/a&gt; &lt;ul&gt;
&lt;li&gt;&lt;a href="#lets-play-the-song-of-our-people"&gt;Let&amp;rsquo;s play the song of our people&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#lets-play-the-song-of-our-people"&gt;Let&amp;rsquo;s play the song of our people&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#let-us-start"&gt;Let us start&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/hr&gt;&lt;/center&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/p&gt;</content>
&lt;/ul&gt;
&lt;/li&gt;
...&lt;/ul&gt;&lt;/hr&gt;&lt;/center&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/p&gt;</content>
</entry> </entry>
<entry> <entry>
<id>tag:yannesposito.com,2012-02-08:/Scratch/fr/blog/Haskell-the-Hard-Way/</id> <id>tag:yannesposito.com,2012-02-08:/Scratch/fr/blog/Haskell-the-Hard-Way/</id>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="fr" xml:lang="fr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="shortcut icon" type="image/x-icon" href="/Scratch/img/favicon.ico" />
<link rel="stylesheet" type="text/css" href="/Scratch/css/twilight.css" />
<link rel="stylesheet" type="text/css" href="/Scratch/assets/css/layout.css" />
<link rel="stylesheet" type="text/css" href="/Scratch/css/shadows.css" />
<link rel="stylesheet" type="text/css" href="/Scratch/assets/css/gen.css" />
<link rel="alternate" type="application/rss+xml" title="RSS" href="http://feeds.feedburner.com/yannespositocomfr"/>
<link rel="stylesheet" type="text/css" href="/Scratch/css/js.css" />
<link rel="alternate" lang="fr" xml:lang="fr" title="Bienvenue" type="text/html" hreflang="fr" href="/Scratch/fr/" />
<link rel="alternate" lang="en" xml:lang="en" title="Welcome" type="text/html" hreflang="en" href="/Scratch/en/" />
<script type="text/javascript" src="/Scratch/js/jquery-1.3.1.min.js"></script>
<script type="text/javascript" src="/Scratch/js/jquery.cookie.js"></script>
<script>
$(document).ready(function(){
$.cookie('admin',1, { path: '/Scratch'});
$('#info').html('Analytics can no more see you.')
});
</script>
<title>Hide to analytics</title>
</head>
<body lang="fr">
<div id="content">
<div id="titre">
<h1>
Hide to Analytics
</h1>
</div>
<div class="flush"></div>
<div id="afterheader">
<div class="corps">
<div id="info">
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="fr" xml:lang="fr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="shortcut icon" type="image/x-icon" href="/Scratch/img/favicon.ico" />
<link rel="stylesheet" type="text/css" href="/Scratch/css/twilight.css" />
<link rel="stylesheet" type="text/css" href="/Scratch/assets/css/layout.css" />
<link rel="stylesheet" type="text/css" href="/Scratch/css/shadows.css" />
<link rel="stylesheet" type="text/css" href="/Scratch/assets/css/gen.css" />
<link rel="alternate" type="application/rss+xml" title="RSS" href="http://feeds.feedburner.com/yannespositocomfr"/>
<link rel="stylesheet" type="text/css" href="/Scratch/css/js.css" />
<link rel="alternate" lang="fr" xml:lang="fr" title="Bienvenue" type="text/html" hreflang="fr" href="/Scratch/fr/" />
<link rel="alternate" lang="en" xml:lang="en" title="Welcome" type="text/html" hreflang="en" href="/Scratch/en/" />
<script type="text/javascript" src="/Scratch/js/jquery-1.3.1.min.js"></script>
<script type="text/javascript" src="/Scratch/js/jquery.cookie.js"></script>
<script>
$(document).ready(function(){
$.cookie('admin',null, { path: '/Scratch'});
$('#info').html('Analytics can see you.')
});
</script>
<title>Be visible to analytics</title>
</head>
<body lang="fr">
<div id="content">
<div id="titre">
<h1>
Be visible to Analytics
</h1>
</div>
<div class="flush"></div>
<div id="afterheader">
<div class="corps">
<div id="info">
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,87 @@
<!-- saved from url=(0013)about:internet -->
<!-- ^^^ This is for IE not to show security warning for local files,
see http://www.microsoft.com/technet/prodtechnol/winxppro/maintain/sp2brows.mspx-->
<!--
Highlighted code export
Copyright (c) Vladimir Gubarkov <xonixx@gmail.com>
-->
<html>
<head>
<title>Highlited code export</title>
<link rel="stylesheet" href="styles/default.css">
<meta charset="utf-8">
<style type="text/css">
#t1, #t2 { width: 100%;}
tr { vertical-align: top; }
address { margin-top: 4em; }
</style>
<script src="highlight.pack.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body>
<script type="text/javascript">
String.prototype.escape = function() {
return this.replace(/&/gm, '&amp;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;');
}
function doIt() {
var viewDiv = document.getElementById("highlight-view");
var t1 = document.getElementById("t1");
var t2 = document.getElementById("t2");
var selector = document.getElementById("langSelector");
var selectedLang = selector.options[selector.selectedIndex].value.toLowerCase();
if(selectedLang) {
viewDiv.innerHTML = '<pre><code class="'+selectedLang+'">'+t1.value.escape()+"</code></pre>";
} else { // try auto
viewDiv.innerHTML = '<pre><code>' + t1.value.escape() + "</code></pre>";
}
hljs.highlightBlock(viewDiv.firstChild.firstChild);
t2.value = viewDiv.innerHTML;
}
function copyToBuffer(textToCopy) {
if (window.clipboardData) { // IE
window.clipboardData.setData("Text", textToCopy);
} else if (window.netscape) { // FF
// from http://developer.mozilla.org/en/docs/Using_the_Clipboard
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
var gClipboardHelper = Components.classes["@mozilla.org/widget/clipboardhelper;1"].getService(Components.interfaces.nsIClipboardHelper);
gClipboardHelper.copyString(textToCopy);
}
}
</script>
<script type="text/javascript">
var langSelectorHtml = '<label>Language <select id="langSelector">';
langSelectorHtml += '<option value="">Auto</option>';
for (var i in hljs.LANGUAGES) {
if (hljs.LANGUAGES.hasOwnProperty(i))
langSelectorHtml += '<option value=\"'+i+'\">'+i.charAt(0).toUpperCase()+i.substr(1)+'</option>';
}
langSelectorHtml += '</select></label>';
document.write(langSelectorHtml);
</script>
<table width="100%">
<tr>
<td><textarea rows="20" cols="50" id="t1"></textarea></td>
<td><textarea rows="20" cols="50" id="t2"></textarea></td>
</tr>
<tr>
<td>Write a code snippet</td>
<td>Get HTML to paste anywhere (for actual styles and colors see sample.css)</td>
</tr>
</table>
<table width="98%">
<tr>
<td><input type="button" value="Export &rarr;" onclick="doIt()"/></td>
<td align="right"><input type="button" value="Copy to buffer" onclick="copyToBuffer(document.getElementById('t2').value);"/></td>
</tr>
</table>
<div id="highlight-view"></div>
<address>
Export script: <a href="mailto:xonixx@gmail.com">Vladimir Gubarkov</a><br>
Highlighting: <a href="http://softwaremaniacs.org/soft/highlight/">highlight.js</a>
</address>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff