Stéphane Laurent
Stéphane Laurent

Reputation: 84699

How to rotate an OpenGL graphics in Haskell without uselessly re-evaluating the graphical objects?

Simplifying the reality, my OpenGL program has the following structure:

So, is there a way to rotate the graphics by pressing a key without triggering triangulize f ? In other words, to "freeze" triangulize f so that it is evaluated only once and is never re-evaluated, which is time-consuming but useless since anyway the result is always the same.

I believe this is a standard way to rotate a graphics in Haskell OpenGL (I viewed that way in some tutos), so I don't think it is necessary to post my code. But of course I can post it if needed.

The reality is more complicated since there are other IORef's to control some parameters of the surface. But I would like to firstly know some solutions for this simplified situation.

EDIT: more details and some code

Simplified code

So, if I follow the simplified description above, my program looks like

fBretzel5 :: (Double,Double,Double) -> Double
fBretzel5 (x,y,z) = ((x*x+y*y/4-1)*(x*x/4+y*y-1))^2 + z*z

triangles :: [Triangle] -- Triangle: triplet of 3 vertices
triangles =
  triangulize fBretzel5 ((-2.5,2.5),(-2.5,2.5),(-0.5,0.5))
-- "triangulize f (xbounds, ybounds, zbounds)"
--   calculates a triangular mesh of the surface f(x,y,z)=0

display :: IORef Float -> DisplayCallback
display rot = do
  clear [ColorBuffer, DepthBuffer]
  rot' <- get rot
  loadIdentity
  rotate rot $ Vector3 1 0 0
  renderPrimitive Triangles $ do
    materialDiffuse FrontAndBack $= red
    mapM_ drawTriangle triangles
  swapBuffers
  where
    drawTriangle (v1,v2,v3) = do
      triangleNormal (v1,v2,v3) -- the normal of the triangle
      vertex v1
      vertex v2
      vertex v3

keyboard :: IORef Float -- rotation angle
         -> KeyboardCallback
keyboard rot c _ = do
  case c of
    'e' -> rot $~! subtract 2
    'r' -> rot $~! (+ 2)
    'q' -> leaveMainLoop
    _   -> return ()
  postRedisplay Nothing

This causes the issue described above. Each time the key 'e' or 'r' is pressed, the triangulize function runs while its output remains the same.

True code (almost)

Now, here is a version of my program closest to the reality. In fact, it calculates a triangular mesh for a surface f(x,y,z)=l, where the "isolevel" l can be changed with the keyboard.

voxel :: IO Voxel
voxel = makeVoxel fBretzel5 ((-2.5,2.5),(-2.5,2.5),(-0.5,0.5))
-- the voxel is a 3D-array of points; each entry of the array is
--   the value of the function at this point
-- !! the voxel should never changes throughout the program !!

trianglesBretz :: Double -> IO [Triangle]
trianglesBretz level = do
  vxl <- voxel
  computeContour3d vxl level
-- "computeContour3d vxl level" calculates a triangular mesh
--   of the surface f(x,y,z)=level

display :: IORef Float -> IORef Float -> DisplayCallback
display rot level = do
  clear [ColorBuffer, DepthBuffer]
  rot' <- get rot
  level' <- get level
  triangles <- trianglesBretz level'
  loadIdentity
  rotate rot $ Vector3 1 0 0
  renderPrimitive Triangles $ do
    materialDiffuse FrontAndBack $= red
    mapM_ drawTriangle triangles
  swapBuffers
  where
    drawTriangle (v1,v2,v3) = do
      triangleNormal (v1,v2,v3) -- the normal of the triangle
      vertex v1
      vertex v2
      vertex v3

keyboard :: IORef Float  -- rotation angle
         -> IORef Double -- isolevel
         -> KeyboardCallback
keyboard rot level c _ = do
  case c of
    'e' -> rot $~! subtract 2
    'r' -> rot $~! (+ 2)
    'h' -> level $~! (+ 0.1)
    'n' -> level $~! subtract 0.1
    'q' -> leaveMainLoop
    _   -> return ()
  postRedisplay Nothing

A part of a solution

In fact, I have found a solution in order to "freeze" the voxel:

voxel :: Voxel
{-# NOINLINE voxel #-}
voxel = unsafePerformIO $ makeVoxel fBretzel5 ((-2.5,2.5),(-2.5,2.5),(-0.5,0.5))

trianglesBretz :: Double -> IO [Triangle]
trianglesBretz level =
  computeContour3d voxel level

In this way, I think the voxel is never re-evaluated.

But there is still a problem. When the IORef rot changes, to rotate the graphics, then there's no reason to re-evaluate trianglesBretz: the triangular mesh of f(x,y,z)=level is always the same whatever the rotation.

So, how can I say to the display function: "hey! when rot changes, do not re-evaluate trianglesBretz, since you will find the same result" ?

I don't know how to use NOINLINE for trianglesBretz, as I did for voxel. Something which would "freezes" trianglesBretz level unless level changes.

And here is the 5-holes bretzel:

enter image description here

EDIT: solution based on @Petr Pudlák's answer.

After @Petr Pudlák's very good answer I came to the following code. I give this solution here in order to place the answer more in the context of OpenGL.

data Context = Context
    {
      contextRotation  :: IORef Float
    , contextTriangles :: IORef [Triangle]
    }

red :: Color4 GLfloat
red = Color4 1 0 0 1

fBretz :: XYZ -> Double
fBretz (x,y,z) = ((x2+y2/4-1)*(x2/4+y2-1))^2 + z*z
  where
  x2 = x*x
  y2 = y*y

voxel :: Voxel
{-# NOINLINE voxel #-}
voxel = unsafePerformIO $ makeVoxel fBretz ((-2.5,2.5),(-2.5,2.5),(-1,1))

trianglesBretz :: Double -> IO [Triangle]
trianglesBretz level = computeContour3d voxel level

display :: Context -> DisplayCallback
display context = do
  clear [ColorBuffer, DepthBuffer]
  rot <- get (contextRotation context)
  triangles <- get (contextTriangles context)
  loadIdentity
  rotate rot $ Vector3 1 0 0
  renderPrimitive Triangles $ do
    materialDiffuse FrontAndBack $= red
    mapM_ drawTriangle triangles
  swapBuffers
  where
    drawTriangle (v1,v2,v3) = do
      triangleNormal (v1,v2,v3) -- the normal of the triangle
      vertex v1
      vertex v2
      vertex v3

keyboard :: IORef Float      -- rotation angle
         -> IORef Double     -- isolevel
         -> IORef [Triangle] -- triangular mesh
         -> KeyboardCallback
keyboard rot level trianglesRef c _ = do
  case c of
    'e' -> rot $~! subtract 2
    'r' -> rot $~! (+ 2)
    'h' -> do
             l $~! (+ 0.1)
             l' <- get l
             triangles <- trianglesBretz l'
             writeIORef trianglesRef triangles
    'n' -> do
             l $~! (- 0.1)
             l' <- get l
             triangles <- trianglesBretz l'
             writeIORef trianglesRef triangles
    'q' -> leaveMainLoop
    _   -> return ()
  postRedisplay Nothing

main :: IO ()
main = do
  _ <- getArgsAndInitialize
  _ <- createWindow "Bretzel"
  windowSize $= Size 500 500
  initialDisplayMode $= [RGBAMode, DoubleBuffered, WithDepthBuffer]
  clearColor $= white
  materialAmbient FrontAndBack $= black
  lighting $= Enabled
  lightModelTwoSide $= Enabled
  light (Light 0) $= Enabled
  position (Light 0) $= Vertex4 0 0 (-100) 1
  ambient (Light 0) $= black
  diffuse (Light 0) $= white
  specular (Light 0) $= white
  depthFunc $= Just Less
  shadeModel $= Smooth
  rot <- newIORef 0.0
  level <- newIORef 0.1
  triangles <- trianglesBretz 0.1
  trianglesRef <- newIORef triangles
  displayCallback $= display Context {contextRotation = rot,
                                      contextTriangles = trianglesRef}
  reshapeCallback $= Just yourReshapeCallback
  keyboardCallback $= Just (keyboard rot level trianglesRef)
  idleCallback $= Nothing
  putStrLn "*** Bretzel ***\n\
        \    To quit, press q.\n\
        \    Scene rotation:\n\
        \        e, r, t, y, u, i\n\
        \    Increase/Decrease level: h, n\n\
        \"
  mainLoop

And now my bretzel can be rotated without performing useless calculations.

enter image description here

Upvotes: 3

Views: 196

Answers (1)

Petr
Petr

Reputation: 63409

I'm not very familiar with OpenGL, so I have some difficulty understanding the code in detail - please correct me if I misunderstood something.

I'd try to abstain from using unsafe functions or relying on INLINE as much as possible. This usually makes code brittle and obscures more natural solutions.

In the simplest case, if you don't need to re-evaluate triangularize, we could just replace it with its output. So we'd have

data Context = Context
    { contextRotation :: IORef Float,
    , contextTriangles :: [Triangle]
    }

and then

display :: Context -> DisplayCallback

which won't reevaluate triangles at all, they'll be computed only once when Context is created.

Now if there are two parameters, rotation and level, and triangles depend on the level, but not on rotation: The trick here would be to manage dependencies properly. Now we expose the storage for parameters explicitly (IORef Float), and as a consequence, we can't monitor when the value inside changes. But the caller doesn't need to know the representation of how the parameters are stored. It just needs to store them somehow. So instead, let's have

data Context = Context
    { contextRotation :: IORef Float,
    , contextTriangles :: IORef [Triangle]
    }

and

setLevel :: Context -> Float -> IO ()

That is, we expose a function to store the parameter, but we hide the internals. Now we can implement it as:

setLevel (Context _ trianglesRef) level = do
    let newTriangles = ... -- compute the new triangles
    writeIORef trianglesRef newTriangles

And as triangles don't depend on the rotation parameter, we can have just:

setRotation :: Context -> Float -> IO ()
setRoration (Context rotationRef _) = writeIORef rotationRef

Now the dependencies are hidden for callers. They can set the level or the rotation, without knowing what depends on them. At the same time, triangles are updated when needed (level changes), and only then. And Haskell's lazy evaluation gives a nice bonus: If the level changes multiple times before the triangles are needed, they are not evaluated. The [Triangle] thunk inside the IORef will be only evaluated when requested by display.

Upvotes: 1

Related Questions