Solar System View: A Three-Day Journey from Concept to Production

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?

Phase 5 Scope (November 8-9, 2025)
  • 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);
Result: Group icons now render at the correct location—overlaying the clustered objects perfectly.

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.

Performance: Recalculating clusters every frame (~60 FPS) with 100+ celestial objects costs <2ms per frame on average hardware.

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
  );
}
Easing Function

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:

Feature Completeness:
  • 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)
Performance:
  • 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)
Timeline:
  • 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:

But for now, the core experience is production-ready and delivering value to hundreds of daily users.

Try It Yourself

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.

← Back to Blog Try Solar System View →