Local Info

Topics

j3d.org

Texturing Basics Example

For most applications, textures provide the majority of the visuals. Where geometry is too costly for every single fine detail, textures can make do to give quite an acceptable alternative. Where textures really fall down is when you are starting to get up really close to the objects. That flat nature really shows out. Multitexturing was developed to allow the end programmer to tweak the visual effects by combining a series of textures together to generate a single image that is then applied to the object. Multi-texturing is most well known as the technique for "bump mapping" - making a surface look rough when it really isn't. In this tutorial we'll show you how to set up your own application to use multi-texturing techniques.

If you would like to see the complete version of this code, it can be found in the examples/texture directory. There are two demo applications - SimpleMultiTextureDemo.java and Dot3MultiTextureDemo.java.

 

Setting up the application

The demo starts, once again with a slight variation of the Hello World demo. In order to demostrate multitexturing effectively, the first demonstration is going to use a box for the geometry and offset the viewpoint so you can see it as though looking down on it from an angle. The second demo, where we show off bump mapping will just use a big flat rectangle.

Since this is a texturing example, we need to load a heap of textures for the various stages. If you're not familiar with how to create a basic texture instance in Aviatrix3D, then please wander back to the Texturing Basics tutorial.

Note: Multitexturing requires hardware support. The number of stages that can be combined together is dependent on the hardware and is different across video cards. Aviatrix3D does not provide fallback mechanisms for when you request more stages of multitexture than the local hardware supports.

What's the difference between Single and Multi Texturing?

In the previous tutorial you got an overview of the classes available to texturing. In normal texturing, you create a single texture instance and apply that to the object. At the Aviatrix3D level, that means only creating a single instance of TextureUnit and assigning that to your Appearance. With multitexture, you are now creating more than once instance and providing the instructions on how to combine them.

Multitexturing works as a pipeline. Each texture represents a step in a sequential series of operations. Each stage uses the output from the previous stage and the texture in this stage as it's input, along with some instructions on the way to combine it. At the OpenGL level, this is relatively simple to understand as each stage is just one command followed by another in a state machine. At the scene graph level dealing with objects rather than state, the ideas become a little more muddled. In the Aviatrix3D view of the world, each instance of TextureUnit represents the single stage of the OpenGL pipeline. The order that the instances are placed in the array that is handed to the Appearance instance. Change the instances in the array and the visual output changes.

The TextureUnit objects still follow the same basic state system that OpenGL uses. The commands for how to combine the stages are held in the TextureAttributes class. As with OpenGL, the first unit instance uses the underlying object's colour as the default input value.

A Simple two-stage demo

The first demo shows setting up a basic two-stage test that multiplies two textures together. The base texture is just a white texture with some text on it. This is then modified by a second texture that adds colour to the first, and ignores the underlying object colour.

Our application starts by loading up two textures - the base and colour, so let's assume we have two variables for these:

TextureComponent2D[] base_img = loadImage("textures/modulate_base.gif");
TextureComponent2D[] filter_img = loadImage("textures/modulate_red.gif");

Texture2D base_texture = new Texture2D();
base_texture.setImages(Texture.MODE_BASE_LEVEL,
					   Texture.FORMAT_RGB,
					   base_img,
					   1);

Texture2D filter_texture = new Texture2D();
filter_texture.setImages(Texture.MODE_BASE_LEVEL,
						 Texture.FORMAT_RGB,
						 filter_img,
						 1);

Now we need to set up TextureUnits to correspond to these two stages. Each stage needs to be given instructions about how to combine this texture with the input. These instructions are provided through the TextureAttributes class. There are two method options setTextureMode() and setCombineMode(). The former is used to set the primary mode of the texture combination operations. The latter is used when you want to start using more complex texturing options such as using different instructions for the RGB and Alpha channels. For this demo, we don't need that complexity and so only make use of setTextureMode().

The first stage supplied to the pipeline has two options for the input - the object's basic colour (derived from material and lighting values). There's no option for using the previous texture, because there is no previous texture for the first stage. However, we do have the option to completely ignore the object's colour, which is what we want to do for this demo. To ignore the values from the previous stage (in this case the underlying object colour), set the mode to MODE_REPLACE.

TextureAttributes base_ta = new TextureAttributes();
base_ta.setTextureMode(TextureAttributes.MODE_REPLACE);

For the second stage, the input texture is just a plain red square texture. The idea is to take the plain texture of the first stage and change it's colour to red. To do this, we use the MODE_MODULATE mode. This takes the input colour value and multiplies it by the texture colour value, on a per-channel basis. So, it takes the input red component (which is 1.0 since the input channel is mostly white), and multiplies it by the red component of the texture at this stage, which results in a red output pixel. Go to the blue channel and the texture value for this stage is 0, meaning that the blue component from the previous stage is completely removed, resulting in an output that is only red or black.

TextureAttributes filter_ta = new TextureAttributes();
filter_ta.setTextureMode(TextureAttributes.MODE_MODULATE);

Now you must combine them all together. To do this, you'll need to create an array to hold the two instances of TextureUnit. The order that the parts are put into this array determines the execution order. Therefore, to get the right look, we need to place the base texture as the first element in the array, and then the colour filter as the second element, just like this:

TextureUnit[] tu = new TextureUnit[2];
tu[0] = new TextureUnit();
tu[1] = new TextureUnit();
tu[0].setTexture(base_texture);
tu[1].setTexture(filter_texture);
tu[0].setTextureAttributes(base_ta);
tu[1].setTextureAttributes(filter_ta);

And, once again, apply this array to the object's appearance:

Appearance app = new Appearance();
app.setTextureUnits(tu, 2);

Dealing with texture coordinates

An important, yet frequently forgotten part of multitexturing is making sure that the textures are actually used correctly. When using single textures, you have to set the correct texture coordinates for the vertices of the geometry. As you may remember, if you don't set them, the default values are all zeros, meaning you end up with a single coloured object. Working with multitexture is no different. Each stage is a texture, which implies that for each stage you need to supply texture coordinates, otherwise the default values are used, and the results look really odd.

Specifying texture coordinates for multitextured geometry is exactly the same as providing a single set of coordinates. You've probably noticed that the setTextureCoordinates() method of VertexGeometry requires a two-dimensional array of values. The reason is that it allows you to provide more than one set of texture coordinates at a time. Each index in the array corresponds to the same index in the TextureUnit list. In the following example two textures are applied to an object - the first is held the normal way, while the second flips the texture in the S axis:

float[][] tex_coord = { { 0, 0,  1, 0,  0, 1,   1, 0,  1, 1, 0, 1 }
                        { 1, 0,  0, 0,  1, 1,   0, 0,  0, 1, 1, 1 } };

In some cases you want to have the same texture coordinates used for more than one of the stages. One option is to provide the same array twice. However, this is inefficient as you now pass the same data twice down the bus to the video card. An alternative way of accomplishing the same task is to use the texture set mapping ability of Aviatrix3D. What this does is allow you to only specify an array of data once, and tell the renderer that you'd like to replicate that data for more than one strip. To do this, you need to create another int array, where the values in the array point to the source texture coordinate array to use. For example, to use the same array for both the first and second texture units, use the following:

int[] tex_maps = { 0, 0 };
geometry.setTextureSetMap(tex_maps, 2);

Any ordering can be used, for example:

int[] tex_maps = { 0, 1, 2, 0, 0 };
geometry.setTextureSetMap(tex_maps, 5);

which uses 5 texture units, with the first three units using their own array, while units 3 and 4 share the first array.

Finally, with all the pieces in place - textures, stage setups, and texture coordinates, you can run the application to get something that looks like this:

Using Texture Combiners

Now that you have a basic understanding of the way Aviatrix3D texture units work, it's time to move onto the more interesting capabilities. One of the widest uses of multitexturing is to create effects that only use parts of the texture to change the output. For example, instead of using geometry to control the shape of a complex object, use a texture that describes the outline and use it to "cut out" the desired shape, while still leaving the original colour of the object. In simple terms, this means using one stage of the texture pipeline to only effect the alpha channel, while leaving the RGB values untouched.

One of the most well-known uses of texture combiners is bump mapping, sometimes known as Dot-3 bump mapping. This gives the surface a look of being rough as it modulates the incoming light by a percieved surface normal that is defined by another texture.

To use texture combiners in Aviatrix3D, you start with the same setup as for the previous example - you'll need a TextureUnit per texture that you'd like to combine together. There will also need to be texture coordinates etc constructed and set up. Where the differences will lie is how you assign the values for the TextureAttributes instance used on each stage.

As noted earlier, there are two methods on TextureAttributes - setTextureMode() and setCombineMode(). The method that we're interested in is the latter. However, we can't make use of the latter unless we have already made use of the former. That is, unless you tell the attributes that you want to use combine mode details, it will ignore anything you set there. Thus, the first step to using texture combiners is to inform Aviatrix3D that you want to use them:

TextureAttributes base_ta = new TextureAttributes();
base_ta.setTextureMode(TextureAttributes.MODE_COMBINE);

Now that is out of the way, you can start to set the combine modes that you would like to make use of. This tutorial won't go into the specifics of this though, as there are plenty of tutorials about what they are and how to use them. For example, to set up for a bump map stage you would use the following code (the blend colour value can be ignored if you like):

TextureAttributes base_ta = new TextureAttributes();
base_ta.setBlendColor(0.5f, 0, 0, 0);
base_ta.setTextureMode(TextureAttributes.MODE_COMBINE);
base_ta.setCombineMode(false, TextureAttributes.COMBINE_DOT3_RGB);
base_ta.setCombineMode(true, TextureAttributes.COMBINE_REPLACE);
base_ta.setCombineSource(false, 0, TextureAttributes.SOURCE_CURRENT_TEXTURE);
base_ta.setCombineSource(true, 0, TextureAttributes.SOURCE_CONSTANT_COLOR);

The first argument of the setCombineMode() defines whether this mode value is being set for the alpha channel (true) or the RGB values (false). The second argument is one of the combine value constants. For setCombineSource() the first parameter also specifies whether this instruction applies to the alpha or RGB values, then which source to apply the function to, and finally the place to source the incoming data from. Texture combiners can have up to 3 different input sources for a given stage, but in general only one or two are used, so the value for the second parameter will always be 0, 1 or 2.

The code above gives you the basic black and white bump map, so now you'd like to put some colour back into it. To do that, just add an extra unit that combines the real colour with the basic shading that is the output from the first stage. To do that, you really don't need to use texture combiners, just the normal texture modes will do - in this case just a modulate to add the colour values in.

TextureAttributes filter_ta = new TextureAttributes();
filter_ta.setTextureMode(TextureAttributes.MODE_MODULATE);

And finally the TextureUnit setup:

Texture2D base_texture = new Texture2D();
base_texture.setImages(Texture.MODE_BASE_LEVEL,
					   Texture.FORMAT_RGB,
					   base_img,
					   1);

Texture2D filter_texture = new Texture2D();
filter_texture.setImages(Texture.MODE_BASE_LEVEL,
						 Texture.FORMAT_RGB,
						 filter_img,
						 1);

TextureUnit[] tu = new TextureUnit[2];
tu[0] = new TextureUnit();
tu[0].setTexture(base_texture);
tu[0].setTextureAttributes(base_ta);

tu[1] = new TextureUnit();
tu[1].setTexture(filter_texture);
tu[1].setTextureAttributes(filter_ta);

Which leads to the final look like this: