Modified .gitignore (shoud work out of heroku)
This commit is contained in:
parent
cf1b30a2d4
commit
cc0ff59931
38 changed files with 3708 additions and 1387 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -759,10 +797,10 @@ depthPoints = do
|
||||||
c2 = mandel (x+1) y (z2+1)
|
c2 = mandel (x+1) y (z2+1)
|
||||||
c3 = mandel (x+1) (y+1) (z3+1)
|
c3 = mandel (x+1) (y+1) (z3+1)
|
||||||
c4 = mandel x (y+1) (z4+1)
|
c4 = mandel x (y+1) (z4+1)
|
||||||
p1 = ( x /width, y /height, z1/deep,colorFromValue c1)
|
p1 = ( x /width, y /height, z1/deep, colorFromValue c1)
|
||||||
p2 = ((x+1)/width, y /height, z2/deep,colorFromValue c2)
|
p2 = ((x+1)/width, y /height, z2/deep, colorFromValue c2)
|
||||||
p3 = ((x+1)/width,(y+1)/height, z3/deep,colorFromValue c3)
|
p3 = ((x+1)/width,(y+1)/height, z3/deep, colorFromValue c3)
|
||||||
p4 = ( x /width,(y+1)/height, z4/deep,colorFromValue c4)
|
p4 = ( x /width,(y+1)/height, z4/deep, colorFromValue c4)
|
||||||
if (and $ map (>=57) [c1,c2,c3,c4])
|
if (and $ map (>=57) [c1,c2,c3,c4])
|
||||||
then []
|
then []
|
||||||
else [p1,p2,p3,p1,p3,p4]
|
else [p1,p2,p3,p1,p3,p4]
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -759,10 +797,10 @@ depthPoints = do
|
||||||
c2 = mandel (x+1) y (z2+1)
|
c2 = mandel (x+1) y (z2+1)
|
||||||
c3 = mandel (x+1) (y+1) (z3+1)
|
c3 = mandel (x+1) (y+1) (z3+1)
|
||||||
c4 = mandel x (y+1) (z4+1)
|
c4 = mandel x (y+1) (z4+1)
|
||||||
p1 = ( x /width, y /height, z1/deep,colorFromValue c1)
|
p1 = ( x /width, y /height, z1/deep, colorFromValue c1)
|
||||||
p2 = ((x+1)/width, y /height, z2/deep,colorFromValue c2)
|
p2 = ((x+1)/width, y /height, z2/deep, colorFromValue c2)
|
||||||
p3 = ((x+1)/width,(y+1)/height, z3/deep,colorFromValue c3)
|
p3 = ((x+1)/width,(y+1)/height, z3/deep, colorFromValue c3)
|
||||||
p4 = ( x /width,(y+1)/height, z4/deep,colorFromValue c4)
|
p4 = ( x /width,(y+1)/height, z4/deep, colorFromValue c4)
|
||||||
if (and $ map (>=57) [c1,c2,c3,c4])
|
if (and $ map (>=57) [c1,c2,c3,c4])
|
||||||
then []
|
then []
|
||||||
else [p1,p2,p3,p1,p3,p4]
|
else [p1,p2,p3,p1,p3,p4]
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -762,10 +800,10 @@ depthPoints = do
|
||||||
c2 = mandel (x+1) y (z2+1)
|
c2 = mandel (x+1) y (z2+1)
|
||||||
c3 = mandel (x+1) (y+1) (z3+1)
|
c3 = mandel (x+1) (y+1) (z3+1)
|
||||||
c4 = mandel x (y+1) (z4+1)
|
c4 = mandel x (y+1) (z4+1)
|
||||||
p1 = ( x /width, y /height, z1/deep,colorFromValue c1)
|
p1 = ( x /width, y /height, z1/deep, colorFromValue c1)
|
||||||
p2 = ((x+1)/width, y /height, z2/deep,colorFromValue c2)
|
p2 = ((x+1)/width, y /height, z2/deep, colorFromValue c2)
|
||||||
p3 = ((x+1)/width,(y+1)/height, z3/deep,colorFromValue c3)
|
p3 = ((x+1)/width,(y+1)/height, z3/deep, colorFromValue c3)
|
||||||
p4 = ( x /width,(y+1)/height, z4/deep,colorFromValue c4)
|
p4 = ( x /width,(y+1)/height, z4/deep, colorFromValue c4)
|
||||||
if (and $ map (>=57) [c1,c2,c3,c4])
|
if (and $ map (>=57) [c1,c2,c3,c4])
|
||||||
then []
|
then []
|
||||||
else [p1,p2,p3,p1,p3,p4]
|
else [p1,p2,p3,p1,p3,p4]
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
|
0
output/Scratch/blank.html
Normal file
0
output/Scratch/blank.html
Normal 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>
|
|
@ -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>
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -252,10 +273,10 @@ depthPoints = do
|
||||||
c2 = mandel (x+1) y (z2+1)
|
c2 = mandel (x+1) y (z2+1)
|
||||||
c3 = mandel (x+1) (y+1) (z3+1)
|
c3 = mandel (x+1) (y+1) (z3+1)
|
||||||
c4 = mandel x (y+1) (z4+1)
|
c4 = mandel x (y+1) (z4+1)
|
||||||
p1 = ( x /width, y /height, z1/deep,colorFromValue c1)
|
p1 = ( x /width, y /height, z1/deep, colorFromValue c1)
|
||||||
p2 = ((x+1)/width, y /height, z2/deep,colorFromValue c2)
|
p2 = ((x+1)/width, y /height, z2/deep, colorFromValue c2)
|
||||||
p3 = ((x+1)/width,(y+1)/height, z3/deep,colorFromValue c3)
|
p3 = ((x+1)/width,(y+1)/height, z3/deep, colorFromValue c3)
|
||||||
p4 = ( x /width,(y+1)/height, z4/deep,colorFromValue c4)
|
p4 = ( x /width,(y+1)/height, z4/deep, colorFromValue c4)
|
||||||
if (and $ map (>=57) [c1,c2,c3,c4])
|
if (and $ map (>=57) [c1,c2,c3,c4])
|
||||||
then []
|
then []
|
||||||
else [p1,p2,p3,p1,p3,p4]
|
else [p1,p2,p3,p1,p3,p4]
|
||||||
|
@ -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.
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
>
|
>
|
||||||
|
|
|
@ -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,25 +314,21 @@ 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
|
mapM_ drawAtom (atoms shape)
|
||||||
renderPrimitive Triangles $ do
|
|
||||||
-- solarized base3 color
|
|
||||||
-- color $ hexColor "#fdf603"
|
|
||||||
mapM_ drawAtom (atoms shape)
|
|
||||||
|
|
||||||
-- simply draw an Atom
|
-- simply draw an Atom
|
||||||
drawAtom :: Atom -> IO ()
|
drawAtom :: Atom -> IO ()
|
||||||
|
|
|
@ -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"><p><img alt="The plan in image" src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/HGL_Plan.png" /></p>
|
<content type="html"><p><img alt="The B in Benoît B. Mandelbrot stand for Benoît B. Mandelbrot" src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/BenoitBMandelbrot.jpg" /></p>
|
||||||
|
|
||||||
|
|
||||||
<div class="intro">
|
<div class="intro">
|
||||||
|
|
||||||
|
|
||||||
<p><span class="sc"><abbr title="Too long; didn't read">tl;dr</abbr>: </span> A progressive real world example.</p>
|
<p><span class="sc"><abbr title="Too long; didn't read">tl;dr</abbr>: </span> You will see how to go from theory to a real application using Haskell.</p>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<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>
|
||||||
|
@ -34,10 +34,7 @@
|
||||||
<li><a href="#introduction">Introduction</a></li>
|
<li><a href="#introduction">Introduction</a></li>
|
||||||
<li><a href="#first-version">First version</a> <ul>
|
<li><a href="#first-version">First version</a> <ul>
|
||||||
<li><a href="#lets-play-the-song-of-our-people">Let&rsquo;s play the song of our people</a></li>
|
<li><a href="#lets-play-the-song-of-our-people">Let&rsquo;s play the song of our people</a></li>
|
||||||
<li><a href="#let-us-start">Let us start</a></li>
|
...</ul></li></ul></hr></center></blockquote></div></p></content>
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li></li></ul></hr></center></blockquote></div></p></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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -252,10 +273,10 @@ depthPoints = do
|
||||||
c2 = mandel (x+1) y (z2+1)
|
c2 = mandel (x+1) y (z2+1)
|
||||||
c3 = mandel (x+1) (y+1) (z3+1)
|
c3 = mandel (x+1) (y+1) (z3+1)
|
||||||
c4 = mandel x (y+1) (z4+1)
|
c4 = mandel x (y+1) (z4+1)
|
||||||
p1 = ( x /width, y /height, z1/deep,colorFromValue c1)
|
p1 = ( x /width, y /height, z1/deep, colorFromValue c1)
|
||||||
p2 = ((x+1)/width, y /height, z2/deep,colorFromValue c2)
|
p2 = ((x+1)/width, y /height, z2/deep, colorFromValue c2)
|
||||||
p3 = ((x+1)/width,(y+1)/height, z3/deep,colorFromValue c3)
|
p3 = ((x+1)/width,(y+1)/height, z3/deep, colorFromValue c3)
|
||||||
p4 = ( x /width,(y+1)/height, z4/deep,colorFromValue c4)
|
p4 = ( x /width,(y+1)/height, z4/deep, colorFromValue c4)
|
||||||
if (and $ map (>=57) [c1,c2,c3,c4])
|
if (and $ map (>=57) [c1,c2,c3,c4])
|
||||||
then []
|
then []
|
||||||
else [p1,p2,p3,p1,p3,p4]
|
else [p1,p2,p3,p1,p3,p4]
|
||||||
|
@ -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.
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
>
|
>
|
||||||
|
|
|
@ -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,25 +314,21 @@ 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
|
mapM_ drawAtom (atoms shape)
|
||||||
renderPrimitive Triangles $ do
|
|
||||||
-- solarized base3 color
|
|
||||||
-- color $ hexColor "#fdf603"
|
|
||||||
mapM_ drawAtom (atoms shape)
|
|
||||||
|
|
||||||
-- simply draw an Atom
|
-- simply draw an Atom
|
||||||
drawAtom :: Atom -> IO ()
|
drawAtom :: Atom -> IO ()
|
||||||
|
|
|
@ -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"><p><img alt="The plan in image" src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/HGL_Plan.png" /></p>
|
<content type="html"><p><img alt="The B in Benoît B. Mandelbrot stand for Benoît B. Mandelbrot" src="/Scratch/img/blog/Haskell-OpenGL-Mandelbrot/BenoitBMandelbrot.jpg" /></p>
|
||||||
|
|
||||||
|
|
||||||
<div class="intro">
|
<div class="intro">
|
||||||
|
|
||||||
|
|
||||||
<p><span class="sc"><abbr title="Trop long à lire">tlàl</abbr>&nbsp;: </span> Un exemple progressif de programmation avec Haskell.</p>
|
<p><span class="sc"><abbr title="Trop long à lire">tlàl</abbr>&nbsp;: </span> Un exemple progressif d&rsquo;utilisation d&rsquo;Haskell.</p>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<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>
|
||||||
|
@ -34,10 +34,7 @@
|
||||||
<li><a href="#introduction">Introduction</a></li>
|
<li><a href="#introduction">Introduction</a></li>
|
||||||
<li><a href="#first-version">First version</a> <ul>
|
<li><a href="#first-version">First version</a> <ul>
|
||||||
<li><a href="#lets-play-the-song-of-our-people">Let&rsquo;s play the song of our people</a></li>
|
<li><a href="#lets-play-the-song-of-our-people">Let&rsquo;s play the song of our people</a></li>
|
||||||
<li><a href="#let-us-start">Let us start</a></li>
|
<li></li></ul></li></ul></hr></center></blockquote></div></p></content>
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
...</ul></hr></center></blockquote></div></p></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>
|
||||||
|
|
45
output/Scratch/js/become_hidden.html
Normal file
45
output/Scratch/js/become_hidden.html
Normal 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>
|
45
output/Scratch/js/become_visible.html
Normal file
45
output/Scratch/js/become_visible.html
Normal 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>
|
87
output/Scratch/js/highlight/export.html
Normal file
87
output/Scratch/js/highlight/export.html
Normal 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, '&').replace(/</gm, '<').replace(/>/gm, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 →" 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>
|
1845
output/Scratch/js/highlight/test.html
Normal file
1845
output/Scratch/js/highlight/test.html
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue