Last week, we shipped a fully functional 3D solar system explorer for EVE Frontier Map—complete with dynamic icon grouping, starfield backgrounds, smooth camera transitions, and proper coordinate transformations. The entire journey from concept to production took just three intensive days.
This is the story of how we built it, the bugs we encountered, and the lessons learned along the way.
The Goal: Explore Every Planet and Moon
EVE Frontier's universe isn't just star systems connected by stargates—each solar system contains planets, moons, asteroid belts, and Lagrange Points scattered across millions of kilometers. Players need to scout mining sites, plan logistics routes, and identify strategic positions.
The challenge: how do you render celestial objects spanning 3.8 AU (569 million km) while keeping UI icons readable and performance smooth?
- Starfield Background: Rotating backdrop synced with universal coordinates
- Dynamic Icon Grouping: Automatic clustering of co-located celestial objects
- Smooth Transitions: 500ms camera animations with easing
- Coordinate Fixes: World-to-local space transformations
Day 1: The Coordinate Space Bug
The first working prototype had a critical flaw: group icons (clusters of nearby planets/moons) appeared ~1361 units offset from where they should be. Individual planet sprites rendered correctly, but grouped icons floated in the wrong location.
The Investigation
We added debug logging to trace the coordinate flow:
// Grouped planet positions (world space)
Planet A: { x: 1377.8, y: -14.6, z: 118.1 }
Planet B: { x: 1361.4, y: -16.2, z: 122.5 }
// Centroid calculation (correct in world space)
Centroid: { x: 1369.6, y: -15.4, z: 120.3 }
// Group icon THREE.Sprite position (wrong!)
Group Icon: { x: 1369.6, y: -15.4, z: 120.3 } // still in world space!
The bug was subtle: individual planet sprites were children of the solarSystemGroup container (positioned at the solar system's universe coordinates), so their local positions were automatically correct. But group icons were being created with world space positions without converting to local space.
The Fix
We needed to subtract the solar system's world position from the centroid:
// Convert world-space centroid to local space
const groupPosition = {
x: centroid.x - solarSystemGroup.position.x,
y: centroid.y - solarSystemGroup.position.y,
z: centroid.z - solarSystemGroup.position.z
};
sprite.position.set(groupPosition.x, groupPosition.y, groupPosition.z);
Day 2: The Aggressive Grouping Problem
After fixing coordinates, we hit a new issue: icons that were visually separated on screen were still grouping together. A planet 200 pixels away from its moon would cluster into a single icon.
The Naive Distance Approach
Our initial grouping used world-space distance thresholds:
const distance = Math.sqrt(
(a.x - b.x) ** 2 +
(a.y - b.y) ** 2 +
(a.z - b.z) ** 2
);
if (distance < 5000) { // 5000 km threshold
group(a, b);
}
The problem: 5000 km in world space translates to wildly different screen-space distances depending on camera zoom. At wide zoom levels, planets 5000 km apart appear 300+ pixels separated—but our algorithm still grouped them.
Dynamic Screen-Space Clustering
The solution: recalculate clusters every frame based on screen-space pixel distance:
function shouldGroup(obj1, obj2, camera) {
const screen1 = worldToScreen(obj1.position, camera);
const screen2 = worldToScreen(obj2.position, camera);
const pixelDistance = Math.sqrt(
(screen1.x - screen2.x) ** 2 +
(screen1.y - screen2.y) ** 2
);
return pixelDistance < 30; // 30px threshold
}
This approach dynamically adjusts grouping based on what the user actually sees. Zoom out → more grouping. Zoom in → groups dissolve into individual icons.
The Starfield Challenge
Early versions had a jarring visual problem: when transitioning from the universe view (star-filled background) to solar system view, the starfield would disappear, replaced by a solid black background. It felt like teleporting into a void.
Implementation Strategy
We reused the existing universe starfield renderer, but scaled it to match the solar system's local coordinate space:
// Universe starfield spans 5000 units
const STARFIELD_SCALE = 1750; // Scale for solar system view
starfield.position.copy(solarSystemGroup.position);
starfield.scale.setScalar(STARFIELD_SCALE);
starfield.visible = inSolarSystemView;
This creates a local starfield bubble around the solar system, giving players spatial context without overwhelming the scene.
Smooth Camera Transitions
We added 500ms animated transitions using Three.js tweening:
function transitionToSolarSystem(targetPosition, targetLookAt) {
const duration = 500; // milliseconds
const startPosition = camera.position.clone();
const startLookAt = controls.target.clone();
animateCamera(
startPosition, targetPosition,
startLookAt, targetLookAt,
duration,
easeInOutCubic
);
}
We used cubic easing (accelerate → cruise → decelerate) for natural motion. Linear easing felt robotic; cubic provides cinematic smoothness.
Day 3: The Sprite Transparency Bug
Just before deployment, we noticed planet icons had square black backgrounds instead of being fully transparent. The circular icon PNGs were rendering with visible rectangular bounds.
The Material Configuration
Three.js sprites require specific material settings for proper alpha blending:
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true, // Enable alpha channel
depthWrite: false, // Prevent z-fighting
depthTest: true, // Respect depth ordering
sizeAttenuation: false // Fixed pixel size
});
The missing properties were transparent: true and depthWrite: false. Without them, Three.js rendered the sprite's bounding box as opaque black.
The Sun Glow Filtering Problem
Our dynamic grouping algorithm had an unexpected side effect: it was clustering the sun's glow sprite (a massive, semi-transparent visual effect) with nearby planets. This created bizarre group labels like "Sun + Planet IV (3 objects)".
Sprite Type Filtering
The fix: only group celestial objects, not visual effects:
const GROUPABLE_TYPES = [
'PLANET',
'MOON',
'LAGRANGE_POINT',
'SUN' // The sun itself, not its glow
];
function shouldConsiderForGrouping(sprite) {
return GROUPABLE_TYPES.includes(sprite.userData.type) &&
!sprite.userData.isGlowEffect;
}
Production Metrics
After three days of intensive development, here's what we shipped:
- 13 major features completed (MVP + Phase 5)
- Dynamic screen-space grouping (30px threshold)
- Starfield background (1750× scale, rotating)
- Smooth camera transitions (500ms, cubic easing)
- Coordinate space transformations (world → local)
- Sprite transparency fixes
- Adaptive zoom (0.005 minimum, size-based multipliers)
- IndexedDB caching (67 MB database, 90-day cache)
- Clustering algorithm: <2ms per frame (100+ objects)
- Camera transitions: 60 FPS maintained throughout
- Database load: ~500ms from IndexedDB cache
- Initial network load: 3.2s on 4G (first visit only)
- November 6-8: MVP (11 features)
- November 8: Starfield + grouping implementation
- November 9: Coordinate bugs, transparency fixes, deployment
- Total: 3 intensive days from concept to production
Key Lessons Learned
1. Coordinate Spaces Are Tricky
When working with nested Three.js objects, always be explicit about whether positions are in world space or local space. Debug by logging positions at multiple transformation stages.
2. Screen-Space Beats World-Space for UI
Grouping algorithms that operate on world-space distances create frustrating UX at varying zoom levels. Screen-space thresholds (pixels) provide consistent behavior regardless of camera position.
3. Performance Budget for Frame-by-Frame Work
Recalculating clusters 60 times per second sounds expensive, but with proper spatial indexing and early exits, it's negligible. The UX improvement far outweighs the <2ms cost.
4. Visual Consistency Matters
The starfield background was a "nice-to-have" feature that became essential once we saw the stark transition from universe view. Small polish details like this dramatically improve perceived quality.
5. Iterative Debugging Wins
Every major bug (coordinates, grouping, transparency) was solved by adding granular logging at each transformation step. When dealing with 3D math, visibility into intermediate calculations is critical.
What's Next?
Phase 5 is complete, but the solar system view has room for enhancements:
- Orbit Predictions: Show orbital paths based on Lagrange Points
- Resource Overlays: Highlight mining-rich asteroid belts
- Distance Measurements: Show inter-object distances on hover
- Multi-System Comparison: Side-by-side views for scouting
But for now, the core experience is production-ready and delivering value to hundreds of daily users.
Click any star in the EVE Frontier Map and select "View Solar System" to explore planets, moons, and Lagrange Points in full 3D. Zoom in close to inspect individual objects, or zoom out to see the entire system layout.