This page describes how individual output-related nodes (such as materials, textures and geometry) interact with each other during the scene graph output processing. It is assumed that you have basic knowledge about these different types of nodes. If you have not read the previous page (Output Processing) yet, it is advised to do so before continuing.
Output States
While the whole topic of output generation covers both audio and video rendering, the main focus here will be on visual output only as sound playback is rather straight-forward. The Sound tutorial explains in detail how to create audio output using sound objects and sequences.
The following sub-sections present different ways of how the Murl Engine's internal output states can be controlled for effective rendering of visual output.
Encapsulated States
If you have already worked through the Cube tutorial, the following XML scene graph example should look somehow familiar. It represents one of the most heavily stripped-down examples of how to render some geometry on screen, with all necessary nodes.
<?xml version="1.0" ?> <Graph> <View id="view"/> <Camera viewId="view" fieldOfViewX="2"> <FixedProgram id="prog"/> <Material programId="prog"> <PlaneGeometry posZ="-1"/> </Material> </Camera> </Graph>
The Camera
has two child nodes: A FixedProgram
node obviously using default parameters, and a Material
node linked to it via its programId
attribute. The Material
node itself has one child: the PlaneGeometry
node.
So, what is happening here?
- Firstly, as described in the tutorial, we can activate a camera by simply defining some drawable geometry somewhere within its sub-graph. We did so with the
PlaneGeometry
, which is a grand-child of the camera. - Secondly, it is possible to assign a specific material to a geometry object again by defining the geometry somewhere within the sub-graph of the material.
We can see that the Murl Engine must somehow know which camera and material is active at a particular time during traversal, when encountering some geometry to render. In our case, the engine stores a pointer to our camera node in an internal state object before processing its children, processes all children one after the other, and restores the previous state when done (which means no camera is active anymore). The same applies here for the material node, which gets also stored in a separate internal state object.
We will continue to use the term state encapsulating for this method of assigning nodes to each other.
Regarding geometry nodes (implementing the IDrawable
interface), the following interfaces and their nodes have effect:
Audio nodes (implementing IPlayable
) are affected by the following interface only:
Below is another code sample showing how to render a plane object using a camera, material, parameters and a texture. As an example, the highlighted lines mark the region where exactly the given material is active for rendering, i.e. where the internal material state points to that material.
- Note
- Note for this and the upcoming examples: In the presented code fragments below, a lot of information will be omitted and only relevant pieces will be shown. The respective examples do not necessarily represent functioning code. In this case, we refer to an image resource called
"main:image1"
, which must be supplied externally:
<View id="view"/> <Camera viewId="view" fieldOfViewX="2"> <FixedParameters diffuseColor="FF0080BFh"> <FixedProgram id="prog" coloringEnabled="yes"/> <Material programId="prog"> <FlatTexture imageResourceId="main:image1"> <PlaneGeometry posZ="-1"/> </FlatTexture> </Material> </FixedParameters> </Camera>
Note that in this example, the order in which material, texture etc. are defined is not important as long as all desired states are set for the geometry. The following code fragment shows a reordered hierarchy, but is functionally equivalent. Again, the highlighted lines mark the region where the material is active:
<View id="view"/> <FixedProgram id="prog" coloringEnabled="yes"/> <Material programId="prog"> <FlatTexture imageResourceId="main:image1"> <FixedParameters diffuseColor="FF0080BFh"> <Camera viewId="view" fieldOfViewX="2"> <PlaneGeometry posZ="-1"/> </Camera> </FixedParameters> </FlatTexture> </Material>
Let's construct another example. Say we have a number of little animal characters in a 2D game, with the following properties:
- Each animal should consist of a plane geometry representing the actual looks, and another plane geometry used for rendering a simple, semi-transparent drop shadow image.
- The drop shadow should be a little offset to the right and bottom.
- Each animal should use one of three different textures, for distinct looks.
- In addition, each animal should have a unique color value assigned, independent from the texture image.
- All animals share the same drop shadow texture.
- The main image and the shadow image should be rendered using different materials.
- We want to distribute these animals over the play field and move them independently.
The following image shows an illustration of what we want to accomplish. It is taken from the game "Crazy Rings", courtesy by Cervo Media GmbH (http://www.cervomedia.com).
As a daunting example, we will first try to create the scene graph using encapsulated states only. For clarity reasons, we will restrict the number of animals to three. We will omit some XML attributes such as the camera view etc., also for clarity.
Here is our first approach:
<FixedProgram id="animal_prg" coloringEnabled="yes"/> <FixedProgram id="shadow_prg"/> <Camera> <!-- Main transformation for animal #1 --> <Transform id="animal_pos_1"> <Material programId="animal_prg"> <Texture imageResourceId="main:image1"> <FixedParameters diffuseColor="FF8080h"> <PlaneGeometry depthOrder="1"/> </FixedParameters> </Texture> </Material> <Material programId="shadow_prg"> <Texture imageResourceId="main:shadow"> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Texture> </Material> </Transform> <!-- Main transformation for animal #2 --> <Transform id="animal_pos_2"> <Material programId="animal_prg"> <Texture imageResourceId="main:image2"> <FixedParameters diffuseColor="80FF80h"> <PlaneGeometry depthOrder="1"/> </FixedParameters> </Texture> </Material> <Material programId="shadow_prg"> <Texture imageResourceId="main:shadow"> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Texture> </Material> </Transform> <!-- Main transformation for animal #2 --> <Transform id="animal_pos_3"> <Material programId="animal_prg"> <Texture imageResourceId="main:image3"> <FixedParameters diffuseColor="8080FFh"> <PlaneGeometry depthOrder="1"/> </FixedParameters> </Texture> </Material> <Material programId="shadow_prg"> <Texture imageResourceId="main:shadow"> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Texture> </Material> </Transform> </Camera>
What we can clearly see here is that we do have lots of duplicated materials and worse duplicated textures. This is generally a bad idea as each texture node holds its own memory pointer for storing the image pixel data. Therefore, we better come up with an alternative.
In our second approach using encapsulated states only, we try to regroup the nodes in the graph so that we at least do not duplicate any textures. This approach might look something like this:
<FixedProgram id="animal_prg" coloringEnabled="yes"/> <FixedProgram id="shadow_prg"/> <Camera> <Material programId="animal_prg"> <Texture imageResourceId="main:image1"> <!-- Main transformation for animal #1 --> <Transform id="animal_pos_1"> <FixedParameters diffuseColor="FF8080h"> <PlaneGeometry depthOrder="1"/> </FixedParameters> </Transform> </Texture> <Texture imageResourceId="main:image3"> <!-- Main transformation for animal #2 --> <Transform id="animal_pos_2"> <FixedParameters diffuseColor="80FF80h"> <PlaneGeometry depthOrder="1"/> </FixedParameters> </Transform> </Texture> <Texture imageResourceId="main:image2"> <!-- Main transformation for animal #3 --> <Transform id="animal_pos_3"> <FixedParameters diffuseColor="8080FFh"> <PlaneGeometry depthOrder="1"/> </FixedParameters> </Transform> </Texture> </Material> <Material programId="shadow_prg"> <Texture imageResourceId="main:shadow"> <!-- Shadow transformation for animal #1 --> <Transform id="shadow_pos_1"> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Transform> <!-- Shadow transformation for animal #2 --> <Transform id="shadow_pos_1"> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Transform> <!-- Shadow transformation for animal #3 --> <Transform id="shadow_pos_1"> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Transform> </Texture> </Material> </Camera>
We managed to get rid of the duplicated materials and even the texture duplicates, but at the cost of introducing a separate transform node for each shadow plane. The result is quite annoying when we want to reposition the individual animals as we would need to explicitly set the shadow position. There has to be a better way!
Explicit State Nodes
When we look at the examples above, we clearly see that using state encapsulation may become very unwieldy when it comes to large scene graphs where a lot of geometry objects are rendered and e.g. a lot of different materials and/or textures have to be applied.
Additionally, we can see that with that scheme it is impossible to reuse e.g. a specific material node for geometry that is not contained within its sub-graph. We are therefore forced to use massive duplication of nodes (which is not particularly resource-friendly), or to create a scene graph structure that is not very intuitive to interact with.
Fortunately, there is a solution to these problems: the use of explicit state nodes. All of the node entities described on the previous page (Output Processing) have an accompanying state node, which allows activating that entity in the scene graph later. The basic concept is illustrated in the following code fragment performing the same task of rendering a plane using a camera, material, parameters and a texture as in the respective example in the Encapsulated States sub-section. For illustration purposes, the material's active region is highlighted again:
<!-- Create the actual nodes --> <View id="view"/> <Camera id="camera" viewId="view" fieldOfViewX="2"/> <FixedParameters id="params" diffuseColor="FF0080BFh"/> <FixedProgram id="prog" coloringEnabled="yes"/> <Material id="mat" programId="prog"/> <FlatTexture id="tex" imageResourceId="main:image1"/> <!-- Set the desired states --> <CameraState cameraId="camera"/> <ParametersState parametersId="params"/> <MaterialState materialId="mat"/> <TextureState textureId="tex"/> <!-- Render geometry --> <PlaneGeometry posZ="-1"/>
As you can see, one notable difference (besides the completely changed structure) is that the total node count in our example has almost doubled. In practice, this is usually not a big problem as the newly introduced state nodes are very light-weight ones (compared to the nodes they refer to), do not occupy large amounts of memory and only take very little time to process.
The other difference is that the texture must have a unique ID assigned in order to reference e.g. a texture node via its accompanying state node. Here, the benefits usually also outweigh the higher effort as the respective node may be referenced (and thus reused) any number of times to quickly activate it in the engine's corresponding internal state object.
We now take a third approach on the game animals example using explicit state nodes only. The resulting XML code might look similar to the following sample. As an extra we highlight the region in which the first FixedParameters
node (named "par_1"
) is active to indicate the actual life time of a given state:
<!-- Create the actual nodes --> <Camera id="camera"/> <FixedProgram id="figure_prg" coloringEnabled="yes"/> <FixedProgram id="shadow_prg"/> <Material id="figure_mat" programId="figure_prg"/> <Material id="shadow_mat" programId="shadow_prg"/> <Texture id="tex_1" imageResourceId="main:image1"/> <Texture id="tex_2" imageResourceId="main:image2"/> <Texture id="tex_3" imageResourceId="main:image3"/> <Texture id="tex_shadow" imageResourceId="main:shadow"/> <FixedParameters id="par_1" diffuseColor="FF8080h"/> <FixedParameters id="par_2" diffuseColor="80FF80h"/> <FixedParameters id="par_3" diffuseColor="8080FFh"/> <!-- Set common states --> <CameraState cameraId="camera"/> <!-- Main transformation for animal #1 --> <Transform id="animal_pos_1"> <MaterialState materialId="figure_mat"/> <TextureState textureId="tex_1"/> <ParametersState parametersId="par_1"/> <PlaneGeometry depthOrder="1"/> <MaterialState materialId="shadow_mat"/> <TextureState textureId="tex_shadow"/> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Transform> <!-- Main transformation for animal #2 --> <Transform id="animal_pos_2"> <MaterialState materialId="figure_mat"/> <TextureState textureId="tex_2"/> <ParametersState parametersId="par_2"/> <PlaneGeometry depthOrder="1"/> <MaterialState materialId="shadow_mat"/> <TextureState textureId="tex_shadow"/> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Transform> <!-- Main transformation for animal #3 --> <Transform id="animal_pos_3"> <MaterialState materialId="figure_mat"/> <TextureState textureId="tex_3"/> <ParametersState parametersId="par_3"/> <PlaneGeometry depthOrder="1"/> <MaterialState materialId="shadow_mat"/> <TextureState textureId="tex_shadow"/> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Transform>
In this approach we managed to get rid of any material and texture duplicates simply by switching the respective states right before rendering our planes. In addition, we were able to get by with only a single transform node per animal, which acts on both the main image and shadow image planes.
We have a higher total node count though, but all of the newly added nodes are simple light-weight state nodes.
As you can see from the highlighted region, the life time of the internal parameters state set by the first ParametersState
node actually extends beyond the closing tag of its parent Transform
node. As long as no other node manipulating the respective state is encountered during traversal, the most recently set state persists. This may be of importance later as you might run into problems that occur when you forget to unset a specific state. We will later discuss possibilities that may help you in preventing such situations. See section Sub-States below for further details.
- Note
- As a side note: You might have wondered about the individual
ParametersState
nodes setting the parameters for their subsequent plane geometries: The current parameters state is still active when the following shadow plane geometry is rendered! In fact, this does not matter in our case as we did not enable coloring on the"shadow_mat"
material, so the color parameters have no effect when rendering the shadow planes.
Mixing Explicit State Nodes and Encapsulated States
Of course, the two methods of state encapsulation and explicit states do not exclude each other. Depending on the actual task you want to perform, one or the other method may be the better alternative, and you can also create scene graphs that contain both methods.
As a general rule, state encapsulation may be the better choice, if you know that you do not need to reuse a specific node. Very often this is the case for e.g. FixedParameters
nodes, when they actually specify a distinct color for a specific geometry, such as in our animals example (we stated that each animal should have a unique color assigned). In this simple example, we are also using only a single camera for all geometry, so this is the next candidate for encapsulation.
Again, we might rewrite the scene graph, but this time it is just a matter of taste:
<!-- Create the nodes we want to reuse --> <FixedProgram id="figure_prg" coloringEnabled="yes"/> <FixedProgram id="shadow_prg"/> <Material id="figure_mat" programId="figure_prg"/> <Material id="shadow_mat" programId="shadow_prg"/> <Texture id="tex_1" imageResourceId="main:image1"/> <Texture id="tex_2" imageResourceId="main:image2"/> <Texture id="tex_3" imageResourceId="main:image3"/> <Texture id="tex_shadow" imageResourceId="main:shadow"/> <!-- Activate single camera --> <Camera> <!-- Main transformation for animal #1 --> <Transform id="animal_pos_1"> <MaterialState materialId="figure_mat"/> <TextureState textureId="tex_1"/> <FixedParameters diffuseColor="FF8080h"> <PlaneGeometry depthOrder="1"/> </FixedParameters> <MaterialState materialId="shadow_mat"/> <TextureState textureId="tex_shadow"/> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Transform> <!-- Main transformation for animal #2 --> <Transform id="animal_pos_2"> <MaterialState materialId="figure_mat"/> <TextureState textureId="tex_2"/> <FixedParameters diffuseColor="80FF80h"> <PlaneGeometry depthOrder="1"/> </FixedParameters> <MaterialState materialId="shadow_mat"/> <TextureState textureId="tex_shadow"/> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Transform> <!-- Main transformation for animal #3 --> <Transform id="animal_pos_3"> <MaterialState materialId="figure_mat"/> <TextureState textureId="tex_3"/> <FixedParameters diffuseColor="8080FFh"> <PlaneGeometry depthOrder="1"/> </FixedParameters> <MaterialState materialId="shadow_mat"/> <TextureState textureId="tex_shadow"/> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Transform> </Camera>
Now, the resulting scene graph looks much more like what we want than in our first few approaches. However, this is still not the end! Simply continue reading to find out how this example can be further optimized.
State Slots
So far, we have explained how the Murl Engine's internal output generation state mechanism works, and we are able to assign specific properties to a simple geometry object.
Nevertheless, when looking at the most recent animals example above, it still feels overly complicated somehow. For each individual animal, we keep switching states back and forth, and the resulting scene graph looks a bit overcrowded.
To further improve the scene graph in this example, the Murl Engine provides an additional mechanism: State Slots. Simply speaking, the engine internally keeps track of more than one state simultaneously for the following entities:
All these interfaces derive from the common IStateSlot
interface, which provides methods to set and get a specific slot index. Nodes implementing these interfaces commonly provide a slot
attribute to define the slot index from within a scene graph XML file. There exist a total of 128 simultaneous state slots for materials, 128 for parameters and also 128 for textures.
So, what does this mean? In all our previous examples, we did not specify a slot
attribute anywhere in our scene graph, which means the default slot with index 0 was used for everything we did. We now start to rewrite the animals scene graph again, this time using slot indices other than 0 for certain nodes in the following code fragment. Although not necessary, we also specify the slot="0"
attribute for the other relevant nodes, just for clarity. The changed lines are higlighted:
<!-- Main transformation for animal #1 --> <Transform id="animal_pos_1"> <MaterialState materialId="figure_mat" slot="0"/> <TextureState textureId="tex_1" slot="0"/> <FixedParameters diffuseColor="FF8080h" slot="0"> <PlaneGeometry depthOrder="1"/> </FixedParameters> <MaterialState materialId="shadow_mat" slot="1"/> <TextureState textureId="tex_shadow" slot="4"/> <PlaneGeometry depthOrder="0" x="10" y="-10"/> </Transform>
If we now were to check the resulting output, we would notice that we completely screwed up rendering of the animals' shadows. The reason is that while we are setting the "shadow_mat"
material at slot 1 and the "tex_shadow"
texture at slot 4, the following plane geometry is still using the currently active material at slot 0 and texture also at slot 0. In these slots, the "figure_mat"
material and "tex_1"
texture are still active, which is clearly not what we want.
To solve this issue, we can now use the materialSlot
, parametersSlot
and textureSlots
attributes accepted by the PlaneGeometry
node, which allow to define the actual slot indices the plane geometry uses for rendering (Note that it is called textureSlots
and not textureSlot
! We will later see why.). Again, the redundant zero-index attributes are added for clarity, and the changes are highlighted:
<!-- Main transformation for animal #1 --> <Transform id="animal_pos_1"> <MaterialState materialId="figure_mat" slot="0"/> <TextureState textureId="tex_1" slot="0"/> <FixedParameters diffuseColor="FF8080h" slot="0"> <PlaneGeometry materialSlot="0" textureSlots="0" parametersSlot="0" depthOrder="1"/> </FixedParameters> <MaterialState materialId="shadow_mat" slot="1"/> <TextureState textureId="tex_shadow" slot="4"/> <PlaneGeometry materialSlot="1" textureSlots="4" parametersSlot="0" depthOrder="0" x="10" y="-10"/> </Transform>
Now, the resulting output should be OK again. We apply these changes to the rest of the animals, plus we assign individual slot indices for each of the animals' main textures, ranging from 0 to 2 for "tex_1"
to "tex_3"
, respectively (we can afford to do so, as we have a total of 128 possible slots). We highlight the lines where a material assignment is made:
<!-- Activate single camera --> <Camera> <!-- Main transformation for animal #1 --> <Transform id="animal_pos_1"> <MaterialState materialId="figure_mat" slot="0"/> <TextureState textureId="tex_1" slot="0"/> <FixedParameters diffuseColor="FF8080h" slot="0"> <PlaneGeometry materialSlot="0" textureSlots="0" parametersSlot="0" depthOrder="1"/> </FixedParameters> <MaterialState materialId="shadow_mat" slot="1"/> <TextureState textureId="tex_shadow" slot="4"/> <PlaneGeometry materialSlot="1" textureSlots="4" parametersSlot="0" depthOrder="0" x="10" y="-10"/> </Transform> <!-- Main transformation for animal #2 --> <Transform id="animal_pos_2"> <MaterialState materialId="figure_mat" slot="0"/> <TextureState textureId="tex_3" slot="2"/> <FixedParameters diffuseColor="80FF80h" slot="0"> <PlaneGeometry materialSlot="0" textureSlots="2" parametersSlot="0" depthOrder="1"/> </FixedParameters> <MaterialState materialId="shadow_mat" slot="1"/> <TextureState textureId="tex_shadow" slot="4"/> <PlaneGeometry materialSlot="1" textureSlots="4" parametersSlot="0" depthOrder="0" x="10" y="-10"/> </Transform> <!-- Main transformation for animal #3 --> <Transform id="animal_pos_3"> <MaterialState materialId="figure_mat" slot="0"/> <TextureState textureId="tex_2" slot="1"/> <FixedParameters diffuseColor="8080FFh" slot="0"> <PlaneGeometry materialSlot="0" textureSlots="1" parametersSlot="0" depthOrder="1"/> </FixedParameters> <MaterialState materialId="shadow_mat" slot="1"/> <TextureState textureId="tex_shadow" slot="4"/> <PlaneGeometry materialSlot="1" textureSlots="4" parametersSlot="0" depthOrder="0" x="10" y="-10"/> </Transform> </Camera>
Looking at the individual MaterialState
nodes in the highlighted lines, we can now see that once we have activated a material in a specific slot, that assignment is never changed anymore for that slot in our graph. For example, the "figure_mat"
material is assigned to slot 0 in line 6, and the next assignments to that same slot are made in lines 19 and 32 using the same material. The same is true for material "shadow_mat"
and slot 1, in the lines 12, 25 and 38.
This means, that the state assignments in lines 19, 25, 32 and 38 are redundant in our specific case, in other words utterly useless. The same is true with the texture state assignment for the "tex_shadow"
texture in slot 4. We can now get rid of all the redundant state changes, and may as well move the initial state assignment nodes up in the graph in order to group them together in a more readable way. We also remove the slot attributes for using the color parameters as they are all set to 0 anyway:
<!-- Create the nodes we want to reuse --> <FixedProgram id="figure_prg" coloringEnabled="yes"/> <FixedProgram id="shadow_prg"/> <Material id="figure_mat" programId="figure_prg"/> <Material id="shadow_mat" programId="shadow_prg"/> <Texture id="tex_1" imageResourceId="main:image1"/> <Texture id="tex_2" imageResourceId="main:image2"/> <Texture id="tex_3" imageResourceId="main:image3"/> <Texture id="tex_shadow" imageResourceId="main:shadow"/> <!-- Set common states --> <MaterialState materialId="figure_mat" slot="0"/> <MaterialState materialId="shadow_mat" slot="1"/> <TextureState textureId="tex_1" slot="0"/> <TextureState textureId="tex_2" slot="1"/> <TextureState textureId="tex_3" slot="2"/> <TextureState textureId="tex_shadow" slot="4"/> <!-- Activate single camera --> <Camera> <!-- Main transformation for animal #1 --> <Transform id="animal_pos_1"> <FixedParameters diffuseColor="FF8080h"> <PlaneGeometry materialSlot="0" textureSlots="0" depthOrder="1"/> </FixedParameters> <PlaneGeometry materialSlot="1" textureSlots="4" depthOrder="0" x="10" y="-10"/> </Transform> <!-- Main transformation for animal #2 --> <Transform id="animal_pos_2"> <FixedParameters diffuseColor="80FF80h"> <PlaneGeometry materialSlot="0" textureSlots="2" depthOrder="1"/> </FixedParameters> <PlaneGeometry materialSlot="1" textureSlots="4" depthOrder="0" x="10" y="-10"/> </Transform> <!-- Main transformation for animal #3 --> <Transform id="animal_pos_3"> <FixedParameters diffuseColor="8080FFh"> <PlaneGeometry materialSlot="0" textureSlots="1" depthOrder="1"/> </FixedParameters> <PlaneGeometry materialSlot="1" textureSlots="4" depthOrder="0" x="10" y="-10"/> </Transform> </Camera>
Now, this looks much better! We do not have any duplicate nodes, and no redundant state changes anymore. The individual sub-graphs for the animals are greatly reduced leaving only 4 necessary nodes per animal. That is quite good for what we want to accomplish.
(Strictly speaking, almost no duplicated nodes, except the shadow plane geometries. In addition, we notice that all three of our animal sub-graphs look very similar to each other. There are other useful methods you can try to address these issues, the page Instances and References will give you some hints on how you can optimize your scene graph even further.)
Working with state slots also has another advantage: When you e.g. quickly want to change the main image of a particular animal from code, you can simply access the plane geometry in the respective sub-graph via its IDrawable
interface and change its texture slot index to a different value by calling the SetTextureSlot()
method, which is a very efficient way for performing this task.
However, there is one important aspect of working with multiple state slots: Know the state you're in! Once you have set up a more complex state environment, it may become difficult to keep an overview of what is assigned where, when you do not plan your layout carefully. See section Sub-States below for some helpful techniques on that subject.
State Units
Do you remember the textureSlots
vs. textureSlot
attribute naming confusion in the previous section? There is one more thing to add here regarding multiple states.
Until now, we have only used a single texture to be applied to a geometry object at a time. However, modern graphics processors support a very useful feature for performing advanced rendering techniques, commonly known as multitexturing (see the following Wikipedia article: http://en.wikipedia.org/wiki/Texture_mapping). The Murl Engine (currently) supports the use of up to 8 individual textures in parallel for rendering a single geometry object, which usually get combined using a specific GPU shader program.
So, how do we apply multiple textures at the same time? The solution is state units.
We already know how to assign up to 128 different materials to the 128 available material slots (of which exactly one will be picked by the geometry for rendering), and we already know how to do so for textures and texture slots. But until now, we have only used one texture unit at default index 0.
We can now change this behavior as shown in the following scene graph fragment. We will pretend to e.g. render a number of light mapped geometry planes (see http://en.wikipedia.org/wiki/Lightmap) using two different materials, with different base color textures expected to be assigned to texture unit 0, and the individual light maps assigned to unit 1. We assume that the shader program of the respective material will combine these two textures to produce the output image. For details on how to actually do so, see the Shader-based Rendering page.
<!-- Set material state --> <MaterialState materialId="my_lightmap_mat_1" slot="0"/> <MaterialState materialId="my_lightmap_mat_2" slot="1"/> <!-- Set base texture states --> <TextureState textureId="base_tex_1" slot="0" unit=0"/> <TextureState textureId="base_tex_2" slot="1" unit=0"/> <TextureState textureId="base_tex_3" slot="2" unit=0"/> <!-- Set lightmap texture states --> <TextureState textureId="light_tex_1" slot="0" unit=1"/> <TextureState textureId="light_tex_2" slot="1" unit=1"/> <TextureState textureId="light_tex_3" slot="2" unit=1"/> <TextureState textureId="light_tex_4" slot="3" unit=1"/> <TextureState textureId="light_tex_5" slot="4" unit=1"/> <!-- Render planes --> <PlaneGeometry materialSlot="0" textureSlots="0,2" x="0"/> <PlaneGeometry materialSlot="0" textureSlots="1,1" x="10"/> <PlaneGeometry materialSlot="0" textureSlots="2,0" x="20"/> <PlaneGeometry materialSlot="0" textureSlots="0,1" x="30"/> <PlaneGeometry materialSlot="0" textureSlots="1,4" x="40"/> <PlaneGeometry materialSlot="1" textureSlots="2,3" x="50"/> <PlaneGeometry materialSlot="1" textureSlots="0,4" x="60"/> <PlaneGeometry materialSlot="1" textureSlots="1,2" x="70"/>
We can see that assigning a texture to a unit other than 0 can simply be done by using the unit
attribute of a TextureState
node with the desired index in a range from 0 to 7.
We can also see that there must obviously be an extra set of slots for texture unit 1 as otherwise we would have overwritten the assignments made in lines 6 to 8 by the state nodes in lines 11 to 13. In fact, there is exactly one array of 128 texture slots for each unit; in total this means that there can be at most 128*8=1024 different state assignments to cover all available entries.
In our case, it is now possible to pick any combination of two textures for rendering with the restriction that one must be selected from the slots at unit 0 and the other one from the slots at unit 1. We do so for every PlaneGeometry
in the example.
Here it becomes obvious why the attribute is called textureSlots
and not textureSlot
: We can specify a comma-separated list of slot indices that will be used for selecting the appropriate textures. The first value specified represents the index into the slot array for unit 0, the next one for unit 1 and so on. This comma-separated list must contain at least one value (for unit 0) and can have at most 8 individual ones (representing one for each available texture unit in ascending order).
In the example, the geometry node at line 22 uses the material assigned to slot 0 ("my_lightmap_mat_1"
), for texture unit 0 it uses the texture assigned to slot 1 ("base_tex_2"
), and texture unit 1 uses the texture from slot 4 ("light_tex_5"
) for rendering.
There might be situations, where a given shader program is only expecting e.g. texture units 0 and 7 to be active for rendering. In this case, it feels quite annoying having to specify all irrelevant slot indices for units 1 to 6, just to be able to set the last one by writing e.g. textureSlots="17,0,0,0,0,0,0,39"
. For this reason, geometry nodes actually do accept a textureSlot
(note: singular form!) attribute as input, but only in the following way, e.g.:
<PlaneGeometry materialSlot="42" textureSlot.0="17" textureSlot.7="39" x="0" />
Now, textures are not the only entities that support more than one unit. The following list shows all node interfaces supporting multiple units, i.e. which are derived from the IStateUnit
interface:
ITexture
andITextureState
, as we already know supporting 8 different units with 128 slots eachILight
andILightState
, currently supporting 2 different unitsIBone
andIBoneState
, supporting 24 different units.
Sub-States
As mentioned in the sub-section Explicit State Nodes above, using explicit state nodes has the effect that a specific state assignment always persists until another state node reassigns a different object to that internal state. See the following XML fragment, where we set up the state for a single material to be used for rendering multiple plane geometries arranged in some transform hierarchy:
<MaterialState materialId="mat1"/> <Transform posX="5"> <PlaneGeometry id="plane1" posY="2"> <PlaneGeometry id="plane2" posZ="3"/> </PlaneGeometry> <PlaneGeometry id="plane3" posY="4"/> </Transform>
We would now like to add another plane geometry that is supposed to always move together with plane1
as a sibling of plane2
. Additionally, we would like to use a different material for rendering our new plane4
, so we place a new MaterialState
node right in front of it. The changes are highlighted:
<MaterialState materialId="mat1"/> <Transform posX="5"> <PlaneGeometry id="plane1" posY="2"> <PlaneGeometry id="plane2" posZ="3"/> <MaterialState materialId="mat2"/> <PlaneGeometry id="plane4" posZ="-3"/> </PlaneGeometry> <PlaneGeometry id="plane3" posY="4"/> </Transform>
We now ran into trouble as the newly added MaterialState
node reassigns the material mat2
to slot 0 and this state persists until the end. As a consequence, plane3
is rendered using the wrong material. We correct this mistake by reverting the state assignment after plane4
has been rendered:
<MaterialState materialId="mat1"/> <Transform posX="5"> <PlaneGeometry id="plane1" posY="2"> <PlaneGeometry id="plane2" posZ="3"/> <MaterialState materialId="mat2"/> <PlaneGeometry id="plane4" posZ="-3"/> <MaterialState materialId="mat1"/> </PlaneGeometry> <PlaneGeometry id="plane3" posY="4"/> </Transform>
The result is OK again, but we can clearly see one drawback: The MaterialState
node responsible for reverting the original material state has to know which material was originally set. This might not be a problem in our simple example, but for larger scene graphs this can certainly become a major issue. Especially when state assignments are made from within a sub-graph added via an Instance
or a Reference
node, it is too often not obvious which assignments are actually made (see page Instances and References, as a side note). Maintenance of the scene graph can become a real pain even for a simple graph like this:
<MaterialState materialId="mat1"/> <Transform posX="5"> <PlaneGeometry id="plane1" posY="2"> <PlaneGeometry id="plane2" posZ="3"/> <!-- Black magic ahead --> <Instance graphResourceId="main:my_largest_sub_graph_ever"/> </PlaneGeometry> <PlaneGeometry id="plane3" posY="4"/> </Transform>
To solve these problems, it is possible to wrap a SubState
node around such a "suspicious" sub-graph in order to confine any state changes made therein:
<MaterialState materialId="mat1"/> <Transform posX="5"> <PlaneGeometry id="plane1" posY="2"> <PlaneGeometry id="plane2" posZ="3"/> <!-- Black magic successfully banned --> <SubState> <Instance graphResourceId="main:my_largest_sub_graph_ever"/> <MaterialState materialId="mat2"/> <PlaneGeometry id="plane4" posZ="-3"/> </SubState> </PlaneGeometry> <PlaneGeometry id="plane3" posY="4"/> </Transform>