Skip to content

Cesium 3dTtiles generation #225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -31,7 +31,11 @@ nbactions.xml
nbbuild.xml
nb-configuration.xml

# Vscode
.vscode/

# Miscellaneous
*~
*.swp
*.tileset.json
.DS_Store
33 changes: 33 additions & 0 deletions build-adds/tiles-to-glb.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash

# Usage ./tiles-to-glb.sh /path/to/tiles

BASE_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd )
BIN="java -jar $BASE_DIR/target/osm2world-0.4.0-SNAPSHOT.jar"

TILES_ROOT=${1%/}

echo "Convert tiles from $TILES_ROOT"
echo $BASE_DIR

mkdir -p "$TILES_ROOT/tiles"

for tile_p in $(find $TILES_ROOT -iname '*.osm'); do
#cut prefix
tile_sfx=${tile_p#"$TILES_ROOT/"}
#cut suffix
tile=${tile_sfx%".osm"}

z=${tile%%/*}
xy=${tile#*/}
x=${xy%%/*}
y=${xy#*/}

if [ -f "$TILES_ROOT/$z/$x/$y.osm" ]; then
echo $tile $z $x $y

cmd="$BIN --input $TILES_ROOT/$z/$x/$y.osm --tile $z/$x/$y --output $TILES_ROOT/tiles/${z}_${x}_${y}.glb"
$cmd > /dev/null 2>&1
fi

done
826 changes: 826 additions & 0 deletions src/main/java/org/cesiumjs/LICENSE.md

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions src/main/java/org/cesiumjs/WGS84Util.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.cesiumjs;

import org.osm2world.core.map_data.creation.LatLon;
import org.osm2world.core.math.VectorXYZ;

/**
* This code is adoptation of Cesiumjs code.
* See LICENSE.md
* */
public class WGS84Util {

private static final double WGS84_A = 6378137.0;
private static final double WGS84_B = 6356752.3142451793;

private static final VectorXYZ oneOverRadiiSquared = new VectorXYZ(
1.0 / (WGS84_A * WGS84_A),
1.0 / (WGS84_A * WGS84_A),
1.0 / (WGS84_B * WGS84_B));

private static final VectorXYZ wgs84RadiiSquared =
new VectorXYZ(WGS84_A * WGS84_A, WGS84_A * WGS84_A, WGS84_B * WGS84_B);

public static VectorXYZ cartesianFromLatLon(LatLon origin, double height) {
double latitude = Math.toRadians(origin.lat);
double longitude = Math.toRadians(origin.lon);
VectorXYZ radiiSquared = wgs84RadiiSquared;

double cosLatitude = Math.cos(latitude);
VectorXYZ scratchN = new VectorXYZ(
cosLatitude * Math.cos(longitude),
cosLatitude * Math.sin(longitude),
Math.sin(latitude));

scratchN = scratchN.normalize();

VectorXYZ scratchK = mulByComponents(radiiSquared, scratchN);
double gamma = Math.sqrt(scratchN.dot(scratchK));
scratchK = scratchK.mult(1.0 / gamma);
scratchN = scratchN.mult(height);

return scratchK.add(scratchN);
}

public static double[] eastNorthUpToFixedFrame(VectorXYZ cartesian) {

VectorXYZ normal = geodeticSurfaceNormal(cartesian);
VectorXYZ tangent = new VectorXYZ(-cartesian.y, cartesian.x, 0.0).normalize();
VectorXYZ bitangent = normal.cross(tangent);

// Matrix 4x4 by columns
return new double[] {
tangent.x,
tangent.y,
tangent.z,
0.0,
bitangent.x,
bitangent.y,
bitangent.z,
0.0,
normal.x,
normal.y,
normal.z,
0.0,
cartesian.x,
cartesian.y,
cartesian.z,
1.0
};
}

public static VectorXYZ geodeticSurfaceNormal(VectorXYZ cartesian) {
VectorXYZ mulByComponents = mulByComponents(cartesian, oneOverRadiiSquared);
return mulByComponents.normalize();
}

public static VectorXYZ mulByComponents(VectorXYZ a, VectorXYZ b) {
return new VectorXYZ(a.x * b.x, a.y * b.y, a.z * b.z);
}

}
27 changes: 19 additions & 8 deletions src/main/java/org/osm2world/console/Output.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package org.osm2world.console;

import static java.time.Instant.now;
import static java.util.Collections.singletonList;
import static java.util.Map.entry;
import static java.util.stream.Collectors.toList;
import static java.time.Instant.*;
import static java.util.Collections.*;
import static java.util.Map.*;
import static java.util.stream.Collectors.*;
import static org.osm2world.core.ConversionFacade.Phase.*;
import static org.osm2world.core.math.AxisAlignedRectangleXZ.bbox;
import static org.osm2world.core.math.AxisAlignedRectangleXZ.*;

import java.io.File;
import java.io.FileWriter;
@@ -32,10 +32,21 @@
import org.osm2world.core.map_data.creation.LatLonBounds;
import org.osm2world.core.map_data.creation.MapProjection;
import org.osm2world.core.map_data.data.MapMetadata;
import org.osm2world.core.map_elevation.creation.*;
import org.osm2world.core.map_elevation.creation.BridgeTunnelEleCalculator;
import org.osm2world.core.map_elevation.creation.ConstraintEleCalculator;
import org.osm2world.core.map_elevation.creation.EleTagEleCalculator;
import org.osm2world.core.map_elevation.creation.LeastSquaresInterpolator;
import org.osm2world.core.map_elevation.creation.NaturalNeighborInterpolator;
import org.osm2world.core.map_elevation.creation.NoOpEleCalculator;
import org.osm2world.core.map_elevation.creation.SimpleEleConstraintEnforcer;
import org.osm2world.core.map_elevation.creation.ZeroInterpolator;
import org.osm2world.core.math.AxisAlignedRectangleXZ;
import org.osm2world.core.math.VectorXYZ;
import org.osm2world.core.osm.creation.*;
import org.osm2world.core.osm.creation.GeodeskReader;
import org.osm2world.core.osm.creation.MbtilesReader;
import org.osm2world.core.osm.creation.OSMDataReader;
import org.osm2world.core.osm.creation.OSMFileReader;
import org.osm2world.core.osm.creation.OverpassReader;
import org.osm2world.core.target.TargetUtil;
import org.osm2world.core.target.TargetUtil.Compression;
import org.osm2world.core.target.common.rendering.Camera;
@@ -230,7 +241,7 @@ public static void output(Configuration config,
? GltfTarget.GltfFlavor.GLB : GltfTarget.GltfFlavor.GLTF;
Compression compression = EnumSet.of(OutputMode.GLTF_GZ, OutputMode.GLB_GZ).contains(outputMode)
? Compression.GZ : Compression.NONE;
GltfTarget gltfTarget = new GltfTarget(outputFile, gltfFlavor, compression, bounds);
GltfTarget gltfTarget = new GltfTarget(outputFile, gltfFlavor, compression, results.getMapProjection(), bounds);
gltfTarget.setConfiguration(config);
boolean underground = config.getBoolean("renderUnderground", true);
TargetUtil.renderWorldObjects(gltfTarget, results.getMapData(), underground);
371 changes: 371 additions & 0 deletions src/main/java/org/osm2world/console/TilesetPyramide.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,371 @@
package org.osm2world.console;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.io.FileUtils;
import org.locationtech.jts.util.Assert;
import org.osm2world.core.target.gltf.tiles_data.TilesetAsset;
import org.osm2world.core.target.gltf.tiles_data.TilesetEntry;
import org.osm2world.core.target.gltf.tiles_data.TilesetParentEntry;
import org.osm2world.core.target.gltf.tiles_data.TilesetRoot;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TilesetPyramide {

private static final class TilesetTreeBuilder {

private record FileRegionTpl(File file, double[] region, int geomErr) {
}

private record TileZXY(int z, int x, int y) {
@Override
public final String toString() {
return z + "_" + x + "_" + y;
}
}

private static double tile2lon(int x, int z) {
return x / Math.pow(2.0, z) * 360.0 - 180;
}

private static double tile2lat(int y, int z) {
double n = Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, z);
return Math.toDegrees(Math.atan(Math.sinh(n)));
}

public static int[] getTileNumber(final double lat, final double lon, final int zoom) {
int xtile = (int) Math.floor((lon + 180) / 360 * (1 << zoom));
int ytile = (int) Math
.floor((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2
* (1 << zoom));
if (xtile < 0)
xtile = 0;
if (xtile >= (1 << zoom))
xtile = ((1 << zoom) - 1);
if (ytile < 0)
ytile = 0;
if (ytile >= (1 << zoom))
ytile = ((1 << zoom) - 1);

return new int[] {zoom, xtile, ytile};
}

public static double[] extendRegion(double[] a, double[] b) {

double west = Math.min(a[0], b[0]);
double south = Math.min(a[1], b[1]);

double east = Math.max(a[2], b[2]);
double north = Math.max(a[3], b[3]);

double minh = Math.min(a[4], b[4]);
double maxh = Math.max(a[5], b[5]);

return new double[] {
west, south, east, north, minh, maxh
};
}

private File outDir;
private List<File> tilesetFiles;
private String regexPattern;
private int srcLevel = 14;

public TilesetTreeBuilder(File outDir, int srcLevel, List<File> tilesetFiles, String pathPattern) {
this.outDir = outDir;
this.srcLevel = srcLevel;
this.tilesetFiles = tilesetFiles;
this.regexPattern = pathPattern.replaceAll("\\{z\\}", "(?<z>[0-9]+)");
this.regexPattern = this.regexPattern.replaceAll("\\{x\\}", "(?<x>[0-9]+)");
this.regexPattern = this.regexPattern.replaceAll("\\{y\\}", "(?<y>[0-9]+)");
}

public void build() {
System.out.println("Build tileset tree for " + this.tilesetFiles.size() + " tileset files");

Pattern pattern = Pattern.compile(this.regexPattern, Pattern.CASE_INSENSITIVE);

Map<TileZXY, List<File>> buckets16 = new HashMap<>();

this.tilesetFiles.stream().forEach(f -> {
Matcher m = pattern.matcher(f.toString());
if (m.matches()) {
String zs = m.group("z");
String xs = m.group("x");
String ys = m.group("y");

if (xs != null && ys != null && zs != null) {
int x = Integer.valueOf(xs);
int y = Integer.valueOf(ys);
int z = Integer.valueOf(zs);

if (z == this.srcLevel) {
TileZXY key16 = getParentZXY(new TileZXY(z, x, y), 2);

Assert.equals(key16.z, z - 2, "Unexpected z value " + f.toString());

if (buckets16.get(key16) == null) {
buckets16.put(key16, new ArrayList<>(16));
}

buckets16.get(key16).add(f);
}
}
}

});

Gson gson = new GsonBuilder().create();

Map<TileZXY, List<FileRegionTpl>> metaTilesetBuckets = new HashMap<>();

buckets16.forEach((TileZXY bucketZXY, List<File> tiles) -> {

try {
TilesetRoot parentTileSet = createEmbeddedChildrenTileset(tiles);

File parentTileJsonFile = new File(outDir, bucketZXY.toString() + ".tileset.json");
FileUtils.write(parentTileJsonFile, gson.toJson(parentTileSet));

TileZXY parentZXY = getParentZXY(bucketZXY, 3);
Assert.equals(bucketZXY.z - 3, parentZXY.z, "Unexpected z value for " + bucketZXY.toString());

if (metaTilesetBuckets.get(parentZXY) == null) {
metaTilesetBuckets.put(parentZXY, new ArrayList<>(64));
}

metaTilesetBuckets.get(parentZXY).add(new FileRegionTpl(
parentTileJsonFile,
parentTileSet.getRoot().getBoundingVolume().getRegion(),
parentTileSet.getRoot().getGeometricError().intValue()));

}
catch (IOException e) {
e.printStackTrace();
}
});

System.out.println(
this.tilesetFiles.size() +
" tilesets were embedded into " +
buckets16.size() +
" level 2 tilesets and written to " +
outDir.toString() + "/" +
buckets16.keySet().iterator().next().z + "_*_*.tileset.json");

Map<TileZXY, List<FileRegionTpl>> subtreeTilesets = generateTreeLevel(metaTilesetBuckets);
System.out.println(
metaTilesetBuckets.size() +
" level 3 tilesets written to " +
outDir.toString() + "/" +
metaTilesetBuckets.keySet().iterator().next().z + "_*_*.tileset.json");

int total = subtreeTilesets.entrySet().stream().map(e -> e.getValue()).collect(Collectors.summingInt(List::size));

while (total > Math.pow(4, 3)) {
System.out.println("Generate tree over " + subtreeTilesets.size() + " subtree tilesets");
subtreeTilesets = generateTreeLevel(subtreeTilesets);
}

List<FileRegionTpl> rootTiles = subtreeTilesets.values().stream().flatMap(List::stream).collect(Collectors.toList());
TilesetRoot rootTileset = createParentTilesetForBucket(rootTiles, "REPLACE");

File rootTilesetFile = new File(outDir, "root.tileset.json");
try {
FileUtils.write(rootTilesetFile, gson.toJson(rootTileset));
}
catch (IOException e) {
throw new RuntimeException(e);
}

System.out.println("Root tileset file: " + rootTilesetFile.toString());
}

/**
* For Files in list create parent tileset, conetents of
* children tiles will be embedded into parent tileset
*/
private TilesetRoot createEmbeddedChildrenTileset(List<File> childrenFiles) throws IOException {
// TODO: Add option for deleting embedded files
Gson gson = new GsonBuilder().create();

TilesetRoot parentTileSet = new TilesetRoot();
parentTileSet.setAsset(new TilesetAsset("1.0"));
TilesetParentEntry parent = new TilesetParentEntry();

parentTileSet.setRoot(parent);

for (File f : childrenFiles) {
String tileJson = FileUtils.readFileToString(f);
TilesetRoot tileset = gson.fromJson(tileJson, TilesetRoot.class);

TilesetParentEntry child = tileset.getRoot();
parent.setGeometricError(child.getGeometricError().intValue() * 16);

parent.addChild(child);

if (parent.getBoundingVolume() == null) {
parent.setBoundingVolume(child.getBoundingVolume());
}
else {
double[] parentRegion = extendRegion(
parent.getBoundingVolume().getRegion(),
child.getBoundingVolume().getRegion());

parent.setBoundingVolume(parentRegion);
}
}

return parentTileSet;
}

private static TileZXY getParentZXY(TileZXY zxy, int levels) {
double lat = tile2lat(zxy.y, zxy.z);
double lon = tile2lon(zxy.x, zxy.z);

// 64 subtile
int[] parentZXY = getTileNumber(lat, lon, Math.max(zxy.z - levels, 0));
return new TileZXY(parentZXY[0], parentZXY[1], parentZXY[2]);
}

private Map<TileZXY, List<FileRegionTpl>> generateTreeLevel(Map<TileZXY, List<FileRegionTpl>> currentLayerBuckets) {
Map<TileZXY, List<FileRegionTpl>> parentTiles = new HashMap<>();

currentLayerBuckets.forEach((TileZXY bucketZXY, List<FileRegionTpl> tilesData) -> {

TilesetRoot bucketTileSet = createParentTilesetForBucket(tilesData, "ADD");

Gson gson = new GsonBuilder().create();

String filename = bucketZXY.toString() + ".tileset.json";
File bucketTilesetFile = new File(outDir, filename);
try {
FileUtils.write(bucketTilesetFile, gson.toJson(bucketTileSet));
}
catch (IOException e) {
throw new RuntimeException(e);
}

TileZXY parentBucketZXY = getParentZXY(bucketZXY, 3);

if (parentTiles.get(parentBucketZXY) == null) {
parentTiles.put(parentBucketZXY, new ArrayList<>());
}
parentTiles.get(parentBucketZXY).add(new FileRegionTpl(
bucketTilesetFile,
bucketTileSet.getRoot().getBoundingVolume().getRegion(),
bucketTileSet.getRoot().getGeometricError().intValue())
);
});

return parentTiles;
}

private TilesetRoot createParentTilesetForBucket(List<FileRegionTpl> tilesData, String refine) {
TilesetRoot bucketTileSet = new TilesetRoot();
bucketTileSet.setAsset(new TilesetAsset("1.0"));
TilesetParentEntry parent = new TilesetParentEntry();
parent.setRefine(refine);

bucketTileSet.setRoot(parent);

for (FileRegionTpl tileData: tilesData) {
TilesetEntry child = new TilesetEntry();
child.setGeometricError(tileData.geomErr);
child.setBoundingVolume(tileData.region);
child.setContent(tileData.file.getName());

parent.setGeometricError(tileData.geomErr * 4);

parent.addChild(child);

if (parent.getBoundingVolume() == null) {
parent.setBoundingVolume(child.getBoundingVolume());
}
else {
double[] parentRegion = extendRegion(
parent.getBoundingVolume().getRegion(),
child.getBoundingVolume().getRegion());

parent.setBoundingVolume(parentRegion);
}
}

return bucketTileSet;
}
}

public static void main(String[] args) {
String pathPattern = args[0];
String[] pathEntries = pathPattern.split("[/\\\\]");

File base = null;
List<String> templates = new ArrayList<>();

for (String p : pathEntries) {
if (templates.isEmpty() && !isTemplated(p)) {
base = base == null ? new File(p) : new File(base, p);
}
else {
templates.add(p);
}
}

List<File> tilesets = new ArrayList<>();

List<File> files = new ArrayList<>();
files.add(base);

for (String template : templates) {
String pattern = template.replace("{z}", "([0-9]+)");
pattern = pattern.replace("{x}", "([0-9]+)");
pattern = pattern.replace("{y}", "([0-9]+)");

System.out.println("Checking for " + pattern);

Pattern regex = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);

List<File> dirs = new ArrayList<>();

for (File parentf : files) {

File[] matched = parentf.listFiles(new FileFilter() {

@Override
public boolean accept(File pathname) {
return regex.matcher(pathname.getName()).matches();
}

});

Arrays.stream(matched)
.filter(f -> f.isDirectory()).forEach(dirs::add);

Arrays.stream(matched)
.filter(f -> !f.isDirectory()).forEach(tilesets::add);
}

files = dirs;
}

TilesetTreeBuilder builder = new TilesetTreeBuilder(base, 14, tilesets, pathPattern);
builder.build();
}

private static boolean isTemplated(String p) {
return p.contains("{z}") || p.contains("{x}") || p.contains("{y}");
}

}
65 changes: 37 additions & 28 deletions src/main/java/org/osm2world/core/ConversionFacade.java
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

import javax.annotation.Nullable;

@@ -89,34 +90,42 @@ public TerrainElevationData getEleData() {

/**
* generates a default list of modules for the conversion
* @param config
*/
static final List<WorldModule> createDefaultModuleList() {

return Arrays.asList((WorldModule)
new RoadModule(),
new RailwayModule(),
new AerowayModule(),
new BuildingModule(),
new ParkingModule(),
new TreeModule(),
new StreetFurnitureModule(),
new TrafficSignModule(),
new BicycleParkingModule(),
new WaterModule(),
new PoolModule(),
new GolfModule(),
new SportsModule(),
new CliffModule(),
new BarrierModule(),
new PowerModule(),
new MastModule(),
new BridgeModule(),
new TunnelModule(),
new SurfaceAreaModule(),
new InvisibleModule(),
new IndoorModule()
);

static final List<WorldModule> createDefaultModuleList(Configuration config) {

List<Class<? extends WorldModule>> excludedModules = new ArrayList<>(0);

if (config.getBoolean("noSurface", false)) {
excludedModules.add(SurfaceAreaModule.class);
};

return Stream.of((WorldModule)
new RoadModule(),
new RailwayModule(),
new AerowayModule(),
new BuildingModule(),
new ParkingModule(),
new TreeModule(),
new StreetFurnitureModule(),
new TrafficSignModule(),
new BicycleParkingModule(),
new WaterModule(),
new PoolModule(),
new GolfModule(),
new SportsModule(),
new CliffModule(),
new BarrierModule(),
new PowerModule(),
new MastModule(),
new BridgeModule(),
new TunnelModule(),
new SurfaceAreaModule(),
new InvisibleModule(),
new IndoorModule()
)
.filter(m -> !excludedModules.contains(m.getClass()))
.toList();
}

private Function<LatLon, ? extends MapProjection> mapProjectionFactory = MetricMapProjection::new;
@@ -265,7 +274,7 @@ public Results createRepresentations(MapProjection mapProjection, MapData mapDat
updatePhase(Phase.REPRESENTATION);

if (worldModules == null) {
worldModules = createDefaultModuleList();
worldModules = createDefaultModuleList(config);
}

Materials.configureMaterials(config);
283 changes: 241 additions & 42 deletions src/main/java/org/osm2world/core/target/gltf/GltfTarget.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
package org.osm2world.core.target.gltf;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.osm2world.core.math.algorithms.NormalCalculationUtil.calculateTriangleNormals;
import static java.util.Arrays.*;
import static java.util.stream.Collectors.*;
import static org.osm2world.core.math.algorithms.NormalCalculationUtil.*;
import static org.osm2world.core.target.TargetUtil.*;
import static org.osm2world.core.target.common.ResourceOutputSettings.ResourceOutputMode.EMBED;
import static org.osm2world.core.target.common.ResourceOutputSettings.ResourceOutputMode.REFERENCE;
import static org.osm2world.core.target.common.material.Material.Interpolation.SMOOTH;

import java.io.*;
import static org.osm2world.core.target.common.ResourceOutputSettings.ResourceOutputMode.*;
import static org.osm2world.core.target.common.material.Material.Interpolation.*;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

import org.apache.commons.io.FilenameUtils;
import org.cesiumjs.WGS84Util;
import org.osm2world.core.GlobalValues;
import org.osm2world.core.map_data.creation.LatLon;
import org.osm2world.core.map_data.creation.MapProjection;
import org.osm2world.core.map_data.data.MapRelation;
import org.osm2world.core.map_data.data.TagSet;
import org.osm2world.core.math.AxisAlignedRectangleXZ;
import org.osm2world.core.math.TriangleXYZ;
import org.osm2world.core.math.Vector3D;
import org.osm2world.core.math.VectorXYZ;
import org.osm2world.core.math.VectorXZ;
import org.osm2world.core.math.shapes.SimpleClosedShapeXZ;
import org.osm2world.core.target.TargetUtil.Compression;
import org.osm2world.core.target.common.MeshStore;
import org.osm2world.core.target.common.MeshStore.MeshMetadata;
import org.osm2world.core.target.common.MeshStore.MeshProcessingStep;
import org.osm2world.core.target.common.MeshStore.MeshWithMetadata;
import org.osm2world.core.target.common.MeshTarget;
import org.osm2world.core.target.common.MeshTarget.MergeMeshes.MergeOption;
import org.osm2world.core.target.common.ResourceOutputSettings;
@@ -37,16 +55,31 @@
import org.osm2world.core.target.common.mesh.LevelOfDetail;
import org.osm2world.core.target.common.mesh.Mesh;
import org.osm2world.core.target.common.mesh.TriangleGeometry;
import org.osm2world.core.target.gltf.data.*;
import org.osm2world.core.target.gltf.data.Gltf;
import org.osm2world.core.target.gltf.data.GltfAccessor;
import org.osm2world.core.target.gltf.data.GltfAsset;
import org.osm2world.core.target.gltf.data.GltfBuffer;
import org.osm2world.core.target.gltf.data.GltfBufferView;
import org.osm2world.core.target.gltf.data.GltfImage;
import org.osm2world.core.target.gltf.data.GltfMaterial;
import org.osm2world.core.target.gltf.data.GltfMaterial.NormalTextureInfo;
import org.osm2world.core.target.gltf.data.GltfMaterial.OcclusionTextureInfo;
import org.osm2world.core.target.gltf.data.GltfMaterial.PbrMetallicRoughness;
import org.osm2world.core.target.gltf.data.GltfMaterial.TextureInfo;
import org.osm2world.core.target.gltf.data.GltfMesh;
import org.osm2world.core.target.gltf.data.GltfNode;
import org.osm2world.core.target.gltf.data.GltfSampler;
import org.osm2world.core.target.gltf.data.GltfScene;
import org.osm2world.core.target.gltf.data.GltfTexture;
import org.osm2world.core.target.gltf.tiles_data.TilesetAsset;
import org.osm2world.core.target.gltf.tiles_data.TilesetParentEntry;
import org.osm2world.core.target.gltf.tiles_data.TilesetRoot;
import org.osm2world.core.util.ConfigUtil;
import org.osm2world.core.util.FaultTolerantIterationUtil;
import org.osm2world.core.util.color.LColor;

import com.google.common.collect.Multimap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonIOException;

@@ -55,11 +88,15 @@
*/
public class GltfTarget extends MeshTarget {

//TODO: Make configurable
private static final int NUM_MESHES_FOR_SUBDIVISION_TOP = 100;

public enum GltfFlavor { GLTF, GLB }

private final File outputFile;
private final GltfFlavor flavor;
private final Compression compression;
private final MapProjection mapProjection;
private final @Nullable SimpleClosedShapeXZ bounds;

/** the gltf asset under construction */
@@ -72,13 +109,20 @@ public enum GltfFlavor { GLTF, GLB }
private final List<ByteBuffer> binChunkData = new ArrayList<>();

public GltfTarget(File outputFile, GltfFlavor flavor, Compression compression,
MapProjection mapProjection,
@Nullable SimpleClosedShapeXZ bounds) {
this.outputFile = outputFile;
this.flavor = flavor;
this.compression = compression;
this.mapProjection = mapProjection;
this.bounds = bounds;
}

public static final class MinMax {
public double min = Double.MAX_VALUE;
public double max = Double.MIN_VALUE;
}

public File outputDir() {
return outputFile.getParentFile();
}
@@ -90,25 +134,200 @@ public String toString() {

@Override
public void finish() {
MeshStore processedMeshStore = processMeshStore();

writeFileWithCompression(outputFile, compression, outputStream -> {
List<MeshWithMetadata> meshesWithMetadata = processedMeshStore.meshesWithMetadata();
meshesWithMetadata.sort(new MeshHeightAndSizeComparator());

MinMax ymm = new MinMax();
meshesWithMetadata.forEach(m -> {
m.mesh().geometry.asTriangles().vertices().forEach(v -> {
ymm.min = Double.min(ymm.min, v.y);
ymm.max = Double.max(ymm.max, v.y);
});
});

List<File> writtenFiles = new ArrayList<>();

if (config.getBoolean("subdivideTiles", false)) {
List<MeshWithMetadata> topMeshes = meshesWithMetadata.subList(0, Math.min(meshesWithMetadata.size(), NUM_MESHES_FOR_SUBDIVISION_TOP));
File p = this.outputFile.getParentFile();

String fname = this.outputFile.getName();
String ext = getExt(fname);
String baseName = fname.replace(ext, "");

File out = new File(p, baseName + "_0" + ext);
writeToFile(out, this.compression, new MeshStore(topMeshes));

writtenFiles.add(out);

List<MeshWithMetadata> restMeshes = meshesWithMetadata.subList(Math.min(meshesWithMetadata.size(), 100), meshesWithMetadata.size());
writeToFile(this.outputFile, this.compression, new MeshStore(restMeshes));
writtenFiles.add(this.outputFile);
}
else {
writeToFile(this.outputFile, this.compression, processedMeshStore);
}

if (config.getBoolean("writeTilesetJson", false)) {
File p = this.outputFile.getParentFile();
String fname = this.outputFile.getName();
String ext = getExt(fname);
String baseName = fname.replace(ext, "");
writeTileset(new File(p, baseName + ".tileset.json"), writtenFiles, mapProjection.getOrigin(), bounds, ymm.min, ymm.max);
}
}

private String getExt(String fname) {
String ext = "";

if (fname.endsWith(".gz")) {
ext = ".gz";
}
if (fname.endsWith(".zip")) {
ext = ".zip";
}

if (fname.endsWith(".gltf" + ext)) {
ext = ".gltf" + ext;
}
if (fname.endsWith(".glb" + ext)) {
ext = ".glb" + ext;
}

return ext;
}

private void writeToFile(File outputFile, Compression compression, MeshStore processedMeshStore) {
writeFileWithCompression(outputFile, compression, outputStream -> {
try {
if (flavor == GltfFlavor.GLTF) {
writeJson(outputStream);
writeJson(processedMeshStore, outputStream);
} else {
try (var jsonChunkOutputStream = new ByteArrayOutputStream()) {
writeJson(jsonChunkOutputStream);
writeJson(processedMeshStore, jsonChunkOutputStream);
ByteBuffer jsonChunkData = asPaddedByteBuffer(jsonChunkOutputStream.toByteArray(), (byte) 0x20);
writeGlb(outputStream, jsonChunkData, binChunkData);
}
}
} catch (IOException | JsonIOException e) {
throw new RuntimeException(e);
}

});
}

private MeshStore processMeshStore() {
boolean keepOsmElements = config.getBoolean("keepOsmElements", true);
boolean clipToBounds = config.getBoolean("clipToBounds", false);

/* process the meshes */

EnumSet<MergeOption> mergeOptions = EnumSet.noneOf(MergeOption.class);

if (!keepOsmElements) {
mergeOptions.add(MergeOption.MERGE_ELEMENTS);
}

LevelOfDetail lod = ConfigUtil.readLOD(config);

// TODO: split into subtiles eighter here as one step
// (good option save tile in meta),
// or one step up by calling processMeshStore
// multiple times with different bboxes (bad option)

List<MeshProcessingStep> processingSteps = new ArrayList<>(asList(
new FilterLod(lod),
new EmulateTextureLayers(lod.ordinal() <= 1 ? 1 : Integer.MAX_VALUE),
new MoveColorsToVertices(), // after EmulateTextureLayers because colorable is per layer
new ReplaceTexturesWithAtlas(t -> getResourceOutputSettings().modeForTexture(t) == REFERENCE),
new MergeMeshes(mergeOptions)));

if (clipToBounds && bounds != null) {
processingSteps.add(1, new ClipToBounds(bounds, true));
}

return meshStore.process(processingSteps);
}

/*
Working example for tileset
{
"asset" : {
"version": "1.0"
},
"root": {
"content": {
"uri": "14_5298_5916_0.glb"
},
"refine": "ADD",
"geometricError": 25,
"boundingVolume": {
"region": [-1.1098350999480917,0.7790694465970149,-1.1094516048185785,0.779342292568195,0.0,100]
},
"transform": [
0.895540041198885, 0.4449809373551852, 0.0, 0.0,
-0.31269461895546163, 0.6293090971636892, 0.7114718093525005, 0.0,
0.3165913926274654, -0.6371514934593837, 0.7027146394495273, 0.0,
2022609.150078308, -4070573.2078238726, 4459382.83869308, 1.0
],
"children": [{
"boundingVolume": {
"region": [-1.1098350999480917,0.7790694465970149,-1.1094516048185785,0.779342292568195,0.0,97.49999999999997]
},
"geometricError": 0,
"content": {
"uri": "14_5298_5916.glb"
}
}]
}
}*/
private void writeTileset(File outFile, List<File> tileContentFiles, LatLon origin, SimpleClosedShapeXZ fullBBOX, double minY, double maxY) {

AxisAlignedRectangleXZ bbox = fullBBOX.boundingBox();

VectorXYZ cartesianOrigin = WGS84Util.cartesianFromLatLon(origin, 0.0);
double[] transform = WGS84Util.eastNorthUpToFixedFrame(cartesianOrigin);

// left top
LatLon westNorth = this.mapProjection.toLatLon(new VectorXZ(bbox.minX, bbox.maxZ));
// right bottom
LatLon eastSouth = this.mapProjection.toLatLon(new VectorXZ(bbox.maxX, bbox.minZ));

double[] boundingRegion = new double[] {
Math.toRadians(westNorth.lon), // west
Math.toRadians(eastSouth.lat), // south
Math.toRadians(eastSouth.lon), // east
Math.toRadians(westNorth.lat), // north
minY,
maxY
};

TilesetRoot tileset = new TilesetRoot();
tileset.setAsset(new TilesetAsset("1.0"));

TilesetParentEntry root = new TilesetParentEntry();
tileset.setRoot(root);

root.setTransform(transform);
root.setGeometricError(25);
root.setBoundingVolume(boundingRegion);
root.setContent(tileContentFiles.get(0).getName());

if (tileContentFiles.size() > 1) {
root.addChild(tileContentFiles.get(1).getName());
}

Gson gson = new GsonBuilder().create();
String out = gson.toJson(tileset);

try (PrintWriter metaWriter = new PrintWriter(new FileOutputStream(outFile))) {
metaWriter.println(out);
metaWriter.flush();
}
catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
}

/** creates a {@link GltfNode} and returns its index in {@link Gltf#nodes} */
@@ -371,36 +590,10 @@ private int createImage(TextureData textureData) throws IOException {
/**
* constructs the JSON document after all parts of the glTF have been created
* and outputs it to an {@link OutputStream}
* @param processedMeshStore
*/
private void writeJson(OutputStream outputStream) throws IOException {

private void writeJson(MeshStore processedMeshStore, OutputStream outputStream) throws IOException {
boolean keepOsmElements = config.getBoolean("keepOsmElements", true);
boolean clipToBounds = config.getBoolean("clipToBounds", false);

/* process the meshes */

EnumSet<MergeOption> mergeOptions = EnumSet.noneOf(MergeOption.class);

if (!keepOsmElements) {
mergeOptions.add(MergeOption.MERGE_ELEMENTS);
}

LevelOfDetail lod = ConfigUtil.readLOD(config);

List<MeshProcessingStep> processingSteps = new ArrayList<>(asList(
new FilterLod(lod),
new EmulateTextureLayers(lod.ordinal() <= 1 ? 1 : Integer.MAX_VALUE),
new MoveColorsToVertices(), // after EmulateTextureLayers because colorable is per layer
new ReplaceTexturesWithAtlas(t -> getResourceOutputSettings().modeForTexture(t) == REFERENCE),
new MergeMeshes(mergeOptions)));

if (clipToBounds && bounds != null) {
processingSteps.add(1, new ClipToBounds(bounds, true));
}

MeshStore processedMeshStore = meshStore.process(processingSteps);

Multimap<MeshMetadata, Mesh> meshesByMetadata = processedMeshStore.meshesByMetadata();

/* create the basic structure of the glTF */

@@ -431,6 +624,9 @@ private void writeJson(OutputStream outputStream) throws IOException {

rootNode.children = new ArrayList<>();

Multimap<MeshMetadata, Mesh> meshesByMetadata = processedMeshStore.meshesByMetadata();
// int top = 100;

for (MeshMetadata objectMetadata : meshesByMetadata.keySet()) {

List<Integer> meshNodeIndizes = new ArrayList<>(meshesByMetadata.size());
@@ -459,6 +655,9 @@ private void writeJson(OutputStream outputStream) throws IOException {

rootNode.children.addAll(meshNodeIndizes);

// if (top-- == 0) {
// break;
// }
}

/* add a buffer for the BIN chunk */
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.osm2world.core.target.gltf;

import java.util.Comparator;

import org.osm2world.core.math.AxisAlignedRectangleXZ;
import org.osm2world.core.target.common.MeshStore;
import org.osm2world.core.target.common.MeshStore.MeshWithMetadata;
import org.osm2world.core.target.gltf.GltfTarget.MinMax;

final class MeshHeightAndSizeComparator implements Comparator<MeshStore.MeshWithMetadata> {
@Override
public int compare(MeshWithMetadata m1, MeshWithMetadata m2) {
MinMax ymm1 = new MinMax();
MinMax ymm2 = new MinMax();

m1.mesh().geometry.asTriangles().vertices().forEach(v -> {
ymm1.max = Double.max(ymm1.max, v.y);
});

m2.mesh().geometry.asTriangles().vertices().forEach(v -> {
ymm2.max = Double.max(ymm2.max, v.y);
});

int h1 = (int)Math.round(ymm1.max * 2) / 2;
int h2 = (int)Math.round(ymm2.max * 2) / 2;

if (h2 != h1) {
return h2 - h1;
}

AxisAlignedRectangleXZ bbx1 = AxisAlignedRectangleXZ.bbox(m1.mesh().geometry.asTriangles().vertices());
AxisAlignedRectangleXZ bbx2 = AxisAlignedRectangleXZ.bbox(m2.mesh().geometry.asTriangles().vertices());

int d1 = (int)Math.round(bbx1.getDiameter() * 4) / 4;
int d2 = (int)Math.round(bbx2.getDiameter() * 4) / 4;

return d2 - d1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.osm2world.core.target.gltf.tiles_data;

public class TilesetAsset {
private String version = "1.0";

public String getVersion() {
return version;
}

public void setVersion(String version) {
this.version = version;
}

public TilesetAsset() {

}

public TilesetAsset(String version) {
this.version = version;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package org.osm2world.core.target.gltf.tiles_data;


/**
* // Example tileset
"content": {
"uri": "14_5298_5916_0.glb"
},
"refine": "ADD",
"geometricError": 25,
"boundingVolume": {
"region": [-1.1098350999480917,0.7790694465970149,-1.1094516048185785,0.779342292568195,0.0,100]
},
"transform": [
0.895540041198885, 0.4449809373551852, 0.0, 0.0,
-0.31269461895546163, 0.6293090971636892, 0.7114718093525005, 0.0,
0.3165913926274654, -0.6371514934593837, 0.7027146394495273, 0.0,
2022609.150078308, -4070573.2078238726, 4459382.83869308, 1.0
],
// This TilesetEntry covers children entries
"children": [{
"boundingVolume": {
"region": [-1.1098350999480917,0.7790694465970149,-1.1094516048185785,0.779342292568195,0.0,97.49999999999997]
},
"geometricError": 0,
"content": {
"uri": "14_5298_5916.glb"
}
}]
*/


public class TilesetEntry {

public static final class Region {

private double[] region = new double[6];

public Region() {
}

public Region(double[] region) {
this.region = region;
}

public double[] getRegion() {
return region;
}

public void setRegion(double[] region) {
this.region = region;
}
}

public static final class TilesetContent {
private String uri;

public TilesetContent(String uri) {
this.uri = uri;
}

public TilesetContent() {
}

public String getUri() {
return uri;
}

public void setUri(String uri) {
this.uri = uri;
}
}

private Number geometricError = 0;
private Region boundingVolume = null;
private TilesetContent content = null;

public TilesetEntry() {
}

public TilesetEntry(Number geometricError, double[] boundingVolume, String contentUri) {
this.geometricError = geometricError;
this.boundingVolume = new Region(boundingVolume);
this.content = new TilesetContent(contentUri);
}

public Number getGeometricError() {
return geometricError;
}
public void setGeometricError(Number geometricError) {
this.geometricError = geometricError;
}

public Region getBoundingVolume() {
return boundingVolume;
}
public void setBoundingVolume(Region boundingVolume) {
this.boundingVolume = boundingVolume;
}
public void setBoundingVolume(double[] region) {
this.boundingVolume = new Region(region);
}

public TilesetContent getContent() {
return content;
}
public void setContent(TilesetContent content) {
this.content = content;
}
public void setContent(String contentUri) {
this.content = new TilesetContent(contentUri);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.osm2world.core.target.gltf.tiles_data;

import java.util.ArrayList;
import java.util.List;

/**
* // Example tileset
"content": {
"uri": "14_5298_5916_0.glb"
},
"refine": "ADD",
"geometricError": 25,
"boundingVolume": {
"region": [-1.1098350999480917,0.7790694465970149,-1.1094516048185785,0.779342292568195,0.0,100]
},
"transform": [
0.895540041198885, 0.4449809373551852, 0.0, 0.0,
-0.31269461895546163, 0.6293090971636892, 0.7114718093525005, 0.0,
0.3165913926274654, -0.6371514934593837, 0.7027146394495273, 0.0,
2022609.150078308, -4070573.2078238726, 4459382.83869308, 1.0
],
"children": [{
"boundingVolume": {
"region": [-1.1098350999480917,0.7790694465970149,-1.1094516048185785,0.779342292568195,0.0,97.49999999999997]
},
"geometricError": 0,
"content": {
"uri": "14_5298_5916.glb"
}
}]
*/
public class TilesetParentEntry extends TilesetEntry {

private String refine = "ADD";
private double[] transform;
private List<TilesetEntry> children;

public TilesetParentEntry() {
}

public String getRefine() {
return refine;
}
public void setRefine(String refine) {
this.refine = refine;
}

public double[] getTransform() {
return transform;
}
public void setTransform(double[] transform) {
this.transform = transform;
}

public List<TilesetEntry> getChildren() {
return children;
}
public void setChildren(List<TilesetEntry> children) {
this.children = children;
}

public void addChild(TilesetEntry chld) {
if (this.children == null) {
this.children = new ArrayList<>();
}
this.children.add(chld);
}

public void addChild(String contentUri) {
this.addChild(new TilesetEntry(
0,
this.getBoundingVolume().getRegion(),
contentUri
));

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.osm2world.core.target.gltf.tiles_data;

public class TilesetRoot {
private TilesetAsset asset;
private TilesetParentEntry root;

public TilesetRoot() {
}

public TilesetAsset getAsset() {
return asset;
}
public void setAsset(TilesetAsset asset) {
this.asset = asset;
}

public TilesetParentEntry getRoot() {
return root;
}
public void setRoot(TilesetParentEntry root) {
this.root = root;
}

}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package org.osm2world.viewer.control.actions;

import java.awt.*;
import java.awt.HeadlessException;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.Serial;
import java.util.Locale;

import javax.swing.*;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileNameExtensionFilter;

import org.osm2world.core.ConversionFacade.Results;
import org.osm2world.core.target.TargetUtil;
import org.osm2world.core.target.TargetUtil.Compression;
import org.osm2world.core.target.gltf.GltfTarget;
@@ -46,12 +47,17 @@ protected void performExport(File file) throws HeadlessException {

try {

boolean underground = data.getConfig() == null || data.getConfig().getBoolean("renderUnderground", true);
boolean underground = data.getConfig() == null ||
data.getConfig().getBoolean("renderUnderground", true);

Results conversionResults = data.getConversionResults();

/* write the file */
GltfTarget gltfTarget = new GltfTarget(file, flavor, Compression.NONE,null);
GltfTarget gltfTarget = new GltfTarget(file, flavor, Compression.NONE, conversionResults.getMapProjection(), null);
gltfTarget.setConfiguration(data.getConfig());
TargetUtil.renderWorldObjects(gltfTarget, data.getConversionResults().getMapData(), underground);

TargetUtil.renderWorldObjects(gltfTarget, conversionResults.getMapData(), underground);

gltfTarget.finish();

messageManager.addMessage("exported glTF file " + file);