Introduction

A brief introduction to the Murl Engine development

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.

Native and Cross Platform

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.

intro-usercode-frameworkcode.png
Application = User Code + Framework Code + 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.

intro-framework-ide.png
OS dependency for building application

Time Based

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.

Scene Graph

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.

intro-scenegraph.png
Scene Graph Example

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 MaterialState, ParametersState or 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 MaterialState node.

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:

  • unique id
  • active property
  • visible property

The 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 active and visible properties must be set to false. This can either be done separately or via the combined activeAndVisible property.

Note
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:

  1. Root
  2. View
  3. PerspectiveCamera
  4. FixedProgram
  5. Material
  6. MaterialState
  7. Transform
  8. CubeGeometry
  9. PlaneGeometry
  10. CameraTransform

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.

XML Files

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 <element/>.

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: attributeName="attributeValue"

Comments within an XML file start with <!-- and end with -->.

Note
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).

Naming Conventions

CLASSES, NAMESPACES, FUNCTIONS, METHODS

Class, namespace, method and function names start with an upper-case letter

e.g. MyApp or LoadData()

INTERFACES

Interface names begin with an upper-case I

e.g. IAppConfiguration

VARIABLES

Variables start with a lower-case letter

e.g. rotationAngleX

MEMBER VARIABLES

Member variables start with a lower-case m

e.g. mVar

FILE NAMES

File names are composed of namespace and class name and consist only of lower-case letters, digits and/or underscore (“_”) as separator.

e.g. murl_my_app.cpp / murl_my_app.h for class MyApp inside the namespace Murl

Primitive Data Types

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 murl_types.h:

murl/base/include/engine/murl_types.h
UInt64 Unsigned 64 bit integer (0 to 18.446.744.073.709.551.615)
UInt32 Unsigned 32 bit integer (0 to 4.294.967.295)
UInt16 Unsigned 16 bit integer (0 to 65.535)
UInt8 Unsigned 8 bit integer (0 to 255)
SInt64 Signed 64 bit integer (−9.223.372.036.854.775.808 to 9.223.372.036.854.775.807)
SInt32 Signed 32 bit integer (−2.147.483.648 to 2.147.483.647)
SInt16 Signed 16 bit integer (−32.768 to 32.767)
SInt8 Signed 8 bit integer (-128 to 127)
Double 64bit IEEE floating point (52 Bit Mantisse, 11 Bit Exponent)
Float 32bit IEEE floating point (23 Bit Mantisse, 8 Bit Exponent)
Real platform-specific - either 32 bit or 64 bit IEEE floating point
Bool Boolean true or false (XML File: "true", "on", "yes", "1" or "false", "off", "no", "0")
Char Character

Matching container classes from murl_types.h:

Mathematical constants from murl_math_types.h:

Corresponding mathematic functions are located in the namespace Murl::Math:

Further util functions can be found in the namespace Murl::Util e.g.:

Strings

Furthermore, the framework provides its own string class, together with a number of conversion functions and useful auxiliary functions.

murl/base/include/engine/murl_string.h
murl/base/include/engine/util/murl_util_string.h

Debug Messages

The header file murl_debug_trace.h contains methods to print debug messages.

murl/base/include/engine/debug/murl_debug_trace.h

DEBUG::TRACE, DEBUG::ERROR

The function 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

The command 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 SetLogLevel and GetLogLevel to set or read the global log level. The printed status information would look like this:

Murl::App::ContainerLogic::PrintDemo(), line 63: Debug Hello World

The output of MURL_TRACE calls will also only be generated in the debug mode.

SYSTEM::CONSOLE::PRINT

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.

murl/base/include/engine/system/murl_system_console.h

It also accepts optional formatting parameters:

System::Console::Print("Print with System Console");
System::Console::Print("Current Time %.10f", state->GetCurrentTickTime());

PRINTTREE

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();

SETUSERDEBUGMESSAGE

As soon as the “debug” package is loaded by the application, the method SetUserDebugMessage of the Murl::Logic::IState interface can be used to show a simple status message on the screen:

state->SetUserDebugMessage("Package Loading succeeded!");


Copyright © 2011-2017 Spraylight GmbH.