/* Author: David Anson */

import java.applet.Applet;
import java.awt.*;
import java.awt.image.ImageObserver;
import java.awt.image.PixelGrabber;
import java.awt.image.MemoryImageSource;
import java.net.URL;
import java.net.MalformedURLException;

public final class TextureMappingApplet extends Applet
	{
	private Choice ViewObject = new Choice(), MappingType = new Choice();
	private TextField ImageNameField = null;
	private Scrollbar Distance = null, Theta = null, Phi = null, Rotation = null;
	private DisplayCanvas Display = new DisplayCanvas();
	private TexMapThread Mapper = null;
	private int Width = -1, Height = -1;
	private int[] SourceBitmap = null;
	private Image SceneImage = null;

	public void init()
		{
		GridBagConstraints gbc = new GridBagConstraints();
		gbc.insets = new Insets(1, 1, 1, 1);
		GridBagLayout gbl = new GridBagLayout();
		setLayout(gbl);
		Label TempLabel;
		Panel TempPanel;
		TempPanel = new Panel();
		GridBagLayout pgbl = new GridBagLayout();
		TempPanel.setLayout(pgbl);
		TempLabel = new Label("Image location:", Label.RIGHT);
 		setupConstraints(gbc, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE);
		pgbl.setConstraints(TempLabel, gbc);
		TempPanel.add(TempLabel);
		ImageNameField = new TextField("BRUCE.GIF");
 		setupConstraints(gbc, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
		pgbl.setConstraints(ImageNameField, gbc);
		TempPanel.add(ImageNameField);
		TempLabel = new Label("Object:", Label.RIGHT);
 		setupConstraints(gbc, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE);
		pgbl.setConstraints(TempLabel, gbc);
		TempPanel.add(TempLabel);
		ViewObject.addItem("Flat Square");
		ViewObject.addItem("Cube");
		ViewObject.addItem("Double Pyramid");
		ViewObject.addItem("Icosahedron");
		ViewObject.addItem("80 Triangle Sphere");
		setupConstraints(gbc, 1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
		pgbl.setConstraints(ViewObject, gbc);
		TempPanel.add(ViewObject);
		TempLabel = new Label("Mapping Type:", Label.RIGHT);
 		setupConstraints(gbc, 0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE);
		pgbl.setConstraints(TempLabel, gbc);
		TempPanel.add(TempLabel);
		MappingType.addItem("Flat Projection");
		MappingType.addItem("Cylindrical Projection");
		MappingType.addItem("Spherical Projection");
		setupConstraints(gbc, 1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
		pgbl.setConstraints(MappingType, gbc);
		TempPanel.add(MappingType);
		setupConstraints(gbc, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
		gbl.setConstraints(TempPanel, gbc);
		add(TempPanel);
		setupConstraints(gbc, 0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.NONE);
		gbl.setConstraints(Display, gbc);
		add(Display);
		TempPanel = new Panel();
		pgbl = new GridBagLayout();
		TempPanel.setLayout(pgbl);
		TempLabel = new Label("Distance: ", Label.RIGHT);
		setupConstraints(gbc, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE);
		pgbl.setConstraints(TempLabel, gbc);
		TempPanel.add(TempLabel);
		Distance = new Scrollbar(Scrollbar.HORIZONTAL, 10, 1, 5, 24);  // Can not let image go behind the camera (no clipping)
		Distance.setPageIncrement(4);
		setupConstraints(gbc, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
		pgbl.setConstraints(Distance, gbc);
		TempPanel.add(Distance);
		TempLabel = new Label("Theta: ", Label.RIGHT);
		setupConstraints(gbc, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE);
		pgbl.setConstraints(TempLabel, gbc);
		TempPanel.add(TempLabel);
		Theta = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, -180, 180);
		Theta.setPageIncrement(10);
		setupConstraints(gbc, 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
		pgbl.setConstraints(Theta, gbc);
		TempPanel.add(Theta);
		TempLabel = new Label("Phi: ", Label.RIGHT);
		setupConstraints(gbc, 0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE);
		pgbl.setConstraints(TempLabel, gbc);
		TempPanel.add(TempLabel);
		Phi = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, -90, 90);
		Phi.setPageIncrement(10);
		setupConstraints(gbc, 1, 2, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
		pgbl.setConstraints(Phi, gbc);
		TempPanel.add(Phi);
		TempLabel = new Label("Rotation: ", Label.RIGHT);
		setupConstraints(gbc, 0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE);
		pgbl.setConstraints(TempLabel, gbc);
		TempPanel.add(TempLabel);
		Rotation = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, -180, 180);
		Rotation.setPageIncrement(10);
		setupConstraints(gbc, 1, 3, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
		pgbl.setConstraints(Rotation, gbc);
		TempPanel.add(Rotation);
		setupConstraints(gbc, 0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);
		gbl.setConstraints(TempPanel, gbc);
		add(TempPanel);
		}

	private void setupConstraints(GridBagConstraints gbc, int x, int y, int width, int height, double weightx, double weighty, int anchor, int fill)
		{
		gbc.gridx = x;
		gbc.gridy = y;
		gbc.gridwidth = width;
		gbc.gridheight = height;
		gbc.weightx = weightx;
		gbc.weighty = weighty;
		gbc.anchor = anchor;
		gbc.fill = fill;
		}

	public void start()
		{
		Mapper = new TexMapThread(Display);
		loadImage();
		Mapper.setViewParams(((double)Distance.getValue())/8.0, ((double)Theta.getValue())*(Math.PI/180.0), ((double)Phi.getValue())*(Math.PI/180.0), ((double)Rotation.getValue())*(Math.PI/180.0));
		Mapper.start();  // Don't start until the parameters are all valid
		}
	
	public void stop()
		{
		Display.setImage(null);
		Mapper.stop();
		Mapper = null;
		}

	private void loadImage()
		{
		showStatus("Loading image...");
		Image SourceImage = null;
		try
			{
			URL SourceImageURL = new URL(getDocumentBase(), ImageNameField.getText());
			SourceImage = getImage(SourceImageURL);
			MediaTracker tracker = new MediaTracker(this);
			tracker.addImage(SourceImage, 0);
			int RemainingTime = 30;
			while(RemainingTime != 0)
				{
				try
					{
					if(tracker.waitForAll(1000))
						break;
					}
				catch(InterruptedException IE)
					{
					SourceImage = null;
					}
				RemainingTime--;
				showStatus("Loading image...  (Automatic abort in "+RemainingTime+" seconds)");
				}
			if((RemainingTime == 0) || ((tracker.statusAll(false) & MediaTracker.COMPLETE) == 0))
				SourceImage = null;
			}
		catch(MalformedURLException ME)
			{
			SourceImage = null;
			}
		catch(SecurityException SE)
			{
			SourceImage = null;
			}
		if(SourceImage == null)
			{
			SourceBitmap = null;
			Mapper.setSourceBitmap(null, -1, -1);
			showStatus("Error loading the image.");
			}
		else
			{
			showStatus("Image loaded.  Analyzing image...");
			Width = SourceImage.getWidth(this);
			Height = SourceImage.getHeight(this);
			SourceBitmap = new int[Width*Height];
			PixelGrabber SourceImageSourceBitmap = new PixelGrabber(SourceImage, 0, 0, Width, Height, SourceBitmap, 0, Width);
			try
				{
				SourceImageSourceBitmap.grabPixels();
				}
			catch(java.lang.InterruptedException IE)
				{
				;
				}
			Mapper.setSourceBitmap(SourceBitmap, Width, Height);
			showStatus("Image ready.");
			}
		}
	
	public boolean action(Event event, Object object)
		{
		if(event.target == ImageNameField)
			{
			loadImage();
			return true;
			}
		if(event.target == ViewObject)
			{
			Mapper.setObject(ViewObject.getSelectedIndex(), true);
			return true;
			}
		if(event.target == MappingType)
			{
			Mapper.setMappingType(MappingType.getSelectedIndex(), true);
			return true;
			}
		return super.action(event, object);
		}

	public boolean handleEvent(Event event)
		{
		if((event.id == Event.SCROLL_ABSOLUTE) || (event.id == Event.SCROLL_LINE_DOWN) || (event.id == Event.SCROLL_LINE_UP) || (event.id == Event.SCROLL_PAGE_DOWN) || (event.id == Event.SCROLL_PAGE_UP))
			{
			if((event.target == Distance) || (event.target == Theta) || (event.target == Phi) || (event.target == Rotation))
				{
				Mapper.setViewParams(((double)Distance.getValue())/8.0, ((double)Theta.getValue())*(Math.PI/180.0), ((double)Phi.getValue())*(Math.PI/180.0), ((double)Rotation.getValue())*(Math.PI/180.0));
				return true;
				}
			}
		return super.handleEvent(event);
		}

	public String getAppletInfo()
		{
		return new String("TextureMappingApplet by David Anson\nCopyright (C) 1997");
		}
	}

final class TexMapThread extends Thread
	{
	private static final double INVALIDt = 10000.0;
	private static final int X = 0, Y = 1, Z = 2;
	private boolean UpdateNeeded = false;
	private DisplayCanvas Display = null;
	private double Distance = 2.0, Theta = 0.0, Phi = 0.0, Rotation = 0.0, NewDistance = 2.0, NewTheta = 0.0, NewPhi = 0.0, NewRotation = 0.0;
	private double[] C = {0.0, 0.0, 0.0}, V = {0.0, 0.0, 0.0}, N = {0.0, 0.0, 0.0}, U = {0.0, 0.0, 0.0};
	private Image DisplayImageFore = null, DisplayImageBack = null;
	private int DisplayWidth = -1, DisplayHeight = -1, SourceWidth = -1, SourceHeight = -1, MappingType = -1;
	private int[] DisplayBitmap = null, SourceBitmap = null, BlankBitmap = null;
	// All points must be within 0.5 of [0,0,0] to avoid clipping problems
	// World is right-handed
	private double[][][] AllObjectPoints = {{{-0.4, -0.4, 0.1}, {0.4, -0.4, 0.1}, {-0.4, 0.4, 0.1}, {0.4, 0.4, 0.1}},
														 {{0.28, 0.28, 0.28}, {-0.28, 0.28, 0.28}, {0.28, -0.28, 0.28}, {-0.28, -0.28, 0.28}, {0.28, 0.28, -0.28}, {-0.28, 0.28, -0.28}, {0.28, -0.28, -0.28}, {-0.28, -0.28, -0.28}},
														 {{-0.4, 0.0, 0.4}, {0.4, 0.0, 0.4}, {0.0, 0.5, 0.0}, {-0.4, 0.0, -0.4}, {0.4, 0.0, -0.4}, {0.0, -0.5, 0.0}},
														 {{0, 0.48, 0}, {0, 0.239999, 0.415692}, {0.395346, 0.239999, 0.128455}, {0.244337, 0.239999, -0.336303}, {-0.244338, 0.239999, -0.336303}, {-0.395347, 0.239999, 0.128455}, {-0.244338, -0.24, 0.336302}, {0.244337, -0.24, 0.336302}, {0.395346, -0.24, -0.128456}, {0, -0.24, -0.415693}, {-0.395347, -0.24, -0.128456}, {0, -0.48, 0}},
														 {{0, 0.48, 0}, {0, 0.239999, 0.415692}, {0.395346, 0.239999, 0.128455}, {0.244337, 0.239999, -0.336303}, {-0.244338, 0.239999, -0.336303}, {-0.395347, 0.239999, 0.128455}, {-0.244338, -0.24, 0.336302}, {0.244337, -0.24, 0.336302}, {0.395346, -0.24, -0.128456}, {0, -0.24, -0.415693}, {-0.395347, -0.24, -0.128456}, {0, -0.48, 0}, {0, 0.415692, 0.24}, {0.228253, 0.415692, 0.074164}, {0.229653, 0.278828, 0.316091}, {0.141068, 0.415692, -0.194165}, {0.371587, 0.278828, -0.120737}, {-0.141069, 0.415692, -0.194165}, {0, 0.278828, -0.390711}, {-0.228254, 0.415692, 0.074164}, {-0.371588, 0.278828, -0.120737}, {-0.229654, 0.278828, 0.316091}, {0.148328, 0, 0.456507}, {0.388328, 0, 0.282136}, {0.48, 0, 0}, {0.388328, 0, -0.282137}, {0.148328, 0, -0.456508}, {-0.148329, 0, -0.456508}, {-0.388329, 0, -0.282137}, {-0.48, 0, -1e-006}, {-0.388329, 0, 0.282136}, {-0.148329, 0, 0.456507}, {0, -0.278829, 0.39071}, {0.371587, -0.278829, 0.120736}, {0.229653, -0.278829, -0.316092}, {-0.229654, -0.278829, -0.316092}, {-0.371588, -0.278829, 0.120736}, {-0.141069, -0.415693, 0.194164}, {0.141068, -0.415693, 0.194164}, {0.228253, -0.415693, -0.074165}, {0, -0.415693, -0.24}, {-0.228254, -0.415693, -0.074165}}};
														 

	private int[][][] AllObjectTriangles = {{{0, 1, 2}, {1, 2, 3}},
														 {{0, 1, 3}, {0, 3, 2}, {0, 4, 5}, {0, 5, 1}, {0, 4, 6}, {0, 6, 2}, {3, 7, 5}, {3, 5, 1}, {3, 7, 6}, {3, 6, 2}, {4, 5, 7}, {4, 7, 6}}, 
														 {{0, 1, 2}, {3, 4, 2}, {1, 4, 2}, {0, 3, 2}, {0, 1, 5}, {3, 4, 5}, {1, 4, 5}, {0, 3, 5}},
														 {{0, 1, 2}, {0, 2, 3}, {0, 3, 4}, {0, 4, 5}, {0, 5, 1}, {1, 2, 7}, {2, 3, 8}, {3, 4, 9}, {4, 5, 10}, {5, 1, 6}, {6, 7, 1}, {7, 8, 2}, {8, 9, 3}, {9, 10, 4}, {10, 6, 5}, {6, 7, 11}, {7, 8, 11}, {8, 9, 11}, {9, 10, 11}, {10, 6, 11}},
														 {{0, 12, 13}, {12, 1, 14}, {12, 13, 14}, {13, 14, 2}, {0, 13, 15}, {13, 2, 16}, {13, 15, 16}, {15, 16, 3}, {0, 15, 17}, {15, 3, 18}, {15, 17, 18}, {17, 18, 4}, {0, 17, 19}, {17, 4, 20}, {17, 19, 20}, {19, 20, 5}, {0, 19, 12}, {19, 5, 21}, {19, 12, 21}, {12, 21, 1}, {1, 14, 22}, {14, 2, 23}, {14, 22, 23}, {22, 23, 7}, {2, 16, 24}, {16, 3, 25}, {16, 24, 25}, {24, 25, 8}, {3, 18, 26}, {18, 4, 27}, {18, 26, 27}, {26, 27, 9}, {4, 20, 28}, {20, 5, 29}, {20, 28, 29}, {28, 29, 10}, {5, 21, 30}, {21, 1, 31}, {21, 30, 31}, {30, 31, 6}, {6, 32, 31}, {32, 7, 22}, {32, 31, 22}, {31, 22, 1}, {7, 33, 23}, {33, 8, 24}, {33, 23, 24}, {23, 24, 2}, {8, 34, 25}, {34, 9, 26}, {34, 25, 26}, {25, 26, 3}, {9, 35, 27}, {35, 10, 28}, {35, 27, 28}, {27, 28, 4}, {10, 36, 29}, {36, 6, 30}, {36, 29, 30}, {29, 30, 5}, {6, 32, 37}, {32, 7, 38}, {32, 37, 38}, {37, 38, 11}, {7, 33, 38}, {33, 8, 39}, {33, 38, 39}, {38, 39, 11}, {8, 34, 39}, {34, 9, 40}, {34, 39, 40}, {39, 40, 11}, {9, 35, 40}, {35, 10, 41}, {35, 40, 41}, {40, 41, 11}, {10, 36, 41}, {36, 6, 37}, {36, 41, 37}, {41, 37, 11}}};

	private int[][] Triangles = null;
	private double[][] Points = null, TriangleN = null, TriangleEdgeA = null, TriangleEdgeB = null;
	private double[] TriangleD = null;

	TexMapThread(DisplayCanvas Display)
		{
		this.Display = Display;
		DisplayWidth = Display.getDimension().width;
		DisplayHeight = Display.getDimension().height;
		DisplayBitmap = new int[DisplayWidth*DisplayHeight];
		BlankBitmap = new int[DisplayWidth*DisplayHeight];
		int BlankBitmapOffset = 0;
		for(int y = 0 ; y < DisplayHeight ; y++)
			{
			int CurrentGradient = (int)(256.0*((double)y/(double)DisplayHeight));
			int CurrentColor = 0xFF000000 | (CurrentGradient*0x10000) | (CurrentGradient*0x100) | CurrentGradient;
			for(int x = 0 ; x < DisplayWidth ; x++)
				{
				BlankBitmap[BlankBitmapOffset] = CurrentColor; //& 0xFF000000;
				BlankBitmapOffset++;
				}
			}
		System.arraycopy(BlankBitmap, 0, DisplayBitmap, 0, BlankBitmap.length);
		DisplayImageFore = Display.createImage(new MemoryImageSource(DisplayWidth, DisplayHeight, DisplayBitmap, 0, DisplayWidth));
		DisplayImageBack = Display.createImage(new MemoryImageSource(DisplayWidth, DisplayHeight, DisplayBitmap, 0, DisplayWidth));
		setObject(0, false);
		setMappingType(0, false);
		}

	public synchronized void setSourceBitmap(int[] SourceBitmap, int Width, int Height)
		{
		this.SourceWidth = Width;
		this.SourceHeight = Height;
		this.SourceBitmap = SourceBitmap;
		UpdateNeeded = true;
		}

	public synchronized void setViewParams(double Distance, double Theta, double Phi, double Rotation)
		{
		this.NewDistance = Distance;
		this.NewTheta = Theta;
		this.NewPhi = Phi;
		this.NewRotation = Rotation;
		UpdateNeeded = true;
		}

	private synchronized void copyViewParams()
		{
		Distance = NewDistance;
		Theta = NewTheta;
		Phi = NewPhi;
		Rotation = NewRotation;
		UpdateNeeded = false;
		}

	public void run()
		{
		while(true)
			{
			if(UpdateNeeded)
				{
				System.arraycopy(BlankBitmap, 0, DisplayBitmap, 0, BlankBitmap.length);
				if(SourceBitmap != null)
					{
					copyViewParams();
					RenderNextBitmap();
					}
				DisplayImageFore.flush();
				Display.setImage(DisplayImageFore);
				Image TempDisplayImage = DisplayImageFore;
				DisplayImageFore = DisplayImageBack;
				DisplayImageBack = TempDisplayImage;
				}
			try
				{
				sleep(100);
				}
			catch(InterruptedException IE)
				{
				;
				}
			}
		}

	public synchronized void setObject(int Object, boolean RequestUpdate)
		{
		Points = AllObjectPoints[Object];
		Triangles = AllObjectTriangles[Object];
		TriangleEdgeA = new double[Triangles.length][3];
		TriangleEdgeB = new double[Triangles.length][3];
		TriangleN = new double[Triangles.length][3];
		TriangleD = new double[Triangles.length];
		for(int CurrentTriangle = 0 ; CurrentTriangle < Triangles.length ; CurrentTriangle++)
			{
			TriangleEdgeA[CurrentTriangle][X] = Points[Triangles[CurrentTriangle][2]][X]-Points[Triangles[CurrentTriangle][0]][X];
			TriangleEdgeA[CurrentTriangle][Y] = Points[Triangles[CurrentTriangle][2]][Y]-Points[Triangles[CurrentTriangle][0]][Y];
			TriangleEdgeA[CurrentTriangle][Z] = Points[Triangles[CurrentTriangle][2]][Z]-Points[Triangles[CurrentTriangle][0]][Z];
			TriangleEdgeB[CurrentTriangle][X] = Points[Triangles[CurrentTriangle][1]][X]-Points[Triangles[CurrentTriangle][0]][X];
			TriangleEdgeB[CurrentTriangle][Y] = Points[Triangles[CurrentTriangle][1]][Y]-Points[Triangles[CurrentTriangle][0]][Y];
			TriangleEdgeB[CurrentTriangle][Z] = Points[Triangles[CurrentTriangle][1]][Z]-Points[Triangles[CurrentTriangle][0]][Z];
			TriangleN[CurrentTriangle][X] = TriangleEdgeA[CurrentTriangle][Y]*TriangleEdgeB[CurrentTriangle][Z]-TriangleEdgeA[CurrentTriangle][Z]*TriangleEdgeB[CurrentTriangle][Y];
			TriangleN[CurrentTriangle][Y] = TriangleEdgeA[CurrentTriangle][Z]*TriangleEdgeB[CurrentTriangle][X]-TriangleEdgeA[CurrentTriangle][X]*TriangleEdgeB[CurrentTriangle][Z];
			TriangleN[CurrentTriangle][Z] = TriangleEdgeA[CurrentTriangle][X]*TriangleEdgeB[CurrentTriangle][Y]-TriangleEdgeA[CurrentTriangle][Y]*TriangleEdgeB[CurrentTriangle][X];
			TriangleD[CurrentTriangle] = TriangleN[CurrentTriangle][X]*Points[Triangles[CurrentTriangle][0]][X]+TriangleN[CurrentTriangle][Y]*Points[Triangles[CurrentTriangle][0]][Y]+TriangleN[CurrentTriangle][Z]*Points[Triangles[CurrentTriangle][0]][Z];
			}
		if(RequestUpdate)
			UpdateNeeded = true;
		}

	public synchronized void setMappingType(int MappingType, boolean RequestUpdate)
		{
		this.MappingType = MappingType;
		if(RequestUpdate)
			UpdateNeeded = true;
		}

	private void RenderNextBitmap()
		{
		// Update the view parameters - View is left-handed
		double st = Math.sin(Theta);
		double ct = Math.cos(Theta);
		double sp = Math.sin(Phi);
		double cp = Math.cos(Phi);
		double sr = Math.sin(Rotation);
		double cr = Math.cos(Rotation);
		double[][] M = {{ ct*cr-st*sp*sr, cp*sr, -st*cr-ct*sp*sr},
							 {-ct*sr-st*sp*cr, cp*cr,  st*sr-ct*sp*cr},
							 {          st*cp,    sp,           ct*cp}};
		N[X] = -M[2][0];
		N[Y] = -M[2][1];
		N[Z] = -M[2][2];
		V[X] = M[1][0];
		V[Y] = M[1][1];
		V[Z] = M[1][2];
		U[X] = M[0][0];
		U[Y] = M[0][1];
		U[Z] = M[0][2];
		C[X] = -N[X]*Distance;
		C[Y] = -N[Y]*Distance;
		C[Z] = -N[Z]*Distance;

		// Compute the point projections to determine the max/min x and y
		// Uses standard viewing transform
		int MinX = DisplayWidth-1, MaxX = 0, MinY = DisplayHeight-1, MaxY = 0;
		int[][] PointProjections = new int[Points.length][2];
		for(int CurrentPoint = 0 ; CurrentPoint < Points.length ; CurrentPoint++)
			{
			double XDiff = Points[CurrentPoint][X]-C[X];
			double YDiff = Points[CurrentPoint][Y]-C[Y];
			double ZDiff = Points[CurrentPoint][Z]-C[Z];
			double ZPos = N[X]*XDiff+N[Y]*YDiff+N[Z]*ZDiff;
			double XPos = (((U[X]*XDiff+U[Y]*YDiff+U[Z]*ZDiff)/ZPos)+1.0)/2.0;
			double YPos = (((V[X]*XDiff+V[Y]*YDiff+V[Z]*ZDiff)/ZPos)+1.0)/2.0;
			int XCoord = (int)(XPos*DisplayWidth);
			int YCoord = DisplayHeight-(int)(YPos*DisplayHeight);
			PointProjections[CurrentPoint][X] = XCoord;
			PointProjections[CurrentPoint][Y] = YCoord;
			if(XCoord < MinX)
				MinX = XCoord;
			if(XCoord > MaxX)
				MaxX = XCoord;
			if(YCoord < MinY)
				MinY = YCoord;
			if(YCoord > MaxY)
				MaxY = YCoord;
			}
		if(MinX < 0)
			MinX = 0;
		if(MaxX >= DisplayWidth)
			MaxX = DisplayWidth-1;
		if(MinY < 0)
			MinY = 0;
		if(MaxY >= DisplayHeight)
			MaxY = DisplayHeight-1;

		// Determine the bounding box for each triangle
		int[] TriangleMinX = new int[Triangles.length];
		int[] TriangleMaxX = new int[Triangles.length];
		int[] TriangleMinY = new int[Triangles.length];
		int[] TriangleMaxY = new int[Triangles.length];
		for(int CurrentTriangle = 0 ; CurrentTriangle < Triangles.length ; CurrentTriangle++)
			{
			TriangleMinX[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][0]][X];
			TriangleMaxX[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][0]][X];
			TriangleMinY[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][0]][Y];
			TriangleMaxY[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][0]][Y];
			if(PointProjections[Triangles[CurrentTriangle][1]][X] < TriangleMinX[CurrentTriangle])
				TriangleMinX[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][1]][X];
			if(PointProjections[Triangles[CurrentTriangle][1]][X] > TriangleMaxX[CurrentTriangle])
				TriangleMaxX[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][1]][X];
			if(PointProjections[Triangles[CurrentTriangle][1]][Y] < TriangleMinY[CurrentTriangle])
				TriangleMinY[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][1]][Y];
			if(PointProjections[Triangles[CurrentTriangle][1]][Y] > TriangleMaxY[CurrentTriangle])
				TriangleMaxY[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][1]][Y];
			if(PointProjections[Triangles[CurrentTriangle][2]][X] < TriangleMinX[CurrentTriangle])
				TriangleMinX[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][2]][X];
			if(PointProjections[Triangles[CurrentTriangle][2]][X] > TriangleMaxX[CurrentTriangle])
				TriangleMaxX[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][2]][X];
			if(PointProjections[Triangles[CurrentTriangle][2]][Y] < TriangleMinY[CurrentTriangle])
				TriangleMinY[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][2]][Y];
			if(PointProjections[Triangles[CurrentTriangle][2]][Y] > TriangleMaxY[CurrentTriangle])
				TriangleMaxY[CurrentTriangle] = PointProjections[Triangles[CurrentTriangle][2]][Y];
			}

		// Ray-trace to determine the polygon (if any) displayed by each pixel
		for(int DispY = MinY ; DispY <= MaxY ; DispY++)
			{
			for(int DispX = MinX ; DispX <= MaxX ; DispX++)
				{
				// From DispX and DispY, alter the N vector to point at that part of the screen
				double[] ModifiedN = {((double)DispX/(double)DisplayWidth)*2.0-1.0, ((double)(DisplayHeight-DispY)/(double)DisplayHeight)*2.0-1.0, -1.0};
				// Transform the modified N according to the viewing transform - Now it is being fired from the camera with the correct orientation
				double[] ViewRay = {ModifiedN[X]*M[0][0]+ModifiedN[Y]*M[1][0]+ModifiedN[Z]*M[2][0], ModifiedN[X]*M[0][1]+ModifiedN[Y]*M[1][1]+ModifiedN[Z]*M[2][1], ModifiedN[X]*M[0][2]+ModifiedN[Y]*M[1][2]+ModifiedN[Z]*M[2][2]};
				double Nearestt = INVALIDt;
				double[] Nearestp = null;
				for(int CurrentTriangle = 0 ; CurrentTriangle < Triangles.length ; CurrentTriangle++)
					{
					// If outside the bounding box, the point is not from this triangle
					if((DispX < TriangleMinX[CurrentTriangle]) || (TriangleMaxX[CurrentTriangle] < DispX) || (DispY < TriangleMinY[CurrentTriangle]) || (TriangleMaxY[CurrentTriangle] < DispY))
						continue;
					double t = (TriangleD[CurrentTriangle]-TriangleN[CurrentTriangle][X]*C[X]-TriangleN[CurrentTriangle][Y]*C[Y]-TriangleN[CurrentTriangle][Z]*C[Z])/(TriangleN[CurrentTriangle][X]*ViewRay[X]+TriangleN[CurrentTriangle][Y]*ViewRay[Y]+TriangleN[CurrentTriangle][Z]*ViewRay[Z]);
					// If not closer than what we already have, ignore it - simulated Z-Buffering
					if(t >= Nearestt)
						continue;
					// The point on the triangle
					double[] p = {C[X]+ViewRay[X]*t, C[Y]+ViewRay[Y]*t, C[Z]+ViewRay[Z]*t};
					// Vectors from corner 0 of the triangle to p, and each of the other two points of the triangle
					double[] P = {p[X]-Points[Triangles[CurrentTriangle][0]][X], p[Y]-Points[Triangles[CurrentTriangle][0]][Y], p[Z]-Points[Triangles[CurrentTriangle][0]][Z]};
					double[] A = TriangleEdgeA[CurrentTriangle];
					double[] B = TriangleEdgeB[CurrentTriangle];
					/* Edges A and B are linearly independant and span the plane of the triangle
					So:
					  [Ax]     [Bx]   [Px]
					a*[Ay] + b*[By] = [Py]
					  [Az]     [Bz]   [Pz]
					for some a and b.  If a >= 0 and b >= 0 and a+b <= 1, then P is inside the
					triangle made by [0, 0, 0], A, and B.  This means p is a point inside the
					original triangle.
					Note: Three equations in two unknowns - Pick the two that will yield the
					smallest rounding error and solve for a and b. */
					double a = 0.0, b = 0.0;
					double Det1 = A[X]*B[Y]-B[X]*A[Y];
					double Det2 = A[X]*B[Z]-B[X]*A[Z];
					double Det3 = A[Y]*B[Z]-B[Y]*A[Z];
					if((Math.abs(Det1) >= Math.abs(Det2)) && (Math.abs(Det1) >= Math.abs(Det3)))
						{
						a = (B[Y]*P[X]-B[X]*P[Y])/Det1;
						b = (-A[Y]*P[X]+A[X]*P[Y])/Det1;
						}
					else if(Math.abs(Det2) >= Math.abs(Det3))
						{
						a = (B[Z]*P[X]-B[X]*P[Z])/Det2;
						b = (-A[Z]*P[X]+A[X]*P[Z])/Det2;
						}
					else
						{
						a = (B[Z]*P[Y]-B[Y]*P[Z])/Det3;
						b = (-A[Z]*P[Y]+A[Y]*P[Z])/Det3;
						}
					// Inside the triangle?
					if((a >= 0.0) && (b >= 0.0) && (a+b <= 1.0))
						{
						Nearestt = t;
						Nearestp = p;
						}
					}
				if(Nearestt != INVALIDt)
					{
					double SourceXPercent = -1.0, SourceYPercent = -1.0;
					switch(MappingType)
						{
						case 0:
							SourceXPercent = Nearestp[X]+0.5;
							SourceYPercent = 1.0-(Nearestp[Y]+0.5);
							break;
						case 1:
							SourceXPercent = (Math.atan2(Nearestp[X], Nearestp[Z])/(2.0*Math.PI))+0.5;
							SourceYPercent = 1.0-(Nearestp[Y]+0.5);
							break;
						case 2:
							SourceXPercent = (Math.atan2(Nearestp[X], Nearestp[Z])/(2.0*Math.PI))+0.5;
							double XsZs = Nearestp[X]*Nearestp[X]+Nearestp[Z]*Nearestp[Z];
							if(XsZs != 0.0)
								SourceYPercent = 1.0-((Math.atan(Nearestp[Y]/Math.sqrt(XsZs))/Math.PI)+0.5);
							break;
						}
						int SourceX = (int)((double)(SourceWidth-1)*SourceXPercent);
						int SourceY = (int)((double)(SourceHeight-1)*SourceYPercent);
						try
							{
							DisplayBitmap[DispX+DispY*DisplayWidth] = SourceBitmap[SourceX+SourceY*SourceWidth];
							}
						catch(ArrayIndexOutOfBoundsException AIOOBE)
							{
							;  // Avoid a potential problem due to rounding
							}
					}
				}
			}
		}
	}

final class DisplayCanvas extends Canvas
	{
	private static final int DESIRED_SIZE = 200;
	private Image DisplayImage = null;

	public Dimension getDimension()
		{
		return size();
		}

	public void setImage(Image DisplayImage)
		{
		this.DisplayImage = DisplayImage;
		repaint();
		}

	public void update(Graphics g)
		{
		paint(g);
		}

	public void paint(Graphics g)
		{
		if(DisplayImage != null)
			{
			g.drawImage(DisplayImage, 0, 0, size().width, size().height, this);
			}
		else
			{
			g.setColor(getBackground());
			g.fillRect(0, 0, size().width, size().height);
			g.setColor(getForeground());
			g.drawString("No image.", 10, 10);
			}
		}											  

	public Dimension minimumSize()
		{
		return new Dimension(DESIRED_SIZE, DESIRED_SIZE);
		}

	public Dimension preferredSize()
		{
		return new Dimension(DESIRED_SIZE, DESIRED_SIZE);
		}
	}
