Added comment and small fixes

This commit is contained in:
Yann Esposito 2012-05-23 17:01:43 +02:00
parent a643a17f7d
commit 1757474cce
2 changed files with 99 additions and 31 deletions

View file

@ -8,11 +8,11 @@ Some points:
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 impertive language we will continue to use the `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 writting these lines, I don't know if I'll do something looking close to that.
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.
@ -30,13 +30,20 @@ Clearly, ideally we should provide only three parameters to this main loop funct
- an initial World state
- a mapping between the user interaction and function which modify the world
- a function which transorm the world without user interaction.
- a function taking two parameters: time and world state and render a new world without user interaction.
The mapping between user input and actions.
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 [
@ -58,11 +65,9 @@ The mapping between user input and actions.
> ,(Press 'g' , resize (1/1.2))
> ]
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.
And of course a type design the World State:
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 {
@ -71,9 +76,15 @@ And of course a type design the World State:
> , 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,
@ -83,7 +94,14 @@ And of course a type design the World State:
> res = resolution $ box w
> defbox = box w
With all associated functions:
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)
@ -91,7 +109,10 @@ With all associated functions:
> 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 {
@ -111,17 +132,21 @@ With all associated functions:
> box = (box world) {
> resolution = sqrt ((resolution (box world))**2 * r) }}
- [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering
- [`Mandel`](code/04_Mandelbulb/Mandel.hs), the mandel function
- [`ExtComplex`](code/04_Mandelbulb/ExtComplex.hs), the extended complexes
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
>
> -- yMainLoop takes two arguments
> -- the title of the window
> -- a function from time to triangles
> main :: IO ()
> main = yMainLoop "3D Mandelbrot" inputActionMap initialWorld
>
> 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
@ -134,8 +159,32 @@ With all associated functions:
> , box = Box3D { minPoint = makePoint3D (-2,-2,-2)
> , maxPoint = makePoint3D (2,2,2)
> , resolution = 0.2 }
> , 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
@ -145,8 +194,9 @@ With all associated functions:
> val <- [res], xeps <- [-val,val], yeps<-[-val,val]]
> then Nothing
> else Just z
>
>
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
@ -158,3 +208,11 @@ With all associated functions:
>
> 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

View file

@ -13,6 +13,7 @@ Typically separate the display function.
module YGL (
-- Datas
Point
, Time
, Scalar
, Point3D
, makePoint3D -- helper (x,y,z) -> Point3D
@ -25,6 +26,7 @@ module YGL (
, Camera (..)
, YObject (..)
, Box3D (..)
, makeBox
-- Datas related to user Input
, InputMap
, UserInput (Press,Ctrl,Alt,CtrlAlt)
@ -48,6 +50,8 @@ import Data.Maybe (isNothing)
type Point = GLfloat
-- | A Scalar value
type Scalar = GLfloat
-- | The time type (currently its Int
type Time = Int
-- | A 3D Point mainly '(x,y,z)'
data Point3D = P (Point,Point,Point) deriving (Eq,Show,Read)
@ -127,6 +131,8 @@ class DisplayableWorld world where
lights _ = []
objects :: world -> [YObject]
objects _ = []
winTitle :: world -> String
winTitle _ = "YGL"
-- | the Camera type to know how to
-- | Transform the scene to see the right view.
@ -186,24 +192,24 @@ inputMapFromList = Map.fromList
- it will look like a standard function.
--}
yMainLoop :: (DisplayableWorld worldType) =>
String -- window name
-> InputMap worldType -- the mapping user input / world
InputMap worldType -- the mapping user input / world
-> (Time -> worldType -> worldType)
-> worldType -- the world state
-> IO () -- into IO () for obvious reason
yMainLoop winTitle
inputActionMap
yMainLoop inputActionMap
worldTranformer
world = do
-- The boilerplate
_ <- getArgsAndInitialize
initialDisplayMode $=
[WithDepthBuffer,DoubleBuffered,RGBMode]
_ <- createWindow winTitle
_ <- createWindow $ winTitle world
depthFunc $= Just Less
windowSize $= Size 500 500
-- The state variables for the world (I know it feels BAD)
worldRef <- newIORef world
-- Action to call when waiting
idleCallback $= Just idle
idleCallback $= Just (idle worldTranformer worldRef)
-- the keyboard will update the world
keyboardMouseCallback $=
Just (keyboardMouse inputActionMap worldRef)
@ -219,8 +225,12 @@ yMainLoop winTitle
mainLoop
-- When no user input entered do nothing
idle :: IO ()
idle = postRedisplay Nothing
idle :: (Time -> worldType -> worldType) -> IORef worldType -> IO ()
idle worldTranformer world = do
w <- get world
t <- get elapsedTime
world $= worldTranformer t w
postRedisplay Nothing
-- Get User Input
-- both cleaner, terser and more expendable than the preceeding code