I want to implement a simple SceneGraph in Haskell using Data.Tree
consisting of Transform
and Shape
nodes. In a SceneGraph the spatial transformation is accumulated while traversing and applied to the shape for rendering.
type Transform = Vector2 Double
data Shape = Circle Double | Square Double
data SceneNode = XFormNode Transform | ShapeNode Shape
Say we have a scene with an object which is moved to the right and consisting of a Square at bottom and a Circle on top
^
|
| ()
| []
0----->
I came up with this tree definition:
let tree = Node (XFormNode (vector2 10 0))
[Node (ShapeNode (Square 10)) []
,Node (XFormNode (vector2 0 10))
[Node (ShapeNode (Circle 10)) []]
]
The rendering would be something like this:
render :: Position2 -> Shape -> IO ()
render p (Circle r) = drawCircle p r
render p (Square a) = drawSquare p a
My questions are:
1) How do you define a traverse
function, which accumulates the transformation and calls the render tasks?
2) How do you avoid making the traverse IO?
3) Is there a shorter version to define this tree? All but the first Node definition and all empty subForests are actually superfluous.
Thank you!
Paradoxically, Data.Tree
is not used very often in Haskell because defining a custom tree type is so easy. In your case, I would implement the scene graph (tree) as follows:
type Transform = Vector2 Double
data Shape = Circle Double | Square Double
data Scene = Transform Transform [Scene] | Shape Shape
Your example becomes
example :: Scene
example = Transform (vector2 10 0)
[ Shape (Square 10)
, Transform (vector2 0 10) [Shape (Circle 10)]
]
This answers point 3.
To traverse the tree, use recursion:
render :: Position2 -> Scene -> IO ()
render p (Transform v scenes) = mapM_ (render (p+v)) scenes
render p (Shape (Circle r)) = drawCircle p r
render p (Shape (Square a)) = drawSquare p a
There are more generic traversals available, for instance in Data.Traversable
, but they are more "uniform". In short, using recursion on trees is perfectly fine.
Concering point 2, there is nothing much you can do once you decide that circles and squares should be rendered in the IO
monad.