229 lines
8 KiB
Text
229 lines
8 KiB
Text
## Functional organization?
|
|
|
|
Some points:
|
|
|
|
1. OpenGL and GLUT are linked to the C library.
|
|
In particular the `mainLoop` function is a direct link to the C library (FFI).
|
|
This function if so far from the pure spirit of functional languages.
|
|
Could we make this better?
|
|
We will have two choices, or create our own `mainLoop` function to make it more functional.
|
|
Or deal with the imperative nature of the GLUT `mainLoop` function.
|
|
As a goal of this article is to understand how to deal with existing library and particularly the one coming from imperative language we will continue to use the `mainLoop` function.
|
|
2. Or main problem come from user interaction.
|
|
If you ask the Internet, about how to deal with user interaction with a functional paradigm, the main answer is to use _functional reactive programming_ (FRP).
|
|
I read very few about FRP, and I might be completely wrong when I say that it is about creating a DSL where atoms are time functions.
|
|
While I'm writing these lines, I don't know if I'll do something looking close to that.
|
|
For now I'll simply try to resolve the first problem.
|
|
|
|
Then here is how I imagine things should go.
|
|
First, what the main loop should look like:
|
|
|
|
<code class="no-highlight">
|
|
functionalMainLoop =
|
|
Read user inputs and provide a list of actions
|
|
Apply all actions to the World
|
|
Display one frame
|
|
repetere aeternum
|
|
</code>
|
|
|
|
Clearly, ideally we should provide only three parameters to this main loop function:
|
|
|
|
- an initial World state
|
|
- a mapping between the user interaction and function which modify the world
|
|
- a function taking two parameters: time and world state and render a new world without user interaction.
|
|
|
|
Here is a real working code, I've hidden most display functions.
|
|
The YGL, is a kind of framework to display 3D functions.
|
|
But it can easily be extended to many kind of representation.
|
|
|
|
> import YGL -- Most the OpenGL Boilerplate
|
|
> import Mandel -- The 3D Mandelbrot maths
|
|
|
|
We first set the mapping between user input and actions.
|
|
The type of each couple should be of the form
|
|
`(user input, f)` where (in a first time) `f:World -> World`.
|
|
It means, the user input will transform the world state.
|
|
|
|
> -- Centralize all user input interaction
|
|
> inputActionMap :: InputMap World
|
|
> inputActionMap = inputMapFromList [
|
|
> (Press 'k' , rotate xdir 5)
|
|
> ,(Press 'i' , rotate xdir (-5))
|
|
> ,(Press 'j' , rotate ydir 5)
|
|
> ,(Press 'l' , rotate ydir (-5))
|
|
> ,(Press 'o' , rotate zdir 5)
|
|
> ,(Press 'u' , rotate zdir (-5))
|
|
> ,(Press 'f' , translate xdir 0.1)
|
|
> ,(Press 's' , translate xdir (-0.1))
|
|
> ,(Press 'e' , translate ydir 0.1)
|
|
> ,(Press 'd' , translate ydir (-0.1))
|
|
> ,(Press 'z' , translate zdir 0.1)
|
|
> ,(Press 'r' , translate zdir (-0.1))
|
|
> ,(Press '+' , zoom 1.1)
|
|
> ,(Press '-' , zoom (1/1.1))
|
|
> ,(Press 'h' , resize 1.2)
|
|
> ,(Press 'g' , resize (1/1.2))
|
|
> ]
|
|
|
|
And of course a type design the World State.
|
|
The important part is that it is our World State type.
|
|
We could have used any kind of data type.
|
|
|
|
> -- I prefer to set my own name for these types
|
|
> data World = World {
|
|
> angle :: Point3D
|
|
> , scale :: Scalar
|
|
> , position :: Point3D
|
|
> , shape :: Scalar -> Function3D
|
|
> , box :: Box3D
|
|
> , told :: Time -- last frame time
|
|
> }
|
|
|
|
The important part to glue our own type to the framework
|
|
is to make our type an instance of the type class `DisplayableWorld`.
|
|
We simply have to provide the definition of some functions.
|
|
|
|
> instance DisplayableWorld World where
|
|
> winTitle _ = "The YGL Mandelbulb"
|
|
> camera w = Camera {
|
|
> camPos = position w,
|
|
> camDir = angle w,
|
|
> camZoom = scale w }
|
|
> objects w = [XYFunc ((shape w) res) defbox]
|
|
> where
|
|
> res = resolution $ box w
|
|
> defbox = box w
|
|
|
|
The `camera` function will retrieve an object of type `Camera` which contains
|
|
most necessary information to set our camera.
|
|
The `objects` function will returns a list of objects.
|
|
Their type is `YObject`. Note the generation of triangles is no more in this file.
|
|
Until here we only used declarative pattern.
|
|
|
|
We also need to set all our transformation functions.
|
|
These function are used to update the world state.
|
|
|
|
> xdir :: Point3D
|
|
> xdir = makePoint3D (1,0,0)
|
|
> ydir :: Point3D
|
|
> ydir = makePoint3D (0,1,0)
|
|
> zdir :: Point3D
|
|
> zdir = makePoint3D (0,0,1)
|
|
|
|
Note `(-*<)` is scalar product.
|
|
Also note we could add Point3D as numbers.
|
|
|
|
> rotate :: Point3D -> Scalar -> World -> World
|
|
> rotate dir angleValue world =
|
|
> world {
|
|
> angle = (angle world) + (angleValue -*< dir) }
|
|
>
|
|
> translate :: Point3D -> Scalar -> World -> World
|
|
> translate dir len world =
|
|
> world {
|
|
> position = (position world) + (len -*< dir) }
|
|
>
|
|
> zoom :: Scalar -> World -> World
|
|
> zoom z world = world {
|
|
> scale = z * scale world }
|
|
>
|
|
> resize :: Scalar -> World -> World
|
|
> resize r world = world {
|
|
> box = (box world) {
|
|
> resolution = sqrt ((resolution (box world))**2 * r) }}
|
|
|
|
The resize is used to generate the 3D function.
|
|
As I wanted the time spent to generate a more detailed view
|
|
to grow linearly I use this not so straightforward formula.
|
|
|
|
The `yMainLoop` takes three arguments.
|
|
|
|
- A map between user Input and world transformation
|
|
- A timed world transformation
|
|
- An initial world state
|
|
|
|
> main :: IO ()
|
|
> main = yMainLoop inputActionMap idleAction initialWorld
|
|
|
|
Here is our initial world state.
|
|
|
|
> -- We initialize the world state
|
|
> -- then angle, position and zoom of the camera
|
|
> -- And the shape function
|
|
> initialWorld :: World
|
|
> initialWorld = World {
|
|
> angle = makePoint3D (-30,-30,0)
|
|
> , position = makePoint3D (0,0,0)
|
|
> , scale = 0.8
|
|
> , shape = shapeFunc
|
|
> , box = Box3D { minPoint = makePoint3D (-2,-2,-2)
|
|
> , maxPoint = makePoint3D (2,2,2)
|
|
> , resolution = 0.16 }
|
|
> , told = 0
|
|
> }
|
|
|
|
We will define `shapeFunc` later.
|
|
Here is the function which transform the world even without user action.
|
|
Mainly it makes some rotation.
|
|
|
|
> idleAction :: Time -> World -> World
|
|
> idleAction tnew world = world {
|
|
> angle = (angle world) + (delta -*< zdir)
|
|
> , told = tnew
|
|
> }
|
|
> where
|
|
> anglePerSec = 5.0
|
|
> delta = anglePerSec * elapsed / 1000.0
|
|
> elapsed = fromIntegral (tnew - (told world))
|
|
|
|
Now the function which will generate points in 3D.
|
|
The first parameter (`res`) is the resolution of the vertex generation.
|
|
More precisely, `res` is distance between two points on one direction.
|
|
We need it to "close" our shape.
|
|
|
|
The type `Function3D` is `Point -> Point -> Maybe Point`.
|
|
Because we consider partial functions
|
|
(for some `(x,y)` our function can be undefined).
|
|
|
|
> shapeFunc :: Scalar -> Function3D
|
|
> shapeFunc res x y =
|
|
> let
|
|
> z = findMaxOrdFor (ymandel x y) 0 1 20
|
|
> in
|
|
> if and [ findMaxOrdFor (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 |
|
|
> val <- [res], xeps <- [-val,val], yeps<-[-val,val]]
|
|
> then Nothing
|
|
> else Just (z,colorFromValue ((ymandel x y z) * 64))
|
|
|
|
With the color function.
|
|
|
|
> colorFromValue :: Point -> Color
|
|
> colorFromValue n =
|
|
> let
|
|
> t :: Point -> Scalar
|
|
> t i = 0.7 + 0.3*cos( i / 10 )
|
|
> in
|
|
> makeColor (t n) (t (n+5)) (t (n+10))
|
|
|
|
The rest is similar to the preceding sections.
|
|
|
|
> findMaxOrdFor :: (Fractional a,Num a,Num b,Eq b) =>
|
|
> (a -> b) -> a -> a -> Int -> a
|
|
> findMaxOrdFor _ minval maxval 0 = (minval+maxval)/2
|
|
> findMaxOrdFor func minval maxval n =
|
|
> if func medpoint /= 0
|
|
> then findMaxOrdFor func minval medpoint (n-1)
|
|
> else findMaxOrdFor func medpoint maxval (n-1)
|
|
> where medpoint = (minval+maxval)/2
|
|
>
|
|
> ymandel :: Point -> Point -> Point -> Point
|
|
> ymandel x y z = fromIntegral (mandel x y z 64) / 64
|
|
|
|
I won't put how the magic occurs directly here.
|
|
But all the magic occurs in the file `YGL.hs`.
|
|
This file is commented a lot.
|
|
|
|
- [`YGL.hs`](code/05_Mandelbulb/YGL.hs), the 3D rendering framework
|
|
- [`Mandel`](code/05_Mandelbulb/Mandel.hs), the mandel function
|
|
- [`ExtComplex`](code/05_Mandelbulb/ExtComplex.hs), the extended complexes
|
|
|