Controlling Your Textures
by Bob Crispen <vrmlworks@crispen.org>
Way back in my relative youth, I used to lay textures on objects and then curse at them because they turned out upside down or backwards. For a while I didn't even use textures. But one day I got mad and decided to learn how to take control of those darn textures.
I decided to write down what I was doing in a tutorial because of one of the WeekEnd Themes (WETs) that they post every Friday afternoon on alt.binaries.comp-graphics. The idea behind a WET is to take the original image and do whatever you want with it. I decided to take this image and turn it into a VRML world. In the process, I discovered I'd used textures just about every way you could use them, so all I had to do was take that world and write a few words and I'd have a tutorial. It wasn't quite that easy, but thanks to the people who'd gone before me down this path, it was nearly that easy.
The first step on your way to mastering textures should be the wonderful texture tutorial by Cindy Ballreich, which is so good and so thorough that this tutorial can be a few odd notes that emphasize different things than Cindy did. If the going gets a little thick anywhere in this tutorial, just jump back and review Cindy's tutorial.
You can download this webpage, all the figures, and all the examples (.zip | .tar.gz) in this tutorial and work off line, and you'll save the trouble of downloading
individual files, or you can simply continue on and click on the links as you see a subject that interests you.
Texturing a wall with part of an image
First, let's take a simpler version of the WET. We could take the whole store, but that's too much work for a tutorial. Here's our simplified WET image that we want to use as a texture (I reduced most of the other images, but left this one pretty big -- sorry about the download time):
Figure 1: The texture we want to put on our wall.
Just so we don't make this too easy, let's get rid of that ugly door (we'll replace it shortly), but let's keep the walls, the doorframe, and the little step at the bottom of the door.
Ready to start? The first thing you need to figure out is how many points you need. Save the image above to your machine, print it out (or draw a sketch), and figure out how many points you'll need to cover the image (except the door) with a surface, and without overlapping polygons. Don't read any farther until you've done this.
I believe you need 12 points, which I've numbered from 0 to 11 in Figure 2:
Figure 2: The points we need to construct the wall.
This is by no means the only way to set out these points. For instance, points 6 and 9 could be below points 7 and 8 respectively. And of course, the numbers we give individual points don't matter. I'm not convinced my answer has the minimum number of points, but that doesn't matter either: there are enough points there to do the job. Keep the points you came up with (add to them if you see you forgot some). You'll use them in a minute.
The next thing you need to do is find the coordinates for the IndexedFaceSet you're going to build. You can do this any way you like, but what I did for this tutorial was arbitrarily decide that the image would be 4 meters tall and 2 meters wide, and that the center of the wall will be in the middle of the bottom. I also decided that we wouldn't worry about thickness for this example, so we can set all our Z values to 0. I used a 2D bitmap graphics program to measure the X and Y values for the points we decided we'd need.
Things were a little different with the WET. I used an existing storefront I had in my model library and then went in and moved the vertices on the windows and door so they'd match up with the texture.
Whether you start from an existing object or make an object from scratch, the technique is the same. After you figure out what points you need, you need to figure out their X, Y, and Z values.
So go do that. Figure out the geometry coordinates for the points you came up with. There's only 12 of them, so it isn't that big a job.
Here's some approximate X and Y values I came up with (and we already said the Z values would all be zero):

Figure 3: X and Y geometry coordinatesfor the wall
We may end up tweaking these values later on -- I don't know if you noticed, but the ratio of width to height for the original image is not exactly 1:2. Mostly you can get away with changing ratios, and even with stretching parts of the texture and squashing other parts of the texture. Just watch for lines appearing in the texture or parts of the image not lining up.
So it looks like we're ready to do part of our IndexedFaceSet already:
IndexedFaceSet {
coord Coordinate {
point [
-1 4 0, 1 4 0,
-1 2.2 0, -.41 2.2 0, .44 2.2 0, 1 2.2 0,
-1 .11 0, 0.41 .11 0, .44 .11 0, 1 .11 0,
-1 0 0, 1 0 0
]
}
coordIndex [
0 2 5 1 -1
2 6 7 3 -1
4 8 9 5 -1
6 10 11 9 -1
]
You can do the coordIndex in your head from looking at the coordinate numbers in Figure 2 above. Go ahead and do this with your points, and save them in a text file. We'll be coming back to this.
Now it's time to figure out the texture coordinates. Unlike the geometry coordinates for the IndexedFaceSet, which can be any value, texture coordinates go from 0 to 1 on the s and t axes (which, in the case of an upright object like the one we're doing, correspond to the x and y axes -- see Cindy Ballreich's tutorial for an explanation of the s and t axes, along with u, v, and w), with the origin in the lower left-hand corner. So go ahead and measure the texture coordinates for your set of points.
Here's the texture coordinates I came up with:

Figure 4: Texture coordinates for the wall
Let me say a couple of more words about the process I used to get these values. I loaded the image into Paint Shop Pro on my PC and resized it to 1000 by 1000 pixels. Then I flipped the image, because Paint Shop Pro puts the origin in the top left-hand corner. I moved the cursor over the points on the image, wrote down what I found, and divided by 1000. Did you do it an easier way or a harder way? What tools to you have on your machine that will make this job easier? Hint: most Unix systems have a program called xv.
Note that there are nearly twice as many pixels between 0 and 1 on the t axis as there are on the s axis. That's fine. The important thing is that, no matter what the shape of your image, the bottom left-hand corner is at (0,0) and the top right-hand corner is at (1,1).
Now all we have to do is make each point on the texture correspond with the equivalent point on the geometry. The easiest way to do that is to write down the texture coordinates the same way we wrote down the geometry coordinates. So, following the order in Figure 2, we set them down as follows:
IndexedFaceSet {
coord Coordinate {
point [
-1 4 0, 1 4 0,
-1 2.2 0, -.42 2.2 0, .44 2.2 0, 1 2.2 0,
-1 .11 0, -.42 .11 0, .44 .11 0, 1 .11 0,
-1 0 0, 1 0 0
]
}
coordIndex [
0 2 5 1 -1
2 6 7 3 -1
4 8 9 5 -1
6 10 11 9 -1
]
texCoord TextureCoordinate {
point [
0 1, 1 1,
0 .552, .288 .552, .72 .552, 1 .552,
0 .028, .288 .028, .72 .028, 1 .028,
0 0, 1 0
]
}
}
Go ahead and do this for your texture coordinates in the file you started. Make sure that the texture coordinates for point 0 are first in your list, then the coordinates for point 1, and so on, so that the points in the Coordinate match the points in the TextureCoordinate node one for one.
We could make a texCoordIndex field, but the spec says, "If the texCoordIndex field is empty, then the coordIndex array is used to choose texture coordinates from the TextureCoordinate node." So let's let the coordIndex serve both purposes.
Note that, instead of writing down the texture coordinates in the same order as the geometry coordinates the way we did, you could write down the points in the TextureCoordinate node in any order, in which case you'll need a texCoordIndex field to map the texture coordinates to the geometry coordinates.
Here's what it looks like (with some fancy lights and background). Go ahead and download this file and replace the IndexedFaceSet that's there with your IndexedFaceSet. Now look around the doorframe. Does the bottom of the doorframe match up with the rest of the doorframe? If it doesn't, play with the 3rd, 4th, 7th and 8th coordinates in the TextureCoordinate node, depending on where it seems to be out of line (see Figure 2 above).
Now, was all this really necessary? Let's find out. Comment out the texCoord field, and you should get something like this.
So if looks like it was worth the trouble.
Adding a door
Now let's add a door. I'm just going to copy the X and Y coordinates numbered 3, 4, 7 and 8 in Figure
2 above for the edges, give it a thickness (so we'll have to duplicate our coordinates so that half of them have
a Z of zero and half a Z of some negative number), put a window in it, and figure out what all the surfaces are.
We'll also add a back wall and some sides, just to clean things up. Here's the result,
and it really isn't worth talking about very much, except to say that when we applied a texture to the door without
using a TextureCoordinate, it didn't look that bad. Don't worry, we'll be back to this. There really
was a reason for it.
Putting a house number on the door
Remember how there was a house number (505) on the original door? Well, we don't want the letter carrier to miss our house now that it has a new door. We'll make a square IndexedFaceSet and put a transparent PNG on there for the numbers. And here's the result. Oh dear, we've certainly seen that before.
Go ahead and download the wood texture file, the "505" texture file, and the VRML file so you can work with it.
Let's take a look at the door the way we want it to be:
Figure 5: Geometry coordinates and texture coordinatesfor the number
The green numbers in figure 5 are the coordinate numbers of the IndexedFaceSet. The yellow numbers are the s and t values of the image the way we want it. So what we need is a TextureCoordinate node to map one to the other, like we have in this world.
So that simple line:
texCoord TextureCoordinate { point [ 0 0, 1 0, 1 1, 0 1 ] }
means that you're in control of the image.
Add that line to your file and it should turn the image the right way.
Back to the door
Wouldn't the door look a little nicer if the wood grain ran vertically instead of horizontally? Why does it show up horizontally?
The answer is in the spec: "If the texCoord field is NULL, a default texture coordinate mapping is calculated using the local coordinate system bounding box of the shape. The longest dimension of the bounding box defines the S coordinates, and the next longest defines the T coordinates."
That means that if the Y dimension of the object (or, more properly, its bounding box) is longer than the X dimension, then if you don't have a TextureCoordinate node, the image is going to end up 90 degrees from the way it appears in your 2D bitmap graphics package. That's the object, not the texture image.
So the easy way to solve this problem is simply to rotate the image 90 degrees, like we do in this world. You can do it yourself with the VRML file you downloaded. Take the wood texture image into your favorite 2D bitmap editor and rotate the image 90 degrees.
You could, I suppose, make a TextureCoordinate for your wood paneling if you absolutely had to, but that sounds to me like too much work for too little payoff.
You might want to read what the spec says about the TextureTransform node, though. By setting the scale
field of that node (which is a subnode of the Appearance node), you can control the number of repetitions of the
wood pattern across the surface. I'm not going to devote any time to it in this tutorial, since the spec
is pretty clear on it, and it's covered in most books. Play with it yourself, now that you've got your door,
and see what it does.
What good is the texCoordIndex?
We said earlier that if you had your TextureCoordinate points correspond one for one with your Coordinate points, you didn't need to have a texCoordIndex. So why have that field in the spec at all?
Here's an example where nothing else will do (I think I went a little overboard reducing the image size, sorry):
Figure 6: An ingot-shaped VRML button (vertices numbered)
This is two nodes. The first is an IndexedFaceSet that includes all the faces except the one with the texture on it that says "VRML", and the second node is the one face with the texture on it.
In order to make it easy on ourselves as we tweak the vertices to make it look nice, we USE the coordinates from the first node in the second node. We could have copied the relevant coordinates (4, 5, 6, and 7 in the image above) to the second node, renumbered them from 0 to 3, and avoided the texCoordIndex, but every time you changed one of the vertices in that surface, you'd have to change two nodes instead of one. Here's the code for the second node that lets you USE the coordinates from another node and still map a texture to the surface:
Shape {
appearance Appearance {
texture ImageTexture {
url "vrml.jpg"
}
}
geometry IndexedFaceSet {
coord USE ButtonCoordinates
coordIndex [ 5 6 7 4 -1 ]
texCoord TextureCoordinate {
point [ 0 0, 1 0, 1 1, 0 1 ]
}
texCoordIndex [ 0 1 2 3 -1 ]
}
}
By looking at the vertex numbers in Figure 6, you can see what's going on in the code above: vertex number 5 corresponds to texture coordinate 0,0 (remember the s and t axes in Figure 4), vertex number 6 corresponds to texture coordinate 1,0 and so on. So the ordering of the texture coordinates is 0, 1, 2, and 3, and that's the value we put into the texCoordIndex.
Download this world and this texture and put them in the same directory. Then comment out:
I'm not going to speculate on what happens when you leave out both the texCoord and texCoordIndex. I'm sure it's in the spec, but I don't need to know it. I just know that when I have only a subset of the possible coordinates in my coordIndex field, I need a texCoord and a texCoordIndex if I'm going to expect texture coordinates to work properly.
It's the second case, where you leave off the texCoordIndex that's really interesting. Regardless of what
your VRML browser does, this is erroneous VRML. Your browser is supposed to use the coordIndex to fetch values
out of your TextureCoordinate, but the TextureCoordinate only has 4 points. How's it going to get the sixth
point (index 5), the
seventh one (index 6) and so on?
As long as you've got that world in your text editor, play around with the order of the vertices in the coordIndex
field and see if you can figure out the changes you need to make in the TextureCoordinate and the texCoordIndex.
If you leave the TextureCoordinate alone, can you always find an order of indices in the texCoordIndex field that
will make the texture come out right? Hint: 4! (four factorial) is only 24. You could make and display
24 objects, each with a different permutation of the texCoordIndex, without very much work.
Multiple textures from one image file
Here's a cute trick. Take a look at this image:
Figure 7: An image we'll use to texture two objects
Suppose we wanted to use the top half of this image to texture one button and the bottom half to texture another button, as in this example.
Here's the code that makes it work, for the first node:
texCoord TextureCoordinate {
point [ 0 0.5, 1 0.5, 1 1, 0 1 ]
}
and for the second node (below it):
texCoord TextureCoordinate {
point [ 0 0, 1 0, 1 0.5, 0 0.5 ]
}
The first node has a minimum t-axis value (the vertical axis) of 0.5 and the second node has a maximum t-axis value of 0.5, so you can use one image file to texture two or more nodes.
If you have a lot of little bitty textures, it might help the download time for your world to put them all into
one file. Often the net isn't short on bandwidth as much as it's short on connections, and you need one connection
(at least) for every file you transfer.
Powers of two
Here's something that isn't mentioned anywhere in the spec, but it helps in the real world. You may end up with better looking textures if you make all your images an integer power of two high and an integer power of two wide. The reason behind it has to do with the inner workings of the more popular rendering libraries, and since I don't know anything about that, I'll simply say that it often works out that a 128 x 64 texture looks lots better than a 127 x 63 texture or a 129 x 65 texture.
In fact, if you've got an image of 100 x 50, you may end up happier with the look of it if you use a 2D bitmap graphics editor to stretch it to 128 x 64 or shrink it to 64 x 32. That's because your 2D bitmap graphics program may have a better algorithm for resizing/resampling images than the algorithm in the rendering library that sits underneath your VRML browser (which has to work in real time).
So if your image textures in your VRML world start looking ragged, you may want to crop them or resize them
and see if it makes a difference. If your browser lets you choose between rendering libraries (e.g.,
OpenGL and DirectX), try it both ways.
Final thoughts
JPEG Files. JPEG compression is what's called "lossy" compression: all the information in the image doesn't get saved in the file. What's more, even the lowest level of compression is lossy.
What that means is that, even if you use the lowest level of image compression (best appearance, largest file size), if you load and save a file in JPEG format over and over again, your image can and probably will degrade. Save to some other file format as you're doing your work (some 2D bitmap graphics programs such as Paint Shop Pro have their own format), and only save to JPEG format at the very end.
GIFs and PNGs. The GIF format does have lossless compression, but it has a limited pallette and the VRML spec doesn't require browsers to support it. It's therefore possible that a conformant VRML browser could be built that didn't support GIF images. PNGs have so many superiorities (small file size, as many pallette choices as JPEGs, lossless compression, 8-bit transparency values) they're clearly the graphics format of the future. Trouble is, we don't live in the future. In the real world, some browser support GIFs but not PNGs.
My advice: since PNGs are required by the spec, since most reasonably good 2D bitmap graphics programs can save a PNG file, and since PNGs are often as small or smaller than the equivalent GIFs, I recommend not using a GIF file unless you back it up with a PNG. E.g.,
texture ImageTexture {
url [ "foo.gif", "foo.png" ]
}
and (for a while yet) not using a PNG file unless you back it up with a GIF or JPEG.
Image Distortion. It may sometimes be easier to use TextureCoordinates to distort an image than to distort the image in a 2D bitmap graphics program.
Animated Textures. The point field in a TextureCoordinate node is an exposedField, as are all the
fields in a TextureTransform node, and there is an eventIn set_texCoordIndex in the IndexedFaceSet node.
You can use a Script to modify these values dynamically and do weird and wonderful things with your textures.