Connecting Yampa with LambdaCube-Engine

This blog post was originally written back in 2012-06-11

_images/Lambdacube-tileracer.png

Abstract

In this post I present a minimal LambdaCube-Engine program which connects a 3D rendering engine to Yampa FRP. The code is an adaption of the lambdacube-basic.hs example delivered with lambdacube-examples. Some changes to Yampa were necessary to allow MonadIO transformations in order to connect with LambdaCube. The goal is to get quickstarted with Yampa and LambdaCube, so the code was intentionally kept simple. Full source code is available at bottom of post.

LambdaCube Examples

First we’re going to setup LamdaCube, run the lambdacube-basic example and see how it does work. Note that the latest version (0.2.4) of LamdaCube is available at GoogleCode (instead of Hackage (0.2.3)).

>>> cabal unpack lambdacube-engine # old version
>>> svn checkout http://lambdacube.googlecode.com/svn/trunk/ lambdacube # new version
>>> cd lambdacube/lambdacube-examples
>>> dist/build/lambdacube-basic/lambdacube-basic
# ls lambdacube-examples.cabal
# ls src/lambdacube-basic.hs
# ls media/

It should show 1 frame of a bluesky scene. However it stops immediately because of an issue in Elerea FRP with the following message (bug report): thread blocked indefinitely in an MVar operation

This is unfortunate, however we’re going to integrate Yampa anyway. The important parts of the example are outlined here:

lambdacube-basic.hs (outlined)

import FRP.Elerea.Param
...

main = do
    openWindow
    ...
    runLCM renderSystem [Stb.loadImage] $ do
    ...
    addScene [ node "Root" "Obj" ...]
    ...
    addRenderWindow "MainWindow" 640 480 ...
    ...
    sc <- liftIO $ start $ scene ...
    driveNetwork sc (readInput ...)

scene :: RenderSystem r vb ib q t p lp
      => ...
      -> SignalGen FloatType (Signal (LCM (World r vb ib q t p lp) e ()))
scene = do
        ...
        return $ drawGLScene <$> ...

drawGLScene :: RenderSystem r vb ib q t2 p lp
            => ...
            -> FloatType
            -> LCM   (World r vb ib q t2 p lp) e ()
drawGLScene ... time = do
    updateTransforms $ ...
    updateTargetSize "MainWindow" w h
    renderWorld (realToFrac time) "MainWindow"

What happens here is that a LCM world (LambdaCubeMonad) is created and several changes are made by “shoving” through the World via runLCM. Finally Elerea’s driveNetwork starts an infinite loop to make repeated renderWorld calls within the LCM context. So all we have to do is replace Elerea’s driveNetwork with Yampa’s reactimate and “shove” the LCM into reactimate. Unfortunately the types don’t match: (IO vs LCM). Also note how an external renderWorld is called within an LCM context. What’s going on? Let’s look up the definition! Because there are no public docs available we’ll generate them ourselves:

>>> cd lambdacube/lambdacube-engine
>>> cabal haddock
>>> firefox dist/doc/html/lambdacube-engine/index.html
data LCM w e a

Instances
Monad (LCM w e)
Functor (LCM w e)
Applicative (LCM w e)
MonadIO (LCM w e)

What’s interesting here is that LCM instances MonadIO and provides a free type variable a. Thus LCM has an IO context and can be extended with other Monad contexts and types. All we have to do therefore is to make Yampa’s reactimate less strict and do some Monad lifting.

>>> cabal install transformers
>>> cabal unpack Yampa
>>> cd Yampa-0.9.3/src/FRP

Yampa.hs

reactimate :: IO a
          -> (Bool -> IO (DTime, Maybe a))
          -> (Bool -> b -> IO Bool)
          -> SF a b
          -> IO ()

change to:

import Control.Monad.IO.Class (MonadIO)
...

reactimate :: MonadIO m
          => m a
          -> (Bool -> m (DTime, Maybe a))
          -> (Bool -> b -> m Bool)
          -> SF a b
          -> m ()

Now we can call reactimate within runLCM and use Yampa as usual. Copy the lambdacube-basic.hs file and extend lambdacube-examples.cabal with the new executable.

YampaCube.hs (outlined)

type ObjInput  = (Int, Int)      -- mouse-position
type ObjOutput = (String, Proj4) -- object name, transformation

main :: IO ()
main = do
    mediaPath <- getDataFileName "media"
    renderSystem <- mkGLRenderSystem
    runLCM renderSystem [Stb.loadImage] (reactimate (initput title mediaPath) input output (process objs))
    closeWindow

initput :: RenderSystem r vb ib q t p lp
      => Bool
      -> LCM     (World r vb ib q t p lp) e (DTime, Maybe (Bool, ObjInput))
initput _ = do
    ...
    liftIO $ openWindow ...
    ...
    addScene [ node "Root" "Obj" ...]
    ...

output :: RenderSystem r vb ib q t p lp
      => Bool -> [ObjOutput]
      -> LCM   (World r vb ib q t p lp) e Bool

cam :: SF ObjInput ObjOutput
cam = proc _ -> do
    t <- localTime -< ()
    returnA -< ("Cam", translation (Vec3 0 0 (realToFrac t)))
>>> cd lambdacube/lambdacube-examples
>>> cabal configure
>>> cabal build
>>> dist/build/YampaCube/YampaCube

Download full source code: YampaCube.hs

Viola! What you should see is a small box with the bluesky texture and the camera slowly moving away from it.