It's free to join Gamasutra!|Have a question? Want to know who runs this site? Here you go.|Targeting the game development market with your product or service? Get info on advertising here.||For altering your contact information or changing email subscription preferences.
Registered members can log in here.Back to the home page.

Search articles, jobs, buyers guide, and more.

By Haim Barad, Mark Atkins, Or Gerlitz & Daniel Goehring
Gamasutra
May 1, 1998



Features

Real-Time Procedural
Texturing Techniques Using MMX

Photorealistic two- and three-dimensional graphics systems require the ability to apply textures to objects.. For example, a room in a 3D game looks more realistic if the walls and floor have interesting patterns, rather than a solid color. Traditional texture mapping methods wrap a 2D bitmap on a 3D object. The procedural texture mapping method produces natural textures on the fly, using mathematical approximations for materials, such as wood, marble, and stone.

Procedural textures are rarely used in real-time, hardware-based rendering engines. Procedural textures use the basic Perlin noise algorithm, which has many variants and is non-standard. In addition, each texture requires a different hardware circuit to implement it, whereas regular texture mapping uses the same circuit, but loads different textures.

Procedural textures are rarely used in real-time, software rendering engines, primarily because the calculations are time consuming. The Perlin gradient noise function interpolates random values that are precomputed for each lattice point in the object space. This computation is floating-point intensive and requires many table reads for each texel. In addition, the calculations required for turbulence and sine wave evaluation make the Perlin method even more time-consuming.

These problems seem to imply that the procedural texture method can't produce the many mega-pixels per second required for real-time hardware and software game engines. However, we present an MMX implementation of Perlin noise that produces fast procedural textures, which perform competitively with regular texture mapping methods.

The following lists summarizes the strengths and weaknesses of the two different texture mapping methods:

Procedural Textures:
  • Infinite resolution; can be changed on the fly.
  • Actually 3D
  • Very CPU intensive
  • Almost no bandwidth requirements
  • Large variety of natural textures without image map storage
  • Hard to control
  • Can't handle certain cases: people, pictures on the wall, cola can labels, etc.

Traditional Texture Mapping:

  • Simple calculation
  • Represents 2D data
  • Memory intensive, very high bandwidth
  • Each texture is loaded explicitly from memory
  • Different resolutions require loading different textures
  • Can handle and manipulate any data captured by camera or drawing

Most procedural texture mapping techniques are based on a noise function, such as Perlin noise. Generally speaking, noise functions assign each location in space some random value, but in a somehow controllable way. The values are assigned to the integer points and are interpolated for other points. The function can be defined for any dimension (e.g. 1D, 2D, 3D, 4D...) and at arbitrary resolution sampling.

Fractional Brownian motion (fBm), developed by F. Kenton Musgrave, is based on an iterative method which sums different (Perlin) noise values together. To explain how the fBm works, imagine an image like Figure 3.1, treated as a height map. In other words, the colors in the image represent actual heights. Therefore, by looking at the image from the side, an imaginative person might see rolling hills and mountains. Now, repeat this image many times. For each copy of the image, scale the amplitude of the heights of the hills by varying amounts. Next, vary the magnification of the scene for each image. Some scenes might be zoomed out, while other scenes might be zoomed in. Lastly, to form the final image, sum the images together. See Figure 3.2 for an output example.

The number of iterations of Perlin's noise in the fBm are known as "Octaves". Musgrave suggests the number of octaves used should be:

octaves = log_base2(screen.resolution) - 2

For a screen resolution of 640x480,

octaves = log_base2(640) - 2 = ~7 octaves

This is a general rule to follow. Usually, fewer octaves produces an image close to the original and requires less computation time.

 

Figure 3.1
Figure 3.2

Figure 3.1. Original Perlin noise image

Figure 3.2. Final fBm output consisting of 7 noise function outputs appropriately scaled and summed together

The following diagram shows how the image is built. Seven noise-function outputs are scaled and added together. Noted under each of the images are the zoom factors along with the amplitude modification factor. In practice, experiments show octaves beyond 3 are essentially unneeded.

 
+
+
+
+
+
+
=
Noise #0
Noise #1
Noise #2
Noise #3
Noise #4
Noise
#5
Noise
#6
fBm, 7 oct
S=1
S=2
S=4
S=8
S=16
S=32
S=64
 
A=1.000
A=0.500
A=0.250
A=0.125
A=0.0625
A=0.03125
A=0.015625
 

Since the textures in Figures 3.1 and 3.2 are based on fBm, Appendix A shows the code segment used.

The output of fBm can be varied mathematically to form new images. For example, a function can take the output of the fBm function and distort it in some way to generate wood, marble, clouds, water, fire, rock strata, and so on. These textures can be used as 2D textures, or extended into the 3rd dimension. For example, several games on the market use voxel graphics for land generation (for more information on voxels, see Andre Lamothe's article in Game Developer, Sept 97). fBm can be used to generate height maps for real-time land generation.

While 3D developers have mastered the art of creating indoor scenes, there's much work left to do when it comes to adequately depicting the outdoors. Proceduralism can be used to help achieve the effect many are looking for.

Wood texture can be computed using the relative distance of a point from the tree's axis to construct rings of similar color, like wood rings. The algorithm calculates the radius and perturbs it with the turbulence, which is the fractional Brownian motion discussed in Section 3.0. Thus, the wood at point (u,v) is evaluated at (sqrt(u2 + v2) + turbulence(u, v)).

Figure 4.2 is a section of the texture formed from concentric rings that alternate between different shades of brown and black. These rings are modeled by the equation: sqrt(u2 + v2). Each color in Figure 4.2 is an index into an array containing ordered shades of browns. The colors in Figure 4.3 are random offsets in the range of 0 to 63, which were calculated by the fBm using two octaves. These offsets are added to the color indexes in Figure 4.2. The final numerical result is used as an index into an array containing wood colors, which produces the image in Figure 4.4.

Figure 4.1

Figure 4.1: Final procedural wood output

 
Figure 4.2 + Figure 4.3 = Figure 4.4

Figure 4.2: Image produced by r = sqrt(u2 + v2)

Figure 4.3: Color offsets to the original image, produced by fBm using 2 octaves.

Figure 4.4: Final procedural wood output

The algorithm for wood at texel location x = (u,v) contains four steps:

  1. Calculate r = sqrt(u2 + v2).
  2. Calculate turbulence(x).
  3. Calculate index to the Wood table: i= 10 * r + 15 * turbulence(x).
  4. Determine wood(x) by reading Wood[i], i.e., wood(x) = Wood[i].

Wood[] is an array of gradient colors, based on the RenderMan wood function (see "The RenderMan Companion: A Programmer's Guide to Realistic Computer Graphics", by Steve Upstill, Addison-Wesley). For each point, this wood function uses the fractional part of the perturbed radial distance from the tree axis to build an interpolation scheme between [0-1], going from black to brown in a smooth way. Since MMX technology lacks a fast, parallel square root evaluation, step 1 reads the square root values from a table. Another table is used in step 4 for RenderMan's wood function.

The domain of the wood texture is 2D (i.e.,
wood = Wood(u,v)). If the square root calculation in step 1 is replaced by an absolute value calculation, the final result, after wrapping the 2D wood texture on a 3D object, is almost identical to the original texture. Therefore, step 1 in the algorithm can be replaced by:

  1. Calculate r = | u - v |.

All the points (u,v) which obey the equation, |u -v| = c, lie along the straight line |u - v| - c = 0. This optimization replaces the serial table reads of the square root values with a fast, parallel absolute value calculation. The absolute value calculation takes advantage of the SIMD nature of the MMX instructions.

Figure 4.5

Figure 4.6

Figure 4.5. Output of the linear wood algorithm

Figure 4.6. Output of the sqrt wood algorithm

The code in Listing 1 implements the wood texture algorithm.

Listing 1. Wood Texture Algorithm.

//*****************************************************************************
// woodPassMMX - calculates turbulence and then calls 
// SIMD_Wood_Linear() or SIMD_Wood_Sqrt()
//
// Inputs:
// u_init, v_init: Starting U and V coordinates into the texture map.
// du, dv: Measures the change in U and V for each pixel of the scanline.
// Num_Pix: Length of the scanline in pixels.
// screen_buffer: Pointer to the drawing surface.
// sqrtTable: A pointer to an array containing the square root of 2048 numbers
// woodTable: A pointer to an array containing pre calculated wood colors.
//
//*****************************************************************************
void woodPassMMX(unsigned long u_init, unsigned long v_init, 
                 signed   long du    , signed   long dv, 
                 long Num_Pix        , unsigned __int16* screen_buffer)
{
  unsigned __int16 alignPixNum;
  static unsigned __int16 turbulenceBuf[1024];
  unsigned long num_octaves = 3;




  //Used for Quad alignment
  alignPixNum = (Num_Pix + 3) & 0xFFFFFFFC; 


  //Clear out the turbulence buffer.
  memset(turbulenceBuf, 0, sizeof(__int16) * alignPixNum); 
  
  //Calculate the turbulence
  SIMD_Octave(u_init, v_init, du, dv, (alignPixNum >> 2), turbulenceBuf, num_octaves);


  
  //Using the averaging scheme for the even pixels while the odd pixels are calculate, 
  //the first value for pixel #0 isn't truly known.  Therefore assign the color of pixel 1
  //to pixel #0.
  turbulenceBuf[0] = turbulenceBuf[1];


  //Calculate the wood colors for the scanline.


  if(LINEAR == TRUE)
  {
    SIMD_Wood_Linear(u_init, v_init, du, dv, alignPixNum);
  }
  else
  {
    SIMD_Wood_Sqrt(u_init, v_init, du, dv, alignPixNum);
  }


  memcpy(screen_buffer, turbulenceBuf, sizeof(__int16) * Num_Pix);
}

 

The SIMD_Octave() procedure pre-calculates the turbulence values and stores them in a buffer. The rest of the algorithm is implemented in the two wood procedures, SIMD_Wood_Linear() and SIMD_Wood_Sqrt()

The table containing the square root values, sqrtTable, is defined by the following:


for (i = 0; i ((2048; i++) 
  sqrtTable[i] = (unsigned __int16)floor(sqrt((i << 10))); 


This initialization section is used to set up the smooth gradient
wood colors. The colors start off black and smoothly change to brown.

         

for (i = 0; i (6000; i++) 
{ 
  //The equation for "r" is just a linear one. If graphed, 
  //a line with positive slope results. This gives us the backbone 
  //for the smooth gradients that will be used for the wood color. 


  r = (float)4.0 * i; 
  r *= 1.0 / 512.0; 
  r -= (float)floor(r); 
  r = smoothstep((float)0, (float)0.83, r) - smoothstep((float)0.83, 
      (float)1.0, r); 


  comp_r = 1 - r; 


  //One r is calculated, the individual red, green, and blue components 
  //are found. These components are on a scale from 0.0 to 1.0. 
  wood_red = r * (float)0.30 * 2.0 + comp_r * (float)0.050 * 2.0; 
  wood_green = r * (float)0.12 * 2.0 + comp_r * (float)0.010 * 2.0; 
  wood_blue = r * (float)0.03 * 2.0 + comp_r * (float)0.005 * 2.0; 


  red = ((long)(wood_red * 255)) & 0xF8; 
  green = ((long)(wood_green * 255)) & 0xFC; 


  if (FORMAT565) //for 565 bits per pixel 
  { 
   green = ((long)(wood_green * 255)) &; 0xF8; 
   woodTable[i] = (unsigned __int16)((red (( 8) | 
                  (green ((3 ) | (blue ) 3));
  }


  else 		//for 555 bits per pixel
  {
    green = ((long)(wood_green * 255)) 0xF8;
    woodTable[i] = (unsigned __int16)((red (( 7) | 
                   (green (( 2 ) | (blue ) 3));
  }
}

For the MMX technology source code listings of SIMD_Octave() and SIMD_Wood(), see Appendix A and Appendix B respectively. For the linear approximation of the sqrt() version of the code, see Appendix G. Marble has a fractal-like appearance, which can be approximated by evaluating the sine(x + turbulence(x)) and applying a perturbation based on turbulence(x) to the object's normals during the lighting procedure.

The algorithm for marble at location x = (u,v) contains five steps. Steps 1 through 4 are necessary to produce the marble texture. Step 5 is not required, but does improve the overall result by adding lighting.

  1. Calculate turbulence(x).
  2. Calculate marble(x) = sine(u + 10 * turbulence(x)).
  3. Transform the sine output from the range [-1,1] to [0,1].
  4. Use the transformed scalar value to blend random or color spline output rgb values with a constant base intensity.
  5. Use the turbulence value during lighting to get a random fraction and perturb the object's normals.

Figure 5.1 illustrates the first four steps of the marble texture algorithm. The fifth step is part of the lighting procedure and will be explained in Section 7.2.

Marble A
Marble B
Marble C

Figure 5.1: Result after
step 1

Figure 5.2: Intermed.
result, u +
10 * turbulence (x)

Figure 5.3: Result after
Step 4

Since the algorithm uses fixed point arithmetic, the inputs to the sine in step 2 are known in advance. Therefore, the complicated calculation of steps 2, 3 and 4 can be performed in setup time, and stored in a table. At rendering time the algorithm indexes into this table. Although the reads are serial, they capture only a small amount of time compared to the rest of the parallel computation. The actual marble algorithm at point x = (u,v) is as follows:

  1. Calculate turbulence(x).
  2. Calculate index to the Marble table: i= u + 10 * turbulence(x).
  3. Determine marble(x) by reading Marble[i], i.e. marble(x) = Marble[i].
  4. Use the turbulence value during lighting to get a random fraction and use this fraction to perturb the object's normals.

The content of the Marble table can be replaced with a different variant every few frames, without impacting overall performance. This prevents you from having to load a new texture from memory when using image texture mapping. In addition, the original steps 2, 3 and 4 can be replaced with any texture calculation based on the location ( u ) and the turbulence of (u,v). When calculating the turbulence, the number of octaves used is critical. As more octaves are used, the computation time increases but the end result is better. See Figures 5.4 to 5.8.

 

Figure 5.4

Figure 5.5

Figure 5.6

Figure 5.4: One Octave

Figure 5.5: Two Octaves

Figure 5.6: Three Octaves

Figure 5.6

Figure 5.8

Figure 5.7: Four Octaves

Figure 5.8: Five Octaves

The code in Listing 2 implements the marble texture algorithm.

 

Listing 2. The Marble Texturing Algorithm.
//******************************************************************************
//Procedure marblePassMMX()
//
//Inputs:
// u_init, v_init: Starting U and V coordinates into the texture map.
// du, dv: Measures the change in U and V for each pixel of the scanline.
// Num_Pix: Length of the scanline in pixels.
// screen_buffer: Pointer to the drawing surface.
// MarbleTable: A pointer to an array containing the sin of some range of numbers.
//******************************************************************************

void marblePassMMX(unsigned long u_init, unsigned long v_init,
signed long du , signed long dv,
long Num_Pix , unsigned __int16* screen_buffer)
{
unsigned __int16 alignPixNum;
static unsigned __int16 turbulenceBuf[1024];
unsigned long num_octaves = 3;


//Used for Quad alignment
alignPixNum = (Num_Pix + 3) &amp; 0xFFFFFFFC;

//Clear out the turbulence buffer.
memset(turbulenceBuf, 0, sizeof(__int16) * alignPixNum);

//Calculate the turbulence
SIMD_Octave(u_init, v_init, du, dv, (alignPixNum 2),
turbulenceBuf, num_octaves);

//Using the averaging scheme for the even pixels while the odd pixels are calculate,
//the first value for pixel #0 isn't truly known. Therefore assign the color of pixel #1
//to pixel #0.
turbulenceBuf[0] = turbulenceBuf[1];

//Calculate the marble colors for the scanline.
SIMD_Marble(u_init, du, alignPixNum);

memcpy(screen_buffer, turbulenceBuf, sizeof(__int16) * Num_Pix);
}

 

The SIMD_Octave() procedure calculates the turbulence values and stores them in a buffer. The rest of the algorithm is implemented in the SIMD_Marble() procedure. SIMD_Marble() uses the values of the turbulence buffer filled by SIMD_Octave with several octaves of noise. Four pixels are calculated in each iteration.

For the MMX technology source code listings of
SIMD_Octave() and SIMD_Marble(), see Appendix A and Appendix C respectively. During rendering time, the marble table values should be based on the sine(x), where x is a floating point number with a fraction. During initialization, this fraction is approximated by dividing each input by 256. The result is then multiplied by Pi to produce a smooth shape.

The table containing the marble values,
marbleTable, is defined by the following.


for (i = 0; i < 5000; i++)
{ 
  val     = (double)i  / 256.0;
  sin_val = (sin(val * Pi) + 1.0) * 0.5;
  red     = ((long) ((0.33 + 0.66 * sin_val) * 256)) & 0xF8;
  blue    = ((long) ((0.60 + 0.39 * sin_val) * 256)) & 0xF8;


  if (FORMAT565)  //for 565 bits per pixel
  {
    green = ((long) ((0.27 + 0.72 * sin_val) * 256)) & 0xFC; 
    MarbleTable[i] = (unsigned __int16)((red << 8) | 
                 (green <<3 ) | (blue > 3));
  }


  else //for 555 bits per pixel
  {
    green = ((long) ((0.27 + 0.72 * sin_val) * 256)) & 0xF8; 
    MarbleTable[i] = (unsigned __int16)((red << 7) | 
                 (green <<2 ) | (blue > 3));
  }
}



For the MMX technology source code listings of SIMD_Octave() and SIMD_Marble() see Appendix A and Appendix C respectively.

The figures below zoom into the marble texture and demonstrate the difference in the final outcome using 1-5 octaves of Perlin noise.

Figure 10.1

Figure 10.2

Figure 10.3

Figure 10.4

Figure 10.5

Figure 10.1: One octave

Figure 10.2: Two octaves

Figure 10.3: Three octaves

Figure 10.4: Four octaves

Figure 10.5: Five octaves

With games that use a perspective viewing frustrum instead of an orthogonal view, drawing perspective-corrected textures can be difficult. The mathematics required to draw perfect perspective textures is generally too much for a PC to handle in real time. Algorithms can approximate perspective without many viewing artifacts. One algorithm, quadratic approximation, involves finding the per-pixel change in du and dv across each scanline, known as ddu and ddv respectively.
 

Figure 6.1: A perspective perfect texture mapping. Image created by Bryce Software.

Figure 6.2: An example of a texture anomaly that results from some forms of texture mapping. This imperfect texture mapping occurs when the ddu and ddv terms are ignored. This is especially noticeable in moving objects.

 

Using ddu and ddv to update du and dv across each scanline poses another problem with the new procedural texture scanline algorithms. The problem is that, since four pixels are calculated in parallel, DU, DV, DDU, and DDV must be calculated in parallel as well, and the assembly code needed to set up these parameters is expensive for the CPU to calculate. As an alternative, the procedural textures developed in this application don't use ddu and ddv to update du and dv for each pixel drawn.

 

Figure 6.3: Even if the ddu and ddv terms are ignored, in some cases the errors won't occur. As seen from the above image, the texture mapping is fine. This is because for each scanline drawn, the ddu and ddv terms are close to zero.

As a result, if the polygons are too big, gross artifacts develop during the texturing process. There are ways to get around this. One is to keep the polygons small with small scanlines. When only drawing a few pixels, there isn't enough time for errors to accumulate. The other technique is to sub-divide each long scanline into shorter segments. Many short line segments can be put end-to-end to construct a longer segment. For example, if a scanline is 600 pixels long, it can be drawn as 37 16-pixel scanlines, with eight pixels left over. At the start of each sub-scanline, the du, dv parameters are recalculated to remove error accumulation. This techniques works well but in some instances, a ripple artifact in the textures can be seen. This is because as each pixel is drawn, more errors accumulate. Then after pixel N, the du and dv values are recalculated. Then as more pixels are drawn, the errors begin to accumulate again. At pixel 2N, the du and dv values are recalculated to be exact. With the repetition of this over and over, it can be seen how a ripple develops.

If possible, leave out the DDU and DDV terms. If gross artifacts are noticeable then use the assembly code segment in Appendix D to add the ddu and ddv terms.
The following table explains the MMX code in Appendix D. Since four pixels are drawn per loop iteration, four U, V, DU, DV, DDU, and DDV values need to be tracked. As the even pixels are averaged from the odd pixels, only two U, V, DU, DV, DDU, and DDV terms need to be tracked: pixels number one and three.

U, V, DU, DV, DDU, and DDV are in 10.22 fixed format. Therefore the code stores the U value for pixels one and three in one register. The V value for pixels one and three is stored in another register. The same is also true for the DU, DV, DDU, and DDV terms. Table 1 explains the U, DU, and DDU terms for the first sixteen pixels drawn in a scanline. For each U update, you add the current U value to the previous DU value. For each DU update, add DDU to the previous DU value. The code calculates only the odd numbered pixels, which are shown in the table 2. The initialization code needs to set up U, V, DU, DV, DDU and DDV for pixels one and three. After each loop iteration, U, V, DU, and DV are updated to match the table.

Table 1

Current Pixel

Current U Value

Current DU Value

Current DDU Value

Pixel #0: U DU DDU
Pixel #1: U + DU DU + DDU DDU
Pixel #2: U + 2DU + DDU DU + 2DDU DDU
Pixel #3: U + 3DU + 3DDU DU + 3DDU DDU
Pixel #4: U + 4DU + 6DDU DU + 4DDU DDU
Pixel #5: U + 5DU + 10DDU DU + 5DDU DDU
Pixel #6: U + 6DU + 15DDU DU + 6DDU DDU
Pixel #7: U + 7DU + 21DDU DU + 7DDU DDU
Pixel #8: U + 8DU + 28DDU DU + 8DDU DDU
Pixel #9: U + 9DU + 36DDU DU + 9DDU DDU
Pixel #10: U + 10DU + 45DDU DU + 10DDU DDU
Pixel #11: U + 11DU + 55DDU DU + 11DDU DDU
Pixel #12: U + 12DU + 66DDU DU + 12DDU DDU
Pixel #13: U + 13DU + 78DDU DU + 13DDU DDU
Pixel #14: U + 14DU + 91DDU DU + 14DDU DDU
Pixel #15: U + 15DU + 105DDU DU + 15DDU DDU

 

Table 2

Current Pixel Current U Value Current DU Value Current DDU Value
Pixel #1: U + DU DU + DDU DDU
Pixel #3: U + 3DU + 3DDU DU + 3DDU DDU
Pixel #5: U + 5DU + 10DDU DU + 5DDU DDU
Pixel #7: U + 7DU + 21DDU DU + 7DDU DDU
Pixel #9: U + 9DU + 36DDU DU + 9DDU DDU
Pixel #11: U + 11DU + 55DDU DU + 11DDU DDU
Pixel #13: U + 13DU + 78DDU DU + 13DDU DDU
Pixel #15: U + 15DU + 105DDU DU + 15DDU DDU

The MMX registers containing the initial U and V values need to be setup as shown below:


;Note: UV values are stored in 10.22 fixed integer format.
;This sets up the U parameters for pixels 1 and 3 in register MM0 and
;V in MM1.  After setup, the registers will contain:
;      |--------- 32 bit ------------|
;      +-------------------------------------------------------------------+
;MM0 = | U texel for pix #1 = u + du | U texel for pix #3 = u + 3du + 3ddu |
;      +-------------------------------------------------------------------+
;      +-------------------------------------------------------------------+
;MM1 = | V texel for pix #1 = v + dv | V texel for pix #3 = v + 3dv + 3ddv |
;      +-------------------------------------------------------------------+

The code in Appendix D shows how this is done.

For the DU and DV initialization, the change in U and V should be measured to see if a pattern develops. Equations can be constructed that model each pattern. Using the formula, DU = U_Next - U_Previous, the table 3 is developed.


Table 3

U Change between Pixel #1 and Pixel #5: 4DU + 10DDU
U Change between Pixel #3 and Pixel #7: 4DU + 18DDU
U Change between Pixel #5 and Pixel #9: 4DU + 26DDU
U Change between Pixel #7 and Pixel #11: 4DU + 34DDU
U Change between Pixel #9 and Pixel #13: 4DU + 42DDU
U Change between Pixel #11 and Pixel #15: 4DU + 50DDU

As shown from the above table, the MMX registers used to contain the initial DU and DV values need to be setup as shown below.


;Note: du dv texel values are stored in 10.22 fixed integer format.
;This sets up the du parameters for pixels 1 and 3 in MM0 register and
;dv parameter in MM1 register.  After setup, the registers will contain:
;      |--------- 32 bit --------------|
;      +---------------------------------------------------------------+
;MM0 = | DU texel for p1 = 4du + 10ddu | DU texel for p3 = 4du + 18ddu |
;      +---------------------------------------------------------------+
;      +---------------------------------------------------------------+
;MM1 = | DV texel for p1 = 4dv + 10ddv | DV texel for p3 = 4dv + 18ddv |
;      +---------------------------------------------------------------+

To determine what the DDU and DDV values should be, the change in the DU and DV values is measured when moving from pixel to pixel. Applying the formula DDU = Next DU - Previous DU to the previous table produces the following table of values:

DU Change between Pixel #1 and Pixel #5: 16DDU
DU Change between Pixel #3 and Pixel #7: 16DDU
DU Change between Pixel #5 and Pixel #9: 16DDU
DU Change between Pixel #7 and Pixel #11: 16DDU
DU Change between Pixel #9 and Pixel #13: 16DDU
DU Change between Pixel #11 and Pixel #15: 16DDU

This table shows that the initial values for variables DDU and DDV should be set up in the MMX registers as shown in the following:


;Note: ddu ddv texel values are stored in 10.22 fixed integer format.
;This sets up the ddu parameters for pixels 1 and 3 in MM0 register and
;ddv parameter in MM1 register.  After setup, the registers will contain:
;      |--------- 32 bit ---------|
;      +-----------------------------------------------------+
;MM0 = | DDU texel for p1 = 16ddu | DDU texel for p3 = 16ddu |
;      +-----------------------------------------------------+
;      +-----------------------------------------------------+
;MM1 = | DDV texel for p1 = 16ddv | DDV texel for p3 = 16ddv |
;      +-----------------------------------------------------+

Since the DDU and DDV terms are constant, no additional calculations are required across the scanline.

For each pass through the inner loop, four pixels are drawn and the following variables need to be updated. The MMX register that contains the DU terms needs to be added to the MMX register that contains the U terms. The PADDD instruction needs to be used. The MMX register that contains the DDU terms needs to be added to the MMX register that contains the DU terms. Again the PADDD instruction should be used. The above instructions should also be applied to the V domain.
There are several software techniques that improve the appearance of objects and accelerate the lighting procedure. Using them you can produce quick specular effects, improve your scene's appearance by perturbing the normals, and convert floating-point variables to longs quickly.

The classic lighting equation has three components: ambient, diffuse and specular. This equation can be written as:

Color = Ka * Amb_Color + Kd * Obj_Color * (N dot L) + Ks * Light_Color * (R dot V)n

  • Ka, Kd and Ks are the ambient, diffuse and specular coefficients.
  • N is the object's normal.
  • L is the vector from the object to the light source.
  • R is the reflected vector from the object.
  • V is the view vector.
  • n is the specular exponent.

Gouraud shading calculates the color at each vertex of the polygon and interpolates it for each internal pixel. The Phong method calculates the color at each internal pixel by interpolating the normal, but it's an expensive calculation. Therefore, most graphic systems implement the Gouraud method, often without the specular part.

A high quality lighting procedure which calculates a color for each internal pixel but eliminates the slow exponent part is:

Color = (Ka + Kds * (N dot L)n) * Texture_Color

Instead of calculating (R dot V)n, the term (Ni dot L)n is evaluated at the vertices of the object and interpolated inside the polygon. This substitution is mathematically incorrect when n doesn't equal 1, because then ((aN1 + bN2 + cN3) dot L)n does not equal a ((N1 dot L)n) + b((N2 dot L)n) + c((N3 dot L)n). However, the result appears similar to real specular highlights, but is faster to calculate. Still this simplified lighting equation must be evaluated at each pixel, since the Texture _Color term is different for consecutive pixels.
The derivative of a 3D Perlin noise function generates a random vector field, which can be used to perturb the object's normals. This method is known as bump-mapping and is implemented in off-line systems such as Pov-Ray. Bump-mapping calculates DNoise(x,y,z) = (DNx, DNy, DNz) and blends this vector with the object's normal at (x,y,z) to make the surface look bumpy.

Deriving a 3D vector field from a 2D noise function is difficult. However, a 2D noise function still provides enough randomness for special effects. The turbulence calculation, sine evaluation, and color blending in the marble algorithm discussed in Section 5.1 incorporate a lot of randomness, which was used to create two effects. The first one uses the color stored in the Marble table, while the second uses the turbulence.

For the first effect, the 16-bit color, which is the output of the texture procedure, is divided by 512. The result's fraction is multiplied by the interpolated 'specular' component and used in the lighting equation. At pixel P, having Texture_ColorP, the final color is calculated as follows:

  • Specular highlight approximation:
    SpecularP = a((N1 dot L)^n) + b((N2 dot L)^n) + c((N3 dot L)^n)
  • Random fraction from the texture
    fracP = Texture_ColorP / 512 - floor(Texture_ColorP / 512)
  • Perturbation of normals
    effectedP = SpecularP * fracP
  • Evaluation of the simplified lighting equation
    output = (Ka + Kds * effectedP ) * Texture_ColorP

For the second effect, the 8-9 bit turbulence value is divided by 64. The result's fraction is multiplied by the interpolated 'specular' component and used in the lighting equation. At pixel P, having turbP, the final color is calculated as follows:

  • Specular highlight approximation:
    SpecularP = a((N1 dot L)^n) + b((N2 dot L)^n) + c((N3 dot L)^n)
  • Random fraction from the texture
    fracP = turbP / 64 - floor(turbP / 64)
  • Perturbation of normals
    effectedP = SpecularP * fracP
  • Evaluation of the simplified lighting equation
    output = (Ka + Kds * effectedP) * Texture_ColorP

Figure 7.1

Figure 7.2

Figure 7.3

Figure 6.1: Regular lighted texturing

Figure 6.2: Lighting effect #1

Figure 6.3: Lighting effect #2

The above images show what is possible when using noise to perturb color and normals. Figure 7.1 is the normal lighted image. Figures 7.2 and 7.3 show what is possible when using the above techniques. Due to the C ANSI standard, when an application converts a number from floating point to integer, the number is truncated. On Pentium and Pentium II processors, this truncation is expensive because it involves changing the floating point control word. During the rendering process there are many places where ftol is called: in the polygon setup part and when converting the output of the lighting to rgb integer values. To save the extra cycles wasted on truncation, the fast_ftolprocedure presented here 'rounds to nearest'.

C declaration:

extern signed long fast_ftol(float d)


ASM implementation:

    result dd 0 ;(in the data section)
    PUBLIC _fast_ftol
    _TEXT SEGMENT
    _d$ = 4
    
    _fast_ftol PROC NEAR
        fld   DWORD PTR _d$[esp]
        fistp DWORD PTR result
        mov   eax , DWORD PTR result
        ret 0
    _fast_ftol ENDP
   
   _TEXT ENDS
Sometimes objects require a Z-buffer in the rendering process. A fixed point 16-bit representation for Z values enables MMX technology to process four data elements (words) in parallel. Using the "compare" instruction (instead of branches) prevents possible stalls after branch miss prediction on the Pentium and Pentium II processors.

Unlike a conventional texture mapping engine, as each new texture is developed and written in assembly, Z-Buffering becomes a problem. The programmer must incorporate optimized Z-Buffer code for each procedural texture developed. This is difficult and tedious to do, but there are two solutions to this problem. One is to come up with a standard Z-Buffer code template that can be slapped into the appropriate section of the texture mapping code. The other is to come up with a separate function callable by procedural texture mappers.

As with most engineering decisions, tradeoffs are involved. Integrating the Z-Buffer with each procedural texture function is clearly the fastest choice but requires more work from the developer.

The algorithm used for Z-Buffer integration is based from the application note 3D Z-Buffer Using MMX Technology. This algorithm removes the jump/compare per pixel typically needed.

The Z-Buffer integration can be broken up into four sections. The first is the initialization. The next section draws four 16 bit pixels at a time to the display. For the scan lines that are not multiples of four, the third section handles the initialization of registers that will be used to draw three or less end pixels. The last section draws these pixels.

Section #1 is the initialization section of the standard Z-Buffer code template. This part should be included outside of the main rasterization loop. Code is optimized to compute Z values for four 16 bit pixels at a time. Two 64 bit MMX registers are split up to accommodate four 32 bit Z-Buffer values. 16 bits are used for the integer part while 16 bits are used for the fractional part. For the Z-Buffer write to the depth surface, a 64 bit write accommodates four pixels at a time (this is because the 16 bit fractional part of each Z-value is discarded).

Variable definitions:
  • z_start: A 32 bit value containing the Z value of the first pixel in the scanline.
  • dz: A 32 bit value containing the Z incremental value of the scanline.
  • high_z: A 64 bit value containing the current two Z values for leftmost pixels. (Most significant DWORD)
  • low_z: A 64 bit value containing the current two Z values for the rightmost two pixels. (Least significant DWORD)
  • z_inc: A 64 bit value containing the two Z incremental values for each Z increment. Each Z incremental value is set to 4 * dz.

"z_start" and "dz" were two variables given to us in the beginning of the procedure. The following code segment shows how the variables "high_z", "low_z", and "z_inc" are calculated.


MOVD        MM0, z_start
MOVD        MM2, dz
PUNPCKLDQ   MM0, MM0
PSLLQ       MM2, 32
PADDD       MM0, MM2
MOVQ        low_z, MM0


PUNPCKHDQ   MM2, MM2
PSLLD       MM2, 1
PADDD       MM0, MM2
MOVQ        high_z, MM0


PSLLD       MM2, 1
MOVQ        z_inc, MM2

After initialization, the variables hold the following information:

Note: The following are what the values look like when stored in a register.
             |------- 32 bits ------|
             +---------------------------------------------+
MM0 = high_z = | z_start + 3dz        | z_start + 2dz        |
             +---------------------------------------------+


             |------- 32 bits ------|
             +---------------------------------------------+
MM1 = low_z  = | z_start + 1dz        | z_start              |
             +---------------------------------------------+


             |------- 32 bits ------|
             +---------------------------------------------+
MM2 = z_inc  = | 4dz                  | 4dz                  |
             +---------------------------------------------+
Once the memory write occurs, this is what the first 8 bytes will look like:
           |--- 16 bits ---|
           +---------------------------------------------------------------+
Z_Buffer = | z_start + 0dz | z_start + 1dz | z_start + 2dz | z_start + 3dz |
           +---------------------------------------------------------------+
Address     0             1|2             3|4             5|6             7

 

Section #2: After initialization, this section draws pixels in multiples of four.

PUSH        ESI
MOV         ESI, z_buffer  ;ESI = pointer to four Z values being looked at in Z-Buffer.


Get the new Z-Buffer values for the four pixels being drawn.
MOVQ        MM4, low_z     ;Move two rightmost Z-Buffer values into MM4
PSRAD       MM4, 16        ;Discard the fractional part of the two Z values
MOVQ        MM2, high_z    ;Move the leftmost Z-Buffer values into MM2
PSRAD       MM2, 16        ;Discard the fractional part of the two Z values
PACKSSDW    MM4, MM2       ;Mesh all four Z-Buffer values into one register


Update the four pixel screen values.
MOVQ        MM2, [ESI]     ;MM2 = the old Z values currently in the Z-Buffer.
PCMPGTW     MM2, MM4       ;Perform a compare between the old and the new Z values.
MOVQ        MM3, MM2       ;Save a copy of MM2 register.
PAND        MM1, MM2       ;MM1 = Colors of current pixel 4 pixels to be drawn.
PANDN       MM3, [EDI]     ;[EDI] = Pointer to existing 4 pixels in the screen buffer.
POR         MM1, MM3       ;"OR" old and new contents together for the 4 pixel colors.
MOVQ      [EDI], MM1       ;Write out the 4 pixels to video memory.


Update the four Z-Buffer values.
MOVQ        MM3, MM2       ;Save a copy of MM2 register.
PAND        MM2, MM4
PANDN       MM3, [ESI]     ;[ESI] = Pointer to existing 4 Z-Buffer values.
POR         MM2, MM3       ;"OR" old and new contents together for the 4 Z values.
MOVQ      [ESI], MM2       ;Update the Z-Buffer with the 4 new values.


Update "high_z" components.  This is Z = Z + Z_inc
MOVQ        MM0, z_inc         
PADDD       MM0, high_z
MOVQ        high_z, MM0    ;Add Delta_Z to the High Z components.


;Update "low_z" components.  This is Z = Z + Z_inc
MOVQ        MM0, z_in
PADDD       MM0, low_z
MOVQ        low_z, MM0     ;Add Delta_Z to the Low Z components.


;Update the Z-Buffer pointer by four pixels.
ADD         z_buffer, 8    ;z_buffer pointer is incremented eight bytes (4 pixels).


;Restore ESI
POP         ESI


 

Section #3: For the three or less pixels at the end of the scanline, the following code template can be used. This initializes certain registers and variables therefore shouldn't be put into the main loop. This part is used to point ESI to the Z-Buffer where the pixel write is going to occur. CX will contain the current Z-depth value.

MOVQ        MM2, low_z     ;We want the starting Z-Buffer value
PSRLD       MM2, 16        ;Truncate the 16 bit fractional part.
MOVD        ECX, MM2       ;Copy the Z-value to CX
MOV         ESI, z_buffer  ;ESI points to the Z-Buffer

 

Section #4: This section handles drawing the pixels and Z-Buffer update for the three or less pixels at the end of the scanline. This code is based on traditional Z-Buffering. A compare is made and a branch is taken depending on the results of the compare. The code is self-explanatory so no explanation will be given.

end_pixels:
CMP          CX, [ESI]     ;Compare new Z value against old value in Z-Buffer.
JGE         skip_pix       ;If new Z value is greater than old then skip the pixel write.


MOVD        EAX, MM3       ;Move the previous color to eax


MOV       [EDI], AX        ;Write 16 bit color to video buffer.
MOV       [ESI], CX        ;Write new Z value to Z-Buffer.


skip_pix:          
ADD         EDI, 2         ;Increment the pointer to the video buffer.
ADD         ESI, 2         ;Increment the pointer to the Z-Buffer.


PSRLQ       MM3, 16        ;Shift to the next color.
DEC         EDX            ;Decrement the end pixel counter.


JNZ         end_pixels     ;Repeat if there are more pixels to draw.

 

Programmer dilemmas:

  1. Aligned 64 bit writes. For the section that writes 64 bit values, the writes aren't aligned. Therefore a penalty will result for each unaligned write. Because this routine was written for procedural texturing, and the current procedural texturing algorithm requires greater than 120 clocks per four pixels, it was determined that the penalty is insignificant. In fact, the extra code needed to make all writes aligned will cancel out the amount of clocks saved.
  2. For code section #4, the Z value isn't incremented. This is because this section of the code is used to draw the last three or less pixels at the end of the scan line. The author assumes that the Z range cannot change drastically within three pixel lengths therefore a constant value can be used.
This routine is simpler to use but less efficient than technique #1. The Z-Buffer function takes as input the following:
  • A pointer to the screen buffer at the start of the scanline.
  • A pointer to the temporary scanline that will copied to the main drawing surface.
  • A pointer to the Z-Buffer where the start of the scanline is to be drawn.
  • Z-start
  • Z-increment
  • The length of the scanline

The function then runs through each of the pixels in the scanline and determines whether or not the pixel should be drawn based on the calculated Z-values. This allows the programmer to put any information into the off-screen scanline buffer. Then the Z-Buffer function writes pixels to the display depending on the Z-depth values.

The code template below is not optimized and shouldn't be used in a real application but is provided to help explain what is going on.


void z_buffer(unsigned __int16* screen_pointer,
			unsigned __int16* temp_buffer, 
			signed __int16* z_pointer, long z_start, long dz, 
			unsigned long num_pixels)
{
  unsigned long index;


  for(index = 0 ; index < num_pixels; index++)
  {
    if ((z_start  16) < *(z_pointer))
    {
      *(z_pointer) = (signed __int16)(z_start  16);
      *(screen_pointer) = temp_buffer[index];
    }
                        
    z_pointer++;
    screen_pointer++;
    z_start += dz;
  }
}

More optimized versions can be written by converting the above into assembly using aligned 64 bit writes with MMX technology. See Appendix E for a better full featured Z-Buffer scanline algorithm, fully optimized for the Pentium and Pentium II processors. The table below gives clock cycle information on the various code samples in this document. These results were obtained through Intel's VTune profiler utility.

Pentium Processor with MMX Technology Performance Measurements

Function Name % Pairing CPI Clocks Required to Draw 4 Pixels
Perspective Correct Code 84.21% 0.83 48 Clocks
Z-Buffering Integration, Part I 50.00% 2.0 22 Clocks
Z-Buffering Integration, Part II 92.86% 1.21 34 Clocks
Z-Buffering Integration, Part IV 80.0% 2.90 29 Clocks

 
Pentium Processor with MMX Technology Performance Measurements
Function Name % Pairing CPI Clocks per Pixel for Various Scanline Lengths
4 10 20 40 60 80 100 140 180 220
SIMD_Octave( ) 68.80% 0.72 39.50 30.30 32.10 31.18 31.04 30.71 30.62 30.51 30.46 30.42
SIMD_Wood_Linear 65.35% 0.72 17.00 14.20 10.80 10.03 9.77 9.64 9.56 9.47 9.42 9.39
SIMD_Wood_Sqrt 61.82% 0.74 21.25 19.10 14.85 14.05 13.78 13.65 13.57 13.48 13.43 13.40
SIMD_Marble( ) 64.63% 0.74 11.25 9.10 6.85 6.30 6.12 6.03 5.97 5.91 5.87 5.85

The procedures/code segments in the first table are meant to be called outside of the main rasterization loop. Therefore only the number of clocks required for one pass are given. These values are the amount of clock cycles required to calculate four pixel values. To find clks/pix, divide by four. Because these routines are called far less than others, memory stalls occur more often. This significantly drives up the clock/pixel ratio.

The second table lists routines located inside the main rasterization loop. Therefore, per-pixel clock cycles are given as a function of the length of a scanline (4, 10, 20, 40, 60, 80, 100, 140, 180, and 220 pixels).

Note, all measurements of a procedure start when first called and until (and including) the "ret" command. Measurement of the SIMD_Octave() function was with one octave of noise.

To figure out the total amount of clocks required for a procedural texture, follow this rule. First, start out by adding in the amount of clocks required for the SIMD_Octave() function. Multiply this by the number of octaves of noise used. Then add in the appropriate clock value for marble, wood, etc... This will give an approximate value of the performance to expect. Adding in Z-Buffering and texture perspective correction will increase the clock count as shown in the first table.

The table below gives clock cycle information for Pentium II processor, as measured using the PMONSTAT profiler utility.

Pentium II Processor Performance Measurements

Function Name

Clocks per Pixel for Various Scanline Lengths

4 10 20 40 60 80 100 140 180 220
SIMD_Octave( ) 37.00 32.80 31.14 30.07 30.46 30.35 30.28 30.20 30.15 30.12
SIMD_Wood_Linear( ) 15.75 11.75 10.55 9.90 9.68 9.57 9.49 9.41 9.37 9.34
SIMD_Wood_Sqrt( ) 19.50 15.50 14.30 13.65 13.43 13.32 13.26 13.18 13.14 13.11
SIMD_Marble( ) 10.50 7.50 6.50 6.00 5.83 5.75 5.70 5.64 5.61 5.59

This article and the earlier application note, Using MMX Instructions for Procedural Texture Mapping, present a new approach for implementing procedural textures using MMX technology. Using the Perlin noise function as a building block, wood, marble and grass textures were developed. Based on one octave of noise, marble takes 40 clocks, wood takes 44 clocks, while simple grass takes 30 clocks, as measured on the Pentium II processor. Perspective correction and z-buffering add more cycles.

Procedural texturing is an advanced rendering technique that requires more CPU time to produce a pixel than simpler techniques. Even a fast SIMD implementation of procedural textures may not produce the 30-60 frames per second required by future 3D applications. However, procedural textures have many advantages, such as low memory bandwidth, infinite resolution, and the ability to create many different natural textures based on a single noise function.

To demonstrate a possible usage of the procedural textures presented in this application note, the marble and wood code was integrated into a Mixed Rendering scheme, where a full-screen scene is rendered by two threads. The hardware thread uses a traditional rendering pipeline for the majority of the scene; the software thread renders a high-quality, small object using procedural textures. The outputs of both threads are combined to produce a single frame for the application.

For More Information

This application note extends an earlier application note, Using MMX Instructions for Procedural Texture Mapping. The original paper highlights the importance of the Perlin noise function. This application note extends the noise function to include fractional Brownian motion (fBm), and wood and marble textures. For each, the application note includes a description of the algorithm and its C and Assembly implementation. The paper also discusses ways to extend the fBm function to create other textures.

"Texturing and Modeling, A Procedural Approach", David S. Ebert, Editor. AP Professional, 1994


join | contact us | advertise | write | my profile
news | features | companies | jobs | resumes | education | product guide | projects | store



Copyright © 2003 CMP Media LLC

privacy policy
| terms of service