Performance
What costs what
| Cost | Grows with | Lever |
|---|---|---|
| Drawing | screen pixels and the threshold, not scene size | errorThreshold, triangleBudget |
| Selection (compute) | visible instances | maxDrawDistance, minPixelRadius, occlusion |
| CPU | number of unique meshes (draw calls only) | nothing to do |
| Load time | source triangles | the build cache, or baked .vgeo files |
| GPU memory | source triangles and instances | maxTriangles and the other capacities |
Tips
- Give dense meshes. Virtual geometry shines with detailed sources. A 100k-triangle rock costs about the same as a 1k one once it is a few dozen pixels tall.
- Instance everything you can.
vg.addmerges repeated geometry automatically. WithcreateMesh, put all copies of an asset in one mesh. - Set a draw distance on small clutter. Grass, pebbles and flowers at 300 to 500 m cost almost nothing beyond that: whole cells of 128 instances are rejected at once, and instances shrink away instead of popping.
- Use
prune: truefor thin geometry (grass, leaves) when building. - Shadows already use a coarser selection (
shadowErrorScale). TurncastShadowoff for small clutter. - On weak GPUs, raise
errorThresholdto 1.5 to 2, or set atriangleBudget.
Benchmarks
Uncapped frame rates of the demo scenes: frames are submitted back to back with no vsync, and the GPU is waited on after every batch of 8, so these are the frame rates the hardware sustains, not the display's refresh rate. Each scene is measured from several fixed camera views at a 1 px error threshold with shadows on, at 1920×1080 and at 3840×2160.
| Scene | Full-detail triangles | Instances | Drawn per frame | FPS at 1080p | FPS at 4K |
|---|---|---|---|---|---|
| Ruins | 5.7B | 113k | 6.5M – 9.0M | 160 – 230 | 103 – 158 |
| World (6 km) | 37.5B | 3.0M | 8.3M – 9.1M | 147 – 150 | 101 – 120 |
| Valley (2 km) | 1.1B | 152k | 3.1M – 8.6M | 159 – 329 | 109 – 203 |
| glTF import | 5.1M | 12k | 522k – 557k | 941 – 964 | 468 – 516 |
| Stress, 1M instances | 39.5B | 1.0M | 642k – 8.4M | 177 – 1,404 | 154 – 696 |
Uncapped (no vsync): frames are submitted back to back and the GPU is waited on after every 8, so this is the sustained frame rate, not the display's. Each camera view is measured in 3 rounds of 96 frames (median round), after the automatic occlusion setting has settled; ranges span the views of each scene. Error threshold 1 px, shadows on (the stress test has none). Apple M4 Pro, 24 GB, darwin 25.5.0, Chrome 154.0.8037.98, 2026-10-08.
Ruins: every camera view
| View | Drawn | Shadow | Visible instances | 1080p: FPS (ms, CPU ms) | 4K: FPS (ms, CPU ms) |
|---|---|---|---|---|---|
| bronze close-up | 6.8M | 2.9M | 9k | 223 (4.47, 0.26) | 146 (6.84, 0.25) |
| altar reliefs | 7.2M | 2.6M | 8k | 223 (4.49, 0.25) | 158 (6.32, 0.32) |
| sanctuary | 9.0M | 2.1M | 17k | 160 (6.26, 0.24) | 103 (9.7, 0.24) |
| whole city | 8.3M | 531k | 11k | 230 (4.35, 0.21) | 136 (7.34, 0.22) |
| avenue | 8.1M | 2.4M | 10k | 169 (5.91, 0.29) | 111 (9.04, 0.3) |
| colonnade | 8.1M | 2.5M | 13k | 198 (5.05, 0.32) | 135 (7.4, 0.59) |
World (6 km): every camera view
| View | Drawn | Shadow | Visible instances | 1080p: FPS (ms, CPU ms) | 4K: FPS (ms, CPU ms) |
|---|---|---|---|---|---|
| start | 9.0M | 1.3M | 78k | 147 (6.81, 0.29) | 101 (9.89, 0.24) |
| aerial | 8.3M | 955k | 214k | 150 (6.66, 0.27) | 120 (8.34, 0.25) |
Valley (2 km): every camera view
| View | Drawn | Shadow | Visible instances | 1080p: FPS (ms, CPU ms) | 4K: FPS (ms, CPU ms) |
|---|---|---|---|---|---|
| overview | 3.1M | 533k | 64k | 329 (3.04, 0.18) | 203 (4.92, 0.16) |
| tree-level | 8.4M | 834k | 62k | 175 (5.71, 0.14) | 137 (7.29, 0.18) |
| meadow | 6.5M | 612k | 36k | 205 (4.89, 0.15) | 132 (7.6, 0.13) |
| lake-shore | 8.6M | 1.3M | 54k | 159 (6.28, 0.13) | 109 (9.19, 0.14) |
glTF import: every camera view
| View | Drawn | Shadow | Visible instances | 1080p: FPS (ms, CPU ms) | 4K: FPS (ms, CPU ms) |
|---|---|---|---|---|---|
| start | 522k | 171k | 5k | 964 (1.04, 0.88) | 516 (1.94, 1.1) |
| aerial | 557k | 116k | 7k | 941 (1.06, 0.79) | 468 (2.14, 1.01) |
Stress, 1M instances: every camera view
| View | Drawn | Shadow | Visible instances | 1080p: FPS (ms, CPU ms) | 4K: FPS (ms, CPU ms) |
|---|---|---|---|---|---|
| start | 8.4M | 0 | 649k | 177 (5.65, 0.14) | 154 (6.51, 0.1) |
| aerial | 642k | 0 | 187k | 1,404 (0.71, 0.09) | 696 (1.44, 0.1) |
Run them yourself
npm run bench # every scene, 1080p and 4K; writes docs/benchmarks.json (this table)
npm run bench -- --scenes ruins,map # some scenes
npm run bench -- --url https://elad12390.github.io/three-virtual-geometry/demo/ # the deployed demoIt drives your installed Google Chrome with Playwright. In a browser you can also open any demo scene with ?bench appended and read window.__bench, or tick uncapped fps in its settings panel to see the uncapped frame rate live.
Real scans
The other demo scenes are generated in code, so they load in seconds from a web page. Virtual geometry exists for a different kind of data: real-world scans, captured by 3D scanners or photogrammetry at millions of triangles per object, used as they are, without anyone making simplified versions by hand. The real scans scene shows them as exhibits in a museum:
| Exhibits | Data | Source and license |
|---|---|---|
| 723 statues in the galleries and the rotunda, 5 of them monumental on the hero bases | 14 scans of plaster casts of classical sculpture (Herakles, Apollo Lykeios, the Dying Gladiator, the Wrestlers, Minerva, Thalia, Sophocles, ...), 1 to 4 million triangles each: 2.1 billion triangles in all | SMK, National Gallery of Denmark, public domain |
| 40 specimens in the glazed Great Court | 13 photoscanned coastal rocks and cliffs, 1 to 2.9 million triangles each, with 2K textures | Poly Haven, CC0 |
The building itself (rotunda and coffered dome, colonnaded galleries, glazed court) is generated in code, like the other demo scenes. Each frame draws about 0.4 to 1.5 million of those 2.1 billion triangles, at one pixel of error.
Why it's a video on this site
Together the scans are about 2 GB. Downloading that just by opening a web page would be unreasonable, so the live demo page shows a recording instead. The recording is the real thing: the same scene rendered live on a MacBook Pro (M4 Pro) in Chrome, recorded in real time at 1920×1080 with the stats panel showing. The frame rate is uncapped: each frame starts as soon as the GPU has finished the last one, and the panel shows that rate (a little lower than without recording, since the same machine is also encoding the video). The video itself is 60 frames per second.
Run it yourself
git clone https://github.com/elad12390/three-virtual-geometry
cd three-virtual-geometry
npm install
npm run demo:scans # downloads the scans once (about 2 GB), bakes the statues, opens the scene
npm run demo:scans -- --small # a quicker subset: 3 rock scans and 2 statues
npm run demo:scans -- --dev # open it on the dev server (live reload) instead of the production buildnpm run demo:scans downloads into examples/scans/ (not part of the repository), resumes interrupted downloads, and skips everything already there, so running it again only opens the scene. It then builds the demo and serves the production build at http://localhost:8092, as on this site; the dev server is noticeably slower. If a statue fails to download (the museum's server sometimes drops large transfers), the scene uses the others, and running the command again picks up where it stopped. The statues are baked to .vgeo files with vg-bake once. The textured rock scans load as glTF through vg.add(), so the first visit builds them in the browser (a few minutes) and later visits load them from the cache. In the scene, &tour (or the fly-through button) plays the fly-through, a 214-second loop that circles the statues at eye level, spirals up around the rotunda's giant and crosses the court before returning to where it started.