Added comment and small fixes
This commit is contained in:
parent
a643a17f7d
commit
1757474cce
2 changed files with 99 additions and 31 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue