The Murl Engine is a native, platform-independent, time-based scene graph framework for developing games and multimedia applications. The framework has been designed for maximum performance and flexibility and it allows the cross platform development of native applications with only a single code base.
The framework development package consists of a platform-independent core module (“framework code”) and a platform-specific adaption module (“platform code”) for each supported platform. The framework code was developed in C++ and the platform code in C, C++, ObjectiveC and Java depending on the actual target.
Application development (“user code”) is performed in C++ or LUA based on the framework's API (Application Programming Interface). It is possible to create a native application for each supported target platform from these three components: user code, framework code and platform code.
Using C++ for native application development allows efficient programming at a high abstraction level with optimum run-time performance.
In this context, the term “native” means that the created applications can be directly run on the selected target devices with their actual operating systems. There is no need for additional plugins, emulators, run-time environments or similar external components, which helps to reach optimum performance and system integration.
Basically, application development can be performed by using any available C++ IDE. However, the framework's support for project creation and maintenance is currently only provided for Microsoft Visual Studio (Express) and Apple XCode, which are therefore recommended for application development.
In order to build applications for iOS and/or Mac OS X, a computer running Mac OS X and XCode is necessary, whereas a computer running Microsoft Windows and Visual Studio is required for the development of Windows applications. Applications for Android can be built on both of these systems.
Applications are executed in a time-based manner. The framework uses a rendering thread and a logic thread, which are always processed simultaneously in order to create a sequence of individual display frames.
During the process of each loop, the logic thread calls a user code method (e.g.
OnProcessTick()), which can be used to update the current state of the application. Typically, the implementation of this method should check how much time has passed since the last call (e.g.
GetCurrentTickDuration()) and which user input has been carried out. Based on this information, the application is “simulated” and relevant objects are updated accordingly.
After finishing this procedure one or more times, the rendering thread starts to actually render all visible objects, while the logic thread already begins to evaluate the subsequent step.
The logic thread's actual step size (i.e. the time between two steps) can be defined via specific items in the main configuration of the framework. It is possible to either select a fixed or variable step size. Furthermore, it is also possible to define a minimum and maximum time value. Additionally, the configuration allows defining the actual number of steps which are carried out for each frame, again with possible minimum and maximum values.
In order to describe the virtual world, the Murl Engine uses a scene graph. A scene graph is an object-oriented data structure, specifically a directed acyclic graph (DAG). Within a scene graph individual objects can be stored, transformed and/or grouped together in a user-defined way.
The scene graph represents a tree-like structure made up from individual graph nodes with exactly one root node and any number of child nodes. Each graph node contains specific information about the virtual scene such as geometry, color, light sources, audio sources or transformation depending on its specialization.
Usually, every graph node affects its child nodes. For instance, a
Transform node can be used to hierarchically modify e.g. the position and rotation of the sub-graph below the node, i.e. all of its children, grand-children etc. Generic nodes may be e.g. made invisible, which also affects all child nodes in their sub-graphs.
State nodes, such as
CameraState, form an exception. State nodes cause a change in the current traversal context and affect therefore not only the child nodes, but also all subsequent nodes. If, for instance, a
MaterialState node is used to select a certain material, every subsequent node is rendered with this material until a different material is chosen with another
The graph node
SubState can be used to locally restrict state changes. That means that any context changes within a child of a
SubState node do not affect any subsequent nodes. After processing its child nodes, the
SubState node resets all changes made to the traversal context.
All types of graph nodes have some basic properties in common:
id property is used to clearly identify a specific node in the graph. The
visible property allows controlling the visibility of the node and its child graphs. All nodes, which are set invisible, are skipped during the rendering of a frame (the Output Traversal is skipped). The
active property controls, if logic operations are carried out or not (the Logic Traversal is skipped). To completely deactivate a node, both the
visible properties must be set to false. This can either be done separately or via the combined
- Caution, only alphanumeric characters and the characters point and underscore are allowed as any character of the id property. In addition, id properties may not start with a numeric character. Other characters (e.g. -, +, :, etc.) are not allowed.
During the process of each loop, the framework performs a number of traversals on the specified scene graph. Starting at the root, the graph is processed in a depth-first manner. All children are visited recursively in the order they are defined. For the sample graph above, the order, in which the graph nodes are traversed, is as follows:
Basically, there are two different traversals which are carried out during each loop - Logic Traversals and Output Traversals:
- A logic traversal always happens directly after the calls in the
OnProcessTick()methods of the user code are finished. Nodes, whose properties were modified through the user code, have their internal state updated, and a physics simulation step is performed, if the graph contains nodes which require such an action.
- After all logic traversals for a frame are finished, a single output traversal is carried out. All relevant information for output generation is gathered and all visible nodes are prepared for rendering.
One way to build a scene graph or parts of a scene graph for the Murl Engine is to create one or more text documents using XML notation. These XML files can be loaded into the system memory by the application. Afterwards, the individual nodes can be accessed and modified as required. The following paragraphs give a brief overview of the structure of such a file.
<?xml version="1.0" ?> <Graph> <View id="view" /> <PerspectiveCamera id="camera" viewId="view" fieldOfViewX="400" nearPlane="400" farPlane="2500" clearColorBuffer="1" > <!-- comment --> <FixedProgram id="prg_white" /> <Material id="mat_white" programId="prg_white" /> <MaterialState materialId="mat_white" slot="0" /> <Transform id="transform"> <CubeGeometry id="myCube01" scaleFactor="200" posX="0" posY="0" posZ="0" /> <PlaneGeometry id="myPlane01" scaleFactorX="42" scaleFactorY="100" posX="0" posY="0" posZ="0" /> </Transform> <CameraTransform cameraId="camera" posX="0" posY="0" posZ="800" /> </PerspectiveCamera> </Graph>
Generally, XML documents start with an optional XML declaration, e.g.
<?xml version="1.0" ?>
Elements within an XML document are indicated by
< >. Every element consists of a start tag
<element> and an end tag
</element>. Child elements are defined in between the start and end tags. For elements, which do not contain any child elements, the empty element tag
<element/> can be used optionally. Defining an element via
<element></element> is equivalent to
Attributes of an element are specified in the start tag (or in the empty element tag) as a pair of attribute name and attribute value:
Comments within an XML file start with
<!-- and end with
- Caution! Individual attributes cannot be put within comment markers.
There are several basic rules for an XML document to be well-formed:
- A well-formed XML document has to contain exactly one root element, which encloses all other (child-) elements.
- For every start tag there has to be exactly one matching end tag at the same nesting level and vice-versa.
- All attribute names of an element must be unique (there cannot be two or more attributes with the same name within an element).
Class, namespace, method and function names start with an upper-case letter
Interface names begin with an upper-case I
Variables start with a lower-case letter
Member variables start with a lower-case m
File names are composed of namespace and class name and consist only of lower-case letters, digits and/or underscore (“_”) as separator.
murl_my_app.h for class
MyApp inside the namespace
C++ compilers are free to define the standard C++ data types with e.g. different bit depths and value ranges. For this reason, it should be considered to always use the platform-independent data types defined in the framework (Note the upper-case, e.g.
Char instead of
char .). These data types are defined in the file
Matching container classes from
Mathematical constants from
The header file
murl_debug_trace.h contains methods to print
Debug::Trace can be used to print simple status information to the console. On some platforms, the output also includes time stamp information. Just as for the function
printf, formatting parameters can be used optionally.
Debug::Trace("Hello World!"); Debug::Trace("The result is %u:", result);
Any output of
Debug::Trace calls will only be generated in the debug mode and will automatically be omitted during the release build of an application. If a message should be visible in a release build as well,
Debug::Error can be used instead.
MURL_TRACE prints in addition to the status information also the method name and the line number. Optional formatting parameters can also be used.
MURL_TRACE(0, "Debug Hello World");
The first parameter (0) defines the log level for this messages. Debug messages with a log level greater than the global log level will be suppressed. If for example the global log level is set to 1, only debug messages with a log level smaller than or equal to 1 are printetd. You can use the method
GetLogLevel to set or read the global log level. The printed status information would look like this:
The output of
MURL_TRACE calls will also only be generated in the debug mode.
Another way to print information to the console is the
Murl::System::Console::Print function. By using this function, the output will be retained even in a release build. In addition, it prints the given text “as is”, with no extra information such as time stamp, new lines etc.
It also accepts optional formatting parameters:
System::Console::Print("Print with System Console"); System::Console::Print("Current Time %.10f", state->GetCurrentTickTime());
By using the
PrintTree method, it is possible to print the structure of the current scene graph to the console:
Graph::IRoot* root = state->GetGraphRoot(); root->PrintTree();
Depending on the given start node, it is possible to print the whole scene graph or only a sub-graph.
Graph::INode* node = mBallTransform->GetNodeInterface(); node->PrintTree();
state->SetUserDebugMessage("Package Loading succeeded!");