191 lines
6.9 KiB
Markdown
191 lines
6.9 KiB
Markdown
## Floating Origin & Double-Precision Coordinates
|
|
|
|
Precision-safe coordinate system that stores authoritative world positions in double precision and dynamically shifts the rendering origin to keep local float coordinates near zero.
|
|
|
|
### Problem
|
|
|
|
Single-precision floats (32-bit) have ~7 significant digits. At large world positions (e.g., 100km from origin), sub-meter precision is lost, causing:
|
|
|
|
- Vertex jitter / z-fighting
|
|
- Camera stutter during movement
|
|
- Picking inaccuracy
|
|
- Physics instability
|
|
|
|
### Solution
|
|
|
|
1. **Double-precision world coordinates**: Store authoritative positions as `glm::dvec3` (`WorldVec3`).
|
|
2. **Floating origin**: Maintain a world-space origin point that follows the camera.
|
|
3. **Local-space rendering**: Convert world positions to local float coordinates relative to the origin.
|
|
|
|
This keeps all render-local coordinates within a few hundred meters of (0,0,0), preserving full float precision.
|
|
|
|
### Core Types (`src/core/world.h`)
|
|
|
|
```c++
|
|
// Authoritative world-space coordinates (double precision)
|
|
using WorldVec3 = glm::dvec3;
|
|
|
|
// Convert world position to local float (for rendering)
|
|
glm::vec3 world_to_local(const WorldVec3 &world, const WorldVec3 &origin_world);
|
|
|
|
// Convert local float back to world position
|
|
WorldVec3 local_to_world(const glm::vec3 &local, const WorldVec3 &origin_world);
|
|
|
|
// Snap a world position to a grid (for stable origin placement)
|
|
WorldVec3 snap_world(const WorldVec3 &p, double grid_size);
|
|
```
|
|
|
|
### Origin Management (`SceneManager`)
|
|
|
|
The `SceneManager` automatically recenters the origin when the camera drifts too far:
|
|
|
|
```c++
|
|
// Private members in SceneManager
|
|
WorldVec3 _origin_world{0.0, 0.0, 0.0};
|
|
glm::vec3 _camera_position_local{0.0f, 0.0f, 0.0f};
|
|
double _floating_origin_recenter_threshold = 1000.0; // meters
|
|
double _floating_origin_snap_size = 100.0; // grid snap size
|
|
```
|
|
|
|
**Recentering Logic** (in `update_scene()`):
|
|
|
|
1. Compute distance from camera to current origin.
|
|
2. If distance exceeds threshold, snap camera position to grid and use as new origin.
|
|
3. Recompute all local positions relative to new origin.
|
|
|
|
```c++
|
|
if (_floating_origin_recenter_threshold > 0.0) {
|
|
const WorldVec3 d = mainCamera.position_world - _origin_world;
|
|
if (glm::length2(d) > threshold2) {
|
|
_origin_world = snap_world(mainCamera.position_world, _floating_origin_snap_size);
|
|
}
|
|
}
|
|
_camera_position_local = world_to_local(mainCamera.position_world, _origin_world);
|
|
```
|
|
|
|
### Coordinate Storage
|
|
|
|
| Component | Type | Description |
|
|
|-----------|------|-------------|
|
|
| `Camera::position_world` | `WorldVec3` | Camera world position (double) |
|
|
| `MeshInstance::translation_world` | `WorldVec3` | Instance world position |
|
|
| `GLTFInstance::translation_world` | `WorldVec3` | glTF instance world position |
|
|
| `PointLight::position_world` | `WorldVec3` | Light world position |
|
|
| `SceneManager::_origin_world` | `WorldVec3` | Current floating origin |
|
|
| `SceneManager::_camera_position_local` | `glm::vec3` | Camera in local/render space |
|
|
|
|
### Rendering Pipeline
|
|
|
|
During `update_scene()`, all world-space objects are converted to local coordinates:
|
|
|
|
1. **Static glTF scenes**: Apply `world_to_local_root` transform that offsets by `-origin`.
|
|
2. **Dynamic instances**: Convert `translation_world` to local position, build TRS matrix.
|
|
3. **Point lights**: Convert `position_world` to local for GPU upload.
|
|
4. **Camera**: Store both world and local positions; view matrix uses local.
|
|
|
|
```c++
|
|
// Root transform for static world content
|
|
const glm::mat4 world_to_local_root =
|
|
glm::translate(glm::mat4{1.f}, world_to_local(WorldVec3(0.0, 0.0, 0.0), _origin_world));
|
|
|
|
// Dynamic instance positioning
|
|
glm::vec3 tLocal = world_to_local(inst.translation_world, _origin_world);
|
|
glm::mat4 instanceTransform = make_trs_matrix(tLocal, inst.rotation, inst.scale);
|
|
```
|
|
|
|
### GameAPI Double-Precision Variants
|
|
|
|
The `GameAPI` exposes both float and double-precision transform types:
|
|
|
|
```c++
|
|
// Float-precision (relative/local usage)
|
|
struct Transform {
|
|
glm::vec3 position{0.0f};
|
|
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
|
|
glm::vec3 scale{1.0f};
|
|
};
|
|
|
|
// Double-precision (world-space usage)
|
|
struct TransformD {
|
|
glm::dvec3 position{0.0};
|
|
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
|
|
glm::vec3 scale{1.0f};
|
|
};
|
|
|
|
// API accepts both variants
|
|
bool add_gltf_instance(const std::string& name, const std::string& modelPath,
|
|
const Transform& transform, bool preloadTextures = true);
|
|
bool add_gltf_instance(const std::string& name, const std::string& modelPath,
|
|
const TransformD& transform, bool preloadTextures = true);
|
|
```
|
|
|
|
Similarly for `PointLight` / `PointLightD` and `IBLVolume` / `IBLVolumeD`.
|
|
|
|
### Picking Integration
|
|
|
|
Picking operates in local coordinates but returns world positions:
|
|
|
|
```c++
|
|
// Ray origin in local space
|
|
glm::vec3 rayOrigin = world_to_local(mainCamera.position_world, _origin_world);
|
|
|
|
// ... perform ray-object intersection in local space ...
|
|
|
|
// Convert hit position back to world
|
|
outWorldPos = local_to_world(bestHitPos, _origin_world);
|
|
```
|
|
|
|
### Query Functions
|
|
|
|
```c++
|
|
// Get current floating origin (world coordinates)
|
|
WorldVec3 SceneManager::get_world_origin() const;
|
|
|
|
// Get camera position in local/render coordinates
|
|
glm::vec3 SceneManager::get_camera_local_position() const;
|
|
```
|
|
|
|
### Configuration Parameters
|
|
|
|
| Parameter | Default | Description |
|
|
|-----------|---------|-------------|
|
|
| `_floating_origin_recenter_threshold` | 1000.0 | Distance (m) before recentering |
|
|
| `_floating_origin_snap_size` | 100.0 | Grid snap size (m) for new origin |
|
|
|
|
Setting `_floating_origin_recenter_threshold` to 0 or negative disables floating origin.
|
|
|
|
### Best Practices
|
|
|
|
1. **Store world positions in double**: Use `WorldVec3` / `TransformD` for anything that persists or needs absolute positioning.
|
|
|
|
2. **Work in local space for rendering**: All GPU-visible transforms should be in local coordinates.
|
|
|
|
3. **Convert at boundaries**: Convert world↔local at the scene update boundary, not per-frame in shaders.
|
|
|
|
4. **Use snap size**: Grid-aligned origins prevent micro-shifts that could cause subtle jitter.
|
|
|
|
5. **Consider threshold tuning**:
|
|
- Smaller threshold (500m): More frequent recenters, tighter precision
|
|
- Larger threshold (2000m): Fewer recenters, slightly reduced precision at edges
|
|
|
|
### Debugging
|
|
|
|
The engine UI displays origin and camera positions:
|
|
|
|
```
|
|
Origin (world): (1200.000, 0.000, 3400.000)
|
|
Camera (local): (23.456, 1.234, -12.789)
|
|
```
|
|
|
|
If you observe jitter at large world coordinates, verify:
|
|
- Positions are stored as `WorldVec3`, not `glm::vec3`
|
|
- `world_to_local()` is applied before GPU upload
|
|
- Origin recentering is enabled (threshold > 0)
|
|
|
|
### Related Files
|
|
|
|
- `src/core/world.h` — `WorldVec3` type and conversion functions
|
|
- `src/scene/vk_scene.h` — `SceneManager` origin management
|
|
- `src/scene/vk_scene.cpp` — Recentering logic in `update_scene()`
|
|
- `src/scene/camera.h` — `Camera::position_world` double-precision position
|
|
- `src/core/game_api.h` — `Transform` / `TransformD` API types |