|
/*
DEVELOPING GAME IN JAVA
Caracteristiques
Editeur : NEW RIDERS
Auteur : BRACKEEN
Parution : 09 2003
Pages : 972
Isbn : 1-59273-005-1
Reliure : Paperback
Disponibilite : Disponible a la librairie
*/
import java.awt.AWTException;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.DisplayMode;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class ShadingTest2 extends TextureMapTest2 {
public static void main(String[] args) {
new ShadingTest2().run();
}
private List lights;
private float ambientLightIntensity;
public void init() {
ambientLightIntensity = .05f;
lights = new ArrayList();
lights.add(new PointLight3D(-100, 100, -975, 1f, 500));
lights.add(new PointLight3D(50, 150, -700, 1f, 500));
lights.add(new PointLight3D(2000, 2000, -2000, .1f, -1));
lights.add(new PointLight3D(-250, 250, -1200, 1f, 500));
super.init(LOW_RES_MODES);
}
public void setTexture(TexturedPolygon3D poly, Texture texture) {
ShadedSurface.createShadedSurface(poly, (ShadedTexture) texture,
lights, ambientLightIntensity);
}
public Texture loadTexture(String imageName) {
return Texture.createTexture(imageName, true);
}
public void createPolygonRenderer() {
viewWindow = new ViewWindow(0, 0, screen.getWidth(),
screen.getHeight(), (float) Math.toRadians(75));
Transform3D camera = new Transform3D(0, 100, 0);
polygonRenderer = new ShadedSurfacePolygonRenderer(camera, viewWindow);
}
}
/**
* The ShadedSurfacePolygonRenderer is a PolygonRenderer that renders polygons
* with ShadedSurfaces. It keeps track of built surfaces, and clears any
* surfaces that weren't used in the last rendered frame to save memory.
*/
class ShadedSurfacePolygonRenderer extends FastTexturedPolygonRenderer {
private List builtSurfaces = new LinkedList();
public ShadedSurfacePolygonRenderer(Transform3D camera,
ViewWindow viewWindow) {
this(camera, viewWindow, true);
}
public ShadedSurfacePolygonRenderer(Transform3D camera,
ViewWindow viewWindow, boolean eraseView) {
super(camera, viewWindow, eraseView);
}
public void endFrame(Graphics2D g) {
super.endFrame(g);
// clear all built surfaces that weren't used this frame.
Iterator i = builtSurfaces.iterator();
while (i.hasNext()) {
ShadedSurface surface = (ShadedSurface) i.next();
if (surface.isDirty()) {
surface.clearSurface();
i.remove();
} else {
surface.setDirty(true);
}
}
}
protected void drawCurrentPolygon(Graphics2D g) {
buildSurface();
super.drawCurrentPolygon(g);
}
/**
* Builds the surface of the polygon if it has a ShadedSurface that is
* cleared.
*/
protected void buildSurface() {
// build surface, if needed
if (sourcePolygon instanceof TexturedPolygon3D) {
Texture texture = ((TexturedPolygon3D) sourcePolygon).getTexture();
if (texture instanceof ShadedSurface) {
ShadedSurface surface = (ShadedSurface) texture;
if (surface.isCleared()) {
surface.buildSurface();
builtSurfaces.add(surface);
}
surface.setDirty(false);
}
}
}
}
class TextureMapTest2 extends GameCore3D {
public static void main(String[] args) {
new TextureMapTest2().run();
}
public void init() {
init(LOW_RES_MODES);
}
// create a house (convex polyhedra)
public void createPolygons() {
// create Textures
Texture wall = loadTexture("../images/wall1.png");
Texture roof = loadTexture("../images/roof1.png");
TexturedPolygon3D poly;
// walls
poly = new TexturedPolygon3D(new Vector3D(-200, 250, -1000),
new Vector3D(-200, 0, -1000), new Vector3D(200, 0, -1000),
new Vector3D(200, 250, -1000));
setTexture(poly, wall);
polygons.add(poly);
poly = new TexturedPolygon3D(new Vector3D(200, 250, -1400),
new Vector3D(200, 0, -1400), new Vector3D(-200, 0, -1400),
new Vector3D(-200, 250, -1400));
setTexture(poly, wall);
polygons.add(poly);
poly = new TexturedPolygon3D(new Vector3D(-200, 250, -1400),
new Vector3D(-200, 0, -1400), new Vector3D(-200, 0, -1000),
new Vector3D(-200, 250, -1000));
setTexture(poly, wall);
polygons.add(poly);
poly = new TexturedPolygon3D(new Vector3D(200, 250, -1000),
new Vector3D(200, 0, -1000), new Vector3D(200, 0, -1400),
new Vector3D(200, 250, -1400));
setTexture(poly, wall);
polygons.add(poly);
// roof
poly = new TexturedPolygon3D(new Vector3D(-200, 250, -1000),
new Vector3D(200, 250, -1000), new Vector3D(75, 400, -1200),
new Vector3D(-75, 400, -1200));
setTexture(poly, roof);
polygons.add(poly);
poly = new TexturedPolygon3D(new Vector3D(-200, 250, -1400),
new Vector3D(-200, 250, -1000), new Vector3D(-75, 400, -1200));
setTexture(poly, roof);
polygons.add(poly);
poly = new TexturedPolygon3D(new Vector3D(200, 250, -1400),
new Vector3D(-200, 250, -1400), new Vector3D(-75, 400, -1200),
new Vector3D(75, 400, -1200));
setTexture(poly, roof);
polygons.add(poly);
poly = new TexturedPolygon3D(new Vector3D(200, 250, -1000),
new Vector3D(200, 250, -1400), new Vector3D(75, 400, -1200));
setTexture(poly, roof);
polygons.add(poly);
}
public void setTexture(TexturedPolygon3D poly, Texture texture) {
Vector3D origin = poly.getVertex(0);
Vector3D dv = new Vector3D(poly.getVertex(1));
dv.subtract(origin);
Vector3D du = new Vector3D();
du.setToCrossProduct(poly.getNormal(), dv);
Rectangle3D textureBounds = new Rectangle3D(origin, du, dv, texture
.getWidth(), texture.getHeight());
poly.setTexture(texture, textureBounds);
}
public Texture loadTexture(String imageName) {
return Texture.createTexture(imageName);
}
public void createPolygonRenderer() {
viewWindow = new ViewWindow(0, 0, screen.getWidth(),
screen.getHeight(), (float) Math.toRadians(75));
Transform3D camera = new Transform3D(0, 100, 0);
polygonRenderer = new FastTexturedPolygonRenderer(camera, viewWindow);
}
}
/**
* The FastTexturedPolygonRenderer is a PolygonRenderer that efficiently renders
* Textures.
*/
class FastTexturedPolygonRenderer extends PolygonRenderer {
public static final int SCALE_BITS = 12;
public static final int SCALE = 1 << SCALE_BITS;
public static final int INTERP_SIZE_BITS = 4;
public static final int INTERP_SIZE = 1 << INTERP_SIZE_BITS;
protected Vector3D a = new Vector3D();
protected Vector3D b = new Vector3D();
protected Vector3D c = new Vector3D();
protected Vector3D viewPos = new Vector3D();
protected BufferedImage doubleBuffer;
protected short[] doubleBufferData;
protected HashMap scanRenderers;
public FastTexturedPolygonRenderer(Transform3D camera, ViewWindow viewWindow) {
this(camera, viewWindow, true);
}
public FastTexturedPolygonRenderer(Transform3D camera,
ViewWindow viewWindow, boolean clearViewEveryFrame) {
super(camera, viewWindow, clearViewEveryFrame);
}
protected void init() {
destPolygon = new TexturedPolygon3D();
scanConverter = new ScanConverter(viewWindow);
// create renders for each texture (HotSpot optimization)
scanRenderers = new HashMap();
scanRenderers.put(PowerOf2Texture.class, new PowerOf2TextureRenderer());
scanRenderers.put(ShadedTexture.class, new ShadedTextureRenderer());
scanRenderers.put(ShadedSurface.class, new ShadedSurfaceRenderer());
}
public void startFrame(Graphics2D g) {
// initialize buffer
if (doubleBuffer == null
|| doubleBuffer.getWidth() != viewWindow.getWidth()
|| doubleBuffer.getHeight() != viewWindow.getHeight()) {
doubleBuffer = new BufferedImage(viewWindow.getWidth(), viewWindow
.getHeight(), BufferedImage.TYPE_USHORT_565_RGB);
//doubleBuffer = g.getDeviceConfiguration().createCompatibleImage(
//viewWindow.getWidth(), viewWindow.getHeight());
DataBuffer dest = doubleBuffer.getRaster().getDataBuffer();
doubleBufferData = ((DataBufferUShort) dest).getData();
}
// clear view
if (clearViewEveryFrame) {
for (int i = 0; i < doubleBufferData.length; i++) {
doubleBufferData[i] = 0;
}
}
}
public void endFrame(Graphics2D g) {
// draw the double buffer onto the screen
g.drawImage(doubleBuffer, viewWindow.getLeftOffset(), viewWindow
.getTopOffset(), null);
}
protected void drawCurrentPolygon(Graphics2D g) {
if (!(sourcePolygon instanceof TexturedPolygon3D)) {
// not a textured polygon - return
return;
}
TexturedPolygon3D poly = (TexturedPolygon3D) destPolygon;
Texture texture = poly.getTexture();
ScanRenderer scanRenderer = (ScanRenderer) scanRenderers.get(texture
.getClass());
scanRenderer.setTexture(texture);
Rectangle3D textureBounds = poly.getTextureBounds();
a.setToCrossProduct(textureBounds.getDirectionV(), textureBounds
.getOrigin());
b.setToCrossProduct(textureBounds.getOrigin(), textureBounds
.getDirectionU());
c.setToCrossProduct(textureBounds.getDirectionU(), textureBounds
.getDirectionV());
int y = scanConverter.getTopBoundary();
viewPos.y = viewWindow.convertFromScreenYToViewY(y);
viewPos.z = -viewWindow.getDistance();
while (y <= scanConverter.getBottomBoundary()) {
ScanConverter.Scan scan = scanConverter.getScan(y);
if (scan.isValid()) {
viewPos.x = viewWindow.convertFromScreenXToViewX(scan.left);
int offset = (y - viewWindow.getTopOffset())
* viewWindow.getWidth()
+ (scan.left - viewWindow.getLeftOffset());
scanRenderer.render(offset, scan.left, scan.right);
}
y++;
viewPos.y--;
}
}
/**
* The ScanRenderer class is an abstract inner class of
* FastTexturedPolygonRenderer that provides an interface for rendering a
* horizontal scan line.
*/
public abstract class ScanRenderer {
protected Texture currentTexture;
public void setTexture(Texture texture) {
this.currentTexture = texture;
}
public abstract void render(int offset, int left, int right);
}
//================================================
// FASTEST METHOD: no texture (for comparison)
//================================================
public class Method0 extends ScanRenderer {
public void render(int offset, int left, int right) {
for (int x = left; x <= right; x++) {
doubleBufferData[offset++] = (short) 0x0007;
}
}
}
//================================================
// METHOD 1: access pixel buffers directly
// and use textures sizes that are a power of 2
//================================================
public class Method1 extends ScanRenderer {
public void render(int offset, int left, int right) {
for (int x = left; x <= right; x++) {
int tx = (int) (a.getDotProduct(viewPos) / c
.getDotProduct(viewPos));
int ty = (int) (b.getDotProduct(viewPos) / c
.getDotProduct(viewPos));
doubleBufferData[offset++] = currentTexture.getColor(tx, ty);
viewPos.x++;
}
}
}
//================================================
// METHOD 2: avoid redundant calculations
//================================================
public class Method2 extends ScanRenderer {
public void render(int offset, int left, int right) {
float u = a.getDotProduct(viewPos);
float v = b.getDotProduct(viewPos);
float z = c.getDotProduct(viewPos);
float du = a.x;
float dv = b.x;
float dz = c.x;
for (int x = left; x <= right; x++) {
doubleBufferData[offset++] = currentTexture.getColor(
(int) (u / z), (int) (v / z));
u += du;
v += dv;
z += dz;
}
}
}
//================================================
// METHOD 3: use ints instead of floats
//================================================
public class Method3 extends ScanRenderer {
public void render(int offset, int left, int right) {
int u = (int) (SCALE * a.getDotProduct(viewPos));
int v = (int) (SCALE * b.getDotProduct(viewPos));
int z = (int) (SCALE * c.getDotProduct(viewPos));
int du = (int) (SCALE * a.x);
int dv = (int) (SCALE * b.x);
int dz = (int) (SCALE * c.x);
for (int x = left; x <= right; x++) {
doubleBufferData[offset++] = currentTexture.getColor(u / z, v
/ z);
u += du;
v += dv;
z += dz;
}
}
}
//================================================
// METHOD 4: reduce the number of divides
// (interpolate every 16 pixels)
// Also, apply a VM optimization by referring to
// the texture's class rather than it's parent class.
//================================================
// the following three ScanRenderers are the same, but refer
// to textures explicitly as either a PowerOf2Texture, a
// ShadedTexture, or a ShadedSurface.
// This allows HotSpot to do some inlining of the textures'
// getColor() method, which significantly increases
// performance.
public class PowerOf2TextureRenderer extends ScanRenderer {
public void render(int offset, int left, int right) {
PowerOf2Texture texture = (PowerOf2Texture) currentTexture;
float u = SCALE * a.getDotProduct(viewPos);
float v = SCALE * b.getDotProduct(viewPos);
float z = c.getDotProduct(viewPos);
float du = INTERP_SIZE * SCALE * a.x;
float dv = INTERP_SIZE * SCALE * b.x;
float dz = INTERP_SIZE * c.x;
int nextTx = (int) (u / z);
int nextTy = (int) (v / z);
int x = left;
while (x <= right) {
int tx = nextTx;
int ty = nextTy;
int maxLength = right - x + 1;
if (maxLength > INTERP_SIZE) {
u += du;
v += dv;
z += dz;
nextTx = (int) (u / z);
nextTy = (int) (v / z);
int dtx = (nextTx - tx) >> INTERP_SIZE_BITS;
int dty = (nextTy - ty) >> INTERP_SIZE_BITS;
int endOffset = offset + INTERP_SIZE;
while (offset < endOffset) {
doubleBufferData[offset++] = texture.getColor(
tx >> SCALE_BITS, ty >> SCALE_BITS);
tx += dtx;
ty += dty;
}
x += INTERP_SIZE;
} else {
// variable interpolation size
int interpSize = maxLength;
u += interpSize * SCALE * a.x;
v += interpSize * SCALE * b.x;
z += interpSize * c.x;
nextTx = (int) (u / z);
nextTy = (int) (v / z);
int dtx = (nextTx - tx) / interpSize;
int dty = (nextTy - ty) / interpSize;
int endOffset = offset + interpSize;
while (offset < endOffset) {
doubleBufferData[offset++] = texture.getColor(
tx >> SCALE_BITS, ty >> SCALE_BITS);
tx += dtx;
ty += dty;
}
x += interpSize;
}
}
}
}
public class ShadedTextureRenderer extends ScanRenderer {
public void render(int offset, int left, int right) {
ShadedTexture texture = (ShadedTexture) currentTexture;
float u = SCALE * a.getDotProduct(viewPos);
float v = SCALE * b.getDotProduct(viewPos);
float z = c.getDotProduct(viewPos);
float du = INTERP_SIZE * SCALE * a.x;
float dv = INTERP_SIZE * SCALE * b.x;
float dz = INTERP_SIZE * c.x;
int nextTx = (int) (u / z);
int nextTy = (int) (v / z);
int x = left;
while (x <= right) {
int tx = nextTx;
int ty = nextTy;
int maxLength = right - x + 1;
if (maxLength > INTERP_SIZE) {
u += du;
v += dv;
z += dz;
nextTx = (int) (u / z);
nextTy = (int) (v / z);
int dtx = (nextTx - tx) >> INTERP_SIZE_BITS;
int dty = (nextTy - ty) >> INTERP_SIZE_BITS;
int endOffset = offset + INTERP_SIZE;
while (offset < endOffset) {
doubleBufferData[offset++] = texture.getColor(
tx >> SCALE_BITS, ty >> SCALE_BITS);
tx += dtx;
ty += dty;
}
x += INTERP_SIZE;
} else {
// variable interpolation size
int interpSize = maxLength;
u += interpSize * SCALE * a.x;
v += interpSize * SCALE * b.x;
z += interpSize * c.x;
nextTx = (int) (u / z);
nextTy = (int) (v / z);
int dtx = (nextTx - tx) / interpSize;
int dty = (nextTy - ty) / interpSize;
int endOffset = offset + interpSize;
while (offset < endOffset) {
doubleBufferData[offset++] = texture.getColor(
tx >> SCALE_BITS, ty >> SCALE_BITS);
tx += dtx;
ty += dty;
}
x += interpSize;
}
}
}
}
public class ShadedSurfaceRenderer extends ScanRenderer {
public int checkBounds(int vScaled, int bounds) {
int v = vScaled >> SCALE_BITS;
if (v < 0) {
vScaled = 0;
} else if (v >= bounds) {
vScaled = (bounds - 1) << SCALE_BITS;
}
return vScaled;
}
public void render(int offset, int left, int right) {
ShadedSurface texture = (ShadedSurface) currentTexture;
float u = SCALE * a.getDotProduct(viewPos);
float v = SCALE * b.getDotProduct(viewPos);
float z = c.getDotProduct(viewPos);
float du = INTERP_SIZE * SCALE * a.x;
float dv = INTERP_SIZE * SCALE * b.x;
float dz = INTERP_SIZE * c.x;
int nextTx = (int) (u / z);
int nextTy = (int) (v / z);
int x = left;
while (x <= right) {
int tx = nextTx;
int ty = nextTy;
int maxLength = right - x + 1;
if (maxLength > INTERP_SIZE) {
u += du;
v += dv;
z += dz;
nextTx = (int) (u / z);
nextTy = (int) (v / z);
int dtx = (nextTx - tx) >> INTERP_SIZE_BITS;
int dty = (nextTy - ty) >> INTERP_SIZE_BITS;
int endOffset = offset + INTERP_SIZE;
while (offset < endOffset) {
doubleBufferData[offset++] = texture.getColor(
tx >> SCALE_BITS, ty >> SCALE_BITS);
tx += dtx;
ty += dty;
}
x += INTERP_SIZE;
} else {
// variable interpolation size
int interpSize = maxLength;
u += interpSize * SCALE * a.x;
v += interpSize * SCALE * b.x;
z += interpSize * c.x;
nextTx = (int) (u / z);
nextTy = (int) (v / z);
// make sure tx, ty, nextTx, and nextTy are
// all within bounds
tx = checkBounds(tx, texture.getWidth());
ty = checkBounds(ty, texture.getHeight());
nextTx = checkBounds(nextTx, texture.getWidth());
nextTy = checkBounds(nextTy, texture.getHeight());
int dtx = (nextTx - tx) / interpSize;
int dty = (nextTy - ty) / interpSize;
int endOffset = offset + interpSize;
while (offset < endOffset) {
doubleBufferData[offset++] = texture.getColor(
tx >> SCALE_BITS, ty >> SCALE_BITS);
tx += dtx;
ty += dty;
}
x += interpSize;
}
}
}
}
}
/**
* A ShadedSurface is a pre-shaded Texture that maps onto a polygon.
*/
final class ShadedSurface extends Texture {
public static final int SURFACE_BORDER_SIZE = 1;
public static final int SHADE_RES_BITS = 4;
public static final int SHADE_RES = 1 << SHADE_RES_BITS;
public static final int SHADE_RES_MASK = SHADE_RES - 1;
public static final int SHADE_RES_SQ = SHADE_RES * SHADE_RES;
public static final int SHADE_RES_SQ_BITS = SHADE_RES_BITS * 2;
private short[] buffer;
private SoftReference bufferReference;
private boolean dirty;
private ShadedTexture sourceTexture;
private Rectangle3D sourceTextureBounds;
private Rectangle3D surfaceBounds;
private byte[] shadeMap;
private int shadeMapWidth;
private int shadeMapHeight;
// for incrementally calculating shade values
private int shadeValue;
private int shadeValueInc;
/**
* Creates a ShadedSurface with the specified width and height.
*/
public ShadedSurface(int width, int height) {
this(null, width, height);
}
/**
* Creates a ShadedSurface with the specified buffer, width and height.
*/
public ShadedSurface(short[] buffer, int width, int height) {
super(width, height);
this.buffer = buffer;
bufferReference = new SoftReference(buffer);
sourceTextureBounds = new Rectangle3D();
dirty = true;
}
/**
* Creates a ShadedSurface for the specified polygon. The shade map is
* created from the specified list of point lights and ambient light
* intensity.
*/
public static void createShadedSurface(TexturedPolygon3D poly,
ShadedTexture texture, List lights, float ambientLightIntensity) {
// create the texture bounds
Vector3D origin = poly.getVertex(0);
Vector3D dv = new Vector3D(poly.getVertex(1));
dv.subtract(origin);
Vector3D du = new Vector3D();
du.setToCrossProduct(poly.getNormal(), dv);
Rectangle3D bounds = new Rectangle3D(origin, du, dv,
texture.getWidth(), texture.getHeight());
createShadedSurface(poly, texture, bounds, lights,
ambientLightIntensity);
}
/**
* Creates a ShadedSurface for the specified polygon. The shade map is
* created from the specified list of point lights and ambient light
* intensity.
*/
public static void createShadedSurface(TexturedPolygon3D poly,
ShadedTexture texture, Rectangle3D textureBounds, List lights,
float ambientLightIntensity) {
// create the surface bounds
poly.setTexture(texture, textureBounds);
Rectangle3D surfaceBounds = poly.calcBoundingRectangle();
// give the surfaceBounds a border to correct for
// slight errors when texture mapping
Vector3D du = new Vector3D(surfaceBounds.getDirectionU());
Vector3D dv = new Vector3D(surfaceBounds.getDirectionV());
du.multiply(SURFACE_BORDER_SIZE);
dv.multiply(SURFACE_BORDER_SIZE);
surfaceBounds.getOrigin().subtract(du);
surfaceBounds.getOrigin().subtract(dv);
int width = (int) Math.ceil(surfaceBounds.getWidth()
+ SURFACE_BORDER_SIZE * 2);
int height = (int) Math.ceil(surfaceBounds.getHeight()
+ SURFACE_BORDER_SIZE * 2);
surfaceBounds.setWidth(width);
surfaceBounds.setHeight(height);
// create the shaded surface texture
ShadedSurface surface = new ShadedSurface(width, height);
surface.setTexture(texture, textureBounds);
surface.setSurfaceBounds(surfaceBounds);
// create the surface's shade map
surface.buildShadeMap(lights, ambientLightIntensity);
// set the polygon's surface
poly.setTexture(surface, surfaceBounds);
}
/**
* Gets the 16-bit color of the pixel at location (x,y) in the bitmap. The x
* and y values are assumbed to be within the bounds of the surface;
* otherwise an ArrayIndexOutOfBoundsException occurs.
*/
public short getColor(int x, int y) {
//try {
return buffer[x + y * width];
//}
//catch (ArrayIndexOutOfBoundsException ex) {
// return -2048;
//}
}
/**
* Gets the 16-bit color of the pixel at location (x,y) in the bitmap. The x
* and y values are checked to be within the bounds of the surface, and if
* not, the pixel on the edge of the texture is returned.
*/
public short getColorChecked(int x, int y) {
if (x < 0) {
x = 0;
} else if (x >= width) {
x = width - 1;
}
if (y < 0) {
y = 0;
} else if (y >= height) {
y = height - 1;
}
return getColor(x, y);
}
/**
* Marks whether this surface is dirty. Surfaces marked as dirty may be
* cleared externally.
*/
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
/**
* Checks wether this surface is dirty. Surfaces marked as dirty may be
* cleared externally.
*/
public boolean isDirty() {
return dirty;
}
/**
* Creates a new surface and add a SoftReference to it.
*/
protected void newSurface(int width, int height) {
buffer = new short[width * height];
bufferReference = new SoftReference(buffer);
}
/**
* Clears this surface, allowing the garbage collector to remove it from
* memory if needed.
*/
public void clearSurface() {
buffer = null;
}
/**
* Checks if the surface has been cleared.
*/
public boolean isCleared() {
return (buffer == null);
}
/**
* If the buffer has been previously built and cleared but not yet removed
* from memory by the garbage collector, then this method attempts to
* retrieve it. Returns true if successfull.
*/
public boolean retrieveSurface() {
if (buffer == null) {
buffer = (short[]) bufferReference.get();
}
return !(buffer == null);
}
/**
* Sets the source texture for this ShadedSurface.
*/
public void setTexture(ShadedTexture texture) {
this.sourceTexture = texture;
sourceTextureBounds.setWidth(texture.getWidth());
sourceTextureBounds.setHeight(texture.getHeight());
}
/**
* Sets the source texture and source bounds for this ShadedSurface.
*/
public void setTexture(ShadedTexture texture, Rectangle3D bounds) {
setTexture(texture);
sourceTextureBounds.setTo(bounds);
}
/**
* Sets the surface bounds for this ShadedSurface.
*/
public void setSurfaceBounds(Rectangle3D surfaceBounds) {
this.surfaceBounds = surfaceBounds;
}
/**
* Gets the surface bounds for this ShadedSurface.
*/
public Rectangle3D getSurfaceBounds() {
return surfaceBounds;
}
/**
* Builds the surface. First, this method calls retrieveSurface() to see if
* the surface needs to be rebuilt. If not, the surface is built by tiling
* the source texture and apply the shade map.
*/
public void buildSurface() {
if (retrieveSurface()) {
return;
}
int width = (int) surfaceBounds.getWidth();
int height = (int) surfaceBounds.getHeight();
// create a new surface (buffer)
newSurface(width, height);
// builds the surface.
// assume surface bounds and texture bounds are aligned
// (possibly with different origins)
Vector3D origin = sourceTextureBounds.getOrigin();
Vector3D directionU = sourceTextureBounds.getDirectionU();
Vector3D directionV = sourceTextureBounds.getDirectionV();
Vector3D d = new Vector3D(surfaceBounds.getOrigin());
d.subtract(origin);
int startU = (int) ((d.getDotProduct(directionU) - SURFACE_BORDER_SIZE));
int startV = (int) ((d.getDotProduct(directionV) - SURFACE_BORDER_SIZE));
int offset = 0;
int shadeMapOffsetU = SHADE_RES - SURFACE_BORDER_SIZE - startU;
int shadeMapOffsetV = SHADE_RES - SURFACE_BORDER_SIZE - startV;
for (int v = startV; v < startV + height; v++) {
sourceTexture.setCurrRow(v);
int u = startU;
int amount = SURFACE_BORDER_SIZE;
while (u < startU + width) {
getInterpolatedShade(u + shadeMapOffsetU, v + shadeMapOffsetV);
// keep drawing until we need to recalculate
// the interpolated shade. (every SHADE_RES pixels)
int endU = Math.min(startU + width, u + amount);
while (u < endU) {
buffer[offset++] = sourceTexture.getColorCurrRow(u,
shadeValue >> SHADE_RES_SQ_BITS);
shadeValue += shadeValueInc;
u++;
}
amount = SHADE_RES;
}
}
// if the surface bounds is not aligned with the texture
// bounds, use this (slower) code.
/*
* Vector3D origin = sourceTextureBounds.getOrigin(); Vector3D
* directionU = sourceTextureBounds.getDirectionU(); Vector3D directionV =
* sourceTextureBounds.getDirectionV();
*
* Vector3D d = new Vector3D(surfaceBounds.getOrigin());
* d.subtract(origin); int initTextureU = (int)(SCALE *
* (d.getDotProduct(directionU) - SURFACE_BORDER_SIZE)); int
* initTextureV = (int)(SCALE * (d.getDotProduct(directionV) -
* SURFACE_BORDER_SIZE)); int textureDu1 = (int)(SCALE *
* directionU.getDotProduct( surfaceBounds.getDirectionV())); int
* textureDv1 = (int)(SCALE * directionV.getDotProduct(
* surfaceBounds.getDirectionV())); int textureDu2 = (int)(SCALE *
* directionU.getDotProduct( surfaceBounds.getDirectionU())); int
* textureDv2 = (int)(SCALE * directionV.getDotProduct(
* surfaceBounds.getDirectionU()));
*
* int shadeMapOffset = SHADE_RES - SURFACE_BORDER_SIZE;
*
* for (int v=0; v <height; v++) { int textureU = initTextureU; int
* textureV = initTextureV;
*
* for (int u=0; u <width; u++) { if (((u + shadeMapOffset) &
* SHADE_RES_MASK) == 0) { getInterpolatedShade(u + shadeMapOffset, v +
* shadeMapOffset); } buffer[offset++] = sourceTexture.getColor(
* textureU >> SCALE_BITS, textureV >> SCALE_BITS, shadeValue >>
* SHADE_RES_SQ_BITS); textureU+=textureDu2; textureV+=textureDv2;
* shadeValue+=shadeValueInc; } initTextureU+=textureDu1;
* initTextureV+=textureDv1; }
*/
}
/**
* Gets the shade (from the shade map) for the specified (u,v) location. The
* u and v values should be left-shifted by SHADE_RES_BITS, and the extra
* bits are used to interpolate between values. For an interpolation
* example, a location halfway between shade values 1 and 3 would return 2.
*/
public int getInterpolatedShade(int u, int v) {
int fracU = u & SHADE_RES_MASK;
int fracV = v & SHADE_RES_MASK;
int offset = (u >> SHADE_RES_BITS)
+ ((v >> SHADE_RES_BITS) * shadeMapWidth);
int shade00 = (SHADE_RES - fracV) * shadeMap[offset];
int shade01 = fracV * shadeMap[offset + shadeMapWidth];
int shade10 = (SHADE_RES - fracV) * shadeMap[offset + 1];
int shade11 = fracV * shadeMap[offset + shadeMapWidth + 1];
shadeValue = SHADE_RES_SQ / 2 + (SHADE_RES - fracU) * shade00
+ (SHADE_RES - fracU) * shade01 + fracU * shade10 + fracU
* shade11;
// the value to increment as u increments
shadeValueInc = -shade00 - shade01 + shade10 + shade11;
return shadeValue >> SHADE_RES_SQ_BITS;
}
/**
* Gets the shade (from the built shade map) for the specified (u,v)
* location.
*/
public int getShade(int u, int v) {
return shadeMap[u + v * shadeMapWidth];
}
/**
* Builds the shade map for this surface from the specified list of point
* lights and the ambiant light intensity.
*/
public void buildShadeMap(List pointLights, float ambientLightIntensity) {
Vector3D surfaceNormal = surfaceBounds.getNormal();
int polyWidth = (int) surfaceBounds.getWidth() - SURFACE_BORDER_SIZE
* 2;
int polyHeight = (int) surfaceBounds.getHeight() - SURFACE_BORDER_SIZE
* 2;
// assume SURFACE_BORDER_SIZE is <= SHADE_RES
shadeMapWidth = polyWidth / SHADE_RES + 4;
shadeMapHeight = polyHeight / SHADE_RES + 4;
shadeMap = new byte[shadeMapWidth * shadeMapHeight];
// calculate the shade map origin
Vector3D origin = new Vector3D(surfaceBounds.getOrigin());
Vector3D du = new Vector3D(surfaceBounds.getDirectionU());
Vector3D dv = new Vector3D(surfaceBounds.getDirectionV());
du.multiply(SHADE_RES - SURFACE_BORDER_SIZE);
dv.multiply(SHADE_RES - SURFACE_BORDER_SIZE);
origin.subtract(du);
origin.subtract(dv);
// calculate the shade for each sample point.
Vector3D point = new Vector3D();
du.setTo(surfaceBounds.getDirectionU());
dv.setTo(surfaceBounds.getDirectionV());
du.multiply(SHADE_RES);
dv.multiply(SHADE_RES);
for (int v = 0; v < shadeMapHeight; v++) {
point.setTo(origin);
for (int u = 0; u < shadeMapWidth; u++) {
shadeMap[u + v * shadeMapWidth] = calcShade(surfaceNormal,
point, pointLights, ambientLightIntensity);
point.add(du);
}
origin.add(dv);
}
}
/**
* Determine the shade of a point on the polygon. This computes the
* Lambertian reflection for a point on the plane. Each point light has an
* intensity and a distance falloff value, but no specular reflection or
* shadows from other polygons are computed. The value returned is from 0 to
* ShadedTexture.MAX_LEVEL.
*/
protected byte calcShade(Vector3D normal, Vector3D point, List pointLights,
float ambientLightIntensity) {
float intensity = 0;
Vector3D directionToLight = new Vector3D();
for (int i = 0; i < pointLights.size(); i++) {
PointLight3D light = (PointLight3D) pointLights.get(i);
directionToLight.setTo(light);
directionToLight.subtract(point);
float distance = directionToLight.length();
directionToLight.normalize();
float lightIntensity = light.getIntensity(distance)
* directionToLight.getDotProduct(normal);
lightIntensity = Math.min(lightIntensity, 1);
lightIntensity = Math.max(lightIntensity, 0);
intensity += lightIntensity;
}
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);
return (byte) level;
}
}
/**
* A PointLight3D is a point light that has an intensity (between 0 and 1) and
* optionally a distance falloff value, which causes the light to diminish with
* distance.
*/
class PointLight3D extends Vector3D {
public static final float NO_DISTANCE_FALLOFF = -1;
private float intensity;
private float distanceFalloff;
/**
* Creates a new PointLight3D at (0,0,0) with an intensity of 1 and no
* distance falloff.
*/
public PointLight3D() {
this(0, 0, 0, 1, NO_DISTANCE_FALLOFF);
}
/**
* Creates a copy of the specified PointLight3D.
*/
public PointLight3D(PointLight3D p) {
setTo(p);
}
/**
* Creates a new PointLight3D with the specified location and intensity. The
* created light has no distance falloff.
*/
public PointLight3D(float x, float y, float z, float intensity) {
this(x, y, z, intensity, NO_DISTANCE_FALLOFF);
}
/**
* Creates a new PointLight3D with the specified location. intensity, and no
* distance falloff.
*/
public PointLight3D(float x, float y, float z, float intensity,
float distanceFalloff) {
setTo(x, y, z);
setIntensity(intensity);
setDistanceFalloff(distanceFalloff);
}
/**
* Sets this PointLight3D to the same location, intensity, and distance
* falloff as the specified PointLight3D.
*/
public void setTo(PointLight3D p) {
setTo(p.x, p.y, p.z);
setIntensity(p.getIntensity());
setDistanceFalloff(p.getDistanceFalloff());
}
/**
* Gets the intensity of this light from the specified distance.
*/
public float getIntensity(float distance) {
if (distanceFalloff == NO_DISTANCE_FALLOFF) {
return intensity;
} else if (distance >= distanceFalloff) {
return 0;
} else {
return intensity * (distanceFalloff - distance)
/ (distanceFalloff + distance);
}
}
/**
* Gets the intensity of this light.
*/
public float getIntensity() {
return intensity;
}
/**
* Sets the intensity of this light.
*/
public void setIntensity(float intensity) {
this.intensity = intensity;
}
/**
* Gets the distances falloff value. The light intensity is zero beyond this
* distance.
*/
public float getDistanceFalloff() {
return distanceFalloff;
}
/**
* Sets the distances falloff value. The light intensity is zero beyond this
* distance. Set to NO_DISTANCE_FALLOFF if the light does not diminish with
* distance.
*/
public void setDistanceFalloff(float distanceFalloff) {
this.distanceFalloff = distanceFalloff;
}
}
/**
* The Transform3D class represents a rotation and translation.
*/
class Transform3D {
protected Vector3D location;
private float cosAngleX;
private float sinAngleX;
private float cosAngleY;
private float sinAngleY;
private float cosAngleZ;
private float sinAngleZ;
/**
* Creates a new Transform3D with no translation or rotation.
*/
public Transform3D() {
this(0, 0, 0);
}
/**
* Creates a new Transform3D with the specified translation and no rotation.
*/
public Transform3D(float x, float y, float z) {
location = new Vector3D(x, y, z);
setAngle(0, 0, 0);
}
/**
* Creates a new Transform3D
*/
public Transform3D(Transform3D v) {
location = new Vector3D();
setTo(v);
}
public Object clone() {
return new Transform3D(this);
}
/**
* Sets this Transform3D to the specified Transform3D.
*/
public void setTo(Transform3D v) {
location.setTo(v.location);
this.cosAngleX = v.cosAngleX;
this.sinAngleX = v.sinAngleX;
this.cosAngleY = v.cosAngleY;
this.sinAngleY = v.sinAngleY;
this.cosAngleZ = v.cosAngleZ;
this.sinAngleZ = v.sinAngleZ;
}
/**
* Gets the location (translation) of this transform.
*/
public Vector3D getLocation() {
return location;
}
public float getCosAngleX() {
return cosAngleX;
}
public float getSinAngleX() {
return sinAngleX;
}
public float getCosAngleY() {
return cosAngleY;
}
public float getSinAngleY() {
return sinAngleY;
}
public float getCosAngleZ() {
return cosAngleZ;
}
public float getSinAngleZ() {
return sinAngleZ;
}
public float getAngleX() {
return (float) Math.atan2(sinAngleX, cosAngleX);
}
public float getAngleY() {
return (float) Math.atan2(sinAngleY, cosAngleY);
}
public float getAngleZ() {
return (float) Math.atan2(sinAngleZ, cosAngleZ);
}
public void setAngleX(float angleX) {
cosAngleX = (float) Math.cos(angleX);
sinAngleX = (float) Math.sin(angleX);
}
public void setAngleY(float angleY) {
cosAngleY = (float) Math.cos(angleY);
sinAngleY = (float) Math.sin(angleY);
}
public void setAngleZ(float angleZ) {
cosAngleZ = (float) Math.cos(angleZ);
sinAngleZ = (float) Math.sin(angleZ);
}
public void setAngle(float angleX, float angleY, float angleZ) {
setAngleX(angleX);
setAngleY(angleY);
setAngleZ(angleZ);
}
public void rotateAngleX(float angle) {
if (angle != 0) {
setAngleX(getAngleX() + angle);
}
}
public void rotateAngleY(float angle) {
if (angle != 0) {
setAngleY(getAngleY() + angle);
}
}
public void rotateAngleZ(float angle) {
if (angle != 0) {
setAngleZ(getAngleZ() + angle);
}
}
public void rotateAngle(float angleX, float angleY, float angleZ) {
rotateAngleX(angleX);
rotateAngleY(angleY);
rotateAngleZ(angleZ);
}
}
/**
* The ViewWindow class represents the geometry of a view window for 3D viewing.
*/
class ViewWindow {
private Rectangle bounds;
private float angle;
private float distanceToCamera;
/**
* Creates a new ViewWindow with the specified bounds on the screen and
* horizontal view angle.
*/
public ViewWindow(int left, int top, int width, int height, float angle) {
bounds = new Rectangle();
this.angle = angle;
setBounds(left, top, width, height);
}
/**
* Sets the bounds for this ViewWindow on the screen.
*/
public void setBounds(int left, int top, int width, int height) {
bounds.x = left;
bounds.y = top;
bounds.width = width;
bounds.height = height;
distanceToCamera = (bounds.width / 2) / (float) Math.tan(angle / 2);
}
/**
* Sets the horizontal view angle for this ViewWindow.
*/
public void setAngle(float angle) {
this.angle = angle;
distanceToCamera = (bounds.width / 2) / (float) Math.tan(angle / 2);
}
/**
* Gets the horizontal view angle of this view window.
*/
public float getAngle() {
return angle;
}
/**
* Gets the width of this view window.
*/
public int getWidth() {
return bounds.width;
}
/**
* Gets the height of this view window.
*/
public int getHeight() {
return bounds.height;
}
/**
* Gets the y offset of this view window on the screen.
*/
public int getTopOffset() {
return bounds.y;
}
/**
* Gets the x offset of this view window on the screen.
*/
public int getLeftOffset() {
return bounds.x;
}
/**
* Gets the distance from the camera to to this view window.
*/
public float getDistance() {
return distanceToCamera;
}
/**
* Converts an x coordinate on this view window to the corresponding x
* coordinate on the screen.
*/
public float convertFromViewXToScreenX(float x) {
return x + bounds.x + bounds.width / 2;
}
/**
* Converts a y coordinate on this view window to the corresponding y
* coordinate on the screen.
*/
public float convertFromViewYToScreenY(float y) {
return -y + bounds.y + bounds.height / 2;
}
/**
* Converts an x coordinate on the screen to the corresponding x coordinate
* on this view window.
*/
public float convertFromScreenXToViewX(float x) {
return x - bounds.x - bounds.width / 2;
}
/**
* Converts an y coordinate on the screen to the corresponding y coordinate
* on this view window.
*/
public float convertFromScreenYToViewY(float y) {
return -y + bounds.y + bounds.height / 2;
}
/**
* Projects the specified vector to the screen.
*/
public void project(Vector3D v) {
// project to view window
v.x = distanceToCamera * v.x / -v.z;
v.y = distanceToCamera * v.y / -v.z;
// convert to screen coordinates
v.x = convertFromViewXToScreenX(v.x);
v.y = convertFromViewYToScreenY(v.y);
}
}
/**
* The Texture class is an sabstract class that represents a 16-bit color
* texture.
*/
abstract class Texture {
protected int width;
protected int height;
/**
* Creates a new Texture with the specified width and height.
*/
public Texture(int width, int height) {
this.width = width;
this.height = height;
}
/**
* Gets the width of this Texture.
*/
public int getWidth() {
return width;
}
/**
* Gets the height of this Texture.
*/
public int getHeight() {
return height;
}
/**
* Gets the 16-bit color of this Texture at the specified (x,y) location.
*/
public abstract short getColor(int x, int y);
/**
* Creates an unshaded Texture from the specified image file.
*/
public static Texture createTexture(String filename) {
return createTexture(filename, false);
}
/**
* Creates an Texture from the specified image file. If shaded is true, then
* a ShadedTexture is returned.
*/
public static Texture createTexture(String filename, boolean shaded) {
try {
return createTexture(ImageIO.read(new File(filename)), shaded);
} catch (IOException ex) {
ex.printStackTrace();
return null;
}
}
/**
* Creates an unshaded Texture from the specified image.
*/
public static Texture createTexture(BufferedImage image) {
return createTexture(image, false);
}
/**
* Creates an Texture from the specified image. If shaded is true, then a
* ShadedTexture is returned.
*/
public static Texture createTexture(BufferedImage image, boolean shaded) {
int type = image.getType();
int width = image.getWidth();
int height = image.getHeight();
if (!isPowerOfTwo(width) || !isPowerOfTwo(height)) {
throw new IllegalArgumentException(
"Size of texture must be a power of two.");
}
if (shaded) {
// convert image to an indexed image
if (type != BufferedImage.TYPE_BYTE_INDEXED) {
System.out.println("Warning: image converted to "
+ "256-color indexed image. Some quality may "
+ "be lost.");
BufferedImage newImage = new BufferedImage(image.getWidth(),
image.getHeight(), BufferedImage.TYPE_BYTE_INDEXED);
Graphics2D g = newImage.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
image = newImage;
}
DataBuffer dest = image.getRaster().getDataBuffer();
return new ShadedTexture(((DataBufferByte) dest).getData(),
countbits(width - 1), countbits(height - 1),
(IndexColorModel) image.getColorModel());
} else {
// convert image to an 16-bit image
if (type != BufferedImage.TYPE_USHORT_565_RGB) {
BufferedImage newImage = new BufferedImage(image.getWidth(),
image.getHeight(), BufferedImage.TYPE_USHORT_565_RGB);
Graphics2D g = newImage.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
image = newImage;
}
DataBuffer dest = image.getRaster().getDataBuffer();
return new PowerOf2Texture(((DataBufferUShort) dest).getData(),
countbits(width - 1), countbits(height - 1));
}
}
/**
* Returns true if the specified number is a power of 2.
*/
public static boolean isPowerOfTwo(int n) {
return ((n & (n - 1)) == 0);
}
/**
* Counts the number of "on" bits in an integer.
*/
public static int countbits(int n) {
int count = 0;
while (n > 0) {
count += (n & 1);
n >>= 1;
}
return count;
}
}
/**
* 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.
*/
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;
// the row set in setCurrRow and used in getColorCurrRow
private int currRow;
/**
* 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 it's
* 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 it's 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)])];
}
/**
* Sets the current row for getColorCurrRow(). Pre-calculates the offset for
* this row.
*/
public void setCurrRow(int y) {
currRow = (y & heightMask) << widthBits;
}
/**
* Gets the color at the specified x location at the specified shade level.
* The current row defined in setCurrRow is used.
*/
public short getColorCurrRow(int x, int shadeLevel) {
return shadeTable[(shadeLevel << PALETTE_SIZE_BITS)
| (0xff & buffer[(x & widthMask) | currRow])];
}
}
/**
* The PowerOf2Texture class is a Texture with a width and height that are a
* power of 2 (32, 128, etc.).
*/
final class PowerOf2Texture extends Texture {
private short[] buffer;
private int widthBits;
private int widthMask;
private int heightBits;
private int heightMask;
/**
* Creates a new PowerOf2Texture with the specified buffer. 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).
*/
public PowerOf2Texture(short[] buffer, int widthBits, int heightBits) {
super(1 << widthBits, 1 << heightBits);
this.buffer = buffer;
this.widthBits = widthBits;
this.heightBits = heightBits;
this.widthMask = getWidth() - 1;
this.heightMask = getHeight() - 1;
}
/**
* Gets the 16-bit color of the pixel at location (x,y) in the bitmap.
*/
public short getColor(int x, int y) {
return buffer[(x & widthMask) + ((y & heightMask) << widthBits)];
}
}
/**
* Simple abstract class used for testing. Subclasses should implement the
* draw() method.
*/
abstract class GameCore {
protected static final int DEFAULT_FONT_SIZE = 24;
// various lists of modes, ordered by preference
protected static final DisplayMode[] MID_RES_MODES = {
new DisplayMode(800, 600, 16, 0), new DisplayMode(800, 600, 32, 0),
new DisplayMode(800, 600, 24, 0), new DisplayMode(640, 480, 16, 0),
new DisplayMode(640, 480, 32, 0), new DisplayMode(640, 480, 24, 0),
new DisplayMode(1024, 768, 16, 0),
new DisplayMode(1024, 768, 32, 0),
new DisplayMode(1024, 768, 24, 0), };
protected static final DisplayMode[] LOW_RES_MODES = {
new DisplayMode(640, 480, 16, 0), new DisplayMode(640, 480, 32, 0),
new DisplayMode(640, 480, 24, 0), new DisplayMode(800, 600, 16, 0),
new DisplayMode(800, 600, 32, 0), new DisplayMode(800, 600, 24, 0),
new DisplayMode(1024, 768, 16, 0),
new DisplayMode(1024, 768, 32, 0),
new DisplayMode(1024, 768, 24, 0), };
protected static final DisplayMode[] VERY_LOW_RES_MODES = {
new DisplayMode(320, 240, 16, 0), new DisplayMode(400, 300, 16, 0),
new DisplayMode(512, 384, 16, 0), new DisplayMode(640, 480, 16, 0),
new DisplayMode(800, 600, 16, 0), };
private boolean isRunning;
protected ScreenManager screen;
protected int fontSize = DEFAULT_FONT_SIZE;
/**
* Signals the game loop that it's time to quit
*/
public void stop() {
isRunning = false;
}
/**
* Calls init() and gameLoop()
*/
public void run() {
try {
init();
gameLoop();
} finally {
if (screen != null) {
screen.restoreScreen();
}
lazilyExit();
}
}
/**
* Exits the VM from a daemon thread. The daemon thread waits 2 seconds then
* calls System.exit(0). Since the VM should exit when only daemon threads
* are running, this makes sure System.exit(0) is only called if neccesary.
* It's neccesary if the Java Sound system is running.
*/
public void lazilyExit() {
Thread thread = new Thread() {
public void run() {
// first, wait for the VM exit on its own.
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
}
// system is still running, so force an exit
System.exit(0);
}
};
thread.setDaemon(true);
thread.start();
}
/**
* Sets full screen mode and initiates and objects.
*/
public void init() {
init(MID_RES_MODES);
}
/**
* Sets full screen mode and initiates and objects.
*/
public void init(DisplayMode[] possibleModes) {
screen = new ScreenManager();
DisplayMode displayMode = screen.findFirstCompatibleMode(possibleModes);
screen.setFullScreen(displayMode);
Window window = screen.getFullScreenWindow();
window.setFont(new Font("Dialog", Font.PLAIN, fontSize));
window.setBackground(Color.blue);
window.setForeground(Color.white);
isRunning = true;
}
public Image loadImage(String fileName) {
return new ImageIcon(fileName).getImage();
}
/**
* Runs through the game loop until stop() is called.
*/
public void gameLoop() {
long startTime = System.currentTimeMillis();
long currTime = startTime;
while (isRunning) {
long elapsedTime = System.currentTimeMillis() - currTime;
currTime += elapsedTime;
// update
update(elapsedTime);
// draw the screen
Graphics2D g = screen.getGraphics();
draw(g);
g.dispose();
screen.update();
// don't take a nap! run as fast as possible
/*
* try { Thread.sleep(20); } catch (InterruptedException ex) { }
*/
}
}
/**
* Updates the state of the game/animation based on the amount of elapsed
* time that has passed.
*/
public void update(long elapsedTime) {
// do nothing
}
/**
* Draws to the screen. Subclasses must override this method.
*/
public abstract void draw(Graphics2D g);
}
/**
* The ScreenManager class manages initializing and displaying full screen
* graphics modes.
*/
class ScreenManager {
private GraphicsDevice device;
/**
* Creates a new ScreenManager object.
*/
public ScreenManager() {
GraphicsEnvironment environment = GraphicsEnvironment
.getLocalGraphicsEnvironment();
device = environment.getDefaultScreenDevice();
}
/**
* Returns a list of compatible display modes for the default device on the
* system.
*/
public DisplayMode[] getCompatibleDisplayModes() {
return device.getDisplayModes();
}
/**
* Returns the first compatible mode in a list of modes. Returns null if no
* modes are compatible.
*/
public DisplayMode findFirstCompatibleMode(DisplayMode modes[]) {
DisplayMode goodModes[] = device.getDisplayModes();
for (int i = 0; i < modes.length; i++) {
for (int j = 0; j < goodModes.length; j++) {
if (displayModesMatch(modes[i], goodModes[j])) {
return modes[i];
}
}
}
return null;
}
/**
* Returns the current display mode.
*/
public DisplayMode getCurrentDisplayMode() {
return device.getDisplayMode();
}
/**
* Determines if two display modes "match". Two display modes match if they
* have the same resolution, bit depth, and refresh rate. The bit depth is
* ignored if one of the modes has a bit depth of
* DisplayMode.BIT_DEPTH_MULTI. Likewise, the refresh rate is ignored if one
* of the modes has a refresh rate of DisplayMode.REFRESH_RATE_UNKNOWN.
*/
public boolean displayModesMatch(DisplayMode mode1, DisplayMode mode2)
{
if (mode1.getWidth() != mode2.getWidth()
|| mode1.getHeight() != mode2.getHeight()) {
return false;
}
if (mode1.getBitDepth() != DisplayMode.BIT_DEPTH_MULTI
&& mode2.getBitDepth() != DisplayMode.BIT_DEPTH_MULTI
&& mode1.getBitDepth() != mode2.getBitDepth()) {
return false;
}
if (mode1.getRefreshRate() != DisplayMode.REFRESH_RATE_UNKNOWN
&& mode2.getRefreshRate() != DisplayMode.REFRESH_RATE_UNKNOWN
&& mode1.getRefreshRate() != mode2.getRefreshRate()) {
return false;
}
return true;
}
/**
* Enters full screen mode and changes the display mode. If the specified
* display mode is null or not compatible with this device, or if the
* display mode cannot be changed on this system, the current display mode
* is used.
* <p>
* The display uses a BufferStrategy with 2 buffers.
*/
public void setFullScreen(DisplayMode displayMode) {
final JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setUndecorated(true);
frame.setIgnoreRepaint(true);
frame.setResizable(false);
device.setFullScreenWindow(frame);
if (displayMode != null && device.isDisplayChangeSupported()) {
try {
device.setDisplayMode(displayMode);
} catch (IllegalArgumentException ex) {
}
// fix for mac os x
frame.setSize(displayMode.getWidth(), displayMode.getHeight());
}
// avoid potential deadlock in 1.4.1_02
try {
EventQueue.invokeAndWait(new Runnable() {
public void run() {
frame.createBufferStrategy(2);
}
});
} catch (InterruptedException ex) {
// ignore
} catch (InvocationTargetException ex) {
// ignore
}
}
/**
* Gets the graphics context for the display. The ScreenManager uses double
* buffering, so applications must call update() to show any graphics drawn.
* <p>
* The application must dispose of the graphics object.
*/
public Graphics2D getGraphics() {
Window window = device.getFullScreenWindow();
if (window != null) {
BufferStrategy strategy = window.getBufferStrategy();
return (Graphics2D) strategy.getDrawGraphics();
} else {
return null;
}
}
/**
* Updates the display.
*/
public void update() {
Window window = device.getFullScreenWindow();
if (window != null) {
BufferStrategy strategy = window.getBufferStrategy();
if (!strategy.contentsLost()) {
strategy.show();
}
}
// Sync the display on some systems.
// (on Linux, this fixes event queue problems)
//Toolkit.getDefaultToolkit().sync();
}
/**
* Returns the window currently used in full screen mode. Returns null if
* the device is not in full screen mode.
*/
public JFrame getFullScreenWindow() {
return (JFrame) device.getFullScreenWindow();
}
/**
* Returns the width of the window currently used in full screen mode.
* Returns 0 if the device is not in full screen mode.
*/
public int getWidth() {
Window window = device.getFullScreenWindow();
if (window != null) {
return window.getWidth();
} else {
return 0;
}
}
/**
* Returns the height of the window currently used in full screen mode.
* Returns 0 if the device is not in full screen mode.
*/
public int getHeight() {
Window window = device.getFullScreenWindow();
if (window != null) {
return window.getHeight();
} else {
return 0;
}
}
/**
* Restores the screen's display mode.
*/
public void restoreScreen() {
Window window = device.getFullScreenWindow();
if (window != null) {
window.dispose();
}
device.setFullScreenWindow(null);
}
/**
* Creates an image compatible with the current display.
*/
public BufferedImage createCompatibleImage(int w, int h, int transparancy) {
Window window = device.getFullScreenWindow();
if (window != null) {
GraphicsConfiguration gc = window.getGraphicsConfiguration();
return gc.createCompatibleImage(w, h, transparancy);
}
return null;
}
}
/**
* A thread pool is a group of a limited number of threads that are used to
* execute tasks.
*/
class ThreadPool extends ThreadGroup {
private boolean isAlive;
private LinkedList taskQueue;
private int threadID;
private static int threadPoolID;
/**
* Creates a new ThreadPool.
*
* @param numThreads
* The number of threads in the pool.
*/
public ThreadPool(int numThreads) {
super("ThreadPool-" + (threadPoolID++));
setDaemon(true);
isAlive = true;
taskQueue = new LinkedList();
for (int i = 0; i < numThreads; i++) {
new PooledThread().start();
}
}
/**
* Requests a new task to run. This method returns immediately, and the task
* executes on the next available idle thread in this ThreadPool.
* <p>
* Tasks start execution in the order they are received.
*
* @param task
* The task to run. If null, no action is taken.
* @throws IllegalStateException
* if this ThreadPool is already closed.
*/
public synchronized void runTask(Runnable task) {
if (!isAlive) {
throw new IllegalStateException();
}
if (task != null) {
taskQueue.add(task);
notify();
}
}
protected synchronized Runnable getTask() throws InterruptedException {
while (taskQueue.size() == 0) {
if (!isAlive) {
return null;
}
wait();
}
return (Runnable) taskQueue.removeFirst();
}
/**
* Closes this ThreadPool and returns immediately. All threads are stopped,
* and any waiting tasks are not executed. Once a ThreadPool is closed, no
* more tasks can be run on this ThreadPool.
*/
public synchronized void close() {
if (isAlive) {
isAlive = false;
taskQueue.clear();
interrupt();
}
}
/**
* Closes this ThreadPool and waits for all running threads to finish. Any
* waiting tasks are executed.
*/
public void join() {
// notify all waiting threads that this ThreadPool is no
// longer alive
synchronized (this) {
isAlive = false;
notifyAll();
}
// wait for all threads to finish
Thread[] threads = new Thread[activeCount()];
int count = enumerate(threads);
for (int i = 0; i < count; i++) {
try {
threads[i].join();
} catch (InterruptedException ex) {
}
}
}
/**
* Signals that a PooledThread has started. This method does nothing by
* default; subclasses should override to do any thread-specific startup
* tasks.
*/
protected void threadStarted() {
// do nothing
}
/**
* Signals that a PooledThread has stopped. This method does nothing by
* default; subclasses should override to do any thread-specific cleanup
* tasks.
*/
protected void threadStopped() {
// do nothing
}
/**
* A PooledThread is a Thread in a ThreadPool group, designed to run tasks
* (Runnables).
*/
private class PooledThread extends Thread {
public PooledThread() {
super(ThreadPool.this, "PooledThread-" + (threadID++));
}
public void run() {
// signal that this thread has started
threadStarted();
while (!isInterrupted()) {
// get a task to run
Runnable task = null;
try {
task = getTask();
} catch (InterruptedException ex) {
}
// if getTask() returned null or was interrupted,
// close this thread.
if (task == null) {
break;
}
// run the task, and eat any exceptions it throws
try {
task.run();
} catch (Throwable t) {
uncaughtException(this, t);
}
}
// signal that this thread has stopped
threadStopped();
}
}
}
/**
* The SoundManager class manages sound playback. The SoundManager is a
* ThreadPool, with each thread playing back one sound at a time. This allows
* the SoundManager to easily limit the number of simultaneous sounds being
* played.
* <p>
* Possible ideas to extend this class:
* <ul>
* <li>add a setMasterVolume() method, which uses Controls to set the volume
* for each line.
* <li>don't play a sound if more than, say, 500ms has passed since the request
* to play
* </ul>
*/
class SoundManager extends ThreadPool {
private AudioFormat playbackFormat;
private ThreadLocal localLine;
private ThreadLocal localBuffer;
private Object pausedLock;
private boolean paused;
/**
* Creates a new SoundManager using the maximum number of simultaneous
* sounds.
*/
public SoundManager(AudioFormat playbackFormat) {
this(playbackFormat, getMaxSimultaneousSounds(playbackFormat));
}
/**
* Creates a new SoundManager with the specified maximum number of
* simultaneous sounds.
*/
public SoundManager(AudioFormat playbackFormat, int maxSimultaneousSounds) {
super(Math.min(maxSimultaneousSounds,
getMaxSimultaneousSounds(playbackFormat)));
this.playbackFormat = playbackFormat;
localLine = new ThreadLocal();
localBuffer = new ThreadLocal();
pausedLock = new Object();
// notify threads in pool it's ok to start
synchronized (this) {
notifyAll();
}
}
/**
* Gets the maximum number of simultaneous sounds with the specified
* AudioFormat that the default mixer can play.
*/
public static int getMaxSimultaneousSounds(AudioFormat playbackFormat) {
DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class,
playbackFormat);
Mixer mixer = AudioSystem.getMixer(null);
return mixer.getMaxLines(lineInfo);
}
/**
* Does any clean up before closing.
*/
protected void cleanUp() {
// signal to unpause
setPaused(false);
// close the mixer (stops any running sounds)
Mixer mixer = AudioSystem.getMixer(null);
if (mixer.isOpen()) {
mixer.close();
}
}
public void close() {
cleanUp();
super.close();
}
public void join() {
cleanUp();
super.join();
}
/**
* Sets the paused state. Sounds may not pause immediately.
*/
public void setPaused(boolean paused) {
if (this.paused != paused) {
synchronized (pausedLock) {
this.paused = paused;
if (!paused) {
// restart sounds
pausedLock.notifyAll();
}
}
}
}
/**
* Returns the paused state.
*/
public boolean isPaused() {
return paused;
}
/**
* Loads a Sound from the file system. Returns null if an error occurs.
*/
public Sound getSound(String filename) {
return getSound(getAudioInputStream(filename));
}
/**
* Loads a Sound from an input stream. Returns null if an error occurs.
*/
public Sound getSound(InputStream is) {
return getSound(getAudioInputStream(is));
}
/**
* Loads a Sound from an AudioInputStream.
*/
public Sound getSound(AudioInputStream audioStream) {
if (audioStream == null) {
return null;
}
// get the number of bytes to read
int length = (int) (audioStream.getFrameLength() * audioStream
.getFormat().getFrameSize());
// read the entire stream
byte[] samples = new byte[length];
DataInputStream is = new DataInputStream(audioStream);
try {
is.readFully(samples);
is.close();
} catch (IOException ex) {
ex.printStackTrace();
}
// return the samples
return new Sound(samples);
}
/**
* Creates an AudioInputStream from a sound from the file system.
*/
public AudioInputStream getAudioInputStream(String filename) {
try {
return getAudioInputStream(new FileInputStream(filename));
} catch (IOException ex) {
ex.printStackTrace();
return null;
}
}
/**
* Creates an AudioInputStream from a sound from an input stream
*/
public AudioInputStream getAudioInputStream(InputStream is) {
try {
if (!is.markSupported()) {
is = new BufferedInputStream(is);
}
// open the source stream
AudioInputStream source = AudioSystem.getAudioInputStream(is);
// convert to playback format
return AudioSystem.getAudioInputStream(playbackFormat, source);
} catch (UnsupportedAudioFileException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
}
return null;
}
/**
* Plays a sound. This method returns immediately.
*/
public InputStream play(Sound sound) {
return play(sound, null, false);
}
/**
* Plays a sound with an optional SoundFilter, and optionally looping. This
* method returns immediately.
*/
public InputStream play(Sound sound, SoundFilter filter, boolean loop) {
InputStream is;
if (sound != null) {
if (loop) {
is = new LoopingByteInputStream(sound.getSamples());
} else {
is = new ByteArrayInputStream(sound.getSamples());
}
return play(is, filter);
}
return null;
}
/**
* Plays a sound from an InputStream. This method returns immediately.
*/
public InputStream play(InputStream is) {
return play(is, null);
}
/**
* Plays a sound from an InputStream with an optional sound filter. This
* method returns immediately.
*/
public InputStream play(InputStream is, SoundFilter filter) {
if (is != null) {
if (filter != null) {
is = new FilteredSoundStream(is, filter);
}
runTask(new SoundPlayer(is));
}
return is;
}
/**
* Signals that a PooledThread has started. Creates the Thread's line and
* buffer.
*/
protected void threadStarted() {
// wait for the SoundManager constructor to finish
synchronized (this) {
try {
wait();
} catch (InterruptedException ex) {
}
}
// use a short, 100ms (1/10th sec) buffer for filters that
// change in real-time
int bufferSize = playbackFormat.getFrameSize()
* Math.round(playbackFormat.getSampleRate() / 10);
// create, open, and start the line
SourceDataLine line;
DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class,
playbackFormat);
try {
line = (SourceDataLine) AudioSystem.getLine(lineInfo);
line.open(playbackFormat, bufferSize);
} catch (LineUnavailableException ex) {
// the line is unavailable - signal to end this thread
Thread.currentThread().interrupt();
return;
}
line.start();
// create the buffer
byte[] buffer = new byte[bufferSize];
// set this thread's locals
localLine.set(line);
localBuffer.set(buffer);
}
/**
* Signals that a PooledThread has stopped. Drains and closes the Thread's
* Line.
*/
protected void threadStopped() {
SourceDataLine line = (SourceDataLine) localLine.get();
if (line != null) {
line.drain();
line.close();
}
}
/**
* The SoundPlayer class is a task for the PooledThreads to run. It receives
* the threads's Line and byte buffer from the ThreadLocal variables and
* plays a sound from an InputStream.
* <p>
* This class only works when called from a PooledThread.
*/
protected class SoundPlayer implements Runnable {
private InputStream source;
public SoundPlayer(InputStream source) {
this.source = source;
}
public void run() {
// get line and buffer from ThreadLocals
SourceDataLine line = (SourceDataLine) localLine.get();
byte[] buffer = (byte[]) localBuffer.get();
if (line == null || buffer == null) {
// the line is unavailable
return;
}
// copy data to the line
try {
int numBytesRead = 0;
while (numBytesRead != -1) {
// if paused, wait until unpaused
synchronized (pausedLock) {
if (paused) {
try {
pausedLock.wait();
} catch (InterruptedException ex) {
return;
}
}
}
// copy data
numBytesRead = source.read(buffer, 0, buffer.length);
if (numBytesRead != -1) {
line.write(buffer, 0, numBytesRead);
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
/**
* The LoopingByteInputStream is a ByteArrayInputStream that loops indefinitly.
* The looping stops when the close() method is called.
* <p>
* Possible ideas to extend this class:
* <ul>
* <li>Add an option to only loop a certain number of times.
* </ul>
*/
class LoopingByteInputStream extends ByteArrayInputStream {
private boolean closed;
/**
* Creates a new LoopingByteInputStream with the specified byte array. The
* array is not copied.
*/
public LoopingByteInputStream(byte[] buffer) {
super(buffer);
closed = false;
}
/**
* Reads <code>length</code> bytes from the array. If the end of the array
* is reached, the reading starts over from the beginning of the array.
* Returns -1 if the array has been closed.
*/
public int read(byte[] buffer, int offset, int length) {
if (closed) {
return -1;
}
int totalBytesRead = 0;
while (totalBytesRead < length) {
int numBytesRead = super.read(buffer, offset + totalBytesRead,
length - totalBytesRead);
if (numBytesRead > 0) {
totalBytesRead += numBytesRead;
} else {
reset();
}
}
return totalBytesRead;
}
/**
* Closes the stream. Future calls to the read() methods will return 1.
*/
public void close() throws IOException {
super.close();
closed = true;
}
}
/**
* A abstract class designed to filter sound samples. Since SoundFilters may use
* internal buffering of samples, a new SoundFilter object should be created for
* every sound played. However, SoundFilters can be reused after they are
* finished by called the reset() method.
* <p>
* Assumes all samples are 16-bit, signed, little-endian format.
*
* @see FilteredSoundStream
*/
abstract class SoundFilter {
/**
* Resets this SoundFilter. Does nothing by default.
*/
public void reset() {
// do nothing
}
/**
* Gets the remaining size, in bytes, that this filter plays after the sound
* is finished. An example would be an echo that plays longer than it's
* original sound. This method returns 0 by default.
*/
public int getRemainingSize() {
return 0;
}
/**
* Filters an array of samples. Samples should be in 16-bit, signed,
* little-endian format.
*/
public void filter(byte[] samples) {
filter(samples, 0, samples.length);
}
/**
* Filters an array of samples. Samples should be in 16-bit, signed,
* little-endian format. This method should be implemented by subclasses.
*/
public abstract void filter(byte[] samples, int offset, int length);
/**
* Convenience method for getting a 16-bit sample from a byte array. Samples
* should be in 16-bit, signed, little-endian format.
*/
public static short getSample(byte[] buffer, int position) {
return (short) (((buffer[position + 1] & 0xff) << 8) | (buffer[position] & 0xff));
}
/**
* Convenience method for setting a 16-bit sample in a byte array. Samples
* should be in 16-bit, signed, little-endian format.
*/
public static void setSample(byte[] buffer, int position, short sample) {
buffer[position] = (byte) (sample & 0xff);
buffer[position + 1] = (byte) ((sample >> 8) & 0xff);
}
}
/**
* The FilteredSoundStream class is a FilterInputStream that applies a
* SoundFilter to the underlying input stream.
*
* @see SoundFilter
*/
class FilteredSoundStream extends FilterInputStream {
private static final int REMAINING_SIZE_UNKNOWN = -1;
private SoundFilter soundFilter;
private int remainingSize;
/**
* Creates a new FilteredSoundStream object with the specified InputStream
* and SoundFilter.
*/
public FilteredSoundStream(InputStream in, SoundFilter soundFilter) {
super(in);
this.soundFilter = soundFilter;
remainingSize = REMAINING_SIZE_UNKNOWN;
}
/**
* Overrides the FilterInputStream method to apply this filter whenever
* bytes are read
*/
public int read(byte[] samples, int offset, int length) throws IOException {
// read and filter the sound samples in the stream
int bytesRead = super.read(samples, offset, length);
if (bytesRead > 0) {
soundFilter.filter(samples, offset, bytesRead);
return bytesRead;
}
// if there are no remaining bytes in the sound stream,
// check if the filter has any remaining bytes ("echoes").
if (remainingSize == REMAINING_SIZE_UNKNOWN) {
remainingSize = soundFilter.getRemainingSize();
// round down to nearest multiple of 4
// (typical frame size)
remainingSize = remainingSize / 4 * 4;
}
if (remainingSize > 0) {
length = Math.min(length, remainingSize);
// clear the buffer
for (int i = offset; i < offset + length; i++) {
samples[i] = 0;
}
// filter the remaining bytes
soundFilter.filter(samples, offset, length);
remainingSize -= length;
// return
return length;
} else {
// end of stream
return -1;
}
}
}
/**
* The MoreMath class provides functions not contained in the java.lang.Math or
* java.lang.StrictMath classes.
*/
class MoreMath {
/**
* Returns the sign of the number. Returns -1 for negative, 1 for positive,
* and 0 otherwise.
*/
public static int sign(short v) {
return (v > 0) ? 1 : (v < 0) ? -1 : 0;
}
/**
* Returns the sign of the number. Returns -1 for negative, 1 for positive,
* and 0 otherwise.
*/
public static int sign(int v) {
return (v > 0) ? 1 : (v < 0) ? -1 : 0;
}
/**
* Returns the sign of the number. Returns -1 for negative, 1 for positive,
* and 0 otherwise.
*/
public static int sign(long v) {
return (v > 0) ? 1 : (v < 0) ? -1 : 0;
}
/**
* Returns the sign of the number. Returns -1 for negative, 1 for positive,
* and 0 otherwise.
*/
public static int sign(float v) {
return (v > 0) ? 1 : (v < 0) ? -1 : 0;
}
/**
* Returns the sign of the number. Returns -1 for negative, 1 for positive,
* and 0 otherwise.
*/
public static int sign(double v) {
return (v > 0) ? 1 : (v < 0) ? -1 : 0;
}
/**
* Faster ceil function to convert a float to an int. Contrary to the
* java.lang.Math ceil function, this function takes a float as an argument,
* returns an int instead of a double, and does not consider special cases.
*/
public static int ceil(float f) {
if (f > 0) {
return (int) f + 1;
} else {
return (int) f;
}
}
/**
* Faster floor function to convert a float to an int. Contrary to the
* java.lang.Math floor function, this function takes a float as an
* argument, returns an int instead of a double, and does not consider
* special cases.
*/
public static int floor(float f) {
if (f >= 0
|