Skip to content
Open
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion Obj2Tiles/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public sealed class Options
[Option("scale", Required = false, HelpText = "Scale for data if using units other than meters ( 1200.0/3937.0 for survey ft)", Default = 1.0)]
public double Scale { get; set; }

[Option('e',"error", Required = false, HelpText = "Base error for root node", Default = 100.0)]
[Option('e',"error", Required = false, HelpText = "Base geometric error for the root node, in the model's coordinate units. When 0 (default) it is derived automatically from the model's bounding box diagonal.", Default = 0.0)]
public double BaseError { get; set; }

[Option("use-system-temp", Required = false, HelpText = "Uses the system temp folder", Default = false)]
Expand Down
36 changes: 33 additions & 3 deletions Obj2Tiles/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,47 @@ private static async Task Run(Options opts)
Console.WriteLine();
Console.WriteLine($" => Tiling stage {(gpsCoords != null ? $"with GPS coords {gpsCoords}" : "")}");

var baseError = opts.BaseError;
// Geometric error must be expressed in the model's own coordinate units because it
// drives the screen-space-error refinement of every 3D Tiles renderer. A fixed value
// is meaningless for models that are not that size, so when the caller does not force
// one (--error 0, the default) derive it from the model's bounding box diagonal.
var b = decimateRes.Bounds;
var modelDiagonal = Math.Sqrt(b.Width * b.Width + b.Height * b.Height + b.Depth * b.Depth);
var baseError = opts.BaseError > 0 ? opts.BaseError : modelDiagonal;
if (!(baseError > 0)) baseError = 1.0;
Comment on lines +98 to +100

// Coarsest decimated whole-model mesh, used to give the tileset root renderable content
// (an empty root tile leaves the model invisible in renderers that do not descend into
// the children of a content-less root). Run it through the split stage with 0 divisions,
// which keeps it as a single mesh but compresses its textures, so the bootstrap root tile
// stays small instead of embedding the full-resolution source textures.
string? rootSourceObj = null;
if (decimateRes.DestFiles.Length > 0)
{
rootSourceObj = decimateRes.DestFiles[^1];
try
{
var rootTempDir = createTempFolder($"{pipelineId}-obj2tiles-root");
await StagesFacade.Split(rootSourceObj, rootTempDir, 0);
var compressedRoot = Directory.GetFiles(rootTempDir, "*.obj").FirstOrDefault();
if (compressedRoot != null)
rootSourceObj = compressedRoot;
}
catch (Exception ex)
{
Console.WriteLine($" !> Could not compress root mesh ({ex.Message}); using full-resolution coarsest mesh.");
}
}

Console.WriteLine();
Console.WriteLine($" => Tiling stage with baseError {baseError}");
Console.WriteLine($" => Tiling stage with baseError {baseError:0.000}");

sw.Restart();

if (opts.LocalMode && (opts.Latitude != null || opts.Longitude != null))
Console.WriteLine(" !> Warning: --local overrides --lat/--lon. ECEF transform will not be applied.");

StagesFacade.Tile(destFolderSplit, opts.Output, opts.LODs, opts.BaseError, boundsMapper, gpsCoords, opts.LocalMode, opts.Octree);
StagesFacade.Tile(destFolderSplit, opts.Output, opts.LODs, baseError, boundsMapper, gpsCoords, opts.LocalMode, opts.Octree, rootSourceObj);

Console.WriteLine(" ?> Tiling stage done in {0}", sw.Elapsed);
}
Expand Down
30 changes: 28 additions & 2 deletions Obj2Tiles/Stages/TilingStage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,36 @@ namespace Obj2Tiles.Stages;
public static partial class StagesFacade
{
public static void Tile(string sourcePath, string destPath, int lods, double baseError, Dictionary<string, Box3>[] boundsMapper,
GpsCoords? coords = null, bool localMode = false, bool isOctree = false)
GpsCoords? coords = null, bool localMode = false, bool isOctree = false, string? rootSourceObj = null)
{

Console.WriteLine(" ?> Working on objs conversion");

ConvertAllB3dm(sourcePath, destPath, lods);

// Give the tileset root renderable content. The root tile spans the whole model but, in an
// octree/multi-tile layout, its geometry lives only in the child tiles, leaving the root
// empty (content: null). An empty root is legal per the 3D Tiles spec, but several renderers
// (e.g. giro3d's 3d-tiles-renderer, which hardcodes LOAD_ROOT_SIBLINGS) will not descend into
// the children of a content-less root, so the whole model never appears. Converting the
// coarsest decimated whole-model mesh into "root.b3dm" gives the root a lightweight, complete
// representation that is then refined (REPLACE) by the finer child tiles.
string? rootContentUri = null;
if (rootSourceObj != null && File.Exists(rootSourceObj))
{
try
{
var rootB3dm = Path.Combine(destPath, "root.b3dm");
Utils.ConvertB3dm(rootSourceObj, rootB3dm);
rootContentUri = "root.b3dm";
Console.WriteLine($" ?> Generated root content from '{Path.GetFileName(rootSourceObj)}'");
}
catch (Exception ex)
{
Console.WriteLine($" !> Could not generate root content ({ex.Message}); the root tile will be empty.");
}
}
Comment on lines +22 to +43

Console.WriteLine(" -> Generating tileset.json");

double[] rootTransform;
Expand Down Expand Up @@ -52,8 +75,11 @@ public static void Tile(string sourcePath, string destPath, int lods, double bas
Root = new TileElement
{
GeometricError = baseError,
Refine = "ADD",
// REPLACE (not ADD): the root's coarse whole-model content is superseded by the finer
// child tiles as they load, so the two never render on top of each other.
Refine = "REPLACE",
Transform = rootTransform,
Content = rootContentUri != null ? new Content { Uri = rootContentUri } : null,
Comment on lines +78 to +82
}
};

Expand Down
Loading