Copy Link
Add to Bookmark
Report

Texture Mapping Howto

DrWatson's profile picture
Published in 
atari
 · 30 Nov 2023

WARNING: this document contains hazardous information and should be used with great caution! Also the code contained herein is of the c/c++ breed.

Definitions:

(u, v) coordinates: the (x, y) coordinates of a texture

interpolation: (x2 - x1) / (y2 - y1) or (y2 - y1) / (x2 - x1). X and y don't have to be points, they could be colors.

screen space: flat 2d space (3d space projected onto the sceen.

scanline: a horizontal line, joining two opposite edges of a triangle.

Affine Texture Mapping:

Affine texture mapping is the easiest method to map a texture onto a triangle (or any polygon for that matter). I want to focus on texture mapping triangles because they are easier to render and squares don't cut it in 3d. I also want to use floating point numbers so it's easier to understand what's going on. Each vertex of the triangle has a (x, y, z), a (u, v), a (sx, sy, sz), and a (su, sv):

        struct vertex { 
float x, y, z; // (x, y, z) coords. in 3d space
float u, v; // (u, v) texture coordinates
float sx, sy, sz; // (x, y, z) projected into screen space
float su, sv; // (u, v) projected texture coords.
};

Each triangle has a pointer to a texture and of course 3 vertices:

        struct triangle { 
vertex v[3];
unsigned char* tPtr;
};

To render our triangle, we draw it top-down, left-right, interpolating (sx, sy) and (u, v). We won't need sz, su, and sv until perspective texture mapping.

        void DrawAffineTriangle(const triangle& tri) { 

// Affine texture map a triangle
// if there are any bugs in here don't get pissed, just email me :)

int top = 0; // index to top vertex
int a, b; // other 2 vertices

// interpolants

float dx_A, // change in sx with respect to sy
dx_B,
du_A, // change in u with respect to sy
du_B,
dv_A, // change in v with respect to sy
dv_B;

for(int i = 1; i < 3; i++) { // find top vertex

if(tri.v[i].sy < tri.v[top].sy) top = i;

}

a = top + 1;
b = top - 1;
if(a > 2) a = 0;
if(b < 0) b = 2;

int y = int(tri.v[top].sy);
float x1 = tri.v[top].sx;
float x2 = x1;
float u1 = tri.v[top].u;
float u2 = u1;
float v1 = tri.v[top].v;
float v2 = v1;

int height_A = int(tri.v[a].sy - tri.v[top].sy);
int height_B = int(tri.v[b].sy - tri.v[top].sy);

if(height_A) { // avoid divide by zero

// calculate the interpolants

dx_A = (tri.v[a].sx - tri.v[top].sx) / height_A;
du_A = (tri.v[a].u - tri.v[top].u) / height_A;
dv_A = (tri.v[a].v - tri.v[top].v) / height_A;

}

if(height_B) { // avoid divide by zero

// calculate the interpolants

dx_B = (tri.v[b].sx - tri.v[top].sx) / height_B;
du_B = (tri.v[b].u - tri.v[top].u) / height_B;
dv_B = (tri.v[b].v - tri.v[top].v) / height_B;

}

// start drawing

for(int i = 2; i > 0;) {

while(height_A && height_B) {

DrawAffineScanline(x1, x2, u1, u2, v1, v2, y, tri.tPtr);

y++;
height_A--;
height_B--;

x1 += dx_A; // add the interpolants
x2 += dx_B;
u1 += du_A;
u2 += du_B;
v1 += dv_A;
v2 += dv_B;

}

if(!height_A) {

// new edge

int na = a + 1;
if(na > 2) na = 0;

height_A = int(tri.v[na].sy - tri.v[a].sy);

if(height_A) { // avoid divide by zero

// recalculate the interpolants for the new edge

dx_A = (tri.v[na].sx - tri.v[a].sx) / height_A;
du_A = (tri.v[na].u - tri.v[a].u) / height_A;
dv_A = (tri.v[na].v - tri.v[a].v) / height_A;

}

x1 = tri.v[a].sx;
u1 = tri.v[a].u;
v1 = tri.v[a].v;

i--; // one less vertex
a = na;

} // end if

if(!height_B) {

// new edge

int nb = b - 1;
if(nb < 0) nb = 2;

height_B = int(tri.v[nb].sy - tri.v[b].sy);

if(height_B) { // avoid divide by zero

// recalculate the interpolants for the new edge

dx_B = (tri.v[nb].sx - tri.v[b].sx) / height_B;
du_B = (tri.v[nb].u - tri.v[b].u) / height_B;
dv_B = (tri.v[nb].v - tri.v[b].v) / height_B;

}

x2 = tri.v[b].sx;
u2 = tri.v[b].u;
v2 = tri.v[b].v;

i--; // one less vertex
b = nb;

} // end if

} // end for loop

} // end function

This next function is to draw a scanline:

        DrawAffineScanline(float x1, float x2, float u1, float u2, 
float v1, float v2, int y,
unsigned char* tPtr) {

// we are assuming the texture is 64 units wide, it can be any
// size though.

if(x2 < x1) { // swap coordinates so we
// can draw left-to-right

float tmp = x1; x1 = x2; x2 = tmp;
tmp = u1; u1 = u2; u2 = tmp;
tmp = v1; v1 = v2; v2 = tmp;

}

float width = x2 - x1;
float du, dv; // more interpolants
int u, v;

if(width) { // avoid divide by zero

du = (u2 - u1) / width;
dv = (v2 - v1) / width;

}

// draw the scanline
for(int x = int(x1); x < int(x2); x++) {

u = int(u1);
v = int(v1);

screen[y * 320 + x] = tPtr[v * 64 + u];

u1 += du;
v1 += dv;

}

} // end function

That's it, you got yourself an affine texture mapped triangle. Affine Mapping is very fast but it has two flaws, it warps when using triangles and doesn't exactly look right with large triangles at sharp angles. It doesn't warp if you use squares, but you can't use squares very well to make complex 3d objects. To fix this problem, you have to take the z axis into account when texture mapping. Notice in affine mapping sz, su, and sv wasn't used. Now we are going to use them in Perspective Correct Texture Mapping.

Perspective Correct Texture Mapping:

Perspective Correct Texture Mapping is how the world really looks, none of this fake texture mapping. The problem is that it is pretty slow relative to affine mapping. Fortunately, it can be speeded up a lot. Ok, when we project 3d space onto screen space, we divide (x, y) and (u, v) by z:

        sx = x / z; 
sy = y / z;
su = u / z;
sv = v / z;

We get these new coordinates (sx, sy) and (su, sv). Notice (su, sv), before we didn't project the (u, v) coordinates into screen space. We also need to project the z coordinate into screen space to give us

        sz = 1 / z

Now since (sx, sy, sz) and (su, sv) are in screen space they are linear! We can just interpolate these values along the edges and across scanlines. But we need the original (u, v) coordinates, not the (su, sv) coordinates. That's were interpolating sz comes in. To get (u, v), we divide su by sz and sv by sz:

        u = (su / sz) = (u / z) / (1 / z) 
v = (sv / sz) = (v / z) / (1 / z)

The DrawPerspective function is nearly the same as the DrawAffineTriangle function except you need to interpolate (su, sv) and sz instead of just (u, v):

        void DrawPerspectiveTriangle(const triangle& tri) { 

// Perspective texture map a triangle

int top = 0; // index to top vertex
int a, b; // other 2 vertices

// interpolants

float dx_A, // change in sx with respect to sy
dx_B,
du_A, // change in su with respect to sy
du_B,
dv_A, // change in sv with respect to sy
dv_B,
dz_A, // change in sz with respect to sy
dz_B;

for(int i = 1; i < 3; i++) { // find top vertex

if(tri.v[i].sy < tri.v[top].sy) top = i;

}

a = top + 1;
b = top - 1;
if(a > 2) a = 0;
if(b < 0) b = 2;

int y = int(tri.v[top].sy); // all of the interpolants
float x1 = tri.v[top].sx; // start at the top vertex
float x2 = x1;
float u1 = tri.v[top].su;
float u2 = u1;
float v1 = tri.v[top].sv;
float v2 = v1;
float z1 = tri.v[top].sz;
float z2 = z1;

int height_A = int(tri.v[a].sy - tri.v[top].sy);
int height_B = int(tri.v[b].sy - tri.v[top].sy);

if(height_A) { // avoid divide by zero

// calculate the interpolants

dx_A = (tri.v[a].sx - tri.v[top].sx) / height_A;
du_A = (tri.v[a].su - tri.v[top].su) / height_A;
dv_A = (tri.v[a].sv - tri.v[top].sv) / height_A;
dz_A = (tri.v[a].sz - tri.v[top].sz) / height_A;

}

if(height_B) { // avoid divide by zero

// calculate the interpolants

dx_B = (tri.v[b].sx - tri.v[top].sx) / height_B;
du_B = (tri.v[b].su - tri.v[top].su) / height_B;
dv_B = (tri.v[b].sv - tri.v[top].sv) / height_B;
dz_B = (tri.v[b].sz - tri.v[top].sz) / height_B;

}

// start drawing

for(int i = 2; i > 0;) {

while(height_A && height_B) {

DrawPerspectiveScanline(x1, x2, u1, u2, v1, v2, z1, z2,
y, tri.tPtr);

y++;
height_A--;
height_B--;
x1 += dx_A; // add the interpolants
x2 += dx_B;
u1 += du_A;
u2 += du_B;
v1 += dv_A;
v2 += dv_B;
z1 += dz_A;
z2 += dz_B;

}

if(!height_A) {

// new edge

int na = a + 1; // next vertex
if(na > 2) na = 0;
height_A = int(tri.v[na].sy - tri.v[a].sy);

if(height_A) { // avoid divide by zero

// recalculate the interpolants for the new edge

dx_A = (tri.v[na].sx - tri.v[a].sx) / height_A;
du_A = (tri.v[na].su - tri.v[a].su) / height_A;
dv_A = (tri.v[na].sv - tri.v[a].sv) / height_A;
dz_A = (tri.v[na].sz - tri.v[a].sz) / height_A;

}

x1 = tri.v[a].sx;
u1 = tri.v[a].su;
v1 = tri.v[a].sv;
z1 = tri.v[a].sz;

i--; // one less vertex
a = na;

} // end if

if(!height_B) {

// new edge

int nb = b - 1; // next vertex
if(nb < 0) nb = 2;

height_B = int(tri.v[nb].sy - tri.v[b].sy);

if(height_B) { // avoid divide by zero

// recalculate the interpolants for the new edge

dx_B = (tri.v[nb].sx - tri.v[b].sx) / height_B;
du_B = (tri.v[nb].su - tri.v[b].su) / height_B;
dv_B = (tri.v[nb].sv - tri.v[b].sv) / height_B;
dz_B = (tri.v[nb].sz - tri.v[b].sz) / height_B;

}

x2 = tri.v[b].sx;
u2 = tri.v[b].su;
v2 = tri.v[b].sv;
z2 = tri.v[b].sz;

i--; // one less vertex
b = nb;

} // end if

} // end for loop

} // end function

And here is the perspective scanline function:

        DrawPerspectiveScanline(float x1, float x2, float u1, float u2, 
float v1, float v2, float z1, float z2,
int y, unsigned char* tPtr) {

// remember that x1, x2, u1, u2, v1, v2, z1, and z2 are projected
// coordinates.

// we are assuming the texture is 64 units wide, it can be any
// size though.

if(x2 < x1) { // swap coordinates so we
// can draw left-to-right

float tmp = x1; x1 = x2; x2 = tmp;
tmp = u1; u1 = u2; u2 = tmp;
tmp = v1; v1 = v2; v2 = tmp;
tmp = z1; z1 = z2; z2 = tmp;

}

float width = x2 - x1;
float du, dv, dz; // more interpolants
int u, v;

if(width) { // avoid divide by zero

du = (u2 - u1) / width;
dv = (v2 - v1) / width;
dz = (z2 - z1) / width;

}

// draw the scanline
for(int x = int(x1); x < int(x2); x++) {

u = int(u1 / z1); // u = (u / z) / (1 / z)
v = int(v1 / z1); // v = (v / z) / (1 / z)

screen[y * 320 + x] = tPtr[v * 64 + u];

u1 += du;
v1 += dv;
z1 += dz;

}

} // end function

Now you have a perspective correct texture mapped triangle!

Afterthoughts

The code above is to get the basic idea across, and is not a good way to rasterize polygons. It will produce "grainy" artifacts along with wobbly textures. Actually you can notice some of these things in a lot of current game engines, but I really suggest getting Chris Hecker's articles in Game Developer magazine to see what goes into a perfect rasterizer. It is a five article series in all:

  • April/May 1995 Perspective Texture Mapping Part I: Foundations
  • June/July 1995 Part II: Rasterization
  • August/Sept 1995 Part III: Endpoints and Mapping
  • Dec 1995/Jan 1996 Part IV: Approximations
  • April/Map 1996 Part V: It's About Time

It's around $8 an issue, but I think it's worth it and you can order back issues from Game Developer. It is the best information I found on texture mapping by far.

Other references and information:

  • Pcgpe (Pc game programming encyclopedia) has a document on texture mapping. I thought it was hard to understand and had some bugs in it.
  • Milo's Home Page A pretty cool page on 3d programming, check it out.

I want to thank Mark Feldman for pointing out some bugs in my code. He has also written a new document on texture mapping for Win95GPE (which isn't expected to be out until early '97). Fortunately, you can check it out HERE . Go to the Win95GPE Home Page and you'll find it. It discusses many different methods of texture mapping.

I would like to hear some feed back so why don't you email me?

Last modified or fixed: 2/2/97, 4:06pm

← previous
next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT