|
| 1 | +# Technical notes: coordinate systems & transforms |
| 2 | + |
| 3 | +This document explains how `i3dm.export` computes **translation**, **rotation**, and **scale** for instances in both output modes: |
| 4 | + |
| 5 | +- `--use_gpu_instancing=true`: outputs a `.glb` that uses `EXT_mesh_gpu_instancing`. |
| 6 | +- `--use_gpu_instancing=false`: outputs `i3dm` (or `cmpt` containing `i3dm`) using `NORMAL_UP` / `NORMAL_RIGHT`. |
| 7 | + |
| 8 | +It also documents the coordinate-system conversions between **ECEF (EPSG:4978)** and **glTF Y-up**, and clarifies the **matrix conventions** used by glTF vs `System.Numerics`. |
| 9 | + |
| 10 | +## 1) Coordinate systems |
| 11 | + |
| 12 | +### 1.1 ECEF (EPSG:4978) |
| 13 | +Internally, instance positions are converted to **ECEF** (Earth-Centered, Earth-Fixed) coordinates. |
| 14 | + |
| 15 | +- Right-handed coordinate system. |
| 16 | +- Units: meters. |
| 17 | +- Axes (typical convention): |
| 18 | + - +X: intersection of equator and prime meridian. |
| 19 | + - +Y: 90° east on equator. |
| 20 | + - +Z: north pole. |
| 21 | + |
| 22 | +### 1.2 Local tangent frame (ENU) |
| 23 | +For each instance position we derive a local tangent basis: |
| 24 | + |
| 25 | +- **E**: East (tangent) |
| 26 | +- **N**: North (tangent) |
| 27 | +- **U**: Up (surface normal / ellipsoid normal) |
| 28 | + |
| 29 | +This ENU basis is right-handed: |
| 30 | + |
| 31 | +``` |
| 32 | +E × N = U |
| 33 | +``` |
| 34 | + |
| 35 | +Implementation note: |
| 36 | +- `SpatialConverter.EcefToEnu(position)` computes an orthonormal E/N/U basis at that ECEF position. |
| 37 | +- `EnuCalculator.GetLocalEnuCesium(position, heading, pitch, roll)` starts from that base frame and applies yaw/pitch/roll. |
| 38 | + |
| 39 | +### 1.3 glTF space (Y-up) |
| 40 | +glTF uses a **right-handed** coordinate system with: |
| 41 | + |
| 42 | +- +X right |
| 43 | +- +Y up |
| 44 | +- +Z forward |
| 45 | + |
| 46 | +The exporter outputs **Y-up** glTF. |
| 47 | + |
| 48 | +### 1.4 ECEF → glTF Y-up swizzle |
| 49 | +The exporter maps vectors/points from ECEF to glTF Y-up using the same swizzle in both position and orientation code: |
| 50 | + |
| 51 | +``` |
| 52 | +ToYUp(x, y, z) = ( x, z, -y ) |
| 53 | +``` |
| 54 | + |
| 55 | +So: |
| 56 | +- ECEF +Z (up-ish) becomes glTF +Y. |
| 57 | +- ECEF +Y becomes glTF -Z. |
| 58 | + |
| 59 | +This keeps the resulting glTF basis right-handed. |
| 60 | + |
| 61 | +## 2) Angle conventions (Yaw / Pitch / Roll) |
| 62 | + |
| 63 | +Angles are in **degrees**. |
| 64 | + |
| 65 | +For GPU instancing, the instance record provides: |
| 66 | + |
| 67 | +- **Yaw**: rotation around local **Up** axis (heading) |
| 68 | +- **Pitch**: rotation around local **East** axis |
| 69 | +- **Roll**: rotation around local **North/Forward** axis |
| 70 | + |
| 71 | +Important: the code uses the same convention as the legacy non-GPU rotation: **clockwise-positive** (as seen from the positive axis direction). |
| 72 | + |
| 73 | +Implementation note: |
| 74 | +- `Rotator.RotateVector(...)` converts clockwise-positive degrees into the standard right-hand-rule rotation by using `360 - angle` internally. |
| 75 | + |
| 76 | +## 3) Matrix conventions: glTF vs System.Numerics |
| 77 | + |
| 78 | +### 3.1 glTF convention |
| 79 | +- Matrices are stored **column-major** in the file. |
| 80 | +- Transforms conceptually use **column vectors**: |
| 81 | + |
| 82 | +``` |
| 83 | +world = M * local |
| 84 | +``` |
| 85 | + |
| 86 | +### 3.2 System.Numerics convention |
| 87 | +`System.Numerics.Matrix4x4` + `Vector3.Transform(v, M)` uses **row-vector semantics**: |
| 88 | + |
| 89 | +``` |
| 90 | +world = local * M |
| 91 | +``` |
| 92 | + |
| 93 | +This is the single biggest source of confusion when converting between math written for glTF/Cesium (column vectors) and code using `System.Numerics`. |
| 94 | + |
| 95 | +### 3.3 How this repo constructs rotation matrices |
| 96 | +When we build a rotation matrix from a basis, we intentionally store the **world basis vectors in the matrix rows** so that: |
| 97 | + |
| 98 | +- local X maps to East |
| 99 | +- local Y maps to Up |
| 100 | +- local Z maps to Forward |
| 101 | + |
| 102 | +With row-vector semantics that means: |
| 103 | + |
| 104 | +``` |
| 105 | +(1,0,0) * M = East |
| 106 | +(0,1,0) * M = Up |
| 107 | +(0,0,1) * M = Forward |
| 108 | +``` |
| 109 | + |
| 110 | +This is why `GPUTileHandler.GetTransformationMatrix(...)` writes basis vectors into the **rows**. |
| 111 | + |
| 112 | +## 4) Export mode: `--use_gpu_instancing=true` (EXT_mesh_gpu_instancing) |
| 113 | + |
| 114 | +### 4.1 What glTF applies at runtime |
| 115 | +In glTF, each mesh node has a node transform (TRS or matrix). With `EXT_mesh_gpu_instancing`, each instance adds its own TRS. |
| 116 | + |
| 117 | +Conceptually (glTF / column vector notation): |
| 118 | + |
| 119 | +``` |
| 120 | +worldVertex = NodeWorld * InstanceTRS * vertex |
| 121 | +``` |
| 122 | + |
| 123 | +(Where `NodeWorld` includes the full scene graph above the mesh node.) |
| 124 | + |
| 125 | +### 4.2 Preserving node transforms from the input model |
| 126 | +Many models (including Blender exports) include **axis-correction** or other transforms in the scene graph. |
| 127 | +If we drop them, some nodes will be misplaced or rotated. |
| 128 | + |
| 129 | +This exporter preserves per-node transforms by collecting all nodes with meshes and using their `node.WorldMatrix`. |
| 130 | + |
| 131 | +Code path: |
| 132 | +- `GPUTileHandler.CollectNodesWithMeshes(...)` |
| 133 | +- Each mesh node contributes: |
| 134 | + - the mesh geometry |
| 135 | + - the node’s `WorldMatrix` |
| 136 | + |
| 137 | +### 4.3 Instance TRS calculation |
| 138 | +For each instance we compute: |
| 139 | + |
| 140 | +1) **Position** (ECEF) → **glTF Y-up** using `ToYUp(Point)`. |
| 141 | + |
| 142 | +2) **Orientation**: |
| 143 | + - Compute ENU basis at the ECEF position. |
| 144 | + - Apply yaw/pitch/roll in ENU. |
| 145 | + - Swizzle each basis vector to glTF Y-up: `ToYUp(Vector3)`. |
| 146 | + - Re-orthonormalize to reduce numerical drift. |
| 147 | + - Build a rotation matrix whose rows are `{East, Up, Forward}`. |
| 148 | + - Convert to quaternion with `Quaternion.CreateFromRotationMatrix`. |
| 149 | + |
| 150 | +3) **Scale**: |
| 151 | + - Uniform: `Scale` |
| 152 | + - Non-uniform: `ScaleNonUniform[3]` when `--use_scale_non_uniform=true`. |
| 153 | + |
| 154 | +### 4.4 Combining node and instance transforms |
| 155 | +Each output mesh node uses: |
| 156 | + |
| 157 | +- `nodeTransform = node.WorldMatrix` (from the input model) |
| 158 | +- `instanceTransform = TRS` computed above |
| 159 | + |
| 160 | +And we create a combined transform: |
| 161 | + |
| 162 | +- `combined = nodeTransform * instanceTransform` |
| 163 | + |
| 164 | +(Exact multiplication order is handled by `AffineTransform.Multiply(...)` in SharpGLTF; the intended effect is “apply node’s authored transform, then apply instance TRS”.) |
| 165 | + |
| 166 | +### 4.5 RTC (relative-to-center) translation |
| 167 | +To keep numbers small, we use the first instance position as a per-tile translation anchor. |
| 168 | + |
| 169 | +- We subtract this anchor from each instance translation. |
| 170 | +- At the end, the anchor is applied back to nodes. |
| 171 | + |
| 172 | +This improves numerical precision in clients. |
| 173 | + |
| 174 | +## 5) Export mode: `--use_gpu_instancing=false` (i3dm) |
| 175 | + |
| 176 | +In i3dm, per-instance orientation is encoded via two vectors: |
| 177 | + |
| 178 | +- `NORMAL_RIGHT` (local +X) |
| 179 | +- `NORMAL_UP` (local +Y) |
| 180 | + |
| 181 | +The client derives the final orientation from these vectors. |
| 182 | + |
| 183 | +Current behavior: |
| 184 | +- The non-GPU path currently uses the legacy single **horizontal rotation** value (`rotation`) as a heading angle. |
| 185 | +- It derives `NORMAL_RIGHT` and `NORMAL_UP` from the ENU basis. |
| 186 | + |
| 187 | +(If/when full yaw/pitch/roll is enabled for i3dm, the same ENU + swizzle principles apply; only the storage format differs.) |
| 188 | + |
| 189 | +## 6) Common pitfalls / why models can look “tilted” |
| 190 | + |
| 191 | +1) **Mixing handedness or sign conventions** |
| 192 | + - Clockwise-positive vs right-hand-rule is easy to flip. |
| 193 | + |
| 194 | +2) **Row-vectors vs column-vectors** |
| 195 | + - A basis written into columns will be wrong when used with `Vector3.Transform(v, M)`. |
| 196 | + |
| 197 | +3) **Dropping input node transforms** |
| 198 | + - Some models rely on an axis-correction node; if ignored, the model will be sideways or parts won’t move. |
| 199 | + |
| 200 | +4) **Non-orthonormal basis drift** |
| 201 | + - Small floating point errors can accumulate; we re-orthonormalize the basis before creating quaternions. |
| 202 | + |
| 203 | +## 7) Code pointers |
| 204 | + |
| 205 | +- ENU basis: `src\Cesium\SpatialConverter.cs` (`EcefToEnu`) |
| 206 | +- Y-up swizzle: `src\GPUTileHandler.cs` (`ToYUp`) |
| 207 | +- Instance TRS (GPU): `src\GPUTileHandler.cs` (`GetInstanceTransform`) |
| 208 | +- Node transform preservation: `src\GPUTileHandler.cs` (`CollectNodesWithMeshes`) |
| 209 | +- Yaw/pitch/roll application: `src\EnuCalculator.cs` |
0 commit comments