Here is how EO-Map draws the whole of New Eden in a browser tab without setting your laptop on fire. The short answer is one three.js Points cloud with 8,089 vertices in it, and almost everything else in the renderer falls out of that. I am not a graphics programmer, I am a vibe coder, so what follows is a description of code with file references you can go and check. If something here contradicts the code, the code wins.
Nearly every constant in the renderer is derived from the scale, so that first. The map ships its universe as a SQLite file the browser downloads, and that file holds 8,089 solar systems: 5,485 in New Eden and 2,604 in Anoikis, which is wormhole space. They are joined by 13,978 directed stargate rows, so 6,989 actual links. The cluster is about 101 light years across on its longest axis. That is a big number of dots for a web page and a modest one for a GPU, and most of what follows sits in that gap.
| Thing | Value | Where it comes from |
|---|---|---|
| Systems drawn | 8,089 | map_data_eo_3464040.db, nothing carries the hidden flag |
| New Eden systems | 5,485 | id below 31000000 |
| Anoikis systems | 2,604 | id at or above 31000000 |
| Stargate rows | 13,978 directed, 6,989 links | the stargates table |
| Longest axis | 101.195871877 LY | src/config/eveOnlineMap.ts |
One vertex per star
Every visible system is a single vertex in a single BufferGeometry, and the entire field is one THREE.Points object. This is src/hooks/useStarfieldAndStargates.ts, around line 131.
const pointsGeometry = new THREE.BufferGeometry();
pointsGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
pointsGeometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
pointsGeometry.setAttribute('aSize', new THREE.Float32BufferAttribute(sizes, 1));
pointsGeometry.setAttribute('aEmissive', new THREE.Float32BufferAttribute(
new Float32Array(visibleSystemsRef.current.length).fill(1), 1));
starFieldRef.current = new THREE.Points(pointsGeometry, pointsMaterial);
Four attributes. Position, colour, a size multiplier, and an emissive multiplier that starts at 1.0 and gets filled in later from real stellar temperatures. The colour buffer has exactly one writer, which is the star colour pipeline, and every data overlay composes on top without ever touching it. That rule is written at the top of src/hooks/useStarColorPipeline.ts, and it is the only reason sovereignty, activity and live kills can all be on at once without fighting over the same array.
The material is doing less than it looks like
The material is a stock PointsMaterial with sizeAttenuation: true and vertexColors: true, customised through onBeforeCompile. What surprised me when I checked is that the material's own size no longer reaches the screen. three sets gl_PointSize from size and applies attenuation, then the injected code assigns gl_PointSize outright a few lines later, so the constant is inert. The real curve is in src/app/starPointSizing.ts and it is in CSS pixels: a base of 0.216, times the device pixel ratio, times 165 divided by the star's depth in view space.
That base looks absurd until you work out what it means. At the default framing, with the camera 165 light years back, an ordinary star wants to be about a fifth of a pixel. So there is a floor at 1.75 device pixels and the star is dimmed by its coverage ratio instead, which is 21.6 per cent at that distance. Older GL drew any sub pixel point as a full brightness dot, and switching that on and off across a slow pan is what reads as twinkling. Fading rather than snapping is what killed it.
Three tints, and something I got wrong
The per star appearance is less clever than people assume. There are three hoisted tints, cool, warm and neutral, and each star picks one from a hash of its system id. Across the real 8,089 that comes out at 2,556 cool, 2,719 warm and 2,814 neutral, about as even as a sine hash gets. The result is multiplied by a distance falloff so the far field dims instead of staying uniformly bright, and I think that curve does more work than any of the colour.
Then there is the size variance, and this is the bit I got wrong. The comment says roughly three per cent of stars get the larger 1.6 multiplier. The test is (seed % 37) < 1, the seed is a sine hash that is negative about half the time, and JavaScript's remainder keeps the sign, so every negative seed passes. I measured it against the real ids while writing this and it is 4,158 out of 8,089, or 51.4 per cent. A 1.6 multiplier on a sub pixel dot was never going to get noticed, but I would rather the comment was true.
Why not InstancedMesh
A decent chunk of the search traffic that finds articles like this arrives on queries about InstancedMesh, setColorAt and instanceColor, so here is the honest answer. Instancing solves draw calls. It is the right tool when you have thousands of copies of real geometry and each copy needs its own transform. A starfield is not that. Each star is a screen facing dot with no geometry to instance, and THREE.Points already draws the whole field in one call out of one buffer.
Feeding it is simpler too. You write flat Float32Array attributes instead of building a Matrix4 per star and calling setMatrixAt, and per star colour comes from vertexColors rather than an instanceColor buffer you have to remember to flag dirty. There is no InstancedMesh anywhere in this codebase and I have never wanted one.
Seven things over the same positions
Counting what actually gets added to the scene, it is three Points layers, three meshes and one line buffer.
| Object | three.js type | renderOrder | Note |
|---|---|---|---|
| Star cores | THREE.Points | default | the buffer above |
| Star glow | THREE.Points | 1 | pointsGeometry.clone(), additive halo |
| Star flare | THREE.Points | 2 | another clone plus a per star aRotation |
| Near shell | THREE.Mesh | -10 | icosphere, radius 2,833.5 LY |
| Mid shell | THREE.Mesh | -11 | icosphere, radius 4,958.6 LY |
| Far shell | THREE.Mesh | -12 | icosphere, radius 7,083.7 LY |
| Stargates | THREE.LineSegments | default | one buffer, two vertices per gate |
The glow and flare layers are the expensive ones in principle, because they are additive sprites that grow as the camera closes, and a hundred overlapping translucent quads make the GPU shade every covered pixel. Both ship at zero by default, so most people never pay for them. The three shells are the backdrop, IcosahedronGeometry at subdivision 4 rendered from the inside with depth writes off. Their radii are 28, 49 and 70 times the cluster's own extent, which is what gives the parallax when you orbit. Three spheres is a strange way to fake a skybox and it works better than I expected.
The gates are one line buffer
Gates are drawn as THREE.LineSegments over a BufferGeometry carrying position, color and a mid attribute holding the segment midpoint duplicated onto both vertices, which the shader uses for the distance fade. Two vertices per gate, every gate in one buffer, one draw call for the whole network.
There is a structural fact hiding in that geometry and it is the opposite of what I expected. 5,268 of the 5,485 New Eden systems appear in the stargate table. Only 217 do not, and every one of them sits in just three regions, which I am fairly sure are the closed Jove ones. New Eden is very nearly one connected graph, which is why the line layer reads as a web rather than a scattering. That is not a rendering choice, that is the universe.
Wormhole space sits off to one side
The 2,604 Anoikis systems are drawn at their real coordinates from the same export, and those coordinates put them about 1,289 light years from the origin, roughly thirteen times the width of New Eden itself. Same cloud, same buffer, same material. They just live over there.
That distance has a consequence I only noticed checking the maths for this post. The brightness falloff normalises against New Eden's extent and floors at 0.38, so every New Eden star lands between 0.390 and 0.997 and all 2,604 Anoikis stars are pinned to the floor. It happens to look right, because wormhole space should read as distant and separate. It is still an accident.
The 2D map is the same buffer, moved
The flat schematic layout is CCP's own published 2D arrangement, shipped in the same database as a second pair of coordinates. Switching between the 3D cluster and the flat map is not two scenes. It is one controller holding two immutable endpoint arrays per system and writing an interpolation of them into the geometry on every frame of an 800 millisecond ease. Four buffers move together, the star cores, the glow clone, the flare clone and the gate lines, whose midpoints get recomputed as they go so the depth fade does not track where the gates used to be.
Writing the CPU attribute rather than blending in the shader is deliberate, and it is the detail I like most in that file. Star picking re-projects that same buffer on every pointer event, so hit targets follow the visible stars for free. Anoikis has no published schematic coordinate, so those systems stay where they physically are while New Eden flattens.
The camera is boring on purpose
The camera is new THREE.PerspectiveCamera(75, aspect, 0.1, 10000000). Seventy five degrees is the three.js default and not a telephoto trick. The far plane is generous to the point of silly for a hundred light year cluster, but the outer backdrop shell sits at 7,083 light years and clipping it would be worse than the depth precision. Pixel ratio is capped at Math.min(2, devicePixelRatio) and forced to 1.0 in Performance Mode. Tone mapping defaults to NeutralToneMapping rather than ACES, and the reason is in the comment: star colour here is data, and ACES shifts hue.
The loop does not render every frame. It renders when something is dirty, when the orbit controls are still damping, or once every 200 milliseconds as an idle heartbeat, which is about five frames a second when you are sat looking at a static map.
Picking a star without a raycaster
This is where the naive version quietly breaks. three's Points raycast returns every point inside a world unit threshold, ordered by distance from the camera, so the first hit is the star nearest the camera and not the star under your cursor. In a field this deep those are often different stars.
New Eden is small enough that you can skip the problem. src/app/pointer/starPickPolicy.ts projects every eligible star into CSS pixels on each pointer event, keeps anything within the larger of an 18 pixel radius and the star's own rendered radius, sorts by pixel distance from the cursor, then within a 3 pixel slop prefers the one nearest the camera, with the system id as a stable final tie break. That slop is the whole trick. Without it, two stars that overlap on screen fight over the hover and flicker as you move the mouse by a pixel. No raycaster in that path, and no GPU picking anywhere.
Bloom only exists if you ask for it
Bloom is a chain of RenderPass, then UnrealBloomPass, then a merged output pass that fuses the tone map and sRGB encode with the vignette and dither into one draw. It is built lazily, so anyone who leaves bloom at zero pays nothing rather than paying for a pass that does nothing. It also gets built when Creator Mode asks for a whole frame grade or a vignette. The render target is HalfFloatType with four sample MSAA, because values above 1.0 have to survive to the bloom threshold or hot stars stop blooming.
One thing worth stealing if you use UnrealBloomPass yourself. three sets the blur sigma equal to the kernel radius and truncates sampling there, which cuts the Gaussian off where it still carries most of its weight. That is close to a box filter, and a box horizontally times a box vertically stamps a rounded square around any point source. A starfield is nothing but point sources, so every mip drew square halos. Setting sigma to a third of the radius at the same tap count fixes it for free, and the installed three is 0.179.1 with the stock kernels, so that patch is still earning its place.
What I have not measured
I should be straight about the gap in this. I have described what is in the scene and where each number comes from, and there is not a single millisecond figure anywhere in it. There is a frame sampler in the app and I have never run a proper census on this map, so any frame time I gave you would be borrowed from a different cluster at a different density, which is worth nothing.
So that is what is actually there. One Points cloud, two cloned sprite layers that are off by default, three backside icospheres, one line buffer for the gates, a default camera, and a bloom chain that only exists when you ask for it. Nothing exotic, and the interesting decisions were mostly about what not to add. The wider story of where this map came from is in how EO-Map came about. If something here contradicts the code, the code wins, and I would like to know.