//
import java.awt.*;

// A VERY SIMPLE 3D RENDERER BUILT IN JAVA 1.0 - KEN PERLIN

public class SimpleRender extends BufferedApplet
{
//----- THIS SECTION CONTAINS THE INPUT DATA FOR THE SCENE -----

   public Color bgColor = Color.black;
   public double[] light = {1,1,1};
   public Shape[] shape = {new Shape()};
   public double[][] colors = { {1,1,1}, };
   public double[][][] transformations = { {{0,0,0}, {0,0,0}, {1,1,1}}, };
   public boolean outline = false;

// THE RENDER ROUTINE IS CALLED BY THE APPLET ONCE PER FRAME

   public void render(Graphics g) {
      if (width == 0)
         initialize();                        // FIRST IME ONLY - INITIALIZE

      animate();                        // MOVE THE SHAPES MATRICES
      transform();                        // APPLY MATRICES TO ALL VERTICES
      shade();                                // LIGHT ALL POLYGON FACES
      sort();                                // SORT SHAPES FROM BACK TO FRONT
      draw(g);                                // DRAW EVERYTHING TO AN IMAGE
      renderWidgets(g);

      debug = false;
   }

//--------------------------------------------------------------

   int width = 0, height = 0;                // SIZE OF THE APPLET WINDOW
   boolean debug = true;                // TO PRINT THINGS AT FRAME ONE
   double theta = 0, phi = 0;                // VIEW ROTATION PARAMETERS

   private double[][] camera;                // 4x4 MATRIX OF CAMERA TRANSFORM
   private double[][][] matrices;        // FOR EACH SHAPE, A 4x4 MATRIX
   private Color [][] rgb;                // FOR EACH FACE, A RENDERED RGB
   private double[][][] transformed;        // TRANSFORM EACH VERTEX
   private int[][][] projected;                // PROJECT EACH TRANSFORMED VERTEX
   private int[] order;                        // BACK-TO-FRONT SHAPE ORDERING

/// DEFINE THE ANIMATION  MATRIX FOR THIS FRAME FOR AL SHAPES

   private void animate() {
      for (int i = 0 ; i < shape.length ; i++) {
         shape[i].setColor(colors[i]);
         shape[i].setTranslation(transformations[i][0]);
         shape[i].setRotation(transformations[i][1]);
         shape[i].setScale(transformations[i][2]);
      }
   }

// TRANSFORM ALL VERTICES TO THEIR VIEWED POSITION

   private void transform() {

      double[][] matrix = new double[4][4];

// CREATE CAMERA MATRIX AND APPLY IT TO ALL SHAPE TRANSFORMATIONS

      Matrix.identity(camera);                     // BUILD CAMERA MATRIX FROM
      Matrix.rotateX(camera, phi);                 // phi,theta (DEFINED BY
      Matrix.rotateY(camera, theta);               // USER'S MOUSE DRAGGING)

      for (int i = 0 ; i < shape.length ; i++) {  // ADD CAMERA TO ALL
         shape[i].getMatrix(matrix);
         Matrix.postMultiply(matrix, camera); // SHAPE TRANSFORMATIONS

// FOR ALL VERTICES, TRANSFORM AND PROJECT TO SCREEN

         for (int j = 0 ; j < shape[i].vertices.length ; j++) {
            transformVertex(matrix, shape[i].vertices[j], transformed[i][j]);
            projectVertex  (transformed[i][j], projected[i][j]);
         }
      }
   }

// FOR ALL SHAPES, FOR ALL FACES, SHADE THAT FACE FOR THIS FRAME

   private void shade() {
      double[] color = new double[3];

      for (int i = 0 ; i < shape.length ; i++)
      for (int j = 0 ; j < shape[i].face.length ; j++) {
         shape[i].getColor(color);
         rgb[i][j] = shadeFace(color, shape[i].face[j], transformed[i]);
      }
   }

// FIGURE OUT THE BACK-TO-FRONT DRAWING ORDER FOR SHAPES.

   private void sort() {

// FOR ALL SHAPES: COMPUTE ITS LARGEST (IE: NEAREST TO CAMERA) Z

      double[] z = new double[shape.length];
      for (int i = 0 ; i < z.length ; i++) {
         order[i] = i;
         z[i] = distSqr(i);
      }

// SORT THE SHAPE DISPLAY ORDER SO NEAREST SHAPES WILL RENDER LAST

      for (int i = 0 ; i < z.length-1 ; i++)
      for (int j = i+1 ; j < z.length ; j++)
         if (z[order[i]] > z[order[j]]) {
            int tmp = order[i];
            order[i] = order[j];
            order[j] = tmp;
         }
   }

// LARGEST (IE: NEAREST TO CAMERA) Z OF SHAPE'S TRANSFORMED VERTICES

   private double distSqr(int id) {
      double[] sum = {0,0,0};
      for (int j = 0 ; j < transformed[id].length ; j++)
         for (int k = 0 ; k < 3 ; k++)
            sum[k] += transformed[id][j][k];
      double x = sum[0] / transformed[id].length;
      double y = sum[1] / transformed[id].length;
      double z = sum[2] / transformed[id].length - 10;
      return -(x*x + y*y + z*z);
   }

// DRAW THE SCENE

   private void draw(Graphics g) {

// DRAW A BLACK BACKGROUND

      g.setColor(bgColor);
      g.fillRect(0,0,width,height);

// FOR EACH SHAPE (IN BACK-TO-FRONT ORDER), DRAW EACH FACE

      for (int i = 0 ; i < shape.length ; i++) {
         int I = order[i];
         for (int j = 0 ; j < shape[I].face.length ; j++) {
            g.setColor(rgb[I][j]);
            drawFace(g, shape[I].face[j], projected[I]);
         }
      }
   }

// TRANSFORM A VERTEX ACCORDING TO IT'S SHAPE'S MATRIX

   private void transformVertex(double[][] matrix, double[] src, double[] dst) {
      for (int i = 0 ; i < 3 ; i++)
         dst[i] = Vec.dot(matrix[i], src) + matrix[i][3];
   }

// PROJECT A TRANSFORMED VERTEX ONTO A PIXEL, WITH PERSPECTIVE.

   private void projectVertex(double[] src, int[] dst) {
      dst[0] = (int) ( width /2 + width  * src[0] / (10 - src[2]) );
      dst[1] = (int) ( height/2 - height * src[1] / (10 - src[2]) );
   }

// COMPUTE FINAL DISPLAYED COLOR OF ONE POLYGON FACE

   private Color shadeFace(double[] color, int[] face, double[][] verts) {
      int R = 0, G = 0, B = 0;

// USE THREE SUCCESSIVE TRANSFORMED VERTICES a,b,c

      double[] a = verts[face[0]];
      double[] b = verts[face[1]];
      double[] c = verts[face[2]];

// COMPUTE EDGE VECTORS u = b-a AND v = c-b

      double[] u = {b[0]-a[0], b[1]-a[1], b[2]-a[2]};
      double[] v = {c[0]-b[0], c[1]-b[1], c[2]-b[2]};

// TAKE CROSS PRODUCT OF NORMALIZED u,w TO GET FACE NORMAL w

      double[] w = new double[3];
      Vec.normalize(u);
      Vec.normalize(v);
      Vec.cross(u,v,w);

// MAKE FACE BRIGHTER THE MORE IT FACES THE LIGHT SOURCE DIRECTION

      double s = 100 * (1 - Vec.dot(w,light));
      R = (int) (color[0] * s);
      G = (int) (color[1] * s);
      B = (int) (color[2] * s);

// MULTIPLY BY THE SHAPE'S OVERALL COLOR.

      R = Math.max(0, Math.min(255, R));
      G = Math.max(0, Math.min(255, G));
      B = Math.max(0, Math.min(255, B));

      return new Color(R,G,B);
   }

// DRAW ONE FACE

   private void drawFace(Graphics g, int[] face, int[][] verts) {

// ARRANGE THE PROJECTED X AND Y VERTICES INTO THEIR ORDER IN THE FACE.

      int[] X = new int[face.length];
      int[] Y = new int[face.length];

      for (int i = 0 ; i < face.length ; i++) {
         X[i] = verts[face[i]][0]; 
         Y[i] = verts[face[i]][1]; 
      }

// IF FACE IS TOWARD CAMERA (IE: PROJECTED AREA IS POSITIVE) THEN DRAW IT.

      if (area(X,Y) > 0) {
         g.fillPolygon(X, Y, face.length);
	 if (outline) {
	    g.setColor(Color.white);
            g.drawPolygon(X, Y, face.length);
         }
      }
   }

// COMPUTE THE AREA OF A 2D POLYGON

   private int area(int[] X, int[] Y) {
      int sum = 0;
      for (int i = 0 ; i < X.length ; i++) {
         int j = (i + 1) % X.length;
         sum += (X[j] - X[i]) * (Y[i] + Y[j]);
      }
      return sum / 2;
   }

// INITIALIZE USEFUL PARAMETERS AND ALL ARRAYS (ONLY CALLED ONCE).

   public void initialize() {
      width = bounds().width;
      height = bounds().height;

      camera = new double[4][4];
      matrices = new double[shape.length][4][4];
      for (int i = 0 ; i < shape.length ; i++)
         Matrix.identity(matrices[i]);

      rgb = new Color[shape.length][];
      for (int i = 0 ; i < shape.length ; i++)
         rgb[i] = new Color[shape[i].face.length];

      transformed = new double[shape.length][][];
      for (int i = 0 ; i < shape.length ; i++)
         transformed[i] = new double[shape[i].vertices.length][3];

      projected = new int[shape.length][][];
      for (int i = 0 ; i < shape.length ; i++)
         projected[i] = new int[shape[i].vertices.length][2];

      order = new int[shape.length];

      update();
   }

   private int prevX = 0, prevY = 0;

// ON MOUSE DOWN, RECORD THE MOUSE POSITION

   public boolean mouseDown(Event e, int x, int y) {
      if (!widgetsDown(x,y)) {
         prevX = x;
         prevY = y;
      }
      return true;
   }

   public void update() { // IF SLIDERS OR BUTTONS CHANGE
   }

// USE CHANGE IN x,y TO MODIFY CAMERA theta,phi ROTATIONS

   public boolean mouseDrag(Event e, int x, int y) {
      if (widgetsDrag(x,y))
         update();
      else {
         theta += 5. / width * (x - prevX);
         phi   += 5. / width * (y - prevY);
         prevX = x;
         prevY = y;
      }
      return true;
   }

   public boolean mouseUp(Event e, int x, int y) {
      if (widgetsUp(x, y))
	 update();
      return true;
   }

//----------------- HANDLE SLIDERS

   public Slider[] slider = new Slider[100];
   public Button[] button = new Button[100];
   public int nSliders = 0, nButtons = 0;

   void addButton(String[] labels) { addButton(labels,100,15*nButtons); }
   void addButton(String[] labels, int x, int y) {
      Button b = new Button(x, y, 50, 15);
      button[nButtons] = b;
      b.labels = labels;
      nButtons++;
   }
   void addSlider(String label) { addSlider(label,0,15*nSliders); }
   void addSlider(String label, int x, int y) {
      Slider s = new Slider(x, y, 100, 15);
      slider[nSliders] = s;
      s.label = label;
      s.setValue(0.5);
      nSliders++;
   }
   void renderWidgets(Graphics g) {
      for (int i = 0 ; i < nSliders ; i++)
         slider[i].render(g);
      for (int i = 0 ; i < nButtons ; i++)
         button[i].render(g);
   }
   boolean widgetsDown(int x, int y) {
      for (int i = 0 ; i < nSliders ; i++)
         if (slider[i].down(x,y))
            return true;
      for (int i = 0 ; i < nButtons ; i++)
         if (button[i].down(x,y))
            return true;
      return false;
   }
   boolean widgetsDrag(int x, int y) {
      for (int i = 0 ; i < nSliders ; i++)
         if (slider[i].drag(x,y))
            return true;
      for (int i = 0 ; i < nButtons ; i++)
         if (button[i].drag(x,y))
            return true;
      return false;
   }
   boolean widgetsUp(int x, int y) {
      for (int i = 0 ; i < nSliders ; i++)
         if (slider[i].up(x,y))
            return true;
      for (int i = 0 ; i < nButtons ; i++)
         if (button[i].up(x,y))
            return true;
      return false;
   }
}