Evolution of a Renderer: Dismantling Viewports for a Unified 3D World in Godot 4

The architectural evolution of the rendering pipeline in the space 4X/roguelite hybrid Project: Hadean recently shifted from an isolated multi-viewport structure to a single, high-performance 3D scene.

The Discarded Path: The Hybrid 2D/3D Viewport Setup

The initial rendering layer nested an isolated 3D SubViewport inside a 2D SubViewportContainer for every single planet. This approach aimed to let 2D orbit physics coexist with fully shaded 3D planetary surfaces. However, scaling this architecture introduced severe performance bottlenecks:

Lifecycle Race Conditions: Initialization logic inside _enter_tree() triggered frequent ERR_FILE_NOT_FOUND engine failures because global configuration files loaded before @onready node references were resolved. Moving setup to _ready() and caching node references resolved the issue, eliminating frame micro-stutters caused by repeated get_node_or_null() lookups.

VRAM and Resolution Spikes: Every planet rendered its own isolated World3D, duplicating render passes and light setups. Forcing runtime viewport resizing between 64×64 and 2048×2048 via UPDATE_WHEN_VISIBLE failed to keep VRAM overhead within stable bounds as the simulated solar systems expanded.

The Breakthrough: Migration to a Unified World3D

To ensure stable VRAM usage and fluid frame rates, the multi-viewport architecture was completely dismantled. All planetary bodies now inhabit a single, monolithic Node3D scene sharing a common World3D instance, viewed through a locked top-down orthographic Camera3D that handles panning and zoom. Real-time shadows are now efficiently handled via distance-culling rather than being baked per-object.

Shading and Orthographic Lighting Refactors

Planet shading completely overrides Godot’s default PBR lighting pass for maximum performance. Custom lighting logic is executed directly inside the light() shader function, utilizing the ATTENUATION parameter for pure occlusion mapping.

Fixing the sun lighting system to track correct orbital positions under top-down orthography required three major iterations:

DirectionalLight3D: Produced parallel light rays from infinity, causing the day/night terminator line to twist 90–180° during planet orbits.

Synced Global Vectors: Rebuilding shaders to use MODEL_MATRIX * vec4(TANGENT, 0.0) isolated normal mapping from camera transformations, preventing normals from collapsing into funnel-shaped artifacts at the poles. However, directional lighting still failed to track the real on-screen position of a nearby point-source sun.

OmniLight3D Nodes: Spawning an OmniLight3D node at the exact spatial coordinates of the sun successfully resolved the polar vector collapse and fixed the terminator tracking.

Resolved Migration Traps

Tree Membership Trap: Querying GlobalPosition during synchronous node setup before integration into the active scene graph caused engine failures with !is_inside_tree() warnings. Implementing explicit is_inside_tree() validation guards resolved the hidden failures.

Unit Scaling Discrepancy: A legacy sprite-era pixel scale factor was accidentally mapped to the 3D mesh transformation, shrinking planets down to ~6 world units. Metrics are now strictly locked to canonical world-unit radius fields.

1 Like