简化现实,我的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:
在@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可以旋转而无需进行无用的计算。
答案 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
请求时进行评估。