Package com.eteks.sweethome3d.j3d

Source Code of com.eteks.sweethome3d.j3d.PhotoRenderer$Triangle

/*
* PhotoRenderer.java 22 janv. 2009
*
* Sweet Home 3D, Copyright (c) 2009 Emmanuel PUYBARET / eTeks <info@eteks.com>
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
package com.eteks.sweethome3d.j3d;

import java.awt.AlphaComposite;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import javax.imageio.ImageIO;
import javax.media.j3d.Appearance;
import javax.media.j3d.ColoringAttributes;
import javax.media.j3d.Geometry;
import javax.media.j3d.GeometryArray;
import javax.media.j3d.GeometryStripArray;
import javax.media.j3d.Group;
import javax.media.j3d.ImageComponent2D;
import javax.media.j3d.IndexedGeometryArray;
import javax.media.j3d.IndexedGeometryStripArray;
import javax.media.j3d.IndexedLineArray;
import javax.media.j3d.IndexedLineStripArray;
import javax.media.j3d.IndexedQuadArray;
import javax.media.j3d.IndexedTriangleArray;
import javax.media.j3d.IndexedTriangleFanArray;
import javax.media.j3d.IndexedTriangleStripArray;
import javax.media.j3d.LineArray;
import javax.media.j3d.LineStripArray;
import javax.media.j3d.Link;
import javax.media.j3d.Material;
import javax.media.j3d.Node;
import javax.media.j3d.QuadArray;
import javax.media.j3d.RenderingAttributes;
import javax.media.j3d.Shape3D;
import javax.media.j3d.TexCoordGeneration;
import javax.media.j3d.Texture;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TransparencyAttributes;
import javax.media.j3d.TriangleArray;
import javax.media.j3d.TriangleFanArray;
import javax.media.j3d.TriangleStripArray;
import javax.vecmath.Color3f;
import javax.vecmath.Point3f;
import javax.vecmath.TexCoord2f;
import javax.vecmath.Vector3f;
import javax.vecmath.Vector4f;

import org.sunflow.PluginRegistry;
import org.sunflow.SunflowAPI;
import org.sunflow.core.Display;
import org.sunflow.core.Instance;
import org.sunflow.core.ParameterList;
import org.sunflow.core.ParameterList.InterpolationType;
import org.sunflow.core.light.SphereLight;
import org.sunflow.core.light.SunSkyLight;
import org.sunflow.core.primitive.TriangleMesh;
import org.sunflow.image.Color;
import org.sunflow.math.Matrix4;
import org.sunflow.math.Point3;
import org.sunflow.math.Vector3;
import org.sunflow.system.UI;
import org.sunflow.system.ui.SilentInterface;

import com.eteks.sweethome3d.model.Camera;
import com.eteks.sweethome3d.model.Compass;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.HomeFurnitureGroup;
import com.eteks.sweethome3d.model.HomeLight;
import com.eteks.sweethome3d.model.HomePieceOfFurniture;
import com.eteks.sweethome3d.model.HomeTexture;
import com.eteks.sweethome3d.model.LightSource;
import com.eteks.sweethome3d.model.ObserverCamera;
import com.eteks.sweethome3d.model.Room;
import com.eteks.sweethome3d.model.Selectable;
import com.eteks.sweethome3d.model.Wall;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.viewcontroller.Object3DFactory;

/**
* A renderer able to create a photo realistic image of a home.
* @author Emmanuel Puybaret
* @author Fr�d�ric Mantegazza (Sun location algorithm)
*/
public class PhotoRenderer {
  public enum Quality {LOW, HIGH}
 
  private final Quality quality;
  private final Compass compass;
  private final int homeLightColor;
 
  private final SunflowAPI sunflow;
  private boolean useSunSky;
  private boolean useAmbientOcclusion;
  private String sunSkyLightName;
  private String sunLightName;
  private final Map<TransparentTextureKey, String> textureImagesCache = new HashMap<TransparentTextureKey, String>();
  private Thread renderingThread;

  static {
    // Ignore logs
    UI.set(new SilentInterface());
    // Use small triangles for better rendering
    TriangleMesh.setSmallTriangles(true);
    PluginRegistry.lightSourcePlugins.registerPlugin("sphere", SphereLightWithNoRepresentation.class);
  }

  /**
   * Creates an instance ready to render the scene matching the given <code>home</code>.
   * @throws IOException if texture image files required in the scene couldn't be created.
   */
  public PhotoRenderer(Home home, Quality quality) throws IOException {
    this(home,
        new Object3DBranchFactory() {
          public Object createObject3D(Home home, Selectable item, boolean waitForLoading) {
            if (item instanceof Room) {
              // Never display ceiling whit top camera
              return new Room3D((Room)item, home, home.getCamera() == home.getTopCamera(), true, waitForLoading);
            } else {
              return super.createObject3D(home, item, waitForLoading);
           
          }
        }, quality);
  }
 
  /**
   * Creates an instance ready to render the scene matching the given <code>home</code>.
   * @param home the home to render
   * @param object3DFactory a factory able to create 3D objects from <code>home</code> items.
   *            The {@link Object3DFactory#createObject3D(Home, Selectable, boolean) createObject3D} of
   *            this factory is expected to return an instance of {@link Node} in current implementation.
   * @throws IOException if texture image files required in the scene couldn't be created.
   */
  public PhotoRenderer(Home home,
                       Object3DFactory object3DFactory,
                       Quality quality) throws IOException {
    this.compass = home.getCompass();
    this.quality = quality;
    this.sunflow = new SunflowAPI();
   
    this.useAmbientOcclusion = home.getCamera() instanceof ObserverCamera;
    // SunFlow produce too much white spots when silk shader is used with sun sky
    // so use this shader only when observer is used
    boolean silk = this.useAmbientOcclusion && quality == Quality.HIGH;
   
    // Export to SunFlow the Java 3D shapes and appearance of the ground, the walls, the furniture and the rooms          
    for (Wall wall : home.getWalls()) {
      exportNode((Node)object3DFactory.createObject3D(home, wall, true), true, false, silk);
    }
    for (HomePieceOfFurniture piece : home.getFurniture()) {
      exportNode((Node)object3DFactory.createObject3D(home, piece, true), false, false, silk);
    }
    for (Room room : home.getRooms()) {
      exportNode((Node)object3DFactory.createObject3D(home, room, true), true, false, silk);
    }
    // Create a dummy home to export a ground 3D not cut by rooms and large enough to join the sky at the horizon 
    Home groundHome = new Home();
    groundHome.getEnvironment().setGroundColor(home.getEnvironment().getGroundColor());
    groundHome.getEnvironment().setGroundTexture(home.getEnvironment().getGroundTexture());
    Ground3D ground = new Ground3D(groundHome, -1E7f / 2, -1E7f / 2, 1E7f, 1E7f, true);
    Transform3D translation = new Transform3D();
    translation.setTranslation(new Vector3f(0, -0.1f, 0));
    TransformGroup groundTransformGroup = new TransformGroup(translation);
    groundTransformGroup.addChild(ground);
    exportNode(groundTransformGroup, true, true, silk);

    HomeTexture skyTexture = home.getEnvironment().getSkyTexture();
    this.useSunSky = skyTexture == null || !(home.getCamera() instanceof ObserverCamera);
    if (!this.useSunSky) {
      // If observer camera is used with a sky texture,
      // create an image base light from sky texture 
      InputStream skyImageStream = skyTexture.getImage().openStream();
      BufferedImage skyImage = ImageIO.read(skyImageStream);
      skyImageStream.close();
      // Create a temporary image base light twice as high that will contain sky image in the top part
      BufferedImage imageBaseLightImage = new BufferedImage(skyImage.getWidth(),
          skyImage.getHeight() * 2, BufferedImage.TYPE_INT_RGB);
      Graphics2D g2D = (Graphics2D)imageBaseLightImage.getGraphics();
      g2D.drawRenderedImage(skyImage, null);
      g2D.dispose();
      File imageFile = OperatingSystem.createTemporaryFile("ibl", ".png");
      ImageIO.write(imageBaseLightImage, "png", imageFile);
     
      this.sunflow.parameter("texture", imageFile.getAbsolutePath());
      this.sunflow.parameter("center", new Vector3(-1, 0, 0));
      this.sunflow.parameter("up", new Vector3(0, 1, 0));
      this.sunflow.parameter("fixed", true);
      this.sunflow.parameter("samples", 0);
      this.sunflow.light(UUID.randomUUID().toString(), "ibl");
    }
   
    // Set light settings
    int ceillingLightColor = home.getEnvironment().getCeillingLightColor();
    this.homeLightColor = home.getEnvironment().getLightColor();
    if (ceillingLightColor > 0) {
      // Add lights at the top of each room
      for (Room room : home.getRooms()) {
        if (room.isCeilingVisible()) {
          float xCenter = room.getXCenter();
          float yCenter = room.getYCenter();
         
          double smallestDistance = Float.POSITIVE_INFINITY;
          float roomHeight = home.getWallHeight();
         
          // Search the height of the wall closest to the point xCenter, yCenter
          for (Wall wall : home.getWalls()) {
            Float wallHeightAtStart = wall.getHeight();
            float [][] points = wall.getPoints();
            for (int i = 0; i < points.length; i++) {
              double distanceToWallPoint = Point2D.distanceSq(points [i][0], points [i][1], xCenter, yCenter);
              if (distanceToWallPoint < smallestDistance) {
                smallestDistance = distanceToWallPoint;
                if (i == 0 || i == 3) { // Wall start
                  roomHeight = wallHeightAtStart != null
                      ? wallHeightAtStart
                      : home.getWallHeight();
                } else { // Wall end
                  roomHeight = wall.isTrapezoidal()
                      ? wall.getHeightAtEnd()
                      : (wallHeightAtStart != null ? wallHeightAtStart : home.getWallHeight());
                }
              }
            }
          }
         
          float power = (float)Math.sqrt(room.getArea()) / 3;
          this.sunflow.parameter("radiance", null,
              power * (ceillingLightColor >> 16) / 0xD0 * (this.homeLightColor >> 16) / 255,
              power * ((ceillingLightColor >> 8) & 0xFF) / 0xD0 * ((this.homeLightColor >> 8) & 0xFF) / 255,
              power * (ceillingLightColor & 0xFF) / 0xD0 * (this.homeLightColor & 0xFF) / 255);
          this.sunflow.parameter("center", new Point3(xCenter, roomHeight - 25, yCenter));                   
          this.sunflow.parameter("radius", 20f);
          this.sunflow.parameter("samples", 4);
          this.sunflow.light(UUID.randomUUID().toString(), "sphere");
        }
      }
    }

    // Add visible and turned on lights
    for (HomeLight light : getLights(home.getFurniture())) {
      float lightPower = light.getPower();
      if (light.isVisible()
          && lightPower > 0f) {
        float angle = light.getAngle();
        float cos = (float)Math.cos(angle);
        float sin = (float)Math.sin(angle);
        for (LightSource lightSource : ((HomeLight)light).getLightSources()) {
          float lightRadius = lightSource.getDiameter() != null
                  ? lightSource.getDiameter() * light.getWidth() / 2
                  : 3.25f; // Default radius compatible with most lights available before version 3.0
          float power = 5 * lightPower * lightPower / (lightRadius * lightRadius);
          int lightColor = lightSource.getColor();
          this.sunflow.parameter("radiance", null,
              power * (lightColor >> 16) * (this.homeLightColor >> 16),
              power * ((lightColor >> 8) & 0xFF) * ((this.homeLightColor >> 8) & 0xFF),
              power * (lightColor & 0xFF) * (this.homeLightColor & 0xFF));
          float xLightSourceInLight = -light.getWidth() / 2 + (lightSource.getX() * light.getWidth());
          float yLightSourceInLight = light.getDepth() / 2 - (lightSource.getY() * light.getDepth());
          this.sunflow.parameter("center",
              new Point3(light.getX() + xLightSourceInLight * cos - yLightSourceInLight * sin,
                  light.getElevation() + (lightSource.getZ() * light.getHeight()),
                  light.getY() + xLightSourceInLight * sin + yLightSourceInLight * cos));                   
          this.sunflow.parameter("radius", lightRadius);
          this.sunflow.parameter("samples", 4);
          this.sunflow.light(UUID.randomUUID().toString(), "sphere");
        }
      }
    }

    this.sunflow.parameter("depths.diffuse", 1);
    this.sunflow.parameter("depths.reflection", 4);
    this.sunflow.parameter("depths.refraction", 16);
    this.sunflow.options(SunflowAPI.DEFAULT_OPTIONS);
   
    // Use a spiral computing
    this.sunflow.parameter("bucket.size", 32);
    this.sunflow.parameter("bucket.order", "spiral");
    this.sunflow.options(SunflowAPI.DEFAULT_OPTIONS);
  }

  /**
   * Renders home in <code>image</code> at the given <code>camera</code> location and image size.
   * The rendered objects of the home are the ones given in constructor, meaning any change made in
   * home since the instantiation of this renderer won't be updated.
   */
  public void render(final BufferedImage image,
                     Camera camera,
                     final ImageObserver observer) {
    this.renderingThread = Thread.currentThread();

    if (this.sunSkyLightName != null) {
      this.sunflow.remove(this.sunSkyLightName);
    }
    if (this.sunLightName != null) {
      this.sunflow.remove(this.sunLightName);
    }
    float [] sunDirection = getSunDirection(this.compass, Camera.convertTimeToTimeZone(camera.getTime(), this.compass.getTimeZone()));
    // Update Sun direction during daytime
    if (sunDirection [1] > -0.075f) {
      if (this.useSunSky) {
        this.sunflow.parameter("up", new Vector3(0, 1, 0));
        this.sunflow.parameter("east",
            new Vector3((float)Math.sin(compass.getNorthDirection()), 0, (float)Math.cos(compass.getNorthDirection())));
        this.sunflow.parameter("sundir", new Vector3(sunDirection [0], sunDirection [1], sunDirection [2]));
        this.sunflow.parameter("turbidity", 6f);
        this.sunflow.parameter("samples", this.useAmbientOcclusion ? 0 : 12);
        this.sunSkyLightName = UUID.randomUUID().toString();
        this.sunflow.light(this.sunSkyLightName, "sunsky");
      }

      // Retrieve sun color
      SunSkyLight sunSkyLight = new SunSkyLight();
      ParameterList parameterList = new ParameterList();
      parameterList.addVectors("up", InterpolationType.NONE, new float [] {0, 1, 0});
      parameterList.addVectors("east", InterpolationType.NONE,
          new float [] {(float)Math.sin(compass.getNorthDirection()), 0, (float)Math.cos(compass.getNorthDirection())});
      parameterList.addVectors("sundir", InterpolationType.NONE,
          new float [] {sunDirection [0], sunDirection [1], sunDirection [2]});
      sunSkyLight.update(parameterList, this.sunflow);
      float [] sunColor = sunSkyLight.getSunColor().getRGB();
     
      // Simulate additional Sun with a faraway sphere light of a color depending of the hour of the day
      int sunPower = this.useAmbientOcclusion ? 40 : 10;
      this.sunflow.parameter("radiance", null,
          (this.homeLightColor >> 16) * sunPower * (float)Math.sqrt(sunColor [0]),
          ((this.homeLightColor >> 8) & 0xFF) * sunPower * (float)Math.sqrt(sunColor [1]),
          (this.homeLightColor & 0xFF) * sunPower * (float)Math.sqrt(sunColor [2]));
      this.sunflow.parameter("center", new Point3(1000000 * sunDirection [0], 1000000 * sunDirection [1], 1000000 * sunDirection [2]));
      this.sunflow.parameter("radius", 10000f)
      this.sunflow.parameter("samples", 4);
      this.sunLightName = UUID.randomUUID().toString();
      this.sunflow.light(this.sunLightName, "sphere");

      if (this.useAmbientOcclusion) {
        this.sunflow.parameter("gi.engine", "ambocc");
        this.sunflow.parameter("gi.ambocc.bright", null, new float [] {1, 1, 1});
        // Use complementary color
        this.sunflow.parameter("gi.ambocc.dark", null,
            new float [] {(sunColor [1] + sunColor [2]) / 200,
                          (sunColor [0] + sunColor [2]) / 200,
                          (sunColor [0] + sunColor [1]) / 200});
        this.sunflow.parameter("gi.ambocc.samples", 1);
        this.sunflow.options(SunflowAPI.DEFAULT_OPTIONS);
      }
    }

    // Update camera lens
    final String CAMERA_NAME = "camera";   
    switch (camera.getLens()) {
      case SPHERICAL:
        this.sunflow.camera(CAMERA_NAME, "spherical");
        break;
      case FISHEYE:
        this.sunflow.camera(CAMERA_NAME, "fisheye");
        break;
      case NORMAL:
        this.sunflow.parameter("focus.distance", 250f);
        this.sunflow.parameter("lens.radius", 1f);
        this.sunflow.camera(CAMERA_NAME, "thinlens");
        break;
      case PINHOLE:
      default:
        this.sunflow.camera(CAMERA_NAME, "pinhole");
        break;
    }
   
    // Update camera location
    Point3 eye = new Point3(camera.getX(), camera.getZ(), camera.getY());
    Matrix4 transform;
    float yaw = camera.getYaw();
    float pitch;
    if (camera.getLens() == Camera.Lens.SPHERICAL) {
      pitch = 0;
    } else {
      pitch = camera.getPitch();
    }
    double pitchCos = Math.cos(pitch);
    if (Math.abs(pitchCos) > 1E-6) {
      // Set the point the camera is pointed to
      Point3 target = new Point3(
          camera.getX() - (float)(Math.sin(yaw) * pitchCos),
          camera.getZ() - (float)Math.sin(pitch),
          camera.getY() + (float)(Math.cos(yaw) * pitchCos));
      Vector3 up = new Vector3(0, 1, 0);             
      transform = Matrix4.lookAt(eye, target, up);
    } else {
      // Compute matrix directly when the camera points is at top
      transform = new Matrix4((float)-Math.cos(yaw), (float)-Math.sin(yaw), 0, camera.getX(),
          0, 0, 1, camera.getZ(),
          (float)-Math.sin(yaw), (float)Math.cos(yaw), 0, camera.getY());
    }
    this.sunflow.parameter("transform", transform);
    this.sunflow.parameter("fov", (float)Math.toDegrees(camera.getFieldOfView()));
    this.sunflow.parameter("aspect", (float)image.getWidth() / image.getHeight());
    // Update camera
    this.sunflow.camera(CAMERA_NAME, null);

    // Set image size and quality
    this.sunflow.parameter("resolutionX", image.getWidth());
    this.sunflow.parameter("resolutionY", image.getHeight());
   
    if (this.quality == Quality.HIGH) {
      this.sunflow.parameter("filter", "blackman-harris"); // box, gaussian, blackman-harris, sinc, mitchell or triangle
      // The bigger aa.max is, the cleanest rendering you get
      this.sunflow.parameter("aa.min", 1);
      this.sunflow.parameter("aa.max"2);
    } else {
      this.sunflow.parameter("filter", "box");
      this.sunflow.parameter("aa.min", 0);
      this.sunflow.parameter("aa.max", 0);
    }
    this.sunflow.parameter("sampler", "bucket"); // ipr, fast or bucket

    // Render image with default camera
    this.sunflow.parameter("camera", CAMERA_NAME);
    this.sunflow.options(SunflowAPI.DEFAULT_OPTIONS);
    this.sunflow.render(SunflowAPI.DEFAULT_OPTIONS, new BufferedImageDisplay(image, observer));
  }
 
  /**
   * Stops the rendering process.
   */
  public void stop() {
    if (this.renderingThread != null) {
      if (!this.renderingThread.isInterrupted()) {
        this.renderingThread.interrupt();
      }
      this.renderingThread = null;
    }
  }

  /**
   * Returns all the light children of the given <code>furniture</code>
   */
  private List<HomeLight> getLights(List<HomePieceOfFurniture> furniture) {
    List<HomeLight> lights = new ArrayList<HomeLight>();
    for (HomePieceOfFurniture piece : furniture) {
      if (piece instanceof HomeLight) {
        lights.add((HomeLight)piece);
      } else if (piece instanceof HomeFurnitureGroup) {
        lights.addAll(getLights(((HomeFurnitureGroup)piece).getFurniture()));
      }
    }
    return lights;
  }

  /**
   * Returns sun direction at a given <code>time</code>.
   * @author Fr�d�ric Mantegazza
   */
  private float [] getSunDirection(Compass compass, long time) {
    float elevation = compass.getSunElevation(time);
    float azimuth = compass.getSunAzimuth(time);
    azimuth += compass.getNorthDirection() - Math.PI / 2f;
    return new float [] {(float)(Math.cos(azimuth) * Math.cos(elevation)),
                         (float)Math.sin(elevation),
                         (float)(Math.sin(azimuth) * Math.cos(elevation))};
  }

  /**
   * Exports the given Java 3D <code>node</code> and its children to Sunflow API. 
   */
  private void exportNode(Node node, boolean ignoreTransparency,
                          boolean ignoreConstantShader, boolean silk) throws IOException {
    exportNode(node, ignoreTransparency, ignoreConstantShader, silk, new Transform3D());
  }

  /**
   * Exports all the 3D shapes children of <code>node</code> at OBJ format.
   */
  private void exportNode(Node node,
                          boolean ignoreTransparency,
                          boolean ignoreConstantShader,
                          boolean silk,
                          Transform3D parentTransformations) throws IOException {
    if (node instanceof Group) {
      if (node instanceof TransformGroup) {
        parentTransformations = new Transform3D(parentTransformations);
        Transform3D transform = new Transform3D();
        ((TransformGroup)node).getTransform(transform);
        parentTransformations.mul(transform);
      }
      // Export all children
      Enumeration<?> enumeration = ((Group)node).getAllChildren();
      while (enumeration.hasMoreElements()) {
        exportNode((Node)enumeration.nextElement(), ignoreTransparency, ignoreConstantShader, silk, parentTransformations);
      }
    } else if (node instanceof Link) {
      exportNode(((Link)node).getSharedGroup(), ignoreTransparency, ignoreConstantShader, silk, parentTransformations);
    } else if (node instanceof Shape3D) {
      Shape3D shape = (Shape3D)node;
      Appearance appearance = shape.getAppearance();
      RenderingAttributes renderingAttributes = appearance != null
          ? appearance.getRenderingAttributes() : null;
      TransparencyAttributes transparencyAttributes = appearance != null
          ? appearance.getTransparencyAttributes() : null;
      // Ignore invisible shapes and fully transparency shapes without a texture
      if ((renderingAttributes == null
              || renderingAttributes.getVisible())
          && (transparencyAttributes == null
              || transparencyAttributes.getTransparency() != 1)) {
        String shapeName = (String)shape.getUserData();
        // Build a unique object name
        String uuid = UUID.randomUUID().toString();
 
        String appearanceName = null;
        TexCoordGeneration texCoordGeneration = null;
        if (appearance != null) {
          texCoordGeneration = appearance.getTexCoordGeneration();
          appearanceName = "shader" + uuid;
          boolean mirror = shapeName != null
              && shapeName.startsWith(ModelManager.MIRROR_SHAPE_PREFIX);
          exportAppearance(appearance, appearanceName, mirror, ignoreTransparency, ignoreConstantShader, silk);
        }

        // Export object geometries
        for (int i = 0, n = shape.numGeometries(); i < n; i++) {
          String objectNameBase = "object" + uuid + "-" + i;
          // Always ignore normals on walls
          String [] objectsName = exportNodeGeometry(shape.getGeometry(i), parentTransformations, texCoordGeneration,
              objectNameBase);
          if (objectsName != null) {
            for (String objectName : objectsName) {
              if (appearanceName != null) {
                this.sunflow.parameter("shaders", new String [] {appearanceName});
              }
              this.sunflow.instance(objectName + ".instance", objectName);
            }
          }
        }
      }
    }   
  }
 
  /**
   * Returns the names of the exported 3D geometries in Sunflow API.
   */
  private String [] exportNodeGeometry(Geometry geometry,
                                       Transform3D parentTransformations,
                                       TexCoordGeneration texCoordGeneration,
                                       String objectNameBase) {
    if (geometry instanceof GeometryArray) {
      GeometryArray geometryArray = (GeometryArray)geometry;
     
      // Create vertices indices array depending on geometry class
      int [] verticesIndices = null;
      int [] stripVertexCount = null;
      if (geometryArray instanceof IndexedGeometryArray) {
        if (geometryArray instanceof IndexedLineArray) {
          verticesIndices = new int [((IndexedGeometryArray)geometryArray).getIndexCount()];
        } else if (geometryArray instanceof IndexedTriangleArray) {
          verticesIndices = new int [((IndexedGeometryArray)geometryArray).getIndexCount()];
        } else if (geometryArray instanceof IndexedQuadArray) {
          verticesIndices = new int [((IndexedQuadArray)geometryArray).getIndexCount() * 3 / 2];
        } else if (geometryArray instanceof IndexedGeometryStripArray) {
          IndexedTriangleStripArray geometryStripArray = (IndexedTriangleStripArray)geometryArray;
          stripVertexCount = new int [geometryStripArray.getNumStrips()];
          geometryStripArray.getStripIndexCounts(stripVertexCount);         
          if (geometryArray instanceof IndexedLineStripArray) {
            verticesIndices = new int [getLineCount(stripVertexCount) * 2];
          } else {
            verticesIndices = new int [getTriangleCount(stripVertexCount) * 3];
          }
        }
      } else {
        if (geometryArray instanceof LineArray) {
          verticesIndices = new int [((GeometryArray)geometryArray).getVertexCount()];
        } else if (geometryArray instanceof TriangleArray) {
          verticesIndices = new int [((GeometryArray)geometryArray).getVertexCount()];
        } else if (geometryArray instanceof QuadArray) {
          verticesIndices = new int [((QuadArray)geometryArray).getVertexCount() * 3 / 2];
        } else if (geometryArray instanceof GeometryStripArray) {
          GeometryStripArray geometryStripArray = (GeometryStripArray)geometryArray;
          stripVertexCount = new int [geometryStripArray.getNumStrips()];
          geometryStripArray.getStripVertexCounts(stripVertexCount);
          if (geometryArray instanceof LineStripArray) {
            verticesIndices = new int [getLineCount(stripVertexCount) * 2];
          } else {
            verticesIndices = new int [getTriangleCount(stripVertexCount) * 3];
          }      
        }
      }

      if (verticesIndices != null) {
        boolean line = geometryArray instanceof IndexedLineArray
            || geometryArray instanceof IndexedLineStripArray
            || geometryArray instanceof LineArray
            || geometryArray instanceof LineStripArray;
        float [] vertices = new float [geometryArray.getVertexCount() * 3];
        float [] normals = !line && (geometryArray.getVertexFormat() & GeometryArray.NORMALS) != 0
            ? new float [geometryArray.getVertexCount() * 3]
            : null;       
        // Store temporarily exported triangles to avoid to add their opposite triangles
        // (SunFlow doesn't render correctly a face and its opposite) 
        Set<Triangle> exportedTriangles = line
            ? null
            : new HashSet<Triangle>(geometryArray.getVertexCount());
       
        boolean uvsGenerated = false;
        Vector4f planeS = null;
        Vector4f planeT = null;
        if (!line && texCoordGeneration != null) {
          uvsGenerated = texCoordGeneration.getGenMode() == TexCoordGeneration.OBJECT_LINEAR
              && texCoordGeneration.getEnable();
          if (uvsGenerated) {
            planeS = new Vector4f();
            planeT = new Vector4f();
            texCoordGeneration.getPlaneS(planeS);
            texCoordGeneration.getPlaneT(planeT);
          }
        }
 
        float [] uvs;
        if (uvsGenerated
            || (geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) {
          uvs = new float [geometryArray.getVertexCount() * 2];
        } else {
          uvs = null;
        }
      
        if ((geometryArray.getVertexFormat() & GeometryArray.BY_REFERENCE) != 0) {
          if ((geometryArray.getVertexFormat() & GeometryArray.INTERLEAVED) != 0) {
            float [] vertexData = geometryArray.getInterleavedVertices();
            int vertexSize = vertexData.length / geometryArray.getVertexCount();
            // Export vertices coordinates
            for (int index = 0, i = vertexSize - 3, n = geometryArray.getVertexCount();
                 index < n; index++, i += vertexSize) {
              Point3f vertex = new Point3f(vertexData [i], vertexData [i + 1], vertexData [i + 2]);
              exportVertex(parentTransformations, vertex, index, vertices);
            }
            // Export normals
            if (normals != null) {
              for (int index = 0, i = vertexSize - 6, n = geometryArray.getVertexCount();
                   index < n; index++, i += vertexSize) {
                Vector3f normal = new Vector3f(vertexData [i], vertexData [i + 1], vertexData [i + 2]);
                exportNormal(parentTransformations, normal, index, normals);
              }
            }
            // Export texture coordinates
            if (texCoordGeneration != null) {
              if (uvsGenerated) {
                for (int index = 0, i = vertexSize - 3, n = geometryArray.getVertexCount();
                      index < n; index++, i += vertexSize) {
                  TexCoord2f textureCoordinates = generateTextureCoordinates(
                      vertexData [i], vertexData [i + 1], vertexData [i + 2], planeS, planeT);
                  exportTextureCoordinates(textureCoordinates, index, uvs);
                }
              }
            } else if (uvs != null) {
              for (int index = 0, i = 0, n = geometryArray.getVertexCount();
                    index < n; index++, i += vertexSize) {
                TexCoord2f textureCoordinates = new TexCoord2f(vertexData [i], vertexData [i + 1]);
                exportTextureCoordinates(textureCoordinates, index, uvs);
              }
            }
          } else {
            // Export vertices coordinates
            float [] vertexCoordinates = geometryArray.getCoordRefFloat();
            for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 3) {
              Point3f vertex = new Point3f(vertexCoordinates [i], vertexCoordinates [i + 1], vertexCoordinates [i + 2]);
              exportVertex(parentTransformations, vertex, index, vertices);
            }
            // Export normals
            if (normals != null) {
              float [] normalCoordinates = geometryArray.getNormalRefFloat();
              for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 3) {
                Vector3f normal = new Vector3f(normalCoordinates [i], normalCoordinates [i + 1], normalCoordinates [i + 2]);
                exportNormal(parentTransformations, normal, index, normals);
              }
            }
            // Export texture coordinates
            if (texCoordGeneration != null) {
              if (uvsGenerated) {
                for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 3) {
                  TexCoord2f textureCoordinates = generateTextureCoordinates(
                      vertexCoordinates [i], vertexCoordinates [i + 1], vertexCoordinates [i + 2], planeS, planeT);
                  exportTextureCoordinates(textureCoordinates, index, uvs);
                }
              }
            } else if (uvs != null) {
              float [] textureCoordinatesArray = geometryArray.getTexCoordRefFloat(0);
              for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 2) {
                TexCoord2f textureCoordinates = new TexCoord2f(textureCoordinatesArray [i], textureCoordinatesArray [i + 1]);
                exportTextureCoordinates(textureCoordinates, index, uvs);
              }
            }
          }
        } else {
          // Export vertices coordinates
          for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) {
            Point3f vertex = new Point3f();
            geometryArray.getCoordinate(index, vertex);
            exportVertex(parentTransformations, vertex, index, vertices);
          }
          // Export normals
          if (normals != null) {
            for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) {
              Vector3f normal = new Vector3f();
              geometryArray.getNormal(index, normal);
              exportNormal(parentTransformations, normal, index, normals);
            }
          }
          // Export texture coordinates
          if (texCoordGeneration != null) {
            if (uvsGenerated) {
              for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) {
                Point3f vertex = new Point3f();
                geometryArray.getCoordinate(index, vertex);
                TexCoord2f textureCoordinates = generateTextureCoordinates(
                    vertex.x, vertex.y, vertex.z, planeS, planeT);
                exportTextureCoordinates(textureCoordinates, index, uvs);
              }
            }
          } else if (uvs != null) {
            for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) {
              TexCoord2f textureCoordinates = new TexCoord2f();
              geometryArray.getTextureCoordinate(0, index, textureCoordinates);
              exportTextureCoordinates(textureCoordinates, index, uvs);
            }
          }
        }

        // Export lines, triangles or quadrilaterals depending on the geometry
        if (geometryArray instanceof IndexedGeometryArray) {
          int [] normalsIndices = normals != null
              ? new int [verticesIndices.length]
              : null;
          int [] uvsIndices = uvs != null
              ? new int [verticesIndices.length]
              : null;
             
          if (geometryArray instanceof IndexedLineArray) {
            IndexedLineArray lineArray = (IndexedLineArray)geometryArray;
            for (int i = 0, n = lineArray.getIndexCount(); i < n; i += 2) {
              exportIndexedLine(lineArray, i, i + 1, verticesIndices, i);
            }
          } else {
            if (geometryArray instanceof IndexedTriangleArray) {
              IndexedTriangleArray triangleArray = (IndexedTriangleArray)geometryArray;
              for (int i = 0, n = triangleArray.getIndexCount(), triangleIndex = 0; i < n; i += 3) {
                triangleIndex = exportIndexedTriangle(triangleArray, i, i + 1, i + 2,
                    verticesIndices, normalsIndices, uvsIndices, triangleIndex, vertices, exportedTriangles);
              }
            } else if (geometryArray instanceof IndexedQuadArray) {
              IndexedQuadArray quadArray = (IndexedQuadArray)geometryArray;
              for (int i = 0, n = quadArray.getIndexCount(), triangleIndex = 0; i < n; i += 4) {
                triangleIndex = exportIndexedTriangle(quadArray, i, i + 1, i + 2,
                    verticesIndices, normalsIndices, uvsIndices, triangleIndex, vertices, exportedTriangles);
                triangleIndex = exportIndexedTriangle(quadArray, i, i + 2, i + 3,
                    verticesIndices, normalsIndices, uvsIndices, triangleIndex, vertices, exportedTriangles);
              }
            } else if (geometryArray instanceof IndexedLineStripArray) {
              IndexedLineStripArray lineStripArray = (IndexedLineStripArray)geometryArray;
              for (int initialIndex = 0, lineIndex = 0, strip = 0; strip < stripVertexCount.length; strip++) {
                for (int i = initialIndex, n = initialIndex + stripVertexCount [strip] - 1;
                     i < n; i++, lineIndex += 2) {
                   exportIndexedLine(lineStripArray, i, i + 1, verticesIndices, lineIndex);
                }
                initialIndex += stripVertexCount [strip];
              }
            } else if (geometryArray instanceof IndexedTriangleStripArray) {
              IndexedTriangleStripArray triangleStripArray = (IndexedTriangleStripArray)geometryArray;
              for (int initialIndex = 0, triangleIndex = 0, strip = 0; strip < stripVertexCount.length; strip++) {
                for (int i = initialIndex, n = initialIndex + stripVertexCount [strip] - 2, j = 0;
                     i < n; i++, j++) {
                  if (j % 2 == 0) {
                    triangleIndex = exportIndexedTriangle(triangleStripArray, i, i + 1, i + 2,
                        verticesIndices, normalsIndices, uvsIndices, triangleIndex, vertices, exportedTriangles);
                  } else { // Vertices of odd triangles are in reverse order              
                    triangleIndex = exportIndexedTriangle(triangleStripArray, i, i + 2, i + 1,
                        verticesIndices, normalsIndices, uvsIndices, triangleIndex, vertices, exportedTriangles);
                  }
                }
                initialIndex += stripVertexCount [strip];
              }
            } else if (geometryArray instanceof IndexedTriangleFanArray) {
              IndexedTriangleFanArray triangleFanArray = (IndexedTriangleFanArray)geometryArray;
              for (int initialIndex = 0, triangleIndex = 0, strip = 0; strip < stripVertexCount.length; strip++) {
                for (int i = initialIndex, n = initialIndex + stripVertexCount [strip] - 2;
                     i < n; i++) {
                  triangleIndex = exportIndexedTriangle(triangleFanArray, initialIndex, i + 1, i + 2,
                      verticesIndices, normalsIndices, uvsIndices, triangleIndex, vertices, exportedTriangles);
                }
                initialIndex += stripVertexCount [strip];
              }
            }
          }
         
          if (normalsIndices != null && !Arrays.equals(verticesIndices, normalsIndices)
              || uvsIndices != null && !Arrays.equals(verticesIndices, uvsIndices)) {
            // Remove indirection in verticesIndices, normals and uvsIndices
            // because SunFlow use only verticesIndices
            float [] directVertices = new float [verticesIndices.length * 3];
            float [] directNormals =  normalsIndices != null
                ? new float [verticesIndices.length * 3]
                : null;
            float [] directUvs =  uvsIndices != null
                ? new float [verticesIndices.length * 2]
                : null;
            int verticeIndex = 0;
            int normalIndex = 0;
            int uvIndex = 0;
            for (int i = 0; i < verticesIndices.length; i++) {
              int indirectIndex = verticesIndices [i] * 3;
              directVertices [verticeIndex++] = vertices [indirectIndex++];
              directVertices [verticeIndex++] = vertices [indirectIndex++];
              directVertices [verticeIndex++] = vertices [indirectIndex++];
              if (normalsIndices != null) {
                indirectIndex = normalsIndices [i] * 3;
                directNormals [normalIndex++] = normals [indirectIndex++];
                directNormals [normalIndex++] = normals [indirectIndex++];
                directNormals [normalIndex++] = normals [indirectIndex++];
              }
              if (uvsIndices != null) {
                indirectIndex = uvsIndices [i] * 2;
                directUvs [uvIndex++] = uvs [indirectIndex++];
                directUvs [uvIndex++] = uvs [indirectIndex++];
              }
              verticesIndices [i] = i;
            }
            vertices = directVertices;
            normals = directNormals;
            uvs = directUvs;
          }
        } else {
          if (geometryArray instanceof LineArray) {
            LineArray lineArray = (LineArray)geometryArray;
            for (int i = 0, n = lineArray.getVertexCount(); i < n; i += 2) {
              exportLine(lineArray, i, i + 1, verticesIndices, i);
            }
          } else {
            if (geometryArray instanceof TriangleArray) {
              TriangleArray triangleArray = (TriangleArray)geometryArray;
              for (int i = 0, n = triangleArray.getVertexCount(), triangleIndex = 0; i < n; i += 3) {
                triangleIndex = exportTriangle(triangleArray, i, i + 1, i + 2,
                    verticesIndices, triangleIndex, vertices, exportedTriangles);
              }
            } else if (geometryArray instanceof QuadArray) {
              QuadArray quadArray = (QuadArray)geometryArray;
              for (int i = 0, n = quadArray.getVertexCount(), triangleIndex = 0; i < n; i += 4) {
                triangleIndex = exportTriangle(quadArray, i, i + 1, i + 2,
                    verticesIndices, triangleIndex, vertices, exportedTriangles);
                triangleIndex = exportTriangle(quadArray, i + 2, i + 3, i,
                    verticesIndices, triangleIndex, vertices, exportedTriangles);
              }
            } else if (geometryArray instanceof LineStripArray) {
              LineStripArray lineStripArray = (LineStripArray)geometryArray;
              for (int initialIndex = 0, lineIndex = 0, strip = 0; strip < stripVertexCount.length; strip++) {
                for (int i = initialIndex, n = initialIndex + stripVertexCount [strip] - 1;
                     i < n; i++, lineIndex += 2) {
                  exportLine(lineStripArray, i, i + 1, verticesIndices, lineIndex);
                }
                initialIndex += stripVertexCount [strip];
              }
            } else if (geometryArray instanceof TriangleStripArray) {
              TriangleStripArray triangleStripArray = (TriangleStripArray)geometryArray;
              for (int initialIndex = 0, triangleIndex = 0, strip = 0; strip < stripVertexCount.length; strip++) {
                for (int i = initialIndex, n = initialIndex + stripVertexCount [strip] - 2, j = 0;
                     i < n; i++, j++) {
                  if (j % 2 == 0) {
                    triangleIndex = exportTriangle(triangleStripArray, i, i + 1, i + 2,
                        verticesIndices, triangleIndex, vertices, exportedTriangles);
                  } else { // Vertices of odd triangles are in reverse order              
                    triangleIndex = exportTriangle(triangleStripArray, i, i + 2, i + 1,
                        verticesIndices, triangleIndex, vertices, exportedTriangles);
                  }
                }
                initialIndex += stripVertexCount [strip];
              }
            } else if (geometryArray instanceof TriangleFanArray) {
              TriangleFanArray triangleFanArray = (TriangleFanArray)geometryArray;
              for (int initialIndex = 0, triangleIndex = 0, strip = 0; strip < stripVertexCount.length; strip++) {
                for (int i = initialIndex, n = initialIndex + stripVertexCount [strip] - 2;
                     i < n; i++) {
                  triangleIndex = exportTriangle(triangleFanArray, initialIndex, i + 1, i + 2, verticesIndices,
                      triangleIndex, vertices, exportedTriangles);
                }
                initialIndex += stripVertexCount [strip];
              }
            }
          }
        }
     
        if (line) {
          String [] objectNames = new String [verticesIndices.length / 2];
          for (int startIndex = 0; startIndex < verticesIndices.length; startIndex += 2) {
            String objectName = objectNameBase + "-" + startIndex;
            objectNames [startIndex / 2] = objectName;
           
            // Get points coordinates of a segment
            float [] points = new float [6];
            int pointIndex = 0;
            for (int i = startIndex; i <= startIndex + 1; i++) {
              int indirectIndex = verticesIndices [i] * 3;
              points [pointIndex++] = vertices [indirectIndex++];
              points [pointIndex++] = vertices [indirectIndex++];
              points [pointIndex++] = vertices [indirectIndex];
            }
           
            // Create as many hairs as segments otherwise long hairs become invisible
            this.sunflow.parameter("segments", 1);
            this.sunflow.parameter("widths", 0.15f);
            this.sunflow.parameter("points", "point", "vertex", points);
            this.sunflow.geometry(objectName, "hair");
          }
          return objectNames;
        } else {
          int exportedTrianglesVertexCount = exportedTriangles.size() * 3;
          if (exportedTrianglesVertexCount < verticesIndices.length) {
            // Reduce verticesIndices array to contain only exported triangles
            int [] tmp = new int [exportedTrianglesVertexCount];
            System.arraycopy(verticesIndices, 0, tmp, 0, tmp.length);
            verticesIndices = tmp;             
          }
         
          this.sunflow.parameter("triangles", verticesIndices);
          this.sunflow.parameter("points", "point", "vertex", vertices);
          if (normals != null) {
            // Check there's no NaN values in normals to avoid endless loop in SunFlow
            boolean noNaN = true;
            for (float val : normals) {
              if (Float.isNaN(val)) {
                noNaN = false;
                break;
              }
            }
            if (noNaN)  {
              this.sunflow.parameter("normals", "vector", "vertex", normals);
            }
          }
          if (uvs != null) {
            // Check there's no huge values in uvs to avoid problems in SunFlow
            boolean noHugeValues = true;
            for (float val : uvs) {
              if (Math.abs(val) > 1E9) {
                noHugeValues = false;
                break;
              }
            }
            if (noHugeValues)  {
              this.sunflow.parameter("uvs", "texcoord", "vertex", uvs);
            }
          }
          this.sunflow.geometry(objectNameBase, "triangle_mesh");
          return new String [] {objectNameBase};
        }
      }
    }
    return null;
  }
 
  /**
   * Returns texture coordinates generated with <code>texCoordGeneration</code> computed
   * as described in <code>TexCoordGeneration</code> javadoc.
   */
  private TexCoord2f generateTextureCoordinates(float x, float y, float z,
                                                Vector4f planeS,
                                                Vector4f planeT) {
    return new TexCoord2f(x * planeS.x + y * planeS.y + z * planeS.z,
        x * planeT.x + y * planeT.y + z * planeT.z);
  }

  /**
   * Returns the sum of line integers in <code>stripVertexCount</code> array.
   */
  private int getLineCount(int [] stripVertexCount) {
    int lineCount = 0;
    for (int strip = 0; strip < stripVertexCount.length; strip++) {
      lineCount += stripVertexCount [strip] - 1;
    }
    return lineCount;
  }

  /**
   * Returns the sum of triangle integers in <code>stripVertexCount</code> array.
   */
  private int getTriangleCount(int [] stripVertexCount) {
    int triangleCount = 0;
    for (int strip = 0; strip < stripVertexCount.length; strip++) {
      triangleCount += stripVertexCount [strip] - 2;
    }
    return triangleCount;
  }

  /**
   * Applies to <code>vertex</code> the given transformation, and stores it in <code>vertices</code>
   */
  private void exportVertex(Transform3D transformationToParent,
                            Point3f vertex, int index,
                            float [] vertices) {
    transformationToParent.transform(vertex);
    index *= 3;
    vertices [index++] = vertex.x;
    vertices [index++] = vertex.y;
    vertices [index] = vertex.z;
  }

  /**
   * Applies to <code>normal</code> the given transformation, and stores it in <code>normals</code>
   */
  private void exportNormal(Transform3D transformationToParent,
                            Vector3f normal, int index,
                            float [] normals) {
    transformationToParent.transform(normal);
    int i = index * 3;
    normals [i++] = normal.x;
    normals [i++] = normal.y;
    normals [i] = normal.z;
  }

  /**
   * Stores <code>textureCoordinates</code> in <code>uvs</code>
   */
  private void exportTextureCoordinates(TexCoord2f textureCoordinates, int index,
                                        float [] uvs) {
    index *= 2;
    uvs [index++] = textureCoordinates.x;
    uvs [index] = textureCoordinates.y;
  }

  /**
   * Stores in <code>verticesIndices</code> the indices given at vertexIndex1, vertexIndex2.
   */
  private void exportIndexedLine(IndexedGeometryArray geometryArray,
                                 int vertexIndex1, int vertexIndex2,
                                 int [] verticesIndices,
                                 int index) {
    verticesIndices [index++] = geometryArray.getCoordinateIndex(vertexIndex1);
    verticesIndices [index] = geometryArray.getCoordinateIndex(vertexIndex2);
  }
   
  /**
   * Stores in <code>verticesIndices</code> the indices given at vertexIndex1, vertexIndex2, vertexIndex3.
   */
  private int exportIndexedTriangle(IndexedGeometryArray geometryArray,
                                    int vertexIndex1, int vertexIndex2, int vertexIndex3,
                                    int [] verticesIndices, int [] normalsIndices, int [] textureCoordinatesIndices,
                                    int index,
                                    float [] vertices,
                                    Set<Triangle> exportedTriangles) {
    int coordinateIndex1 = geometryArray.getCoordinateIndex(vertexIndex1);
    int coordinateIndex2 = geometryArray.getCoordinateIndex(vertexIndex2);
    int coordinateIndex3 = geometryArray.getCoordinateIndex(vertexIndex3);
    Triangle exportedTriangle = new Triangle(vertices, coordinateIndex1, coordinateIndex2, coordinateIndex3);
    if (!exportedTriangles.contains(exportedTriangle)) {
      exportedTriangles.add(exportedTriangle);
      verticesIndices [index] = coordinateIndex1;
      verticesIndices [index + 1] = coordinateIndex2;
      verticesIndices [index + 2] = coordinateIndex3;
      if (normalsIndices != null) {
        normalsIndices [index] = geometryArray.getNormalIndex(vertexIndex1);
        normalsIndices [index + 1] = geometryArray.getNormalIndex(vertexIndex2);
        normalsIndices [index + 2] = geometryArray.getNormalIndex(vertexIndex3);
      }
      if (textureCoordinatesIndices != null) {
        textureCoordinatesIndices [index] = geometryArray.getTextureCoordinateIndex(0, vertexIndex1);
        textureCoordinatesIndices [index + 1] = geometryArray.getTextureCoordinateIndex(0, vertexIndex2);
        textureCoordinatesIndices [index + 2] = geometryArray.getTextureCoordinateIndex(0, vertexIndex3);
      }
      return index + 3;
    }
    return index;
  }
   
  /**
   * Stores in <code>verticesIndices</code> the indices vertexIndex1, vertexIndex2, vertexIndex3.
   */
  private void exportLine(GeometryArray geometryArray,
                          int vertexIndex1, int vertexIndex2,
                          int [] verticesIndices, int index) {
    verticesIndices [index++] = vertexIndex1;
    verticesIndices [index] = vertexIndex2;
  }
   
  /**
   * Stores in <code>verticesIndices</code> the indices vertexIndex1, vertexIndex2, vertexIndex3.
   */
  private int exportTriangle(GeometryArray geometryArray,
                             int vertexIndex1, int vertexIndex2, int vertexIndex3,
                             int [] verticesIndices, int index,
                             float [] vertices,
                             Set<Triangle> exportedTriangles) {
    Triangle exportedTriangle = new Triangle(vertices, vertexIndex1, vertexIndex2, vertexIndex3);
    if (!exportedTriangles.contains(exportedTriangle)) {
      exportedTriangles.add(exportedTriangle);
      verticesIndices [index++] = vertexIndex1;
      verticesIndices [index++] = vertexIndex2;
      verticesIndices [index++] = vertexIndex3;
    }
    return index;
  }
   
  /**
   * Stores an appearance as a Sunflow shader. 
   */
  private void exportAppearance(Appearance appearance,
                                String appearanceName,
                                boolean mirror,
                                boolean ignoreTransparency,
                                boolean ignoreConstantShader,
                                boolean silk) throws IOException {
    Texture texture = appearance.getTexture();   
    if (mirror) {
      Material material = appearance.getMaterial();
      if (material != null) {
        Color3f color = new Color3f();
        material.getDiffuseColor(color);
        this.sunflow.parameter("color", null, new float [] {color.x, color.y, color.z});
      }
      this.sunflow.shader(appearanceName, "mirror");
    } else if (texture != null) {
      // Check shape transparency
      TransparencyAttributes transparencyAttributes = appearance.getTransparencyAttributes();
      float transparency;
      if (transparencyAttributes != null
          && transparencyAttributes.getTransparency() > 0
          && !ignoreTransparency) {
        transparency = 1 - transparencyAttributes.getTransparency();
      } else {
        transparency = 1;
      }
     
      TransparentTextureKey key = new TransparentTextureKey(texture, transparency);     
      String imagePath = this.textureImagesCache.get(key);
      if (imagePath == null) {
        if (texture.getUserData() instanceof URL && transparency == 1) {
          imagePath = texture.getUserData().toString();
        } else {
          ImageComponent2D imageComponent = (ImageComponent2D)texture.getImage(0);
          RenderedImage image = imageComponent.getRenderedImage();
          if (transparency < 1) {
            // Compute a partially transparent image
            BufferedImage transparentImage = new BufferedImage(image.getWidth(),
                image.getHeight(), BufferedImage.TYPE_INT_ARGB);
            Graphics2D g2D = (Graphics2D)transparentImage.getGraphics();
            g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, transparency));
            g2D.drawRenderedImage(image, null);
            g2D.dispose();
            image = transparentImage;
          }
          File imageFile = OperatingSystem.createTemporaryFile("texture", ".png");
          ImageIO.write(image, "png", imageFile);
          imagePath = imageFile.getAbsolutePath();
        }
        this.textureImagesCache.put(key, imagePath);
      }
      Material material = appearance.getMaterial();
      float shininess;
      if (material != null
          && (shininess = material.getShininess()) > 1) {
        if (silk) {
          this.sunflow.parameter("diffuse.texture", imagePath);
          Color3f color = new Color3f();
          material.getSpecularColor(color);
          float [] specularColor = new float [] {
              (float)Math.sqrt(color.x) / 2, (float)Math.sqrt(color.y) / 2, (float)Math.sqrt(color.z) / 2};
          this.sunflow.parameter("specular", null, specularColor);
          this.sunflow.parameter("glossyness", (float)Math.pow(10, -Math.log(shininess) / Math.log(5)));
          this.sunflow.parameter("samples", 1);
          this.sunflow.shader(appearanceName, "uber");
        } else {
          this.sunflow.parameter("texture", imagePath);
          this.sunflow.parameter("shiny", shininess / 512f);
          this.sunflow.shader(appearanceName, "textured_shiny_diffuse");
        }
      } else {
        this.sunflow.parameter("texture", imagePath);
        this.sunflow.shader(appearanceName, "textured_diffuse");
      }
    } else {
      Material material = appearance.getMaterial();
      if (material != null) {
        Color3f color = new Color3f();
        material.getDiffuseColor(color);
        float [] diffuseColor = new float [] {color.x, color.y, color.z};

        TransparencyAttributes transparencyAttributes = appearance.getTransparencyAttributes();
        if (transparencyAttributes != null
            && transparencyAttributes.getTransparency() > 0
            && !ignoreTransparency) {
          if (material instanceof OBJMaterial
              && ((OBJMaterial)material).isOpticalDensitySet()) {
            this.sunflow.parameter("eta", ((OBJMaterial)material).getOpticalDensity());
          } else {
            // Use glass ETA as default
            this.sunflow.parameter("eta", 1.55f);
          }
          float transparency = 1 - transparencyAttributes.getTransparency();
          this.sunflow.parameter("color", null,
              new float [] {(1 - transparency) + transparency * diffuseColor [0],
                            (1 - transparency) + transparency * diffuseColor [1],
                            (1 - transparency) + transparency * diffuseColor [2]});
          this.sunflow.parameter("absorption.color", null,
              new float [] {transparency * (1 - diffuseColor [0]),
                            transparency * (1 - diffuseColor [1]),
                            transparency * (1 - diffuseColor [2])});
          this.sunflow.shader(appearanceName, "glass");
        } else if (material.getLightingEnable()
                   || ignoreConstantShader) { 
          this.sunflow.parameter("diffuse", null, diffuseColor);
          float shininess = material.getShininess();
          if (shininess > 1) {
            if (silk) {
              material.getSpecularColor(color);
              float [] specularColor = new float [] {
                   (float)Math.sqrt(color.x) / 2, (float)Math.sqrt(color.y) / 2, (float)Math.sqrt(color.z) / 2};
              this.sunflow.parameter("specular", null, specularColor);
              this.sunflow.parameter("glossyness", (float)Math.pow(10, -Math.log(shininess) / Math.log(5)));
              this.sunflow.parameter("samples", 1);
              this.sunflow.shader(appearanceName, "uber");
            } else {
              this.sunflow.parameter("shiny", shininess / 512f);
              this.sunflow.shader(appearanceName, "shiny_diffuse");
            }
          } else {
            this.sunflow.shader(appearanceName, "diffuse");
          }
        } else {
          this.sunflow.parameter("color", null, diffuseColor);
          this.sunflow.shader(appearanceName, "constant");
        }
      } else {
        ColoringAttributes coloringAttributes = appearance.getColoringAttributes();
        if (coloringAttributes != null) {
          Color3f color = new Color3f();
          coloringAttributes.getColor(color);
          if (ignoreConstantShader) {
            this.sunflow.parameter("diffuse", null, new float [] {color.x, color.y, color.z});
            this.sunflow.shader(appearanceName, "diffuse");
          } else {
            this.sunflow.parameter("color", null, new float [] {color.x, color.y, color.z});
            this.sunflow.shader(appearanceName, "constant");
          }
        }
      }
    }
  }

  /**
   * A SunFlow display that updates an existing image.
   * Implementation mostly copied from org.sunflow.system.ImagePanel.
   */
  private static final class BufferedImageDisplay implements Display {
    private static final int BASE_INFO_FLAGS = ImageObserver.WIDTH | ImageObserver.HEIGHT | ImageObserver.PROPERTIES;
    private static final int [] BORDERS = {Color.RED.toRGB(), Color.GREEN.toRGB(), Color.BLUE.toRGB(),
                                           Color.YELLOW.toRGB(), Color.CYAN.toRGB(), Color.MAGENTA.toRGB(),
                                           new Color(1, 0.5f, 0).toRGB(), new Color(0.5f, 1, 0).toRGB()};
   
    private final ImageObserver observer;
    private final BufferedImage image;

    private BufferedImageDisplay(BufferedImage image, ImageObserver observer) {
      this.observer = observer;
      this.image = image;
    }

    public synchronized void imageBegin(int width, int height, int bucketSize) {
      for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
          int rgba = this.image.getRGB(x, y);
          this.image.setRGB(x, y, ((rgba & 0xFEFEFEFE) >>> 1) + ((rgba & 0xFCFCFCFC) >>> 2));
        }
      }
      notifyObserver(ImageObserver.FRAMEBITS | BASE_INFO_FLAGS, 0, 0, width, height);
    }

    public synchronized void imagePrepare(int x, int y, int width, int height, int id) {
      int border = BORDERS [id % BORDERS.length] | 0xFF000000;
      for (int by = 0; by < height; by++) {
        for (int bx = 0; bx < width; bx++) {
          if (bx < 2 || bx > width - 3) {
            if (5 * by < height || 5 * (height - by - 1) < height) {
              this.image.setRGB(x + bx, y + by, border);
            }
          } else if (by < 2 || by > height - 3) {
            if (5 * bx < width || 5 * (width - bx - 1) < width) {
              this.image.setRGB(x + bx, y + by, border);
            }
          }
        }
      }
      notifyObserver(ImageObserver.SOMEBITS | BASE_INFO_FLAGS, x, y, width, height);
    }

    public synchronized void imageUpdate(int x, int y, int width, int height, Color [] data, float [] alpha) {
      for (int j = 0, index = 0; j < height; j++) {
        for (int i = 0; i < width; i++, index++) {
          this.image.setRGB(x + i, y + j,
              data [index].copy().mul(1.0f / alpha [index]).toNonLinear().toRGBA(alpha [index]));
        }
      }
      notifyObserver(ImageObserver.SOMEBITS | BASE_INFO_FLAGS, x, y, width, height);
    }

    public synchronized void imageFill(int x, int y, int width, int height, Color c, float alpha) {
      int rgba = c.copy().mul(1.0f / alpha).toNonLinear().toRGBA(alpha);
      for (int j = 0, index = 0; j < height; j++) {
        for (int i = 0; i < width; i++, index++) {
          this.image.setRGB(x + i, y + j, rgba);
        }
      }
      notifyObserver(ImageObserver.SOMEBITS | BASE_INFO_FLAGS, x, y, width, height);
    }

    public void imageEnd() {
      notifyObserver(ImageObserver.FRAMEBITS | BASE_INFO_FLAGS,
            0, 0, this.image.getWidth(), this.image.getHeight());
    }

    private void notifyObserver(final int flags, final int x, final int y, final int width, final int height) {
      if (observer != null) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
              observer.imageUpdate(image, flags, x, y, width, height);
            }
          });
      }
    }
  }

  /**
   * A SunFlow sphere light with no representation.
   */
  public static class SphereLightWithNoRepresentation extends SphereLight {
    public Instance createInstance() {
      return null;
    }
  }
 
  /**
   * A triangle used to remove faces cited for that once (opposite faces included).
   */
  private static class Triangle {
    private float [] point1;
    private float [] point2;
    private float [] point3;
    private int      hashCode;
    private boolean  hashCodeSet;
   
    public Triangle(float [] vertices, int index1, int index2, int index3) {
      this.point1 = new float [] {vertices [index1 * 3], vertices [index1 * 3 + 1], vertices [index1 * 3 + 2]};
      this.point2 = new float [] {vertices [index2 * 3], vertices [index2 * 3 + 1], vertices [index2 * 3 + 2]};
      this.point3 = new float [] {vertices [index3 * 3], vertices [index3 * 3 + 1], vertices [index3 * 3 + 2]};
    }

    @Override
    public int hashCode() {
      if (!this.hashCodeSet) {
        this.hashCode = 31 * Arrays.hashCode(this.point1)
            + 31 * Arrays.hashCode(this.point2)
            + 31 * Arrays.hashCode(this.point3);
        this.hashCodeSet = true;
      }
      return this.hashCode;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      } else if (obj instanceof Triangle) {
        Triangle triangle = (Triangle)obj;
        // Compare first with point with opposite face
        return Arrays.equals(this.point1, triangle.point3)
               && Arrays.equals(this.point2, triangle.point2)
               && Arrays.equals(this.point3, triangle.point1)
            || Arrays.equals(this.point1, triangle.point2)
               && Arrays.equals(this.point2, triangle.point1)
               && Arrays.equals(this.point3, triangle.point3)
            || Arrays.equals(this.point1, triangle.point1)
               && Arrays.equals(this.point2, triangle.point3)
               && Arrays.equals(this.point3, triangle.point2)
            || Arrays.equals(this.point1, triangle.point1)
               && Arrays.equals(this.point2, triangle.point2)
               && Arrays.equals(this.point3, triangle.point3);
      }
      return false;
    }
  }
 
  /**
   * A key used to manage textures at different levels of transparency.
   */
  private static class TransparentTextureKey {
    private Texture texture;
    private float   transparency;

    public TransparentTextureKey(Texture texture, float transparency) {
      this.texture = texture;
      this.transparency = transparency;
    }
   
    @Override
    public boolean equals(Object obj) {
      return ((TransparentTextureKey)obj).texture.equals(this.texture)
          && ((TransparentTextureKey)obj).transparency == this.transparency;
    }
   
    @Override
    public int hashCode() {
      return this.texture.hashCode() + Float.floatToIntBits(this.transparency);
    }
  }
}
TOP

Related Classes of com.eteks.sweethome3d.j3d.PhotoRenderer$Triangle

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.