Skip to content
Merged
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
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Input database table contains following columns:
. pitch - double with pitch in degrees;
. roll - double with roll in degrees;

Non-GPU (i3dm) mode supports yaw/pitch/roll. For backwards compatibility, if `yaw/pitch/roll` columns are missing the tool will fall back to legacy `rotation` (deprecated) and print a warning (rotation is treated as yaw; pitch/roll = 0).
For backwards compatibility, if `yaw/pitch/roll` columns are missing the tool will fall back to legacy `rotation` (deprecated) and print a warning (rotation is treated as yaw; pitch/roll = 0).

. tags - json with instance attribute information;

Expand Down Expand Up @@ -133,11 +133,9 @@ displaying the Instanced 3D Tiles, like 3DTilesRenderer/Giro3D/ITowns/QGIS Web C
When using `--keep_projection=true`, the following limitations apply:

- **Scale**: Works correctly in both GPU and non-GPU modes
- **With `--use_gpu_instancing=true`**: Incorrect rotation (ECEF rotation is still applied, which is wrong for Cartesian coordinates)
- **Rotation (yaw/pitch/roll)**: Only supported with `--use_gpu_instancing=true`.
- **With `--use_gpu_instancing=false`**: Yaw, pitch, and roll are not supported (all set to 0). Only the base model rotation (90° X-axis + 180° Z-axis) is applied.

For Cartesian projection mode, it is recommended to use `--use_gpu_instancing=false` until full rotation support is implemented.

## Docker

See https://hub.docker.com/r/geodan/i3dm.export
Expand Down
98 changes: 0 additions & 98 deletions docs/technical-transforms.md

This file was deleted.

3 changes: 1 addition & 2 deletions i3dm.export.sln
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.4.11506.43 insiders
VisualStudioVersion = 18.4.11506.43
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{68333F7F-D9B8-4215-AE46-6B3AFF14FFBB}"
ProjectSection(SolutionItems) = preProject
docs\Box.glb = docs\Box.glb
docs\getting_started.md = docs\getting_started.md
README.md = README.md
docs\screenshot.png = docs\screenshot.png
docs\technical-transforms.md = docs\technical-transforms.md
docs\trees.png = docs\trees.png
EndProjectSection
EndProject
Expand Down
37 changes: 37 additions & 0 deletions src/EnuCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,41 @@ public static (Vector3 East, Vector3 North, Vector3 Up) GetLocalEnuCesium(Vector

return (east, north, up);
}

public static (Vector3 East, Vector3 North, Vector3 Up) GetLocalCartesianBasis(double yaw, double pitch = 0, double roll = 0)
{
// Cartesian mode: start with identity basis (X=East, Y=North, Z=Up)
var east = new Vector3(1, 0, 0);
var north = new Vector3(0, 1, 0);
var up = new Vector3(0, 0, 1);

// Apply rotations using same convention as ECEF mode (clockwise-positive).
// Yaw: rotation around Up axis (heading)
if (yaw != 0)
{
east = Vector3.Normalize(Rotator.RotateVector(east, up, yaw));
north = Vector3.Normalize(Rotator.RotateVector(north, up, yaw));
}

// Pitch: rotation around East axis
if (pitch != 0)
{
north = Vector3.Normalize(Rotator.RotateVector(north, east, pitch));
up = Vector3.Normalize(Rotator.RotateVector(up, east, pitch));
}

// Roll: rotation around North/Forward axis
if (roll != 0)
{
east = Vector3.Normalize(Rotator.RotateVector(east, north, roll));
up = Vector3.Normalize(Rotator.RotateVector(up, north, roll));
}

// Re-orthonormalize to avoid drift.
east = Vector3.Normalize(east);
north = Vector3.Normalize(Vector3.Cross(up, east));
up = Vector3.Normalize(Vector3.Cross(east, north));

return (east, north, up);
}
}
109 changes: 76 additions & 33 deletions src/GPUTileHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
namespace i3dm.export;
public static class GPUTileHandler
{
public static void SaveGPUTile(string filePath, List<Instance> instances, bool UseScaleNonUniform)
public static void SaveGPUTile(string filePath, List<Instance> instances, bool UseScaleNonUniform, bool keepProjection = false)
{
var externalTextures = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

var model = BuildGpuModel(instances, UseScaleNonUniform, externalTextures);
var model = BuildGpuModel(instances, UseScaleNonUniform, externalTextures, keepProjection);

// If textures are embedded in the input model, keep them embedded in the output GLB.
var hasEmbeddedImages = model.LogicalImages.Any(i => !i.Content.IsEmpty && string.IsNullOrWhiteSpace(i.Content.SourcePath));
Expand All @@ -35,19 +35,20 @@ public static void SaveGPUTile(string filePath, List<Instance> instances, bool U
model.SaveGLB(filePath, writeSettings);
}

public static byte[] GetGPUTile(List<Instance> instances, bool UseScaleNonUniform)
public static byte[] GetGPUTile(List<Instance> instances, bool UseScaleNonUniform, bool keepProjection = false)
{
var model = BuildGpuModel(instances, UseScaleNonUniform);
var model = BuildGpuModel(instances, UseScaleNonUniform, null, keepProjection);
return model.WriteGLB().Array;
}

private static ModelRoot BuildGpuModel(List<Instance> instances, bool UseScaleNonUniform, Dictionary<string, string> externalTextures = null)
private static ModelRoot BuildGpuModel(List<Instance> instances, bool UseScaleNonUniform, Dictionary<string, string> externalTextures = null, bool keepProjection = false)
{
var firstPosition = (Point)instances[0].Position;
// Always convert to Y-up for glTF, regardless of keepProjection
var translation = ToYUp(firstPosition);

var meshNodeCountsByModel = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var sceneBuilder = AddModels(instances, translation, UseScaleNonUniform, externalTextures, meshNodeCountsByModel);
var sceneBuilder = AddModels(instances, translation, UseScaleNonUniform, externalTextures, meshNodeCountsByModel, keepProjection);

var settings = SceneBuilderSchema2Settings.WithGpuInstancing;
settings.GpuMeshInstancingMinCount = 0;
Expand Down Expand Up @@ -82,6 +83,7 @@ private static ModelRoot BuildGpuModel(List<Instance> instances, bool UseScaleNo

foreach (var node in model.LogicalNodes)
{
// glTF always uses Y-up, so translation is already in Y-up format
var tra = new Vector3((float)translation.X, (float)translation.Y, (float)translation.Z);
node.LocalTransform *= Matrix4x4.CreateTranslation(tra);
}
Expand All @@ -108,7 +110,7 @@ private static StructuralMetadataClass AddMetadataSchema(ModelRoot gltf)
return schemaClass;
}

private static SceneBuilder AddModels(IEnumerable<Instance> instances, Point translation, bool UseScaleNonUniform, Dictionary<string, string> externalTextures = null, Dictionary<string, int> meshNodeCountsByModel = null)
private static SceneBuilder AddModels(IEnumerable<Instance> instances, Point translation, bool UseScaleNonUniform, Dictionary<string, string> externalTextures = null, Dictionary<string, int> meshNodeCountsByModel = null, bool keepProjection = false)
{
var sceneBuilder = new SceneBuilder();

Expand All @@ -121,14 +123,14 @@ private static SceneBuilder AddModels(IEnumerable<Instance> instances, Point tra

ExternalTextureHelper.CollectExternalTextures(externalTextures, modelPath, modelRoot);

var meshNodeCount = AddModelInstancesToScene(sceneBuilder, instances, UseScaleNonUniform, translation, modelPath, modelRoot);
var meshNodeCount = AddModelInstancesToScene(sceneBuilder, instances, UseScaleNonUniform, translation, modelPath, modelRoot, keepProjection);
if (meshNodeCountsByModel != null) meshNodeCountsByModel[modelPath] = meshNodeCount;
}

return sceneBuilder;
}

private static int AddModelInstancesToScene(SceneBuilder sceneBuilder, IEnumerable<Instance> instances, bool UseScaleNonUniform, Point translation, string model, ModelRoot modelRoot)
private static int AddModelInstancesToScene(SceneBuilder sceneBuilder, IEnumerable<Instance> instances, bool UseScaleNonUniform, Point translation, string model, ModelRoot modelRoot, bool keepProjection = false)
{
var modelInstances = instances.Where(s => s.Model.Equals(model)).ToList();
var pointId = 0;
Expand All @@ -147,7 +149,7 @@ private static int AddModelInstancesToScene(SceneBuilder sceneBuilder, IEnumerab
{
foreach (var (meshBuilder, nodeWorldMatrix) in meshNodes)
{
var sceneBuilderModel = GetSceneBuilder(meshBuilder, nodeWorldMatrix, instance, UseScaleNonUniform, translation, pointId);
var sceneBuilderModel = GetSceneBuilder(meshBuilder, nodeWorldMatrix, instance, UseScaleNonUniform, translation, pointId, keepProjection);
sceneBuilder.AddScene(sceneBuilderModel, Matrix4x4.Identity);
}

Expand All @@ -170,9 +172,9 @@ private static void CollectNodesWithMeshes(Node node, List<(IMeshBuilder<Materia
}
}

private static SceneBuilder GetSceneBuilder(IMeshBuilder<MaterialBuilder> meshBuilder, Matrix4x4 nodeWorldMatrix, Instance instance, bool UseScaleNonUniform, Point translation, int pointId)
private static SceneBuilder GetSceneBuilder(IMeshBuilder<MaterialBuilder> meshBuilder, Matrix4x4 nodeWorldMatrix, Instance instance, bool UseScaleNonUniform, Point translation, int pointId, bool keepProjection = false)
{
var instanceTransform = GetInstanceTransform(instance, UseScaleNonUniform, translation);
var instanceTransform = GetInstanceTransform(instance, UseScaleNonUniform, translation, keepProjection);
var nodeTransform = new AffineTransform(nodeWorldMatrix);
var combinedTransform = AffineTransform.Multiply(in nodeTransform, in instanceTransform);

Expand All @@ -182,37 +184,78 @@ private static SceneBuilder GetSceneBuilder(IMeshBuilder<MaterialBuilder> meshBu
return sceneBuilder;
}

private static AffineTransform GetInstanceTransform(Instance instance, bool UseScaleNonUniform, Point translation)
private static AffineTransform GetInstanceTransform(Instance instance, bool UseScaleNonUniform, Point translation, bool keepProjection = false)
{
var point = (Point)instance.Position;

var position = ToYUp(point);
Vector3 position2;
Quaternion res;

// Use the same angle convention as non-GPU instancing (I3DM): degrees, clockwise-positive.
// yaw : rotation around local Up axis ("heading")
// pitch : rotation around local East/Right axis
// roll : rotation around local Forward axis
var positionVector3 = new Vector3((float)point.X, (float)point.Y, (float)point.Z);
if (keepProjection)
{
// Cartesian mode: positions are in local XYZ coordinates (X=East, Y=North, Z=Up)
// Transform to glTF Y-up space

// Position: transform from Cartesian XYZ to Y-up, then compute offset from RTC
var cartesianPos = new Vector3(
(float)point.X,
(float)point.Y,
(float)point.Z.GetValueOrDefault());

var cartesianPosYUp = ToYUp(cartesianPos);

// translation is already in Y-up format (ToYUp applied in BuildGpuModel)
position2 = new Vector3(
cartesianPosYUp.X - (float)translation.X,
cartesianPosYUp.Y - (float)translation.Y,
cartesianPosYUp.Z - (float)translation.Z);

// Rotation: apply yaw/pitch/roll in local Cartesian frame
var (east, north, up) = EnuCalculator.GetLocalCartesianBasis(instance.Yaw, instance.Pitch, instance.Roll);

// Convert Cartesian basis (Z-up) to glTF Y-up space
var eastYUp = Vector3.Normalize(ToYUp(east));
var northYUp = Vector3.Normalize(ToYUp(north));
var upYUp = Vector3.Normalize(ToYUp(up));

// Re-orthonormalize for numerical stability
var forwardYUp = Vector3.Normalize(Vector3.Cross(eastYUp, upYUp));
upYUp = Vector3.Normalize(Vector3.Cross(forwardYUp, eastYUp));

var m4 = GetTransformationMatrix((eastYUp, new Vector3(0, 0, 0), upYUp), forwardYUp);
res = Quaternion.CreateFromRotationMatrix(m4);
}
else
{
// ECEF mode: convert to glTF Y-up space
var position = ToYUp(point);

// Use the same angle convention as non-GPU instancing (I3DM): degrees, clockwise-positive.
// yaw : rotation around local Up axis ("heading")
// pitch : rotation around local East/Right axis
// roll : rotation around local Forward axis
var positionVector3 = new Vector3((float)point.X, (float)point.Y, (float)point.Z.GetValueOrDefault());

var enu = EnuCalculator.GetLocalEnuCesium(positionVector3, instance.Yaw, instance.Pitch, instance.Roll);
var enu = EnuCalculator.GetLocalEnuCesium(positionVector3, instance.Yaw, instance.Pitch, instance.Roll);

var east = Vector3.Normalize(enu.East);
var up = Vector3.Normalize(enu.Up);
var forward = Vector3.Normalize(enu.North);
var east = Vector3.Normalize(enu.East);
var up = Vector3.Normalize(enu.Up);
var forward = Vector3.Normalize(enu.North);

// Convert basis from ECEF to glTF Y-up and build the quaternion in that coordinate system.
var eastYUp = Vector3.Normalize(ToYUp(east));
var upYUp = Vector3.Normalize(ToYUp(up));
var forwardYUp = Vector3.Normalize(ToYUp(forward));
// Convert basis from ECEF to glTF Y-up and build the quaternion in that coordinate system.
var eastYUp = Vector3.Normalize(ToYUp(east));
var upYUp = Vector3.Normalize(ToYUp(up));
var forwardYUp = Vector3.Normalize(ToYUp(forward));

// Orthonormalize (numerical stability).
forwardYUp = Vector3.Normalize(Vector3.Cross(eastYUp, upYUp));
upYUp = Vector3.Normalize(Vector3.Cross(forwardYUp, eastYUp));
// Orthonormalize (numerical stability).
forwardYUp = Vector3.Normalize(Vector3.Cross(eastYUp, upYUp));
upYUp = Vector3.Normalize(Vector3.Cross(forwardYUp, eastYUp));

var m4 = GetTransformationMatrix((eastYUp, new Vector3(0, 0, 0), upYUp), forwardYUp);
var res = Quaternion.CreateFromRotationMatrix(m4);
var m4 = GetTransformationMatrix((eastYUp, new Vector3(0, 0, 0), upYUp), forwardYUp);
res = Quaternion.CreateFromRotationMatrix(m4);

var position2 = new Vector3((float)(position.X - translation.X), (float)(position.Y - translation.Y), (float)(position.Z - translation.Z));
position2 = new Vector3((float)(position.X - translation.X), (float)(position.Y - translation.Y), (float)(position.Z - translation.Z));
}

var scale = UseScaleNonUniform ?
new Vector3((float)instance.ScaleNonUniform[0], (float)instance.ScaleNonUniform[1], (float)instance.ScaleNonUniform[2]) :
Expand Down
Loading