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.

            pushad

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.

            popad
            pop ebp
            ret

Restore all the registers and return.

Final words

Now you understand the basis of texture mapping works, there's not much you don't know about creating effects. All there is left is to cover are slightly more elaborate techniques for texture mapping, which we should cover some time soon. Assembler used to be a standard for most demos in the past, but most people nowadays don't really care anymore. I still strongly recommend that you learn about it, since all the knowledge you will gain of low level hardware will be extremely useful. I'd also like to point out the fact that the demo runs 80% quicker with the assembler loop, and there's a lot of room for improvement, like writing 4 pixels at once.
Oh yeah, I promised you something on profiling last week. Don't worry, I haven't forgotten, I've just delayed it until next week. Next time we will also learn about roto zoomers, and more about visual performance testing.
Feel free to download this week's example with source code right here (141k)
Enjoy,
Alex

Article Series:

> - The Art of Demomaking - Issue 01 - Prologue
> - The Art of Demomaking - Issue 02 - Introduction To Computer Graphics
> - The Art of Demomaking - Issue 03 - Timer Related Issues
> - The Art of Demomaking - Issue 04 - Per Pixel Control
> - The Art of Demomaking - Issue 05 - Filters
> - The Art of Demomaking - Issue 06 - Bitmap Distortion
> - The Art of Demomaking - Issue 07 - Bump Mapping
> - The Art of Demomaking - Issue 08 - Fractal Zooming
> - The Art of Demomaking - Issue 09 - Static Texture Mapping
> - The Art of Demomaking - Issue 10 - Roto-Zooming
> - The Art of Demomaking - Issue 11 - Particle Systems
> - The Art of Demomaking - Issue 12 - Span Based Rendering
> - The Art of Demomaking - Issue 13 - Polygon Engines
> - The Art of Demomaking - Issue 14 - Perspective Correct Texture Mapping
> - The Art of Demomaking - Issue 15 - Music And Synchronization
> - The Art of Demomaking - Issue 16 - The Final Product

Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
Please read our Terms, Conditions, and Privacy information.