scratch/output/Scratch/en/blog/Haskell-OpenGL-Mandelbrot/code/06_Mandelbulb/Mandelbulb.lhs

221 lines
6.7 KiB
Text
Raw Normal View History

2012-06-01 13:24:58 +00:00
## Optimization
2012-06-15 13:25:51 +00:00
Our code architecture feel very clean.
All the meaningful code is in our main file and all display details are
externalized.
2012-06-14 16:04:16 +00:00
If you read the code of `YGL.hs`, you'll see I didn't made everything perfect.
For example, I didn't finished the code of the lights.
2012-06-13 15:59:29 +00:00
But I believe it is a good first step and it will be easy to go further.
Unfortunately the program of the preceding session is extremely slow.
We compute the Mandelbulb for each frame now.
2012-06-01 13:24:58 +00:00
2012-06-15 13:25:51 +00:00
Before our program structure was:
2012-06-01 13:24:58 +00:00
2012-06-13 15:59:29 +00:00
<code class="no-highlight">
Constant Function -> Constant List of Triangles -> Display
</code>
2012-06-01 13:24:58 +00:00
Now we have
2012-06-13 15:59:29 +00:00
<code class="no-highlight">
2012-06-15 13:25:51 +00:00
Main loop -> World -> Function -> List of Objects -> Atoms -> Display
2012-06-13 15:59:29 +00:00
</code>
2012-06-01 13:24:58 +00:00
2012-06-15 13:25:51 +00:00
The World state could change.
The compiler can no more optimize the computation for us.
We have to manually explain when to redraw the shape.
2012-06-01 13:24:58 +00:00
2012-06-15 13:25:51 +00:00
To optimize we must do some things in a lower level.
Mostly the program remains the same,
but it will provide the list of atoms directly.
2012-06-01 13:24:58 +00:00
<div style="display:none">
> import YGL -- Most the OpenGL Boilerplate
> import Mandel -- The 3D Mandelbrot maths
>
> -- Centralize all user input interaction
> inputActionMap :: InputMap World
> inputActionMap = inputMapFromList [
2012-06-15 10:02:30 +00:00
> (Press ' ' , switchRotation)
> ,(Press 'k' , rotate xdir 5)
2012-06-01 13:24:58 +00:00
> ,(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 2.0)
> ,(Press 'g' , resize (1/2.0))
2012-06-01 13:24:58 +00:00
> ]
</div>
> data World = World {
> angle :: Point3D
> , anglePerSec :: Scalar
2012-06-01 13:24:58 +00:00
> , scale :: Scalar
> , position :: Point3D
> , box :: Box3D
> , told :: Time
> -- We replace shape by cache
> , cache :: [YObject]
> }
> instance DisplayableWorld World where
> winTitle _ = "The YGL Mandelbulb"
> camera w = Camera {
> camPos = position w,
> camDir = angle w,
> camZoom = scale w }
> -- We update our objects instanciation
> objects = cache
<div style="display:none">
> xdir :: Point3D
> xdir = makePoint3D (1,0,0)
> ydir :: Point3D
> ydir = makePoint3D (0,1,0)
> zdir :: Point3D
> zdir = makePoint3D (0,0,1)
>
> rotate :: Point3D -> Scalar -> World -> World
> rotate dir angleValue world =
> world {
2012-06-15 10:02:30 +00:00
> angle = angle world + (angleValue -*< dir) }
>
2012-06-15 10:02:30 +00:00
> switchRotation :: World -> World
> switchRotation world =
> world {
> anglePerSec = if anglePerSec world > 0 then 0 else 5.0 }
2012-06-01 13:24:58 +00:00
>
> translate :: Point3D -> Scalar -> World -> World
> translate dir len world =
> world {
2012-06-15 10:02:30 +00:00
> position = position world + (len -*< dir) }
2012-06-01 13:24:58 +00:00
>
> zoom :: Scalar -> World -> World
> zoom z world = world {
> scale = z * scale world }
> main :: IO ()
> main = yMainLoop inputActionMap idleAction initialWorld
</div>
Our initial world state is slightly changed:
> -- 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)
> , anglePerSec = 5.0
2012-06-01 13:24:58 +00:00
> , position = makePoint3D (0,0,0)
> , scale = 1.0
2012-06-14 16:04:16 +00:00
> , box = Box3D { minPoint = makePoint3D (0-eps, 0-eps, 0-eps)
> , maxPoint = makePoint3D (0+eps, 0+eps, 0+eps)
> , resolution = 0.02 }
2012-06-01 13:24:58 +00:00
> , told = 0
> -- We declare cache directly this time
> , cache = objectFunctionFromWorld initialWorld
> }
2012-06-14 16:04:16 +00:00
> where eps=2
2012-06-01 13:24:58 +00:00
2012-06-15 13:25:51 +00:00
The use of `eps` is a hint to make a better zoom by computing with the right bounds.
2012-06-01 13:24:58 +00:00
We use the `YGL.getObject3DFromShapeFunction` function directly.
This way instead of providing `XYFunc`, we provide directly a list of Atoms.
> objectFunctionFromWorld :: World -> [YObject]
> objectFunctionFromWorld w = [Atoms atomList]
> where atomListPositive =
> getObject3DFromShapeFunction
> (shapeFunc (resolution (box w))) (box w)
2012-06-01 13:24:58 +00:00
> atomList = atomListPositive ++
> map negativeTriangle atomListPositive
> negativeTriangle (ColoredTriangle (p1,p2,p3,c)) =
> ColoredTriangle (negz p1,negz p3,negz p2,c)
2012-06-01 13:24:58 +00:00
> where negz (P (x,y,z)) = P (x,y,-z)
We know that resize is the only world change that necessitate to
recompute the list of atoms (triangles).
Then we update our world state accordingly.
> resize :: Scalar -> World -> World
> resize r world =
> tmpWorld { cache = objectFunctionFromWorld tmpWorld }
> where
> tmpWorld = world { box = (box world) {
> resolution = sqrt ((resolution (box world))**2 * r) }}
All the rest is exactly the same.
<div style="display:none">
> idleAction :: Time -> World -> World
> idleAction tnew world =
> world {
2012-06-15 10:02:30 +00:00
> angle = angle world + (delta -*< zdir)
2012-06-01 13:24:58 +00:00
> , told = tnew
> }
> where
> delta = anglePerSec world * elapsed / 1000.0
2012-06-01 13:24:58 +00:00
> elapsed = fromIntegral (tnew - (told world))
>
> shapeFunc :: Scalar -> Function3D
> shapeFunc res x y =
> let
2012-06-14 16:04:16 +00:00
> z = maxZeroIndex (ymandel x y) 0 1 20
2012-06-01 13:24:58 +00:00
> in
2012-06-14 16:04:16 +00:00
> if and [ maxZeroIndex (ymandel (x+xeps) (y+yeps)) 0 1 20 < 0.000001 |
2012-06-01 13:24:58 +00:00
> val <- [res], xeps <- [-val,val], yeps<-[-val,val]]
> then Nothing
> else Just (z,colorFromValue 0)
2012-06-01 13:24:58 +00:00
>
> colorFromValue :: Point -> Color
> colorFromValue n =
> let
> t :: Point -> Scalar
> t i = 0.0 + 0.5*cos( i /10 )
2012-06-01 13:24:58 +00:00
> in
> makeColor (t n) (t (n+5)) (t (n+10))
>
2012-06-14 16:04:16 +00:00
> -- given f min max nbtest,
> -- considering
> -- - f is an increasing function
> -- - f(min)=0
> -- - f(max)≠0
> -- then maxZeroIndex f min max nbtest returns x such that
> -- f(x - ε)=0 and f(x + ε)≠0
> -- where ε=(max-min)/2^(nbtest+1)
> maxZeroIndex :: (Fractional a,Num a,Num b,Eq b) =>
2012-06-01 13:24:58 +00:00
> (a -> b) -> a -> a -> Int -> a
2012-06-14 16:04:16 +00:00
> maxZeroIndex _ minval maxval 0 = (minval+maxval)/2
> maxZeroIndex func minval maxval n =
2012-06-01 13:24:58 +00:00
> if func medpoint /= 0
2012-06-14 16:04:16 +00:00
> then maxZeroIndex func minval medpoint (n-1)
> else maxZeroIndex func medpoint maxval (n-1)
2012-06-01 13:24:58 +00:00
> where medpoint = (minval+maxval)/2
>
> ymandel :: Point -> Point -> Point -> Point
> ymandel x y z = fromIntegral (mandel x y z 64) / 64
</div>
2012-06-15 13:25:51 +00:00
And you can also consider minor changes in the `YGL.hs` source file.
2012-06-14 16:04:16 +00:00
2012-06-01 13:24:58 +00:00
- [`YGL.hs`](code/06_Mandelbulb/YGL.hs), the 3D rendering framework
- [`Mandel`](code/06_Mandelbulb/Mandel.hs), the mandel function
- [`ExtComplex`](code/06_Mandelbulb/ExtComplex.hs), the extended complexes