要用 Haskell 渲染一个 OpenGL 场景,我使用这样的结构:
data Context = Context
{
contextRot1 :: IORef GLfloat
, contextRot2 :: IORef GLfloat
, contextRot3 :: IORef GLfloat
, contextZoom :: IORef Double
, contextTriangles :: IORef Triangles
}
Triangles
对象包含要显示的 3D 对象的顶点和法线,排列在形成三角形的三元组列表中。
我使用
reshapeCallback
(在main
函数中)Just (resize 0)
与:
resize :: Double -> Size -> IO ()
resize zoom s@(Size w h) = do
viewport $= (Position 0 0, s)
matrixMode $= Projection
loadIdentity
perspective 45.0 (w'/h') 1.0 100.0
lookAt (Vertex3 0 (-9 + zoom) 0) (Vertex3 0 0 0) (Vector3 0 0 1)
matrixMode $= Modelview 0
where
w' = realToFrac w
h' = realToFrac h
然后我用这个
displayCallback
:
display :: Context -> DisplayCallback
display context = do
clear [ColorBuffer, DepthBuffer]
r1 <- get (contextRot1 context)
r2 <- get (contextRot2 context)
r3 <- get (contextRot3 context)
triangles <- get (contextTriangles context)
zoom <- get (contextZoom context)
(_, size) <- get viewport
loadIdentity
resize zoom size
rotate r1 $ Vector3 1 0 0
rotate r2 $ Vector3 0 1 0
rotate r3 $ Vector3 0 0 1
renderPrimitive Triangles $ mapM_ drawTriangle triangles
swapBuffers
where
drawTriangle ((v1, v2, v3), (n1, n2, n3)) = do
materialDiffuse Front $= whitesmoke
normal (toNormal n1)
vertex (toVertex v1)
normal (toNormal n2)
vertex (toVertex v2)
normal (toNormal n3)
vertex (toVertex v3)
where
toNormal (x, y, z) = Normal3 x y z
toVertex (x, y, z) = Vertex3 x y z
这是
main
功能:
main :: IO ()
main = do
_ <- getArgsAndInitialize
_ <- createWindow "Kohn-Nirenberg surface"
windowSize $= Size 512 512
initialDisplayMode $= [RGBAMode, DoubleBuffered, WithDepthBuffer]
clearColor $= discord
materialAmbient Front $= white
lighting $= Enabled
lightModelTwoSide $= Enabled
light (Light 0) $= Enabled
position (Light 0) $= Vertex4 0 (-100) 0 1
ambient (Light 0) $= black
diffuse (Light 0) $= white
specular (Light 0) $= white
depthFunc $= Just Less
shadeModel $= Smooth
cullFace $= Just Back
rot1 <- newIORef 0.0
rot2 <- newIORef 0.0
rot3 <- newIORef 0.0
zoom <- newIORef 0.0
triangles <- newIORef =<< trianglesIO
displayCallback $= display Context {contextRot1 = rot1,
contextRot2 = rot2,
contextRot3 = rot3,
contextZoom = zoom,
contextTriangles = triangles}
reshapeCallback $= Just (resize 0)
anim <- newIORef False
delay <- newIORef 0
save <- newIORef False
snapshots <- newIORef 0
keyboardCallback $= Just (keyboard rot1 rot2 rot3 zoom anim delay save)
idleCallback $= Just (idle anim delay save snapshots rot3)
putStrLn "*** Kohn-Nirenberg surface ***\n\
\ To quit, press q.\n\
\ Scene rotation:\n\
\ e, r, t, y, u, i\n\
\ Zoom: l, m\n\
\ Animation: a\n\
\ Animation speed: o, p\n\
\ Save animation: s\n\
\"
mainLoop
我没有展示所有代码,因为它太长而且有些部分与当前问题无关(例如保存动画)。如果需要,您可以在此处找到完整代码。
现在,多亏了
keyboardCallback
(此处未显示),我可以旋转场景。我认为这会旋转 3D 对象,而不是相机。对吗?
刚好旋转很耗资源(连续按旋转键我能听到笔记本电脑在呼呼作响)
然而,当我使用带有R包rgl的OpenGL时,我可以用鼠标流畅地旋转场景,这根本不占用资源。所以我想知道我在 Haskell 中使用的方式是否可以改进,如这里所示。我不知道 rgl 是如何执行旋转的。
注1:本例中的三角形没有必要使用
IORef
。
注2: 只看现场,不按任何键笔记本电脑就烧;在我看来,即使没有任何变化,
main
功能也会持续执行 - 没有办法控制它的重新执行吗?
应用程序的主要瓶颈是绘制所有三角形。
您可以通过将三角形按顺序存储在平面数组中并使用更多较低级别的基元来绘制法线和顶点来提高性能:
import Foreign.Ptr
import Foreign.Marshal.Array
type F = Double
type Triangles = (Int, Ptr F)
[..]
fromVoxel :: Voxel F -> F -> (XYZ F -> XYZ F) -> IO Triangles
fromVoxel vox isolevel gradient = do
mesh <- makeMesh vox isolevel
let vertices = _vertices mesh
faces = _faces mesh
normals = makeNormals vertices gradient
triangle face =
( (normals ! i, vertices ! i)
, (normals ! j, vertices ! j)
, (normals ! k, vertices ! k)
)
where
(i, j, k) = face
flatten = concatMap $ \(((x1,x2,x3),(x4,x5,x6)),((x7,x8,x9),(x10,x11,x12)),((x13,x14,x15),(x16,x17,x18))) ->
[x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18]
withArrayLen (flatten (map triangle faces)) (\n p -> pure (n, p))
[..]
display :: Context -> DisplayCallback
display context = do
clear [ColorBuffer, DepthBuffer]
r1 <- get (contextRot1 context)
r2 <- get (contextRot2 context)
r3 <- get (contextRot3 context)
(n, triangles) <- get (contextTriangles context)
zoom <- get (contextZoom context)
(_, size) <- get viewport
loadIdentity
resize zoom size
rotate r1 $ Vector3 1 0 0
rotate r2 $ Vector3 0 1 0
rotate r3 $ Vector3 0 0 1
materialDiffuse Front $= whitesmoke
unsafeRenderPrimitive Triangles $ forM_ [0..n `quot` 18] $ \i -> drawTriangle triangles (i * 18 * 8)
swapBuffers
where
drawTriangle p i = do
normalv (plusPtr p (i + 0 * 8) :: Ptr (Normal3 F))
vertexv (plusPtr p (i + 3 * 8) :: Ptr (Vertex3 F))
normalv (plusPtr p (i + 6 * 8) :: Ptr (Normal3 F))
vertexv (plusPtr p (i + 9 * 8) :: Ptr (Vertex3 F))
normalv (plusPtr p (i + 12 * 8) :: Ptr (Normal3 F))
vertexv (plusPtr p (i + 15 * 8) :: Ptr (Vertex3 F))
但是,如果您的显示器具有非常高的刷新率,这可能还不够。或者,如果禁用了垂直同步,这可能根本无济于事。那么唯一的结果就是这会绘制更多的帧。
不幸的是,我认为没有简单的方法可以使用 GLUT 启用垂直同步。您可以切换到 GLFW-b,它允许您使用
swapInterval 1
启用垂直同步。
如果你真的想尽可能地挤出最大数量的帧,那么你也可以切换到使用顶点缓冲区,如 https://learnopengl.com/Getting-started/Hello-Triangle 中所述。这允许您将形状加载到数组中并将其发送到 GPU 一次。然后你不需要在每一帧上遍历所有的三角形。