/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org

  Copyright (c) 2004-08 Ben Fry and Casey Reas
  Copyright (c) 2001-04 Massachusetts Institute of Technology

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General
  Public License along with this library; if not, write to the
  Free Software Foundation, Inc., 59 Temple Place, Suite 330,
  Boston, MA  02111-1307  USA
*/

package processing.core;

import java.awt.Toolkit;
import java.awt.image.*;
import java.util.*;


/**
 * Subclass of PGraphics that handles 3D rendering.
 * It can render 3D inside a browser window and requires no plug-ins.
 * <p/>
 * The renderer is mostly set up based on the structure of the OpenGL API,
 * if you have questions about specifics that aren't covered here,
 * look for reference on the OpenGL implementation of a similar feature.
 * <p/>
 * Lighting and camera implementation by Simon Greenwold.
 */
public class PGraphics3D extends PGraphics {

  /** The depth buffer. */
  public float[] zbuffer;

  // ........................................................

  /** The modelview matrix. */
  public PMatrix3D modelview;

  /** Inverse modelview matrix, used for lighting. */
  public PMatrix3D modelviewInv;

  /**
   * The camera matrix, the modelview will be set to this on beginDraw.
   */
  public PMatrix3D camera;

  /** Inverse camera matrix */
  protected PMatrix3D cameraInv;

  /** Camera field of view. */
  public float cameraFOV;

  /** Position of the camera. */
  public float cameraX, cameraY, cameraZ;
  public float cameraNear, cameraFar;
  /** Aspect ratio of camera's view. */
  public float cameraAspect;

  /** Current projection matrix. */
  public PMatrix3D projection;


  //////////////////////////////////////////////////////////////


  /**
   * Maximum lights by default is 8, which is arbitrary for this renderer,
   * but is the minimum defined by OpenGL
   */
  public static final int MAX_LIGHTS = 8;

  public int lightCount = 0;

  /** Light types */
  public int[] lightType;

  /** Light positions */
  //public float[][] lightPosition;
  public PVector[] lightPosition;

  /** Light direction (normalized vector) */
  //public float[][] lightNormal;
  public PVector[] lightNormal;

  /** Light falloff */
  public float[] lightFalloffConstant;
  public float[] lightFalloffLinear;
  public float[] lightFalloffQuadratic;

  /** Light spot angle */
  public float[] lightSpotAngle;

  /** Cosine of light spot angle */
  public float[] lightSpotAngleCos;

  /** Light spot concentration */
  public float[] lightSpotConcentration;

  /** Diffuse colors for lights.
   *  For an ambient light, this will hold the ambient color.
   *  Internally these are stored as numbers between 0 and 1. */
  public float[][] lightDiffuse;

  /** Specular colors for lights.
      Internally these are stored as numbers between 0 and 1. */
  public float[][] lightSpecular;

  /** Current specular color for lighting */
  public float[] currentLightSpecular;

  /** Current light falloff */
  public float currentLightFalloffConstant;
  public float currentLightFalloffLinear;
  public float currentLightFalloffQuadratic;


  //////////////////////////////////////////////////////////////


  static public final int TRI_DIFFUSE_R = 0;
  static public final int TRI_DIFFUSE_G = 1;
  static public final int TRI_DIFFUSE_B = 2;
  static public final int TRI_DIFFUSE_A = 3;
  static public final int TRI_SPECULAR_R = 4;
  static public final int TRI_SPECULAR_G = 5;
  static public final int TRI_SPECULAR_B = 6;
  static public final int TRI_COLOR_COUNT = 7;

  // ........................................................

  // Whether or not we have to worry about vertex position for lighting calcs
  private boolean lightingDependsOnVertexPosition;

  static final int LIGHT_AMBIENT_R = 0;
  static final int LIGHT_AMBIENT_G = 1;
  static final int LIGHT_AMBIENT_B = 2;
  static final int LIGHT_DIFFUSE_R = 3;
  static final int LIGHT_DIFFUSE_G = 4;
  static final int LIGHT_DIFFUSE_B = 5;
  static final int LIGHT_SPECULAR_R = 6;
  static final int LIGHT_SPECULAR_G = 7;
  static final int LIGHT_SPECULAR_B = 8;
  static final int LIGHT_COLOR_COUNT = 9;

  // Used to shuttle lighting calcs around
  // (no need to re-allocate all the time)
  protected float[] tempLightingContribution = new float[LIGHT_COLOR_COUNT];
//  protected float[] worldNormal = new float[4];

  /// Used in lightTriangle(). Allocated here once to avoid re-allocating
  protected PVector lightTriangleNorm = new PVector();

  // ........................................................

  /**
   * This is turned on at beginCamera, and off at endCamera
   * Currently we don't support nested begin/end cameras.
   * If we wanted to, this variable would have to become a stack.
   */
  protected boolean manipulatingCamera;

  float[][] matrixStack = new float[MATRIX_STACK_DEPTH][16];
  float[][] matrixInvStack = new float[MATRIX_STACK_DEPTH][16];
  int matrixStackDepth;

  // These two matrices always point to either the modelview
  // or the modelviewInv, but they are swapped during
  // when in camera manipulation mode. That way camera transforms
  // are automatically accumulated in inverse on the modelview matrix.
  protected PMatrix3D forwardTransform;
  protected PMatrix3D reverseTransform;

  // Added by ewjordan for accurate texturing purposes. Screen plane is
  // not scaled to pixel-size, so these manually keep track of its size
  // from frustum() calls. Sorry to add public vars, is there a way
  // to compute these from something publicly available without matrix ops?
  // (used once per triangle in PTriangle with ENABLE_ACCURATE_TEXTURES)
  protected float leftScreen;
  protected float rightScreen;
  protected float topScreen;
  protected float bottomScreen;
  protected float nearPlane; //depth of near clipping plane

  /** true if frustum has been called to set perspective, false if ortho */
  private boolean frustumMode = false;

  /**
   * Use PSmoothTriangle for rendering instead of PTriangle?
   * Usually set by calling smooth() or noSmooth()
   */
  static protected boolean s_enableAccurateTextures = false; //maybe just use smooth instead?

  /** Used for anti-aliased and perspective corrected rendering. */
  public PSmoothTriangle smoothTriangle;


  // ........................................................

  // pos of first vertex of current shape in vertices array
  protected int shapeFirst;

  // i think vertex_end is actually the last vertex in the current shape
  // and is separate from vertexCount for occasions where drawing happens
  // on endDraw() with all the triangles being depth sorted
  protected int shapeLast;

  // vertices may be added during clipping against the near plane.
  protected int shapeLastPlusClipped;

  // used for sorting points when triangulating a polygon
  // warning - maximum number of vertices for a polygon is DEFAULT_VERTICES
  protected int vertexOrder[] = new int[DEFAULT_VERTICES];

  // ........................................................

  // This is done to keep track of start/stop information for lines in the
  // line array, so that lines can be shown as a single path, rather than just
  // individual segments. Currently only in use inside PGraphicsOpenGL.
  protected int pathCount;
  protected int[] pathOffset = new int[64];
  protected int[] pathLength = new int[64];

  // ........................................................

  // line & triangle fields (note that these overlap)
//  static protected final int INDEX = 0;          // shape index
  static protected final int VERTEX1 = 0;
  static protected final int VERTEX2 = 1;
  static protected final int VERTEX3 = 2;        // (triangles only)
  /** used to store the strokeColor int for efficient drawing. */
  static protected final int STROKE_COLOR = 1;   // (points only)
  static protected final int TEXTURE_INDEX = 3;  // (triangles only)
  //static protected final int STROKE_MODE = 2;    // (lines only)
  //static protected final int STROKE_WEIGHT = 3;  // (lines only)

  static protected final int POINT_FIELD_COUNT = 2;  //4
  static protected final int LINE_FIELD_COUNT = 2;  //4
  static protected final int TRIANGLE_FIELD_COUNT = 4;

  // points
  static final int DEFAULT_POINTS = 512;
  protected int[][] points = new int[DEFAULT_POINTS][POINT_FIELD_COUNT];
  protected int pointCount;

  // lines
  static final int DEFAULT_LINES = 512;
  public PLine line;  // used for drawing
  protected int[][] lines = new int[DEFAULT_LINES][LINE_FIELD_COUNT];
  protected int lineCount;

  // triangles
  static final int DEFAULT_TRIANGLES = 256;
  public PTriangle triangle;
  protected int[][] triangles =
    new int[DEFAULT_TRIANGLES][TRIANGLE_FIELD_COUNT];
  protected float triangleColors[][][] =
    new float[DEFAULT_TRIANGLES][3][TRI_COLOR_COUNT];
  protected int triangleCount;   // total number of triangles

  // cheap picking someday
  //public int shape_index;

  // ........................................................

  static final int DEFAULT_TEXTURES = 3;
  protected PImage[] textures = new PImage[DEFAULT_TEXTURES];
  int textureIndex;

  // ........................................................

  DirectColorModel cm;
  MemoryImageSource mis;


  //////////////////////////////////////////////////////////////


  public PGraphics3D() { }


  //public void setParent(PApplet parent)


  //public void setPrimary(boolean primary)


  //public void setPath(String path)


  /**
   * Called in response to a resize event, handles setting the
   * new width and height internally, as well as re-allocating
   * the pixel buffer for the new size.
   *
   * Note that this will nuke any cameraMode() settings.
   */
  public void setSize(int iwidth, int iheight) {  // ignore
    width = iwidth;
    height = iheight;
    width1 = width - 1;
    height1 = height - 1;

    allocate();
    reapplySettings();

    // init lights (in resize() instead of allocate() b/c needed by opengl)
    lightType = new int[MAX_LIGHTS];
    lightPosition = new PVector[MAX_LIGHTS];
    lightNormal = new PVector[MAX_LIGHTS];
    for (int i = 0; i < MAX_LIGHTS; i++) {
      lightPosition[i] = new PVector();
      lightNormal[i] = new PVector();
    }
    lightDiffuse = new float[MAX_LIGHTS][3];
    lightSpecular = new float[MAX_LIGHTS][3];
    lightFalloffConstant = new float[MAX_LIGHTS];
    lightFalloffLinear = new float[MAX_LIGHTS];
    lightFalloffQuadratic = new float[MAX_LIGHTS];
    lightSpotAngle = new float[MAX_LIGHTS];
    lightSpotAngleCos = new float[MAX_LIGHTS];
    lightSpotConcentration = new float[MAX_LIGHTS];
    currentLightSpecular = new float[3];

    projection = new PMatrix3D();
    modelview = new PMatrix3D();
    modelviewInv = new PMatrix3D();

//    modelviewStack = new float[MATRIX_STACK_DEPTH][16];
//    modelviewInvStack = new float[MATRIX_STACK_DEPTH][16];
//    modelviewStackPointer = 0;

    forwardTransform = modelview;
    reverseTransform = modelviewInv;

    // init perspective projection based on new dimensions
    cameraFOV = 60 * DEG_TO_RAD; // at least for now
    cameraX = width / 2.0f;
    cameraY = height / 2.0f;
    cameraZ = cameraY / ((float) Math.tan(cameraFOV / 2.0f));
    cameraNear = cameraZ / 10.0f;
    cameraFar = cameraZ * 10.0f;
    cameraAspect = (float)width / (float)height;

    camera = new PMatrix3D();
    cameraInv = new PMatrix3D();

    // set up the default camera
//    camera();

    // defaults to perspective, if the user has setup up their
    // own projection, they'll need to fix it after resize anyway.
    // this helps the people who haven't set up their own projection.
//    perspective();
  }


  protected void allocate() {
    //System.out.println(this + " allocating for " + width + " " + height);
    //new Exception().printStackTrace();

    pixelCount = width * height;
    pixels = new int[pixelCount];
    zbuffer = new float[pixelCount];

    if (primarySurface) {
      cm = new DirectColorModel(32, 0x00ff0000, 0x0000ff00, 0x000000ff);;
      mis = new MemoryImageSource(width, height, pixels, 0, width);
      mis.setFullBufferUpdates(true);
      mis.setAnimated(true);
      image = Toolkit.getDefaultToolkit().createImage(mis);

    } else {
      // when not the main drawing surface, need to set the zbuffer,
      // because there's a possibility that background() will not be called
      Arrays.fill(zbuffer, Float.MAX_VALUE);
    }

    line = new PLine(this);
    triangle = new PTriangle(this);
    smoothTriangle = new PSmoothTriangle(this);
  }


  //public void dispose()


  ////////////////////////////////////////////////////////////


  //public boolean canDraw()


  public void beginDraw() {
    // need to call defaults(), but can only be done when it's ok
    // to draw (i.e. for opengl, no drawing can be done outside
    // beginDraw/endDraw).
    if (!settingsInited) defaultSettings();

    resetMatrix(); // reset model matrix

    // reset vertices
    vertexCount = 0;

    modelview.set(camera);
    modelviewInv.set(cameraInv);

    // clear out the lights, they'll have to be turned on again
    lightCount = 0;
    lightingDependsOnVertexPosition = false;
    lightFalloff(1, 0, 0);
    lightSpecular(0, 0, 0);

    /*
    // reset lines
    lineCount = 0;
    if (line != null) line.reset();  // is this necessary?
    pathCount = 0;

    // reset triangles
    triangleCount = 0;
    if (triangle != null) triangle.reset();  // necessary?
    */

    shapeFirst = 0;

    // reset textures
    textureIndex = 0;

    normal(0, 0, 1);
  }


  /**
   * See notes in PGraphics.
   * If z-sorting has been turned on, then the triangles will
   * all be quicksorted here (to make alpha work more properly)
   * and then blit to the screen.
   */
  public void endDraw() {
    // no need to z order and render
    // shapes were already rendered in endShape();
    // (but can't return, since needs to update memimgsrc)
    if (hints[ENABLE_DEPTH_SORT]) {
      flush();
    }
    if (mis != null) {
      mis.newPixels(pixels, cm, 0, width);
    }
    // mark pixels as having been updated, so that they'll work properly
    // when this PGraphics is drawn using image().
    updatePixels();
  }


  ////////////////////////////////////////////////////////////


  //protected void checkSettings()


  protected void defaultSettings() {
    super.defaultSettings();

    manipulatingCamera = false;
    forwardTransform = modelview;
    reverseTransform = modelviewInv;

    // set up the default camera
    camera();

    // defaults to perspective, if the user has setup up their
    // own projection, they'll need to fix it after resize anyway.
    // this helps the people who haven't set up their own projection.
    perspective();

    // easiest for beginners
    textureMode(IMAGE);

    emissive(0.0f);
    specular(0.5f);
    shininess(1.0f);
  }


  //protected void reapplySettings()


  ////////////////////////////////////////////////////////////


  public void hint(int which) {
    if (which == DISABLE_DEPTH_SORT) {
      flush();
    } else if (which == DISABLE_DEPTH_TEST) {
      if (zbuffer != null) {  // will be null in OpenGL and others
        Arrays.fill(zbuffer, Float.MAX_VALUE);
      }
    }
    super.hint(which);
  }


  //////////////////////////////////////////////////////////////


  //public void beginShape()


  public void beginShape(int kind) {
    shape = kind;

//    shape_index = shape_index + 1;
//    if (shape_index == -1) {
//      shape_index = 0;
//    }

    if (hints[ENABLE_DEPTH_SORT]) {
      // continue with previous vertex, line and triangle count
      // all shapes are rendered at endDraw();
      shapeFirst = vertexCount;
      shapeLast = 0;

    } else {
      // reset vertex, line and triangle information
      // every shape is rendered at endShape();
      vertexCount = 0;
      if (line != null) line.reset();  // necessary?
      lineCount = 0;
//      pathCount = 0;
      if (triangle != null) triangle.reset();  // necessary?
      triangleCount = 0;
    }

    textureImage = null;
    curveVertexCount = 0;
    normalMode = NORMAL_MODE_AUTO;
//    normalCount = 0;
  }


  //public void normal(float nx, float ny, float nz)


  //public void textureMode(int mode)


  public void texture(PImage image) {
    textureImage = image;

    if (textureIndex == textures.length - 1) {
      textures = (PImage[]) PApplet.expand(textures);
    }
    if (textures[textureIndex] != null) {  // ???
      textureIndex++;
    }
    textures[textureIndex] = image;
  }


  public void vertex(float x, float y) {
    // override so that the default 3D implementation will be used,
    // which will pick up all 3D settings (e.g. emissive, ambient)
    vertex(x, y, 0);
  }


  //public void vertex(float x, float y, float z)


  public void vertex(float x, float y, float u, float v) {
    // see vertex(x, y) for note
    vertex(x, y, 0, u, v);
  }


  //public void vertex(float x, float y, float z, float u, float v)


  //public void breakShape()


  //public void endShape()


  public void endShape(int mode) {
    shapeLast = vertexCount;
    shapeLastPlusClipped = shapeLast;

    // don't try to draw if there are no vertices
    // (fixes a bug in LINE_LOOP that re-adds a nonexistent vertex)
    if (vertexCount == 0) {
      shape = 0;
      return;
    }

    // convert points from model (X/Y/Z) to camera space (VX/VY/VZ).
    // Do this now because we will be clipping them on add_triangle.
    endShapeModelToCamera(shapeFirst, shapeLast);

    if (stroke) {
      endShapeStroke(mode);
    }

    if (fill || textureImage != null) {
      endShapeFill();
    }

    // transform, light, and clip
    endShapeLighting(lightCount > 0 && fill);

    // convert points from camera space (VX, VY, VZ) to screen space (X, Y, Z)
    // (this appears to be wasted time with the OpenGL renderer)
    endShapeCameraToScreen(shapeFirst, shapeLastPlusClipped);

    // render shape and fill here if not saving the shapes for later
    // if true, the shapes will be rendered on endDraw
    if (!hints[ENABLE_DEPTH_SORT]) {
      if (fill || textureImage != null) {
        if (triangleCount > 0) {
          renderTriangles(0, triangleCount);
          if (raw != null) {
            rawTriangles(0, triangleCount);
          }
          triangleCount = 0;
        }
      }
      if (stroke) {
        if (pointCount > 0) {
          renderPoints(0, pointCount);
          if (raw != null) {
            rawPoints(0, pointCount);
          }
          pointCount = 0;
        }

        if (lineCount > 0) {
          renderLines(0, lineCount);
          if (raw != null) {
            rawLines(0, lineCount);
          }
          lineCount = 0;
        }
      }
      pathCount = 0;
    }

    shape = 0;
  }


  protected void endShapeModelToCamera(int start, int stop) {
    for (int i = start; i < stop; i++) {
      float vertex[] = vertices[i];

      vertex[VX] =
        modelview.m00*vertex[X] + modelview.m01*vertex[Y] +
        modelview.m02*vertex[Z] + modelview.m03;
      vertex[VY] =
        modelview.m10*vertex[X] + modelview.m11*vertex[Y] +
        modelview.m12*vertex[Z] + modelview.m13;
      vertex[VZ] =
        modelview.m20*vertex[X] + modelview.m21*vertex[Y] +
        modelview.m22*vertex[Z] + modelview.m23;
      vertex[VW] =
        modelview.m30*vertex[X] + modelview.m31*vertex[Y] +
        modelview.m32*vertex[Z] + modelview.m33;

      // normalize
      if (vertex[VW] != 0 && vertex[VW] != 1) {
        vertex[VX] /= vertex[VW];
        vertex[VY] /= vertex[VW];
        vertex[VZ] /= vertex[VW];
      }
      vertex[VW] = 1;
    }
  }


  protected void endShapeStroke(int mode) {
    switch (shape) {
    case POINTS:
    {
      int stop = shapeLast;
      for (int i = shapeFirst; i < stop; i++) {
//        if (strokeWeight == 1) {
        addPoint(i);
//        } else {
//          addLineBreak();  // total overkill for points
//          addLine(i, i);
//        }
      }
    }
    break;

    case LINES:
    {
      // store index of first vertex
      int first = lineCount;
      int stop = shapeLast - 1;
      //increment = (shape == LINES) ? 2 : 1;

      // for LINE_STRIP and LINE_LOOP, make this all one path
      if (shape != LINES) addLineBreak();

      for (int i = shapeFirst; i < stop; i += 2) {
        // for LINES, make a new path for each segment
        if (shape == LINES) addLineBreak();
        addLine(i, i+1);
      }

      // for LINE_LOOP, close the loop with a final segment
      //if (shape == LINE_LOOP) {
      if (mode == CLOSE) {
        addLine(stop, lines[first][VERTEX1]);
      }
    }
    break;

    case TRIANGLES:
    {
      for (int i = shapeFirst; i < shapeLast-2; i += 3) {
        addLineBreak();
        //counter = i - vertex_start;
        addLine(i+0, i+1);
        addLine(i+1, i+2);
        addLine(i+2, i+0);
      }
    }
    break;

    case TRIANGLE_STRIP:
    {
      // first draw all vertices as a line strip
      int stop = shapeLast-1;

      addLineBreak();
      for (int i = shapeFirst; i < stop; i++) {
        //counter = i - vertex_start;
        addLine(i, i+1);
      }

      // then draw from vertex (n) to (n+2)
      stop = shapeLast-2;
      for (int i = shapeFirst; i < stop; i++) {
        addLineBreak();
        addLine(i, i+2);
      }
    }
    break;

    case TRIANGLE_FAN:
    {
      // this just draws a series of line segments
      // from the center to each exterior point
      for (int i = shapeFirst + 1; i < shapeLast; i++) {
        addLineBreak();
        addLine(shapeFirst, i);
      }

      // then a single line loop around the outside.
      addLineBreak();
      for (int i = shapeFirst + 1; i < shapeLast-1; i++) {
        addLine(i, i+1);
      }
      // closing the loop
      addLine(shapeLast-1, shapeFirst + 1);
    }
    break;

    case QUADS:
    {
      for (int i = shapeFirst; i < shapeLast; i += 4) {
        addLineBreak();
        //counter = i - vertex_start;
        addLine(i+0, i+1);
        addLine(i+1, i+2);
        addLine(i+2, i+3);
        addLine(i+3, i+0);
      }
    }
    break;

    case QUAD_STRIP:
    {
      for (int i = shapeFirst; i < shapeLast - 3; i += 2) {
        addLineBreak();
        addLine(i+0, i+2);
        addLine(i+2, i+3);
        addLine(i+3, i+1);
        addLine(i+1, i+0);
      }
    }
    break;

    case POLYGON:
    {
      // store index of first vertex
      int stop = shapeLast - 1;

      addLineBreak();
      for (int i = shapeFirst; i < stop; i++) {
        addLine(i, i+1);
      }
      if (mode == CLOSE) {
        // draw the last line connecting back to the first point in poly
        addLine(stop, shapeFirst); //lines[first][VERTEX1]);
      }
    }
    break;
    }
  }


  protected void endShapeFill() {
    switch (shape) {
    case TRIANGLE_FAN:
    {
      int stop = shapeLast - 1;
      for (int i = shapeFirst + 1; i < stop; i++) {
        addTriangle(shapeFirst, i, i+1);
      }
    }
    break;

    case TRIANGLES:
    {
      int stop = shapeLast - 2;
      for (int i = shapeFirst; i < stop; i += 3) {
        // have to switch between clockwise/counter-clockwise
        // otherwise the feller is backwards and renderer won't draw
        if ((i % 2) == 0) {
          addTriangle(i, i+2, i+1);
        } else {
          addTriangle(i, i+1, i+2);
        }
      }
    }
    break;

    case TRIANGLE_STRIP:
    {
      int stop = shapeLast - 2;
      for (int i = shapeFirst; i < stop; i++) {
        // have to switch between clockwise/counter-clockwise
        // otherwise the feller is backwards and renderer won't draw
        if ((i % 2) == 0) {
          addTriangle(i, i+2, i+1);
        } else {
          addTriangle(i, i+1, i+2);
        }
      }
    }
    break;

    case QUADS:
    {
      int stop = vertexCount-3;
      for (int i = shapeFirst; i < stop; i += 4) {
        // first triangle
        addTriangle(i, i+1, i+2);
        // second triangle
        addTriangle(i, i+2, i+3);
      }
    }
    break;

    case QUAD_STRIP:
    {
      int stop = vertexCount-3;
      for (int i = shapeFirst; i < stop; i += 2) {
        // first triangle
        addTriangle(i+0, i+2, i+1);
        // second triangle
        addTriangle(i+2, i+3, i+1);
      }
    }
    break;

    case POLYGON:
    {
      addPolygonTriangles();
    }
    break;
    }
  }


  protected void endShapeLighting(boolean lights) {
    if (lights) {
      // If the lighting does not depend on vertex position and there is a single
      // normal specified for this shape, go ahead and apply the same lighting
      // contribution to every vertex in this shape (one lighting calc!)
      if (!lightingDependsOnVertexPosition && normalMode == NORMAL_MODE_SHAPE) {
        calcLightingContribution(shapeFirst, tempLightingContribution);
        for (int tri = 0; tri < triangleCount; tri++) {
          lightTriangle(tri, tempLightingContribution);
        }
      } else {  // Otherwise light each triangle individually...
        for (int tri = 0; tri < triangleCount; tri++) {
          lightTriangle(tri);
        }
      }
    } else {
      for (int tri = 0; tri < triangleCount; tri++) {
        int index = triangles[tri][VERTEX1];
        copyPrelitVertexColor(tri, index, 0);
        index = triangles[tri][VERTEX2];
        copyPrelitVertexColor(tri, index, 1);
        index = triangles[tri][VERTEX3];
        copyPrelitVertexColor(tri, index, 2);
      }
    }
  }


  protected void endShapeCameraToScreen(int start, int stop) {
    for (int i = start; i < stop; i++) {
      float vx[] = vertices[i];

      float ox =
        projection.m00*vx[VX] + projection.m01*vx[VY] +
        projection.m02*vx[VZ] + projection.m03*vx[VW];
      float oy =
        projection.m10*vx[VX] + projection.m11*vx[VY] +
        projection.m12*vx[VZ] + projection.m13*vx[VW];
      float oz =
        projection.m20*vx[VX] + projection.m21*vx[VY] +
        projection.m22*vx[VZ] + projection.m23*vx[VW];
      float ow =
        projection.m30*vx[VX] + projection.m31*vx[VY] +
        projection.m32*vx[VZ] + projection.m33*vx[VW];

      if (ow != 0 && ow != 1) {
        ox /= ow; oy /= ow; oz /= ow;
      }

      vx[TX] = width * (1 + ox) / 2.0f;
      vx[TY] = height * (1 + oy) / 2.0f;
      vx[TZ] = (oz + 1) / 2.0f;
    }
  }



  /////////////////////////////////////////////////////////////////////////////

  // POINTS


  protected void addPoint(int a) {
    if (pointCount == points.length) {
      int[][] temp = new int[pointCount << 1][LINE_FIELD_COUNT];
      System.arraycopy(points, 0, temp, 0, lineCount);
      points = temp;
    }
    points[pointCount][VERTEX1] = a;
    //points[pointCount][STROKE_MODE] = strokeCap | strokeJoin;
    points[pointCount][STROKE_COLOR] = strokeColor;
    //points[pointCount][STROKE_WEIGHT] = (int) (strokeWeight + 0.5f); // hmm
    pointCount++;
  }


  protected void renderPoints(int start, int stop) {
    if (strokeWeight != 1) {
      for (int i = start; i < stop; i++) {
        float[] a = vertices[points[i][VERTEX1]];
        renderLineVertices(a, a);
      }
    } else {
      for (int i = start; i < stop; i++) {
        float[] a = vertices[points[i][VERTEX1]];
        int sx = (int) (a[TX] + 0.4999f);
        int sy = (int) (a[TY] + 0.4999f);
        if (sx >= 0 && sx < width && sy >= 0 && sy < height) {
          int index = sy*width + sx;
          pixels[index] = points[i][STROKE_COLOR];
          zbuffer[index] = a[TZ];
        }
      }
    }
  }


  // alternative implementations of point rendering code...

  /*
      int sx = (int) (screenX(x, y, z) + 0.5f);
      int sy = (int) (screenY(x, y, z) + 0.5f);

      int index = sy*width + sx;
      pixels[index] = strokeColor;
      zbuffer[index] = screenZ(x, y, z);

   */

  /*
  protected void renderPoints(int start, int stop) {
    for (int i = start; i < stop; i++) {
      float a[] = vertices[points[i][VERTEX1]];

      line.reset();

      line.setIntensities(a[SR], a[SG], a[SB], a[SA],
                          a[SR], a[SG], a[SB], a[SA]);

      line.setVertices(a[TX], a[TY], a[TZ],
                       a[TX] + 0.5f, a[TY] + 0.5f, a[TZ] + 0.5f);

      line.draw();
    }
  }
  */

  /*
  // handle points with an actual stroke weight (or scaled by renderer)
  private void point3(float x, float y, float z, int color) {
    // need to get scaled version of the stroke
    float x1 = screenX(x - 0.5f, y - 0.5f, z);
    float y1 = screenY(x - 0.5f, y - 0.5f, z);
    float x2 = screenX(x + 0.5f, y + 0.5f, z);
    float y2 = screenY(x + 0.5f, y + 0.5f, z);

    float weight = (abs(x2 - x1) + abs(y2 - y1)) / 2f;
    if (weight < 1.5f) {
      int xx = (int) ((x1 + x2) / 2f);
      int yy = (int) ((y1 + y2) / 2f);
      //point0(xx, yy, z, color);
      zbuffer[yy*width + xx] = screenZ(x, y, z);
      //stencil?

    } else {
      // actually has some weight, need to draw shapes instead
      // these will be
    }
  }
  */


  protected void rawPoints(int start, int stop) {
    raw.colorMode(RGB, 1);
    raw.noFill();
    raw.strokeWeight(vertices[lines[start][VERTEX1]][SW]);
    raw.beginShape(POINTS);

    for (int i = start; i < stop; i++) {
      float a[] = vertices[lines[i][VERTEX1]];

      if (raw.is3D()) {
        if (a[VW] != 0) {
          raw.stroke(a[SR], a[SG], a[SB], a[SA]);
          raw.vertex(a[VX] / a[VW], a[VY] / a[VW], a[VZ] / a[VW]);
        }
      } else {  // if is2D()
        raw.stroke(a[SR], a[SG], a[SB], a[SA]);
        raw.vertex(a[TX], a[TY]);
      }
    }
    raw.endShape();
  }



  /////////////////////////////////////////////////////////////////////////////

  // LINES


  /**
   * Begin a new section of stroked geometry.
   */
  protected final void addLineBreak() {
    if (pathCount == pathOffset.length) {
      pathOffset = PApplet.expand(pathOffset);
      pathLength = PApplet.expand(pathLength);
    }
    pathOffset[pathCount] = lineCount;
    pathLength[pathCount] = 0;
    pathCount++;
  }


  protected void addLine(int a, int b) {
    addLineWithClip(a, b);
  }


  protected final void addLineWithClip(int a, int b) {
    float az = vertices[a][VZ];
    float bz = vertices[b][VZ];
    if (az > cameraNear) {
      if (bz > cameraNear) {
        return;
      }
      int cb = interpolateClipVertex(a, b);
      addLineWithoutClip(cb, b);
      return;
    }
    else {
      if (bz <= cameraNear) {
        addLineWithoutClip(a, b);
        return;
      }
      int cb = interpolateClipVertex(a, b);
      addLineWithoutClip(a, cb);
      return;
    }
  }


  protected final void addLineWithoutClip(int a, int b) {
    if (lineCount == lines.length) {
      int temp[][] = new int[lineCount<<1][LINE_FIELD_COUNT];
      System.arraycopy(lines, 0, temp, 0, lineCount);
      lines = temp;
    }
    lines[lineCount][VERTEX1] = a;
    lines[lineCount][VERTEX2] = b;

    //lines[lineCount][STROKE_MODE] = strokeCap | strokeJoin;
    //lines[lineCount][STROKE_WEIGHT] = (int) (strokeWeight + 0.5f); // hmm
    lineCount++;

    // mark this piece as being part of the current path
    pathLength[pathCount-1]++;
  }


  protected void renderLines(int start, int stop) {
    for (int i = start; i < stop; i++) {
      renderLineVertices(vertices[lines[i][VERTEX1]],
                         vertices[lines[i][VERTEX2]]);
    }
  }


  protected void renderLineVertices(float[] a, float[] b) {
    // 2D hack added by ewjordan 6/13/07
    // Offset coordinates by a little bit if drawing 2D graphics.
    // http://dev.processing.org/bugs/show_bug.cgi?id=95

    // This hack fixes a bug caused by numerical precision issues when
    // applying the 3D transformations to coordinates in the screen plane
    // that should actually not be altered under said transformations.
    // It will not be applied if any transformations other than translations
    // are active, nor should it apply in OpenGL mode (PGraphicsOpenGL
    // overrides render_lines(), so this should be fine).
    // This fix exposes a last-pixel bug in the lineClipCode() function
    // of PLine.java, so that fix must remain in place if this one is used.

    // Note: the "true" fix for this bug is to change the pixel coverage
    // model so that the threshold for display does not lie on an integer
    // boundary. Search "diamond exit rule" for info the OpenGL approach.

    /*
      // removing for 0149 with the return of P2D
      if (drawing2D() && a[Z] == 0) {
        a[TX] += 0.01;
        a[TY] += 0.01;
        a[VX] += 0.01*a[VW];
        a[VY] += 0.01*a[VW];
        b[TX] += 0.01;
        b[TY] += 0.01;
        b[VX] += 0.01*b[VW];
        b[VY] += 0.01*b[VW];
      }
     */
    // end 2d-hack

    if (a[SW] > 1.25f || a[SW] < 0.75f) {
      float ox1 = a[TX];
      float oy1 = a[TY];
      float ox2 = b[TX];
      float oy2 = b[TY];

      // TODO strokeWeight should be transformed!
      float weight = a[SW] / 2;

      // when drawing points with stroke weight, need to extend a bit
      if (ox1 == ox2 && oy1 == oy2) {
        oy1 -= weight;
        oy2 += weight;
      }

      float dX = ox2 - ox1 + EPSILON;
      float dY = oy2 - oy1 + EPSILON;
      float len = (float) Math.sqrt(dX*dX + dY*dY);

      float rh = weight / len;

      float dx0 = rh * dY;
      float dy0 = rh * dX;
      float dx1 = rh * dY;
      float dy1 = rh * dX;

      float ax1 = ox1+dx0;
      float ay1 = oy1-dy0;

      float ax2 = ox1-dx0;
      float ay2 = oy1+dy0;

      float bx1 = ox2+dx1;
      float by1 = oy2-dy1;

      float bx2 = ox2-dx1;
      float by2 = oy2+dy1;

      if (smooth) {
        smoothTriangle.reset(3);
        smoothTriangle.smooth = true;
        smoothTriangle.interpARGB = true;  // ?

        // render first triangle for thick line
        smoothTriangle.setVertices(ax1, ay1, a[TZ],
                                   bx2, by2, b[TZ],
                                   ax2, ay2, a[TZ]);
        smoothTriangle.setIntensities(a[SR], a[SG], a[SB], a[SA],
                                      b[SR], b[SG], b[SB], b[SA],
                                      a[SR], a[SG], a[SB], a[SA]);
        smoothTriangle.render();

        // render second triangle for thick line
        smoothTriangle.setVertices(ax1, ay1, a[TZ],
                                   bx2, by2, b[TZ],
                                   bx1, by1, b[TZ]);
        smoothTriangle.setIntensities(a[SR], a[SG], a[SB], a[SA],
                                      b[SR], b[SG], b[SB], b[SA],
                                      b[SR], b[SG], b[SB], b[SA]);
        smoothTriangle.render();

      } else {
        triangle.reset();

        // render first triangle for thick line
        triangle.setVertices(ax1, ay1, a[TZ],
                             bx2, by2, b[TZ],
                             ax2, ay2, a[TZ]);
        triangle.setIntensities(a[SR], a[SG], a[SB], a[SA],
                                b[SR], b[SG], b[SB], b[SA],
                                a[SR], a[SG], a[SB], a[SA]);
        triangle.render();

        // render second triangle for thick line
        triangle.setVertices(ax1, ay1, a[TZ],
                             bx2, by2, b[TZ],
                             bx1, by1, b[TZ]);
        triangle.setIntensities(a[SR], a[SG], a[SB], a[SA],
                                b[SR], b[SG], b[SB], b[SA],
                                b[SR], b[SG], b[SB], b[SA]);
        triangle.render();
      }

    } else {
      line.reset();

      line.setIntensities(a[SR], a[SG], a[SB], a[SA],
                          b[SR], b[SG], b[SB], b[SA]);

      line.setVertices(a[TX], a[TY], a[TZ],
                       b[TX], b[TY], b[TZ]);

        /*
        // Seems okay to remove this because these vertices are not used again,
        // but if problems arise, this needs to be uncommented because the above
        // change is destructive and may need to be undone before proceeding.
        if (drawing2D() && a[MZ] == 0) {
          a[X] -= 0.01;
          a[Y] -= 0.01;
          a[VX] -= 0.01*a[VW];
          a[VY] -= 0.01*a[VW];
          b[X] -= 0.01;
          b[Y] -= 0.01;
          b[VX] -= 0.01*b[VW];
          b[VY] -= 0.01*b[VW];
        }
        */

      line.draw();
    }
  }


  /**
   * Handle echoing line data to a raw shape recording renderer. This has been
   * broken out of the renderLines() procedure so that renderLines() can be
   * optimized per-renderer without having to deal with this code. This code,
   * for instance, will stay the same when OpenGL is in use, but renderLines()
   * can be optimized significantly.
   * <br/> <br/>
   * Values for start and stop are specified, so that in the future, sorted
   * rendering can be implemented, which will require sequences of lines,
   * triangles, or points to be rendered in the neighborhood of one another.
   * That is, if we're gonna depth sort, we can't just draw all the triangles
   * and then draw all the lines, cuz that defeats the purpose.
   */
  protected void rawLines(int start, int stop) {
    raw.colorMode(RGB, 1);
    raw.noFill();
    raw.beginShape(LINES);

    for (int i = start; i < stop; i++) {
      float a[] = vertices[lines[i][VERTEX1]];
      float b[] = vertices[lines[i][VERTEX2]];
      raw.strokeWeight(vertices[lines[i][VERTEX2]][SW]);

      if (raw.is3D()) {
        if ((a[VW] != 0) && (b[VW] != 0)) {
          raw.stroke(a[SR], a[SG], a[SB], a[SA]);
          raw.vertex(a[VX] / a[VW], a[VY] / a[VW], a[VZ] / a[VW]);
          raw.stroke(b[SR], b[SG], b[SB], b[SA]);
          raw.vertex(b[VX] / b[VW], b[VY] / b[VW], b[VZ] / b[VW]);
        }
      } else if (raw.is2D()) {
        raw.stroke(a[SR], a[SG], a[SB], a[SA]);
        raw.vertex(a[TX], a[TY]);
        raw.stroke(b[SR], b[SG], b[SB], b[SA]);
        raw.vertex(b[TX], b[TY]);
      }
    }
    raw.endShape();
  }



  /////////////////////////////////////////////////////////////////////////////

  // TRIANGLES


  protected void addTriangle(int a, int b, int c) {
    addTriangleWithClip(a, b, c);
  }


  protected final void addTriangleWithClip(int a, int b, int c) {
    boolean aClipped = false;
    boolean bClipped = false;
    int clippedCount = 0;

//    cameraNear = -8;
    if (vertices[a][VZ] > cameraNear) {
      aClipped = true;
      clippedCount++;
    }
    if (vertices[b][VZ] > cameraNear) {
      bClipped = true;
      clippedCount++;
    }
    if (vertices[c][VZ] > cameraNear) {
      //cClipped = true;
      clippedCount++;
    }
    if (clippedCount == 0) {
//        if (vertices[a][VZ] < cameraFar && 
//                vertices[b][VZ] < cameraFar && 
//                vertices[c][VZ] < cameraFar) {
      addTriangleWithoutClip(a, b, c);
//        }

//    } else if (true) {
//        return;
        
    } else if (clippedCount == 3) {
      // In this case there is only one visible point.            |/|
      // So we'll have to make two new points on the clip line   <| |
      // and add that triangle instead.                           |\|

    } else if (clippedCount == 2) {
      //System.out.println("Clipped two");

      int ca, cb, cc, cd, ce;
      if (!aClipped) {
        ca = a;
        cb = b;
        cc = c;
      }
      else if (!bClipped) {
        ca = b;
        cb = a;
        cc = c;
      }
      else { //if (!cClipped) {
        ca = c;
        cb = b;
        cc = a;
      }

      cd = interpolateClipVertex(ca, cb);
      ce = interpolateClipVertex(ca, cc);
      addTriangleWithoutClip(ca, cd, ce);

    } else { // (clippedCount == 1) {
      //                                                          . |
      // In this case there are two visible points.               |\|
      // So we'll have to make two new points on the clip line    | |>
      // and then add two new triangles.                          |/|
      //                                                          . |
      //System.out.println("Clipped one");
      int ca, cb, cc, cd, ce;
      if (aClipped) {
        //System.out.println("aClipped");
        ca = c;
        cb = b;
        cc = a;
      }
      else if (bClipped) {
        //System.out.println("bClipped");
        ca = a;
        cb = c;
        cc = b;
      }
      else { //if (cClipped) {
        //System.out.println("cClipped");
        ca = a;
        cb = b;
        cc = c;
      }

      cd = interpolateClipVertex(ca, cc);
      ce = interpolateClipVertex(cb, cc);
      addTriangleWithoutClip(ca, cd, cb);
      //System.out.println("ca: " + ca + ", " + vertices[ca][VX] + ", " + vertices[ca][VY] + ", " + vertices[ca][VZ]);
      //System.out.println("cd: " + cd + ", " + vertices[cd][VX] + ", " + vertices[cd][VY] + ", " + vertices[cd][VZ]);
      //System.out.println("cb: " + cb + ", " + vertices[cb][VX] + ", " + vertices[cb][VY] + ", " + vertices[cb][VZ]);
      addTriangleWithoutClip(cb, cd, ce);
    }
  }


  protected final int interpolateClipVertex(int a, int b) {
    float[] va;
    float[] vb;
    // Set up va, vb such that va[VZ] >= vb[VZ]
    if (vertices[a][VZ] < vertices[b][VZ]) {
      va = vertices[b];
      vb = vertices[a];
    }
    else {
      va = vertices[a];
      vb = vertices[b];
    }
    float az = va[VZ];
    float bz = vb[VZ];

    float dz = az - bz;
    // If they have the same z, just use pt. a.
    if (dz == 0) {
      return a;
    }
    //float pa = (az - cameraNear) / dz;
    //float pb = (cameraNear - bz) / dz;
    float pa = (cameraNear - bz) / dz;
    float pb = 1 - pa;

    vertex(pa * va[X] + pb * vb[X],
           pa * va[Y] + pb * vb[Y],
           pa * va[Z] + pb * vb[Z]);
    int irv = vertexCount - 1;
    shapeLastPlusClipped++;

    float[] rv = vertices[irv];

    rv[TX] = pa * va[TX] + pb * vb[TX];
    rv[TY] = pa * va[TY] + pb * vb[TY];
    rv[TZ] = pa * va[TZ] + pb * vb[TZ];

    rv[VX] = pa * va[VX] + pb * vb[VX];
    rv[VY] = pa * va[VY] + pb * vb[VY];
    rv[VZ] = pa * va[VZ] + pb * vb[VZ];
    rv[VW] = pa * va[VW] + pb * vb[VW];

    rv[R] = pa * va[R] + pb * vb[R];
    rv[G] = pa * va[G] + pb * vb[G];
    rv[B] = pa * va[B] + pb * vb[B];
    rv[A] = pa * va[A] + pb * vb[A];

    rv[U] = pa * va[U] + pb * vb[U];
    rv[V] = pa * va[V] + pb * vb[V];

    rv[SR] = pa * va[SR] + pb * vb[SR];
    rv[SG] = pa * va[SG] + pb * vb[SG];
    rv[SB] = pa * va[SB] + pb * vb[SB];
    rv[SA] = pa * va[SA] + pb * vb[SA];

    rv[NX] = pa * va[NX] + pb * vb[NX];
    rv[NY] = pa * va[NY] + pb * vb[NY];
    rv[NZ] = pa * va[NZ] + pb * vb[NZ];

//    rv[SW] = pa * va[SW] + pb * vb[SW];

    rv[AR] = pa * va[AR] + pb * vb[AR];
    rv[AG] = pa * va[AG] + pb * vb[AG];
    rv[AB] = pa * va[AB] + pb * vb[AB];

    rv[SPR] = pa * va[SPR] + pb * vb[SPR];
    rv[SPG] = pa * va[SPG] + pb * vb[SPG];
    rv[SPB] = pa * va[SPB] + pb * vb[SPB];
    //rv[SPA] = pa * va[SPA] + pb * vb[SPA];

    rv[ER] = pa * va[ER] + pb * vb[ER];
    rv[EG] = pa * va[EG] + pb * vb[EG];
    rv[EB] = pa * va[EB] + pb * vb[EB];

    rv[SHINE] = pa * va[SHINE] + pb * vb[SHINE];

    rv[BEEN_LIT] = 0;

    return irv;
  }


  protected final void addTriangleWithoutClip(int a, int b, int c) {
    if (triangleCount == triangles.length) {
      int temp[][] = new int[triangleCount<<1][TRIANGLE_FIELD_COUNT];
      System.arraycopy(triangles, 0, temp, 0, triangleCount);
      triangles = temp;
      //message(CHATTER, "allocating more triangles " + triangles.length);
      float ftemp[][][] = new float[triangleCount<<1][3][TRI_COLOR_COUNT];
      System.arraycopy(triangleColors, 0, ftemp, 0, triangleCount);
      triangleColors = ftemp;
    }
    triangles[triangleCount][VERTEX1] = a;
    triangles[triangleCount][VERTEX2] = b;
    triangles[triangleCount][VERTEX3] = c;

    if (textureImage == null) {
      triangles[triangleCount][TEXTURE_INDEX] = -1;
    } else {
      triangles[triangleCount][TEXTURE_INDEX] = textureIndex;
    }

//    triangles[triangleCount][INDEX] = shape_index;
    triangleCount++;
  }


  /**
   * Triangulate the current polygon.
   * <BR> <BR>
   * Simple ear clipping polygon triangulation adapted from code by
   * John W. Ratcliff (jratcliff at verant.com). Presumably
   * <A HREF="http://www.flipcode.org/cgi-bin/fcarticles.cgi?show=63943">this</A>
   * bit of code from the web.
   */
  protected void addPolygonTriangles() {
    if (vertexOrder.length != vertices.length) {
      int[] temp = new int[vertices.length];
      // vertex_start may not be zero, might need to keep old stuff around
      // also, copy vertexOrder.length, not vertexCount because vertexCount
      // may be larger than vertexOrder.length (since this is a post-processing
      // step that happens after the vertex arrays are built).
      PApplet.arrayCopy(vertexOrder, temp, vertexOrder.length);
      vertexOrder = temp;
    }

    // this clipping algorithm only works in 2D, so in cases where a
    // polygon is drawn perpendicular to the z-axis, the area will be zero,
    // and triangulation will fail. as such, when the area calculates to
    // zero, figure out whether x or y is empty, and calculate based on the
    // two dimensions that actually contain information.
    // http://dev.processing.org/bugs/show_bug.cgi?id=111
    int d1 = X;
    int d2 = Y;
    // this brings up the nastier point that there may be cases where
    // a polygon is irregular in space and will throw off the
    // clockwise/counterclockwise calculation. for instance, if clockwise
    // relative to x and z, but counter relative to y and z or something
    // like that.. will wait to see if this is in fact a problem before
    // hurting my head on the math.

    /*
    // trying to track down bug #774
    for (int i = vertex_start; i < vertex_end; i++) {
      if (i > vertex_start) {
        if (vertices[i-1][MX] == vertices[i][MX] &&
            vertices[i-1][MY] == vertices[i][MY]) {
          System.out.print("**** " );
        }
      }
      System.out.println(i + " " + vertices[i][MX] + " " + vertices[i][MY]);
    }
    System.out.println();
    */

    // first we check if the polygon goes clockwise or counterclockwise
    float area = 0;
    for (int p = shapeLast - 1, q = shapeFirst; q < shapeLast; p = q++) {
      area += (vertices[q][d1] * vertices[p][d2] -
               vertices[p][d1] * vertices[q][d2]);
    }
    // rather than checking for the perpendicular case first, only do it
    // when the area calculates to zero. checking for perpendicular would be
    // a needless waste of time for the 99% case.
    if (area == 0) {
      // figure out which dimension is the perpendicular axis
      boolean foundValidX = false;
      boolean foundValidY = false;

      for (int i = shapeFirst; i < shapeLast; i++) {
        for (int j = i; j < shapeLast; j++){
          if ( vertices[i][X] != vertices[j][X] ) foundValidX = true;
          if ( vertices[i][Y] != vertices[j][Y] ) foundValidY = true;
        }
      }

      if (foundValidX) {
        //d1 = MX;  // already the case
        d2 = Z;
      } else if (foundValidY) {
        // ermm.. which is the proper order for cw/ccw here?
        d1 = Y;
        d2 = Z;
      } else {
        // screw it, this polygon is just f-ed up
        return;
      }

      // re-calculate the area, with what should be good values
      for (int p = shapeLast - 1, q = shapeFirst; q < shapeLast; p = q++) {
        area += (vertices[q][d1] * vertices[p][d2] -
                 vertices[p][d1] * vertices[q][d2]);
      }
    }

    // don't allow polygons to come back and meet themselves,
    // otherwise it will anger the triangulator
    // http://dev.processing.org/bugs/show_bug.cgi?id=97
    float vfirst[] = vertices[shapeFirst];
    float vlast[] = vertices[shapeLast-1];
    if ((abs(vfirst[X] - vlast[X]) < EPSILON) &&
        (abs(vfirst[Y] - vlast[Y]) < EPSILON) &&
        (abs(vfirst[Z] - vlast[Z]) < EPSILON)) {
      shapeLast--;
    }

    // then sort the vertices so they are always in a counterclockwise order
    int j = 0;
    if (area > 0) {
      for (int i = shapeFirst; i < shapeLast; i++) {
        j = i - shapeFirst;
        vertexOrder[j] = i;
      }
    } else {
      for (int i = shapeFirst; i < shapeLast; i++) {
        j = i - shapeFirst;
        vertexOrder[j] = (shapeLast - 1) - j;
      }
    }

    // remove vc-2 Vertices, creating 1 triangle every time
    int vc = shapeLast - shapeFirst;
    int count = 2*vc;  // complex polygon detection

    for (int m = 0, v = vc - 1; vc > 2; ) {
      boolean snip = true;

      // if we start over again, is a complex polygon
      if (0 >= (count--)) {
        break; // triangulation failed
      }

      // get 3 consecutive vertices <u,v,w>
      int u = v ; if (vc <= u) u = 0;    // previous
      v = u + 1; if (vc <= v) v = 0;     // current
      int w = v + 1; if (vc <= w) w = 0; // next

      // Upgrade values to doubles, and multiply by 10 so that we can have
      // some better accuracy as we tessellate. This seems to have negligible
      // speed differences on Windows and Intel Macs, but causes a 50% speed
      // drop for PPC Macs with the bug's example code that draws ~200 points
      // in a concave polygon. Apple has abandoned PPC so we may as well too.
      // http://dev.processing.org/bugs/show_bug.cgi?id=774

      // triangle A B C
      double Ax = -10 * vertices[vertexOrder[u]][d1];
      double Ay =  10 * vertices[vertexOrder[u]][d2];
      double Bx = -10 * vertices[vertexOrder[v]][d1];
      double By =  10 * vertices[vertexOrder[v]][d2];
      double Cx = -10 * vertices[vertexOrder[w]][d1];
      double Cy =  10 * vertices[vertexOrder[w]][d2];

      // first we check if <u,v,w> continues going ccw
      if (EPSILON > (((Bx-Ax) * (Cy-Ay)) - ((By-Ay) * (Cx-Ax)))) {
        continue;
      }

      for (int p = 0; p < vc; p++) {
        if ((p == u) || (p == v) || (p == w)) {
          continue;
        }

        double Px = -10 * vertices[vertexOrder[p]][d1];
        double Py =  10 * vertices[vertexOrder[p]][d2];

        double ax  = Cx - Bx;  double ay  = Cy - By;
        double bx  = Ax - Cx;  double by  = Ay - Cy;
        double cx  = Bx - Ax;  double cy  = By - Ay;
        double apx = Px - Ax;  double apy = Py - Ay;
        double bpx = Px - Bx;  double bpy = Py - By;
        double cpx = Px - Cx;  double cpy = Py - Cy;

        double aCROSSbp = ax * bpy - ay * bpx;
        double cCROSSap = cx * apy - cy * apx;
        double bCROSScp = bx * cpy - by * cpx;

        if ((aCROSSbp >= 0.0) && (bCROSScp >= 0.0) && (cCROSSap >= 0.0)) {
          snip = false;
        }
      }

      if (snip) {
        addTriangle(vertexOrder[u], vertexOrder[v], vertexOrder[w]);

        m++;

        // remove v from remaining polygon
        for (int s = v, t = v + 1; t < vc; s++, t++) {
          vertexOrder[s] = vertexOrder[t];
        }
        vc--;

        // reset error detection counter
        count = 2 * vc;
      }
    }
  }


  private void toWorldNormal(float nx, float ny, float nz, float[] out) {
    out[0] =
      modelviewInv.m00*nx + modelviewInv.m10*ny +
      modelviewInv.m20*nz + modelviewInv.m30;
    out[1] =
      modelviewInv.m01*nx + modelviewInv.m11*ny +
      modelviewInv.m21*nz + modelviewInv.m31;
    out[2] =
      modelviewInv.m02*nx + modelviewInv.m12*ny +
      modelviewInv.m22*nz + modelviewInv.m32;
    out[3] =
      modelviewInv.m03*nx + modelviewInv.m13*ny +
      modelviewInv.m23*nz + modelviewInv.m33;

    if (out[3] != 0 && out[3] != 1) {
      // divide by perspective coordinate
      out[0] /= out[3]; out[1] /= out[3]; out[2] /= out[3];
    }
    out[3] = 1;

    float nlen = mag(out[0], out[1], out[2]);  // normalize
    if (nlen != 0 && nlen != 1) {
      out[0] /= nlen; out[1] /= nlen; out[2] /= nlen;
    }
  }


  //private PVector calcLightingNorm = new PVector();
  //private PVector calcLightingWorldNorm = new PVector();
  float[] worldNormal = new float[4];


  private void calcLightingContribution(int vIndex,
                                        float[] contribution) {
    calcLightingContribution(vIndex, contribution, false);
  }


  private void calcLightingContribution(int vIndex,
                                        float[] contribution,
                                        boolean normalIsWorld) {
    float[] v = vertices[vIndex];

    float sr = v[SPR];
    float sg = v[SPG];
    float sb = v[SPB];

    float wx = v[VX];
    float wy = v[VY];
    float wz = v[VZ];
    float shine = v[SHINE];

    float nx = v[NX];
    float ny = v[NY];
    float nz = v[NZ];

    if (!normalIsWorld) {
//      System.out.println("um, hello?");
//      calcLightingNorm.set(nx, ny, nz);
//      //modelviewInv.mult(calcLightingNorm, calcLightingWorldNorm);
//
////      PMatrix3D mvi = modelViewInv;
////      float ox = mvi.m00*nx + mvi.m10*ny + mvi*m20+nz +
//      modelviewInv.cmult(calcLightingNorm, calcLightingWorldNorm);
//
//      calcLightingWorldNorm.normalize();
//      nx = calcLightingWorldNorm.x;
//      ny = calcLightingWorldNorm.y;
//      nz = calcLightingWorldNorm.z;

      toWorldNormal(v[NX], v[NY], v[NZ], worldNormal);
      nx = worldNormal[X];
      ny = worldNormal[Y];
      nz = worldNormal[Z];

//      float wnx = modelviewInv.multX(nx, ny, nz);
//      float wny = modelviewInv.multY(nx, ny, nz);
//      float wnz = modelviewInv.multZ(nx, ny, nz);
//      float wnw = modelviewInv.multW(nx, ny, nz);

//      if (wnw != 0 && wnw != 1) {
//        wnx /= wnw;
//        wny /= wnw;
//        wnz /= wnw;
//      }
//      float nlen = mag(wnx, wny, wnw);
//      if (nlen != 0 && nlen != 1) {
//        nx = wnx / nlen;
//        ny = wny / nlen;
//        nz = wnz / nlen;
//      } else {
//        nx = wnx;
//        ny = wny;
//        nz = wnz;
//      }
//      */
    } else {
      nx = v[NX];
      ny = v[NY];
      nz = v[NZ];
    }

    // Since the camera space == world space,
    // we can test for visibility by the dot product of
    // the normal with the direction from pt. to eye.
    float dir = dot(nx, ny, nz, -wx, -wy, -wz);
    // If normal is away from camera, choose its opposite.
    // If we add backface culling, this will be backfacing
    // (but since this is per vertex, it's more complicated)
    if (dir < 0) {
      nx = -nx;
      ny = -ny;
      nz = -nz;
    }

    // These two terms will sum the contributions from the various lights
    contribution[LIGHT_AMBIENT_R] = 0;
    contribution[LIGHT_AMBIENT_G] = 0;
    contribution[LIGHT_AMBIENT_B] = 0;

    contribution[LIGHT_DIFFUSE_R] = 0;
    contribution[LIGHT_DIFFUSE_G] = 0;
    contribution[LIGHT_DIFFUSE_B] = 0;

    contribution[LIGHT_SPECULAR_R] = 0;
    contribution[LIGHT_SPECULAR_G] = 0;
    contribution[LIGHT_SPECULAR_B] = 0;

    // for (int i = 0; i < MAX_LIGHTS; i++) {
    // if (!light[i]) continue;
    for (int i = 0; i < lightCount; i++) {

      float denom = lightFalloffConstant[i];
      float spotTerm = 1;

      if (lightType[i] == AMBIENT) {
        if (lightFalloffQuadratic[i] != 0 || lightFalloffLinear[i] != 0) {
          // Falloff depends on distance
          float distSq = mag(lightPosition[i].x - wx,
                             lightPosition[i].y - wy,
                             lightPosition[i].z - wz);
          denom +=
            lightFalloffQuadratic[i] * distSq +
            lightFalloffLinear[i] * sqrt(distSq);
        }
        if (denom == 0) denom = 1;

        contribution[LIGHT_AMBIENT_R] += lightDiffuse[i][0] / denom;
        contribution[LIGHT_AMBIENT_G] += lightDiffuse[i][1] / denom;
        contribution[LIGHT_AMBIENT_B] += lightDiffuse[i][2] / denom;

      } else {
        // If not ambient, we must deal with direction

        // li is the vector from the vertex to the light
        float lix, liy, liz;
        float lightDir_dot_li = 0;
        float n_dot_li = 0;

        if (lightType[i] == DIRECTIONAL) {
          lix = -lightNormal[i].x;
          liy = -lightNormal[i].y;
          liz = -lightNormal[i].z;
          denom = 1;
          n_dot_li = (nx * lix + ny * liy + nz * liz);
          // If light is lighting the face away from the camera, ditch
          if (n_dot_li <= 0) {
            continue;
          }
        } else { // Point or spot light (must deal also with light location)
          lix = lightPosition[i].x - wx;
          liy = lightPosition[i].y - wy;
          liz = lightPosition[i].z - wz;
          // normalize
          float distSq = mag(lix, liy, liz);
          if (distSq != 0) {
            lix /= distSq;
            liy /= distSq;
            liz /= distSq;
          }
          n_dot_li = (nx * lix + ny * liy + nz * liz);
          // If light is lighting the face away from the camera, ditch
          if (n_dot_li <= 0) {
            continue;
          }

          if (lightType[i] == SPOT) { // Must deal with spot cone
            lightDir_dot_li =
              -(lightNormal[i].x * lix +
                lightNormal[i].y * liy +
                lightNormal[i].z * liz);
            // Outside of spot cone
            if (lightDir_dot_li <= lightSpotAngleCos[i]) {
              continue;
            }
            spotTerm = (float) Math.pow(lightDir_dot_li, lightSpotConcentration[i]);
          }

          if (lightFalloffQuadratic[i] != 0 || lightFalloffLinear[i] != 0) {
            // Falloff depends on distance
            denom +=
              lightFalloffQuadratic[i] * distSq +
              lightFalloffLinear[i] * (float) sqrt(distSq);
          }
        }
        // Directional, point, or spot light:

        // We know n_dot_li > 0 from above "continues"

        if (denom == 0)
          denom = 1;
        float mul = n_dot_li * spotTerm / denom;
        contribution[LIGHT_DIFFUSE_R] += lightDiffuse[i][0] * mul;
        contribution[LIGHT_DIFFUSE_G] += lightDiffuse[i][1] * mul;
        contribution[LIGHT_DIFFUSE_B] += lightDiffuse[i][2] * mul;

        // SPECULAR

        // If the material and light have a specular component.
        if ((sr > 0 || sg > 0 || sb > 0) &&
            (lightSpecular[i][0] > 0 ||
             lightSpecular[i][1] > 0 ||
             lightSpecular[i][2] > 0)) {

          float vmag = mag(wx, wy, wz);
          if (vmag != 0) {
            wx /= vmag;
            wy /= vmag;
            wz /= vmag;
          }
          float sx = lix - wx;
          float sy = liy - wy;
          float sz = liz - wz;
          vmag = mag(sx, sy, sz);
          if (vmag != 0) {
            sx /= vmag;
            sy /= vmag;
            sz /= vmag;
          }
          float s_dot_n = (sx * nx + sy * ny + sz * nz);

          if (s_dot_n > 0) {
            s_dot_n = (float) Math.pow(s_dot_n, shine);
            mul = s_dot_n * spotTerm / denom;
            contribution[LIGHT_SPECULAR_R] += lightSpecular[i][0] * mul;
            contribution[LIGHT_SPECULAR_G] += lightSpecular[i][1] * mul;
            contribution[LIGHT_SPECULAR_B] += lightSpecular[i][2] * mul;
          }

        }
      }
    }
    return;
  }


  // Multiply the lighting contribution into the vertex's colors.
  // Only do this when there is ONE lighting per vertex
  // (MANUAL_VERTEX_NORMAL or SHAPE_NORMAL mode).
  private void applyLightingContribution(int vIndex, float[] contribution) {
    float[] v = vertices[vIndex];

    v[R] = clamp(v[ER] + v[AR] * contribution[LIGHT_AMBIENT_R] + v[DR] * contribution[LIGHT_DIFFUSE_R]);
    v[G] = clamp(v[EG] + v[AG] * contribution[LIGHT_AMBIENT_G] + v[DG] * contribution[LIGHT_DIFFUSE_G]);
    v[B] = clamp(v[EB] + v[AB] * contribution[LIGHT_AMBIENT_B] + v[DB] * contribution[LIGHT_DIFFUSE_B]);
    v[A] = clamp(v[DA]);

    v[SPR] = clamp(v[SPR] * contribution[LIGHT_SPECULAR_R]);
    v[SPG] = clamp(v[SPG] * contribution[LIGHT_SPECULAR_G]);
    v[SPB] = clamp(v[SPB] * contribution[LIGHT_SPECULAR_B]);
    //v[SPA] = min(1, v[SPA]);

    v[BEEN_LIT] = 1;
  }


  private void lightVertex(int vIndex, float[] contribution) {
    calcLightingContribution(vIndex, contribution);
    applyLightingContribution(vIndex, contribution);
  }


  private void lightUnlitVertex(int vIndex, float[] contribution) {
    if (vertices[vIndex][BEEN_LIT] == 0) {
      lightVertex(vIndex, contribution);
    }
  }


  private void copyPrelitVertexColor(int triIndex, int index, int colorIndex) {
    float[] triColor = triangleColors[triIndex][colorIndex];
    float[] v = vertices[index];

    triColor[TRI_DIFFUSE_R] = v[R];
    triColor[TRI_DIFFUSE_G] = v[G];
    triColor[TRI_DIFFUSE_B] = v[B];
    triColor[TRI_DIFFUSE_A] = v[A];
    triColor[TRI_SPECULAR_R] = v[SPR];
    triColor[TRI_SPECULAR_G] = v[SPG];
    triColor[TRI_SPECULAR_B] = v[SPB];
    //triColor[TRI_SPECULAR_A] = v[SPA];
  }


  private void copyVertexColor(int triIndex, int index, int colorIndex,
                               float[] contrib) {
    float[] triColor = triangleColors[triIndex][colorIndex];
    float[] v = vertices[index];

    triColor[TRI_DIFFUSE_R] =
      clamp(v[ER] + v[AR] * contrib[LIGHT_AMBIENT_R] + v[DR] * contrib[LIGHT_DIFFUSE_R]);
    triColor[TRI_DIFFUSE_G] =
      clamp(v[EG] + v[AG] * contrib[LIGHT_AMBIENT_G] + v[DG] * contrib[LIGHT_DIFFUSE_G]);
    triColor[TRI_DIFFUSE_B] =
      clamp(v[EB] + v[AB] * contrib[LIGHT_AMBIENT_B] + v[DB] * contrib[LIGHT_DIFFUSE_B]);
    triColor[TRI_DIFFUSE_A] = clamp(v[DA]);

    triColor[TRI_SPECULAR_R] = clamp(v[SPR] * contrib[LIGHT_SPECULAR_R]);
    triColor[TRI_SPECULAR_G] = clamp(v[SPG] * contrib[LIGHT_SPECULAR_G]);
    triColor[TRI_SPECULAR_B] = clamp(v[SPB] * contrib[LIGHT_SPECULAR_B]);
  }


  private void lightTriangle(int triIndex, float[] lightContribution) {
    int vIndex = triangles[triIndex][VERTEX1];
    copyVertexColor(triIndex, vIndex, 0, lightContribution);
    vIndex = triangles[triIndex][VERTEX2];
    copyVertexColor(triIndex, vIndex, 1, lightContribution);
    vIndex = triangles[triIndex][VERTEX3];
    copyVertexColor(triIndex, vIndex, 2, lightContribution);
  }


  private void lightTriangle(int triIndex) {
    int vIndex;

    // Handle lighting on, but no lights (in this case, just use emissive)
    // This wont be used currently because lightCount == 0 is don't use
    // lighting at all... So. OK. If that ever changes, use the below:
    /*
    if (lightCount == 0) {
      vIndex = triangles[triIndex][VERTEX1];
      copy_emissive_vertex_color_to_triangle(triIndex, vIndex, 0);
      vIndex = triangles[triIndex][VERTEX2];
      copy_emissive_vertex_color_to_triangle(triIndex, vIndex, 1);
      vIndex = triangles[triIndex][VERTEX3];
      copy_emissive_vertex_color_to_triangle(triIndex, vIndex, 2);
      return;
    }
    */

    // In MANUAL_VERTEX_NORMAL mode, we have a specific normal
    // for each vertex. In that case, we light any verts that
    // haven't already been lit and copy their colors straight
    // into the triangle.
    if (normalMode == NORMAL_MODE_VERTEX) {
      vIndex = triangles[triIndex][VERTEX1];
      lightUnlitVertex(vIndex, tempLightingContribution);
      copyPrelitVertexColor(triIndex, vIndex, 0);

      vIndex = triangles[triIndex][VERTEX2];
      lightUnlitVertex(vIndex, tempLightingContribution);
      copyPrelitVertexColor(triIndex, vIndex, 1);

      vIndex = triangles[triIndex][VERTEX3];
      lightUnlitVertex(vIndex, tempLightingContribution);
      copyPrelitVertexColor(triIndex, vIndex, 2);

    }

    // If the lighting doesn't depend on the vertex position, do the
    // following: We've already dealt with NORMAL_MODE_SHAPE mode before
    // we got into this function, so here we only have to deal with
    // NORMAL_MODE_AUTO. So we calculate the normal for this triangle,
    // and use that for the lighting.
    else if (!lightingDependsOnVertexPosition) {
      vIndex = triangles[triIndex][VERTEX1];
      int vIndex2 = triangles[triIndex][VERTEX2];
      int vIndex3 = triangles[triIndex][VERTEX3];

      /*
      dv1[0] = vertices[vIndex2][VX] - vertices[vIndex][VX];
      dv1[1] = vertices[vIndex2][VY] - vertices[vIndex][VY];
      dv1[2] = vertices[vIndex2][VZ] - vertices[vIndex][VZ];

      dv2[0] = vertices[vIndex3][VX] - vertices[vIndex][VX];
      dv2[1] = vertices[vIndex3][VY] - vertices[vIndex][VY];
      dv2[2] = vertices[vIndex3][VZ] - vertices[vIndex][VZ];

      cross(dv1, dv2, norm);
      */

      cross(vertices[vIndex2][VX] - vertices[vIndex][VX],
            vertices[vIndex2][VY] - vertices[vIndex][VY],
            vertices[vIndex2][VZ] - vertices[vIndex][VZ],
            vertices[vIndex3][VX] - vertices[vIndex][VX],
            vertices[vIndex3][VY] - vertices[vIndex][VY],
            vertices[vIndex3][VZ] - vertices[vIndex][VZ], lightTriangleNorm);

      lightTriangleNorm.normalize();
      vertices[vIndex][NX] = lightTriangleNorm.x;
      vertices[vIndex][NY] = lightTriangleNorm.y;
      vertices[vIndex][NZ] = lightTriangleNorm.z;

      // The true at the end says the normal is already in world coordinates
      calcLightingContribution(vIndex, tempLightingContribution, true);
      copyVertexColor(triIndex, vIndex, 0, tempLightingContribution);
      copyVertexColor(triIndex, vIndex2, 1, tempLightingContribution);
      copyVertexColor(triIndex, vIndex3, 2, tempLightingContribution);
    }

    // If lighting is position-dependent
    else {
      if (normalMode == NORMAL_MODE_SHAPE) {
        vIndex = triangles[triIndex][VERTEX1];
        vertices[vIndex][NX] = vertices[shapeFirst][NX];
        vertices[vIndex][NY] = vertices[shapeFirst][NY];
        vertices[vIndex][NZ] = vertices[shapeFirst][NZ];
        calcLightingContribution(vIndex, tempLightingContribution);
        copyVertexColor(triIndex, vIndex, 0, tempLightingContribution);

        vIndex = triangles[triIndex][VERTEX2];
        vertices[vIndex][NX] = vertices[shapeFirst][NX];
        vertices[vIndex][NY] = vertices[shapeFirst][NY];
        vertices[vIndex][NZ] = vertices[shapeFirst][NZ];
        calcLightingContribution(vIndex, tempLightingContribution);
        copyVertexColor(triIndex, vIndex, 1, tempLightingContribution);

        vIndex = triangles[triIndex][VERTEX3];
        vertices[vIndex][NX] = vertices[shapeFirst][NX];
        vertices[vIndex][NY] = vertices[shapeFirst][NY];
        vertices[vIndex][NZ] = vertices[shapeFirst][NZ];
        calcLightingContribution(vIndex, tempLightingContribution);
        copyVertexColor(triIndex, vIndex, 2, tempLightingContribution);
      }

      // lighting mode is AUTO_NORMAL
      else {
        vIndex = triangles[triIndex][VERTEX1];
        int vIndex2 = triangles[triIndex][VERTEX2];
        int vIndex3 = triangles[triIndex][VERTEX3];

        /*
        dv1[0] = vertices[vIndex2][VX] - vertices[vIndex][VX];
        dv1[1] = vertices[vIndex2][VY] - vertices[vIndex][VY];
        dv1[2] = vertices[vIndex2][VZ] - vertices[vIndex][VZ];

        dv2[0] = vertices[vIndex3][VX] - vertices[vIndex][VX];
        dv2[1] = vertices[vIndex3][VY] - vertices[vIndex][VY];
        dv2[2] = vertices[vIndex3][VZ] - vertices[vIndex][VZ];

        cross(dv1, dv2, norm);
        */

        cross(vertices[vIndex2][VX] - vertices[vIndex][VX],
              vertices[vIndex2][VY] - vertices[vIndex][VY],
              vertices[vIndex2][VZ] - vertices[vIndex][VZ],
              vertices[vIndex3][VX] - vertices[vIndex][VX],
              vertices[vIndex3][VY] - vertices[vIndex][VY],
              vertices[vIndex3][VZ] - vertices[vIndex][VZ], lightTriangleNorm);
//        float nmag = mag(norm[X], norm[Y], norm[Z]);
//        if (nmag != 0 && nmag != 1) {
//          norm[X] /= nmag; norm[Y] /= nmag; norm[Z] /= nmag;
//        }
        lightTriangleNorm.normalize();
        vertices[vIndex][NX] = lightTriangleNorm.x;
        vertices[vIndex][NY] = lightTriangleNorm.y;
        vertices[vIndex][NZ] = lightTriangleNorm.z;
        // The true at the end says the normal is already in world coordinates
        calcLightingContribution(vIndex, tempLightingContribution, true);
        copyVertexColor(triIndex, vIndex, 0, tempLightingContribution);

        vertices[vIndex2][NX] = lightTriangleNorm.x;
        vertices[vIndex2][NY] = lightTriangleNorm.y;
        vertices[vIndex2][NZ] = lightTriangleNorm.z;
        // The true at the end says the normal is already in world coordinates
        calcLightingContribution(vIndex2, tempLightingContribution, true);
        copyVertexColor(triIndex, vIndex2, 1, tempLightingContribution);

        vertices[vIndex3][NX] = lightTriangleNorm.x;
        vertices[vIndex3][NY] = lightTriangleNorm.y;
        vertices[vIndex3][NZ] = lightTriangleNorm.z;
        // The true at the end says the normal is already in world coordinates
        calcLightingContribution(vIndex3, tempLightingContribution, true);
        copyVertexColor(triIndex, vIndex3, 2, tempLightingContribution);
      }
    }
  }


  protected void renderTriangles(int start, int stop) {
    for (int i = start; i < stop; i++) {
      float a[] = vertices[triangles[i][VERTEX1]];
      float b[] = vertices[triangles[i][VERTEX2]];
      float c[] = vertices[triangles[i][VERTEX3]];
      int tex = triangles[i][TEXTURE_INDEX];

      /*
      // removing for 0149 with the return of P2D
      // ewjordan: hack to 'fix' accuracy issues when drawing in 2d
      // see also render_lines() where similar hack is employed
      float shift = 0.15f;//was 0.49f
      boolean shifted = false;
      if (drawing2D() && (a[Z] == 0)) {
        shifted = true;
        a[TX] += shift;
        a[TY] += shift;
        a[VX] += shift*a[VW];
        a[VY] += shift*a[VW];
        b[TX] += shift;
        b[TY] += shift;
        b[VX] += shift*b[VW];
        b[VY] += shift*b[VW];
        c[TX] += shift;
        c[TY] += shift;
        c[VX] += shift*c[VW];
        c[VY] += shift*c[VW];
      }
      */

      triangle.reset();

      // This is only true when not textured.
      // We really should pass specular straight through to triangle rendering.
      float ar = clamp(triangleColors[i][0][TRI_DIFFUSE_R] + triangleColors[i][0][TRI_SPECULAR_R]);
      float ag = clamp(triangleColors[i][0][TRI_DIFFUSE_G] + triangleColors[i][0][TRI_SPECULAR_G]);
      float ab = clamp(triangleColors[i][0][TRI_DIFFUSE_B] + triangleColors[i][0][TRI_SPECULAR_B]);
      float br = clamp(triangleColors[i][1][TRI_DIFFUSE_R] + triangleColors[i][1][TRI_SPECULAR_R]);
      float bg = clamp(triangleColors[i][1][TRI_DIFFUSE_G] + triangleColors[i][1][TRI_SPECULAR_G]);
      float bb = clamp(triangleColors[i][1][TRI_DIFFUSE_B] + triangleColors[i][1][TRI_SPECULAR_B]);
      float cr = clamp(triangleColors[i][2][TRI_DIFFUSE_R] + triangleColors[i][2][TRI_SPECULAR_R]);
      float cg = clamp(triangleColors[i][2][TRI_DIFFUSE_G] + triangleColors[i][2][TRI_SPECULAR_G]);
      float cb = clamp(triangleColors[i][2][TRI_DIFFUSE_B] + triangleColors[i][2][TRI_SPECULAR_B]);

      // ACCURATE TEXTURE CODE
      boolean failedToPrecalc = false;
      if (s_enableAccurateTextures && frustumMode){
        boolean textured = true;
        smoothTriangle.reset(3);
        smoothTriangle.smooth = true;
        smoothTriangle.interpARGB = true;
        smoothTriangle.setIntensities(ar, ag, ab, a[A],
                                      br, bg, bb, b[A],
                                      cr, cg, cb, c[A]);
        if (tex > -1 && textures[tex] != null) {
          smoothTriangle.setCamVertices(a[VX], a[VY], a[VZ],
                                        b[VX], b[VY], b[VZ],
                                        c[VX], c[VY], c[VZ]);
          smoothTriangle.interpUV = true;
          smoothTriangle.texture(textures[tex]);
          float umult = textures[tex].width;  // apparently no check for textureMode is needed here
          float vmult = textures[tex].height;
          smoothTriangle.vertices[0][U] = a[U]*umult;
          smoothTriangle.vertices[0][V] = a[V]*vmult;
          smoothTriangle.vertices[1][U] = b[U]*umult;
          smoothTriangle.vertices[1][V] = b[V]*vmult;
          smoothTriangle.vertices[2][U] = c[U]*umult;
          smoothTriangle.vertices[2][V] = c[V]*vmult;
        } else {
          smoothTriangle.interpUV = false;
          textured = false;
        }

        smoothTriangle.setVertices(a[TX], a[TY], a[TZ],
                                   b[TX], b[TY], b[TZ],
                                   c[TX], c[TY], c[TZ]);


        if (!textured || smoothTriangle.precomputeAccurateTexturing()){
                smoothTriangle.render();
        } else {
                // Something went wrong with the precomputation,
                // so we need to fall back on normal PTriangle
                // rendering.
                failedToPrecalc = true;
        }
      }

      // Normal triangle rendering
      // Note: this is not an end-if from the smoothed texturing mode
      // because it's possible that the precalculation will fail and we
      // need to fall back on normal rendering.
      if (!s_enableAccurateTextures || failedToPrecalc || (frustumMode == false)){
      if (tex > -1 && textures[tex] != null) {
        triangle.setTexture(textures[tex]);
        triangle.setUV(a[U], a[V], b[U], b[V], c[U], c[V]);
      }

      triangle.setIntensities(ar, ag, ab, a[A],
                              br, bg, bb, b[A],
                              cr, cg, cb, c[A]);

      triangle.setVertices(a[TX], a[TY], a[TZ],
                           b[TX], b[TY], b[TZ],
                           c[TX], c[TY], c[TZ]);

          triangle.render();
      }

      /*
      // removing for 0149 with the return of P2D
      if (drawing2D() && shifted){
        a[TX] -= shift;
        a[TY] -= shift;
        a[VX] -= shift*a[VW];
        a[VY] -= shift*a[VW];
        b[TX] -= shift;
        b[TY] -= shift;
        b[VX] -= shift*b[VW];
        b[VY] -= shift*b[VW];
        c[TX] -= shift;
        c[TY] -= shift;
        c[VX] -= shift*c[VW];
        c[VY] -= shift*c[VW];
      }
      */
    }
  }


  protected void rawTriangles(int start, int stop) {
    raw.colorMode(RGB, 1);
    raw.noStroke();
    raw.beginShape(TRIANGLES);

    for (int i = start; i < stop; i++) {
      float a[] = vertices[triangles[i][VERTEX1]];
      float b[] = vertices[triangles[i][VERTEX2]];
      float c[] = vertices[triangles[i][VERTEX3]];

      float ar = clamp(triangleColors[i][0][TRI_DIFFUSE_R] + triangleColors[i][0][TRI_SPECULAR_R]);
      float ag = clamp(triangleColors[i][0][TRI_DIFFUSE_G] + triangleColors[i][0][TRI_SPECULAR_G]);
      float ab = clamp(triangleColors[i][0][TRI_DIFFUSE_B] + triangleColors[i][0][TRI_SPECULAR_B]);
      float br = clamp(triangleColors[i][1][TRI_DIFFUSE_R] + triangleColors[i][1][TRI_SPECULAR_R]);
      float bg = clamp(triangleColors[i][1][TRI_DIFFUSE_G] + triangleColors[i][1][TRI_SPECULAR_G]);
      float bb = clamp(triangleColors[i][1][TRI_DIFFUSE_B] + triangleColors[i][1][TRI_SPECULAR_B]);
      float cr = clamp(triangleColors[i][2][TRI_DIFFUSE_R] + triangleColors[i][2][TRI_SPECULAR_R]);
      float cg = clamp(triangleColors[i][2][TRI_DIFFUSE_G] + triangleColors[i][2][TRI_SPECULAR_G]);
      float cb = clamp(triangleColors[i][2][TRI_DIFFUSE_B] + triangleColors[i][2][TRI_SPECULAR_B]);

      int tex = triangles[i][TEXTURE_INDEX];
      PImage texImage = (tex > -1) ? textures[tex] : null;
      if (texImage != null) {
        if (raw.is3D()) {
          if ((a[VW] != 0) && (b[VW] != 0) && (c[VW] != 0)) {
            raw.fill(ar, ag, ab, a[A]);
            raw.vertex(a[VX] / a[VW], a[VY] / a[VW], a[VZ] / a[VW], a[U], a[V]);
            raw.fill(br, bg, bb, b[A]);
            raw.vertex(b[VX] / b[VW], b[VY] / b[VW], b[VZ] / b[VW], b[U], b[V]);
            raw.fill(cr, cg, cb, c[A]);
            raw.vertex(c[VX] / c[VW], c[VY] / c[VW], c[VZ] / c[VW], c[U], c[V]);
          }
        } else if (raw.is2D()) {
          raw.fill(ar, ag, ab, a[A]);
          raw.vertex(a[TX], a[TY], a[U], a[V]);
          raw.fill(br, bg, bb, b[A]);
          raw.vertex(b[TX], b[TY], b[U], b[V]);
          raw.fill(cr, cg, cb, c[A]);
          raw.vertex(c[TX], c[TY], c[U], c[V]);
        }
      } else {  // no texture
        if (raw.is3D()) {
          if ((a[VW] != 0) && (b[VW] != 0) && (c[VW] != 0)) {
            raw.fill(ar, ag, ab, a[A]);
            raw.vertex(a[VX] / a[VW], a[VY] / a[VW], a[VZ] / a[VW]);
            raw.fill(br, bg, bb, b[A]);
            raw.vertex(b[VX] / b[VW], b[VY] / b[VW], b[VZ] / b[VW]);
            raw.fill(cr, cg, cb, c[A]);
            raw.vertex(c[VX] / c[VW], c[VY] / c[VW], c[VZ] / c[VW]);
          }
        } else if (raw.is2D()) {
          raw.fill(ar, ag, ab, a[A]);
          raw.vertex(a[TX], a[TY]);
          raw.fill(br, bg, bb, b[A]);
          raw.vertex(b[TX], b[TY]);
          raw.fill(cr, cg, cb, c[A]);
          raw.vertex(c[TX], c[TY]);
        }
      }
    }

    raw.endShape();
  }


  //////////////////////////////////////////////////////////////


  //public void bezierVertex(float x2, float y2,
  //                         float x3, float y3,
  //                         float x4, float y4)


  //public void bezierVertex(float x2, float y2, float z2,
  //                         float x3, float y3, float z3,
  //                         float x4, float y4, float z4)



  //////////////////////////////////////////////////////////////


  //public void curveVertex(float x, float y)


  //public void curveVertex(float x, float y, float z)



  ////////////////////////////////////////////////////////////


  /**
   * Emit any sorted geometry that's been collected on this frame.
   */
  public void flush() {
    if (hints[ENABLE_DEPTH_SORT]) {
      sort();
    }
    render();

    /*
    if (triangleCount > 0) {
      if (hints[ENABLE_DEPTH_SORT]) {
        sortTriangles();
      }
      renderTriangles();
    }
    if (lineCount > 0) {
      if (hints[ENABLE_DEPTH_SORT]) {
        sortLines();
      }
      renderLines();
    }
    // Clear this out in case flush() is called again.
    // For instance, with hint(ENABLE_DEPTH_SORT), it will be called
    // once on endRaw(), and once again at endDraw().
    triangleCount = 0;
    lineCount = 0;
    */
  }


  protected void render() {
    if (pointCount > 0) {
      renderPoints(0, pointCount);
      if (raw != null) {
        rawPoints(0, pointCount);
      }
      pointCount = 0;
    }
    if (lineCount > 0) {
      renderLines(0, lineCount);
      if (raw != null) {
        rawLines(0, lineCount);
      }
      lineCount = 0;
      pathCount = 0;
    }
    if (triangleCount > 0) {
      renderTriangles(0, triangleCount);
      if (raw != null) {
        rawTriangles(0, triangleCount);
      }
      triangleCount = 0;
    }
  }


  /**
   * Handle depth sorting of geometry. Currently this only handles triangles,
   * however in the future it will be expanded for points and lines, which
   * will also need to be interspersed with one another while rendering.
   */
  protected void sort() {
    if (triangleCount > 0) {
      sortTrianglesInternal(0, triangleCount-1);
    }
  }


  private void sortTrianglesInternal(int i, int j) {
    int pivotIndex = (i+j)/2;
    sortTrianglesSwap(pivotIndex, j);
    int k = sortTrianglesPartition(i-1, j);
    sortTrianglesSwap(k, j);
    if ((k-i) > 1) sortTrianglesInternal(i, k-1);
    if ((j-k) > 1) sortTrianglesInternal(k+1, j);
  }


  private int sortTrianglesPartition(int left, int right) {
    int pivot = right;
    do {
      while (sortTrianglesCompare(++left, pivot) < 0) { }
      while ((right != 0) &&
             (sortTrianglesCompare(--right, pivot) > 0)) { }
      sortTrianglesSwap(left, right);
    } while (left < right);
    sortTrianglesSwap(left, right);
    return left;
  }


  private void sortTrianglesSwap(int a, int b) {
    int tempi[] = triangles[a];
    triangles[a] = triangles[b];
    triangles[b] = tempi;
    float tempf[][] = triangleColors[a];
    triangleColors[a] = triangleColors[b];
    triangleColors[b] = tempf;
  }


  private float sortTrianglesCompare(int a, int b) {
    /*
    if (Float.isNaN(vertices[triangles[a][VERTEX1]][TZ]) ||
        Float.isNaN(vertices[triangles[a][VERTEX2]][TZ]) ||
        Float.isNaN(vertices[triangles[a][VERTEX3]][TZ]) ||
        Float.isNaN(vertices[triangles[b][VERTEX1]][TZ]) ||
        Float.isNaN(vertices[triangles[b][VERTEX2]][TZ]) ||
        Float.isNaN(vertices[triangles[b][VERTEX3]][TZ])) {
      System.err.println("NaN values in triangle");
    }
    */
    return ((vertices[triangles[b][VERTEX1]][TZ] +
             vertices[triangles[b][VERTEX2]][TZ] +
             vertices[triangles[b][VERTEX3]][TZ]) -
            (vertices[triangles[a][VERTEX1]][TZ] +
             vertices[triangles[a][VERTEX2]][TZ] +
             vertices[triangles[a][VERTEX3]][TZ]));
  }



  //////////////////////////////////////////////////////////////

  // POINT, LINE, TRIANGLE, QUAD

  // Because vertex(x, y) is mapped to vertex(x, y, 0), none of these commands
  // need to be overridden from their default implementation in PGraphics.


  //public void point(float x, float y)


  //public void point(float x, float y, float z)


  //public void line(float x1, float y1, float x2, float y2)


  //public void line(float x1, float y1, float z1,
  //                 float x2, float y2, float z2)


  //public void triangle(float x1, float y1, float x2, float y2,
  //                     float x3, float y3)


  //public void quad(float x1, float y1, float x2, float y2,
  //                 float x3, float y3, float x4, float y4)



  //////////////////////////////////////////////////////////////

  // RECT


  //public void rectMode(int mode)


  //public void rect(float a, float b, float c, float d)


  //protected void rectImpl(float x1, float y1, float x2, float y2)



  //////////////////////////////////////////////////////////////

  // ELLIPSE


  //public void ellipseMode(int mode)


  //public void ellipse(float a, float b, float c, float d)


  protected void ellipseImpl(float x, float y, float w, float h) {
    float radiusH = w / 2;
    float radiusV = h / 2;

    float centerX = x + radiusH;
    float centerY = y + radiusV;

//    float sx1 = screenX(x, y);
//    float sy1 = screenY(x, y);
//    float sx2 = screenX(x+w, y+h);
//    float sy2 = screenY(x+w, y+h);

    // returning to pre-1.0 version of algorithm because of problems
    int rough = (int)(4+Math.sqrt(w+h)*3);
    int accuracy = PApplet.constrain(rough, 6, 100);

    if (fill) {
      // returning to pre-1.0 version of algorithm because of problems
//      int rough = (int)(4+Math.sqrt(w+h)*3);
//      int rough = (int) (TWO_PI * PApplet.dist(sx1, sy1, sx2, sy2) / 20);
//      int accuracy = PApplet.constrain(rough, 6, 100);

      float inc = (float)SINCOS_LENGTH / accuracy;
      float val = 0;

      boolean strokeSaved = stroke;
      stroke = false;
      boolean smoothSaved = smooth;
      if (smooth && stroke) {
        smooth = false;
      }

      beginShape(TRIANGLE_FAN);
      normal(0, 0, 1);
      vertex(centerX, centerY);
      for (int i = 0; i < accuracy; i++) {
        vertex(centerX + cosLUT[(int) val] * radiusH,
               centerY + sinLUT[(int) val] * radiusV);
        val = (val + inc) % SINCOS_LENGTH;
      }
      // back to the beginning
      vertex(centerX + cosLUT[0] * radiusH,
             centerY + sinLUT[0] * radiusV);
      endShape();

      stroke = strokeSaved;
      smooth = smoothSaved;
    }

    if (stroke) {
//      int rough = (int) (TWO_PI * PApplet.dist(sx1, sy1, sx2, sy2) / 8);
//      int accuracy = PApplet.constrain(rough, 6, 100);

      float inc = (float)SINCOS_LENGTH / accuracy;
      float val = 0;

      boolean savedFill = fill;
      fill = false;

      val = 0;
      beginShape();
      for (int i = 0; i < accuracy; i++) {
        vertex(centerX + cosLUT[(int) val] * radiusH,
               centerY + sinLUT[(int) val] * radiusV);
        val = (val + inc) % SINCOS_LENGTH;
      }
      endShape(CLOSE);

      fill = savedFill;
    }
  }


  //public void arc(float a, float b, float c, float d,
  //                float start, float stop)


  protected void arcImpl(float x, float y, float w, float h,
                         float start, float stop) {
    float hr = w / 2f;
    float vr = h / 2f;

    float centerX = x + hr;
    float centerY = y + vr;

    if (fill) {
      // shut off stroke for a minute
      boolean savedStroke = stroke;
      stroke = false;

      int startLUT = (int) (0.5f + (start / TWO_PI) * SINCOS_LENGTH);
      int stopLUT = (int) (0.5f + (stop / TWO_PI) * SINCOS_LENGTH);

      beginShape(TRIANGLE_FAN);
      vertex(centerX, centerY);
      int increment = 1; // what's a good algorithm? stopLUT - startLUT;
      for (int i = startLUT; i < stopLUT; i += increment) {
        int ii = i % SINCOS_LENGTH;
        // modulo won't make the value positive
        if (ii < 0) ii += SINCOS_LENGTH;
        vertex(centerX + cosLUT[ii] * hr,
               centerY + sinLUT[ii] * vr);
      }
      // draw last point explicitly for accuracy
      vertex(centerX + cosLUT[stopLUT % SINCOS_LENGTH] * hr,
             centerY + sinLUT[stopLUT % SINCOS_LENGTH] * vr);
      endShape();

      stroke = savedStroke;
    }

    if (stroke) {
      // Almost identical to above, but this uses a LINE_STRIP
      // and doesn't include the first (center) vertex.

      boolean savedFill = fill;
      fill = false;

      int startLUT = (int) (0.5f + (start / TWO_PI) * SINCOS_LENGTH);
      int stopLUT = (int) (0.5f + (stop / TWO_PI) * SINCOS_LENGTH);

      beginShape(); //LINE_STRIP);
      int increment = 1; // what's a good algorithm? stopLUT - startLUT;
      for (int i = startLUT; i < stopLUT; i += increment) {
        int ii = i % SINCOS_LENGTH;
        if (ii < 0) ii += SINCOS_LENGTH;
        vertex(centerX + cosLUT[ii] * hr,
               centerY + sinLUT[ii] * vr);
      }
      // draw last point explicitly for accuracy
      vertex(centerX + cosLUT[stopLUT % SINCOS_LENGTH] * hr,
             centerY + sinLUT[stopLUT % SINCOS_LENGTH] * vr);
      endShape();

      fill = savedFill;
    }
  }



  //////////////////////////////////////////////////////////////

  // BOX


  //public void box(float size)


  public void box(float w, float h, float d) {
    if (triangle != null) {  // triangle is null in gl
      triangle.setCulling(true);
    }

    super.box(w, h, d);

    if (triangle != null) {  // triangle is null in gl
      triangle.setCulling(false);
    }
  }



  //////////////////////////////////////////////////////////////

  // SPHERE


  //public void sphereDetail(int res)


  //public void sphereDetail(int ures, int vres)


  public void sphere(float r) {
    if (triangle != null) {  // triangle is null in gl
      triangle.setCulling(true);
    }

    super.sphere(r);

    if (triangle != null) {  // triangle is null in gl
      triangle.setCulling(false);
    }
  }



  //////////////////////////////////////////////////////////////

  // BEZIER


  //public float bezierPoint(float a, float b, float c, float d, float t)


  //public float bezierTangent(float a, float b, float c, float d, float t)


  //public void bezierDetail(int detail)


  //public void bezier(float x1, float y1,
  //                   float x2, float y2,
  //                   float x3, float y3,
  //                   float x4, float y4)


  //public void bezier(float x1, float y1, float z1,
  //                   float x2, float y2, float z2,
  //                   float x3, float y3, float z3,
  //                   float x4, float y4, float z4)



  //////////////////////////////////////////////////////////////

  // CATMULL-ROM CURVES


  //public float curvePoint(float a, float b, float c, float d, float t)


  //public float curveTangent(float a, float b, float c, float d, float t)


  //public void curveDetail(int detail)


  //public void curveTightness(float tightness)


  //public void curve(float x1, float y1,
  //                  float x2, float y2,
  //                  float x3, float y3,
  //                  float x4, float y4)


  //public void curve(float x1, float y1, float z1,
  //                  float x2, float y2, float z2,
  //                  float x3, float y3, float z3,
  //                  float x4, float y4, float z4)



  //////////////////////////////////////////////////////////////

  // SMOOTH


  public void smooth() {
    //showMethodWarning("smooth");
          s_enableAccurateTextures = true;
          smooth = true;
  }


  public void noSmooth() {
          s_enableAccurateTextures = false;
          smooth = false;
  }



  //////////////////////////////////////////////////////////////

  // IMAGES


  //public void imageMode(int mode)


  //public void image(PImage image, float x, float y)


  //public void image(PImage image, float x, float y, float c, float d)


  //public void image(PImage image,
  //                  float a, float b, float c, float d,
  //                  int u1, int v1, int u2, int v2)


  //protected void imageImpl(PImage image,
  //                         float x1, float y1, float x2, float y2,
  //                         int u1, int v1, int u2, int v2)



  //////////////////////////////////////////////////////////////

  // SHAPE


  //public void shapeMode(int mode)


  //public void shape(PShape shape)


  //public void shape(PShape shape, float x, float y)


  //public void shape(PShape shape, float x, float y, float c, float d)



  //////////////////////////////////////////////////////////////

  // TEXT SETTINGS

  // Only textModeCheck overridden from PGraphics, no textAlign, textAscent,
  // textDescent, textFont, textLeading, textMode, textSize, textWidth


  protected boolean textModeCheck(int mode) {
    return (textMode == MODEL) || (textMode == SCREEN);
  }



  //////////////////////////////////////////////////////////////

  // TEXT

  // None of the variations of text() are overridden from PGraphics.



  //////////////////////////////////////////////////////////////

  // TEXT IMPL

  // Not even the text drawing implementation stuff is overridden.



  //////////////////////////////////////////////////////////////

  // MATRIX STACK


  public void pushMatrix() {
    if (matrixStackDepth == MATRIX_STACK_DEPTH) {
      throw new RuntimeException(ERROR_PUSHMATRIX_OVERFLOW);
    }
    modelview.get(matrixStack[matrixStackDepth]);
    modelviewInv.get(matrixInvStack[matrixStackDepth]);
    matrixStackDepth++;
  }


  public void popMatrix() {
    if (matrixStackDepth == 0) {
      throw new RuntimeException(ERROR_PUSHMATRIX_UNDERFLOW);
    }
    matrixStackDepth--;
    modelview.set(matrixStack[matrixStackDepth]);
    modelviewInv.set(matrixInvStack[matrixStackDepth]);
  }



  //////////////////////////////////////////////////////////////

  // MATRIX TRANSFORMATIONS


  public void translate(float tx, float ty) {
    translate(tx, ty, 0);
  }


  public void translate(float tx, float ty, float tz) {
    forwardTransform.translate(tx, ty, tz);
    reverseTransform.invTranslate(tx, ty, tz);
  }


  /**
   * Two dimensional rotation. Same as rotateZ (this is identical
   * to a 3D rotation along the z-axis) but included for clarity --
   * it'd be weird for people drawing 2D graphics to be using rotateZ.
   * And they might kick our a-- for the confusion.
   */
  public void rotate(float angle) {
    rotateZ(angle);
  }


  public void rotateX(float angle) {
    forwardTransform.rotateX(angle);
    reverseTransform.invRotateX(angle);
  }


  public void rotateY(float angle) {
    forwardTransform.rotateY(angle);
    reverseTransform.invRotateY(angle);
  }


  public void rotateZ(float angle) {
    forwardTransform.rotateZ(angle);
    reverseTransform.invRotateZ(angle);
  }


  /**
   * Rotate around an arbitrary vector, similar to glRotate(),
   * except that it takes radians (instead of degrees).
   */
  public void rotate(float angle, float v0, float v1, float v2) {
    forwardTransform.rotate(angle, v0, v1, v2);
    reverseTransform.invRotate(angle, v0, v1, v2);
  }


  /**
   * Same as scale(s, s, s).
   */
  public void scale(float s) {
    scale(s, s, s);
  }


  /**
   * Same as scale(sx, sy, 1).
   */
  public void scale(float sx, float sy) {
    scale(sx, sy, 1);
  }


  /**
   * Scale in three dimensions.
   */
  public void scale(float x, float y, float z) {
    forwardTransform.scale(x, y, z);
    reverseTransform.invScale(x, y, z);
  }



  //////////////////////////////////////////////////////////////

  // MATRIX MORE!


  public void resetMatrix() {
    forwardTransform.reset();
    reverseTransform.reset();
  }


  public void applyMatrix(PMatrix2D source) {
    applyMatrix(source.m00, source.m01, source.m02,
                source.m10, source.m11, source.m12);
  }


  public void applyMatrix(float n00, float n01, float n02,
                          float n10, float n11, float n12) {
    applyMatrix(n00, n01, n02, 0,
                n10, n11, n12, 0,
                0,   0,   1,   0,
                0,   0,   0,   1);
  }


  public void applyMatrix(PMatrix3D source) {
    applyMatrix(source.m00, source.m01, source.m02, source.m03,
                source.m10, source.m11, source.m12, source.m13,
                source.m20, source.m21, source.m22, source.m23,
                source.m30, source.m31, source.m32, source.m33);
  }


  /**
   * Apply a 4x4 transformation matrix. Same as glMultMatrix().
   * This call will be slow because it will try to calculate the
   * inverse of the transform. So avoid it whenever possible.
   */
  public void applyMatrix(float n00, float n01, float n02, float n03,
                          float n10, float n11, float n12, float n13,
                          float n20, float n21, float n22, float n23,
                          float n30, float n31, float n32, float n33) {

    forwardTransform.apply(n00, n01, n02, n03,
                           n10, n11, n12, n13,
                           n20, n21, n22, n23,
                           n30, n31, n32, n33);

    reverseTransform.invApply(n00, n01, n02, n03,
                              n10, n11, n12, n13,
                              n20, n21, n22, n23,
                              n30, n31, n32, n33);
  }



  //////////////////////////////////////////////////////////////

  // MATRIX GET/SET/PRINT


  public PMatrix getMatrix() {
    return modelview.get();
  }


  //public PMatrix2D getMatrix(PMatrix2D target)


  public PMatrix3D getMatrix(PMatrix3D target) {
    if (target == null) {
      target = new PMatrix3D();
    }
    target.set(modelview);
    return target;
  }


  //public void setMatrix(PMatrix source)


  public void setMatrix(PMatrix2D source) {
    // not efficient, but at least handles the inverse stuff.
    resetMatrix();
    applyMatrix(source);
  }


  /**
   * Set the current transformation to the contents of the specified source.
   */
  public void setMatrix(PMatrix3D source) {
    // not efficient, but at least handles the inverse stuff.
    resetMatrix();
    applyMatrix(source);
  }


  /**
   * Print the current model (or "transformation") matrix.
   */
  public void printMatrix() {
    modelview.print();
  }


  /*
   * This function checks if the modelview matrix is set up to likely be
   * drawing in 2D. It merely checks if the non-translational piece of the
   * matrix is unity. If this is to be used, it should be coupled with a
   * check that the raw vertex coordinates lie in the z=0 plane.
   * Mainly useful for applying sub-pixel shifts to avoid 2d artifacts
   * in the screen plane.
   * Added by ewjordan 6/13/07
   *
   * TODO need to invert the logic here so that we can simply return
   * the value, rather than calculating true/false and returning it.
   */
  /*
  private boolean drawing2D() {
    if (modelview.m00 != 1.0f ||
        modelview.m11 != 1.0f ||
        modelview.m22 != 1.0f || // check scale
        modelview.m01 != 0.0f ||
        modelview.m02 != 0.0f || // check rotational pieces
        modelview.m10 != 0.0f ||
        modelview.m12 != 0.0f ||
        modelview.m20 != 0.0f ||
        modelview.m21 != 0.0f ||
        !((camera.m23-modelview.m23) <= EPSILON &&
          (camera.m23-modelview.m23) >= -EPSILON)) { // check for z-translation
      // Something about the modelview matrix indicates 3d drawing
      // (or rotated 2d, in which case 2d subpixel fixes probably aren't needed)
      return false;
    } else {
      //The matrix is mapping z=0 vertices to the screen plane,
      // which means it's likely that 2D drawing is happening.
      return true;
    }
  }
  */



  //////////////////////////////////////////////////////////////

  // CAMERA


  /**
   * Set matrix mode to the camera matrix (instead of the current
   * transformation matrix). This means applyMatrix, resetMatrix, etc.
   * will affect the camera.
   * <P>
   * Note that the camera matrix is *not* the perspective matrix,
   * it is in front of the modelview matrix (hence the name "model"
   * and "view" for that matrix).
   * <P>
   * beginCamera() specifies that all coordinate transforms until endCamera()
   * should be pre-applied in inverse to the camera transform matrix.
   * Note that this is only challenging when a user specifies an arbitrary
   * matrix with applyMatrix(). Then that matrix will need to be inverted,
   * which may not be possible. But take heart, if a user is applying a
   * non-invertible matrix to the camera transform, then he is clearly
   * up to no good, and we can wash our hands of those bad intentions.
   * <P>
   * begin/endCamera clauses do not automatically reset the camera transform
   * matrix. That's because we set up a nice default camera transform int
   * setup(), and we expect it to hold through draw(). So we don't reset
   * the camera transform matrix at the top of draw(). That means that an
   * innocuous-looking clause like
   * <PRE>
   * beginCamera();
   * translate(0, 0, 10);
   * endCamera();
   * </PRE>
   * at the top of draw(), will result in a runaway camera that shoots
   * infinitely out of the screen over time. In order to prevent this,
   * it is necessary to call some function that does a hard reset of the
   * camera transform matrix inside of begin/endCamera. Two options are
   * <PRE>
   * camera(); // sets up the nice default camera transform
   * resetMatrix(); // sets up the identity camera transform
   * </PRE>
   * So to rotate a camera a constant amount, you might try
   * <PRE>
   * beginCamera();
   * camera();
   * rotateY(PI/8);
   * endCamera();
   * </PRE>
   */
  public void beginCamera() {
    if (manipulatingCamera) {
      throw new RuntimeException("beginCamera() cannot be called again " +
                                 "before endCamera()");
    } else {
      manipulatingCamera = true;
      forwardTransform = cameraInv;
      reverseTransform = camera;
    }
  }


  /**
   * Record the current settings into the camera matrix, and set
   * the matrix mode back to the current transformation matrix.
   * <P>
   * Note that this will destroy any settings to scale(), translate(),
   * or whatever, because the final camera matrix will be copied
   * (not multiplied) into the modelview.
   */
  public void endCamera() {
    if (!manipulatingCamera) {
      throw new RuntimeException("Cannot call endCamera() " +
                                 "without first calling beginCamera()");
    }
    // reset the modelview to use this new camera matrix
    modelview.set(camera);
    modelviewInv.set(cameraInv);

    // set matrix mode back to modelview
    forwardTransform = modelview;
    reverseTransform = modelviewInv;

    // all done
    manipulatingCamera = false;
  }


  /**
   * Set camera to the default settings.
   * <P>
   * Processing camera behavior:
   * <P>
   * Camera behavior can be split into two separate components, camera
   * transformation, and projection. The transformation corresponds to the
   * physical location, orientation, and scale of the camera. In a physical
   * camera metaphor, this is what can manipulated by handling the camera
   * body (with the exception of scale, which doesn't really have a physcial
   * analog). The projection corresponds to what can be changed by
   * manipulating the lens.
   * <P>
   * We maintain separate matrices to represent the camera transform and
   * projection. An important distinction between the two is that the camera
   * transform should be invertible, where the projection matrix should not,
   * since it serves to map three dimensions to two. It is possible to bake
   * the two matrices into a single one just by multiplying them together,
   * but it isn't a good idea, since lighting, z-ordering, and z-buffering
   * all demand a true camera z coordinate after modelview and camera
   * transforms have been applied but before projection. If the camera
   * transform and projection are combined there is no way to recover a
   * good camera-space z-coordinate from a model coordinate.
   * <P>
   * Fortunately, there are no functions that manipulate both camera
   * transformation and projection.
   * <P>
   * camera() sets the camera position, orientation, and center of the scene.
   * It replaces the camera transform with a new one. This is different from
   * gluLookAt(), but I think the only reason that GLU's lookat doesn't fully
   * replace the camera matrix with the new one, but instead multiplies it,
   * is that GL doesn't enforce the separation of camera transform and
   * projection, so it wouldn't be safe (you'd probably stomp your projection).
   * <P>
   * The transformation functions are the same ones used to manipulate the
   * modelview matrix (scale, translate, rotate, etc.). But they are bracketed
   * with beginCamera(), endCamera() to indicate that they should apply
   * (in inverse), to the camera transformation matrix.
   * <P>
   * This differs considerably from camera transformation in OpenGL.
   * OpenGL only lets you say, apply everything from here out to the
   * projection or modelview matrix. This makes it very hard to treat camera
   * manipulation as if it were a physical camera. Imagine that you want to
   * move your camera 100 units forward. In OpenGL, you need to apply the
   * inverse of that transformation or else you'll move your scene 100 units
   * forward--whether or not you've specified modelview or projection matrix.
   * Remember they're just multiplied by model coods one after another.
   * So in order to treat a camera like a physical camera, it is necessary
   * to pre-apply inverse transforms to a matrix that will be applied to model
   * coordinates. OpenGL provides nothing of this sort, but Processing does!
   * This is the camera transform matrix.
   */
  public void camera() {
    camera(cameraX, cameraY, cameraZ,
           cameraX, cameraY, 0,
           0, 1, 0);
  }


  /**
   * More flexible method for dealing with camera().
   * <P>
   * The actual call is like gluLookat. Here's the real skinny on
   * what does what:
   * <PRE>
   * camera(); or
   * camera(ex, ey, ez, cx, cy, cz, ux, uy, uz);
   * </PRE>
   * do not need to be called from with beginCamera();/endCamera();
   * That's because they always apply to the camera transformation,
   * and they always totally replace it. That means that any coordinate
   * transforms done before camera(); in draw() will be wiped out.
   * It also means that camera() always operates in untransformed world
   * coordinates. Therefore it is always redundant to call resetMatrix();
   * before camera(); This isn't technically true of gluLookat, but it's
   * pretty much how it's used.
   * <P>
   * Now, beginCamera(); and endCamera(); are useful if you want to move
   * the camera around using transforms like translate(), etc. They will
   * wipe out any coordinate system transforms that occur before them in
   * draw(), but they will not automatically wipe out the camera transform.
   * This means that they should be at the top of draw(). It also means
   * that the following:
   * <PRE>
   * beginCamera();
   * rotateY(PI/8);
   * endCamera();
   * </PRE>
   * will result in a camera that spins without stopping. If you want to
   * just rotate a small constant amount, try this:
   * <PRE>
   * beginCamera();
   * camera(); // sets up the default view
   * rotateY(PI/8);
   * endCamera();
   * </PRE>
   * That will rotate a little off of the default view. Note that this
   * is entirely equivalent to
   * <PRE>
   * camera(); // sets up the default view
   * beginCamera();
   * rotateY(PI/8);
   * endCamera();
   * </PRE>
   * because camera() doesn't care whether or not it's inside a
   * begin/end clause. Basically it's safe to use camera() or
   * camera(ex, ey, ez, cx, cy, cz, ux, uy, uz) as naked calls because
   * they do all the matrix resetting automatically.
   */
  public void camera(float eyeX, float eyeY, float eyeZ,
                     float centerX, float centerY, float centerZ,
                     float upX, float upY, float upZ) {
    float z0 = eyeX - centerX;
    float z1 = eyeY - centerY;
    float z2 = eyeZ - centerZ;
    float mag = sqrt(z0*z0 + z1*z1 + z2*z2);

    if (mag != 0) {
      z0 /= mag;
      z1 /= mag;
      z2 /= mag;
    }

    float y0 = upX;
    float y1 = upY;
    float y2 = upZ;

    float x0 =  y1*z2 - y2*z1;
    float x1 = -y0*z2 + y2*z0;
    float x2 =  y0*z1 - y1*z0;

    y0 =  z1*x2 - z2*x1;
    y1 = -z0*x2 + z2*x0;
    y2 =  z0*x1 - z1*x0;

    mag = sqrt(x0*x0 + x1*x1 + x2*x2);
    if (mag != 0) {
      x0 /= mag;
      x1 /= mag;
      x2 /= mag;
    }

    mag = sqrt(y0*y0 + y1*y1 + y2*y2);
    if (mag != 0) {
      y0 /= mag;
      y1 /= mag;
      y2 /= mag;
    }

    // just does an apply to the main matrix,
    // since that'll be copied out on endCamera
    camera.set(x0, x1, x2, 0,
               y0, y1, y2, 0,
               z0, z1, z2, 0,
               0,  0,  0,  1);
    camera.translate(-eyeX, -eyeY, -eyeZ);

    cameraInv.reset();
    cameraInv.invApply(x0, x1, x2, 0,
                       y0, y1, y2, 0,
                       z0, z1, z2, 0,
                       0,  0,  0,  1);
    cameraInv.translate(eyeX, eyeY, eyeZ);

    modelview.set(camera);
    modelviewInv.set(cameraInv);
  }


  /**
   * Print the current camera matrix.
   */
  public void printCamera() {
    camera.print();
  }


  //////////////////////////////////////////////////////////////

  // PROJECTION


  /**
   * Calls ortho() with the proper parameters for Processing's
   * standard orthographic projection.
   */
  public void ortho() {
    ortho(0, width, 0, height, -10, 10);
  }


  /**
   * Similar to gluOrtho(), but wipes out the current projection matrix.
   * <P>
   * Implementation partially based on Mesa's matrix.c.
   */
  public void ortho(float left, float right,
                    float bottom, float top,
                    float near, float far) {
    float x =  2.0f / (right - left);
    float y =  2.0f / (top - bottom);
    float z = -2.0f / (far - near);

    float tx = -(right + left) / (right - left);
    float ty = -(top + bottom) / (top - bottom);
    float tz = -(far + near) / (far - near);

    projection.set(x, 0, 0, tx,
                   0, y, 0, ty,
                   0, 0, z, tz,
                   0, 0, 0, 1);
    updateProjection();

    frustumMode = false;
  }


  /**
   * Calls perspective() with Processing's standard coordinate projection.
   * <P>
   * Projection functions:
   * <UL>
   * <LI>frustrum()
   * <LI>ortho()
   * <LI>perspective()
   * </UL>
   * Each of these three functions completely replaces the projection
   * matrix with a new one. They can be called inside setup(), and their
   * effects will be felt inside draw(). At the top of draw(), the projection
   * matrix is not reset. Therefore the last projection function to be
   * called always dominates. On resize, the default projection is always
   * established, which has perspective.
   * <P>
   * This behavior is pretty much familiar from OpenGL, except where
   * functions replace matrices, rather than multiplying against the
   * previous.
   * <P>
   */
  public void perspective() {
    perspective(cameraFOV, cameraAspect, cameraNear, cameraFar);
  }


  /**
   * Similar to gluPerspective(). Implementation based on Mesa's glu.c
   */
  public void perspective(float fov, float aspect, float zNear, float zFar) {
    //float ymax = zNear * tan(fovy * PI / 360.0f);
    float ymax = zNear * (float) Math.tan(fov / 2);
    float ymin = -ymax;

    float xmin = ymin * aspect;
    float xmax = ymax * aspect;

    frustum(xmin, xmax, ymin, ymax, zNear, zFar);
  }


  /**
   * Same as glFrustum(), except that it wipes out (rather than
   * multiplies against) the current perspective matrix.
   * <P>
   * Implementation based on the explanation in the OpenGL blue book.
   */
  public void frustum(float left, float right, float bottom,
                      float top, float znear, float zfar) {

    leftScreen = left;
    rightScreen = right;
    bottomScreen = bottom;
    topScreen = top;
    nearPlane = znear;
    frustumMode = true;

    //System.out.println(projection);
    projection.set((2*znear)/(right-left), 0, (right+left)/(right-left), 0,
                   0, (2*znear)/(top-bottom), (top+bottom)/(top-bottom), 0,
                   0, 0, -(zfar+znear)/(zfar-znear),-(2*zfar*znear)/(zfar-znear),
                   0, 0, -1, 0);
    updateProjection();
  }


  /** Called after the 'projection' PMatrix3D has changed. */
  protected void updateProjection() {
  }


  /**
   * Print the current projection matrix.
   */
  public void printProjection() {
    projection.print();
  }



  //////////////////////////////////////////////////////////////

  // SCREEN AND MODEL COORDS


  public float screenX(float x, float y) {
    return screenX(x, y, 0);
  }


  public float screenY(float x, float y) {
    return screenY(x, y, 0);
  }


  public float screenX(float x, float y, float z) {
    float ax =
      modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03;
    float ay =
      modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13;
    float az =
      modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23;
    float aw =
      modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33;

    float ox =
      projection.m00*ax + projection.m01*ay +
      projection.m02*az + projection.m03*aw;
    float ow =
      projection.m30*ax + projection.m31*ay +
      projection.m32*az + projection.m33*aw;

    if (ow != 0) ox /= ow;
    return width * (1 + ox) / 2.0f;
  }


  public float screenY(float x, float y, float z) {
    float ax =
      modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03;
    float ay =
      modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13;
    float az =
      modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23;
    float aw =
      modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33;

    float oy =
      projection.m10*ax + projection.m11*ay +
      projection.m12*az + projection.m13*aw;
    float ow =
      projection.m30*ax + projection.m31*ay +
      projection.m32*az + projection.m33*aw;

    if (ow != 0) oy /= ow;
    return height * (1 + oy) / 2.0f;
  }


  public float screenZ(float x, float y, float z) {
    float ax =
      modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03;
    float ay =
      modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13;
    float az =
      modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23;
    float aw =
      modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33;

    float oz =
      projection.m20*ax + projection.m21*ay +
      projection.m22*az + projection.m23*aw;
    float ow =
      projection.m30*ax + projection.m31*ay +
      projection.m32*az + projection.m33*aw;

    if (ow != 0) oz /= ow;
    return (oz + 1) / 2.0f;
  }


  public float modelX(float x, float y, float z) {
    float ax =
      modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03;
    float ay =
      modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13;
    float az =
      modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23;
    float aw =
      modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33;

    float ox =
      cameraInv.m00*ax + cameraInv.m01*ay +
      cameraInv.m02*az + cameraInv.m03*aw;
    float ow =
      cameraInv.m30*ax + cameraInv.m31*ay +
      cameraInv.m32*az + cameraInv.m33*aw;

    return (ow != 0) ? ox / ow : ox;
  }


  public float modelY(float x, float y, float z) {
    float ax =
      modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03;
    float ay =
      modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13;
    float az =
      modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23;
    float aw =
      modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33;

    float oy =
      cameraInv.m10*ax + cameraInv.m11*ay +
      cameraInv.m12*az + cameraInv.m13*aw;
    float ow =
      cameraInv.m30*ax + cameraInv.m31*ay +
      cameraInv.m32*az + cameraInv.m33*aw;

    return (ow != 0) ? oy / ow : oy;
  }


  public float modelZ(float x, float y, float z) {
    float ax =
      modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03;
    float ay =
      modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13;
    float az =
      modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23;
    float aw =
      modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33;

    float oz =
      cameraInv.m20*ax + cameraInv.m21*ay +
      cameraInv.m22*az + cameraInv.m23*aw;
    float ow =
      cameraInv.m30*ax + cameraInv.m31*ay +
      cameraInv.m32*az + cameraInv.m33*aw;

    return (ow != 0) ? oz / ow : oz;
  }



  //////////////////////////////////////////////////////////////

  // STYLE

  // pushStyle(), popStyle(), style() and getStyle() inherited.



  //////////////////////////////////////////////////////////////

  // STROKE CAP/JOIN/WEIGHT


//  public void strokeWeight(float weight) {
//    if (weight != DEFAULT_STROKE_WEIGHT) {
//      showMethodWarning("strokeWeight");
//    }
//  }


  public void strokeJoin(int join) {
    if (join != DEFAULT_STROKE_JOIN) {
      showMethodWarning("strokeJoin");
    }
  }


  public void strokeCap(int cap) {
    if (cap != DEFAULT_STROKE_CAP) {
      showMethodWarning("strokeCap");
    }
  }



  //////////////////////////////////////////////////////////////

  // STROKE COLOR

  // All methods inherited from PGraphics.



  //////////////////////////////////////////////////////////////

  // TINT COLOR

  // All methods inherited from PGraphics.



  //////////////////////////////////////////////////////////////

  // FILL COLOR


  protected void fillFromCalc() {
    super.fillFromCalc();
    ambientFromCalc();
  }



  //////////////////////////////////////////////////////////////

  // MATERIAL PROPERTIES

  // ambient, specular, shininess, and emissive all inherited.



  //////////////////////////////////////////////////////////////

  // LIGHTS


  PVector lightPositionVec = new PVector();
  PVector lightDirectionVec = new PVector();

  /**
   * Sets up an ambient and directional light.
   * <PRE>
   * The Lighting Skinny:
   *
   * The way lighting works is complicated enough that it's worth
   * producing a document to describe it. Lighting calculations proceed
   * pretty much exactly as described in the OpenGL red book.
   *
   * Light-affecting material properties:
   *
   *   AMBIENT COLOR
   *   - multiplies by light's ambient component
   *   - for believability this should match diffuse color
   *
   *   DIFFUSE COLOR
   *   - multiplies by light's diffuse component
   *
   *   SPECULAR COLOR
   *   - multiplies by light's specular component
   *   - usually less colored than diffuse/ambient
   *
   *   SHININESS
   *   - the concentration of specular effect
   *   - this should be set pretty high (20-50) to see really
   *     noticeable specularity
   *
   *   EMISSIVE COLOR
   *   - constant additive color effect
   *
   * Light types:
   *
   *   AMBIENT
   *   - one color
   *   - no specular color
   *   - no direction
   *   - may have falloff (constant, linear, and quadratic)
   *   - may have position (which matters in non-constant falloff case)
   *   - multiplies by a material's ambient reflection
   *
   *   DIRECTIONAL
   *   - has diffuse color
   *   - has specular color
   *   - has direction
   *   - no position
   *   - no falloff
   *   - multiplies by a material's diffuse and specular reflections
   *
   *   POINT
   *   - has diffuse color
   *   - has specular color
   *   - has position
   *   - no direction
   *   - may have falloff (constant, linear, and quadratic)
   *   - multiplies by a material's diffuse and specular reflections
   *
   *   SPOT
   *   - has diffuse color
   *   - has specular color
   *   - has position
   *   - has direction
   *   - has cone angle (set to half the total cone angle)
   *   - has concentration value
   *   - may have falloff (constant, linear, and quadratic)
   *   - multiplies by a material's diffuse and specular reflections
   *
   * Normal modes:
   *
   * All of the primitives (rect, box, sphere, etc.) have their normals
   * set nicely. During beginShape/endShape normals can be set by the user.
   *
   *   AUTO-NORMAL
   *   - if no normal is set during the shape, we are in auto-normal mode
   *   - auto-normal calculates one normal per triangle (face-normal mode)
   *
   *   SHAPE-NORMAL
   *   - if one normal is set during the shape, it will be used for
   *     all vertices
   *
   *   VERTEX-NORMAL
   *   - if multiple normals are set, each normal applies to
   *     subsequent vertices
   *   - (except for the first one, which applies to previous
   *     and subsequent vertices)
   *
   * Efficiency consequences:
   *
   *   There is a major efficiency consequence of position-dependent
   *   lighting calculations per vertex. (See below for determining
   *   whether lighting is vertex position-dependent.) If there is no
   *   position dependency then the only factors that affect the lighting
   *   contribution per vertex are its colors and its normal.
   *   There is a major efficiency win if
   *
   *   1) lighting is not position dependent
   *   2) we are in AUTO-NORMAL or SHAPE-NORMAL mode
   *
   *   because then we can calculate one lighting contribution per shape
   *   (SHAPE-NORMAL) or per triangle (AUTO-NORMAL) and simply multiply it
   *   into the vertex colors. The converse is our worst-case performance when
   *
   *   1) lighting is position dependent
   *   2) we are in AUTO-NORMAL mode
   *
   *   because then we must calculate lighting per-face * per-vertex.
   *   Each vertex has a different lighting contribution per face in
   *   which it appears. Yuck.
   *
   * Determining vertex position dependency:
   *
   *   If any of the following factors are TRUE then lighting is
   *   vertex position dependent:
   *
   *   1) Any lights uses non-constant falloff
   *   2) There are any point or spot lights
   *   3) There is a light with specular color AND there is a
   *      material with specular color
   *
   * So worth noting is that default lighting (a no-falloff ambient
   * and a directional without specularity) is not position-dependent.
   * We should capitalize.
   *
   * Simon Greenwold, April 2005
   * </PRE>
   */
  public void lights() {
    // need to make sure colorMode is RGB 255 here
    int colorModeSaved = colorMode;
    colorMode = RGB;

    lightFalloff(1, 0, 0);
    lightSpecular(0, 0, 0);

    ambientLight(colorModeX * 0.5f,
                 colorModeY * 0.5f,
                 colorModeZ * 0.5f);
    directionalLight(colorModeX * 0.5f,
                     colorModeY * 0.5f,
                     colorModeZ * 0.5f,
                     0, 0, -1);

    colorMode = colorModeSaved;

    lightingDependsOnVertexPosition = false;
  }


  /**
   * Turn off all lights.
   */
  public void noLights() {
    // write any queued geometry, because lighting will be goofed after
    flush();
    // set the light count back to zero
    lightCount = 0;
  }


  /**
   * Add an ambient light based on the current color mode.
   */
  public void ambientLight(float r, float g, float b) {
    ambientLight(r, g, b, 0, 0, 0);
  }


  /**
   * Add an ambient light based on the current color mode.
   * This version includes an (x, y, z) position for situations
   * where the falloff distance is used.
   */
  public void ambientLight(float r, float g, float b,
                           float x, float y, float z) {
    if (lightCount == MAX_LIGHTS) {
      throw new RuntimeException("can only create " + MAX_LIGHTS + " lights");
    }
    colorCalc(r, g, b);
    lightDiffuse[lightCount][0] = calcR;
    lightDiffuse[lightCount][1] = calcG;
    lightDiffuse[lightCount][2] = calcB;

    lightType[lightCount] = AMBIENT;
    lightFalloffConstant[lightCount] = currentLightFalloffConstant;
    lightFalloffLinear[lightCount] = currentLightFalloffLinear;
    lightFalloffQuadratic[lightCount] = currentLightFalloffQuadratic;
    lightPosition(lightCount, x, y, z);
    lightCount++;
    //return lightCount-1;
  }


  public void directionalLight(float r, float g, float b,
                               float nx, float ny, float nz) {
    if (lightCount == MAX_LIGHTS) {
      throw new RuntimeException("can only create " + MAX_LIGHTS + " lights");
    }
    colorCalc(r, g, b);
    lightDiffuse[lightCount][0] = calcR;
    lightDiffuse[lightCount][1] = calcG;
    lightDiffuse[lightCount][2] = calcB;

    lightType[lightCount] = DIRECTIONAL;
    lightFalloffConstant[lightCount] = currentLightFalloffConstant;
    lightFalloffLinear[lightCount] = currentLightFalloffLinear;
    lightFalloffQuadratic[lightCount] = currentLightFalloffQuadratic;
    lightSpecular[lightCount][0] = currentLightSpecular[0];
    lightSpecular[lightCount][1] = currentLightSpecular[1];
    lightSpecular[lightCount][2] = currentLightSpecular[2];
    lightDirection(lightCount, nx, ny, nz);
    lightCount++;
  }


  public void pointLight(float r, float g, float b,
                         float x, float y, float z) {
    if (lightCount == MAX_LIGHTS) {
      throw new RuntimeException("can only create " + MAX_LIGHTS + " lights");
    }
    colorCalc(r, g, b);
    lightDiffuse[lightCount][0] = calcR;
    lightDiffuse[lightCount][1] = calcG;
    lightDiffuse[lightCount][2] = calcB;

    lightType[lightCount] = POINT;
    lightFalloffConstant[lightCount] = currentLightFalloffConstant;
    lightFalloffLinear[lightCount] = currentLightFalloffLinear;
    lightFalloffQuadratic[lightCount] = currentLightFalloffQuadratic;
    lightSpecular[lightCount][0] = currentLightSpecular[0];
    lightSpecular[lightCount][1] = currentLightSpecular[1];
    lightSpecular[lightCount][2] = currentLightSpecular[2];
    lightPosition(lightCount, x, y, z);
    lightCount++;

    lightingDependsOnVertexPosition = true;
  }


  public void spotLight(float r, float g, float b,
                        float x, float y, float z,
                        float nx, float ny, float nz,
                        float angle, float concentration) {
    if (lightCount == MAX_LIGHTS) {
      throw new RuntimeException("can only create " + MAX_LIGHTS + " lights");
    }
    colorCalc(r, g, b);
    lightDiffuse[lightCount][0] = calcR;
    lightDiffuse[lightCount][1] = calcG;
    lightDiffuse[lightCount][2] = calcB;

    lightType[lightCount] = SPOT;
    lightFalloffConstant[lightCount] = currentLightFalloffConstant;
    lightFalloffLinear[lightCount] = currentLightFalloffLinear;
    lightFalloffQuadratic[lightCount] = currentLightFalloffQuadratic;
    lightSpecular[lightCount][0] = currentLightSpecular[0];
    lightSpecular[lightCount][1] = currentLightSpecular[1];
    lightSpecular[lightCount][2] = currentLightSpecular[2];
    lightPosition(lightCount, x, y, z);
    lightDirection(lightCount, nx, ny, nz);
    lightSpotAngle[lightCount] = angle;
    lightSpotAngleCos[lightCount] = Math.max(0, (float) Math.cos(angle));
    lightSpotConcentration[lightCount] = concentration;
    lightCount++;

    lightingDependsOnVertexPosition = true;
  }


  /**
   * Set the light falloff rates for the last light that was created.
   * Default is lightFalloff(1, 0, 0).
   */
  public void lightFalloff(float constant, float linear, float quadratic) {
    currentLightFalloffConstant = constant;
    currentLightFalloffLinear = linear;
    currentLightFalloffQuadratic = quadratic;

    lightingDependsOnVertexPosition = true;
  }


  /**
   * Set the specular color of the last light created.
   */
  public void lightSpecular(float x, float y, float z) {
    colorCalc(x, y, z);
    currentLightSpecular[0] = calcR;
    currentLightSpecular[1] = calcG;
    currentLightSpecular[2] = calcB;

    lightingDependsOnVertexPosition = true;
  }


  /**
   * internal function to set the light position
   * based on the current modelview matrix.
   */
  protected void lightPosition(int num, float x, float y, float z) {
    lightPositionVec.set(x, y, z);
    modelview.mult(lightPositionVec, lightPosition[num]);
    /*
    lightPosition[num][0] =
      modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03;
    lightPosition[num][1] =
      modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13;
    lightPosition[num][2] =
      modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23;
    */
  }


  /**
   * internal function to set the light direction
   * based on the current modelview matrix.
   */
  protected void lightDirection(int num, float x, float y, float z) {
    lightNormal[num].set(modelviewInv.m00*x + modelviewInv.m10*y + modelviewInv.m20*z + modelviewInv.m30,
                         modelviewInv.m01*x + modelviewInv.m11*y + modelviewInv.m21*z + modelviewInv.m31,
                         modelviewInv.m02*x + modelviewInv.m12*y + modelviewInv.m22*z + modelviewInv.m32);
    lightNormal[num].normalize();

    /*
    lightDirectionVec.set(x, y, z);
    System.out.println("dir vec " + lightDirectionVec);
    //modelviewInv.mult(lightDirectionVec, lightNormal[num]);
    modelviewInv.cmult(lightDirectionVec, lightNormal[num]);
    System.out.println("cmult vec " + lightNormal[num]);
    lightNormal[num].normalize();
    System.out.println("setting light direction " + lightNormal[num]);
    */

    /*
    // Multiply by inverse transpose.
    lightNormal[num][0] =
      modelviewInv.m00*x + modelviewInv.m10*y +
      modelviewInv.m20*z + modelviewInv.m30;
    lightNormal[num][1] =
      modelviewInv.m01*x + modelviewInv.m11*y +
      modelviewInv.m21*z + modelviewInv.m31;
    lightNormal[num][2] =
      modelviewInv.m02*x + modelviewInv.m12*y +
      modelviewInv.m22*z + modelviewInv.m32;

    float n = mag(lightNormal[num][0], lightNormal[num][1], lightNormal[num][2]);
    if (n == 0 || n == 1) return;

    lightNormal[num][0] /= n;
    lightNormal[num][1] /= n;
    lightNormal[num][2] /= n;
    */
  }



  //////////////////////////////////////////////////////////////

  // BACKGROUND

  // Base background() variations inherited from PGraphics.


  protected void backgroundImpl(PImage image) {
    System.arraycopy(image.pixels, 0, pixels, 0, pixels.length);
    Arrays.fill(zbuffer, Float.MAX_VALUE);
  }


  /**
   * Clear pixel buffer. With P3D and OPENGL, this also clears the zbuffer.
   */
  protected void backgroundImpl() {
    Arrays.fill(pixels, backgroundColor);
    Arrays.fill(zbuffer, Float.MAX_VALUE);
  }



  //////////////////////////////////////////////////////////////

  // COLOR MODE

  // all colorMode() variations inherited from PGraphics.



  //////////////////////////////////////////////////////////////

  // COLOR CALCULATIONS

  // protected colorCalc and colorCalcARGB inherited.



  //////////////////////////////////////////////////////////////

  // COLOR DATATYPE STUFFING

  // final color() variations inherited.



  //////////////////////////////////////////////////////////////

  // COLOR DATATYPE EXTRACTION

  // final methods alpha, red, green, blue,
  // hue, saturation, and brightness all inherited.



  //////////////////////////////////////////////////////////////

  // COLOR DATATYPE INTERPOLATION

  // both lerpColor variants inherited.



  //////////////////////////////////////////////////////////////

  // BEGIN/END RAW

  // beginRaw, endRaw() both inherited.



  //////////////////////////////////////////////////////////////

  // WARNINGS and EXCEPTIONS

  // showWarning and showException inherited.



  //////////////////////////////////////////////////////////////

  // RENDERER SUPPORT QUERIES


  //public boolean displayable()


  public boolean is2D() {
    return false;
  }


  public boolean is3D() {
    return true;
  }



  //////////////////////////////////////////////////////////////

  // PIMAGE METHODS

  // All these methods are inherited, because this render has a
  // pixels[] array that can be accessed directly.

  // getImage
  // setCache, getCache, removeCache
  // isModified, setModified
  // loadPixels, updatePixels
  // resize
  // get, getImpl, set, setImpl
  // mask
  // filter
  // copy
  // blendColor, blend



  //////////////////////////////////////////////////////////////

  // MATH (internal use only)


  private final float sqrt(float a) {
    return (float) Math.sqrt(a);
  }


  private final float mag(float a, float b, float c) {
    return (float) Math.sqrt(a*a + b*b + c*c);
  }


  private final float clamp(float a) {
    return (a < 1) ? a : 1;
  }


  private final float abs(float a) {
    return (a < 0) ? -a : a;
  }


  private float dot(float ax, float ay, float az,
                    float bx, float by, float bz) {
    return ax*bx + ay*by + az*bz;
  }


  /*
  private final void cross(float a0, float a1, float a2,
                           float b0, float b1, float b2,
                           float[] out) {
    out[0] = a1*b2 - a2*b1;
    out[1] = a2*b0 - a0*b2;
    out[2] = a0*b1 - a1*b0;
  }
  */


  private final void cross(float a0, float a1, float a2,
                           float b0, float b1, float b2,
                           PVector out) {
    out.x = a1*b2 - a2*b1;
    out.y = a2*b0 - a0*b2;
    out.z = a0*b1 - a1*b0;
  }


  /*
  private final void cross(float[] a, float[] b, float[] out) {
    out[0] = a[1]*b[2] - a[2]*b[1];
    out[1] = a[2]*b[0] - a[0]*b[2];
    out[2] = a[0]*b[1] - a[1]*b[0];
  }
  */
}

