/* * 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.ResourceBundle; 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.HomeEnvironment; 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.Level; 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 useSunskyLight; 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 with top camera return new Room3D((Room)item, home, !(home.getCamera() instanceof ObserverCamera), 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.useSunskyLight = !(home.getCamera() instanceof ObserverCamera); // SunFlow produce too much white spots when silk shader is used with sun sky light // so use this shader only when observer is used boolean silk = !this.useSunskyLight && quality == Quality.HIGH; String shininessShader = getRenderingParameterValue("shininessShader"); if ("glossy".equals(shininessShader)) { silk = false; } else if ("silk".equals(shininessShader)) { silk = true; } // Export to SunFlow the Java 3D shapes and appearance of the ground, the walls, the furniture and the rooms HomeEnvironment homeEnvironment = home.getEnvironment(); float subpartSize = homeEnvironment.getSubpartSizeUnderLight(); // Dividing walls and rooms surface in subparts is useless homeEnvironment.setSubpartSizeUnderLight(0); for (Wall wall : home.getWalls()) { exportNode((Node)object3DFactory.createObject3D(home, wall, true), true, silk); } for (HomePieceOfFurniture piece : home.getFurniture()) { exportNode((Node)object3DFactory.createObject3D(home, piece, true), false, silk); } for (Room room : home.getRooms()) { exportNode((Node)object3DFactory.createObject3D(home, room, true), true, silk); } // Create a 3D ground large enough to join the sky at the horizon Ground3D ground = new Ground3D(home, -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, silk); homeEnvironment.setSubpartSizeUnderLight(subpartSize); HomeTexture skyTexture = homeEnvironment.getSkyTexture(); this.useSunSky = skyTexture == null || this.useSunskyLight; 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 = homeEnvironment.getCeillingLightColor(); this.homeLightColor = homeEnvironment.getLightColor(); if (ceillingLightColor > 0) { // Add lights at the top of each room for (Room room : home.getRooms()) { Level roomLevel = room.getLevel(); if (room.isCeilingVisible() && (roomLevel == null || roomLevel.isVisible())) { float xCenter = room.getXCenter(); float yCenter = room.getYCenter(); double smallestDistance = Float.POSITIVE_INFINITY; float roomElevation = roomLevel != null ? roomLevel.getElevation() : 0; float roomHeight = roomElevation + (roomLevel == null ? home.getWallHeight() : roomLevel.getHeight()); List<Level> levels = home.getLevels(); if (roomLevel == null || levels.indexOf(roomLevel) == levels.size() - 1) { // Search the height of the wall closest to the point xCenter, yCenter for (Wall wall : home.getWalls()) { if (wall.isAtLevel(roomLevel)) { float wallElevation = wall.getLevel() == null ? 0 : wall.getLevel().getElevation(); 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 == points.length - 1) { // Wall start roomHeight = wallHeightAtStart != null ? wallHeightAtStart : home.getWallHeight(); } else { // Wall end roomHeight = wall.isTrapezoidal() ? wall.getHeightAtEnd() : (wallHeightAtStart != null ? wallHeightAtStart : home.getWallHeight()); } roomHeight += wallElevation; } } } } } 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(); Level level = light.getLevel(); if (light.isVisible() && lightPower > 0f && (level == null || level.isVisible())) { 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()); float lightElevation = light.getGroundElevation(); this.sunflow.parameter("center", new Point3(light.getX() + xLightSourceInLight * cos - yLightSourceInLight * sin, lightElevation + (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", Integer.parseInt(getRenderingParameterValue("diffusedBounces"))); this.sunflow.parameter("depths.reflection", 4); this.sunflow.parameter("depths.refraction", 16); this.sunflow.options(SunflowAPI.DEFAULT_OPTIONS); Integer causticsEmit = new Integer(getRenderingParameterValue("causticsPhotons")); if (causticsEmit > 0) { this.sunflow.parameter("caustics.emit", causticsEmit); this.sunflow.parameter("caustics", "kd"); this.sunflow.parameter("caustics.gather", 64); this.sunflow.parameter("caustics.radius", 0.5f); 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); } // Possible values: default, path String globalIllumination = getRenderingParameterValue("globalIllumination"); 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.useSunskyLight ? 12 : 0); 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.useSunskyLight ? 10 : 40; 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.useSunskyLight && "default".equals(globalIllumination)) { 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); } } if ("path".equals(globalIllumination)) { this.sunflow.parameter("gi.engine", "path"); this.sunflow.parameter("gi.path.samples", 64); 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", new Float(getRenderingParameterValue("normalLens.focusDistance"))); this.sunflow.parameter("lens.radius", new Float(getRenderingParameterValue("normalLens.radius"))); 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()); int antiAliasingMin = Integer.parseInt(getRenderingParameterValue("antiAliasing.min")); int antiAliasingMax = Integer.parseInt(getRenderingParameterValue("antiAliasing.max")); this.sunflow.parameter("filter", antiAliasingMax > 0 ? "blackman-harris" : "box"); this.sunflow.parameter("aa.min", antiAliasingMin); this.sunflow.parameter("aa.max", antiAliasingMax); 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 the value of the given rendering parameter. */ private String getRenderingParameterValue(String parameterName) { // Try to retrieve overridden parameter value from System property // (for example: System property com.eteks.sweethome3d.j3d.PhotoRenderer.lowQuality.antiAliasing.min) String prefixedParameter = this.quality.name().toLowerCase() + "Quality." + parameterName; String baseName = PhotoRenderer.class.getName(); String value = System.getProperty(baseName + '.' + prefixedParameter); if (value != null) { return value; } else { // Return default value stored in properties resource file // (for example: property lowQuality.antiAliasing.min // in com/eteks/sweethome3d/j3d/PhotoRenderer.properties file) return ResourceBundle.getBundle(baseName).getString(prefixedParameter); } } /** * 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 with SunFlow API. */ private void exportNode(Node node, boolean ignoreTransparency, boolean silk) throws IOException { exportNode(node, ignoreTransparency, silk, new Transform3D()); } /** * Exports all the 3D shapes children of <code>node</code> with SunFlow API. */ private void exportNode(Node node, boolean ignoreTransparency, 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, silk, parentTransformations); } } else if (node instanceof Link) { exportNode(((Link)node).getSharedGroup(), ignoreTransparency, 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, 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 with 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 according to 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 according to 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 uses 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 + planeS.w, x * planeT.x + y * planeT.y + z * planeT.z + planeT.w); } /** * 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 and vertexIndex2. */ 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; } /** * Exports a Java3D appearance as a SunFlow shader. */ private void exportAppearance(Appearance appearance, String appearanceName, boolean mirror, boolean ignoreTransparency, 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()) { 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); 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; j < height; j++) { for (int i = 0; i < width; i++) { 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 more 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); } } }