package org.osm2world.core.target.obj; import static java.awt.Color.WHITE; import static java.lang.Math.max; import static java.util.Collections.nCopies; import static org.osm2world.core.target.common.material.Material.multiplyColor; import java.awt.Color; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.osm2world.core.map_data.data.MapArea; import org.osm2world.core.map_data.data.MapElement; import org.osm2world.core.map_data.data.MapNode; import org.osm2world.core.map_data.data.MapWaySegment; 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.osm.data.OSMElement; import org.osm2world.core.target.common.FaceTarget; import org.osm2world.core.target.common.TextureData; import org.osm2world.core.target.common.material.Material; import org.osm2world.core.target.common.material.Materials; import org.osm2world.core.world.data.WorldObject; public class ObjTarget extends FaceTarget<RenderableToObj> { private final PrintStream objStream; private final PrintStream mtlStream; private final Map<VectorXYZ, Integer> vertexIndexMap = new HashMap<VectorXYZ, Integer>(); private final Map<VectorXYZ, Integer> normalsIndexMap = new HashMap<VectorXYZ, Integer>(); private final Map<VectorXZ, Integer> texCoordsIndexMap = new HashMap<VectorXZ, Integer>(); private final Map<Material, String> materialMap = new HashMap<Material, String>(); private Class<? extends WorldObject> currentWOGroup = null; private int anonymousWOCounter = 0; private Material currentMaterial = null; private int currentMaterialLayer = 0; private static int anonymousMaterialCounter = 0; // this is approximatly one millimeter private static final double SMALL_OFFSET = 1e-3; public ObjTarget(PrintStream objStream, PrintStream mtlStream) { this.objStream = objStream; this.mtlStream = mtlStream; } @Override public Class<RenderableToObj> getRenderableType() { return RenderableToObj.class; } @Override public void render(RenderableToObj renderable) { renderable.renderTo(this); } @Override public boolean reconstructFaces() { return config != null && config.getBoolean("reconstructFaces", false); } @Override public void beginObject(WorldObject object) { if (object == null) { currentWOGroup = null; objStream.println("g null"); objStream.println("o null"); } else { /* maybe start a group depending on the object's class */ if (!object.getClass().equals(currentWOGroup)) { currentWOGroup = object.getClass(); objStream.println("g " + currentWOGroup.getSimpleName()); } /* start an object with the object's class * and the underlying OSM element's name/ref tags */ MapElement element = object.getPrimaryMapElement(); OSMElement osmElement; if (element instanceof MapNode) { osmElement = ((MapNode) element).getOsmNode(); } else if (element instanceof MapWaySegment) { osmElement = ((MapWaySegment) element).getOsmWay(); } else if (element instanceof MapArea) { osmElement = ((MapArea) element).getOsmObject(); } else { osmElement = null; } if (osmElement != null && osmElement.tags.containsKey("name")) { objStream.println("o " + object.getClass().getSimpleName() + " " + osmElement.tags.getValue("name")); } else if (osmElement != null && osmElement.tags.containsKey("ref")) { objStream.println("o " + object.getClass().getSimpleName() + " " + osmElement.tags.getValue("ref")); } else { objStream.println("o " + object.getClass().getSimpleName() + anonymousWOCounter ++); } } } @Override public void drawFace(Material material, List<VectorXYZ> vs, List<VectorXYZ> normals, List<List<VectorXZ>> texCoordLists) { int[] normalIndices = null; if (normals != null) { normalIndices = normalsToIndices(normals); } VectorXYZ faceNormal = new TriangleXYZ(vs.get(0), vs.get(1), vs.get(2)).getNormal(); for (int layer = 0; layer < max(1, material.getNumTextureLayers()); layer++) { useMaterial(material, layer); int[] texCoordIndices = null; if (texCoordLists != null && !texCoordLists.isEmpty()) { texCoordIndices = texCoordsToIndices(texCoordLists.get(layer)); } writeFace(verticesToIndices((layer == 0)? vs : offsetVertices(vs, nCopies(vs.size(), faceNormal), layer * SMALL_OFFSET)), normalIndices, texCoordIndices); } } @Override public void drawTrianglesWithNormals(Material material, Collection<? extends TriangleXYZWithNormals> triangles, List<List<VectorXZ>> texCoordLists) { for (int layer = 0; layer < max(1, material.getNumTextureLayers()); layer++) { useMaterial(material, layer); int triangleNumber = 0; for (TriangleXYZWithNormals t : triangles) { int[] texCoordIndices = null; if (texCoordLists != null && !texCoordLists.isEmpty()) { List<VectorXZ> texCoords = texCoordLists.get(layer); texCoordIndices = texCoordsToIndices( texCoords.subList(3*triangleNumber, 3*triangleNumber + 3)); } writeFace(verticesToIndices((layer == 0)? t.getVertices() : offsetVertices(t.getVertices(), t.getNormals(), layer * SMALL_OFFSET)), normalsToIndices(t.getNormals()), texCoordIndices); triangleNumber ++; } } } private void useMaterial(Material material, int layer) { if (!material.equals(currentMaterial) || (layer != currentMaterialLayer)) { String name = materialMap.get(material); if (name == null) { name = Materials.getUniqueName(material); if (name == null) { name = "MAT_" + anonymousMaterialCounter; anonymousMaterialCounter += 1; } materialMap.put(material, name); writeMaterial(material, name); } objStream.println("usemtl " + name + "_" + layer); currentMaterial = material; currentMaterialLayer = layer; } } private List<? extends VectorXYZ> offsetVertices(List<? extends VectorXYZ> vs, List<VectorXYZ> directions, double offset) { List<VectorXYZ> result = new ArrayList<VectorXYZ>(vs.size()); for (int i = 0; i < vs.size(); i++) { result.add(vs.get(i).add(directions.get(i).mult(offset))); } return result; } private int[] verticesToIndices(List<? extends VectorXYZ> vs) { return vectorsToIndices(vertexIndexMap, "v ", vs); } private int[] normalsToIndices(List<? extends VectorXYZ> normals) { return vectorsToIndices(normalsIndexMap, "vn ", normals); } private int[] texCoordsToIndices(List<VectorXZ> texCoords) { return vectorsToIndices(texCoordsIndexMap, "vt ", texCoords); } private <V> int[] vectorsToIndices(Map<V, Integer> indexMap, String objLineStart, List<? extends V> vectors) { int[] indices = new int[vectors.size()]; for (int i=0; i<vectors.size(); i++) { final V v = vectors.get(i); Integer index = indexMap.get(v); if (index == null) { index = indexMap.size(); objStream.println(objLineStart + " " + formatVector(v)); indexMap.put(v, index); } indices[i] = index; } return indices; } private String formatVector(Object v) { if (v instanceof VectorXYZ) { VectorXYZ vXYZ = (VectorXYZ)v; return vXYZ.x + " " + vXYZ.y + " " + (-vXYZ.z); } else { VectorXZ vXZ = (VectorXZ)v; return vXZ.x + " " + vXZ.z; } } private void writeFace(int[] vertexIndices, int[] normalIndices, int[] texCoordIndices) { assert normalIndices == null || vertexIndices.length == normalIndices.length; objStream.print("f"); for (int i = 0; i < vertexIndices.length; i++) { objStream.print(" " + (vertexIndices[i]+1)); if (texCoordIndices != null && normalIndices == null) { objStream.print("/" + (texCoordIndices[i]+1)); } else if (texCoordIndices == null && normalIndices != null) { objStream.print("//" + (normalIndices[i]+1)); } else if (texCoordIndices != null && normalIndices != null) { objStream.print("/" + (texCoordIndices[i]+1) + "/" + (normalIndices[i]+1)); } } objStream.println(); } private void writeMaterial(Material material, String name) { for (int i = 0; i < max(1, material.getNumTextureLayers()); i++) { TextureData textureData = null; if (material.getNumTextureLayers() > 0) { textureData = material.getTextureDataList().get(i); } mtlStream.println("newmtl " + name + "_" + i); if (textureData == null || textureData.colorable) { writeColorLine("Ka", material.ambientColor()); writeColorLine("Kd", material.diffuseColor()); //Ks //Ns } else { writeColorLine("Ka", multiplyColor(WHITE, material.getAmbientFactor())); writeColorLine("Kd", multiplyColor(WHITE, 1 - material.getAmbientFactor())); //Ks //Ns } if (textureData != null) { mtlStream.println("map_Ka " + textureData.file); mtlStream.println("map_Kd " + textureData.file); } mtlStream.println(); } } private void writeColorLine(String lineStart, Color color) { mtlStream.println(lineStart + " " + color.getRed() / 255f + " " + color.getGreen() / 255f + " " + color.getBlue() / 255f); } }