6.9 KiB
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
- Double-precision world coordinates: Store authoritative positions as
glm::dvec3(WorldVec3). - Floating origin: Maintain a world-space origin point that follows the camera.
- 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)
// 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:
// 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()):
- Compute distance from camera to current origin.
- If distance exceeds threshold, snap camera position to grid and use as new origin.
- Recompute all local positions relative to new origin.
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:
- Static glTF scenes: Apply
world_to_local_roottransform that offsets by-origin. - Dynamic instances: Convert
translation_worldto local position, build TRS matrix. - Point lights: Convert
position_worldto local for GPU upload. - Camera: Store both world and local positions; view matrix uses local.
// 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:
// 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:
// 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
// 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
-
Store world positions in double: Use
WorldVec3/TransformDfor anything that persists or needs absolute positioning. -
Work in local space for rendering: All GPU-visible transforms should be in local coordinates.
-
Convert at boundaries: Convert world↔local at the scene update boundary, not per-frame in shaders.
-
Use snap size: Grid-aligned origins prevent micro-shifts that could cause subtle jitter.
-
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, notglm::vec3 world_to_local()is applied before GPU upload- Origin recentering is enabled (threshold > 0)
Related Files
src/core/world.h—WorldVec3type and conversion functionssrc/scene/vk_scene.h—SceneManagerorigin managementsrc/scene/vk_scene.cpp— Recentering logic inupdate_scene()src/scene/camera.h—Camera::position_worlddouble-precision positionsrc/core/game_api.h—Transform/TransformDAPI types