That function is opaque, because RenderLayer is an opaque IO value that wraps side-effects. We don’t really want that.
World, SubWorld and Retina
A SubWorld is a part of the whole scene. Actually, a SubWorld contain objects that will be shaded the same way. That is, a SubWorld is a type:
data SubWorld = forall a. SubWorld (Program' a) [Instance (Model a)]
We rely on existential quantification here so that we can abstract the shader away. If we group all SubWorld and add lights, we end up with a World:
data World = World Ambient [Instance Omni] [SubWorld]
And a Retina is a simple object that adds a Camera to a World. Add some perspective!
Pretty straight-forward. The cool stuff is that we can now traverse a World in order to render it.
Rendering vs. compositing
Rendering is the fact of turning a World into a Frame. We can really state that a renderer is such a function:
renderer :: World -> m Frame
But what is m? That depends on what we’ll need the Frame for. There aren’t many options. We’ll use the Frame in the next step: the compositing phasing.
Compositing is the fact of blending frames into others in order to enhance and assemble images. Compositing should be seen as a frame endomorphism:
compositing :: c Frame Frame
But once again, what is c? Well, c is the type representing the compositing.
compositing :: Compositing Frame Frame
Now, what does that type do? That’s actually pretty simple. A compositing graph is composed of nodes, which can host two kind of things:
a world and a camera we have to render ;
a shader for post-processing purposes.
Let’s capture that in a type Node:
data Node a b
= Render Retina (Frame -> b)
| PostProcess a (Program' a) (Frame -> b)
Render ret f is a Node that hosts the Retina ret and a function f that will use the resulting Frame to build another type b.
PostProcess a prog f hosts custom properties and a shader program to sink them and a function f that will use the resulting Frame to build another type b. It’s important to understand that custom properties a can have Frames, and that we should have a way to send Frames down to shaders.
That doesn’t tell us how to build the Compositing type. Well, that’s actually pretty simple. We can define a Functor instance for Node a:
instance Functor (Node a) where
fmap f n = case n of
Render ret g -> Render ret (f . g)
PostProcess a prog g -> PostProcess a prog (f . g)
And then we can use a free monad to avoid the boiler-plate:
newtype Compositing a b = Compositing (Free (Node a) b) deriving (Applicative,Functor,Monad)
render :: Retina -> Compositing a Frame
render ret = Compositing . Free $ Render ret Pure
postProcess :: a -> Program' a -> Compositing a Frame
postProcess a prog = Compositing . Free $ PostProcess a prog Pure
We’ll then have to change our renderer function to adapt to compositing. Let’s call that display instead:
display :: (MonadIO m) => a -> Compositing a Frame -> m ()
And that’s possible because the Compositing a Frame hosts all Retina and all post-process phases and that we should output a single Frame. We could have used another type, like SinkFrame, to sink the resulting Frame through screen or onto the disk, but I don’t actually give a shit right now.
Problem
There’s a nasty effect of using
IO
directly to render our objects. Functions such as:That function is opaque, because
RenderLayer
is an opaqueIO
value that wraps side-effects. We don’t really want that.World, SubWorld and Retina
A
SubWorld
is a part of the whole scene. Actually, aSubWorld
contain objects that will be shaded the same way. That is, aSubWorld
is a type:We rely on existential quantification here so that we can abstract the shader away. If we group all
SubWorld
and add lights, we end up with aWorld
:And a
Retina
is a simple object that adds aCamera
to aWorld
. Add some perspective!Pretty straight-forward. The cool stuff is that we can now traverse a
World
in order to render it.Rendering vs. compositing
Rendering is the fact of turning a
World
into aFrame
. We can really state that a renderer is such a function:But what is
m
? That depends on what we’ll need theFrame
for. There aren’t many options. We’ll use theFrame
in the next step: the compositing phasing.Compositing is the fact of blending frames into others in order to enhance and assemble images. Compositing should be seen as a frame endomorphism:
But once again, what is
c
? Well,c
is the type representing the compositing.Now, what does that type do? That’s actually pretty simple. A compositing graph is composed of nodes, which can host two kind of things:
Let’s capture that in a type
Node
:Render ret f
is aNode
that hosts theRetina
ret and a functionf
that will use the resultingFrame
to build another typeb
.PostProcess a prog f
hosts custom properties and a shader program to sink them and a functionf
that will use the resultingFrame
to build another typeb
. It’s important to understand that custom propertiesa
can haveFrame
s, and that we should have a way to sendFrame
s down to shaders.That doesn’t tell us how to build the
Compositing
type. Well, that’s actually pretty simple. We can define aFunctor
instance forNode a
:And then we can use a free monad to avoid the boiler-plate:
We’ll then have to change our
renderer
function to adapt to compositing. Let’s call thatdisplay
instead:And that’s possible because the
Compositing a Frame
hosts allRetina
and all post-process phases and that we should output a singleFrame
. We could have used another type, likeSinkFrame
, to sink the resultingFrame
through screen or onto the disk, but I don’t actually give a shit right now.