Tech/LxEngine/Tutorials/Tutorial 3

Overview
Tutorial 3 adds to the spinning cube from Tutorial 2 by adding more advanced geometry and materials.

Tutorial 3 sets up command-line arguments for the program, loads up an XML document and processes its elements to load the geometry and materials, uses the BlendLoad subsystem to load .blend files to use as the geometry, uses the ShaderBuilder subsystem to create complex shaders from simple  descriptions, and lastly sets up some basic user controls to cycle through the combinations of geometry and materials.

Controls


 * - exits the application
 * - advances to the next model (geometry)
 * - rewinds to the previous model
 * - advances to the next material
 * - rewinds to the previous material

Concepts Introduced

 * The LxEngine Document Object Model
 * Engine::globals
 * Engine::parseCommandLine
 * LightSet
 * BlendLoad
 * ShaderBuilder

Prerequisites

 * Tutorial 2
 * Basic familiarity with XML
 * Basic familiarity with the XML/HTML DOM (Document Object Model)
 * Basic familiarity with the JSON data format

Results
If all goes well, you should end up with a viewer app that allows you toggle through various materials and models.

(If you can't see the video below, you may want to try refreshing the page.)



Headers
Let's start with the headers:

Two new headers of note have been included since the last tutorial: and. These will be used, respectively, to dynamically build shaders from a convenient JSON-syntax and to load Blender model files.

Program Start-up
Next, let's compare the start-up from the previous tutorial and from this one. The start-up prior to the main loop (i.e. ) was fairly simplistic last time. For Tutorial 3, however, we'd like to add some command-line arguments as well as load all the application models and materials from a file (rather than hard-coding that data).

Here's the code from last time. It creates the Engine and then creates an empty Document that we never did anything with.

Command-line parameters
In Tutorial 3, we add several lines of code that add to the and then make a call to. This new code should be a bit self-explanatory as it sets up Tutorial 3's command-line arguments:

In more detail...

The Engine::globals method returns a special table of variant data that for storing global configuration parameters: i.e. int values, string values, even arrays and nested data. This provides a convenient means for storing "named" application data in a way that will be accessible to the whole application. Using string named variables like this makes it easier for the engine and its components expose this data to the user via command-line arguments (as in this case), more generally via an in-application console and/or scripting language, or any sort of user configurable application-wide variables and settings.

The table entries get three parts:
 * a name
 * a set of flags in this case describing what type of data that variable can hold
 * a validation function to ensure the data being set is a valid value
 * a default value.

This is enough information that the Engine object can now expose each of these variables as a command-line parameter on application start-up.

In Tutorial 3, the method is called next. This uses the globals table along with Boost Program Options to construct a set of command-line options. This makes it very easy to add new options to the command-line and do so in a standardized fashion (since Boost Program Options is based on Unix standards). For example, now "view_width" can be set to 1024 pixels via the "--view_width=1024" command.

But of course, if you don't like this approach or you require a non-standard argument on the command-line, there's no necessity to call parseCommandLine - you can set up your own parser and completely ignore this method. It's there for convenience and is not a necessity.

Read-back
The parseCommandLine checks for the command-line arguments, but what about using them?

Trivial:

The table entries are lxvar data objects which now should either be storing the user provided value or the default.

We won't go into in too much detail other than to say it's used throughout the system in non-performance critical areas and is a convenient way to store variant data. Also note the flags and validation functions set up earlier ensured that the value stored are the right type and value; for example "view_width" is going to be an int between 32 and 4096 so there's no need to double-check those data values before passing them off to the.

The Document
The command-line arguments are handled, but now what about the main application data which we'll be getting from disk rather than hard-coding this time?

Loading the Document
This starts with the change from the call to in Tutorial 2 to a call to  in Tutorial 3...

The loadDocument call now creates an actual, useful data object (or 'model' in MVC terms) that our 'view' can now read its data out of. The Document is an XML document composed of Elements as the individual nodes of the document. This is using the LxEngine DOM (Document Object Model) which is a simplification of a full XML/HTML DOM.

In LxEngine, the XML nodes are all Element objects: there is no Node class, no TextNode, or other specializations. The Element object contains one single value and a set of attributes. The value is usually a JSON-value as are the attributes (though attributes are often a 'simple' JSON value such a single string or number). These JSON values are stored in the code as objects.

There are a lot of details about the LxEngine DOM versus a proper XML DOM, but those don't really matter in the context of these early tutorials. Think of Document as an XML Document storing a tree of Elements each composed of a set of attributes and a single JSON value and references to their direct children.


 * Document is a...
 * Tree of Elements, each with...
 * 0 or more named attributes, each with an lxvar value
 * 1 lxvar value for the Element itself

What's the Document look like?
Rather than continuing to speak abstractly about the Document, let's take a look at a snippet of the actual XML document being loaded:

So the XML document contains a Element which contains a set of  and  elements. The Geometry element is pretty straight-forward: it just has an attribute indicating the .blend file to use as the source for that geometry. The Material element is a little more interesting simply because it is using JSON as it's value to describe its data. The use of JSON for the value is common as there are many cases where XML document is aimed more towards conveniently storing structured data than plain text.

Renderer Initialization
Ok, so we've shown how to get a bunch of data into the app. Now it's time to do something with that data. As we'll see the method has changed rather substantially.

Note the new code after the call to on the rasterizer:

What's happening here?

Two new important calls to protected methods we've added Renderer:
 * to go through the values on the we set up earlier
 * to go through that XML document we just loaded up

As you might guess from those checks for empty - the above two methods will be populating the Renderer with some materials and geometry

_processConfiguration
Let's take it for granted that  loads up the given model and   loads up a material (given a shader name and an optional list of parameters for the shader). With those assumptions, the above code should be pretty self-explanatory. Right? It's basically using 's variant data to check if values exist and process them if they do.

We'll get to the implementation of  and   in a minute.

_processDocument
Command-line options out of the way, it's time to process the Document that this View is supposed to be viewing. This means creating view-specific caches for all the geometry and materials in that document.

Let's dive in: find all elements of Material type and Geometry type and pass them on to the next worker functions (the getElementsByTagName method should be familiar to HTML DOM users)...

_processGeometry and BlendLoad
We'll start with Geometry because it's very simple. Read the attribute call "src", treat it as a string, and then call _addGeometry...

And what does _addGeometry do?

It uses the BlendLoad subsystem to do all the work. That subsystem takes a .blend file and a reference to a Rasterizer and sets up a GeometryPtr for you. It's basically doing a more generic version of the geometry creation you saw back in Tutorial 2. That's it. Nothing tricky. If you want to know more about BlendLoad, see it's documentation.

Note that we split _processGeometry and _addGeometry into two methods so that the if a model_filename is added on the command-line that same _addGeometry method can be called.

_processMaterial and ShaderBuilder
Not surprisingly, the code to add a new Material is not that different:

The interesting part of the above is use of the ShaderBuilder. We grab the Element's value (i.e. that chunk of JSON describing the material) and pass it off to the shader builder to create a ShaderBuilder::Material. This material has three parts:


 * A unique name for the source code generated
 * The GLSL source code itself that gets generated
 * A set of name-value parameters to pass to the shader's uniforms to render the shader

A material in LxEngine (and most real-time systems for that matter) is composed of two parts: a shader and the set of parameters to that shader.

The ShaderBuilder therefore is smart about realizing when two materials require different shader code versus simply a different set of parameters. For example, if you define a black and white checker material and a red and blue check material, the ShaderBuilder will generate two different sets of parameters for the same shader source code.

The "unique name" output is basically an identifier which is guaranteed to be the same for two materials with the same shader source and different for any two materials for differ shader source; in other words, comparing the unique name of two materials is sufficient to see which shader they should use (rather than comparing the entire shader source text). This is all designed so the rasterizer can cache compiled shaders and reuse them rather than creating a shader for every single material (since shaders are fairly expensive objects).

LxEngine's RasterizerGL is, of course, designed to create materials via this very mechanism (note however there is no hard dependency between the ShaderBuilder and RasterizerGL - it's perfectly feasible to write an application that just uses the ShaderBuilder and virtually no other piece of LxEngine or to use RasterizerGL with your own shaders or shader generators):

We'll get into the ShaderBuilder's data definitions in a bit, but the actual creation process is pretty straightforward.

Finishing up Renderer::initialize
Ok, so all the data is now loaded. On to the rest of :

We'll be a bit lazy here and just hard-code a light set since some of the materials require lights.

Then we have the familiar Item creation, but now we're reading out data out of the arrays of materials and geometry we created.

And lastly, we've moved the camera initialization so that we can set up the camera position based on the size of the model we just loaded. No new concepts in that code, just some use of the GLM and GLGeom libraries to do the math of computing the new position.

The render Method
No changes in the render method other than passing in that light set we created:

User Interface
Let's fly through the trivial changes to add the new keyboard controls:

Add a new virtual method implemenation and pass messages to the View.

The change of materials versus geometry is intentionally handled differently just to show two ways of accomplishing the same thing.

Then in the Renderer's handleEvent:

ShaderBuilder
We flew through the code and really glossed over how the ShaderBuilder materials are defined. The geometry was simple: a Blender file name is specified in the XML document and a utility function creates a GeometryPtr. But what about these materials?

It's a little beyond the scope of this tutorial to describe the entire ShaderBuilder interface. The ShaderBuilder documentation should be used for a full description. But for this tutorial, we'll provide a brief introduction.

JSON Shader Graphs
The materials are created via a simple shader graph (at the moment, it needs to be a DAG) specified in JSON code. The graph root is specified via a the "graph" element at the base of the JSON data. Then a shader graph node is specified.

The node itself is composed of a "_type" and a set of optional parameters, which depends on the type of node being used. All the parameters other than _type are optional as the node definition includes a default value for each parameter. In the example below, we use the solid color node to produce a very simple shader.

The ShaderBuilder does have some convenience intelligence so that the "solid" node can actually be used where a vec3 or a vec4 output is required. But a solid colored shader isn't that interesting.

How about a Phong material with a black and white checker pattern for the diffuse channel, where the checker pattern is applied using a spherical map that scaled 4x?

Or how about a checker pattern where each tile of the checker actually has it's own Phong definition with different levels of specularity on each tile?

As long as the input and output types are aligned correctly, the nesting is limited only by what the runtime hardware can handle. Again, this is intended only to be a very brief intro. The ShaderBuilder documentation lists all the nodes and features of ShaderBuilder.

Conclusion
This tutorial introduced how to setup convenient command-line parameters for your program, how to load a document and process the XML elements in that document, and how to build custom materials using the ShaderBuilder subsystem.

At this point, the reader should have a fairly good idea of how to get data into their application and how render some basic geometry and materials that are a little more exciting than a simple spinning cube.

Appendix A: Feedback, Questions, & Issues
Please provide any feedback, questions, or issues on the forums.

LxEngine is in active development and in need of improvements. Suggestions for making it simpler to use are most welcome!

Appendix B: Future Improvements
These are some tentatively planned improvements for future revisions of this tutorial:


 * Texture map examples
 * Shadows
 * Diffuse Gradient Lights
 * Cel Shading
 * Gooch Shading
 * Wireframe Shading
 * (more suggestions welcome)