如何在Haskell中旋转OpenGL图形而不会无用地重新评估图形对象?

时间:2018-04-14 01:29:23

标签: haskell opengl

简化现实,我的OpenGL程序具有以下结构:

  • 一开始,有一个函数f : (Double,Double,Double) -> Double

  • 然后有一个函数triangulize :: ((Double,Double,Double) -> Double) -> [Triangle]triangulize f计算表面f(x,y,z)=0的三角形网格。

  • 然后是displayCallback,一个显示图形的函数display :: IORef Float -> DisplayCallBack(也就是说它显示三角形网格)。第一个参数IORef Float用于旋转图形,当用户按下键盘上的键时,其值(旋转角度)会发生变化,这要归功于稍后定义的keyboardCallback。不要忘记display函数调用triangulize f

  • 然后问题是以下问题。当用户按下键以旋转图形时,将触发display功能。然后重新评估triangulize f,而不需要重新评估:旋转图形不会改变三角形网格(即triangulize f的结果与之前相同)。

那么,有没有办法通过按键而不触发triangulize f来旋转图形?换句话说,要“冻结”triangulize f以便它只被评估一次并且永远不会被重新评估,这是耗时但无用的,因为无论如何结果总是相同的。

我相信这是在Haskell OpenGL中旋转图形的标准方法(我在某些tutos中以这种方式查看),所以我认为没有必要发布我的代码。但当然,如果需要,我可以发布它。

现实更加复杂,因为还有其他IORef来控制曲面的某些参数。但我想首先了解这种简化情况的一些解决方案。

编辑:更多细节和一些代码

简化代码

所以,如果我按照上面简化的描述,我的程序看起来像

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

这导致上述问题。每次按下'e''r'键时,triangulize函数会在其输出保持不变的情况下运行。

真实代码(几乎)

现在,这是我最接近现实的程序版本。实际上,它会计算表面f(x,y,z)=l的三角形网格,其中“isolevel”l可以通过键盘进行更改。

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

解决方案的一部分

事实上,我找到了一个解决方案,以“冻结”体素:

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

通过这种方式,我认为voxel永远不会被重新评估。

但仍有问题。当IORef rot发生变化时,要旋转图形,就没有理由重新评估trianglesBretz:无论旋转如何,f(x,y,z)=level的三角形网格总是相同的。

那么,我怎么能对display函数说:“嘿!当rot发生变化时,不要重新评估trianglesBretz,因为你会发现相同的结果“

我不知道如何NOINLINE使用trianglesBretz,就像我为voxel所做的那样。除非trianglesBretz level发生变化,否则会冻结level的内容。

这是5洞的bretzel:

enter image description here

编辑:解决方案基于@PetrPudlák的回答。

在@PetrPudlák非常好的回答后,我得到了以下代码。我在这里给出了这个解决方案,以便将答案更多地放在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

现在我的bretzel可以旋转而无需进行无用的计算。

enter image description here

1 个答案:

答案 0 :(得分:1)

我对OpenGL不是很熟悉,所以我对代码的理解有些困难 - 如果我误解了某些内容,请纠正我。

我会尽量避免使用不安全的函数或依赖INLINE。这通常会使代码变得脆弱并掩盖更自然的解决方案。

在最简单的情况下,如果您不需要重新评估triangularize,我们可以将其替换为输出。所以我们有

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

然后

display :: Context -> DisplayCallback

根本不会重新评估三角形,只有在创建Context时才会计算它们。

现在,如果有两个参数,旋转和水平,三角形取决于级别,而不是旋转:这里的技巧是正确管理依赖关系。现在我们明确地公开了存储参数(IORef Float),因此,我们无法监视内部值何时发生变化。但是调用者不需要知道参数如何存储的表示。它只需要以某种方式存储它们。所以相反,让我们有

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

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

也就是说,我们公开了一个存储参数的函数,但我们隐藏了内部。现在我们可以将其实现为:

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

由于三角形不依赖于旋转参数,我们可以只有:

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

现在为调用者隐藏了依赖项。他们可以设置水平或旋转,而不知道取决于他们的是什么。同时,三角形在需要时(级别更改)更新,然后才更新。而Haskell的懒惰评估给出了一个很好的奖励:如果在需要三角形之前水平发生了多次变化,则不会对它们进行评估。 [Triangle]内的IORef thunk仅在display请求时进行评估。