Texture Shading



 So, you've got your engine running, you have texture mapping, and some kind
 of shading in place, it runs fast, its smooth, but you feel somethings
 missing. You need to shade your texture maps. But you haven't got a clue
 how to do it! Well, I shall now explain the simple process of adding
 shading or transparency to your textures, in 256 colour mode.

 16*16 Linear Palette

 The easiest way to make primitive shading is to change your palette.
 Firstly, you will only be able to have 256 / n shades, where n is the
 number of colours you will be using. Secondly, the image quality is very
 poor. But, I'll explain it anyway, and you should at least implement it, if
 at least just to sharpen your coding skills. You'll find it leads on well
 to the better method...

 Ok, the idea here is that instead of having just one big lump of 256
 colours, you organise the colours into a table. Say a 16*16 table. Going
 down the rows you have increasing shade, and going across the table you
 have colours. Building this table is easy. You start at black for the first
 row, for every colour. Then, you fade up to the correct RGB in each
 successive row. So for colour 1:

 R, G, B = RGB values for the colour
 DeltaR, DeltaG, DeltaB = Amount to step at each row
 DeltaR = (R << 8) / numshades
 DeltaG = (B << 8) / numshades
 DeltaB = (B << 8) / numshades
 CurR, CurG, CurB = current red, green, blue
 CurR = CurG = CurB = 0

 for row = 0 to numshades - 1
         for colour = 0 to numcolours - 1
                 palette[row*16+colour].red = CurR[colour] >> 8
                 palette[row*16+colour].green = CurG[colour] >> 8
                 palette[row*16+colour].blue = CurB[colour] >> 8

                 CurR[colour] += DeltaR[colour]
                 CurG[colour] += DeltaG[colour]
                 CurB[colour] += DeltaB[colour]

 This is only pseudo-code. Coding this up is a 15-min maximum job. OK, now
 you've got that going, you need to light your textures. Again, this is
 easy. Simply do:

 PutPixel(x, y, shade*16+texture[v][u])

 Simple! Hold on to this idea of using a lookup table to get your colour,
 its a very important concept.


 Using A Seperate Look-up Table

 In the previous section, the lookup table was your palette. Now, we will
 create a seperate lookup table, which will let us do better shading, and
 improve the versatility of our code.

 Allocate a 256*256 block of memory. If you're still in real-mode, and can't
 allocate this much memory, stop for a minute, and ask yourself what the
 hell you're playing at? You're using a computer that has anyway between 4
 and 32 megs of RAM installed, and you're constricting yourself to 1 meg?
 Get yourself a new compiler! If you're a C/C++ man, get DJGPP or Watcom
 C++. If Pascal is your thing, go for TMT Pascal, or perhaps GNU Pascal
 under Linux. Finally, if you're a masochist, and use 100% assembly, get
 yourself DOS32, or Trans PMODE extender. Personally I prefer DOS32 and

 Okay, that block of memory is going to be used for a lookup table. As
 before, you're going to increasing shade with increasing row, and colour
 running across the columns:

           Colour -->
 Shade 0 |                      |
       1 |                      |
      ... ~~~~~~~~~~~~~~~~~~~~~~

     256 |                      |

 That really should have been a .gif picture, but what the hell, you can see
 what I mean. Now you'll need to fill that table. And this is the tricky,
 time consuming bit.


 Finding The Colour Of Best Fit

 We only have a finite number of colours in our palette. And to fully shade
 the texture, we would need 256*256 colours. So, we need to find the best
 fitting colour for a given shade. The way this is done is pretty simple.
 You need to find the 'distance' between the target colour, and the colour
 in the palette. Then return the colour with the minimum distance. Distance
 is found by :

 Dist = sqrt((TargetR - R) ^ 2 + (TargetG - G) ^ 2 + (TargetB - B) ^ 2)

 Set your best-fit distance variable to some high, impossible number, say a
 million. Loop for every colour, and if dist < best-dist, then set best-dist
 to dist, and best-colour to current-colour. If distance is 0, then just
 return the colour; you can't do better than a perfect fit. Otherwise, at
 the end of the loop, just return your best fitting colour. Your code should
 look something like :

 int BestColour(float r, float g, float b, char *palette)
         int             n;
         int             bestcol = 0;
         int             bestdist = 256000;
         float           dist, rdist, gdist, bdist;
         float           red, green, blue;

         for(n=0; n<256; n++) {
                 red   = (float) palette[0];
                 green = (float) palette[1];
                 blue  = (float) palette[2];
                 rdist = red   - r;
                 gdist = green - g;
                 bdist = blue  - b;
                 dist = sqrt(rdist * rdist +
                             gdist * gdist +
                             bdist * bdist);

                 if(dist <= bestdist) {
                         bestdist = dist;
                         bestcol = n;
                 if(dist == 0)
                         return n;
                 palette += 3;

         return bestcol;

 Its also a good idea to weight the rgb distances differently. Your eyes are
 generally more sensitive to the green band of colour, then red, then blue.
 So weight them perhaps:


 This usually improves quality a bit. Also, you can avoid the sqrt()
 function call, by just comparing the squared distance. I'm not sure if this
 works perfectly as a replacement, but it seems fine to me.


 Calculating The Colour For A Given Shade

 Calculating what colour to search for is simple enough. You can use any
 formulae you like for this, a phong lighting model, a transparency, a depth
 cue formula. The point is you need to calculate RGB for a given colour, and
 a given 'shade'.

 Phong Lighting Model

 The formula for this is:

 Ambient + Diffuse + Specular

 Which becomes:
 Ia*ka*Oa + I*(Od*kd*(N.L) + Os*ks*(R.V)^n)

 Ia is intensity of ambient
 Ka is ambient co-ef
 Oa is ambient colour. These are constant, set to whatever you like
 I is intensity of light. Say 1.0
 Od is colour for diffuse, replace with colour[n]
 Kd is diffuse co-efficient. About 0.95 - 1.0 is good
 (N.L) is angle of incidence, replace shade[n]
 Os is specular colour, say 255
 Ks is specular co-ef, say 0.75
 (R.V) is angle of reflection. Replace with spec[n]
 N is the shinyness. A value of around 20 looks fine.

 You'll need to calculate this for R G and B seperately. Make the N.L and
 (R.V)^n terms into lookup tables. Then search for the value in the palette,
 and you're done.

 Depth cueing

 A simple approximation can be done with


 Where k is shade/256. Its also a good idea to make the colour fade to
 white, instead of black, which gives the illusion of fogging.


 Transparency is slightly different. Instead of shade, you will have
 background-colour, and colour will be replaced with foreground colour.
 Formulae for this:

 background * transparency + foreground*(1.0 - transparency)

 Repeated for RGB. Its also worth considering the shape of the surface. For
 example, a sphere could be more transparent at the centre, becoming less
 transparent to the edges. Or you could fade transparencies. This will take
 quite a bit of memory though.

 Putting It All Together

 Now you need to put all this together. Your code might look something like:

 Precalculate as much as possible

 For every shade
         For every colour
                 Determine target colour
                 Colour = BestFit(target colour)
                 Store Colour

 Its a good idea to pre-calculate this, and store it to a file, because it
 can take quite a while to run through.

 Also, consider the alignment of such tables, and your texture maps. If they
 are aligned to 64k boundaries, we can improve our routine greatly. We can
 use the upper 16 bits of the address as the address, and the lower 16 bits
 as the index:

         mov     ebx, [pointer]
         mov     bl, [u]
         mov     bh, [v]

 Loading values into BH has the effect of an automatic multiplication by
 256. Remember to insert 3 instructions between loading BL/BH, and actually
 using them - or you will cause an AGI.

 Tom Hammersley, tomh@globalnet.co.uk

 [BACK] Back

Discuss this article in the forums

Date this article was posted to GameDev.net: 7/16/1999
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
Lighting and Shading

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!