The Art of Demomaking - Issue 09 - Static Texture Mapping by (18 October 1999) Return to The Archives
 Introduction
 It's funny how I always struggle to find a good starting sentence ;)Well, this week we will learn more texture mapping, and how the simplest effects that use texturing work. I will also explain how the classic tunnel effect is done. And finally, I will tell you about using main loops in assembler.

 What Is A Texture
 A texture is usually a square bitmap, with a size that is a power of two, most of the time 256x256. You can apply a texture to virtually anything, as long as you can come up with an algorithm that can map a texture onto it. That includes polygons, spheres, planes... The trick obviously is to get it running quick enough to be realtime.A nice characteristic that a texture can have is being seamless. This means that if you repeat your texture the edges will align. This is what you need in most cases. The following figure shows a seamless texture: The u and v coordinates are used to address the texture's texels. They define what we call texture space. If you specify u and v within the range [0,1] then you'll get the corresponding texel in the texture. Anything outside that range will wrap round, and you will get the corresponding texel with modulous arithmetic.

 Static Texture Mapping

This is a very easy technique, used mainly because it is extremely fast. The basic idea is that you precalculate the (u,v) pairs for each pixel, and in realtime you just add a texture displacement (du,dv) which moves the texture. This technique can be used to create tunnels that don't change shape, flowers, kaleidoscopes... It's a lot slower to use this same technique in realtime, so other techniques are usually used.

Basically we load our precalculated texture coordinates, add some time related constants to them, fetch the corresponding texel and store the pixel. In our case texture coordinates are in the range [0..255]. This is how simple the main loop is:

 long doffs = 0, soffs = 0; unsigned char u, v; for (int i=0; i<64000; i++) { u = texcoord[soffs] + du; v = texcoord[soffs+1] + dv; buf[doffs] = texdata[(v<<8)+u]; doffs++; soffs+=2; }

Of course you could combine different layers of textures, and mix them in real time. You'd use a combination of this main loop, and the cross fader we coded in week two.

 Generating Interesting Patterns
 There is absolutely nothing difficult about the main loop. The secret of this effect resides in how good your generated patterns looks. For example, a flower you could calculate u given the distance to the centre, and v given the angle of rotation. You can also add some interesting sine functions to make the pattern look less boring.The effects looks even better if you generate some 3D data, like a tunnel. This takes a bit more time to pre-compute. But this doesn't really matter since it's not in the main loop, and you could always store it as a bitmap.I'll vaguely describe how I did this, but won't get into much detail since it's all about raytracing, and it could get a bit confusing to some of you. You could always look at the source for additional information. The idea is that for each pixel you find the equation of the corresponding ray, and find it's intersection with the tunnel you defined. Given that intersection, you compute u given the angle of rotation, and the v component given the depth. Don't panic if you don't understand the raytracing part, it's not too important. The key here is that you can apply your texture to any shape you like, as long as you can describe it with a mathematical formula.

 Assembler Or Not Assembler
 I've been debating whether to include an explanation on using assembler in main loops or not... There are many arguments that support it, and not quite as many against. But i'll leave it up to you to decide if you want to learn about it or not. Disadvantages A bit tedious, it often takes a lot of simple instructions to make up a full main loop. Also time consuming. Not very safe if you don't know what you are doing. It's easy to crash your computer. Not as easy to maintain code than good old C++. Advantages It's always much quicker than the fastest C code, no matter what people say about the quality of modern compilers. I always get about 80% speed increase, and my assembler is far from perfect. Knowledge of the lowest level of the PC hardware will give you a huge advantage when designing and optimising algorithms. It's extremely satisfying, in my humble opinion. All the instructions are the simplest you can get. I personally recommend using assembler for main loops, if you have the extra time. Once you get enough experience, you'll be able to knock up fully assembler main loops in very little time indeed ;) Of course, you could decide to skip this next part, and you wouldn't suffer from it in the next tutorials. I won't mention it again.

 Using Assembler For Main Loops
Well thanks for reading this section... you won't regret it.

The first thing you need to do is to get the best assembler compiler around, which happens to be free. It's called NASM, and you can get it from here. Simply extract the zip, and put NASM.EXE into your path somewhere: C:\DJGPP\BIN would be good. Read the included text files for more information. NASM can compile INTEL assembler code into virtually any binary format, and on virtually any platform. So all you learn for DJGPP you will be able to use with MSVC also. The generated object files can then easily be linked into your main programs.

The next thing to do is to get a very nice tutorial on Assembler, called The Art Of Assembler. I don't recommend going through it all, it would take you a while, and you wouldn't need all that information to program just a main loop. The best way to learn is to read some source code, and look up anything you don't understand.

So I'll take my own advice and go through the source with you ;) This should give you a nice template you can use when creating your own main loops.

 [BITS 32]

This first part tells NASM we want to use 32 bit code.

 [GLOBAL _Draw_Hole__FPUcUcUc]

This is how we define our procedure, so that we can link it with other programs. The __FPUcUcUc suffix is a standard C++ calling convention, to take into account function overloading. All you need to do is add __F at the end of your function name, to tell the linker it's a function, and then tell it what the parameters are:

 Character Data Type c char s short i int x long long int f float d double v void Prefix Modifier P pointer * R reference & U unsigned

You can of course forget all these extensions, and just define the procedure as an external C function. Another problem we run into while linking is accessing external variables.

 [EXTERN _texdata] [EXTERN _texcoord]

We just define their names here, and make sure we use them correctly in our code.

 [SECTION .data] exampleVar dd 00000000h

That's how you define local variables.

 [SECTION .text]

We tell NASM this is the part that contains the actual code.

 p_buf equ 8 p_du equ 12 p_dv equ 16

We define some constants that point to our parameters on the stack. This makes things easier to understand later.

 _Draw_Hole__FPUcUcUc

We start our main loop.

 push ebp mov ebp, esp

We save the register ebp and use it to address the stack (esp), where our parameters are.

Save all the other registers, so that we can exit the procedure in the same conditions we entered.

 mov edi, dword [ebp+p_buf]

edi now contains the address of the buffer we're drawing into.

 mov esi, dword [_texcoord]

esi now contains the address of the buffer containing our (u,v) coordinates.

 mov ebx, dword [_texdata]

ebx contains a segment aligned address, which means only the higher 16 bits of the address are used. The lower 16 bits address the texels.

 mov dl, byte [ebp+p_du] mov dh, byte [ebp+p_dv]

We load the texture displacement into dx.

 mov ecx, 64000

Setup our loop counter to cover all 64000 pixels.

 .L mov bx, [esi] ; load u and v into bx add bl, dl ; u += du add bh, dh ; v += dv mov al, [ebx] ; load texel mov [edi], al ; store pixel add esi, 2 ; next location in coord buffer inc edi ; inc destination pointer dec ecx ; decrement counter jnz .L ; exit loop if counter is null

That's how simple the main loop is ;) The trick here is the segment aligned buffer in ebx. ebx is 32 bits, and only the higher 16 bits contain the address. The lower 16 bits of ebx (i.e. bx = (bh<<8) + bl ) are used to address the texels, so no extra operation is needed to address the texels.