⏱️ Reading time: 14 min

Loading 526,000 objects into a 3D scene should crash any browser: that count multiplied by one draw call per object saturates any GPU in the very first frame. Space Now, the solar system visualizer from the Belle Lune 2 project, advertises that catalog (526,000 asteroids and satellites, as of September 30, 2026) and displays it with a free-roaming camera and no frame drops, thanks to a GPU technique known as WebGL instancing.

📑 En este artículo
  1. TL;DR
  2. What is WebGL instancing?
  3. Why rendering this way matters in the browser
  4. How instanced rendering works
  5. Practical examples and how to get started
  6. The Space Now case: a full solar system catalog in the browser
  7. Real-world use cases for instancing beyond astronomy
  8. Common mistakes and best practices
  9. Comparison with alternative mass-rendering techniques
  10. Going deeper: what happens inside the GPU
  11. Frequently Asked Questions
    1. What is WebGL instancing and how is it different from drawing each object separately?
    2. How many instances can Three.js’s InstancedMesh support without losing smoothness?
    3. Do I need WebGL2 to use instanced geometry?
    4. Can I animate each asteroid independently with instanced rendering?
    5. Does instanced rendering work for satellites in real orbit, not just static asteroids?
    6. What rendering engine does Space Now use to display 526,000 objects?
  12. References

The same technique powers forests of thousands of trees in video games and particle systems, and now entire astronomical catalogs inside a single browser tab. Understanding how it works explains why a laptop with an integrated GPU can push more objects on screen than a server relying on brute force.

TL;DR

  • WebGL instancing groups thousands of identical objects into a single draw call instead of one per object.
  • Three.js exposes the technique through InstancedMesh: one geometry, one material, and one matrix per instance.
  • Space Now advertises 526,000 asteroids and satellites as of September 30, 2026 in a single WebGL canvas.
  • renderer.info.render.calls confirms in the console how many draw calls your scene emits.
  • A data texture replaces the matrix buffer once the number of instances exceeds the practical limit of a single attribute.

What is WebGL instancing?

WebGL instancing is a rendering technique that tells the graphics card to draw multiple copies of the same geometry, such as an asteroid or a satellite, in a single draw call, distinguishing each copy only by a position, rotation, and scale matrix.

The core idea is simple. The CPU uploads the object’s shape once, for example the vertices of a sphere or an irregular icosahedron, and then separately uploads a list of transformations with the position, size, and orientation of each copy. The GPU combines both in parallel and draws everything at once.

WebGL inherits this capability from OpenGL ES. In WebGL 1 it arrives through the ANGLE_instanced_arrays extension; in WebGL 2 it’s already part of the API core, with no need to request extra permission from the browser to use it.

Why rendering this way matters in the browser

Every draw call has a fixed cost that doesn’t depend on the size of the object. The JavaScript engine builds the command, the GPU driver validates it, and the browser crosses the boundary between the JavaScript thread and the graphics compositing process, and that cost gets paid even if the object is a single-triangle cube.

With one asteroid per call, a catalog of a few tens of thousands of objects already starts eating into the budget of a full frame, which in a browser running at 60 frames per second lasts just 16.6 milliseconds. The bottleneck isn’t the GPU drawing triangles: it’s the CPU-GPU communication repeated thousands of times per second.

Instancing turns the problem into a single one. It reduces everything to one call and one boundary crossing, letting the work of positioning hundreds of thousands of copies get resolved inside the GPU, which is designed to parallelize exactly that kind of work.

💭 Key point: uploading 526,000 full matrices every frame would be almost as costly as the original problem; the real optimization is updating only the matrices that changed, not the entire buffer at once.
The asteroid belt between Mars and Jupiter concentrates most of the solar system’s catalogued objects. Foto de National Cancer Institute en Unsplash

How instanced rendering works

In a normal render, each object travels through the GPU pipeline with its own call, activating its geometry, uploading its transformation matrix, and firing drawArrays or drawElements. Repeating this 526,000 times per frame is, in practice, impossible at 60 frames per second.

Instanced rendering changes the flow: the geometry and material get activated only once, and alongside the normal attributes of each vertex (position, normal, UV) an extra attribute gets added that varies per instance instead of per vertex. That attribute is usually a 4×4 transformation matrix, stored in an InstancedBufferAttribute.

Inside the vertex shader, the GPU multiplies each vertex’s position by the matrix of the instance it belongs to, identified through the internal gl_InstanceID variable. As a result, the same asteroid mesh repeats 526,000 times, each with its own place, size, and orientation, all within a single drawArraysInstanced or drawElementsInstanced call.

flowchart LR
subgraph WithoutInstancing["Without instancing"]
A1["Object 1"] --> D1["Draw call 1"]
A2["Object 2"] --> D2["Draw call 2"]
A3["Object N"] --> D3["Draw call N"]
end
subgraph WithInstancing["With instancing"]
B1["Geometry + N matrices"] --> D4["1 draw call"]
end
sequenceDiagram
participant JS as JavaScript
participant Buf as Instance buffer
participant GPU as GPU
JS->>Buf: writes a 4x4 matrix per asteroid
Buf->>GPU: uploads the full buffer once
JS->>GPU: drawElementsInstanced(count)
GPU->>GPU: multiplies each vertex by its matrix via gl_InstanceID
GPU-->>JS: a single draw call renders all instances

Practical examples and how to get started

The simplest possible example: five identical cubes at five different positions, all in a single draw call.

import * as THREE from 'three';

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x8899ff });
const count = 5;
const mesh = new THREE.InstancedMesh(geometry, material, count);

const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
  dummy.position.set(i * 2, 0, 0);
  dummy.updateMatrix();
  mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);

renderer.render(scene, camera);
console.log(renderer.info.render.calls);

The console output confirms that, despite five objects separated in space, the cost for the GPU was a single call:

1

Going from five cubes to a real catalog is the same code; only the count and how each matrix gets calculated change. This example distributes 50,000 asteroids across a spherical shell, mimicking the main belt:

const asteroidGeo = new THREE.IcosahedronGeometry(0.05, 0);
const asteroidMat = new THREE.MeshStandardMaterial({ color: 0xaaaaaa });
const count = 50000;
const asteroids = new THREE.InstancedMesh(asteroidGeo, asteroidMat, count);

const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
  const radius = 2.2 + Math.random() * 0.6;
  const theta = Math.random() * Math.PI * 2;
  const phi = Math.acos((Math.random() * 2) - 1);
  dummy.position.set(
    radius * Math.sin(phi) * Math.cos(theta),
    radius * Math.cos(phi) * 0.05,
    radius * Math.sin(phi) * Math.sin(theta)
  );
  dummy.scale.setScalar(0.5 + Math.random());
  dummy.updateMatrix();
  asteroids.setMatrixAt(i, dummy.matrix);
}
asteroids.instanceMatrix.needsUpdate = true;
scene.add(asteroids);

To tell asteroids apart from satellites without duplicating geometry, a per-instance color is enough, instead of a full material per instance:

const colors = new Float32Array(count * 3);
const color = new THREE.Color();
for (let i = 0; i < count; i++) {
  color.setHSL(Math.random() * 0.1 + 0.05, 0.3, 0.5);
  color.toArray(colors, i * 3);
}
asteroids.instanceColor = new THREE.InstancedBufferAttribute(colors, 3);
asteroidMat.vertexColors = true;

To get this example running on a clean machine, you first need Node.js and a package manager installed. With that in place, the commands are the same on Windows, macOS, and Linux because they run on top of Node.js, not the operating system:

npm create vite@latest orrery-demo -- --template vanilla
cd orrery-demo
npm install three
npm run dev

The development server stays listening and shows the local URL in the terminal, usually http://localhost:5173; that’s where you paste the code above inside main.js.

The Space Now case: a full solar system catalog in the browser

Space Now, published at space.bl2.net as part of the Belle Lune 2 project, is a 3D solar system visualizer that runs entirely inside a browser tab: the Sun, planets, moons, and a catalog that the site itself advertises, as of September 30, 2026, at 526,000 asteroids and satellites. The camera is controlled with mouse and keyboard: dragging rotates, the wheel zooms in, a click shows an object’s info card and orbit, and a double click flies straight to it. WASD moves, R and F go up or down, Q, E and the arrow keys rotate, and Shift speeds up movement.

That level of interaction, a free-roaming real-time camera over hundreds of thousands of bodies with self-updating data, is exactly the scenario where instanced rendering stops being an optional optimization and becomes the only viable way to build the product. Drawing each asteroid as an independent mesh, with its own draw call, wouldn’t come close to a stable frame rate.

The site doesn’t publish its source code, so there’s no way to confirm from the outside exactly which engine it uses. What can be stated with technical certainty is the general pattern, not the implementation detail. Any project that moves that many objects in a WebGL canvas, with a smooth camera and no frame drops, needs some scheme of instanced geometry, level-of-detail grouping, and culling of what falls outside the camera.

Real-world use cases for instancing beyond astronomy

Game engines use GPU instancing to populate forests, crowds, and battlefields: thousands of trees or soldiers share the same base mesh, only changing their position, orientation, and sometimes a color tint.

Satellite constellation trackers, increasingly common as low-orbit megaconstellations keep growing, face the same problem as Space Now at a smaller scale. Thousands of points moving in real time need to update frame by frame without recreating geometry.

In CAD and architectural visualization, instancing solves scenes with repeated components, such as screws, chairs, or facade panels, without the 3D file growing linearly with each copy.

In data science, some 3D visualization tools use the same technique to draw scatter plots with tens of thousands of points represented as geometric glyphs instead of flat pixels, gaining real depth and lighting.

Low-orbit satellite megaconstellations have multiplied the number of objects an orbital tracker must draw in real time. Foto de CDC en Unsplash

Common mistakes and best practices

Forgetting to set instanceMatrix.needsUpdate = true after modifying the matrices is the most frequent mistake. Without that flag, the GPU keeps using the old buffer and the objects don’t move on screen even if the positioning code is correct.

A single draw call doesn’t mean free. The vertex shader and fragment shader still run for every vertex and every pixel of every instance, so a geometry with too many triangles per asteroid, multiplied by 526,000, still weighs heavily even though the draw call count is one. The usual solution is a low level of detail for distant or small objects.

InstancedMesh doesn’t apply per-instance frustum culling automatically. Three.js computes a bounding box that covers all instances together, so if the camera is looking at only a fraction of the catalog, all of them still get processed unless the developer implements their own culling, for example by dividing the catalog into a grid or octree and updating only the buffer for the visible region.

Mixing transparent materials with instancing without sorting by depth creates visual artifacts. Translucent objects get drawn in buffer order, not in order of distance from the camera.

Each instance matrix takes up four four-component vectors, 16 floats in total. Past a certain volume, that attribute starts competing for the graphics card’s vertex memory limit, and at that point it’s worth moving the transformations to a data texture that the shader reads by index instead of through a fixed attribute.

Comparison with alternative mass-rendering techniques

Choosing the right technique depends on the volume of objects, whether they need real geometry with lighting, and whether their positions change constantly or only once when the scene loads.

TechniqueWhen to use itAdvantageLimitation
InstancedMesh (buffer attribute)Up to hundreds of thousands of instances with simple geometryA single draw call, straightforward API in Three.jsEach matrix consumes vertex memory; no per-instance culling by default
Points / SpritesMillions of points without real 3D volumeMinimal memory per particleNo real geometry: doesn’t work for objects with their own shape or lighting
Static merged geometryObjects that never change position relative to each otherA single draw call with no per-instance attribute overheadMoving a single copy requires rebuilding the entire merged geometry
Data texture (DataTexture) + instancingMillions of instances with transformations calculated on the GPUAvoids the vertex attribute limit, moves the calculation to the graphics cardRequires custom shaders; higher code complexity

Going deeper: what happens inside the GPU

When the driver receives drawElementsInstanced, it spreads the work across the GPU’s thousands of parallel cores instead of launching 526,000 sequential execution threads, processing a vertex from a different instance simultaneously in each core. It’s the same massive parallelism model any graphics card uses to shade millions of pixels per frame, now applied to repeating geometry instead of computing color.

The vertex shader receives, in addition to each vertex’s normal attributes, the corresponding instance matrix through gl_InstanceID. Three.js generates this code automatically when it detects an InstancedMesh, adding a line equivalent to multiplying the vertex position by instanceMatrix before applying the camera matrix.

For catalogs exceeding a million objects, some projects move the position calculation directly to the shader. Instead of the CPU computing each asteroid’s orbit frame by frame and uploading a new matrix, the vertex shader receives the orbital elements (semi-major axis, eccentricity, inclination) as per-instance data and calculates the position in real time inside the GPU.

WebGPU, the successor to WebGL that already runs in Chromium-based browsers and Firefox, adds general-purpose compute shaders: code that runs on the GPU outside the drawing pipeline, useful for calculating which instances are visible before the render pipeline even touches them. It’s the natural next step after instancing for anyone looking to scale from hundreds of thousands to millions of objects.

flowchart TD
F["New frame"] --> C["Calculate which instances fall inside the camera frustum"]
C --> U["Update only those matrices in the buffer"]
U --> D["1 instanced draw call"]
D --> R["Frame rendered"]
R --> F

Your next step: clone this article’s 50,000-asteroid example, bump the count to 500,000 by changing a single line (const count = 500000), and watch in the console how long renderer.info.render.calls keeps returning 1.

📬 Get new articles by email

We only email about big articles (1-2 a month).

Frequently Asked Questions

What is WebGL instancing and how is it different from drawing each object separately?

It’s a technique that draws multiple copies of the same geometry in a single call to the GPU, instead of issuing a separate draw call for each object. The difference between one and 526,000 draw calls is, in practice, the difference between a smooth frame and a frozen browser.

How many instances can Three.js’s InstancedMesh support without losing smoothness?

It depends on the number of triangles per instance, whether culling is implemented, and the user’s GPU. There’s no universal number; the right way to find out is to measure with renderer.info.render.calls and renderer.info.render.triangles on the target hardware, not assume a figure from another project.

Do I need WebGL2 to use instanced geometry?

No. WebGL1 supports it through the ANGLE_instanced_arrays extension; WebGL2 includes it natively in the API core, with no need to request the extension separately.

Can I animate each asteroid independently with instanced rendering?

Yes, by updating each instance’s matrix frame by frame and setting instanceMatrix.needsUpdate = true. Past a certain volume, it’s worth moving that calculation into the shader itself instead of recalculating it in JavaScript every frame.

Does instanced rendering work for satellites in real orbit, not just static asteroids?

Yes, it’s the same pattern: each satellite is an instance with its own transformation matrix, recalculated based on its orbital elements at every time update.

What rendering engine does Space Now use to display 526,000 objects?

The site doesn’t publish its source code, so the exact engine can’t be confirmed from the outside. What’s verifiable is the behavior: a smooth camera over a catalog of that scale in a single WebGL canvas, something that in practice requires instancing or an equivalent GPU-based technique.

References

📱 Do you like this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.

Featured image: Foto de Trnava University en Unsplash

Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.

Leave a comment
Categories: Tech NewsTutorials

Andrés Morales

Developer and AI researcher. Writes about language models, frameworks, developer tooling, and open source releases. Covers ML papers, the tech startup ecosystem, and programming trends.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

You can include code inside <code>…</code> or, for several lines, <pre><code>…</code></pre>.

This site uses Akismet to reduce spam. Learn how your comment data is processed.