package org.osm2world.core.target.povray; import java.awt.Color; import java.io.PrintStream; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.osm2world.core.math.TriangleXYZ; import org.osm2world.core.math.TriangleXYZWithNormals; import org.osm2world.core.math.VectorXYZ; import org.osm2world.core.math.VectorXZ; import org.osm2world.core.target.common.AbstractTarget; import org.osm2world.core.target.common.TextureData; import org.osm2world.core.target.common.material.Material; import org.osm2world.core.target.common.material.Materials; public class POVRayTarget extends AbstractTarget<RenderableToPOVRay> { private static final String INDENT = " "; // this is approximatly one millimeter private static final double SMALL_OFFSET = 1e-3; private final PrintStream output; private Map<TextureData, String> textureNames = new HashMap<TextureData, String>(); public POVRayTarget(PrintStream output) { this.output = output; } @Override public Class<RenderableToPOVRay> getRenderableType() { return RenderableToPOVRay.class; } @Override public void render(RenderableToPOVRay renderable) { renderable.renderTo(this); } // int openBrackets = 0; // // /** // * appends indentation based on {@link #INDENT} and {@link #openBrackets} // */ // private void appendIndent() { // for (int i=0; i<openBrackets; i++) { // append(INDENT); // } // } /** * provides direct write access to the generated source code. * This is intended for Renderables using special POVRay features. */ public void append(String code) { output.print(code); // if (code.contains("union") && openBrackets > 0) { // System.out.println(openBrackets); // } // for (int i=0; i<code.length(); i++) { // char c = code.charAt(i); // if (c == '{') { // openBrackets++; // } else if (c == '}') { // openBrackets--; // } // } } public void append(int value) { output.print(value); } public void append(double value) { output.print(value); } // private final LinkedList<StringBuilder> stack = new LinkedList<StringBuilder>(); // // int openBrackets = 0; // // public void append(int value) { // stack.peek().append(value); // } // // public void append(double value) { // stack.peek().append(value); // } // // public void startBlock(String s) { // StringBuilder newBlock = new StringBuilder(s + "{"); // stack.push(newBlock); // } // // public void endBlock(String block) { // StringBuilder closedBlock = stack.poll(); // if (stack.isEmpty()) { // output.append(closedBlock); // } else { // stack.peek().append(closedBlock); // } // } public void appendDefaultParameterValue(String name, String value) { append("#ifndef (" + name + ")\n"); append("#declare " + name + " = " + value); append("\n#end\n\n"); } public void appendMaterialDefinitions() { for (Material material : Materials.getMaterials()) { String uniqueName = Materials.getUniqueName(material); String name = "texture_" + uniqueName; append("#ifndef (" + name + ")\n"); append("#declare " + name + " = "); appendMaterial(material); append("#end\n\n"); if (material.getNumTextureLayers() == 1) { TextureData td = material.getTextureDataList().get(0); if (!td.colorable) { textureNames.put(td, uniqueName); } } } } @Override public void drawTriangles(Material material, Collection<? extends TriangleXYZ> triangles, List<List<VectorXZ>> texCoordLists) { if (!checkMeshValidity(triangles)) return; for (TriangleXYZ triangle : triangles) { performNaNCheck(triangle); } if (material.getNumTextureLayers() > 1) { int count = 0; for (TextureData textureData : material.getTextureDataList()) { append("mesh {\n"); drawTriangleMesh(triangles, texCoordLists.get(count), count); append(" uv_mapping "); appendMaterial(material, textureData); if (count > 0) append(" no_shadow"); append("}\n"); count++; } } else { append("mesh {\n"); if (texCoordLists.size() > 0) { drawTriangleMesh(triangles, texCoordLists.get(0), 0); } else { for (TriangleXYZ triangle : triangles) { append(INDENT); appendTriangle(triangle.v1, triangle.v2, triangle.v3); } } append(" uv_mapping "); appendMaterialOrName(material); append("}\n"); } } @Override public void drawTrianglesWithNormals(Material material, Collection<? extends TriangleXYZWithNormals> triangles, List<List<VectorXZ>> texCoordLists) { if (!checkMeshValidity(triangles)) return; if (material.getNumTextureLayers() > 1) { int count = 0; for (TextureData textureData : material.getTextureDataList()) { append("mesh {\n"); drawTriangleNormalMesh(triangles, texCoordLists.get(count), count); append(" uv_mapping "); appendMaterial(material, textureData); if (count > 0) append(" no_shadow"); append("}\n"); count++; } } else { append("mesh {\n"); if (texCoordLists.size() > 0) { drawTriangleNormalMesh(triangles, texCoordLists.get(0), 0); } else { for (TriangleXYZWithNormals triangle : triangles) { append(INDENT); appendTriangle(triangle.v1, triangle.v2, triangle.v3, triangle.n1, triangle.n2, triangle.n3, true); } } append(" uv_mapping "); appendMaterialOrName(material); append("}\n"); } } private void drawTriangleNormalMesh(Collection<? extends TriangleXYZWithNormals> triangles, List<VectorXZ> texCoordList, int depth) { Iterator<? extends TriangleXYZWithNormals> itr1 = triangles.iterator(); Iterator<VectorXZ> itr2 = texCoordList.iterator(); while (itr1.hasNext()) { TriangleXYZWithNormals triangle = itr1.next(); VectorXYZ n1 = triangle.n1; VectorXYZ n2 = triangle.n2; VectorXYZ n3 = triangle.n3; VectorXZ tex1 = itr2.next(); VectorXZ tex2 = itr2.next(); VectorXZ tex3 = itr2.next(); append(INDENT); if (depth > 0) { VectorXYZ d1 = n1.mult(depth*SMALL_OFFSET); VectorXYZ d2 = n2.mult(depth*SMALL_OFFSET); VectorXYZ d3 = n3.mult(depth*SMALL_OFFSET); appendTriangle( triangle.v1.add(d1), triangle.v2.add(d2), triangle.v3.add(d3), n1, n2, n3, tex1, tex2, tex3, false, true); } else { appendTriangle(triangle.v1, triangle.v2, triangle.v3, null, null, null, tex1, tex2, tex3, false, true); } } } private void drawTriangleMesh(Collection<? extends TriangleXYZ> triangles, List<VectorXZ> texCoordList, int depth) { Iterator<? extends TriangleXYZ> itr1 = triangles.iterator(); Iterator<VectorXZ> itr2 = texCoordList.iterator(); while (itr1.hasNext()) { TriangleXYZ triangle = itr1.next(); VectorXYZ normal = triangle.getNormal(); VectorXZ tex1 = itr2.next(); VectorXZ tex2 = itr2.next(); VectorXZ tex3 = itr2.next(); append(INDENT); if (depth > 0) { normal = normal.mult(depth*SMALL_OFFSET); appendTriangle( triangle.v1.add(normal), triangle.v2.add(normal), triangle.v3.add(normal), null, null, null, tex1, tex2, tex3, false, true); } else { appendTriangle(triangle.v1, triangle.v2, triangle.v3, null, null, null, tex1, tex2, tex3, false, true); } } } // @Override // public void drawTriangleStrip(Material material, VectorXYZ... vectors) { // // for (VectorXYZ vector : vectors) { // performNaNCheck(vector); // } // // append("union {\n"); // // for (int triangle = 0; triangle + 2 < vectors.length; triangle++) { // // append(INDENT); // // appendTriangle( // vectors[triangle], // vectors[triangle + 1], // vectors[triangle + 2]); // // } // // appendMaterial(material); // // append("}\n"); // // } // @Override // public void drawTriangleFan(Material material, List<? extends VectorXYZ> vs) { // for (VectorXYZ vector : vs) { // performNaNCheck(vector); // } // // append("union {\n"); // // VectorXYZ center = vs.get(0); // // for (int triangle = 0; triangle + 2 < vs.size(); triangle ++) { // // append(INDENT); // // appendTriangle( // center, // vs.get(triangle + 1), // vs.get(triangle + 2)); // // } // // appendMaterial(material); // // append("}\n"); // // } @Override public void drawConvexPolygon(Material material, List<VectorXYZ> vs, List<List<VectorXZ>> texCoordLists) { for (VectorXYZ vector : vs) { performNaNCheck(vector); } append("polygon {\n "); append(vs.size()); append(", "); for (VectorXYZ v : vs) { appendVector(v); } appendMaterialOrName(material); append("}\n"); } @Override public void drawColumn(Material material, Integer corners, VectorXYZ base, double height, double radiusBottom, double radiusTop, boolean drawBottom, boolean drawTop) { performNaNCheck(base); if (height <= 0) return; if (corners == null) { if (radiusBottom == radiusTop) { // cylinder append("cylinder {\n "); appendVector(base); append(", "); appendVector(base.y(base.y + height)); append(", "); append(radiusTop); } else { // (truncated) cone append("cone {\n "); appendVector(base); append(", "); append(radiusBottom); append(", "); appendVector(base.y(base.y + height)); append(", "); append(radiusTop); } if (!drawBottom && !drawTop) { // TODO: incorrect if only one is false append(" open"); } appendMaterialOrName(material); append("}\n"); } else { // not round super.drawColumn(material, corners, base, height, radiusBottom, radiusTop, drawBottom, drawTop); } } /** * variant of {@link #drawColumn(Material, Integer, VectorXYZ, double, double, double, boolean, boolean)} * that allows arbitrarily placed columns */ public void drawColumn(Material material, Integer corners, VectorXYZ base, VectorXYZ cap, double radiusBottom, double radiusTop, boolean drawBottom, boolean drawTop) { performNaNCheck(base); performNaNCheck(cap); if (cap.equals(base)) return; if (corners == null) { if (radiusBottom == radiusTop) { // cylinder append("cylinder {\n "); appendVector(base); append(", "); appendVector(cap); append(", "); append(radiusTop); } else { // (truncated) cone append("cone {\n "); appendVector(base); append(", "); append(radiusBottom); append(", "); appendVector(cap); append(", "); append(radiusTop); } if (!drawBottom && !drawTop) { // TODO: incorrect if only one is false append(" open"); } } else { // not round throw new UnsupportedOperationException( "drawing non-round columns isn't implemented yet"); } appendMaterialOrName(material); append("}\n"); } private boolean checkMeshValidity(Collection<? extends TriangleXYZ> triangles) { if (triangles.size() == 0) return false; boolean result = false; for (TriangleXYZ triangle : triangles) { result |= !isDegenerated(triangle); } return result; } private boolean isDegenerated(TriangleXYZ triangle) { VectorXYZ a = triangle.v1; VectorXYZ b = triangle.v2; VectorXYZ c = triangle.v3; if (a.equals(b) || a.equals(c) || b.equals(c)) { return true; } else if (a.x == b.x && b.x == c.x && a.y == b.y && b.y == c.y) { return true; } else if (a.x == b.x && b.x == c.x && a.z == b.z && b.z == c.z) { return true; } else if (a.y == b.y && b.y == c.y && a.z == b.z && b.z == c.z) { return true; } return false; } public void appendTriangle(VectorXYZ a, VectorXYZ b, VectorXYZ c) { appendTriangle(a, b, c, null, null, null, false); } public void appendTriangle( VectorXYZ a, VectorXYZ b, VectorXYZ c, VectorXYZ na, VectorXYZ nb, VectorXYZ nc, boolean smooth) { appendTriangle(a, b, c, na, nb, nc, null, null, null, smooth, false); } public void appendTriangle( VectorXYZ a, VectorXYZ b, VectorXYZ c, VectorXYZ na, VectorXYZ nb, VectorXYZ nc, VectorXZ ta, VectorXZ tb, VectorXZ tc, boolean smooth, boolean texture) { // append the triangle if (smooth) append("smooth_"); append("triangle { "); appendVector(a); if (smooth) { append(", "); appendVector(na); } append(", "); appendVector(b); if (smooth) { append(", "); appendVector(nb); } append(", "); appendVector(c); if (smooth) { append(", "); appendVector(nc); } if (texture) { /* append(" uv_vectors "); appendInverseVector(ta); append(", "); appendInverseVector(tb); append(", "); appendInverseVector(tc); */ append(" uv_vectors "); appendVector(ta); append(", "); appendVector(tb); append(", "); appendVector(tc); } append("}\n"); } /** * adds a color. Syntax is "color rgb <x, y, z>". */ public void appendRGBColor(Color color) { append("color rgb "); appendVector( color.getRed()/255f, color.getGreen()/255f, color.getBlue()/255f); } public void appendMaterialOrName(Material material) { String materialName = Materials.getUniqueName(material); if (materialName != null) { append(" texture { texture_" + materialName + " }"); } else { appendMaterial(material); } } private void appendMaterial(Material material) { if (material.getNumTextureLayers() == 0) { append(" texture {\n"); append(" pigment { "); appendRGBColor(material.getColor()); append(" }\n finish {\n"); append(" ambient " + material.getAmbientFactor() + "\n"); append(" diffuse " + material.getDiffuseFactor() + "\n"); append(" }\n"); append(" }\n"); } else { for (TextureData textureData : material.getTextureDataList()) { appendMaterial(material, textureData); } } } private void appendMaterial(Material material, TextureData textureData) { String textureName = textureNames.get(textureData); if (textureName == null) { if (textureData.colorable) { append(" texture {\n"); append(" pigment { "); appendRGBColor(material.getColor()); append(" }\n finish {\n"); append(" ambient " + material.getAmbientFactor() + "\n"); append(" diffuse " + material.getDiffuseFactor() + "\n"); append(" }\n"); append(" }\n"); } append(" texture {\n"); append(" pigment { "); appendImageMap(textureData); append(" }\n finish {\n"); append(" ambient " + material.getAmbientFactor() + "\n"); append(" diffuse " + material.getDiffuseFactor() + "\n"); append(" }\n"); append(" }\n"); } else { append(" texture { texture_" + textureName + "}"); } } private void appendImageMap(TextureData textureData) { append(" image_map {\n"); if (textureData.file.getName().toLowerCase().endsWith("png")) { append(" png \"" + textureData.file + "\"\n"); } else { append(" jpeg \"" + textureData.file + "\"\n"); } if (textureData.colorable) { append(" filter all 1.0\n"); } append("\n }"); } /** * adds a vector to the String built by a StringBuilder. * Syntax is "<x, y, z>". */ public void appendVector(float x, float y, float z) { if (Float.isNaN(x) || Float.isNaN(y) || Float.isNaN(z)) { throw new IllegalArgumentException("NaN vector " + x + ", " + y + ", " + z); } append("<"); append(x); append(", "); append(y); append(", "); append(z); append(">"); } public void appendVector(double x, double y, double z) { if (Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z)) { throw new IllegalArgumentException("NaN vector " + x + ", " + y + ", " + z); } append("<"); append(x); append(", "); append(y); append(", "); append(z); append(">"); } /** * alternative to {@link #appendVector(double, double)} * using a vector object as parameter instead of individual coordinates */ public void appendVector(VectorXYZ vector) { appendVector(vector.getX(), vector.getY(), vector.getZ()); } /** * adds a vector to the String built by a StringBuilder. * Syntax is "<v1, v2>". */ public void appendVector(double x, double z) { append("<"); append(x); append(", "); append(z); append(">"); } /** * alternative to {@link #appendVector(double, double)} * using a vector object as parameter instead of individual coordinates */ public void appendVector(VectorXZ vector) { appendVector(vector.x, vector.z); } /** * append a vector with inverted coordinates */ public void appendInverseVector(VectorXZ vector) { appendVector(-vector.x, -vector.z); } /** * * @param vs polygon vertices; first and last should be equal */ public void appendPolygon(VectorXYZ... vs) { assert !vs[0].equals(vs[vs.length-1]) : "polygon not closed"; append("polygon {\n "); append(vs.length); append(", "); for (VectorXYZ v : vs) { appendVector(v); } append("}\n"); } public void appendPrism(float y1, float y2, VectorXZ... vs) { append("prism {\n "); append(y1); append(", "); append(y2); append(", "); append(vs.length); append(",\n "); for (VectorXZ v : vs) { appendVector(v); } append("\n}"); } //TODO: avoid having to do this private void performNaNCheck(TriangleXYZWithNormals triangle) { performNaNCheck(triangle.v1); performNaNCheck(triangle.v2); performNaNCheck(triangle.v3); performNaNCheck(triangle.n1); performNaNCheck(triangle.n2); performNaNCheck(triangle.n3); } private void performNaNCheck(TriangleXYZ triangle) { performNaNCheck(triangle.v1); performNaNCheck(triangle.v2); performNaNCheck(triangle.v3); } private void performNaNCheck(VectorXYZ v) { if (Double.isNaN(v.x) || Double.isNaN(v.y) || Double.isNaN(v.z)) { throw new IllegalArgumentException("NaN vector " + v.x + ", " + v.y + ", " + v.z); } } }