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

标签 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中旋转图形的标准方法(我在某些tuto中以这种方式查看),因此我认为不必发布我的代码。但是我当然可以在需要时发布它。

    由于存在其他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孔椒盐脆饼:

    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

    最佳答案

    我对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内部的display thunk。

    关于haskell - 如何在Haskell中旋转OpenGL图形而又无用地重新评估图形对象?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49827020/

    相关文章:

    Haskell Control.镜头移动棱镜

    c++ - 我的卫星绕行,但沿着非圆形路径(校正球面坐标数学)

    c++ - OpenGL:渲染到帧缓冲区以及显示

    c++ - 如何使用 Qt 5 + VTK 启用 OpenGL 核心配置文件 3.2?

    haskell - 为什么 Haskell 在函数组合后不接受参数?

    haskell - let 绑定(bind)中的类型变量不明确

    haskell - 类型安全的模块化算术无注释

    haskell - 你如何实现 "show"尾递归?

    c++ - 为什么使用程序会导致我的形状消失?

    opengl - gluProject 和 2D 显示