Implementing Texture Lighting

The problem to solve here is to actually implement lighting on a texture. Basically, you have an intensity value from 0 to 1 for a polygon, and you need to apply that intensity value to the texture. A value of 1 leaves the texture unchanged, and values less than 1 modify the texture to be increasingly darker, all the way to an intensity of 0 that modifies the texture to be almost completely black. You can't simply multiply the intensity value by the 16-bit color value because the 16-bit color value is a packed data structure. Really, you'd have to extract the red, green, and blue values; then multiply each by the light intensity; and then convert them back to a 16-bit color value. That's a bit too much work to do for every pixel. Alternatively, you could limit the number of intensity values to, say, 64 and simply load 64 different versions of each texture, from dark to light. Then you could select the correct texture for each polygon depending on intensity value. This works and is fast, but it's a waste of memory. A 128x128 16-bit texture takes up 32K of memory, so if you had 64 versions, that would be 2MB! Obviously, this severely limits the number of textures you can have in memory. Another approach is, instead of having 64 versions of the texture, to have just 1 version of the texture and 64 versions of its color palette. You can do this by first requiring that all textures are 8-bit color; thus, each texture has its own small 256-color palette. Then, instead of creating 64 versions of the texture, you just create 64 versions of the palette. The basic idea is shown in a simplified version in Screenshot.

Screenshot To light a texture, keep a normal, full-lit copy of the texture and several versions of the texture's color palette, ranging from the normal palette to an almost completely dark palette.

Java graphics 08fig08.gif


If the palettes themselves represent 16-bit color, then having 64 256-color palettes creates an overhead of only 32K. For an 8-bit 128x128 texture, that means a total of just 80K of memory, much more conservative than the previous 2MB idea. To implement this concept, create the ShadedTexture class shown in Listing 8.7.

Listing 8.7 ShadedTexture.java
package com.brackeen.javagamebook.graphics3D.texture;
import java.awt.Color;
import java.awt.image.IndexColorModel;
/**
 The ShadedTexture class is a Texture that has multiple
 shades. The texture source image is stored as a 8-bit image
 with a palette for every shade.
*/
public final class ShadedTexture extends Texture {
 public static final int NUM_SHADE_LEVELS = 64;
 public static final int MAX_LEVEL = NUM_SHADE_LEVELS-1;
 private static final int PALETTE_SIZE_BITS = 8;
 private static final int PALETTE_SIZE = 1 << PALETTE_SIZE_BITS;
 private byte[] buffer;
 private IndexColorModel palette;
 private short[] shadeTable;
 private int defaultShadeLevel;
 private int widthBits;
 private int widthMask;
 private int heightBits;
 private int heightMask;
 /**
 Creates a new ShadedTexture from the specified 8-bit image
 buffer and palette. The width of the bitmap is 2 to the
 power of widthBits, or (1 << widthBits). Likewise, the
 height of the bitmap is 2 to the power of heightBits, or
 (1 << heightBits). The texture is shaded from its
 original color to black.
 */
 public ShadedTexture(byte[] buffer,
 int widthBits, int heightBits,
 IndexColorModel palette)
 {
 this(buffer, widthBits, heightBits, palette, Color.BLACK);
 }
 /**
 Creates a new ShadedTexture from the specified 8-bit image
 buffer, palette, and target shaded. The width of the
 bitmap is 2 to the power of widthBits, or (1 << widthBits).
 Likewise, the height of the bitmap is 2 to the power of
 heightBits, or (1 << heightBits). The texture is shaded
 from its original color to the target shade.
 */
 public ShadedTexture(byte[] buffer,
 int widthBits, int heightBits,
 IndexColorModel palette, Color targetShade)
 {
 super(1 << widthBits, 1 << heightBits);
 this.buffer = buffer;
 this.widthBits = widthBits;
 this.heightBits = heightBits;
 this.widthMask = getWidth() - 1;
 this.heightMask = getHeight() - 1;
 this.buffer = buffer;
 this.palette = palette;
 defaultShadeLevel = MAX_LEVEL;
 makeShadeTable(targetShade);
 }
 /**
 Creates the shade table for this ShadedTexture. Each entry
 in the palette is shaded from the original color to the
 specified target color.
 */
 public void makeShadeTable(Color targetShade) {
 shadeTable = new short[NUM_SHADE_LEVELS*PALETTE_SIZE];
 for (int level=0; level<NUM_SHADE_LEVELS; level++) {
 for (int i=0; i<palette.getMapSize(); i++) {
 int red = calcColor(palette.getRed(i),
 targetShade.getRed(), level);
 int green = calcColor(palette.getGreen(i),
 targetShade.getGreen(), level);
 int blue = calcColor(palette.getBlue(i),
 targetShade.getBlue(), level);
 int index = level * PALETTE_SIZE + i;
 // RGB 5:6:5
 shadeTable[index] = (short)(
 ((red >> 3) << 11) |
 ((green >> 2) << 5) |
 (blue >> 3));
 }
 }
 }
 private int calcColor(int palColor, int target, int level) {
 return (palColor - target) * (level+1) /
 NUM_SHADE_LEVELS + target;
 }
 /**
 Sets the default shade level that is used when getColor()
 is called.
 */
 public void setDefaultShadeLevel(int level) {
 defaultShadeLevel = level;
 }
 /**
 Gets the default shade level that is used when getColor()
 is called.
 */
 public int getDefaultShadeLevel() {
 return defaultShadeLevel;
 }
 /**
 Gets the 16-bit color of this Texture at the specified
 (x,y) location, using the default shade level.
 */
 public short getColor(int x, int y) {
 return getColor(x, y, defaultShadeLevel);
 }
 /**
 Gets the 16-bit color of this Texture at the specified
 (x,y) location, using the specified shade level.
 */
 public short getColor(int x, int y, int shadeLevel) {
 return shadeTable[(shadeLevel << PALETTE_SIZE_BITS) |
 (0xff & buffer[
 (x & widthMask) |
 ((y & heightMask) << widthBits)])];
 }
}


In the ShadedTexture class, the texture itself is stored in the buffer byte array. The byte array is useless by itself because it does not contain any color data; it contains only indices into a 256-color palette. Sixty-four versions of this 256-color palette are stored in the shadeTable array. The shade table is created in the makeShadeTable() method. In the getColor() method, the shadeTable is used to convert an index in the 8-bit buffer to a 16-bit color in the palette. Also, you'll notice that the shade table is created with a default shade value of black, so all the textures fade from the normal palette at 1 to black at 0. You could use other colors to create some other effects. Another idea is to create a spotlight effect by fading from the normal palette at 0 and full white at 1. Now let's try out the new shaded textures. Create a ShadedTexturedPolygonRenderer class (see Listing 8.8) to draw ShadedTextures, calculating the light intensity for each polygon on the fly.

Listing 8.8 ShadedTexturedPolygonRenderer.java
package com.brackeen.javagamebook.graphics3D;
import java.awt.*;
import java.awt.image.*;
import com.brackeen.javagamebook.math3D.*;
import com.brackeen.javagamebook.graphics3D.texture.*;
/**
 The ShadedTexturedPolygonRenderer class is a PolygonRenderer
 that renders ShadedTextured dynamically with one light source.
 By default, the ambient light intensity is 0.5 and there
 is no point light.
*/
public class ShadedTexturedPolygonRenderer
 extends FastTexturedPolygonRenderer
{
 private PointLight3D lightSource;
 private float ambientLightIntensity = 0.5f;
 private Vector3D directionToLight = new Vector3D();
 public ShadedTexturedPolygonRenderer(Transform3D camera,
 ViewWindow viewWindow)
 {
 this(camera, viewWindow, true);
 }
 public ShadedTexturedPolygonRenderer(Transform3D camera,
 ViewWindow viewWindow, boolean clearViewEveryFrame)
 {
 super(camera, viewWindow, clearViewEveryFrame);
 }
 /**
 Gets the light source for this renderer.
 */
 public PointLight3D getLightSource() {
 return lightSource;
 }
 /**
 Sets the light source for this renderer.
 */
 public void setLightSource(PointLight3D lightSource) {
 this.lightSource = lightSource;
 }
 /**
 Gets the ambient light intensity.
 */
 public float getAmbientLightIntensity() {
 return ambientLightIntensity;
 }
 /**
 Sets the ambient light intensity, generally between 0 and
 1.
 */
 public void setAmbientLightIntensity(float i) {
 ambientLightIntensity = i;
 }
 protected void drawCurrentPolygon(Graphics2D g) {
 // set the shade level of the polygon before drawing it
 if (sourcePolygon instanceof TexturedPolygon3D) {
 TexturedPolygon3D poly =
 ((TexturedPolygon3D)sourcePolygon);
 Texture texture = poly.getTexture();
 if (texture instanceof ShadedTexture) {
 calcShadeLevel();
 }
 }
 super.drawCurrentPolygon(g);
 }
 /**
 Calculates the shade level of the current polygon
 */
 private void calcShadeLevel() {
 TexturedPolygon3D poly = (TexturedPolygon3D)sourcePolygon;
 float intensity = 0;
 if (lightSource != null) {
 // average all the vertices in the polygon
 directionToLight.setTo(0,0,0);
 for (int i=0; i<poly.getNumVertices(); i++) {
 directionToLight.add(poly.getVertex(i));
 }
 directionToLight.divide(poly.getNumVertices());
 // make the vector from the average vertex
 // to the light
 directionToLight.subtract(lightSource);
 directionToLight.multiply(-1);
 // get the distance to the light for falloff
 float distance = directionToLight.length();
 // compute the diffuse reflect
 directionToLight.normalize();
 Vector3D normal = poly.getNormal();
 intensity = lightSource.getIntensity(distance)
 * directionToLight.getDotProduct(normal);
 intensity = Math.min(intensity, 1);
 intensity = Math.max(intensity, 0);
 }
 intensity+=ambientLightIntensity;
 intensity = Math.min(intensity, 1);
 intensity = Math.max(intensity, 0);
 int level =
 Math.round(intensity*ShadedTexture.MAX_LEVEL);
 ((ShadedTexture)poly.getTexture()).
 setDefaultShadeLevel(level);
 }
}


The ShadedTexturedPolygonRenderer class calculates the shade level for every polygon in the calcShadeLevel() method. This class is just a demonstration; really you'd want to recalculate the shade level for a polygon only if a light source changed or a polygon moved. Also, ShadedTexturedPolygonRenderer uses only one point light, and the ambient light intensity is the same for every polygon. Despite its simplicity, this class enables you to change the light intensity of the light source on the fly, which is a neat feature. You'll create the ShadingTest1 class to take advantage of the new renderer. A screenshot from ShadingTest1 is shown in Screenshot.

Screenshot ShadingTest1 gives each polygon a different shade based on a light source. The light source intensity can be dynamically changed.

Java graphics 08fig09.jpg


ShadingTest1 is just like TextureMapTest2, except that it uses ShadedTexturedPolygonRenderer and enables you to change the light intensity with the +/– keys.



   
Comments