Friday 22 February 2013

xeoEngine - An Embeddable WebGL Engine


xeoEngine is a message-driven WebGL engine I recently built on ActorJS and SceneJS that lets you create and manage 3D worlds via JSON-RPC. In this post I'm just going to give really quick intro to the project, with more detail to follow in future posts.

The engine itself is typically embedded in an iframe, then via a lightweight client library we fire messages at it to make it do stuff, as well as subscribe to its events. The coolness is that the container page only needs to load the client library, while the engine in the iframe provides all the textures, scripts etc. from its own reusable component libraries (ie. GitHub pages). This lets us throw scenes together on code-sharing tools like CodePen, where we post just an HTML page, the client library, and our JavaScript that drives the client.

Check out this procedurally-generated city example on CodePen. Click the JS tab to see our client code, and the HTML tab for the iframe. I sure hope you have WebGL ;)

Check out this Pen!

xeoEngine in more Detail

Via the JSON-RPC calls, we tell xeoEngine to plug actors together to create worlds, then we can fire calls at the actors to make the world do stuff. xeoEngine dynamically loads actor types on demand from libraries of AMD modules, and over time I want to build up a library of those actors from which I can select as required for apps on xeoEngine.

To use xeoEngine, first embed the server page in its iframe:
<iframe id="myIFrame" style="width:800px; height:600px;"
src="http://xeolabs.github.com/xeoEngine/server.html"></iframe>
pull in the xeoEngine client library:
<script src="http://xeolabs.github.com/xeoEngine/client.js"></script>

and then instantiate a client and drive the xeoEngine server page through it, as shown below. This is for the Newell Teapot example, which happens to demonstrate the API's publish/subscribe ability:

/* Create a client
 */
var engine = new xeoEngine({
    iframe: "myIFrame"
});
 
/* Add a scene actor
 */
engine.call("addActor", {
    type: "scene",
    actorId: "myScene"
});
 
/* Add a teapot to the scene
 */
engine.call("myScene/addActor", {
    type: "objects/prims/teapot",
    yPos: 2
});
 
/* Add a camera to the scene
 */
engine.call("myScene/addActor", {
    type: "camera",
    actorId: "myCamera", 
    eye: { z: 50 }
});
 
/* Subscribe to "eye" messages published 
 * by the camera whenever its eye
 * position changes. See how we specify 
 * a path down through the actor hierarchy
 * to the camera actor's "eye" topic.
 */
engine.subscribe("myScene/myCamera/eye",
    function (update) {
        var eye = update.eye;
        //..
    });
 
/* Call a method on the camera to set the eye position.
 * This will cause the camera to publish an "eye" message,
 * which we'll handle with the subscription we made above.
 */
engine.call("myScene/myCamera/setEye", {
    x: -30,
    y: 0,
    z: 50
});

xeoEngine stores actor types as libraries of RequireJS modules in the file system (GitHub pages). Without going into too much detail in this early post, here's the camera actor used in the snippet above, which lives in repository here:

define(
    function () {

        return  function (cfg) {
        
            // SceneJS scene graph
            var scene = this.getResource("scene"); 

            var nodeId = cfg.nodeId || "lookat";

            var lookat = scene.getNode(nodeId);

            if (!lookat) {
                throw "scene node not found: " + nodeId;
            }

            if (lookat.getType() != "lookAt") {
                throw "scene node should be a 'lookat' type: "
                   + nodeId;
            }

            this.set = function (params) {
                if (params.eye) {
                    this.setEye(params.eye);
                }
                if (params.look) {
                    this.setLook(params.look);
                }
                if (params.up) {
                    this.setUp(params.up);
                }
            };

            this.setEye = function (params) {
                this.publish("eye",
                   lookat.setEye(params).getEye());
            };

            this.setLook = function (params) {
                this.publish("look",
                    lookat.setLook(params).getLook());
            };


            this.setUp = function (params) {
                this.publish("up",
                    lookat.setUp(params).getUp());
            };

            /* Initialise from actor configs
             */
            this.set(cfg);
        };
    });

The addActor calls we saw earlier instantiate actor types into the actor hierarchy. When we add the camera actor, we're bolting it to the scene actor as a child. The scene actor has created a SceneJS scene graph resource, which the camera actor grabs and hooks itself to the graph's lookat node.

Those setter methods on the camera actor are exposed as RPC endpoints, which you can see being called in the snippet.


Project Status

Totally alpha at this stage! As of writing this post, xeoEngine is in a state of flux as I explore what kind of functionality I should put in the actors, but over time it should settle down into something that other people can build on. There's still a few things to do before its ready, such as better error reporting (there's not a lot at the moment). Please hit the issue tracker if you have any ideas - I'd love to hear from the 3D gurus out there.

No comments:

Post a Comment