/* Copyright (C) 2001-2004 by Peter Eastman

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

   This program 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 General Public License for more details. */

package artofillusion.raster;

import artofillusion.*;
import artofillusion.image.*;
import artofillusion.material.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.texture.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.awt.image.*;
import java.util.*;

/** Raster is a Renderer which generates images with a scanline algorithm. */

public class Raster implements Runnable
{
  ObjectInfo light[];
  FormContainer configPanel;
  BCheckBox transparentBox, adaptiveBox, hideBackfaceBox;
  BComboBox shadeChoice, aliasChoice, sampleChoice;
  ValueField errorField, smoothField;
  int pixel[], imagePixel[], width, height, envMode, imageWidth, imageHeight;
  int shadingMode, samplesPerPixel, subsample;
  float zbuffer[], imageZbuffer[];
  long updateTime;
  MemoryImageSource imageSource;
  SceneRenderInfoProvider theScene;
  Camera theCamera;
  RenderListener listener;
  Image img;
  Thread renderThread;
  Vec3 tempVec[], lightPosition[], lightDirection[];
  RGBColor color, tempColor[], ambColor, envColor, fogColor;
  TextureMapping envMapping;
  TextureSpec surfSpec, surfSpec2;
  MaterialSpec matSpec;
  double time, smoothing, smoothScale, depthOfField, focalDist, surfaceError, fogDist;
  boolean fog, transparentBackground, adaptive, hideBackfaces, positionNeeded, depthNeeded;

  public static final int GOURAUD = 0;
  public static final int HYBRID = 1;
  public static final int PHONG = 2;

  public static final double TOL = 1e-12;
  
  public static final ThreadGroup DEAFTHREADGROUP = new ThreadGroup("Render")
      {
        public void uncaughtException(Thread t, Throwable e)
        {
          if (e instanceof OutOfMemoryError)
          {
            e.printStackTrace();
            try
            {
              Thread.sleep(100);
            }
            catch(InterruptedException ie)
            {}
            System.gc();
          }
          else
            e.printStackTrace();
        }
      };

  public Raster()
  {
    samplesPerPixel = 1;
    subsample = 1;
  }
  
  /* Methods from the Renderer interface. */

  public synchronized void renderScene(SceneRenderInfoProvider theScene, Camera camera, RenderListener rl, SceneCamera sceneCamera)
  {
    Dimension dim = camera.getSize();

    listener = rl;
    this.theScene = theScene;
    theCamera = camera.duplicate();
    if (sceneCamera == null)
      {
        depthOfField = 0.0;
        focalDist = theCamera.getDistToScreen();
        depthNeeded = false;
      }
    else
      {
        depthOfField = sceneCamera.getDepthOfField();
        focalDist = sceneCamera.getFocalDistance();
        depthNeeded = ((sceneCamera.getComponentsForFilters()&ComplexImage.DEPTH) != 0);
      }
    time = theScene.getTime();
    if (imagePixel == null || imageWidth != dim.width || imageHeight != dim.height)
      {
	imageWidth = dim.width;
	imageHeight = dim.height;
	imagePixel = new int [imageWidth*imageHeight];
        imageZbuffer = new float [imageWidth*imageHeight];
	imageSource = new MemoryImageSource(imageWidth, imageHeight, imagePixel, 0, imageWidth);
	imageSource.setAnimated(true);
	img = Toolkit.getDefaultToolkit().createImage(imageSource);
      }
    width = imageWidth*samplesPerPixel;
    height = imageHeight*samplesPerPixel;
    if (samplesPerPixel == 1)
      {
	pixel = imagePixel;
	zbuffer = imageZbuffer;
      }
    else if (pixel == null || pixel.length != width*height)
      {
	pixel = new int [width*height];
	zbuffer = new float [width*height];
      }
    theCamera.setSize(width, height);
    theCamera.setDistToScreen(theCamera.getDistToScreen()*samplesPerPixel);
    color = new RGBColor();
    surfSpec = new TextureSpec();
    surfSpec2 = new TextureSpec();
    matSpec = new MaterialSpec();
    tempColor = new RGBColor [3];
    for (int i = 0; i < tempColor.length; i++)
      tempColor[i] = new RGBColor(0.0f, 0.0f, 0.0f);
    tempVec = new Vec3 [4];
    for (int i = 0; i < tempVec.length; i++)
      tempVec[i] = new Vec3();
    renderThread = new Thread(DEAFTHREADGROUP, this);
    renderThread.start();
  }

  public synchronized void cancelRendering(SceneRenderInfoProvider sc)
  {
    Thread t = renderThread;
    RenderListener l = listener;
    cancelRenderingAsync(sc);
    
    if (t == null)
      return;
    try
      {
        t.join();
//        while (t.isAlive())
//          {
//            Thread.sleep(100);
//          }
      }
    catch (InterruptedException ex)
      {
      }
    l.renderingCanceled();
    listener = null;
    finish();
  }

  public synchronized void cancelRenderingAsync(SceneRenderInfoProvider sc)
  {
    if (theScene != sc)
      return;
    
    Thread t = renderThread;
    if (t == null)
      return;
    t.interrupt();
    setAllNull();  // This may lead to a NullPointerException which is intended
  }

  public Widget getConfigPanel()
  {
    if (configPanel == null)
    {
      configPanel = new FormContainer(3, 5);
      LayoutInfo leftLayout = new LayoutInfo(LayoutInfo.EAST, LayoutInfo.NONE, new Insets(0, 0, 0, 5), null);
      LayoutInfo rightLayout = new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, null, null);
      configPanel.add(Translate.label("surfaceAccuracy"), 0, 0, leftLayout);
      configPanel.add(Translate.label("shadingMethod"), 0, 1, leftLayout);
      configPanel.add(Translate.label("supersampling"), 0, 2, leftLayout);
      configPanel.add(errorField = new ValueField(0.02, ValueField.POSITIVE, 6), 1, 0, rightLayout);
      configPanel.add(shadeChoice = new BComboBox(new String [] {
        Translate.text("gouraud"),
        Translate.text("hybrid"),
        Translate.text("phong")
      }), 1, 1, rightLayout);
      shadeChoice.setSelectedIndex(1);
      configPanel.add(aliasChoice = new BComboBox(new String [] {
        Translate.text("none"),
        Translate.text("Edges"),
        Translate.text("Everything")
      }), 1, 2, rightLayout);
      configPanel.add(sampleChoice = new BComboBox(new String [] {"2x2", "3x3"}), 2, 2, rightLayout);
      sampleChoice.setEnabled(false);
      configPanel.add(transparentBox = new BCheckBox(Translate.text("transparentBackground"), false), 0, 3, 3, 1);
      configPanel.add(Translate.button("advanced", this, "showAdvancedWindow"), 0, 4, 3, 1);
      smoothField = new ValueField(1.0, ValueField.NONNEGATIVE);
      adaptiveBox = new BCheckBox(Translate.text("reduceAccuracyForDistant"), true);
      hideBackfaceBox = new BCheckBox(Translate.text("eliminateBackfaces"), true);
      aliasChoice.addEventLink(ValueChangedEvent.class, new Object() {
        void processEvent()
        {
          sampleChoice.setEnabled(aliasChoice.getSelectedIndex() > 0);
        }
      });
    }
    return configPanel;
  }
  
  private void showAdvancedWindow(WidgetEvent ev)
  {
    // Record the current settings.

    smoothing = smoothField.getValue();
    adaptive = adaptiveBox.getState();
    hideBackfaces = hideBackfaceBox.getState();
    
    // Show the window.
    
    WindowWidget parent = UIUtilities.findWindow(ev.getWidget());
    ComponentsDialog dlg;
    if (parent instanceof BDialog)
      dlg = new ComponentsDialog((BDialog) parent, Translate.text("advancedOptions"), 
          new Widget [] {smoothField, adaptiveBox, hideBackfaceBox},
          new String [] {Translate.text("texSmoothing"), null, null});
    else
      dlg = new ComponentsDialog((BFrame) parent, Translate.text("advancedOptions"), 
          new Widget [] {smoothField, adaptiveBox, hideBackfaceBox},
          new String [] {Translate.text("texSmoothing"), null, null});
    if (!dlg.clickedOk())
    {
      // Reset the components.
      
      smoothField.setValue(smoothing);
      adaptiveBox.setState(adaptive);
      hideBackfaceBox.setState(hideBackfaces);
    }
  }

  public boolean recordConfiguration()
  {
    smoothing = smoothField.getValue();
    adaptive = adaptiveBox.getState();
    hideBackfaces = hideBackfaceBox.getState();
    surfaceError = errorField.getValue();
    shadingMode = shadeChoice.getSelectedIndex();
    transparentBackground = transparentBox.getState();
    if (aliasChoice.getSelectedIndex() == 0)
      samplesPerPixel = subsample = 1;
    else if (aliasChoice.getSelectedIndex() == 1)
      samplesPerPixel = subsample = sampleChoice.getSelectedIndex()+2;
    else
      {
        samplesPerPixel = sampleChoice.getSelectedIndex()+2;
        subsample = 1;
      }
    return true;
  }
  
  /** Makes the configuration of this renderer identical to another one. */
  public synchronized void makeEqualConfig(Raster r)
  {
    smoothing = r.smoothing;
    adaptive = r.adaptive;
    hideBackfaces = r.hideBackfaces;
    surfaceError = r.surfaceError;
    shadingMode = r.shadingMode;
    transparentBackground = r.transparentBackground;
    samplesPerPixel = r.samplesPerPixel;
    subsample = r.subsample;
  }

  public Map getConfiguration()
  {
    HashMap map = new HashMap();
    map.put("textureSmoothing", new Double(smoothing));
    map.put("reduceAccuracyForDistant", new Boolean(adaptive));
    map.put("hideBackfaces", new Boolean(hideBackfaces));
    map.put("maxSurfaceError", new Double(surfaceError));
    map.put("shadingMethod", new Integer(shadingMode));
    map.put("transparentBackground", new Boolean(transparentBackground));
    int antialiasLevel = 0;
    if (samplesPerPixel == 2)
      antialiasLevel = subsample;
    else if (samplesPerPixel == 3)
      antialiasLevel = (subsample == 1 ? 3 : 4);
    map.put("antialiasing", new Integer(antialiasLevel));
    return map;
  }
  
  public void setConfiguration(String property, Object value)
  {
    if ("textureSmoothing".equals(property))
      smoothing = ((Number) value).doubleValue();
    else if ("reduceAccuracyForDistant".equals(property))
      adaptive = ((Boolean) value).booleanValue();
    else if ("hideBackfaces".equals(property))
      hideBackfaces = ((Boolean) value).booleanValue();
    else if ("maxSurfaceError".equals(property))
      surfaceError = ((Number) value).doubleValue();
    else if ("shadingMethod".equals(property))
      shadingMode = ((Integer) value).intValue();
    else if ("transparentBackground".equals(property))
      transparentBackground = ((Boolean) value).booleanValue();
    else if ("antialiasing".equals(property))
    {
      int antialiasLevel = ((Integer) value).intValue();
      switch (antialiasLevel)
      {
        case 0:
          samplesPerPixel = subsample = 1;
          break;
        case 1:
          samplesPerPixel = 2;
          subsample = 1;
          break;
        case 2:
          samplesPerPixel = 2;
          subsample = 2;
          break;
        case 3:
          samplesPerPixel = 3;
          subsample = 1;
          break;
        case 4:
          samplesPerPixel = 3;
          subsample = 3;
          break;
      }
    }
  }

  public void configurePreview()
  {
    transparentBackground = false;
    smoothing = 1.0;
    adaptive = hideBackfaces = true;
    surfaceError = 0.02;
    shadingMode = HYBRID;
    samplesPerPixel = subsample = 1;
  }
  
  /** Find all the light sources in the scene. */
  
  void findLights()
  {
    Vector lt = new Vector();
    Mat4 trans = theCamera.getWorldToView();
    int i;
    
    positionNeeded = false;
    for (i = 0; i < theScene.getNumObjects(); i++)
      {
        ObjectInfo info = theScene.getObject(i);
        if (info.object instanceof Light && info.visible)
          lt.addElement(info);
      }
    light = new ObjectInfo [lt.size()];
    for (i = 0; i < light.length; i++)
      {
	light[i] = (ObjectInfo) lt.elementAt(i);
	if (!(light[i].object instanceof DirectionalLight))
	  positionNeeded = true;
      }
    lightPosition = new Vec3 [light.length];
    lightDirection = new Vec3 [light.length];
  }
  
  /** Main method in which the image is rendered. */
  
  public void run()
  {
    try
    {
      Thread thisThread = Thread.currentThread();
      Vec3 viewdir, orig, center, hvec, vvec;
      Point p;
      boolean done = false;
      int i;

      if (renderThread != thisThread)
        return;
      updateTime = System.currentTimeMillis();

      // Record information about the scene.

      findLights();
      ambColor = theScene.getAmbientColor();
      envColor = theScene.getEnvironmentColor();
      envMapping = theScene.getEnvironmentMapping();
      envMode = theScene.getEnvironmentMode();
      fogColor = theScene.getFogColor();
      fog = theScene.getFogState();
      fogDist = theScene.getFogDistance();
      for (i = pixel.length-1; i >= 0; i--)
        {
          pixel[i] = 0;
          zbuffer[i] = Float.MAX_VALUE;
        }

      // Determine information about the viewpoint.

      viewdir = theCamera.getViewToWorld().timesDirection(Vec3.vz());
      p = new Point(width/2, height/2);
      orig = theCamera.getCameraCoordinates().getOrigin();
      center = theCamera.convertScreenToWorld(p, focalDist);
      p.x++;
      hvec = theCamera.convertScreenToWorld(p, focalDist).minus(center);
      p.x--;
      p.y++;
      vvec = theCamera.convertScreenToWorld(p, focalDist).minus(center);
      p.y--;
      smoothScale = smoothing*hvec.length()/focalDist;

      // Render the objects.

      for (i = theScene.getNumObjects()-1; i >= 0; i--)
        {
          ObjectInfo obj = theScene.getObject(i);
          theCamera.setObjectTransform(obj.coords.fromLocal());
          renderObject(obj, orig, viewdir, obj.coords.toLocal());
          if (thisThread != renderThread)
            {
              finish();
              return;
            }
          if (System.currentTimeMillis()-updateTime > 5000)
            updateImage();
        }

      // Apply fog and fill in the background.

      Vec3 dir = tempVec[1];
      if (fog || !transparentBackground)
        for (i = pixel.length-1; i >= 0; i--)
          {
            float transparency = 1.0f-(((pixel[i] >> 24) & 0xFF)/255.0f);
            tempColor[0].setARGB(pixel[i]);
            if (fog && transparency < 1.0f)
              {
                float fract1 = (float) Math.exp(-zbuffer[i]/fogDist), fract2 = 1.0f-fract1;
                tempColor[0].setRGB(fract1*tempColor[0].getRed() + fract2*fogColor.getRed(), 
                  fract1*tempColor[0].getGreen() + fract2*fogColor.getGreen(), 
                  fract1*tempColor[0].getBlue() + fract2*fogColor.getBlue());
                transparency *= fract1;
              }
            if (!transparentBackground && transparency > 0.0f)
              {
                float fract = 1.0f-transparency;
                if (envMode == SceneRenderInfoProvider.ENVIRON_SOLID)
                  tempColor[0].setRGB(fract*tempColor[0].getRed() + transparency*envColor.getRed(), 
                    fract*tempColor[0].getGreen() + transparency*envColor.getGreen(), 
                    fract*tempColor[0].getBlue() + transparency*envColor.getBlue());
                else
                  {
                    double h = (i%width)-width/2.0, v = (i/width)-height/2.0;
                    dir.x = center.x + h*hvec.x + v*vvec.x;
                    dir.y = center.y + h*hvec.y + v*vvec.y;
                    dir.z = center.z + h*hvec.z + v*vvec.z;
                    dir.subtract(orig);
                    dir.normalize();
                    envMapping.getTextureSpec(dir, surfSpec, 1.0, smoothScale, time, null);
                    if (envMode == SceneRenderInfoProvider.ENVIRON_DIFFUSE)
                      tempColor[0].setRGB(fract*tempColor[0].getRed() + transparency*surfSpec.diffuse.getRed(), 
                        fract*tempColor[0].getGreen() + transparency*surfSpec.diffuse.getGreen(), 
                        fract*tempColor[0].getBlue() + transparency*surfSpec.diffuse.getBlue());
                    else
                      tempColor[0].setRGB(fract*tempColor[0].getRed() + transparency*surfSpec.emissive.getRed(), 
                        fract*tempColor[0].getGreen() + transparency*surfSpec.emissive.getGreen(), 
                        fract*tempColor[0].getBlue() + transparency*surfSpec.emissive.getBlue());
                    transparency = 0.0f;
                  }
              }
            pixel[i] = calcARGB(tempColor[0], transparency);
          }
      createFinalImage();
      finish();
    }
    catch(NullPointerException npe)
    {
      // setAllNull();
    }
    catch(Throwable e)
    {
      e.printStackTrace();
      //finish();
      // setAllNull();
    }
//    catch(OutOfMemoryError ome)
//    {
//      ome.printStackTrace();
//      setAllNull();
//    }
  }

  /** Update the image being displayed. */

  private void updateImage()
  {
    if (imagePixel != pixel)
      {
	for (int i1 = 0, i2 = 0; i1 < imageHeight; i1++, i2 += samplesPerPixel)
	  for (int j1 = 0, j2 = 0; j1 < imageWidth; j1++, j2 += samplesPerPixel)
	    imagePixel[i1*imageWidth+j1] = pixel[i2*width+j2];
      }
    imageSource.newPixels();
    listener.imageUpdated(img);
    updateTime = System.currentTimeMillis();
  }
  
  /** Create the final version of the image. */

  private void createFinalImage()
  {
    int n = samplesPerPixel*samplesPerPixel;
    
    if (imagePixel != pixel)
      {
	for (int i1 = 0, i2 = 0; i1 < imageHeight; i1++, i2 += samplesPerPixel)
	  for (int j1 = 0, j2 = 0; j1 < imageWidth; j1++, j2 += samplesPerPixel)
	    {
	      int a = 0, r = 0, g = 0, b = 0;
	      for (int k = 0; k < samplesPerPixel; k++)
		{
		  int base = width*(i2+k)+j2;
		  for (int m = 0; m < samplesPerPixel; m++)
		    {
		      int color = pixel[base+m];
		      a += (color>>24)&0xFF;
		      r += (color>>16)&0xFF;
		      g += (color>>8)&0xFF;
		      b += color&0xFF;
		    }
		}
	      a /= n;
	      r /= n;
	      g /= n;
	      b /= n;
	      imagePixel[i1*imageWidth+j1] = (a<<24) + (r<<16) + (g<<8) + b;
	    }
        if (depthNeeded)
          for (int i1 = 0, i2 = 0; i1 < imageHeight; i1++, i2 += samplesPerPixel)
            for (int j1 = 0, j2 = 0; j1 < imageWidth; j1++, j2 += samplesPerPixel)
              {
                float minDepth = Float.MAX_VALUE;
                for (int k = 0; k < samplesPerPixel; k++)
                  {
                    int base = width*(i2+k)+j2;
                    for (int m = 0; m < samplesPerPixel; m++)
                      {
                        float z = zbuffer[base+m];
                        if (z < minDepth)
                          minDepth = z;
                      }
                  }
                imageZbuffer[i1*imageWidth+j1] = minDepth;
              }
      }
    imageSource.newPixels();
  }

  /** This routine is called when rendering is finished. */

  private void finish()
  {
    RenderListener rl = listener;
    setAllNull();
    if (rl != null)
    {
      ComplexImage image = new ComplexImage(img);
      if (depthNeeded)
        image.setComponentValues(ComplexImage.DEPTH, imageZbuffer);
      rl.imageComplete(image);
    }
  }
  
  /** This methods sets variables of the renderer to null. */
  
  protected synchronized void setAllNull()
  {
    light = null;
    theScene = null;
    theCamera = null;
    envMapping = null;
    renderThread = null;
    listener = null;
//    imageSource = null;
  }

  /** Given an RGBColor and a transparency value, calculate the ARGB value. */
  
  private int calcARGB(RGBColor color, double t)
  {
    if (!transparentBackground || t <= 0.0)
      return color.getARGB();
    if (t >= 1.0)
      return 0;
    double scale = 255.0/(1.0-t);
    int a, r, g, b;
    a = (int) (255.0*(1.0-t));
    r = (int) (color.getRed()*scale);
    g = (int) (color.getGreen()*scale);
    b = (int) (color.getBlue()*scale);
    if (r < 0) r = 0;
    if (r > 255) r = 255;
    if (g < 0) g = 0;
    if (g > 255) g = 255;
    if (b < 0) b = 0;
    if (b > 255) b = 255;
    return (a<<24) + (r<<16) + (g<<8) + b;
  }

  /** A faster replacement for Math.floor(). */
  
  private static int floor(double d)
  {
    if (d < 0.0)
    {
      int f = (int) d;
      if (f != d)
        f -= 1;
      return f;
    }
    return (int) d;
  }

  /** A faster replacement for Math.ceil(). */
  
  private static int ceil(double d)
  {
    if (d > 0.0)
    {
      int f = (int) d;
      if (f != d)
        f += 1;
      return f;
    }
    return ((int) d);
  }
  
  /** A faster replacement for Math.round(). */
  
  private static int round(double d)
  {
    return floor(d+0.5);
  }
  
  /** Render a single object into the scene.  viewdir is the direction from 
     which the object is being viewed in world coordinates. */
  
  private void renderObject(ObjectInfo obj, Vec3 orig, Vec3 viewdir, Mat4 toLocal)
  {
    RenderingMesh mesh;
    Object3D theObject;
    TextureSpec spec;
    double tol;
    int i;
    
    if (Thread.currentThread() != renderThread)
      return;
    if (!obj.visible)
      return;
    theObject = obj.object;
    if (theCamera.visibility(obj.getBounds()) == Camera.NOT_VISIBLE)
      return;
    if (theObject instanceof ObjectCollection)
      {
        Enumeration objenum = ((ObjectCollection) theObject).getObjects(obj, false, theScene);
        Mat4 fromLocal = theCamera.getObjectToWorld();
        while (objenum.hasMoreElements())
          {
            ObjectInfo elem = (ObjectInfo) objenum.nextElement();
            CoordinateSystem coords = elem.coords.duplicate();
            coords.transformCoordinates(fromLocal);
            theCamera.setObjectTransform(coords.fromLocal());
            renderObject(elem, orig, viewdir, coords.toLocal());
          }
        return;
      }
    if (adaptive)
      {
	double dist = obj.getBounds().distanceToPoint(toLocal.times(orig));
	double distToScreen = theCamera.getDistToScreen();
	if (dist < distToScreen)
	  tol = surfaceError;
	else
	  tol = surfaceError*dist/distToScreen;
      }
    else
      tol = surfaceError;
    mesh = obj.getRenderingMesh(tol);
    if (mesh == null)
      return;
    if (Thread.currentThread() != renderThread)
      return;
    viewdir = toLocal.timesDirection(viewdir);
    for (i = light.length-1; i >= 0; i--)
    {
      lightPosition[i] = toLocal.times(light[i].coords.getOrigin());
      if (!(light[i].object instanceof PointLight))
	lightDirection[i] = toLocal.timesDirection(light[i].coords.getZDirection());
    }
    boolean bumpMap = theObject.getTexture().hasComponent(Texture.BUMP_COMPONENT);
    if (theObject.getTexture().hasComponent(Texture.DISPLACEMENT_COMPONENT))
      renderMeshDisplaced(mesh, viewdir, tol, theObject.isClosed(), bumpMap);
    else if (shadingMode == GOURAUD)
      renderMeshGouraud(mesh, viewdir, theObject.isClosed());
    else if (shadingMode == HYBRID && !bumpMap)
      renderMeshHybrid(mesh, viewdir, theObject.isClosed());
    else
      renderMeshPhong(mesh, viewdir, theObject.isClosed(), bumpMap);
  }
  
  /** Calculate the lighting model at a point on a surface.  If either diffuse or specular
     is null, the component will not be calculated. */
  
  private void calcLight(Vec3 pos, Vec3 norm, Vec3 viewdir, Vec3 faceNorm, double roughness, RGBColor diffuse, RGBColor specular)
  {
    Vec3 reflectDir = tempVec[0], lightDir = tempVec[1];
    double viewDot = viewdir.dot(norm), faceDot = viewdir.dot(faceNorm);
    RGBColor outputColor = tempColor[0];
    
    if (diffuse != null)
      diffuse.copy(ambColor);
    if (specular != null)
      {
	if (envMode == SceneRenderInfoProvider.ENVIRON_SOLID)
	  specular.copy(envColor);
	else
	  {
	    // Find the reflection direction and add in the environment color.

	    reflectDir.set(norm);
	    reflectDir.scale(-2.0*viewDot);
	    reflectDir.add(viewdir);
	    theCamera.getViewToWorld().transformDirection(reflectDir);
	    envMapping.getTextureSpec(reflectDir, surfSpec2, 1.0, smoothScale, time, null);
	    if (envMode == SceneRenderInfoProvider.ENVIRON_DIFFUSE)
	      specular.copy(surfSpec2.diffuse);
	    else
	      specular.copy(surfSpec2.emissive);
	  }
      }
    
    // Prevent artifacts where the triangle is facing toward the viewer, but the local
    // interpolated normal is facing away.
    
    if (viewDot < 0.0 && faceDot > 0.0)
      viewDot = TOL;
    else if (viewDot > 0.0 && faceDot < 0.0)
      viewDot = -TOL;
    
    // Loop over the lights and add in each one.
    
    for (int i = light.length-1; i >= 0; i--)
      {
	Light lt = (Light) light[i].object;
	Vec3 lightPos = lightPosition[i];
	double distToLight = 0.0, fatt = 0.0, lightDot = 0.0;
	if (lt instanceof PointLight)
	  {
	    lightDir.set(pos);
	    lightDir.subtract(lightPos);
	    distToLight = lightDir.length();
	    lightDir.normalize();
	  }
	else if (lt instanceof SpotLight)
	  {
	    lightDir.set(pos);
	    lightDir.subtract(lightPos);
	    distToLight = lightDir.length();
	    lightDir.normalize();
	    fatt = lightDir.dot(lightDirection[i]);
	    if (fatt < ((SpotLight) lt).getAngleCosine())
	      continue;
	  }
	else if (lt instanceof DirectionalLight)
	  lightDir.set(lightDirection[i]);
	lt.getLight(outputColor, (float) distToLight);
	if (lt instanceof SpotLight)
	  outputColor.scale(Math.pow(fatt, ((SpotLight) lt).getExponent()));
	if (lt.isAmbient())
	  {
	    if (diffuse != null)
	      diffuse.add(outputColor.getRed(), outputColor.getGreen(), outputColor.getBlue());
	    continue;
	  }
	lightDot = lightDir.dot(norm);
	if ((lightDot >= 0.0 && viewDot <= 0.0) || (lightDot <= 0.0 && viewDot >= 0.0))
	  continue;
	if (diffuse != null)
	  {
	    float dot = (float) (lightDot < 0.0 ? -lightDot : lightDot);
	    diffuse.add(outputColor.getRed()*dot, outputColor.getGreen()*dot, outputColor.getBlue()*dot);
	  }
	if (specular != null)
	  {
	    lightDir.add(viewdir);
	    lightDir.normalize();
	    double dot = lightDir.dot(norm);
	    dot = (dot < 0.0 ? -dot : dot);
	    outputColor.scale(Math.pow(dot, (1.0-roughness)*128.0+1.0));
	    specular.add(outputColor);
	  }
      }
  }

  /** Clip a triangle to the region in front of the z clipping plane. */
  
  private Vec3 [] clipTriangle(Vec3 v1, Vec3 v2, Vec3 v3, float z1, float z2, float z3, float newz[], double newu[], double newv[])
  {
    double clip = theCamera.getClipDistance();
    Mat4 toScreen = theCamera.getObjectToScreen();
    boolean c1 = z1 < clip, c2 = z2 < clip, c3 = z3 < clip;
    Vec3 u1, u2, u3, u4;
    int clipCount = 0;
    
    if (c1) clipCount++;
    if (c2) clipCount++;
    if (c3) clipCount++;
    if (clipCount == 2)
      {
	// Two vertices need to be clipped.
	
	if (!c1)
	  {
	    u1 = v1;
	    newz[0] = z1;
	    newu[0] = 1.0;
	    newv[0] = 0.0;
	    double f2 = (z1-clip)/(z1-z2), f1 = 1.0-f2;
	    u2 = new Vec3(f1*v1.x+f2*v2.x, f1*v1.y+f2*v2.y, f1*v1.z+f2*v2.z);
	    newz[1] = (float) (f1*z1 + f2*z2);
	    newu[1] = f1;
	    newv[1] = f2;
	    f2 = (z1-clip)/(z1-z3);
	    f1 = 1.0-f2;
	    u3 = new Vec3(f1*v1.x+f2*v3.x, f1*v1.y+f2*v3.y, f1*v1.z+f2*v3.z);
	    newz[2] = (float) (f1*z1 + f2*z3);
	    newu[2] = f1;
	    newv[2] = 0.0;
	  }
	else if (!c2)
	  {
	    u2 = v2;
	    newz[1] = z2;
	    newu[1] = 0.0;
	    newv[1] = 1.0;
	    double f2 = (z2-clip)/(z2-z3), f1 = 1.0-f2;
	    u3 = new Vec3(f1*v2.x+f2*v3.x, f1*v2.y+f2*v3.y, f1*v2.z+f2*v3.z);
	    newz[2] = (float) (f1*z2 + f2*z3);
	    newu[2] = 0.0;
	    newv[2] = f1;
	    f2 = (z2-clip)/(z2-z1);
	    f1 = 1.0-f2;
	    u1 = new Vec3(f1*v2.x+f2*v1.x, f1*v2.y+f2*v1.y, f1*v2.z+f2*v1.z);
	    newz[0] = (float) (f1*z2 + f2*z1);
	    newu[0] = f2;
	    newv[0] = f1;
	  }
	else
	  {
	    u3 = v3;
	    newz[2] = z3;
	    newu[2] = 0.0;
	    newv[2] = 0.0;
	    double f2 = (z3-clip)/(z3-z1), f1 = 1.0-f2;
	    u1 = new Vec3(f1*v3.x+f2*v1.x, f1*v3.y+f2*v1.y, f1*v3.z+f2*v1.z);
	    newz[0] = (float) (f1*z3 + f2*z1);
	    newu[0] = f2;
	    newv[0] = 0.0;
	    f2 = (z3-clip)/(z3-z2);
	    f1 = 1.0-f2;
	    u2 = new Vec3(f1*v3.x+f2*v2.x, f1*v3.y+f2*v2.y, f1*v3.z+f2*v2.z);
	    newz[1] = (float) (f1*z3 + f2*z2);
	    newu[1] = 0.0;
	    newv[1] = f2;
	  }
	return new Vec3 [] {u1, u2, u3};
      }
    
    // Only one vertex needs to be clipped, resulting in a quad.
    
    if (c1)
      {
	u1 = v2;
	newz[0] = z2;
	newu[0] = 0.0;
	newv[0] = 1.0;
	u2 = v3;
	newz[1] = z3;
	newu[1] = 0.0;
	newv[1] = 0.0;
	double f1 = (z2-clip)/(z2-z1), f2 = 1.0-f1;
	u3 = new Vec3(f1*v1.x+f2*v2.x, f1*v1.y+f2*v2.y, f1*v1.z+f2*v2.z);
	newz[2] = (float) (f1*z1 + f2*z2);
	newu[2] = f1;
	newv[2] = f2;
	f1 = (z3-clip)/(z3-z1);
	f2 = 1.0-f1;
	u4 = new Vec3(f1*v1.x+f2*v3.x, f1*v1.y+f2*v3.y, f1*v1.z+f2*v3.z);
	newz[3] = (float) (f1*z1 + f2*z3);
	newu[3] = f1;
	newv[3] = 0.0;
      }
    else if (c2)
      {
	u1 = v3;
	newz[0] = z3;
	newu[0] = 0.0;
	newv[0] = 0.0;
	u2 = v1;
	newz[1] = z1;
	newu[1] = 1.0;
	newv[1] = 0.0;
	double f1 = (z3-clip)/(z3-z2), f2 = 1.0-f1;
	u3 = new Vec3(f1*v2.x+f2*v3.x, f1*v2.y+f2*v3.y, f1*v2.z+f2*v3.z);
	newz[2] = (float) (f1*z2 + f2*z3);
	newu[2] = 0.0;
	newv[2] = f1;
	f1 = (z1-clip)/(z1-z2);
	f2 = 1.0-f1;
	u4 = new Vec3(f1*v2.x+f2*v1.x, f1*v2.y+f2*v1.y, f1*v2.z+f2*v1.z);
	newz[3] = (float) (f1*z2 + f2*z1);
	newu[3] = f2;
	newv[3] = f1;
      }
    else
      {
	u1 = v1;
	newz[0] = z1;
	newu[0] = 1.0;
	newv[0] = 0.0;
	u2 = v2;
	newz[1] = z2;
	newu[1] = 0.0;
	newv[1] = 1.0;
	double f1 = (z1-clip)/(z1-z3), f2 = 1.0-f1;
	u3 = new Vec3(f1*v3.x+f2*v1.x, f1*v3.y+f2*v1.y, f1*v3.z+f2*v1.z);
	newz[2] = (float) (f1*z3 + f2*z1);
	newu[2] = f2;
	newv[2] = 0.0;
	f1 = (z2-clip)/(z2-z3);
	f2 = 1.0-f1;
	u4 = new Vec3(f1*v3.x+f2*v2.x, f1*v3.y+f2*v2.y, f1*v3.z+f2*v2.z);
	newz[3] = (float) (f1*z3 + f2*z2);
	newu[3] = 0.0;
	newv[3] = f2;
      }
    return new Vec3 [] {u1, u2, u3, u4};
  }

  /** Render a triangle mesh with Gouraud shading. */
    
  private void renderMeshGouraud(RenderingMesh mesh, Vec3 viewdir, boolean isClosed)
  {
    Vec3 vert[] = mesh.vert, norm[] = mesh.norm;
    Vec2 pos[] = new Vec2 [vert.length];
    float z[] = new float [vert.length], clip = (float) theCamera.getClipDistance(), clipz[] = new float [4];
    double dot, clipu[] = new double [4], clipv[] = new double [4];
    double distToScreen = theCamera.getDistToScreen(), tol = smoothScale;
    RGBColor diffuse[] = new RGBColor [4], specular[] = new RGBColor [4];
    Mat4 toView = theCamera.getObjectToView(), toScreen = theCamera.getObjectToScreen();
    RenderingTriangle tri;
    int i, v1, v2, v3, n1, n2, n3;
    boolean hide = (hideBackfaces && isClosed), backface;
    
    for (i = 0; i < 4; i++)
      {
	diffuse[i] = new RGBColor();
	specular[i] = new RGBColor();
      }
    for (i = vert.length-1; i >= 0; i--)
      {
	pos[i] = toScreen.timesXY(vert[i]);
	z[i] = (float) toView.timesZ(vert[i]);
      }
    for (i = mesh.triangle.length-1; i >= 0; i--)
      {
	tri = mesh.triangle[i];
	v1 = tri.v1;
	v2 = tri.v2;
	v3 = tri.v3;
	n1 = tri.n1;
	n2 = tri.n2;
	n3 = tri.n3;
	if (z[v1] < clip && z[v2] < clip && z[v3] < clip)
	  continue;
	backface = ((pos[v2].x-pos[v1].x)*(pos[v3].y-pos[v1].y) - (pos[v2].y-pos[v1].y)*(pos[v3].x-pos[v1].x) > 0.0);
        double viewdot = viewdir.dot(mesh.faceNorm[i]);
	if (z[v1] < clip || z[v2] < clip || z[v3] < clip)
	  {
	    Vec3 clipPos[] = clipTriangle(vert[v1], vert[v2], vert[v3], z[v1], z[v2], z[v3], clipz, clipu, clipv);
	    Vec2 clipPos2D[] = new Vec2 [clipPos.length];
	    for (int j = clipPos.length-1; j >= 0; j--)
	      {
	        clipPos2D[j] = toScreen.timesXY(clipPos[j]);
	        double u = clipu[j], v = clipv[j], w = 1.0-u-v;
		tri.getTextureSpec(surfSpec, viewdot, u, v, 1.0-u-v, tol, time);
		tempVec[2].set(norm[n1].x*u + norm[n2].x*v + norm[n3].x*w, norm[n1].y*u + norm[n2].y*v + norm[n3].y*w, norm[n1].z*u + norm[n2].z*v + norm[n3].z*w);
		tempVec[2].normalize();
		calcLight(clipPos[j], tempVec[2], viewdir, mesh.faceNorm[i], surfSpec.roughness, diffuse[j], specular[j]);
	      }
	    renderTriangleGouraud(clipPos2D[0], clipz[0], clipu[0], clipv[0], diffuse[0], specular[0], 
	        clipPos2D[1], clipz[1], clipu[1], clipv[1], diffuse[1], specular[1], 
	        clipPos2D[2], clipz[2], clipu[2], clipv[2], diffuse[2], specular[2], 
	        tri, clip, viewdot);
	    if (clipPos.length == 4)
	      renderTriangleGouraud(clipPos2D[1], clipz[1], clipu[1], clipv[1], diffuse[1], specular[1], 
	        clipPos2D[2], clipz[2], clipu[2], clipv[2], diffuse[2], specular[2], 
	        clipPos2D[3], clipz[3], clipu[3], clipv[3], diffuse[3], specular[3], 
	        tri, clip, viewdot);
	  }
	else
	  {
            if (hide && backface)
              continue;
	    if (z[v1] > distToScreen)
	      tol = smoothScale*z[v1];
	    tri.getTextureSpec(surfSpec, viewdot, 1.0, 0.0, 0.0, tol, time);
	    calcLight(vert[v1], norm[n1], viewdir, mesh.faceNorm[i], surfSpec.roughness, diffuse[0], specular[0]);
	    if (z[v2] > distToScreen)
	      tol = smoothScale*z[v2];
	    tri.getTextureSpec(surfSpec, viewdot, 0.0, 1.0, 0.0, tol, time);
	    calcLight(vert[v2], norm[n2], viewdir, mesh.faceNorm[i], surfSpec.roughness, diffuse[1], specular[1]);
	    if (z[v3] > distToScreen)
	      tol = smoothScale*z[v3];
	    tri.getTextureSpec(surfSpec, viewdot, 0.0, 0.0, 1.0, tol, time);
	    calcLight(vert[v3], norm[n3], viewdir, mesh.faceNorm[i], surfSpec.roughness, diffuse[2], specular[2]);
	    renderTriangleGouraud(pos[v1], z[v1], 1.0, 0.0, diffuse[0], specular[0], 
	        pos[v2], z[v2], 0.0, 1.0, diffuse[1], specular[1], 
	        pos[v3], z[v3], 0.0, 0.0, diffuse[2], specular[2], 
	        tri, clip, viewdot);
	  }
      }
  }

  /** Render a triangle with Gouraud shading. */

  private void renderTriangleGouraud(Vec2 pos1, float zf1, double uf1, double vf1, RGBColor diffuse1, RGBColor specular1, 
        Vec2 pos2, float zf2, double uf2, double vf2, RGBColor diffuse2, RGBColor specular2, 
        Vec2 pos3, float zf3, double uf3, double vf3, RGBColor diffuse3, RGBColor specular3, 
        RenderingTriangle tri, double clip, double viewdot)
  {
    double x1, x2, x3, y1, y2, y3;
    double dx1, dx2, dy1, dy2, mx1, mx2;
    double xstart, xend, delta;
    float z1, z2, z3, dz1, dz2, mz1, mz2, zstart, zend, z, zl, dz;
    double u1, u2, u3, v1, v2, v3, du1, du2, dv1, dv2, mu1, mu2, mv1, mv2;
    double ustart, uend, vstart, vend, u, v, ul, vl, wl, du, dv;
    RGBColor dif1, dif2, dif3, spec1, spec2, spec3;
    float ddifred1, ddifred2, ddifgreen1, ddifgreen2, ddifblue1, ddifblue2;
    float mdifred1, mdifred2, mdifgreen1, mdifgreen2, mdifblue1, mdifblue2;
    float dspecred1, dspecred2, dspecgreen1, dspecgreen2, dspecblue1, dspecblue2;
    float mspecred1, mspecred2, mspecgreen1, mspecgreen2, mspecblue1, mspecblue2;
    float difredstart, difredend, difgreenstart, difgreenend, difbluestart, difblueend;
    float specredstart, specredend, specgreenstart, specgreenend, specbluestart, specblueend;
    float difred, difgreen, difblue, ddifred, ddifgreen, ddifblue;
    float specred, specgreen, specblue, dspecred, dspecgreen, dspecblue;
    float denom;
    double distToScreen = theCamera.getDistToScreen();
    int left, right, prevLeft = width, prevRight = -1, i, index, yend, y;
    boolean doSubsample = (subsample > 1), repeat;
    
    if (pos1.y <= pos2.y && pos1.y <= pos3.y)
      {
	x1 = pos1.x;
	y1 = pos1.y;
	z1 = zf1;
	u1 = uf1;
	v1 = vf1;
	dif1 = diffuse1;
	spec1 = specular1;
	if (pos2.y < pos3.y)
	  {
	    x2 = pos2.x;
	    y2 = pos2.y;
	    z2 = zf2;
	    u2 = uf2;
	    v2 = vf2;
	    dif2 = diffuse2;
	    spec2 = specular2;
	    x3 = pos3.x;
	    y3 = pos3.y;
	    z3 = zf3;
	    u3 = uf3;
	    v3 = vf3;
	    dif3 = diffuse3;
	    spec3 = specular3;
	  }
	else
	  {
	    x2 = pos3.x;
	    y2 = pos3.y;
	    z2 = zf3;
	    u2 = uf3;
	    v2 = vf3;
	    dif2 = diffuse3;
	    spec2 = specular3;
	    x3 = pos2.x;
	    y3 = pos2.y;
	    z3 = zf2;
	    u3 = uf2;
	    v3 = vf2;
	    dif3 = diffuse2;
	    spec3 = specular2;
	  }
      }
    else if (pos2.y <= pos1.y && pos2.y <= pos3.y)
      {
	x1 = pos2.x;
	y1 = pos2.y;
	z1 = zf2;
	u1 = uf2;
	v1 = vf2;
	dif1 = diffuse2;
	spec1 = specular2;
	if (pos1.y < pos3.y)
	  {
	    x2 = pos1.x;
	    y2 = pos1.y;
	    z2 = zf1;
	    u2 = uf1;
	    v2 = vf1;
	    dif2 = diffuse1;
	    spec2 = specular1;
	    x3 = pos3.x;
	    y3 = pos3.y;
	    z3 = zf3;
	    u3 = uf3;
	    v3 = vf3;
	    dif3 = diffuse3;
	    spec3 = specular3;
	  }
	else
	  {
	    x2 = pos3.x;
	    y2 = pos3.y;
	    z2 = zf3;
	    u2 = uf3;
	    v2 = vf3;
	    dif2 = diffuse3;
	    spec2 = specular3;
	    x3 = pos1.x;
	    y3 = pos1.y;
	    z3 = zf1;
	    u3 = uf1;
	    v3 = vf1;
	    dif3 = diffuse1;
	    spec3 = specular1;
	  }
      }
    else
      {
	x1 = pos3.x;
	y1 = pos3.y;
	z1 = zf3;
	u1 = uf3;
	v1 = vf3;
	dif1 = diffuse3;
	spec1 = specular3;
	if (pos1.y < pos2.y)
	  {
	    x2 = pos1.x;
	    y2 = pos1.y;
	    z2 = zf1;
	    u2 = uf1;
	    v2 = vf1;
	    dif2 = diffuse1;
	    spec2 = specular1;
	    x3 = pos2.x;
	    y3 = pos2.y;
	    z3 = zf2;
	    u3 = uf2;
	    v3 = vf2;
	    dif3 = diffuse2;
	    spec3 = specular2;
	  }
	else
	  {
	    x2 = pos2.x;
	    y2 = pos2.y;
	    z2 = zf2;
	    u2 = uf2;
	    v2 = vf2;
	    dif2 = diffuse2;
	    spec2 = specular2;
	    x3 = pos1.x;
	    y3 = pos1.y;
	    z3 = zf1;
	    u3 = uf1;
	    v3 = vf1;
	    dif3 = diffuse1;
	    spec3 = specular1;
	  }
      }
    z1 = 1.0f/z1;
    u1 *= z1;
    v1 *= z1;
    z2 = 1.0f/z2;
    u2 *= z2;
    v2 *= z2;
    z3 = 1.0f/z3;
    u3 *= z3;
    v3 *= z3;
    dx1 = x3-x1;
    dy1 = y3-y1;
    dz1 = z3-z1;
    if (dy1 == 0)
      return;
    du1 = u3-u1;
    dv1 = v3-v1;
    ddifred1 = dif3.getRed()-dif1.getRed();
    ddifgreen1 = dif3.getGreen()-dif1.getGreen();
    ddifblue1 = dif3.getBlue()-dif1.getBlue();
    dspecred1 = spec3.getRed()-spec1.getRed();
    dspecgreen1 = spec3.getGreen()-spec1.getGreen();
    dspecblue1 = spec3.getBlue()-spec1.getBlue();
    dx2 = x2-x1;
    dy2 = y2-y1;
    dz2 = z2-z1;
    du2 = u2-u1;
    dv2 = v2-v1;
    ddifred2 = dif2.getRed()-dif1.getRed();
    ddifgreen2 = dif2.getGreen()-dif1.getGreen();
    ddifblue2 = dif2.getBlue()-dif1.getBlue();
    dspecred2 = spec2.getRed()-spec1.getRed();
    dspecgreen2 = spec2.getGreen()-spec1.getGreen();
    dspecblue2 = spec2.getBlue()-spec1.getBlue();
    denom = (float) (1.0/dy1);
    mx1 = dx1*denom;
    mz1 = dz1*denom;
    mu1 = du1*denom;
    mv1 = dv1*denom;
    mdifred1 = ddifred1*denom;
    mdifgreen1 = ddifgreen1*denom;
    mdifblue1 = ddifblue1*denom;
    mspecred1 = dspecred1*denom;
    mspecgreen1 = dspecgreen1*denom;
    mspecblue1 = dspecblue1*denom;
    xstart = xend = x1;
    zstart = zend = z1;
    ustart = uend = u1;
    vstart = vend = v1;
    difredstart = difredend = dif1.getRed();
    difgreenstart = difgreenend = dif1.getGreen();
    difbluestart = difblueend = dif1.getBlue();
    specredstart = specredend = spec1.getRed();
    specgreenstart = specgreenend = spec1.getGreen();
    specbluestart = specblueend = spec1.getBlue();
    y = round(y1);
    if (dy2 > 0.0)
      {
	denom = (float) (1.0/dy2);
	mx2 = dx2*denom;
	mz2 = dz2*denom;
	mu2 = du2*denom;
	mv2 = dv2*denom;
	mdifred2 = ddifred2*denom;
	mdifgreen2 = ddifgreen2*denom;
	mdifblue2 = ddifblue2*denom;
	mspecred2 = dspecred2*denom;
	mspecgreen2 = dspecgreen2*denom;
	mspecblue2 = dspecblue2*denom;
	if (y2 < 0)
	  {
	    xstart += mx1*dy2;
	    xend += mx2*dy2;
	    zstart += mz1*dy2;
	    zend += mz2*dy2;
	    ustart += mu1*dy2;
	    uend += mu2*dy2;
	    vstart += mv1*dy2;
	    vend += mv2*dy2;
	    difredstart += mdifred1*dy2;
	    difredend += mdifred2*dy2;
	    difgreenstart += mdifgreen1*dy2;
	    difgreenend += mdifgreen2*dy2;
	    difbluestart += mdifblue1*dy2;
	    difblueend += mdifblue2*dy2;
	    specredstart += mspecred1*dy2;
	    specredend += mspecred2*dy2;
	    specgreenstart += mspecgreen1*dy2;
	    specgreenend += mspecgreen2*dy2;
	    specbluestart += mspecblue1*dy2;
	    specblueend += mspecblue2*dy2;
	    y = round(y2);
	  }
	else if (y < 0)
	  {
	    xstart -= mx1*y;
	    xend -= mx2*y;
	    zstart -= mz1*y;
	    zend -= mz2*y;
	    ustart -= mu1*y;
	    uend -= mu2*y;
	    vstart -= mv1*y;
	    vend -= mv2*y;
	    difredstart -= mdifred1*y;
	    difredend -= mdifred2*y;
	    difgreenstart -= mdifgreen1*y;
	    difgreenend -= mdifgreen2*y;
	    difbluestart -= mdifblue1*y;
	    difblueend -= mdifblue2*y;
	    specredstart -= mspecred1*y;
	    specredend -= mspecred2*y;
	    specgreenstart -= mspecgreen1*y;
	    specgreenend -= mspecgreen2*y;
	    specbluestart -= mspecblue1*y;
	    specblueend -= mspecblue2*y;
	    y = 0;
	  }
	yend = round(y2);
	if (yend > height)
	  yend = height;
	index = y*width;
	while (y < yend)
	  {
	    if (xstart < xend)
	      {
		left = floor(xstart);
		right = ceil(xend);
		delta = xstart-left;
		z = zstart;
		dz = zend-zstart;
		u = ustart;
		du = uend-ustart;
		v = vstart;
		dv = vend-vstart;
		difred = difredstart;
		ddifred = difredend-difredstart;
		difgreen = difgreenstart;
		ddifgreen = difgreenend-difgreenstart;
		difblue = difbluestart;
		ddifblue = difblueend-difbluestart;
		specred = specredstart;
		dspecred = specredend-specredstart;
		specgreen = specgreenstart;
		dspecgreen = specgreenend-specgreenstart;
		specblue = specbluestart;
		dspecblue = specblueend-specbluestart;
	      }
	    else
	      {
		left = floor(xend);
		right = ceil(xstart);
		delta = xend-left;
		z = zend;
		dz = zstart-zend;
		u = uend;
		du = ustart-uend;
		v = vend;
		dv = vstart-vend;
		difred = difredend;
		ddifred = difredstart-difredend;
		difgreen = difgreenend;
		ddifgreen = difgreenstart-difgreenend;
		difblue = difblueend;
		ddifblue = difbluestart-difblueend;
		specred = specredend;
		dspecred = specredstart-specredend;
		specgreen = specgreenend;
		dspecgreen = specgreenstart-specgreenend;
		specblue = specblueend;
		dspecblue = specbluestart-specblueend;
	      }
	    if (left != right)
	      {
		if (xend == xstart)
		  denom = 1.0f;
		else if (xend > xstart)
		  denom = (float) (1.0/(xend-xstart));
		else
		  denom = (float) (1.0/(xstart-xend));
		dz *= denom;
		du *= denom;
		dv *= denom;
		ddifred *= denom;
		ddifgreen *= denom;
		ddifblue *= denom;
		dspecred *= denom;
		dspecgreen *= denom;
		dspecblue *= denom;
		if (left < 0)
		  {
		    delta += left;
		    left = 0;
		  }
		u -= du*delta;
		v -= dv*delta;
		z -= dz*delta;
		difred -= ddifred*delta;
		difgreen -= ddifgreen*delta;
		difblue -= ddifblue*delta;
		specred -= dspecred*delta;
		specgreen -= dspecgreen*delta;
		specblue -= dspecblue*delta;
		if (right > width)
		  right = width;
		repeat = false;
		for (i = left; i < right; i++)
		  {
		    zl = 1.0f/z;
		    if (zl < zbuffer[index+i] && zl > clip)
		      {
			if (repeat && (i%subsample != 0))
			  {
			    pixel[index+i] = pixel[index+i-1];
			    zbuffer[index+i] = zl;
			  }
			else if (repeat && (y%subsample != 0) && (i >= prevLeft) && (i < prevRight))
			  {
			    pixel[index+i] = pixel[index+i-width];
			    zbuffer[index+i] = zl;
			  }
			else
			  {
			    ul = u*zl;
			    vl = v*zl;
			    wl = 1.0-ul-vl;
			    tri.getTextureSpec(surfSpec, viewdot, ul, vl, wl, smoothScale*z, time);
			    tempColor[0].setRGB(surfSpec.diffuse.getRed()*difred + surfSpec.hilight.getRed()*specred + surfSpec.emissive.getRed(),
			      surfSpec.diffuse.getGreen()*difgreen + surfSpec.hilight.getGreen()*specgreen + surfSpec.emissive.getGreen(),
			      surfSpec.diffuse.getBlue()*difblue + surfSpec.hilight.getBlue()*specblue + surfSpec.emissive.getBlue());
			    pixel[index+i] = calcARGB(tempColor[0], 0.0);
			    zbuffer[index+i] = zl;
			  }
			repeat = doSubsample;
		      }
		    else
		      repeat = false;
		    z += dz;
		    u += du;
		    v += dv;
		    difred += ddifred;
		    difgreen += ddifgreen;
		    difblue += ddifblue;
		    specred += dspecred;
		    specgreen += dspecgreen;
		    specblue += dspecblue;
		  }
		prevLeft = left;
		prevRight = right;
	      }
	    xstart += mx1;
	    zstart += mz1;
	    ustart += mu1;
	    vstart += mv1;
	    difredstart += mdifred1;
	    difgreenstart += mdifgreen1;
	    difbluestart += mdifblue1;
	    specredstart += mspecred1;
	    specgreenstart += mspecgreen1;
	    specbluestart += mspecblue1;
	    xend += mx2;
	    zend += mz2;
	    uend += mu2;
	    vend += mv2;
	    difredend += mdifred2;
	    difgreenend += mdifgreen2;
	    difblueend += mdifblue2;
	    specredend += mspecred2;
	    specgreenend += mspecgreen2;
	    specblueend += mspecblue2;
	    index += width;
	    y++;
	  }
      }
    dx2 = x3-x2;
    dy2 = y3-y2;
    dz2 = z3-z2;
    du2 = u3-u2;
    dv2 = v3-v2;
    ddifred2 = dif3.getRed()-dif2.getRed();
    ddifgreen2 = dif3.getGreen()-dif2.getGreen();
    ddifblue2 = dif3.getBlue()-dif2.getBlue();
    dspecred2 = spec3.getRed()-spec2.getRed();
    dspecgreen2 = spec3.getGreen()-spec2.getGreen();
    dspecblue2 = spec3.getBlue()-spec2.getBlue();
    if (dy2 > 0.0)
      {
	denom = (float) (1.0/dy2);
	mx2 = dx2*denom;
	mz2 = dz2*denom;
	mu2 = du2*denom;
	mv2 = dv2*denom;
	mdifred2 = ddifred2*denom;
	mdifgreen2 = ddifgreen2*denom;
	mdifblue2 = ddifblue2*denom;
	mspecred2 = dspecred2*denom;
	mspecgreen2 = dspecgreen2*denom;
	mspecblue2 = dspecblue2*denom;
	xend = x2;
	zend = z2;
	uend = u2;
	vend = v2;
	difredend = dif2.getRed();
	difgreenend = dif2.getGreen();
	difblueend = dif2.getBlue();
	specredend = spec2.getRed();
	specgreenend = spec2.getGreen();
	specblueend = spec2.getBlue();
	if (y < 0)
	  {
	    xstart -= mx1*y;
	    xend -= mx2*y;
	    zstart -= mz1*y;
	    zend -= mz2*y;
	    ustart -= mu1*y;
	    uend -= mu2*y;
	    vstart -= mv1*y;
	    vend -= mv2*y;
	    difredstart -= mdifred1*y;
	    difredend -= mdifred2*y;
	    difgreenstart -= mdifgreen1*y;
	    difgreenend -= mdifgreen2*y;
	    difbluestart -= mdifblue1*y;
	    difblueend -= mdifblue2*y;
	    specredstart -= mspecred1*y;
	    specredend -= mspecred2*y;
	    specgreenstart -= mspecgreen1*y;
	    specgreenend -= mspecgreen2*y;
	    specbluestart -= mspecblue1*y;
	    specblueend -= mspecblue2*y;
	    y = 0;
	  }
	yend = round(y3 < height ? y3 : height);
	index = y*width;
	while (y < yend)
	  {
	    if (xstart < xend)
	      {
		left = floor(xstart);
		right = ceil(xend);
		delta = xstart-left;
		z = zstart;
		dz = zend-zstart;
		u = ustart;
		du = uend-ustart;
		v = vstart;
		dv = vend-vstart;
		difred = difredstart;
		ddifred = difredend-difredstart;
		difgreen = difgreenstart;
		ddifgreen = difgreenend-difgreenstart;
		difblue = difbluestart;
		ddifblue = difblueend-difbluestart;
		specred = specredstart;
		dspecred = specredend-specredstart;
		specgreen = specgreenstart;
		dspecgreen = specgreenend-specgreenstart;
		specblue = specbluestart;
		dspecblue = specblueend-specbluestart;
	      }
	    else
	      {
		left = floor(xend);
		right = ceil(xstart);
		delta = xend-left;
		z = zend;
		dz = zstart-zend;
		u = uend;
		du = ustart-uend;
		v = vend;
		dv = vstart-vend;
		difred = difredend;
		ddifred = difredstart-difredend;
		difgreen = difgreenend;
		ddifgreen = difgreenstart-difgreenend;
		difblue = difblueend;
		ddifblue = difbluestart-difblueend;
		specred = specredend;
		dspecred = specredstart-specredend;
		specgreen = specgreenend;
		dspecgreen = specgreenstart-specgreenend;
		specblue = specblueend;
		dspecblue = specbluestart-specblueend;
	      }
	    if (left != right)
	      {
		if (xend == xstart)
		  denom = 1.0f;
		else if (xend > xstart)
		  denom = (float) (1.0/(xend-xstart));
		else
		  denom = (float) (1.0/(xstart-xend));
		dz *= denom;
		du *= denom;
		dv *= denom;
		ddifred *= denom;
		ddifgreen *= denom;
		ddifblue *= denom;
		dspecred *= denom;
		dspecgreen *= denom;
		dspecblue *= denom;
		if (left < 0)
		  {
		    delta += left;
		    left = 0;
		  }
		u -= du*delta;
		v -= dv*delta;
		z -= dz*delta;
		difred -= ddifred*delta;
		difgreen -= ddifgreen*delta;
		difblue -= ddifblue*delta;
		specred -= dspecred*delta;
		specgreen -= dspecgreen*delta;
		specblue -= dspecblue*delta;
		if (right > width)
		  right = width;
		repeat = false;
		for (i = left; i < right; i++)
		  {
		    zl = 1.0f/z;
		    if (zl < zbuffer[index+i] && zl > clip)
		      {
			if (repeat && (i%subsample != 0))
			  {
			    pixel[index+i] = pixel[index+i-1];
			    zbuffer[index+i] = zl;
			  }
			else if (repeat && (y%subsample != 0) && (i >= prevLeft) && (i < prevRight))
			  {
			    pixel[index+i] = pixel[index+i-width];
			    zbuffer[index+i] = zl;
			  }
			else
			  {
			    ul = u*zl;
			    vl = v*zl;
			    wl = 1.0-ul-vl;
			    tri.getTextureSpec(surfSpec, viewdot, ul, vl, wl, smoothScale*z, time);
			    tempColor[0].setRGB(surfSpec.diffuse.getRed()*difred + surfSpec.hilight.getRed()*specred + surfSpec.emissive.getRed(),
			      surfSpec.diffuse.getGreen()*difgreen + surfSpec.hilight.getGreen()*specgreen + surfSpec.emissive.getGreen(),
			      surfSpec.diffuse.getBlue()*difblue + surfSpec.hilight.getBlue()*specblue + surfSpec.emissive.getBlue());
			    pixel[index+i] = calcARGB(tempColor[0], 0.0);
			    zbuffer[index+i] = zl;
			  }
			repeat = doSubsample;
		      }
		    else
		      repeat = false;
		    z += dz;
		    u += du;
		    v += dv;
		    difred += ddifred;
		    difgreen += ddifgreen;
		    difblue += ddifblue;
		    specred += dspecred;
		    specgreen += dspecgreen;
		    specblue += dspecblue;
		  }
		prevLeft = left;
		prevRight = right;
	      }
	    xstart += mx1;
	    zstart += mz1;
	    ustart += mu1;
	    vstart += mv1;
	    difredstart += mdifred1;
	    difgreenstart += mdifgreen1;
	    difbluestart += mdifblue1;
	    specredstart += mspecred1;
	    specgreenstart += mspecgreen1;
	    specbluestart += mspecblue1;
	    xend += mx2;
	    zend += mz2;
	    uend += mu2;
	    vend += mv2;
	    difredend += mdifred2;
	    difgreenend += mdifgreen2;
	    difblueend += mdifblue2;
	    specredend += mspecred2;
	    specgreenend += mspecgreen2;
	    specblueend += mspecblue2;
	    index += width;
	    y++;
	  }
      }
  }

  /** Render a triangle mesh with hybrid Gouraud/Phong shading. */
    
  private void renderMeshHybrid(RenderingMesh mesh, Vec3 viewdir, boolean isClosed)
  {
    Vec3 vert[] = mesh.vert, norm[] = mesh.norm, clipNorm[] = new Vec3 [4];
    Vec2 pos[] = new Vec2 [vert.length];
    float z[] = new float [vert.length], clip = (float) theCamera.getClipDistance(), clipz[] = new float [4];
    double dot, clipu[] = new double [4], clipv[] = new double [4];
    double distToScreen = theCamera.getDistToScreen(), tol = smoothScale;
    RGBColor diffuse[] = new RGBColor [4];
    Mat4 toView = theCamera.getObjectToView(), toScreen = theCamera.getObjectToScreen();
    RenderingTriangle tri;
    int i, v1, v2, v3, n1, n2, n3;
    boolean hide = (hideBackfaces && isClosed), backface;
    
    for (i = 0; i < 4; i++)
      {
	diffuse[i] = new RGBColor();
	clipNorm[i] = new Vec3();
      }
    for (i = vert.length-1; i >= 0; i--)
      {
	pos[i] = toScreen.timesXY(vert[i]);
	z[i] = (float) toView.timesZ(vert[i]);
      }
    for (i = mesh.triangle.length-1; i >= 0; i--)
      {
	tri = mesh.triangle[i];
	v1 = tri.v1;
	v2 = tri.v2;
	v3 = tri.v3;
	n1 = tri.n1;
	n2 = tri.n2;
	n3 = tri.n3;
	if (z[v1] < clip && z[v2] < clip && z[v3] < clip)
	  continue;
	backface = ((pos[v2].x-pos[v1].x)*(pos[v3].y-pos[v1].y) - (pos[v2].y-pos[v1].y)*(pos[v3].x-pos[v1].x) > 0.0);
        double viewdot = viewdir.dot(mesh.faceNorm[i]);
	if (z[v1] < clip || z[v2] < clip || z[v3] < clip)
	  {
	    Vec3 clipPos[] = clipTriangle(vert[v1], vert[v2], vert[v3], z[v1], z[v2], z[v3], clipz, clipu, clipv);
	    Vec2 clipPos2D[] = new Vec2 [clipPos.length];
	    for (int j = clipPos.length-1; j >= 0; j--)
	      {
	        clipPos2D[j] = toScreen.timesXY(clipPos[j]);
	        double u = clipu[j], v = clipv[j], w = 1.0-u-v;
		tri.getTextureSpec(surfSpec, viewdot, u, v, 1.0-u-v, tol, time);
		clipNorm[j].set(norm[n1].x*u + norm[n2].x*v + norm[n3].x*w, norm[n1].y*u + norm[n2].y*v + norm[n3].y*w, norm[n1].z*u + norm[n2].z*v + norm[n3].z*w);
		clipNorm[j].normalize();
		calcLight(clipPos[j], tempVec[2], viewdir, mesh.faceNorm[i], surfSpec.roughness, diffuse[j], null);
	      }
	    renderTriangleHybrid(clipPos2D[0], clipz[0], clipPos[0], clipNorm[0], clipu[0], clipv[0], diffuse[0], 
	        clipPos2D[1], clipz[1], clipPos[1], clipNorm[1], clipu[1], clipv[1], diffuse[1], 
	        clipPos2D[2], clipz[2], clipPos[2], clipNorm[2], clipu[2], clipv[2], diffuse[2], 
	        tri, viewdir, mesh.faceNorm[i], clip, viewdot);
	    if (clipPos.length == 4)
	      renderTriangleHybrid(clipPos2D[1], clipz[1], clipPos[1], clipNorm[1], clipu[1], clipv[1], diffuse[1], 
	        clipPos2D[2], clipz[2], clipPos[2], clipNorm[2], clipu[2], clipv[2], diffuse[2], 
	        clipPos2D[3], clipz[3], clipPos[3], clipNorm[3], clipu[3], clipv[3], diffuse[3], 
	        tri, viewdir, mesh.faceNorm[i], clip, viewdot);
	  }
	else
	  {
            if (hide && backface)
              continue;
	    if (z[v1] > distToScreen)
	      tol = smoothScale*z[v1];
	    tri.getTextureSpec(surfSpec, viewdot, 1.0, 0.0, 0.0, tol, time);
	    calcLight(vert[v1], norm[n1], viewdir, mesh.faceNorm[i], surfSpec.roughness, diffuse[0], null);
	    if (z[v2] > distToScreen)
	      tol = smoothScale*z[v2];
	    tri.getTextureSpec(surfSpec, viewdot, 0.0, 1.0, 0.0, tol, time);
	    calcLight(vert[v2], norm[n2], viewdir, mesh.faceNorm[i], surfSpec.roughness, diffuse[1], null);
	    if (z[v3] > distToScreen)
	      tol = smoothScale*z[v3];
	    tri.getTextureSpec(surfSpec, viewdot, 0.0, 0.0, 1.0, tol, time);
	    calcLight(vert[v3], norm[n3], viewdir, mesh.faceNorm[i], surfSpec.roughness, diffuse[2], null);
	    renderTriangleHybrid(pos[v1], z[v1], vert[v1], norm[n1], 1.0, 0.0, diffuse[0], 
	        pos[v2], z[v2], vert[v2], norm[n2], 0.0, 1.0, diffuse[1], 
	        pos[v3], z[v3], vert[v3], norm[n3], 0.0, 0.0, diffuse[2], 
	        tri, viewdir, mesh.faceNorm[i], clip, viewdot);
	  }
      }
  }

  /** Render a triangle with hybrid Gouraud/Phong shading. */

  private void renderTriangleHybrid(Vec2 pos1, float zf1, Vec3 vert1, Vec3 normf1, double uf1, double vf1, RGBColor diffuse1, 
        Vec2 pos2, float zf2, Vec3 vert2, Vec3 normf2, double uf2, double vf2, RGBColor diffuse2,
        Vec2 pos3, float zf3, Vec3 vert3, Vec3 normf3, double uf3, double vf3, RGBColor diffuse3,
        RenderingTriangle tri, Vec3 viewdir, Vec3 faceNorm, double clip, double viewdot)
  {
    double x1, x2, x3, y1, y2, y3;
    double dx1, dx2, dy1, dy2, mx1, mx2;
    double xstart, xend, delta;
    float z1, z2, z3, dz1, dz2, mz1, mz2, zstart, zend, z, zl, dz;
    double u1, u2, u3, v1, v2, v3, du1, du2, dv1, dv2, mu1, mu2, mv1, mv2;
    double ustart, uend, vstart, vend, u, v, ul, vl, wl, du, dv;
    RGBColor dif1, dif2, dif3, specular = tempColor[1];
    Vec3 norm1, norm2, norm3;
    float ddifred1, ddifred2, ddifgreen1, ddifgreen2, ddifblue1, ddifblue2;
    float mdifred1, mdifred2, mdifgreen1, mdifgreen2, mdifblue1, mdifblue2;
    double dnormx1, dnormx2, dnormy1, dnormy2, dnormz1, dnormz2;
    double mnormx1, mnormx2, mnormy1, mnormy2, mnormz1, mnormz2;
    float difredstart, difredend, difgreenstart, difgreenend, difbluestart, difblueend;
    double normxstart, normxend, normystart, normyend, normzstart, normzend;
    float difred, difgreen, difblue, ddifred, ddifgreen, ddifblue;
    double normx, normy, normz, dnormx, dnormy, dnormz;
    float denom;
    double distToScreen = theCamera.getDistToScreen();
    int left, right, prevLeft = width, prevRight = -1, i, index, yend, y;
    boolean doSubsample = (subsample > 1), repeat;
    
    if (pos1.y <= pos2.y && pos1.y <= pos3.y)
      {
	x1 = pos1.x;
	y1 = pos1.y;
	z1 = zf1;
	u1 = uf1;
	v1 = vf1;
	dif1 = diffuse1;
	norm1 = normf1;
	if (pos2.y < pos3.y)
	  {
	    x2 = pos2.x;
	    y2 = pos2.y;
	    z2 = zf2;
	    u2 = uf2;
	    v2 = vf2;
	    dif2 = diffuse2;
	    norm2 = normf2;
	    x3 = pos3.x;
	    y3 = pos3.y;
	    z3 = zf3;
	    u3 = uf3;
	    v3 = vf3;
	    dif3 = diffuse3;
	    norm3 = normf3;
	  }
	else
	  {
	    x2 = pos3.x;
	    y2 = pos3.y;
	    z2 = zf3;
	    u2 = uf3;
	    v2 = vf3;
	    dif2 = diffuse3;
	    norm2 = normf3;
	    x3 = pos2.x;
	    y3 = pos2.y;
	    z3 = zf2;
	    u3 = uf2;
	    v3 = vf2;
	    dif3 = diffuse2;
	    norm3 = normf2;
	  }
      }
    else if (pos2.y <= pos1.y && pos2.y <= pos3.y)
      {
	x1 = pos2.x;
	y1 = pos2.y;
	z1 = zf2;
	u1 = uf2;
	v1 = vf2;
	dif1 = diffuse2;
	norm1 = normf2;
	if (pos1.y < pos3.y)
	  {
	    x2 = pos1.x;
	    y2 = pos1.y;
	    z2 = zf1;
	    u2 = uf1;
	    v2 = vf1;
	    dif2 = diffuse1;
	    norm2 = normf1;
	    x3 = pos3.x;
	    y3 = pos3.y;
	    z3 = zf3;
	    u3 = uf3;
	    v3 = vf3;
	    dif3 = diffuse3;
	    norm3 = normf3;
	  }
	else
	  {
	    x2 = pos3.x;
	    y2 = pos3.y;
	    z2 = zf3;
	    u2 = uf3;
	    v2 = vf3;
	    dif2 = diffuse3;
	    norm2 = normf3;
	    x3 = pos1.x;
	    y3 = pos1.y;
	    z3 = zf1;
	    u3 = uf1;
	    v3 = vf1;
	    dif3 = diffuse1;
	    norm3 = normf1;
	  }
      }
    else
      {
	x1 = pos3.x;
	y1 = pos3.y;
	z1 = zf3;
	u1 = uf3;
	v1 = vf3;
	dif1 = diffuse3;
	norm1 = normf3;
	if (pos1.y < pos2.y)
	  {
	    x2 = pos1.x;
	    y2 = pos1.y;
	    z2 = zf1;
	    u2 = uf1;
	    v2 = vf1;
	    dif2 = diffuse1;
	    norm2 = normf1;
	    x3 = pos2.x;
	    y3 = pos2.y;
	    z3 = zf2;
	    u3 = uf2;
	    v3 = vf2;
	    dif3 = diffuse2;
	    norm3 = normf2;
	  }
	else
	  {
	    x2 = pos2.x;
	    y2 = pos2.y;
	    z2 = zf2;
	    u2 = uf2;
	    v2 = vf2;
	    dif2 = diffuse2;
	    norm2 = normf2;
	    x3 = pos1.x;
	    y3 = pos1.y;
	    z3 = zf1;
	    u3 = uf1;
	    v3 = vf1;
	    dif3 = diffuse1;
	    norm3 = normf1;
	  }
      }
    z1 = 1.0f/z1;
    u1 *= z1;
    v1 *= z1;
    z2 = 1.0f/z2;
    u2 *= z2;
    v2 *= z2;
    z3 = 1.0f/z3;
    u3 *= z3;
    v3 *= z3;
    dx1 = x3-x1;
    dy1 = y3-y1;
    dz1 = z3-z1;
    if (dy1 == 0)
      return;
    du1 = u3-u1;
    dv1 = v3-v1;
    ddifred1 = dif3.getRed()-dif1.getRed();
    ddifgreen1 = dif3.getGreen()-dif1.getGreen();
    ddifblue1 = dif3.getBlue()-dif1.getBlue();
    dnormx1 = norm3.x-norm1.x;
    dnormy1 = norm3.y-norm1.y;
    dnormz1 = norm3.z-norm1.z;
    dx2 = x2-x1;
    dy2 = y2-y1;
    dz2 = z2-z1;
    du2 = u2-u1;
    dv2 = v2-v1;
    ddifred2 = dif2.getRed()-dif1.getRed();
    ddifgreen2 = dif2.getGreen()-dif1.getGreen();
    ddifblue2 = dif2.getBlue()-dif1.getBlue();
    dnormx2 = norm2.x-norm1.x;
    dnormy2 = norm2.y-norm1.y;
    dnormz2 = norm2.z-norm1.z;
    denom = (float) (1.0/dy1);
    mx1 = dx1*denom;
    mz1 = dz1*denom;
    mu1 = du1*denom;
    mv1 = dv1*denom;
    mdifred1 = ddifred1*denom;
    mdifgreen1 = ddifgreen1*denom;
    mdifblue1 = ddifblue1*denom;
    mnormx1 = dnormx1*denom;
    mnormy1 = dnormy1*denom;
    mnormz1 = dnormz1*denom;
    xstart = xend = x1;
    zstart = zend = z1;
    ustart = uend = u1;
    vstart = vend = v1;
    difredstart = difredend = dif1.getRed();
    difgreenstart = difgreenend = dif1.getGreen();
    difbluestart = difblueend = dif1.getBlue();
    normxstart = normxend = norm1.x;
    normystart = normyend = norm1.y;
    normzstart = normzend = norm1.z;
    y = round(y1);
    if (dy2 > 0.0)
      {
	denom = (float) (1.0/dy2);
	mx2 = dx2*denom;
	mz2 = dz2*denom;
	mu2 = du2*denom;
	mv2 = dv2*denom;
	mdifred2 = ddifred2*denom;
	mdifgreen2 = ddifgreen2*denom;
	mdifblue2 = ddifblue2*denom;
	mnormx2 = dnormx2*denom;
	mnormy2 = dnormy2*denom;
	mnormz2 = dnormz2*denom;
	if (y2 < 0)
	  {
	    xstart += mx1*dy2;
	    xend += mx2*dy2;
	    zstart += mz1*dy2;
	    zend += mz2*dy2;
	    ustart += mu1*dy2;
	    uend += mu2*dy2;
	    vstart += mv1*dy2;
	    vend += mv2*dy2;
	    difredstart += mdifred1*dy2;
	    difredend += mdifred2*dy2;
	    difgreenstart += mdifgreen1*dy2;
	    difgreenend += mdifgreen2*dy2;
	    difbluestart += mdifblue1*dy2;
	    difblueend += mdifblue2*dy2;
	    normxstart += mnormx1*dy2;
	    normxend += mnormx2*dy2;
	    normystart += mnormy1*dy2;
	    normyend += mnormy2*dy2;
	    normzstart += mnormz1*dy2;
	    normzend += mnormz2*dy2;
	    y = round(y2);
	  }
	else if (y < 0)
	  {
	    xstart -= mx1*y;
	    xend -= mx2*y;
	    zstart -= mz1*y;
	    zend -= mz2*y;
	    ustart -= mu1*y;
	    uend -= mu2*y;
	    vstart -= mv1*y;
	    vend -= mv2*y;
	    difredstart -= mdifred1*y;
	    difredend -= mdifred2*y;
	    difgreenstart -= mdifgreen1*y;
	    difgreenend -= mdifgreen2*y;
	    difbluestart -= mdifblue1*y;
	    difblueend -= mdifblue2*y;
	    normxstart -= mnormx1*y;
	    normxend -= mnormx2*y;
	    normystart -= mnormy1*y;
	    normyend -= mnormy2*y;
	    normzstart -= mnormz1*y;
	    normzend -= mnormz2*y;
	    y = 0;
	  }
	yend = round(y2);
	if (yend > height)
	  yend = height;
	index = y*width;
	while (y < yend)
	  {
	    if (xstart < xend)
	      {
		left = floor(xstart);
		right = ceil(xend);
		delta = xstart-left;
		z = zstart;
		dz = zend-zstart;
		u = ustart;
		du = uend-ustart;
		v = vstart;
		dv = vend-vstart;
		difred = difredstart;
		ddifred = difredend-difredstart;
		difgreen = difgreenstart;
		ddifgreen = difgreenend-difgreenstart;
		difblue = difbluestart;
		ddifblue = difblueend-difbluestart;
		normx = normxstart;
		dnormx = normxend-normxstart;
		normy = normystart;
		dnormy = normyend-normystart;
		normz = normzstart;
		dnormz = normzend-normzstart;
	      }
	    else
	      {
		left = floor(xend);
		right = ceil(xstart);
		delta = xend-left;
		z = zend;
		dz = zstart-zend;
		u = uend;
		du = ustart-uend;
		v = vend;
		dv = vstart-vend;
		difred = difredend;
		ddifred = difredstart-difredend;
		difgreen = difgreenend;
		ddifgreen = difgreenstart-difgreenend;
		difblue = difblueend;
		ddifblue = difbluestart-difblueend;
		normx = normxend;
		dnormx = normxstart-normxend;
		normy = normyend;
		dnormy = normystart-normyend;
		normz = normzend;
		dnormz = normzstart-normzend;
	      }
	    if (left != right)
	      {
		if (xend == xstart)
		  denom = 1.0f;
		else if (xend > xstart)
		  denom = (float) (1.0/(xend-xstart));
		else
		  denom = (float) (1.0/(xstart-xend));
		dz *= denom;
		du *= denom;
		dv *= denom;
		ddifred *= denom;
		ddifgreen *= denom;
		ddifblue *= denom;
		dnormx *= denom;
		dnormy *= denom;
		dnormz *= denom;
		if (left < 0)
		  {
		    delta += left;
		    left = 0;
		  }
		u -= du*delta;
		v -= dv*delta;
		z -= dz*delta;
		difred -= ddifred*delta;
		difgreen -= ddifgreen*delta;
		difblue -= ddifblue*delta;
		normx -= dnormx*delta;
		normy -= dnormy*delta;
		normz -= dnormz*delta;
		if (right > width)
		  right = width;
		repeat = false;
		for (i = left; i < right; i++)
		  {
		    zl = 1.0f/z;
		    if (zl < zbuffer[index+i] && zl > clip)
		      {
			if (repeat && (i%subsample != 0))
			  {
			    pixel[index+i] = pixel[index+i-1];
			    zbuffer[index+i] = zl;
			  }
			else if (repeat && (y%subsample != 0) && (i >= prevLeft) && (i < prevRight))
			  {
			    pixel[index+i] = pixel[index+i-width];
			    zbuffer[index+i] = zl;
			  }
			else
			  {
			    ul = u*zl;
			    vl = v*zl;
			    wl = 1.0-ul-vl;
			    tri.getTextureSpec(surfSpec, viewdot, ul, vl, wl, smoothScale*z, time);
			    if (surfSpec.hilight.getRed() == 0.0f && surfSpec.hilight.getGreen() == 0.0f && surfSpec.hilight.getBlue() == 0.0f)
			      tempColor[0].setRGB(surfSpec.diffuse.getRed()*difred + surfSpec.emissive.getRed(),
				surfSpec.diffuse.getGreen()*difgreen + surfSpec.emissive.getGreen(),
				surfSpec.diffuse.getBlue()*difblue + surfSpec.emissive.getBlue());
			    else
			      {
				if (positionNeeded)
				  tempVec[2].set(ul*vert1.x+vl*vert2.x+wl*vert3.x, ul*vert1.y+vl*vert2.y+wl*vert3.y, ul*vert1.z+vl*vert2.z+wl*vert3.z);
				tempVec[3].set(normx, normy, normz);
				tempVec[3].normalize();
				calcLight(tempVec[2], tempVec[3], viewdir, faceNorm, surfSpec.roughness, null, specular);
				tempColor[0].setRGB(surfSpec.diffuse.getRed()*difred + surfSpec.hilight.getRed()*specular.getRed() + surfSpec.emissive.getRed(),
				  surfSpec.diffuse.getGreen()*difgreen + surfSpec.hilight.getGreen()*specular.getGreen() + surfSpec.emissive.getGreen(),
				  surfSpec.diffuse.getBlue()*difblue + surfSpec.hilight.getBlue()*specular.getBlue() + surfSpec.emissive.getBlue());
			      }
			    pixel[index+i] = calcARGB(tempColor[0], 0.0);
			    zbuffer[index+i] = zl;
			  }
			repeat = doSubsample;
		      }
		    else
		      repeat = false;
		    z += dz;
		    u += du;
		    v += dv;
		    difred += ddifred;
		    difgreen += ddifgreen;
		    difblue += ddifblue;
		    normx += dnormx;
		    normy += dnormy;
		    normz += dnormz;
		  }
		prevLeft = left;
		prevRight = right;
	      }
	    xstart += mx1;
	    zstart += mz1;
	    ustart += mu1;
	    vstart += mv1;
	    difredstart += mdifred1;
	    difgreenstart += mdifgreen1;
	    difbluestart += mdifblue1;
	    normxstart += mnormx1;
	    normystart += mnormy1;
	    normzstart += mnormz1;
	    xend += mx2;
	    zend += mz2;
	    uend += mu2;
	    vend += mv2;
	    difredend += mdifred2;
	    difgreenend += mdifgreen2;
	    difblueend += mdifblue2;
	    normxend += mnormx2;
	    normyend += mnormy2;
	    normzend += mnormz2;
	    index += width;
	    y++;
	  }
      }
    dx2 = x3-x2;
    dy2 = y3-y2;
    dz2 = z3-z2;
    du2 = u3-u2;
    dv2 = v3-v2;
    ddifred2 = dif3.getRed()-dif2.getRed();
    ddifgreen2 = dif3.getGreen()-dif2.getGreen();
    ddifblue2 = dif3.getBlue()-dif2.getBlue();
    dnormx2 = norm3.x-norm2.x;
    dnormy2 = norm3.y-norm2.y;
    dnormz2 = norm3.z-norm2.z;
    if (dy2 > 0.0)
      {
	denom = (float) (1.0/dy2);
	mx2 = dx2*denom;
	mz2 = dz2*denom;
	mu2 = du2*denom;
	mv2 = dv2*denom;
	mdifred2 = ddifred2*denom;
	mdifgreen2 = ddifgreen2*denom;
	mdifblue2 = ddifblue2*denom;
	mnormx2 = dnormx2*denom;
	mnormy2 = dnormy2*denom;
	mnormz2 = dnormz2*denom;
	xend = x2;
	zend = z2;
	uend = u2;
	vend = v2;
	difredend = dif2.getRed();
	difgreenend = dif2.getGreen();
	difblueend = dif2.getBlue();
	normxend = norm2.x;
	normyend = norm2.y;
	normzend = norm2.z;
	if (y < 0)
	  {
	    xstart -= mx1*y;
	    xend -= mx2*y;
	    zstart -= mz1*y;
	    zend -= mz2*y;
	    ustart -= mu1*y;
	    uend -= mu2*y;
	    vstart -= mv1*y;
	    vend -= mv2*y;
	    difredstart -= mdifred1*y;
	    difredend -= mdifred2*y;
	    difgreenstart -= mdifgreen1*y;
	    difgreenend -= mdifgreen2*y;
	    difbluestart -= mdifblue1*y;
	    difblueend -= mdifblue2*y;
	    normxstart -= mnormx1*y;
	    normxend -= mnormx2*y;
	    normystart -= mnormy1*y;
	    normyend -= mnormy2*y;
	    normzstart -= mnormz1*y;
	    normzend -= mnormz2*y;
	    y = 0;
	  }
	yend = round(y3 < height ? y3 : height);
	index = y*width;
	while (y < yend)
	  {
	    if (xstart < xend)
	      {
		left = floor(xstart);
		right = ceil(xend);
		delta = xstart-left;
		z = zstart;
		dz = zend-zstart;
		u = ustart;
		du = uend-ustart;
		v = vstart;
		dv = vend-vstart;
		difred = difredstart;
		ddifred = difredend-difredstart;
		difgreen = difgreenstart;
		ddifgreen = difgreenend-difgreenstart;
		difblue = difbluestart;
		ddifblue = difblueend-difbluestart;
		normx = normxstart;
		dnormx = normxend-normxstart;
		normy = normystart;
		dnormy = normyend-normystart;
		normz = normzstart;
		dnormz = normzend-normzstart;
	      }
	    else
	      {
		left = floor(xend);
		right = ceil(xstart);
		delta = xend-left;
		z = zend;
		dz = zstart-zend;
		u = uend;
		du = ustart-uend;
		v = vend;
		dv = vstart-vend;
		difred = difredend;
		ddifred = difredstart-difredend;
		difgreen = difgreenend;
		ddifgreen = difgreenstart-difgreenend;
		difblue = difblueend;
		ddifblue = difbluestart-difblueend;
		normx = normxend;
		dnormx = normxstart-normxend;
		normy = normyend;
		dnormy = normystart-normyend;
		normz = normzend;
		dnormz = normzstart-normzend;
	      }
	    if (left != right)
	      {
		if (xend == xstart)
		  denom = 1.0f;
		else if (xend > xstart)
		  denom = (float) (1.0/(xend-xstart));
		else
		  denom = (float) (1.0/(xstart-xend));
		dz *= denom;
		du *= denom;
		dv *= denom;
		ddifred *= denom;
		ddifgreen *= denom;
		ddifblue *= denom;
		dnormx *= denom;
		dnormy *= denom;
		dnormz *= denom;
		if (left < 0)
		  {
		    delta += left;
		    left = 0;
		  }
		u -= du*delta;
		v -= dv*delta;
		z -= dz*delta;
		difred -= ddifred*delta;
		difgreen -= ddifgreen*delta;
		difblue -= ddifblue*delta;
		normx -= dnormx*delta;
		normy -= dnormy*delta;
		normz -= dnormz*delta;
		if (right > width)
		  right = width;
		repeat = false;
		for (i = left; i < right; i++)
		  {
		    zl = 1.0f/z;
		    if (zl < zbuffer[index+i] && zl > clip)
		      {
			if (repeat && (i%subsample != 0))
			  {
			    pixel[index+i] = pixel[index+i-1];
			    zbuffer[index+i] = zl;
			  }
			else if (repeat && (y%subsample != 0) && (i >= prevLeft) && (i < prevRight))
			  {
			    pixel[index+i] = pixel[index+i-width];
			    zbuffer[index+i] = zl;
			  }
			else
			  {
			    ul = u*zl;
			    vl = v*zl;
			    wl = 1.0-ul-vl;
			    tri.getTextureSpec(surfSpec, viewdot, ul, vl, wl, smoothScale*z, time);
			    if (surfSpec.hilight.getRed() == 0.0f && surfSpec.hilight.getGreen() == 0.0f && surfSpec.hilight.getBlue() == 0.0f)
			      tempColor[0].setRGB(surfSpec.diffuse.getRed()*difred + surfSpec.emissive.getRed(),
				surfSpec.diffuse.getGreen()*difgreen + surfSpec.emissive.getGreen(),
				surfSpec.diffuse.getBlue()*difblue + surfSpec.emissive.getBlue());
			    else
			      {
				if (positionNeeded)
				  tempVec[2].set(ul*vert1.x+vl*vert2.x+wl*vert3.x, ul*vert1.y+vl*vert2.y+wl*vert3.y, ul*vert1.z+vl*vert2.z+wl*vert3.z);
				tempVec[3].set(normx, normy, normz);
				tempVec[3].normalize();
				calcLight(tempVec[2], tempVec[3], viewdir, faceNorm, surfSpec.roughness, null, specular);
				tempColor[0].setRGB(surfSpec.diffuse.getRed()*difred + surfSpec.hilight.getRed()*specular.getRed() + surfSpec.emissive.getRed(),
				  surfSpec.diffuse.getGreen()*difgreen + surfSpec.hilight.getGreen()*specular.getGreen() + surfSpec.emissive.getGreen(),
				  surfSpec.diffuse.getBlue()*difblue + surfSpec.hilight.getBlue()*specular.getBlue() + surfSpec.emissive.getBlue());
			      }
			    pixel[index+i] = calcARGB(tempColor[0], 0.0);
			    zbuffer[index+i] = zl;
			  }
			repeat = doSubsample;
		      }
		    else
		      repeat = false;
		    z += dz;
		    u += du;
		    v += dv;
		    difred += ddifred;
		    difgreen += ddifgreen;
		    difblue += ddifblue;
		    normx += dnormx;
		    normy += dnormy;
		    normz += dnormz;
		  }
		prevLeft = left;
		prevRight = right;
	      }
	    xstart += mx1;
	    zstart += mz1;
	    ustart += mu1;
	    vstart += mv1;
	    difredstart += mdifred1;
	    difgreenstart += mdifgreen1;
	    difbluestart += mdifblue1;
	    normxstart += mnormx1;
	    normystart += mnormy1;
	    normzstart += mnormz1;
	    xend += mx2;
	    zend += mz2;
	    uend += mu2;
	    vend += mv2;
	    difredend += mdifred2;
	    difgreenend += mdifgreen2;
	    difblueend += mdifblue2;
	    normxend += mnormx2;
	    normyend += mnormy2;
	    normzend += mnormz2;
	    index += width;
	    y++;
	  }
      }
  }

  /** Render a triangle mesh with Phong shading. */
    
  private void renderMeshPhong(RenderingMesh mesh, Vec3 viewdir, boolean isClosed, boolean bumpMap)
  {
    Vec3 vert[] = mesh.vert, norm[] = mesh.norm, clipNorm[] = new Vec3 [4];
    Vec2 pos[] = new Vec2 [vert.length];
    float z[] = new float [vert.length], clip = (float) theCamera.getClipDistance(), clipz[] = new float [4];
    double dot, clipu[] = new double [4], clipv[] = new double [4];
    double distToScreen = theCamera.getDistToScreen(), tol = smoothScale;
    Mat4 toView = theCamera.getObjectToView(), toScreen = theCamera.getObjectToScreen();
    RenderingTriangle tri;
    int i, v1, v2, v3, n1, n2, n3;
    boolean hide = (hideBackfaces && isClosed), backface;
    
    for (i = 0; i < 4; i++)
      clipNorm[i] = new Vec3();
    for (i = vert.length-1; i >= 0; i--)
      {
	pos[i] = toScreen.timesXY(vert[i]);
	z[i] = (float) toView.timesZ(vert[i]);
      }
    for (i = mesh.triangle.length-1; i >= 0; i--)
      {
	tri = mesh.triangle[i];
	v1 = tri.v1;
	v2 = tri.v2;
	v3 = tri.v3;
	n1 = tri.n1;
	n2 = tri.n2;
	n3 = tri.n3;
	if (z[v1] < clip && z[v2] < clip && z[v3] < clip)
	  continue;
        backface = ((pos[v2].x-pos[v1].x)*(pos[v3].y-pos[v1].y) - (pos[v2].y-pos[v1].y)*(pos[v3].x-pos[v1].x) > 0.0);
	if (z[v1] < clip || z[v2] < clip || z[v3] < clip)
	  {
	    Vec3 clipPos[] = clipTriangle(vert[v1], vert[v2], vert[v3], z[v1], z[v2], z[v3], clipz, clipu, clipv);
	    Vec2 clipPos2D[] = new Vec2 [clipPos.length];
	    for (int j = clipPos.length-1; j >= 0; j--)
	      {
	        clipPos2D[j] = toScreen.timesXY(clipPos[j]);
	        double u = clipu[j], v = clipv[j], w = 1.0-u-v;
		clipNorm[j].set(norm[n1].x*u + norm[n2].x*v + norm[n3].x*w, norm[n1].y*u + norm[n2].y*v + norm[n3].y*w, norm[n1].z*u + norm[n2].z*v + norm[n3].z*w);
		clipNorm[j].normalize();
	      }
	    renderTrianglePhong(clipPos2D[0], clipz[0], clipPos[0], clipNorm[0], clipu[0], clipv[0], 
	        clipPos2D[1], clipz[1], clipPos[1], clipNorm[1], clipu[1], clipv[1], 
	        clipPos2D[2], clipz[2], clipPos[2], clipNorm[2], clipu[2], clipv[2], 
	        tri, viewdir, mesh.faceNorm[i], clip, bumpMap);
	    if (clipPos.length == 4)
	      renderTrianglePhong(clipPos2D[1], clipz[1], clipPos[1], clipNorm[1], clipu[1], clipv[1], 
	        clipPos2D[2], clipz[2], clipPos[2], clipNorm[2], clipu[2], clipv[2], 
	        clipPos2D[3], clipz[3], clipPos[3], clipNorm[3], clipu[3], clipv[3], 
	        tri, viewdir, mesh.faceNorm[i], clip, bumpMap);
	  }
	else
          {
            if (hide && backface)
              continue;
            renderTrianglePhong(pos[v1], z[v1], vert[v1], norm[n1], 1.0, 0.0, 
                pos[v2], z[v2], vert[v2], norm[n2], 0.0, 1.0, 
                pos[v3], z[v3], vert[v3], norm[n3], 0.0, 0.0, 
                tri, viewdir, mesh.faceNorm[i], clip, bumpMap);
          }
      }
  }

  /** Render a triangle with Phong shading. */

  private void renderTrianglePhong(Vec2 pos1, float zf1, Vec3 vert1, Vec3 normf1, double uf1, double vf1, 
        Vec2 pos2, float zf2, Vec3 vert2, Vec3 normf2, double uf2, double vf2,
        Vec2 pos3, float zf3, Vec3 vert3, Vec3 normf3, double uf3, double vf3,
        RenderingTriangle tri, Vec3 viewdir, Vec3 faceNorm, double clip, boolean bumpMap)
  {
    double x1, x2, x3, y1, y2, y3;
    double dx1, dx2, dy1, dy2, mx1, mx2;
    double xstart, xend, delta;
    float z1, z2, z3, dz1, dz2, mz1, mz2, zstart, zend, z, zl, dz;
    double u1, u2, u3, v1, v2, v3, du1, du2, dv1, dv2, mu1, mu2, mv1, mv2;
    double ustart, uend, vstart, vend, u, v, ul, vl, wl, du, dv;
    RGBColor diffuse = tempColor[1], specular = tempColor[2];
    Vec3 norm1, norm2, norm3, normal = tempVec[3];
    double dnormx1, dnormx2, dnormy1, dnormy2, dnormz1, dnormz2;
    double mnormx1, mnormx2, mnormy1, mnormy2, mnormz1, mnormz2;
    double normxstart, normxend, normystart, normyend, normzstart, normzend;
    double normx, normy, normz, dnormx, dnormy, dnormz;
    float denom;
    double distToScreen = theCamera.getDistToScreen();
    int left, right, prevLeft = width, prevRight = -1, i, index, yend, y;
    boolean doSubsample = (subsample > 1), repeat;
    
    if (pos1.y <= pos2.y && pos1.y <= pos3.y)
      {
	x1 = pos1.x;
	y1 = pos1.y;
	z1 = zf1;
	u1 = uf1;
	v1 = vf1;
	norm1 = normf1;
	if (pos2.y < pos3.y)
	  {
	    x2 = pos2.x;
	    y2 = pos2.y;
	    z2 = zf2;
	    u2 = uf2;
	    v2 = vf2;
	    norm2 = normf2;
	    x3 = pos3.x;
	    y3 = pos3.y;
	    z3 = zf3;
	    u3 = uf3;
	    v3 = vf3;
	    norm3 = normf3;
	  }
	else
	  {
	    x2 = pos3.x;
	    y2 = pos3.y;
	    z2 = zf3;
	    u2 = uf3;
	    v2 = vf3;
	    norm2 = normf3;
	    x3 = pos2.x;
	    y3 = pos2.y;
	    z3 = zf2;
	    u3 = uf2;
	    v3 = vf2;
	    norm3 = normf2;
	  }
      }
    else if (pos2.y <= pos1.y && pos2.y <= pos3.y)
      {
	x1 = pos2.x;
	y1 = pos2.y;
	z1 = zf2;
	u1 = uf2;
	v1 = vf2;
	norm1 = normf2;
	if (pos1.y < pos3.y)
	  {
	    x2 = pos1.x;
	    y2 = pos1.y;
	    z2 = zf1;
	    u2 = uf1;
	    v2 = vf1;
	    norm2 = normf1;
	    x3 = pos3.x;
	    y3 = pos3.y;
	    z3 = zf3;
	    u3 = uf3;
	    v3 = vf3;
	    norm3 = normf3;
	  }
	else
	  {
	    x2 = pos3.x;
	    y2 = pos3.y;
	    z2 = zf3;
	    u2 = uf3;
	    v2 = vf3;
	    norm2 = normf3;
	    x3 = pos1.x;
	    y3 = pos1.y;
	    z3 = zf1;
	    u3 = uf1;
	    v3 = vf1;
	    norm3 = normf1;
	  }
      }
    else
      {
	x1 = pos3.x;
	y1 = pos3.y;
	z1 = zf3;
	u1 = uf3;
	v1 = vf3;
	norm1 = normf3;
	if (pos1.y < pos2.y)
	  {
	    x2 = pos1.x;
	    y2 = pos1.y;
	    z2 = zf1;
	    u2 = uf1;
	    v2 = vf1;
	    norm2 = normf1;
	    x3 = pos2.x;
	    y3 = pos2.y;
	    z3 = zf2;
	    u3 = uf2;
	    v3 = vf2;
	    norm3 = normf2;
	  }
	else
	  {
	    x2 = pos2.x;
	    y2 = pos2.y;
	    z2 = zf2;
	    u2 = uf2;
	    v2 = vf2;
	    norm2 = normf2;
	    x3 = pos1.x;
	    y3 = pos1.y;
	    z3 = zf1;
	    u3 = uf1;
	    v3 = vf1;
	    norm3 = normf1;
	  }
      }
    z1 = 1.0f/z1;
    u1 *= z1;
    v1 *= z1;
    z2 = 1.0f/z2;
    u2 *= z2;
    v2 *= z2;
    z3 = 1.0f/z3;
    u3 *= z3;
    v3 *= z3;
    dx1 = x3-x1;
    dy1 = y3-y1;
    dz1 = z3-z1;
    if (dy1 == 0)
      return;
    du1 = u3-u1;
    dv1 = v3-v1;
    dnormx1 = norm3.x-norm1.x;
    dnormy1 = norm3.y-norm1.y;
    dnormz1 = norm3.z-norm1.z;
    dx2 = x2-x1;
    dy2 = y2-y1;
    dz2 = z2-z1;
    du2 = u2-u1;
    dv2 = v2-v1;
    dnormx2 = norm2.x-norm1.x;
    dnormy2 = norm2.y-norm1.y;
    dnormz2 = norm2.z-norm1.z;
    denom = (float) (1.0/dy1);
    mx1 = dx1*denom;
    mz1 = dz1*denom;
    mu1 = du1*denom;
    mv1 = dv1*denom;
    mnormx1 = dnormx1*denom;
    mnormy1 = dnormy1*denom;
    mnormz1 = dnormz1*denom;
    xstart = xend = x1;
    zstart = zend = z1;
    ustart = uend = u1;
    vstart = vend = v1;
    normxstart = normxend = norm1.x;
    normystart = normyend = norm1.y;
    normzstart = normzend = norm1.z;
    y = round(y1);
    if (dy2 > 0.0)
      {
	denom = (float) (1.0/dy2);
	mx2 = dx2*denom;
	mz2 = dz2*denom;
	mu2 = du2*denom;
	mv2 = dv2*denom;
	mnormx2 = dnormx2*denom;
	mnormy2 = dnormy2*denom;
	mnormz2 = dnormz2*denom;
	if (y2 < 0)
	  {
	    xstart += mx1*dy2;
	    xend += mx2*dy2;
	    zstart += mz1*dy2;
	    zend += mz2*dy2;
	    ustart += mu1*dy2;
	    uend += mu2*dy2;
	    vstart += mv1*dy2;
	    vend += mv2*dy2;
	    normxstart += mnormx1*dy2;
	    normxend += mnormx2*dy2;
	    normystart += mnormy1*dy2;
	    normyend += mnormy2*dy2;
	    normzstart += mnormz1*dy2;
	    normzend += mnormz2*dy2;
	    y = round(y2);
	  }
	else if (y < 0)
	  {
	    xstart -= mx1*y;
	    xend -= mx2*y;
	    zstart -= mz1*y;
	    zend -= mz2*y;
	    ustart -= mu1*y;
	    uend -= mu2*y;
	    vstart -= mv1*y;
	    vend -= mv2*y;
	    normxstart -= mnormx1*y;
	    normxend -= mnormx2*y;
	    normystart -= mnormy1*y;
	    normyend -= mnormy2*y;
	    normzstart -= mnormz1*y;
	    normzend -= mnormz2*y;
	    y = 0;
	  }
	yend = round(y2);
	if (yend > height)
	  yend = height;
	index = y*width;
	while (y < yend)
	  {
	    if (xstart < xend)
	      {
		left = floor(xstart);
		right = ceil(xend);
		delta = xstart-left;
		z = zstart;
		dz = zend-zstart;
		u = ustart;
		du = uend-ustart;
		v = vstart;
		dv = vend-vstart;
		normx = normxstart;
		dnormx = normxend-normxstart;
		normy = normystart;
		dnormy = normyend-normystart;
		normz = normzstart;
		dnormz = normzend-normzstart;
	      }
	    else
	      {
		left = floor(xend);
		right = ceil(xstart);
		delta = xend-left;
		z = zend;
		dz = zstart-zend;
		u = uend;
		du = ustart-uend;
		v = vend;
		dv = vstart-vend;
		normx = normxend;
		dnormx = normxstart-normxend;
		normy = normyend;
		dnormy = normystart-normyend;
		normz = normzend;
		dnormz = normzstart-normzend;
	      }
	    if (left != right)
	      {
		if (xend == xstart)
		  denom = 1.0f;
		else if (xend > xstart)
		  denom = (float) (1.0/(xend-xstart));
		else
		  denom = (float) (1.0/(xstart-xend));
		dz *= denom;
		du *= denom;
		dv *= denom;
		dnormx *= denom;
		dnormy *= denom;
		dnormz *= denom;
		if (left < 0)
		  {
		    delta += left;
		    left = 0;
		  }
		u -= du*delta;
		v -= dv*delta;
		z -= dz*delta;
		normx -= dnormx*delta;
		normy -= dnormy*delta;
		normz -= dnormz*delta;
		if (right > width)
		  right = width;
		repeat = false;
		for (i = left; i < right; i++)
		  {
		    zl = 1.0f/z;
		    if (zl < zbuffer[index+i] && zl > clip)
		      {
			if (repeat && (i%subsample != 0))
			  {
			    pixel[index+i] = pixel[index+i-1];
			    zbuffer[index+i] = zl;
			  }
			else if (repeat && (y%subsample != 0) && (i >= prevLeft) && (i < prevRight))
			  {
			    pixel[index+i] = pixel[index+i-width];
			    zbuffer[index+i] = zl;
			  }
			else
			  {
			    ul = u*zl;
			    vl = v*zl;
			    wl = 1.0-ul-vl;
			    if (positionNeeded)
			      tempVec[2].set(ul*vert1.x+vl*vert2.x+wl*vert3.x, ul*vert1.y+vl*vert2.y+wl*vert3.y, ul*vert1.z+vl*vert2.z+wl*vert3.z);
			    normal.set(normx, normy, normz);
			    normal.normalize();
			    tri.getTextureSpec(surfSpec, viewdir.dot(normal), ul, vl, wl, smoothScale*z, time);
			    if (bumpMap)
			      {
				normal.scale(surfSpec.bumpGrad.dot(normal)+1.0);
				normal.subtract(surfSpec.bumpGrad);
				normal.normalize();
			      }
			    if (surfSpec.hilight.getRed() == 0.0f && surfSpec.hilight.getGreen() == 0.0f && surfSpec.hilight.getBlue() == 0.0f)
			      {
				calcLight(tempVec[2], normal, viewdir, faceNorm, surfSpec.roughness, diffuse, null);
				tempColor[0].setRGB(surfSpec.diffuse.getRed()*diffuse.getRed() + surfSpec.emissive.getRed(),
				  surfSpec.diffuse.getGreen()*diffuse.getGreen() + surfSpec.emissive.getGreen(),
				  surfSpec.diffuse.getBlue()*diffuse.getBlue() + surfSpec.emissive.getBlue());
			      }
			    else
			      {
				calcLight(tempVec[2], normal, viewdir, faceNorm, surfSpec.roughness, diffuse, specular);
				tempColor[0].setRGB(surfSpec.diffuse.getRed()*diffuse.getRed() + surfSpec.hilight.getRed()*specular.getRed() + surfSpec.emissive.getRed(),
				  surfSpec.diffuse.getGreen()*diffuse.getGreen() + surfSpec.hilight.getGreen()*specular.getGreen() + surfSpec.emissive.getGreen(),
				  surfSpec.diffuse.getBlue()*diffuse.getBlue() + surfSpec.hilight.getBlue()*specular.getBlue() + surfSpec.emissive.getBlue());
			      }
			    pixel[index+i] = calcARGB(tempColor[0], 0.0);
			    zbuffer[index+i] = zl;
			  }
			repeat = doSubsample;
		      }
		    else
		      repeat = false;
		    z += dz;
		    u += du;
		    v += dv;
		    normx += dnormx;
		    normy += dnormy;
		    normz += dnormz;
		  }
		prevLeft = left;
		prevRight = right;
	      }
	    xstart += mx1;
	    zstart += mz1;
	    ustart += mu1;
	    vstart += mv1;
	    normxstart += mnormx1;
	    normystart += mnormy1;
	    normzstart += mnormz1;
	    xend += mx2;
	    zend += mz2;
	    uend += mu2;
	    vend += mv2;
	    normxend += mnormx2;
	    normyend += mnormy2;
	    normzend += mnormz2;
	    index += width;
	    y++;
	  }
      }
    dx2 = x3-x2;
    dy2 = y3-y2;
    dz2 = z3-z2;
    du2 = u3-u2;
    dv2 = v3-v2;
    dnormx2 = norm3.x-norm2.x;
    dnormy2 = norm3.y-norm2.y;
    dnormz2 = norm3.z-norm2.z;
    if (dy2 > 0.0)
      {
	denom = (float) (1.0/dy2);
	mx2 = dx2*denom;
	mz2 = dz2*denom;
	mu2 = du2*denom;
	mv2 = dv2*denom;
	mnormx2 = dnormx2*denom;
	mnormy2 = dnormy2*denom;
	mnormz2 = dnormz2*denom;
	xend = x2;
	zend = z2;
	uend = u2;
	vend = v2;
	normxend = norm2.x;
	normyend = norm2.y;
	normzend = norm2.z;
	if (y < 0)
	  {
	    xstart -= mx1*y;
	    xend -= mx2*y;
	    zstart -= mz1*y;
	    zend -= mz2*y;
	    ustart -= mu1*y;
	    uend -= mu2*y;
	    vstart -= mv1*y;
	    vend -= mv2*y;
	    normxstart -= mnormx1*y;
	    normxend -= mnormx2*y;
	    normystart -= mnormy1*y;
	    normyend -= mnormy2*y;
	    normzstart -= mnormz1*y;
	    normzend -= mnormz2*y;
	    y = 0;
	  }
	yend = round(y3 < height ? y3 : height);
	index = y*width;
	while (y < yend)
	  {
	    if (xstart < xend)
	      {
		left = floor(xstart);
		right = ceil(xend);
		delta = xstart-left;
		z = zstart;
		dz = zend-zstart;
		u = ustart;
		du = uend-ustart;
		v = vstart;
		dv = vend-vstart;
		normx = normxstart;
		dnormx = normxend-normxstart;
		normy = normystart;
		dnormy = normyend-normystart;
		normz = normzstart;
		dnormz = normzend-normzstart;
	      }
	    else
	      {
		left = floor(xend);
		right = ceil(xstart);
		delta = xend-left;
		z = zend;
		dz = zstart-zend;
		u = uend;
		du = ustart-uend;
		v = vend;
		dv = vstart-vend;
		normx = normxend;
		dnormx = normxstart-normxend;
		normy = normyend;
		dnormy = normystart-normyend;
		normz = normzend;
		dnormz = normzstart-normzend;
	      }
	    if (left != right)
	      {
		if (xend == xstart)
		  denom = 1.0f;
		else if (xend > xstart)
		  denom = (float) (1.0/(xend-xstart));
		else
		  denom = (float) (1.0/(xstart-xend));
		dz *= denom;
		du *= denom;
		dv *= denom;
		dnormx *= denom;
		dnormy *= denom;
		dnormz *= denom;
		if (left < 0)
		  {
		    delta += left;
		    left = 0;
		  }
		u -= du*delta;
		v -= dv*delta;
		z -= dz*delta;
		normx -= dnormx*delta;
		normy -= dnormy*delta;
		normz -= dnormz*delta;
		if (right > width)
		  right = width;
		repeat = false;
		for (i = left; i < right; i++)
		  {
		    zl = 1.0f/z;
		    if (zl < zbuffer[index+i] && zl > clip)
		      {
			if (repeat && (i%subsample != 0))
			  {
			    pixel[index+i] = pixel[index+i-1];
			    zbuffer[index+i] = zl;
			  }
			else if (repeat && (y%subsample != 0) && (i >= prevLeft) && (i < prevRight))
			  {
			    pixel[index+i] = pixel[index+i-width];
			    zbuffer[index+i] = zl;
			  }
			else
			  {
			    ul = u*zl;
			    vl = v*zl;
			    wl = 1.0-ul-vl;
			    if (positionNeeded)
			      tempVec[2].set(ul*vert1.x+vl*vert2.x+wl*vert3.x, ul*vert1.y+vl*vert2.y+wl*vert3.y, ul*vert1.z+vl*vert2.z+wl*vert3.z);
			    normal.set(normx, normy, normz);
			    normal.normalize();
			    tri.getTextureSpec(surfSpec, viewdir.dot(normal), ul, vl, wl, smoothScale*z, time);
			    if (bumpMap)
			      {
				normal.scale(surfSpec.bumpGrad.dot(normal)+1.0);
				normal.subtract(surfSpec.bumpGrad);
				normal.normalize();
			      }
			    if (surfSpec.hilight.getRed() == 0.0f && surfSpec.hilight.getGreen() == 0.0f && surfSpec.hilight.getBlue() == 0.0f)
			      {
				calcLight(tempVec[2], normal, viewdir, faceNorm, surfSpec.roughness, diffuse, null);
				tempColor[0].setRGB(surfSpec.diffuse.getRed()*diffuse.getRed() + surfSpec.emissive.getRed(),
				  surfSpec.diffuse.getGreen()*diffuse.getGreen() + surfSpec.emissive.getGreen(),
				  surfSpec.diffuse.getBlue()*diffuse.getBlue() + surfSpec.emissive.getBlue());
			      }
			    else
			      {
				calcLight(tempVec[2], normal, viewdir, faceNorm, surfSpec.roughness, diffuse, specular);
				tempColor[0].setRGB(surfSpec.diffuse.getRed()*diffuse.getRed() + surfSpec.hilight.getRed()*specular.getRed() + surfSpec.emissive.getRed(),
				  surfSpec.diffuse.getGreen()*diffuse.getGreen() + surfSpec.hilight.getGreen()*specular.getGreen() + surfSpec.emissive.getGreen(),
				  surfSpec.diffuse.getBlue()*diffuse.getBlue() + surfSpec.hilight.getBlue()*specular.getBlue() + surfSpec.emissive.getBlue());
			      }
			    pixel[index+i] = calcARGB(tempColor[0], 0.0);
			    zbuffer[index+i] = zl;
			  }
			repeat = doSubsample;
		      }
		    else
		      repeat = false;
		    z += dz;
		    u += du;
		    v += dv;
		    normx += dnormx;
		    normy += dnormy;
		    normz += dnormz;
		  }
		prevLeft = left;
		prevRight = right;
	      }
	    xstart += mx1;
	    zstart += mz1;
	    ustart += mu1;
	    vstart += mv1;
	    normxstart += mnormx1;
	    normystart += mnormy1;
	    normzstart += mnormz1;
	    xend += mx2;
	    zend += mz2;
	    uend += mu2;
	    vend += mv2;
	    normxend += mnormx2;
	    normyend += mnormy2;
	    normzend += mnormz2;
	    index += width;
	    y++;
	  }
      }
  }

  /** Render a displacement mapped triangle mesh by recursively subdividing the triangles
     until they are sufficiently small. */
    
  private void renderMeshDisplaced(RenderingMesh mesh, Vec3 viewdir, double tol, boolean isClosed, boolean bumpMap)
  {
    Vec3 vert[] = mesh.vert, norm[] = mesh.norm;
    Mat4 toView = theCamera.getObjectToView(), toScreen = theCamera.getObjectToScreen();
    int shading = (bumpMap ? PHONG : shadingMode);
    int v1, v2, v3, n1, n2, n3;
    double dist1, dist2, dist3;
    RenderingTriangle tri;
    
    for (int i = mesh.triangle.length-1; i >= 0; i--)
      {
	tri = mesh.triangle[i];
	v1 = tri.v1;
	v2 = tri.v2;
	v3 = tri.v3;
	n1 = tri.n1;
	n2 = tri.n2;
	n3 = tri.n3;
	dist1 = vert[v1].distance(vert[v2]);
	dist2 = vert[v2].distance(vert[v3]);
	dist3 = vert[v3].distance(vert[v1]);

        // Calculate the gradient vectors for u and v.
        
        tempVec[0].set(vert[v1].x-vert[v3].x, vert[v1].y-vert[v3].y, vert[v1].z-vert[v3].z);
        tempVec[1].set(vert[v3].x-vert[v2].x, vert[v3].y-vert[v2].y, vert[v3].z-vert[v2].z);
        Vec3 vgrad = tempVec[0].cross(mesh.faceNorm[i]);
        Vec3 ugrad = tempVec[1].cross(mesh.faceNorm[i]);
        vgrad.scale(-1.0/vgrad.dot(tempVec[1]));
        ugrad.scale(1.0/ugrad.dot(tempVec[0]));
        DisplacedVertex dv1 = new DisplacedVertex(tri, vert[v1], norm[n1], 1.0, 0.0, toView, toScreen);
        DisplacedVertex dv2 = new DisplacedVertex(tri, vert[v2], norm[n2], 0.0, 1.0, toView, toScreen);
        DisplacedVertex dv3 = new DisplacedVertex(tri, vert[v3], norm[n3], 0.0, 0.0, toView, toScreen);
	renderDisplacedTriangle(tri, dv1, dist1, dv2, dist2, dv3, dist3, viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
      }
  }
  
  /** Render a displacement mapeed triangle by recursively subdividing it. */
  
  private void renderDisplacedTriangle(RenderingTriangle tri, DisplacedVertex dv1, 
      double dist1, DisplacedVertex dv2, double dist2, DisplacedVertex dv3, double dist3,
      Vec3 viewdir, Vec3 ugrad, Vec3 vgrad, double tol, boolean isClosed, boolean bumpMap)
  {
    Mat4 toView = theCamera.getObjectToView(), toScreen = theCamera.getObjectToScreen();
    DisplacedVertex midv1 = null, midv2 = null, midv3 = null;
    double halfdist1 = 0, halfdist2 = 0, halfdist3 = 0;
    boolean split1 = dist1 > tol, split2 = dist2 > tol, split3 = dist3 > tol;
    int shading = (bumpMap ? PHONG : shadingMode), count = 0;

    if (split1)
      {
	midv1 = new DisplacedVertex(tri, new Vec3(0.5*(dv1.vert.x+dv2.vert.x), 0.5*(dv1.vert.y+dv2.vert.y), 0.5*(dv1.vert.z+dv2.vert.z)),
	  new Vec3(0.5*(dv1.norm.x+dv2.norm.x), 0.5*(dv1.norm.y+dv2.norm.y), 0.5*(dv1.norm.z+dv2.norm.z)), 
	  0.5*(dv1.u+dv2.u), 0.5*(dv1.v+dv2.v), toView, toScreen);
	halfdist1 = 0.5*dist1;
	count++;
      }
    if (split2)
      {
	midv2 = new DisplacedVertex(tri, new Vec3(0.5*(dv2.vert.x+dv3.vert.x), 0.5*(dv2.vert.y+dv3.vert.y), 0.5*(dv2.vert.z+dv3.vert.z)),
	  new Vec3(0.5*(dv2.norm.x+dv3.norm.x), 0.5*(dv2.norm.y+dv3.norm.y), 0.5*(dv2.norm.z+dv3.norm.z)), 
	  0.5*(dv2.u+dv3.u), 0.5*(dv2.v+dv3.v), toView, toScreen);
	halfdist2 = 0.5*dist2;
	count++;
      }
    if (split3)
      {
	midv3 = new DisplacedVertex(tri, new Vec3(0.5*(dv3.vert.x+dv1.vert.x), 0.5*(dv3.vert.y+dv1.vert.y), 0.5*(dv3.vert.z+dv1.vert.z)),
	  new Vec3(0.5*(dv3.norm.x+dv1.norm.x), 0.5*(dv3.norm.y+dv1.norm.y), 0.5*(dv3.norm.z+dv1.norm.z)), 
	  0.5*(dv3.u+dv1.u), 0.5*(dv3.v+dv1.v), toView, toScreen);
	halfdist3 = 0.5*dist3;
	count++;
      }
   
    // If any side is still too large, subdivide the triangle further.

    if (count == 1)
      {
	// Split it into two triangles.
	
	if (split1)
	  {
	    double d = dv3.vert.distance(midv1.vert);
	    renderDisplacedTriangle(tri, dv1, halfdist1, midv1, d, dv3, dist3, 
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	    renderDisplacedTriangle(tri, midv1, halfdist1, dv2, dist2, dv3, d, 
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	  }
	else if (split2)
	  {
	    double d = dv1.vert.distance(midv2.vert);
	    renderDisplacedTriangle(tri, dv2, halfdist2, midv2, d, dv1, dist1, 
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	    renderDisplacedTriangle(tri, midv2, halfdist2, dv3, dist3, dv1, d, 
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	  }
	else
	  {
	    double d = dv1.vert.distance(midv3.vert);
	    renderDisplacedTriangle(tri, dv3, halfdist3, midv3, d, dv2, dist2, 
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	    renderDisplacedTriangle(tri, midv3, halfdist3, dv1, dist1, dv2, d, 
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	  }
	return;
      }
    if (count == 2)
      {
	// Split it into three triangles.
	
	if (!split1)
	  {
	    double d1 = midv2.vert.distance(dv1.vert), d2 = midv2.vert.distance(midv3.vert);
	    renderDisplacedTriangle(tri, dv1, dist1, dv2, halfdist2, midv2, d1,
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	    renderDisplacedTriangle(tri, dv1, d1, midv2, d2, midv3, halfdist3,
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	    renderDisplacedTriangle(tri, dv3, halfdist3, midv3, d2, midv2, halfdist2,
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	  }
	else if (!split2)
	  {
	    double d1 = midv3.vert.distance(dv2.vert), d2 = midv3.vert.distance(midv1.vert);
	    renderDisplacedTriangle(tri, dv2, dist2, dv3, halfdist3, midv3, d1,
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	    renderDisplacedTriangle(tri, dv2, d1, midv3, d2, midv1, halfdist1,
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	    renderDisplacedTriangle(tri, dv1, halfdist1, midv1, d2, midv3, halfdist3,
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	  }
	else
	  {
	    double d1 = midv1.vert.distance(dv3.vert), d2 = midv1.vert.distance(midv2.vert);
	    renderDisplacedTriangle(tri, dv3, dist3, dv1, halfdist1, midv1, d1,
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	    renderDisplacedTriangle(tri, dv3, d1, midv1, d2, midv2, halfdist2,
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	    renderDisplacedTriangle(tri, dv2, halfdist2, midv2, d2, midv1, halfdist1,
		viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	  }
	return;
      }
    if (count == 3)
      {
	// Split it into four triangles.
	
	double d1 = midv1.vert.distance(midv2.vert), d2 = midv2.vert.distance(midv3.vert), d3 = midv3.vert.distance(midv1.vert);
	renderDisplacedTriangle(tri, dv1, halfdist1, midv1, d3, midv3, halfdist3,
	    viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	renderDisplacedTriangle(tri, dv2, halfdist2, midv2, d1, midv1, halfdist1,
	    viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	renderDisplacedTriangle(tri, dv3, halfdist3, midv3, d2, midv2, halfdist2,
	    viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	renderDisplacedTriangle(tri, midv1, d1, midv2, d2, midv3, d3,
	    viewdir, ugrad, vgrad, tol, isClosed, bumpMap);
	return;
      }
    
    // The triangle is small enough that it does not need to be split any more, so render it.
    
    float clip = (float) theCamera.getClipDistance();
    if (dv1.z < clip && dv2.z < clip && dv3.z < clip)
      return;
    if (dv1.z <= 0.0f || dv2.z < 0.0f || dv3.z < 0.0f)
      return;
    boolean backface = ((dv2.pos.x-dv1.pos.x)*(dv3.pos.y-dv1.pos.y) - (dv2.pos.y-dv1.pos.y)*(dv3.pos.x-dv1.pos.x) > 0.0);
    if (hideBackfaces && isClosed && backface)
      return;
    if (dv1.dispnorm == null)
      dv1.prepareToRender(tri, viewdir, ugrad, vgrad, shading);
    if (dv2.dispnorm == null)
      dv2.prepareToRender(tri, viewdir, ugrad, vgrad, shading);
    if (dv3.dispnorm == null)
      dv3.prepareToRender(tri, viewdir, ugrad, vgrad, shading);
    Vec3 closestNorm = null;
    if (dv1.z < dv2.z && dv1.z < dv3.z)
      closestNorm = dv1.dispnorm;
    else if (dv2.z < dv1.z && dv2.z < dv3.z)
      closestNorm = dv2.dispnorm;
    else
      closestNorm = dv3.dispnorm;
    if (shading == GOURAUD)
      renderTriangleGouraud(dv1.pos, dv1.z, dv1.u, dv1.v, dv1.diffuse, dv1.specular, 
	dv2.pos, dv2.z, dv2.u, dv2.v, dv2.diffuse, dv2.specular, 
	dv3.pos, dv3.z, dv3.u, dv3.v, dv3.diffuse, dv3.specular, 
	tri, (float) theCamera.getClipDistance(), viewdir.dot(closestNorm));
    else if (shading == HYBRID)
      renderTriangleHybrid(dv1.pos, dv1.z, dv1.dispvert, dv1.dispnorm, dv1.u, dv1.v, dv1.diffuse, 
	dv2.pos, dv2.z, dv2.dispvert, dv2.dispnorm, dv2.u, dv2.v, dv2.diffuse, 
	dv3.pos, dv3.z, dv3.dispvert, dv3.dispnorm, dv3.u, dv3.v, dv3.diffuse, 
	tri, viewdir, closestNorm, (float) theCamera.getClipDistance(), viewdir.dot(closestNorm));
    else
      renderTrianglePhong(dv1.pos, dv1.z, dv1.dispvert, dv1.dispnorm, dv1.u, dv1.v,
	dv2.pos, dv2.z, dv2.dispvert, dv2.dispnorm, dv2.u, dv2.v,
	dv3.pos, dv3.z, dv3.dispvert, dv3.dispnorm, dv3.u, dv3.v,
	tri, viewdir, closestNorm, (float) theCamera.getClipDistance(), bumpMap);
  }
  
  /** This is an inner class for keeping track of information about vertices when 
     doing displacement mapping. */
  
  private class DisplacedVertex
  {
    public Vec3 vert, norm, dispvert, dispnorm;
    public Vec2 pos;
    public double u, v, disp, tol;
    public float z, basez;
    public RGBColor diffuse, specular;
    
    public DisplacedVertex(RenderingTriangle tri, Vec3 vert, Vec3 norm, double u, double v, 
	Mat4 toView, Mat4 toScreen)
    {
      this.vert = vert;
      this.norm = norm;
      this.u = u;
      this.v = v;
      basez = (float) toView.timesZ(vert);
      tol = (basez > theCamera.getDistToScreen()) ? smoothScale*basez : smoothScale;
      disp = tri.getDisplacement(u, v, 1.0-u-v, tol, 0.0);
      dispvert = new Vec3(vert.x+disp*norm.x, vert.y+disp*norm.y, vert.z+disp*norm.z);
      z = (float) toView.timesZ(dispvert);
      pos = toScreen.timesXY(dispvert);
    }
    
    /** Calculate all the properties which are necessary for rendering this point. */
    
    public final void prepareToRender(RenderingTriangle tri, Vec3 viewdir, Vec3 ugrad, Vec3 vgrad, int shading)
    {
      // Find the derivatives of the displacement map, and use them to find the 
      // local normal vector.

      double w = 1.0-u-v;
      double dhdu = (tri.getDisplacement(u+(1e-5), v, w-(1e-5), tol, 0.0)-disp)*1e5;
      double dhdv = (tri.getDisplacement(u, v+(1e-5), w-(1e-5), tol, 0.0)-disp)*1e5;
      dispnorm = new Vec3(norm);
      tempVec[0].set(dhdu*ugrad.x+dhdv*vgrad.x, dhdu*ugrad.y+dhdv*vgrad.y, dhdu*ugrad.z+dhdv*vgrad.z);
      dispnorm.scale(tempVec[0].dot(dispnorm)+1.0);
      dispnorm.subtract(tempVec[0]);
      dispnorm.normalize();

      // Find the screen position and lighting.
      
      tol = (z > theCamera.getDistToScreen()) ? smoothScale*z : smoothScale;
      if (shading == GOURAUD)
	specular = new RGBColor();
      if (shading != PHONG)
	{
	  diffuse = new RGBColor();
	  tri.getTextureSpec(surfSpec, viewdir.dot(dispnorm), u, v, w, tol, time);
	  calcLight(dispvert, dispnorm, viewdir, dispnorm, surfSpec.roughness, diffuse, specular);
	}
    }
  }
}