In a Nutshell
LambdaCube 3D is a domain specific language and library that makes it possible to program GPUs in a purely functional style.
Purely Functional Rendering Engine
Earlier this year we were experimenting with creating an FRP-style API on top of the Bullet physics engine. As a result, we developed a simple example scene using Elerea, which is now available in the project repository. This post is the first in a series to discuss the example in detail. We start the tour by introducing the libraries used.
Bullet is a C++ library, which makes it tricky to drive from Haskell. Csaba solved the problem by generating a plain C wrapper around it, and created a Haskell binding for this wrapper. This provides us a nice steady base to build on. Programming against this interface feels very much like using Gtk2Hs. As an example, let’s see a slightly simplified variant of a function from the example used in mouse picking. In this function, we cast a ray into the world and return the closest dynamic rigid body it hit:
rayTarget :: Vec2 -> CameraInfo -> Vec2 -> Vec3 pickBody :: BtCollisionWorldClass bc => bc -> Vec2 -> CameraInfo -> Vec2 -> IO (Maybe (BtRigidBody, Vec3, Float)) pickBody dynamicsWorld windowSize cameraInfo mousePosition = do let rayFrom = cameraPosition cameraInfo rayTo = rayTarget windowSize cameraInfo mousePosition rayResult <- btCollisionWorld_ClosestRayResultCallback rayFrom rayTo btCollisionWorld_rayTest dynamicsWorld rayFrom rayTo rayResult hasHit <- btCollisionWorld_RayResultCallback_hasHit rayResult case hasHit of False -> return Nothing True -> do collisionObj <- btCollisionWorld_RayResultCallback_m_collisionObject_get rayResult isNotPickable <- btCollisionObject_isStaticOrKinematicObject collisionObj internalType <- btCollisionObject_getInternalType collisionObj case isNotPickable || internalType /= e_btCollisionObject_CollisionObjectTypes_CO_RIGID_BODY of True -> return Nothing False -> do btCollisionObject_setActivationState collisionObj 4 -- DISABLE_DEACTIVATION hitPosition <- btCollisionWorld_ClosestRayResultCallback_m_hitPointWorld_get rayResult body <- btRigidBody_upcast collisionObj -- this would be null if the internal type is not CO_RIGID_BODY return $ Just (body, hitPosition, len (hitPosition &- rayFrom))
We can think of the camera info as the transformation matrix that maps the world on the screen. The rayTarget function returns the endpoint of the ray corresponding to the mouse position on the far plane of the view frustum given all the relevant information. First we create a data structure (the ‘ray result callback’) to hold the result of the raycast, then perform the actual ray test. The value of hasHit is true if the segment between rayFrom and rayTo intersects any object in the physics world.
The C++ snippet corresponding to the first five lines of the do block might look something like this:
btVector3 rayFrom = cameraPosition(cameraInfo); btVector3 rayTo = rayTarget(windowSize, cameraInfo, mousePosition); btCollisionWorld::ClosestRayResultCallback rayCallback(rayFrom, rayTo); dynamicsWorld->rayTest(rayFrom, rayTo, rayCallback); bool hasHit = rayCallback.hasHit();
If hasHit is true, we can get a reference to the object from rayResult, and check if it is of the right type. If everything matches, we return the body, the world position of the point where the ray hit it first, and the distance to that point from the camera. One of the nice things about this binding is that it uses the vector types from the vect library out of the box instead of exposing Bullet specific vectors, so all the spatial calculations are really easy to write without having to jump through extra hoops first.
Elerea is an FRP library that’s primarily aimed at game programming. Its basic abstraction is the discrete generic stream – referred to as Signal –, and it can be used to describe fully dynamic data-flow networks. In essence, it provides a nice compositional way to define the state transformation during the simulation step. It also allows IO computations to be interspersed in this description, thereby providing lightweight (threadless) framework for cooperative multitasking.
There are two kinds of structures in Elerea. A value of type Signal a can be thought of as a time-varying value of type a. All the future values of the signal are fully determined by its definition, i.e. signals are context independent just as we expect from ordinary values in a pure functional language. The other structure is the SignalGen monad, which is a context where stateful signals are constructed. Mutually dependent signals can be defined thanks to the fact that SignalGen is an instance of MonadFix.
The basic idea behind SignalGen can be understood in terms of start times. Every context corresponds to a (discrete) moment on the global timeline, and every stateful signal constructed in that context is considered to start at that moment. However, signals themselves are defined in terms of the global time, which allows us to combine signals that originate from different contexts (e.g. create a numeric signal that’s the point-wise sum of two unrelated numeric signals). The API ensures that no signal can physically exist before its start time; theoretically, signals are undefined until that point.
When executing the resulting data-flow network, Elerea guarantees consistency by double buffering. The superstep that advances the network consists of two phases: read and commit. In the read phase every node queries its dependencies, and no-one changes their output. In the commit phase every node performs its state transition independently, based on the input from the read phase, so the inconsistent state is never observed anywhere in the system.
While the C-style API allows us to access all the functionalities, it’s not very comfortable to use. The biggest issue is its verbosity: all the names are fully qualified out of necessity, and each property of an object has to be set separately. Therefore, we took some inspiration from the glib attribute system used by Gtk2Hs, and implemented something similar in the example. An attribute is the pair of a getter and the corresponding setter for a given property:
data Attr o a = forall x . Attr !(o -> IO a) !(o -> a -> IO x)
We allow arbitrary return types for setters to make it easier to define attributes, since many Bullet setters return something other than unit. However, we discard these values for the time being, so it’s really just to avoid having to type ‘() <$’ so many times.
Attributes are brought to life through attribute operations, which specify how to calculate the value of the property. There are four possibilities: set a given value, transform the current value with a pure function, set a given value coming from an IO computation, and transform the current one with an IO computation. These are denoted as follows:
infixr 0 :=, :~, :!=, :!~ data AttrOp o = forall a . Attr o a := a | forall a . Attr o a :~ (a -> a) | forall a . Attr o a :!= IO a | forall a . Attr o a :!~ (a -> IO a)
We need existentials to hide the type of the property and only expose the type of the object, so we can easily create collections of attributes. Now we can define the functions that connect them to the actual objects:
set :: o -> [AttrOp o] -> IO o set obj attrs = (>> return obj) $ forM_ attrs $ \op -> case op of Attr _ setter := x -> setter obj x >> return () Attr getter setter :~ f -> getter obj >>= setter obj . f >> return () Attr _ setter :!= x -> x >>= setter obj >> return () Attr getter setter :!~ f -> getter obj >>= f >>= setter obj >> return () get :: o -> Attr o a -> IO a get obj (Attr getter _) = getter obj make :: IO o -> [AttrOp o] -> IO o make act flags = do obj <- act set obj flags return obj
This is a fully generic system nothing to do with Bullet at this point. The set function takes an object and updates all the attributes listed in its second argument. The get function is just a thin helper to retrieve the value of a property given the corresponding attribute. Finally, make is another thin helper that allows us to construct an object and set its attributes in a single step.
A simple example is the world transform property of collision objects. It can be read and written by the following two functions:
btCollisionObject_getWorldTransform :: BtCollisionObjectClass bc => bc -> IO Transform btCollisionObject_setWorldTransform :: BtCollisionObjectClass bc => bc -> Transform -> IO Transform
Turning it into an attribute is as simple as constructing a pair out of the above functions:
worldTransform :: BtCollisionObjectClass o => Attr o Transform worldTransform = Attr btCollisionObject_getWorldTransform btCollisionObject_setWorldTransform
Given this definition, we can write set body [worldTransform := …] to update the transform, and get body worldTransform to retrieve it. In the next post we’ll see how to extend the above system to define attributes tied to Elerea signals, which allows us to define all their future values at the time of their creation, and how to use this capability to define a rigid body whose position is reset every time it collides with another given object.
Pingback: Using Bullet physics with an FRP approach (part 2) « LambdaCube 3D
Pingback: Using Bullet physics with an FRP approach (part 3) « LambdaCube 3D