// ----------------------------------------------------------------------------
// Tmap.cpp - Texture mapper example source, focusing on precision
//
// Copyright 1999, Paul D. Nettle. All rights reserved.
//
// ----------------------------------------------------------------------------
//
// NOTES ABOUT THE ROUTINES BELOW:
//
// This file can be compiled in Watcom, using the following command-line:
//
// wcl386 main.cpp /l#dos4g /en /fpi /fp5 /5r /w4 /zq /zp1 /oneatx /s
//
//
// When drawing, there are four polygons plotted (in perspective) that combine
// to create a single continuous surface. This is done to test for overlaps
// in adjacent polygons.
//
// Each polygon routine acheives results as accurate as the algorithm will
// allow. See the comments above each routine for details of accuracy issues.
//
// Each polygon routine performs no wrapping (and the wrapping error should be
// visible if there is overflow error.) They also ADD each pixel to the screen,
// rather than simply plotting them, to show any overlapping of adjacent
// polygons.
//
// Each routine is hard-coded for a 64x64 texture. If you change the texture
// size defines (i.e. TEX_X & TEX_Y) you need to change the inner-loops of the
// texture mappers.
//
// Vertices must be in clock-wise order.
//
// The routines SHOULD not crash with concave polygons, but may only render
// a portion of the given polygon (and incorrectly at that.)
//
// Any other notes about a specific algorithm are given above each polgyon
// routine.
//
// ----------------------------------------------------------------------------
//
// OTHER NOTES:
//
// The routines may not properly handle polygons that have three vertices in
// a straight line across the top of the polygon (causing it to bail too early)
//
// The routines SHOULD not crash with concave polygons, but may only render
// a portion of the given polygon (and incorrectly at that.)
//
// ----------------------------------------------------------------------------
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#include <mem.h>
#include <math.h>
// ----------------------------------------------------------------------------
// THESE FLAGS CONTROL WHAT KIND OF TEXTURE MAPPER TO USE (PICK ONLY ONE!)
// ----------------------------------------------------------------------------
//#define USE_AFFINE
//#define USE_EXACT_PERSPECTIVE
#define USE_SUB_AFFINE_PERSPECTIVE
// ----------------------------------------------------------------------------
// The vertex structure. Note that this uses a linked list. I tend to prefer
// them for ease of managing polygons with large numbers of dynamic vertices,
// though lists will work fine, too.
// ----------------------------------------------------------------------------
typedef struct vertex
{
float u, v, w;
float x, y, z;
int iy;
struct vertex *next;
} sVERT;
// ----------------------------------------------------------------------------
// The edge structure. This is used to keep track of each left & right edge
// during scan conversion. The algorithm does not pre-build an edge list
// prior to rendering, rather it renders edges as it builds them. This
// structure is used only to keep the variables together.
// ----------------------------------------------------------------------------
typedef struct edge
{
float u, du;
float v, dv;
float w, dw;
float x, dx;
int height;
} sEDGE;
// ----------------------------------------------------------------------------
// Simple routine to get the Pentium(tm) high-resolution timer.
// ----------------------------------------------------------------------------
void getTicks(const unsigned int *hi, const unsigned int *lo);
#pragma aux getTicks = \
"rdtsc" \
"mov [esi],edx" \
"mov [edi],eax" \
parm caller [esi] [edi] \
modify nomemory exact [eax edx esi edi];
// ----------------------------------------------------------------------------
// Set the given VGA mode (the polygon routines expect 0x13)
// ----------------------------------------------------------------------------
void setMode(const unsigned int mode);
#pragma aux setMode = \
"int 0x10" \
parm caller [eax] \
modify nomemory exact [eax];
// ----------------------------------------------------------------------------
// Constants
// ----------------------------------------------------------------------------
enum {RES_X = 320}; // Screen resolution
enum {RES_Y = 200}; //
enum {TEX_X = 64}; // Texture resolution
enum {TEX_Y = 64}; //
enum {POLY_COUNT = 10000}; // Number of polygons to draw
enum {SUB_SHIFT = 4}; // Sub-affine span size
enum {SUB_SPAN = 1 << SUB_SHIFT}; //
// ----------------------------------------------------------------------------
// This is handy
// ----------------------------------------------------------------------------
#define MIN(a, b) ((a) < (b) ? (a) : (b))
// ----------------------------------------------------------------------------
// Buffers. This is where we store the texture and the buffer we render into.
// ----------------------------------------------------------------------------
static char textureBuffer[TEX_X * TEX_Y];
static char backBuffer[RES_X * RES_Y];
static char *frameBuffer = (char *) 0xA0000;
// ----------------------------------------------------------------------------
// Draws a checkerboard texture into textureBuffer.
// ----------------------------------------------------------------------------
void drawTexture()
{
// Frequency: the lower the number, the higher the frequency
const int freq = 2;
const int fAnd = 1 << freq;
for (int y = 0; y < TEX_Y; y++)
{
int yIndex = y * TEX_X;
for (int x = 0; x < TEX_X; x++)
{
textureBuffer[yIndex+x] = (y&fAnd) == (x&fAnd) ? 1:15;
}
}
}
// ----------------------------------------------------------------------------
// Calculate the deltas along an edge. This routine is called once per edge
// per polygon. Notice how the affine does not require the calculation of the
// homogenous coordinate (w).
// ----------------------------------------------------------------------------
inline calcEdgeDeltas(sEDGE &edge, sVERT *top, sVERT *bot)
{
// Edge deltas
float overHeight = 1.0 / (bot->y - top->y);
edge.du = (bot->u - top->u) * overHeight;
edge.dv = (bot->v - top->v) * overHeight;
#ifndef USE_AFFINE
edge.dw = (bot->w - top->w) * overHeight;
#endif
edge.dx = (bot->x - top->x) * overHeight;
// Screen pixel Adjustments (some call this "sub-pixel accuracy")
float subPix = (float) top->iy - top->y;
edge.u = top->u + edge.du * subPix;
edge.v = top->v + edge.dv * subPix;
#ifndef USE_AFFINE
edge.w = top->w + edge.dw * subPix;
#endif
edge.x = top->x + edge.dx * subPix;
}
// ----------------------------------------------------------------------------
// Draw an affine texture-mapped polygon.
//
// With a simple affine texture mapper (and without the use of sub-texel
// accuracy) the final pixel on each scanline of the polygon references the
// texel along that edge. If the polygon uses the entire texture, then that
// last pixel will be out of bounds of the texture. For example, a 4-sided
// polygon might reference these UV values:
//
// [0,0] [1,0] [0,0] [64,0]
// +-----+ +-----+
// | | | |
// | | given a 64x64 texture: | |
// | | | |
// | | | |
// +-----+ +-----+
// [0,1] [1,1] [0,64] [64,64]
//
// Note that the edges on the right reference the texel that is just beyond the
// range of the texture map (0-63 does not include 64). This is safe, since
// these polygons are rendered top/left, which means that the far right edge
// and the last scanline of the polygon is not drawn (to avoid overlapping of
// adjacent polygons.)
//
// Normal affine texture mapping has only one form of accuracy loss which can
// not be avoided. The texture mapper interpolates (i.e. adds a delta to each
// U/V value per pixel) which accumulates error, since the deltas calculated
// are only stored at the resolution that the floating point unit will provide.
// Very few numbers can be represented exactly in IEEE floating point, so the
// closest representative is stored instead. This inaccuracy is accumulated
// as values are interpolated from pixel to pixel, accumulating the error in
// the deltas. Add this to the error of the original U/V value where the
// interpolation began (also due to the inability to store an exact value) and
// the error is still small, but not negligable.
//
// This can be reduced by using higher precision floating point values (i.e.
// using doubles rather than floats.) But this sill only reduces the problem,
// and does not solve the problem entirely.
//
// This problem can manifest itself in a few ways. First, it can cause
// inaccurate texel selections, and cause slight jitters in the texture. It
// can cause overflows in the texture (i.e. the right edge of the span may
// reference a texel beyond the range of the texture). It may also cause
// slight inaccuracies along each edge of the polygon, choosing to render to
// the wrong pixel.
//
// These problems are very rare, indeed, and may never be visually noticed.
// Especially the last error (choosing the wrong pixel along the edges of the
// polygon) since the adjacent polygons will most likely make the same wrong
// choice, if the two adjacent polygons share their edge vertices.
//
// There is no cure for this inaccuracy, given the algorithms used. However,
// there is an acceptable work-around. Simply choosing UV values that are just
// INSIDE the bounds of the texture can solve this problem. Also, allowing
// wrapping textures can also solve this problem, provided the overflow wraps
// to a texel that "looks right."
// ----------------------------------------------------------------------------
void drawAffineTexturedPolygon(sVERT *verts)
{
// Find the top-most vertex
sVERT *v = verts, *lastVert = verts, *lTop = verts, *rTop;
while(v)
{
if (v->y < lTop->y) lTop = v;
lastVert = v;
v->iy = ceil(v->y);
v = v->next;
}
rTop = lTop;
// Top scanline of the polygon in the frame buffer
char *fb = &backBuffer[lTop->iy * RES_X];
// Left & Right edges (primed with 0 to force edge calcs first-time through)
sEDGE le, re;
le.height = 0;
re.height = 0;
// Render the polygon
while(1)
{
if (!le.height)
{
sVERT *lBot = lTop - 1; if (lBot < verts) lBot = lastVert;
le.height = lBot->iy - lTop->iy;
if (le.height < 0) return;
calcEdgeDeltas(le, lTop, lBot);
lTop = lBot;
}
if (!re.height)
{
sVERT *rBot = rTop + 1; if (rBot > lastVert) rBot = verts;
re.height = rBot->iy - rTop->iy;
if (re.height < 0) return;
calcEdgeDeltas(re, rTop, rBot);
rTop = rBot;
}
// Polygon must have height
if (!le.height && !re.height) return;
// Get the height
int height = MIN(le.height, re.height);
// Subtract the height from each edge
le.height -= height;
re.height -= height;
// Render the current trapezoid defined by left & right edges
while(height-- > 0)
{
// Texture coordinates
float overWidth = 1.0 / (re.x - le.x);
float du = (re.u - le.u) * overWidth;
float dv = (re.v - le.v) * overWidth;
int idu = int(du * 65536.0);
int idv = int(dv * 65536.0);
// Find the end-points
int start = (int) ceil(le.x);
int end = (int) ceil(re.x);
// Texture adjustment (some call this "sub-texel accuracy")
float subTex = (float) start - le.x;
int iu = int((le.u + du * subTex) * 65536.0);
int iv = int((le.v + dv * subTex) * 65536.0);
// Fill the entire span
char *span = fb + start;
for (; start < end; start++)
{
*(span++) += textureBuffer[((iv>>10)&0xffffffC0) + (iu>>16)];
iu += idu;
iv += idv;
}
// Step
le.u += le.du;
le.v += le.dv;
le.x += le.dx;
re.u += re.du;
re.v += re.dv;
re.x += re.dx;
fb += RES_X;
}
}
}
// ----------------------------------------------------------------------------
// Draw a perspective-correct texture-mapped polygon. The following routine
// performs perspective correction on ALL pixels. This produces a much slower
// routine, but at the same time, much more accurate results.
//
// Given the inaccuracies I've already explained for the affine texture mapper,
// this routine suffers from one more accumulation of error. The fact that
// the values interpolated are not their original values, rather they are
// divided by Z.
//
// Interpolating these u/z and v/z values accumulates the error in an amplified
// form, so that when the values are then divided by W for each pixel, the
// amplified error from accumulation is added to the accuracy lost from the
// two divisions (first division by Z, then the division by W).
//
// This error can manifest itself in the same ways that the affine version can,
// with the exception that the error produced by the following routine is
// amplified.
// ----------------------------------------------------------------------------
void drawPerspectiveTexturedPolygon(sVERT *verts)
{
// Find the top-most vertex
sVERT *v = verts, *lastVert = verts, *lTop = verts, *rTop;
while(v)
{
if (v->y < lTop->y) lTop = v;
lastVert = v;
v->iy = ceil(v->y);
v = v->next;
}
rTop = lTop;
// Top scanline of the polygon in the frame buffer
char *fb = &backBuffer[lTop->iy * RES_X];
// Left & Right edges (primed with 0)
sEDGE le, re; le.height = 0; re.height = 0;
// Render the polygon
while(1)
{
if (!le.height)
{
sVERT *lBot = lTop - 1; if (lBot < verts) lBot = lastVert;
le.height = lBot->iy - lTop->iy;
if (le.height < 0) return;
calcEdgeDeltas(le, lTop, lBot);
lTop = lBot;
}
if (!re.height)
{
sVERT *rBot = rTop + 1; if (rBot > lastVert) rBot = verts;
re.height = rBot->iy - rTop->iy;
if (re.height < 0) return;
calcEdgeDeltas(re, rTop, rBot);
rTop = rBot;
}
// Polygon must have height
if (!le.height && !re.height) return;
// Get the height
int height = MIN(le.height, re.height);
// Subtract the height from each edge
le.height -= height;
re.height -= height;
// Render the current trapezoid defined by left & right edges
while(height-- > 0)
{
// Texture coordinates
float overWidth = 1.0 / (re.x - le.x);
float du = (re.u - le.u) * overWidth;
float dv = (re.v - le.v) * overWidth;
float dw = (re.w - le.w) * overWidth;
// Find the end-points
int start = (int) ceil(le.x);
int end = (int) ceil(re.x);
// Texture adjustment (some call this "sub-texel accuracy")
float subTex = (float) start - le.x;
float u = le.u + du * subTex;
float v = le.v + dv * subTex;
float w = le.w + dw * subTex;
// Fill the entire span
char *span = fb + start;
for (; start < end; start++)
{
float z = 1.0 / w;
int s = (int) (u * z);
int t = (int) (v * z);
*(span++) += textureBuffer[(t<<6)+s];
u += du;
v += dv;
w += dw;
}
// Step
le.u += le.du;
le.v += le.dv;
le.w += le.dw;
le.x += le.dx;
re.u += re.du;
re.v += re.dv;
re.w += re.dw;
re.x += re.dx;
fb += RES_X;
}
}
}
// ----------------------------------------------------------------------------
// Draw a "sub-affine" perspective-correct texture-mapped polygon. This
// routine uses affine texture-mapping between sub-spans of SUB_SPAN length
// while only performing perspective correction every SUB_SPAN pixels. This
// produces a much faster routine that the one above, but suffers from accuracy
// loss.
//
// This routine also suffers from other aliasing problems of the first two,
// however, since these polygons are an estimated perspective-correct, they
// choose texels in a non-perfect fasion. Remember that the perspective curve
// (explained in the comments above the previous example) is being estimated
// with linear interpolation (i.e. straight lines.) This can cause the error
// (already present in a non-linear estimation) to be amplified even more.
//
// The greater the SUB_SPAN length, the less "perspective correction" is
// performed AND the less accurately texels will be chosen.
//
// This routine also uses a fixed-point representation of the UV values as it
// interpolates each sub-span. This should not cause any problems since the
// fixed-point representation is 8.24 (24 bits used to represent the fractional
// component) which is a higher degree of resolution than a 32-bit floating-
// point variable offers. However, if the delta from texel to texel goes
// beyond 255.999... texels from texel to texel, the value will overflow and
// results may be unpredictable.
// ----------------------------------------------------------------------------
void drawSubPerspectiveTexturedPolygon(sVERT *verts)
{
// Find the top-most vertex
sVERT *v = verts, *lastVert = verts, *lTop = verts, *rTop;
while(v)
{
if (v->y < lTop->y) lTop = v;
lastVert = v;
v->iy = ceil(v->y);
v = v->next;
}
rTop = lTop;
// Top scanline of the polygon in the frame buffer
char *fb = &backBuffer[lTop->iy * RES_X];
// Left & Right edges (primed with 0)
sEDGE le, re; le.height = 0; re.height = 0;
// Render the polygon
while(1)
{
if (!le.height)
{
sVERT *lBot = lTop - 1; if (lBot < verts) lBot = lastVert;
le.height = lBot->iy - lTop->iy;
if (le.height < 0) return;
calcEdgeDeltas(le, lTop, lBot);
lTop = lBot;
}
if (!re.height)
{
sVERT *rBot = rTop + 1; if (rBot > lastVert) rBot = verts;
re.height = rBot->iy - rTop->iy;
if (re.height < 0) return;
calcEdgeDeltas(re, rTop, rBot);
rTop = rBot;
}
// Polygon must have height
if (!le.height && !re.height) return;
// Get the height
int height = MIN(le.height, re.height);
// Subtract the height from each edge
le.height -= height;
re.height -= height;
// Render the current trapezoid defined by left & right edges
while(height-- > 0)
{
// Texture coordinates
float overWidth = 1.0 / (re.x - le.x);
float du = (re.u - le.u) * overWidth;
float dv = (re.v - le.v) * overWidth;
float dw = (re.w - le.w) * overWidth;
// Find the end-points
int start = (int) ceil(le.x);
int end = (int) ceil(re.x);
// Texture adjustment (some call this "sub-texel accuracy")
float subTex = (float) start - le.x;
float u = le.u + du * subTex;
float v = le.v + dv * subTex;
float w = le.w + dw * subTex;
// Start of the first span
float z = 1.0 / w;
float s1 = u * z;
float t1 = v * z;
// Fill the entire span
char *span = fb + start;
int pixelsDrawn = 0;
for(; start < end; start += SUB_SPAN)
{
// Start of the current span
float s0 = s1;
float t0 = t1;
int len = MIN(SUB_SPAN, end - start);
pixelsDrawn += len;
// End of the current span
z = 1.0 / (w + dw * pixelsDrawn);
s1 = z * (u + du * pixelsDrawn);
t1 = z * (v + dv * pixelsDrawn);
// The span (8.24 fixed-point)
float divisor = 1.0 / len * 0x1000000;
unsigned int ds = (s1 - s0) * divisor;
unsigned int dt = (t1 - t0) * divisor;
unsigned int s = s0 * 0x1000000;
unsigned int t = t0 * 0x1000000;
// Draw the sub-span
for (int j = 0; j < len; j++)
{
*(span++) += textureBuffer[((t>>18)&0xffffffC0)+(s>>24)];
s += ds;
t += dt;
}
}
// Scanline step
le.u += le.du;
le.v += le.dv;
le.w += le.dw;
le.x += le.dx;
re.u += re.du;
re.v += re.dv;
re.w += re.dw;
re.x += re.dx;
fb += RES_X;
}
}
}
// ----------------------------------------------------------------------------
// Silly little test routine to test the polygon renderers
// ----------------------------------------------------------------------------
void main(void)
{
// Initialize our texture buffer
drawTexture();
// Set the mode
setMode(0x13);
// Setup the 4 adjacent polygons
sVERT p0[4];
p0[0].x = -1.0; p0[0].y = -1.0; p0[0].z = 1.0; p0[0].next = &p0[1];
p0[1].x = 0; p0[1].y = -1.0; p0[1].z = 1.0; p0[1].next = &p0[2];
p0[2].x = 0; p0[2].y = 0; p0[2].z = 0; p0[2].next = &p0[3];
p0[3].x = -1.0; p0[3].y = 0; p0[3].z = 0; p0[3].next = NULL;
sVERT p1[4];
p1[0].x = 0; p1[0].y = -1.0; p1[0].z = 1.0; p1[0].next = &p1[1];
p1[1].x = 1.0; p1[1].y = -1.0; p1[1].z = 1.0; p1[1].next = &p1[2];
p1[2].x = 1.0; p1[2].y = 0; p1[2].z = 0; p1[2].next = &p1[3];
p1[3].x = 0; p1[3].y = 0; p1[3].z = 0; p1[3].next = NULL;
sVERT p2[4];
p2[0].x = -1.0; p2[0].y = 0; p2[0].z = 0; p2[0].next = &p2[1];
p2[1].x = 0; p2[1].y = 0; p2[1].z = 0; p2[1].next = &p2[2];
p2[2].x = 0; p2[2].y = 1.0; p2[2].z = -1.0; p2[2].next = &p2[3];
p2[3].x = -1.0; p2[3].y = 1.0; p2[3].z = -1.0; p2[3].next = NULL;
sVERT p3[4];
p3[0].x = 0; p3[0].y = 0; p3[0].z = 0; p3[0].next = &p3[1];
p3[1].x = 1.0; p3[1].y = 0; p3[1].z = 0; p3[1].next = &p3[2];
p3[2].x = 1.0; p3[2].y = 1.0; p3[2].z = -1.0; p3[2].next = &p3[3];
p3[3].x = 0; p3[3].y = 1.0; p3[3].z = -1.0; p3[3].next = NULL;
sVERT *polys[4] = {p0, p1, p2, p3};
int polyCount = 4;
float theta = 0.0;
// For the timer
double totalTicks = 0.0, totalPolygons = 0.0;
// Animate
float speed = 30.0;
while(!kbhit() && totalPolygons < POLY_COUNT)
{
// Clear the backBuffer
memset(backBuffer, 0, sizeof(backBuffer));
// Rotate just a little
theta += 0.0003 * speed;
// Draw the polygons
for (int i = 0; i < polyCount; i++)
{
// Temporary polygon
sVERT poly[4];
// Offset/scale the vertices
sVERT *src = polys[i];
sVERT *dst = poly;
while(src)
{
// Rotate
dst->u = src->x * (0.49 * TEX_X) + (0.5 * TEX_X);
dst->v = src->y * (0.49 * TEX_Y) + (0.5 * TEX_Y);
dst->w = 1.0;
dst->x = src->x * cos(theta) - src->y * sin(theta);
dst->y = src->x * sin(theta) + src->y * cos(theta);
dst->z = src->z;
// Scale
dst->x *= 700.0;
dst->y *= 700.0;
dst->z *= 10.0; dst->z += 20.0;
// Project
#ifndef USE_AFFINE
dst->u /= dst->z;
dst->v /= dst->z;
dst->w /= dst->z;
#endif
dst->x /= dst->z;
dst->y /= dst->z;
// Offset to screen center
dst->x += RES_X / 2.0 + 0.5;
dst->y += RES_Y / 2.0 + 0.5;
// Terminate the list
dst->next = src->next ? &dst[1] : NULL;
// Next!
src = src->next;
dst = dst->next;
}
// Time the drawing
unsigned int sHi, sLo, eHi, eLo;
getTicks(&sHi, &sLo);
#ifdef USE_AFFINE
drawAffineTexturedPolygon(poly);
#endif
#ifdef USE_EXACT_PERSPECTIVE
drawPerspectiveTexturedPolygon(poly);
#endif
#ifdef USE_SUB_AFFINE_PERSPECTIVE
drawSubPerspectiveTexturedPolygon(poly);
#endif
// Calculate ticks
getTicks(&eHi, &eLo);
double highMultiplier = pow(2.0, 32.0);
double sTicks = (double) sHi * highMultiplier + (double) sLo;
double eTicks = (double) eHi * highMultiplier + (double) eLo;
totalTicks += eTicks - sTicks;
totalPolygons += 1.0;
}
// Copy to the display
memcpy(frameBuffer, backBuffer, sizeof(backBuffer));
}
// Get the pending key
if (totalPolygons < POLY_COUNT) getch();
// Restore text mode
setMode(3);
// Print the timing information
printf("%d average ticks per polygon (%d polygons drawn).\n", (int) (totalTicks / totalPolygons), (int) totalPolygons);
}
// ----------------------------------------------------------------------------
// Tmap.cpp - End of file
// ----------------------------------------------------------------------------
|