diff --git a/build-adds/example_config.properties b/build-adds/example_config.properties index e94a02165..a9a90bd81 100644 --- a/build-adds/example_config.properties +++ b/build-adds/example_config.properties @@ -48,3 +48,6 @@ forceUnbufferedPNGRendering = false # If this is lower than the width or height of the requested png, performance suffers. # Increase it if your graphics hardware is capable of handling larger sizes. canvasLimit = 1024 + +# download external obj models for OSM objects with model:url +useExternalModels = true diff --git a/build-adds/osm2world.sh b/build-adds/osm2world.sh index 74e2d6cf0..6b5460468 100755 --- a/build-adds/osm2world.sh +++ b/build-adds/osm2world.sh @@ -11,6 +11,7 @@ if [[ $1 == --vm-params=* ]] fi # choose path for the native JOGL libs depending on system and java version +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" MACHINE_TYPE=`uname -m` KERNEL_NAME=`uname -s` @@ -27,6 +28,6 @@ fi # run OSM2World -export LD_LIBRARY_PATH=$lpsolvepath +export LD_LIBRARY_PATH=$script_dir/$lpsolvepath -java -Djava.library.path=$lpsolvepath $vmparams -jar OSM2World.jar --config texture_config.properties $@ +java -Djava.library.path=$lpsolvepath $vmparams -jar $script_dir/OSM2World.jar --config texture_config.properties $@ diff --git a/src/org/osm2world/core/ConversionFacade.java b/src/org/osm2world/core/ConversionFacade.java index 113f7cb0e..be7b53d71 100644 --- a/src/org/osm2world/core/ConversionFacade.java +++ b/src/org/osm2world/core/ConversionFacade.java @@ -46,6 +46,7 @@ import org.osm2world.core.world.modules.BridgeModule; import org.osm2world.core.world.modules.BuildingModule; import org.osm2world.core.world.modules.CliffModule; +import org.osm2world.core.world.modules.ExternalModelModule; import org.osm2world.core.world.modules.GolfModule; import org.osm2world.core.world.modules.InvisibleModule; import org.osm2world.core.world.modules.ParkingModule; @@ -130,6 +131,7 @@ public Collection getRenderables( private static final List createDefaultModuleList() { return Arrays.asList((WorldModule) + new ExternalModelModule(), new RoadModule(), new RailwayModule(), new BuildingModule(), diff --git a/src/org/osm2world/core/map_data/creation/OSMToMapDataConverter.java b/src/org/osm2world/core/map_data/creation/OSMToMapDataConverter.java index 5b3c8bc19..37a32dd02 100644 --- a/src/org/osm2world/core/map_data/creation/OSMToMapDataConverter.java +++ b/src/org/osm2world/core/map_data/creation/OSMToMapDataConverter.java @@ -58,7 +58,6 @@ public class OSMToMapDataConverter { private final Configuration config; private static final Tag MULTIPOLYON_TAG = new Tag("type", "multipolygon"); - public OSMToMapDataConverter(MapProjection mapProjection, Configuration config) { this.mapProjection = mapProjection; @@ -73,7 +72,7 @@ public MapData createMapData(OSMData osmData) throws IOException { createMapElements(osmData, mapNodes, mapWaySegs, mapAreas); - MapData mapData = new MapData(mapNodes, mapWaySegs, mapAreas, + MapData mapData = new MapData(mapProjection, mapNodes, mapWaySegs, mapAreas, calculateFileBoundary(osmData.getBounds())); calculateIntersectionsInMapData(mapData); diff --git a/src/org/osm2world/core/map_data/data/MapData.java b/src/org/osm2world/core/map_data/data/MapData.java index ae4d159ee..b2b878e2b 100644 --- a/src/org/osm2world/core/map_data/data/MapData.java +++ b/src/org/osm2world/core/map_data/data/MapData.java @@ -3,6 +3,7 @@ import java.util.Collection; import java.util.List; +import org.osm2world.core.map_data.creation.MapProjection; import org.osm2world.core.math.AxisAlignedBoundingBoxXZ; import org.osm2world.core.math.VectorXZ; import org.osm2world.core.osm.data.OSMData; @@ -23,13 +24,14 @@ public class MapData { final List mapNodes; final List mapWaySegments; final List mapAreas; + final MapProjection mapProjection; AxisAlignedBoundingBoxXZ fileBoundary; AxisAlignedBoundingBoxXZ dataBoundary; - public MapData(List mapNodes, List mapWaySegments, + public MapData(MapProjection mapProjection, List mapNodes, List mapWaySegments, List mapAreas, AxisAlignedBoundingBoxXZ fileBoundary) { - + this.mapProjection = mapProjection; this.mapNodes = mapNodes; this.mapWaySegments = mapWaySegments; this.mapAreas = mapAreas; @@ -145,5 +147,9 @@ public Iterable getWorldObjects() { public Iterable getWorldObjects(Class type) { return Iterables.filter(getWorldObjects(), type); } + + public MapProjection getMapProjection() { + return this.mapProjection; + } } diff --git a/src/org/osm2world/core/target/Target.java b/src/org/osm2world/core/target/Target.java index f076af1fc..e3d1b09f3 100644 --- a/src/org/osm2world/core/target/Target.java +++ b/src/org/osm2world/core/target/Target.java @@ -9,6 +9,7 @@ import org.osm2world.core.math.VectorXYZ; import org.osm2world.core.math.VectorXZ; import org.osm2world.core.target.common.material.Material; +import org.osm2world.core.target.common.model.Model; import org.osm2world.core.world.data.WorldObject; /** @@ -110,6 +111,18 @@ void drawColumn(Material material, Integer corners, VectorXYZ base, double height, double radiusBottom, double radiusTop, boolean drawBottom, boolean drawTop); + /** + * draws an instance of a 3D model + * + * @param model the model to be drawn + * @param direction rotation of the model in the XZ plane, as an angle in radians + * @param height height of the model; null for default (unspecified) height + * @param width width of the model; null for default (unspecified) width + * @param length length of the model; null for default (unspecified) length + */ + public void drawModel(Model model, VectorXYZ position, + double direction, Double height, Double width, Double length); + /** * gives the target the chance to perform finish/cleanup operations * after all objects have been drawn. diff --git a/src/org/osm2world/core/target/common/AbstractTarget.java b/src/org/osm2world/core/target/common/AbstractTarget.java index d4ceb050f..b150a24d6 100644 --- a/src/org/osm2world/core/target/common/AbstractTarget.java +++ b/src/org/osm2world/core/target/common/AbstractTarget.java @@ -16,6 +16,7 @@ import org.osm2world.core.target.Renderable; import org.osm2world.core.target.Target; import org.osm2world.core.target.common.material.Material; +import org.osm2world.core.target.common.model.Model; import org.osm2world.core.world.data.WorldObject; /** @@ -196,6 +197,14 @@ public void drawConvexPolygon(Material material, List vs, drawTriangleFan(material, vs, texCoordLists); } + @Override + public void drawModel(Model model, VectorXYZ position, + double direction, Double height, Double width, Double length) { + + model.render(this, position, direction, height, width, length); + + } + @Override public void finish() {} diff --git a/src/org/osm2world/core/target/common/material/Material.java b/src/org/osm2world/core/target/common/material/Material.java index 7f164485c..9a970036d 100644 --- a/src/org/osm2world/core/target/common/material/Material.java +++ b/src/org/osm2world/core/target/common/material/Material.java @@ -135,6 +135,18 @@ public Material brighter() { getTransparency(), getShadow(), getAmbientOcclusion(), getTextureDataList()); } + public Material withAmbientFactor(float newAmbientFactor) { + return new ImmutableMaterial(interpolation, getColor(), + newAmbientFactor, getDiffuseFactor(), getSpecularFactor(), getShininess(), + getTransparency(), getShadow(), getAmbientOcclusion(), getTextureDataList()); + } + + public Material withDefuseFactor(float newDefuseFactor) { + return new ImmutableMaterial(interpolation, getColor(), + getAmbientFactor(), newDefuseFactor, getSpecularFactor(), getShininess(), + getTransparency(), getShadow(), getAmbientOcclusion(), getTextureDataList()); + } + public Material darker() { return new ImmutableMaterial(interpolation, getColor().darker(), getAmbientFactor(), getDiffuseFactor(), getSpecularFactor(), getShininess(), diff --git a/src/org/osm2world/core/target/common/model/Model.java b/src/org/osm2world/core/target/common/model/Model.java new file mode 100644 index 000000000..0123c64f2 --- /dev/null +++ b/src/org/osm2world/core/target/common/model/Model.java @@ -0,0 +1,23 @@ +package org.osm2world.core.target.common.model; + +import org.osm2world.core.math.VectorXYZ; +import org.osm2world.core.target.Target; + +/** + * a single 3D model, typically loaded from a file or other resource + */ +public interface Model { + + /** + * draws an instance of the model to any {@link Target} + * + * @param target target for the model; != null + * @param direction rotation of the model in the XZ plane, as an angle in radians + * @param height height of the model; null for default (unspecified) height + * @param width width of the model; null for default (unspecified) width + * @param length length of the model; null for default (unspecified) length + */ + public void render(Target target, VectorXYZ position, + double direction, Double height, Double width, Double length); + +} diff --git a/src/org/osm2world/core/target/common/model/obj/ExternalModel.java b/src/org/osm2world/core/target/common/model/obj/ExternalModel.java new file mode 100644 index 000000000..97fee28f0 --- /dev/null +++ b/src/org/osm2world/core/target/common/model/obj/ExternalModel.java @@ -0,0 +1,60 @@ +package org.osm2world.core.target.common.model.obj; + +import java.util.ArrayList; +import java.util.List; + +import org.osm2world.core.math.VectorXYZ; +import org.osm2world.core.math.VectorXZ; +import org.osm2world.core.target.Target; +import org.osm2world.core.target.common.model.Model; +import org.osm2world.core.target.common.model.obj.parser.ModelLinksProxy; +import org.osm2world.core.target.common.model.obj.parser.ObjModel; +import org.osm2world.core.target.common.model.obj.parser.ObjModel.ObjFace; + + +public class ExternalModel implements Model { + + private ObjModel model; + + private VectorXZ originT = new VectorXZ(0.0, 0.0); + private double scale = 1.0; + private boolean zAxisUp = false; + + + public ExternalModel(String link, ModelLinksProxy proxy) { + this.model = new ObjModel(link, proxy); + + originT = this.model.getBBOX().center().xz().invert(); + } + + @Override + public void render(Target target, VectorXYZ position, + double direction, Double height, Double width, + Double length) { + + VectorXYZ translate = position.add(originT); + + for(ObjFace f : this.model.listFaces()) { + List vs = new ArrayList<>(f.vs.size()); + + for (VectorXYZ src : f.vs) { + src = src.mult(scale); + + if (zAxisUp) { + src = src.rotateX(Math.toRadians(-90.0)); + } + + src = src.rotateY(direction); + src = src.add(translate); + + vs.add(src); + } + + if (f.material != null) { + f.material = f.material.withAmbientFactor(0.9f); + target.drawTriangleFan(f.material, vs, f.texCoordLists); + } + } + } + +} diff --git a/src/org/osm2world/core/target/common/model/obj/parser/ModelLinksProxy.java b/src/org/osm2world/core/target/common/model/obj/parser/ModelLinksProxy.java new file mode 100644 index 000000000..29f1b9204 --- /dev/null +++ b/src/org/osm2world/core/target/common/model/obj/parser/ModelLinksProxy.java @@ -0,0 +1,112 @@ +package org.osm2world.core.target.common.model.obj.parser; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; + +public class ModelLinksProxy { + + private String localCachePath; + + public ModelLinksProxy(String localCachePath) { + this.localCachePath = localCachePath; + } + + public static String resolveLink(URL base, String link) throws MalformedURLException { + if (new File(link).isAbsolute()) { + URL root = new URL(base.getProtocol() + "://" + base.getAuthority()); + return new URL(root, link).toString(); + } + else { + return new URL(base, link).toString(); + } + } + + public File getFile(String link) { + try { + if(isURL(link)) { + File file = getPathForObjUrl(link); + return saveFile(file, new URL(link)); + } + return null; + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + private File saveFile(File file, URL link) + throws IOException, FileNotFoundException, MalformedURLException, InterruptedException { + + if (!file.exists()) { + file.getParentFile().mkdirs(); + if(file.createNewFile()) { + FileOutputStream fileOutputStream = new FileOutputStream(file); + try { + java.nio.channels.FileLock lock = fileOutputStream.getChannel().lock(); + try { + saveFile(fileOutputStream, link); + } finally { + lock.release(); + } + } finally { + fileOutputStream.close(); + } + } + } + + // File exists, but might be locked by other thred/app for writing data + // wait untill it will be released + int timeout = 30 * 1000; + while(!file.canWrite()) { + if (timeout > 0) { + Thread.sleep(100); + timeout -= 100; + } + else { + break; + } + } + + return file; + } + + private void saveFile(FileOutputStream fileOutputStream, URL url) throws IOException { + System.err.println("Download " + url); + ReadableByteChannel rbc = Channels.newChannel(url.openStream()); + fileOutputStream.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } + + private File getPathForObjUrl(String link) { + try { + URL url = new URL(link); + String path = url.getPath(); + + return new File(localCachePath, path); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + public File getLinkedFile(String local, String base) { + try { + String link = resolveLink(new URL(base), local); + return getFile(link); + } + catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private static boolean isURL(String link) { + return link.startsWith("http://") || link.startsWith("https://"); + } + +} diff --git a/src/org/osm2world/core/target/common/model/obj/parser/ObjModel.java b/src/org/osm2world/core/target/common/model/obj/parser/ObjModel.java new file mode 100644 index 000000000..ab4ef4c28 --- /dev/null +++ b/src/org/osm2world/core/target/common/model/obj/parser/ObjModel.java @@ -0,0 +1,448 @@ +package org.osm2world.core.target.common.model.obj.parser; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.imageio.ImageIO; + +import org.apache.commons.lang.StringUtils; +import org.osm2world.core.math.AxisAlignedBoundingBoxXYZ; +import org.osm2world.core.math.VectorXYZ; +import org.osm2world.core.math.VectorXZ; +import org.osm2world.core.target.common.TextureData; +import org.osm2world.core.target.common.TextureData.Wrap; +import org.osm2world.core.target.common.material.ConfMaterial; +import org.osm2world.core.target.common.material.Material; +import org.osm2world.core.target.common.material.Material.Interpolation; +import org.osm2world.core.target.common.material.Material.Shadow; +import org.osm2world.core.target.common.material.Material.Transparency; +import org.osm2world.core.target.common.material.NamedTexCoordFunction; + +public class ObjModel { + + private File objFile; + private Map materials = new HashMap<>(); + private ObjMaterial curentMtl; + + private List vertexes = new ArrayList<>(); + private List vertexNorms = new ArrayList<>(); + private List textureVertexes = new ArrayList<>(); + private Material faceMtl; + private List faces = new ArrayList<>(); + private AxisAlignedBoundingBoxXYZ bbox; + private ModelLinksProxy proxy; + private String base; + + private static final class ObjMaterial { + String materialFileLink; + public String name; + + float ni; + + float[] ka; + float[] kd; + float[] ks; + + String map_Ka; + String map_Kd; + + } + + private static String strip(String str) { + return StringUtils.strip(str, " \t"); + } + + private static String parseStringParam(String line, String key) { + return strip(StringUtils.removeStartIgnoreCase(line, key)); + } + + private float parseFloatParam(String line, String key) { + return Float.valueOf(strip(StringUtils.removeStartIgnoreCase(line, key))); + } + + private float[] parseFloatTriplet(String key, String line) { + String numbers = strip(StringUtils.removeStartIgnoreCase(line, key)); + String[] split = StringUtils.split(numbers, " \t"); + return new float[]{ + Float.valueOf(split[0]), + Float.valueOf(split[1]), + Float.valueOf(split[2]) + }; + } + + private float[] parseFloatDuplet(String key, String line) { + String numbers = strip(StringUtils.removeStartIgnoreCase(line, key)); + String[] split = StringUtils.split(numbers, " \t"); + return new float[]{ + Float.valueOf(split[0]), + Float.valueOf(split[1]) + }; + } + + public ObjModel(String link, ModelLinksProxy resolver) { + this.proxy = resolver; + this.base = link; + this.objFile = this.proxy.getFile(link); + + try { + this.iterateOverObjFile(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private void iterateOverObjFile() throws IOException { + BufferedReader br = new BufferedReader(new FileReader(objFile)); + String line = br.readLine(); + while (line != null) + { + line = StringUtils.strip(line); + if (StringUtils.startsWithIgnoreCase(line, "#")) { + handleObjComment(line); + } + else if (StringUtils.startsWithIgnoreCase(line, "call")){ + handleCall(line); + } + else if (StringUtils.startsWithIgnoreCase(line, "mtllib")){ + handleMtlLib(line); + } + else { + handleObjLine(line); + } + + line = br.readLine(); + } + br.close(); + clear(); + } + + private void handleCall(String line) { + System.err.println("call statement in obj is not supported"); + } + + private void handleMtlLib(String line) throws IOException { + String mtlPath = parseStringParam(line, "mtllib"); + String mtlLink = ModelLinksProxy.resolveLink(new URL(this.base), mtlPath); + File mtlFile = this.proxy.getLinkedFile(mtlLink, this.base); + + parseMtlFile(mtlFile, mtlLink); + } + + private void parseMtlFile(File mtlFile, String mtlLink) throws IOException { + BufferedReader br = new BufferedReader(new FileReader(mtlFile)); + String line = StringUtils.strip(br.readLine()); + while (line != null) + { + if (line.startsWith("#")) { + handleMtlComment(line); + } + else if(StringUtils.startsWithIgnoreCase(line, "newmtl")) { + handleMtlNewMtl(line, mtlLink); + } + else if(StringUtils.startsWithIgnoreCase(line, "ka")) { + handleMtlKa(line); + } + else if(StringUtils.startsWithIgnoreCase(line, "kd")) { + handleMtlKd(line); + } + else if(StringUtils.startsWithIgnoreCase(line, "ks")) { + handleMtlKs(line); + } + else if(StringUtils.startsWithIgnoreCase(line, "ni")) { + handleMtlNi(line); + } + else if(StringUtils.startsWithIgnoreCase(line, "map_Ka")) { + handleMtlMapKa(line); + } + else if(StringUtils.startsWithIgnoreCase(line, "map_Kd")) { + handleMtlMapKd(line); + } + + line = StringUtils.strip(br.readLine()); + line = StringUtils.remove(line, '\t'); + } + br.close(); + parseCurentMtl(); + } + + private void handleMtlMapKd(String line) { + this.curentMtl.map_Kd = parseStringParam(line, "map_kd"); + } + + private void handleMtlMapKa(String line) { + this.curentMtl.map_Ka = parseStringParam(line, "map_ka"); + } + + private void handleMtlNi(String line) { + this.curentMtl.ni = parseFloatParam(line, "ni"); + } + + private void handleMtlKs(String line) { + this.curentMtl.ks = parseFloatTriplet("Ks", line); + } + + private void handleMtlKd(String line) { + this.curentMtl.kd = parseFloatTriplet("Kd", line); + } + + private void handleMtlKa(String line) { + this.curentMtl.ka = parseFloatTriplet("Ka", line); + } + + private void handleMtlNewMtl(String line, String mtlLink) throws IOException { + parseCurentMtl(); + String name = parseStringParam(line, "newmtl"); + this.curentMtl = new ObjMaterial(); + this.curentMtl.name = name; + this.curentMtl.materialFileLink = mtlLink; + } + + private void parseCurentMtl() throws IOException { + if (this.curentMtl != null && this.curentMtl.name != null) { + + boolean mapsEmpty = this.curentMtl.map_Ka == null && this.curentMtl.map_Kd == null; + + if (this.curentMtl.ka == null) { + this.curentMtl.ka = new float[]{0.7f, 0.7f, 0.7f}; + } + if (this.curentMtl.kd == null) { + this.curentMtl.kd = new float[]{0.3f, 0.3f, 0.3f}; + } + if (this.curentMtl.ks == null) { + this.curentMtl.ks = new float[]{0.0f, 0.0f, 0.0f}; + } + + float ambientAverage = (this.curentMtl.ka[0] + this.curentMtl.ka[1] + this.curentMtl.ka[2]) / 3.0f; + float diffuseAverage = (this.curentMtl.kd[0] + this.curentMtl.kd[1] + this.curentMtl.kd[2]) / 3.0f; + + boolean kaComponentsEqual = + this.curentMtl.ka[0] == this.curentMtl.ka[1] && this.curentMtl.ka[1] == this.curentMtl.ka[2]; + boolean kdComponentsEqual = + this.curentMtl.kd[0] == this.curentMtl.kd[1] && this.curentMtl.kd[1] == this.curentMtl.kd[2]; + + boolean kaCorrespondKd = Math.abs(ambientAverage + diffuseAverage - 1.0) < 0.0001f; + boolean colorable = mapsEmpty || !kaComponentsEqual || !kdComponentsEqual || !kaCorrespondKd; + + float specularAverage = (this.curentMtl.ks[0] + this.curentMtl.ks[1] + this.curentMtl.ks[2]) / 3.0f; + Float shiness = this.curentMtl.ni; + + Color diffuseColor = new Color( + (int)(this.curentMtl.kd[0] * 255), + (int)(this.curentMtl.kd[1] * 255), + (int)(this.curentMtl.kd[2] * 255)); + + Color ambientColor = new Color( + (int)(this.curentMtl.ka[0] * 255), + (int)(this.curentMtl.ka[1] * 255), + (int)(this.curentMtl.ka[2] * 255)); + + int rm = Math.min(diffuseColor.getRed() + ambientColor.getRed(), 255); + int gm = Math.min(diffuseColor.getGreen() + ambientColor.getGreen(), 255); + int bm = Math.min(diffuseColor.getBlue() + ambientColor.getBlue(), 255); + + Color meanColor = new Color(rm, gm, bm); + + ConfMaterial m = new ConfMaterial(Interpolation.FLAT, meanColor); + + m.setAmbientFactor(ambientAverage); + m.setDiffuseFactor(diffuseAverage); + + m.setSpecularFactor(specularAverage); + m.setShininess(shiness.intValue()); + + m.setInterpolation(Interpolation.FLAT); + m.setShadow(Shadow.FALSE); + m.setTransparency(Transparency.FALSE); + + if (!mapsEmpty) { + String textureLink = this.curentMtl.map_Ka == null + ? this.curentMtl.map_Kd : this.curentMtl.map_Ka; + + File texture = resolveTexture( + textureLink, + this.curentMtl.materialFileLink); + + BufferedImage bimg = ImageIO.read(texture); + + List tl = new ArrayList<>(); + + tl.add(new TextureData(texture, bimg.getWidth(), bimg.getHeight(), + Wrap.REPEAT, NamedTexCoordFunction.GLOBAL_X_Z, colorable, false)); + + m.setTextureDataList(tl); + } + + this.materials.put(curentMtl.name, m); + } + } + + private File resolveTexture(String texture, String materialFileLink) { + return this.proxy.getLinkedFile(texture, materialFileLink); + } + + private void handleMtlComment(String line) { + // Do nothing + } + + private void handleObjLine(String line) { + if (StringUtils.startsWithIgnoreCase(line, "v ")) { + float[] xyz = parseFloatTriplet("v", line); + handleVertex(new VectorXYZ(-xyz[0], xyz[1], xyz[2]) + .rotateY(Math.toRadians(180.0))); + } + else if (StringUtils.startsWithIgnoreCase(line, "vt")) { + float[] uv = parseFloatDuplet("vt ", line); + handleTextureVertex(new VectorXZ(uv[0], uv[1])); + } + else if (StringUtils.startsWithIgnoreCase(line, "vn")) { + float[] xyz = parseFloatTriplet("vn ", line); + handleVertexNormal(new VectorXYZ(xyz[0], xyz[1], xyz[2])); + } + else if (StringUtils.startsWithIgnoreCase(line, "g ")) { + String groupName = parseStringParam(line, "g"); + handleGroup(groupName); + } + else if (line.startsWith("usemtl ")) { + String mtlName = line.replace("usemtl ", "").trim(); + handleUseMtl(mtlName); + } + else if (line.startsWith("s ")) { + String smoothGroup = line.replace("s ", "").trim(); + handleSmoothGroup(smoothGroup); + } + else if (line.startsWith("f ")) { + String faceLine = line.replace("f ", "").trim(); + handleFace(faceLine); + } + } + + private void handleVertexNormal(VectorXYZ vectorXYZ) { + vertexNorms.add(vectorXYZ); + } + + public static final class ObjFace { + public Material material; + public List vs; + public List normals; + public List> texCoordLists; + } + + private void handleFace(String faceLine) { + ObjFace f = new ObjFace(); + f.material = this.faceMtl; + String[] points = faceLine.split(" "); + for (String pointRef : points) { + String[] components = pointRef.split("/"); + Integer vi = Integer.valueOf(components[0]); + if (f.vs == null) { + f.vs = new ArrayList<>(); + } + + if (vi >= 0) { + f.vs.add(this.vertexes.get(vi - 1)); + } + else { + // it's negative so use minus + f.vs.add(this.vertexes.get(this.vertexes.size() + vi)); + } + + updateBbox(f.vs); + + if (components.length > 1) { + if(StringUtils.stripToNull(components[1]) != null) { + + if (f.texCoordLists == null) { + f.texCoordLists = new ArrayList<>(); + f.texCoordLists.add(new ArrayList<>()); + } + + int tvi = Integer.valueOf(components[1]); + if (tvi >= 0) { + f.texCoordLists.get(0).add(textureVertexes.get(tvi - 1)); + } + else { + f.texCoordLists.get(0).add(textureVertexes.get( + textureVertexes.size() + tvi)); + } + } + } + + if (components.length > 2) { + if(f.normals == null) { + f.normals = new ArrayList<>(); + } + int vni = Integer.valueOf(components[2]); + + if (vni >= 0) { + f.normals.add(this.vertexNorms.get(vni - 1)); + } + else { + f.normals.add(this.vertexNorms.get( + this.vertexNorms.size() + vni)); + } + } + } + this.faces.add(f); + } + + private void updateBbox(List vs) { + if (this.bbox == null) { + this.bbox = new AxisAlignedBoundingBoxXYZ(vs); + } + else { + this.bbox = AxisAlignedBoundingBoxXYZ.union( + this.bbox, new AxisAlignedBoundingBoxXYZ(vs)); + } + } + + public List listFaces() { + return faces; + } + + private void handleGroup(String groupName) { + + } + + private void handleSmoothGroup(String smoothGroup) { + + } + + private void handleUseMtl(String mtlName) { + this.faceMtl = this.materials.get(mtlName); + if (this.faceMtl == null) { + System.err.println("Warn: material " + mtlName + " not found"); + } + } + + private void handleTextureVertex(VectorXZ vectorXZ) { + this.textureVertexes.add(vectorXZ); + } + + private void handleVertex(VectorXYZ vectorXYZ) { + this.vertexes.add(vectorXYZ); + } + + private void handleObjComment(String line) { + // Do Nothing + } + + private void clear() { + this.textureVertexes = null; + this.vertexes = null; + } + + public AxisAlignedBoundingBoxXYZ getBBOX() { + return this.bbox; + } + +} diff --git a/src/org/osm2world/core/world/modules/BuildingModule.java b/src/org/osm2world/core/world/modules/BuildingModule.java index 0e71d17f0..3134a5602 100644 --- a/src/org/osm2world/core/world/modules/BuildingModule.java +++ b/src/org/osm2world/core/world/modules/BuildingModule.java @@ -2,16 +2,37 @@ import static com.google.common.base.Preconditions.checkArgument; import static java.lang.Double.POSITIVE_INFINITY; -import static java.lang.Math.*; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.Math.round; +import static java.lang.Math.sqrt; +import static java.lang.Math.toRadians; import static java.util.Arrays.asList; -import static java.util.Collections.*; -import static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.*; -import static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.*; -import static org.osm2world.core.map_elevation.data.GroundState.*; -import static org.osm2world.core.math.GeometryUtil.*; -import static org.osm2world.core.target.common.material.NamedTexCoordFunction.*; -import static org.osm2world.core.target.common.material.TexCoordUtil.*; -import static org.osm2world.core.world.modules.common.WorldModuleParseUtil.*; +import static java.util.Collections.emptyList; +import static java.util.Collections.nCopies; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseAngle; +import static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseColor; +import static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseMeasure; +import static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseOsmDecimal; +import static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.EXACT; +import static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.MIN; +import static org.osm2world.core.map_elevation.data.GroundState.ABOVE; +import static org.osm2world.core.map_elevation.data.GroundState.BELOW; +import static org.osm2world.core.map_elevation.data.GroundState.ON; +import static org.osm2world.core.math.GeometryUtil.distanceFromLine; +import static org.osm2world.core.math.GeometryUtil.distanceFromLineSegment; +import static org.osm2world.core.math.GeometryUtil.insertIntoPolygon; +import static org.osm2world.core.math.GeometryUtil.interpolateBetween; +import static org.osm2world.core.math.GeometryUtil.interpolateValue; +import static org.osm2world.core.target.common.material.NamedTexCoordFunction.GLOBAL_X_Z; +import static org.osm2world.core.target.common.material.NamedTexCoordFunction.SLOPED_TRIANGLES; +import static org.osm2world.core.target.common.material.NamedTexCoordFunction.STRIP_WALL; +import static org.osm2world.core.target.common.material.TexCoordUtil.texCoordLists; +import static org.osm2world.core.target.common.material.TexCoordUtil.triangleTexCoordLists; +import static org.osm2world.core.world.modules.common.WorldModuleParseUtil.parseHeight; +import static org.osm2world.core.world.modules.common.WorldModuleParseUtil.parseWidth; import java.awt.Color; import java.util.ArrayList; @@ -25,6 +46,7 @@ import java.util.Set; import org.openstreetmap.josm.plugins.graphview.core.data.TagGroup; +import org.osm2world.core.map_data.creation.MapProjection; import org.osm2world.core.map_data.data.MapArea; import org.osm2world.core.map_data.data.MapData; import org.osm2world.core.map_data.data.MapElement; @@ -110,7 +132,7 @@ public Building(MapArea area, boolean useBuildingColors, boolean drawBuildingWindows) { this.area = area; - + for (MapOverlap overlap : area.getOverlaps()) { MapElement other = overlap.getOther(area); if (other instanceof MapArea diff --git a/src/org/osm2world/core/world/modules/ExternalModelModule.java b/src/org/osm2world/core/world/modules/ExternalModelModule.java new file mode 100644 index 000000000..a1a2433a3 --- /dev/null +++ b/src/org/osm2world/core/world/modules/ExternalModelModule.java @@ -0,0 +1,190 @@ +package org.osm2world.core.world.modules; + +import static java.util.Collections.singletonList; +import static org.osm2world.core.world.modules.common.WorldModuleParseUtil.*; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.apache.commons.configuration.Configuration; +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.map_elevation.creation.EleConstraintEnforcer; +import org.osm2world.core.map_elevation.data.EleConnector; +import org.osm2world.core.map_elevation.data.GroundState; +import org.osm2world.core.math.VectorXYZ; +import org.osm2world.core.math.VectorXZ; +import org.osm2world.core.target.RenderableToAllTargets; +import org.osm2world.core.target.Target; +import org.osm2world.core.target.common.model.Model; +import org.osm2world.core.target.common.model.obj.ExternalModel; +import org.osm2world.core.target.common.model.obj.parser.ModelLinksProxy; +import org.osm2world.core.world.data.AreaWorldObject; +import org.osm2world.core.world.data.NodeWorldObject; +import org.osm2world.core.world.data.WorldObject; +import org.osm2world.core.world.modules.common.AbstractModule; + +/** + * adds external 3D models to the world. + * The {@link Model} instances are loaded from files, + * and placed in the scene based on special OSM tags. + */ +public class ExternalModelModule extends AbstractModule { + + private ModelLinksProxy modelLinksProxy; + + private abstract static class ExternalModelWorldObject + implements WorldObject, RenderableToAllTargets { + + // TODO will later require a better solution to allow models on bridges etc. + + protected final E element; + protected final Model model; + + protected EleConnector eleConnector = null; + + private ExternalModelWorldObject(E element, Model model) { + this.element = element; + this.model = model; + } + + @Override + public E getPrimaryMapElement() { + return element; + } + + @Override + public GroundState getGroundState() { + return GroundState.ON; + } + + @Override + public void defineEleConstraints(EleConstraintEnforcer enforcer) {} + + @Override + public void renderTo(Target target) { + + VectorXYZ position = eleConnector.getPosXYZ(); + + double direction = parseDirection(element.getTags(), 0); + Double height = (double)parseHeight(element.getTags(), 0); + Double width = (double)parseWidth(element.getTags(), 0); + Double length = (double)parseLength(element.getTags(), 0); + + if (height == 0) { + height = null; + } + + if (width == 0) { + width = null; + } + + if (length == 0) { + length = null; + } + + target.drawModel(model, position, direction, height, width, length); + + } + + } + + private static class ExternalModelNodeWorldObject extends ExternalModelWorldObject + implements NodeWorldObject { + + private ExternalModelNodeWorldObject(MapNode node, Model model) { + super(node, model); + } + + @Override + public Iterable getEleConnectors() { + + if (eleConnector == null) { + VectorXZ pos = element.getPos(); + eleConnector = new EleConnector(pos, element, GroundState.ON); + } + + return singletonList(eleConnector); + + } + + } + + private static class ExternalModelAreaWorldObject extends ExternalModelWorldObject + implements AreaWorldObject { + + private ExternalModelAreaWorldObject(MapArea area, Model model) { + super(area, model); + } + + @Override + public Iterable getEleConnectors() { + + if (eleConnector == null) { + VectorXZ pos = element.getOuterPolygon().getCentroid(); + eleConnector = new EleConnector(pos, element, GroundState.ON); + } + + return singletonList(eleConnector); + + } + + } + + @Override + public void setConfiguration(Configuration config) { + super.setConfiguration(config); + String modelsCachePath = this.config.getString("externalModelsCachePath", "models"); + modelLinksProxy = new ModelLinksProxy(modelsCachePath); + } + + @Override + protected void applyToElement(MapElement element) { + + if (element.getPrimaryRepresentation() != null) return; + + // ways are not yet supported because there's no easy way to obtain the other way segments + if (element instanceof MapWaySegment) return; + + if (element.getTags().containsKey("model:url")) { + + // only use external models if enabled via config options + if (!config.getBoolean("useExternalModels", false)) return; + + try { + + URL modelURL = new URL(element.getTags().getValue("model:url")); + + Model model = new ExternalModel(modelURL.toString(), modelLinksProxy); + + /* place the model in the scene, wrapped in a world object */ + + if (element instanceof MapNode) { + + MapNode node = (MapNode)element; + NodeWorldObject worldObject = new ExternalModelNodeWorldObject(node, model); + node.addRepresentation(worldObject); + + } else if (element instanceof MapArea) { + + MapArea area = (MapArea)element; + AreaWorldObject worldObject = new ExternalModelAreaWorldObject(area, model); + area.addRepresentation(worldObject); + + } else { + + throw new Error("unsupported element type for external model: " + element); + + } + + } catch (MalformedURLException e) { + e.printStackTrace(); + } + + } + + } + +}