Five scans from the Radix catalog (0.5M to 10M splats), each loaded from the same file by every renderer and drawn along the same camera path at 1920×1080: with culling matched between the renderers (the headline), with each renderer's defaults, and with no culling. Apple M3 Max, 128 GB, macOS 15.4.1, Google Chrome 154.0.8037.98.
The renderers' defaults draw different splats: PlayCanvas drops tiny and faint splats that Radix and Spark draw, which makes it look faster than it is at like-for-like work. So the comparison here sets Radix and Spark to cull and dilate exactly as PlayCanvas does by default (section 02). With culling matched, Radix and PlayCanvas run at similar speeds on WebGPU.
The speed of each renderer relative to Radix on WebGPU with culling matched (above 1× is faster), as the geometric mean over the 5 scenes of the ratio of mean frame times.
Spark has no WebGPU path, so the like-for-like comparison with it is on WebGL2, where all three sort on the CPU (on a worker) and draw with WebGL. With culling matched, relative to Radix on WebGL2: Radix · WebGL2 1.00×, Spark · WebGL2 1.08×, PlayCanvas · WebGL2 1.36×.
Every renderer skips splats it considers invisible, but by different rules. Radix and Spark follow the reference 3DGS rasterizer: they draw every splat above a tiny alpha (0.5/255) and blur each by 0.3 px² with opacity compensation. PlayCanvas also drops splats drawn under a 1-pixel radius (σ under about 0.22 px) and, on its WebGPU path, splats whose opacity × area is under 0.75 px², with an alpha floor of 1/255 and a fixed 0.075 px² blur without compensation. On scenes with many distant or faint splats, that is a lot of work skipped, and visible detail with it: on WebGPU it removes most of the bicycle's wheel spokes.
PlayCanvas's settings read larger than that because it measures its 2D covariance with twice the focal length in pixels (viewportWidth × P[0], where the pixel focal length is half of it) and halves its quad again when drawing, so the drawn size is right but its constants are in half pixels: minPixelSize: 2 is a 1 px radius, minContribution: 3 is 0.75 px², and its 0.3 dilation is 0.075 px². That dilation is a quarter of the 0.3 px² that standard 3DGS training uses, so PlayCanvas draws splats smaller than they were trained, and it cannot be changed.
| cull | PlayCanvas default | in pixels | Radix (matched) | Spark (matched) |
|---|---|---|---|---|
| contribution (opacity × 2π √det) | minContribution: 3 (WebGPU only) | 0.75 px² | objectParams.minContribution: 0.75 (WebGPU; 0 on WebGL2) | no such cull (WebGL2, where PlayCanvas has none either) |
| pixel size | minPixelSize: 2 | √8 σ under 1 px | objectParams.minPixelRadius: 1 | minPixelRadius: 1 |
| opacity | alphaClipForward: 1/255 | 1/255 | params.minAlpha: 1/255 | minAlpha: 1/255 |
| dilation | 0.3, no opacity compensation (fixed) | 0.075 px² | blurAmount: 0, preBlurAmount: 0.075 | blurAmount: 0, preBlurAmount: 0.075 |
Every scene ran with three settings: matched (PlayCanvas's default culling and dilation in all three; PlayCanvas at its defaults), defaults (each renderer as it comes), and no culling (pixel size and contribution minimums at 0 and alpha 1/255 for all, the lowest PlayCanvas allows on WebGPU; each renderer keeps its own dilation), so every renderer draws every splat. Sort keys stay at each renderer's default throughout (section 11).
| config | Defaults | No culling | Matched culling |
|---|---|---|---|
| Radix · WebGPU | defaults | objectParams.minPixelRadius: 0 objectParams.minContribution: 0 params.minAlpha: 1/255 | objectParams.minContribution: 0.75 objectParams.minPixelRadius: 1 objectParams.blurAmount: 0 objectParams.preBlurAmount: 0.075 params.minAlpha: 1/255 |
| Radix · WebGL2 | defaults | objectParams.minPixelRadius: 0 objectParams.minContribution: 0 params.minAlpha: 1/255 | objectParams.minContribution: 0 objectParams.minPixelRadius: 1 objectParams.blurAmount: 0 objectParams.preBlurAmount: 0.075 params.minAlpha: 1/255 |
| Spark · WebGL2 | defaults | renderer.minPixelRadius: 0 renderer.minAlpha: 1/255 | renderer.minPixelRadius: 1 renderer.minAlpha: 1/255 renderer.blurAmount: 0 renderer.preBlurAmount: 0.075 |
| PlayCanvas · WebGPU | defaults | minPixelSize: 0 minContribution: 0 alphaClipForward: 1/255 | its defaults (the runs of group default) |
| PlayCanvas · WebGL2 | defaults | minPixelSize: 0 minContribution: 0 alphaClipForward: 1/255 | its defaults (the runs of group default) |
Bicycle was trained without anti-aliasing (a 0.3 px² dilation without opacity compensation), so the defaults and no-culling runs draw it that way: Radix · WebGPU objectParams.blurAmount: 0 objectParams.preBlurAmount: 0.3; Radix · WebGL2 objectParams.blurAmount: 0 objectParams.preBlurAmount: 0.3; Spark · WebGL2 renderer.blurAmount: 0 renderer.preBlurAmount: 0.3; PlayCanvas · WebGPU antiAlias: false; PlayCanvas · WebGL2 antiAlias: false. Matched keeps PlayCanvas's 0.075 px², a constant in its shaders.
Speed relative to Radix on WebGPU with the same culling setting (above 1× is faster), the geometric mean over the scenes. Solid: matched culling.
Mean frame time over the whole path (still, level turn, tilted turn), the median of the rounds; whiskers: the fastest and slowest round. Lower is better.
The path has three parts of equal length: the start view held still, a full turn at the start elevation, and a full turn tilting up and down. Objects (butterfly, Sutro, bicycle) are orbited, ±30° in elevation in the third part; in interiors (the ceramic cave, Grace Cathedral) the camera looks around, ±45° in pitch, while walking a level circle facing out (radius a tenth of the distance to the start view's target), so distance sorts must re-sort. The still part shows a renderer's cost with nothing to re-sort; the turns add sorting, and on the CPU-sort paths, orders that arrive a few frames late. Culling matched:
| Radix · WebGPU | Radix · WebGL2 | Spark · WebGL2 | PlayCanvas · WebGPU | PlayCanvas · WebGL2 | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ms per frame | still | level | tilted | still | level | tilted | still | level | tilted | still | level | tilted | still | level | tilted |
| Butterfly | 3.4 | 3.5 | 3.4 | 3.6 | 4.9 | 4.9 | 3.5 | 3.8 | 3.7 | 3.3 | 3.6 | 3.6 | 2.6 | 3.9 | 4.0 |
| Ceramic (cave) | 8.4 | 4.5 | 4.5 | 9.3 | 8.5 | 8.4 | 9.6 | 6.9 | 6.8 | 7.8 | 4.5 | 4.4 | 8.1 | 5.2 | 5.1 |
| Sutro | 3.2 | 3.6 | 3.5 | 4.2 | 7.2 | 6.6 | 4.8 | 6.0 | 5.4 | 3.2 | 3.6 | 3.5 | 3.6 | 4.5 | 4.2 |
| Bicycle | 4.7 | 4.7 | 5.1 | 9.2 | 13.8 | 14.4 | 11.5 | 12.2 | 12.7 | 4.4 | 4.4 | 4.9 | 8.7 | 9.4 | 10.0 |
| Grace interior | 9.7 | 7.6 | 6.9 | 12.4 | 16.8 | 16.2 | 16.4 | 16.2 | 15.7 | 9.1 | 7.1 | 6.5 | 12.3 | 11.0 | 10.5 |
The median over rounds of each part's mean frame time. Red: slower than the fastest config of that scene and part (full red at 2.5×).
Per scene, culling matched: the median frame (dot), the 95th percentile (tick) and the 99th (end of the line), the median over rounds. A stall is a frame over twice the median: on the CPU-sort paths these are usually the frames that upload a new order.
The CPU time of the frame call alone (the renderer's update, sort dispatch and draw submission), the median over rounds, culling matched. It is small for every renderer, so the timing method's lack of CPU and GPU overlap costs little.
| scene | Radix · WebGPU | Radix · WebGL2 | Spark · WebGL2 | PlayCanvas · WebGPU | PlayCanvas · WebGL2 |
|---|---|---|---|---|---|
| Butterfly | 0.10 ms | 0.24 ms | 0.04 ms | 0.14 ms | 0.12 ms |
| Ceramic (cave) | 0.14 ms | 0.38 ms | 0.08 ms | 0.18 ms | 0.13 ms |
| Sutro | 0.10 ms | 0.31 ms | 0.06 ms | 0.14 ms | 0.12 ms |
| Bicycle | 0.13 ms | 0.44 ms | 0.13 ms | 0.18 ms | 0.21 ms |
| Grace interior | 0.19 ms | 0.40 ms | 0.14 ms | 0.24 ms | 0.23 ms |
The timed numbers above measure each frame alone. An app runs in a requestAnimationFrame loop paced by the display: one frame's CPU work overlaps the previous frame's GPU work, and a frame that misses a vsync is a stutter. Every config also ran that way, in headed Chrome with vsync on (120 Hz, 3 rounds). Frame i draws pose i of the same path and nothing waits for the GPU, so a slower renderer takes longer over the path.
Per scene, the median over rounds: frames per second, the share of frames late (over 1.5 display frames), the p99 frame interval in ms, and the timed mean frame. Green: full rate; red: below it (full red at 33% below).
| scene | Radix · WebGPU | Radix · WebGL2 | Spark · WebGL2 | PlayCanvas · WebGPU | PlayCanvas · WebGL2 |
|---|---|---|---|---|---|
| Butterfly 546k | 120 fps 0.0% late · p99 10.3 timed 3.4 ms | 120 fps 0.0% late · p99 10.3 timed 4.5 ms | 120 fps 0.0% late · p99 10.3 timed 3.7 ms | 120 fps 0.0% late · p99 10.3 timed 3.5 ms | 120 fps 0.0% late · p99 10.3 timed 3.5 ms |
| Ceramic (cave) 2.0M | 120 fps 0.0% late · p99 10.3 timed 5.8 ms | 119 fps 0.8% late · p99 10.4 timed 8.7 ms | 120 fps 0.0% late · p99 10.3 timed 7.8 ms | 120 fps 0.0% late · p99 10.3 timed 5.5 ms | 120 fps 0.0% late · p99 10.3 timed 6.2 ms |
| Sutro 2.0M | 120 fps 0.0% late · p99 10.3 timed 3.4 ms | 120 fps 0.0% late · p99 10.3 timed 6.0 ms | 120 fps 0.0% late · p99 10.3 timed 5.4 ms | 120 fps 0.0% late · p99 10.4 timed 3.4 ms | 120 fps 0.0% late · p99 10.3 timed 4.1 ms |
| Bicycle 6.1M | 120 fps 0.0% late · p99 10.4 timed 4.8 ms | 102 fps 9.1% late · p99 35.9 timed 12.5 ms | 96 fps 11.9% late · p99 34.7 timed 12.1 ms | 120 fps 0.0% late · p99 10.3 timed 4.5 ms | 120 fps 0.1% late · p99 10.3 timed 9.4 ms |
| Grace interior 10.0M | 118 fps 1.6% late · p99 16.2 timed 8.1 ms | 81 fps 22.9% late · p99 50.0 timed 15.1 ms | 73 fps 27.1% late · p99 58.9 timed 16.1 ms | 120 fps 0.0% late · p99 10.3 timed 7.6 ms | 116 fps 3.3% late · p99 16.8 timed 11.3 ms |
| scene | Radix · WebGPU | Radix · WebGL2 | Spark · WebGL2 | PlayCanvas · WebGPU | PlayCanvas · WebGL2 |
|---|---|---|---|---|---|
| Butterfly 546k | 120 fps 0.0% late · p99 10.3 timed 3.7 ms | 120 fps 0.0% late · p99 10.4 timed 4.6 ms | 120 fps 0.0% late · p99 10.3 timed 3.8 ms | 120 fps 0.0% late · p99 10.3 timed 3.5 ms | 120 fps 0.0% late · p99 10.3 timed 3.5 ms |
| Ceramic (cave) 2.0M | 120 fps 0.0% late · p99 10.4 timed 6.3 ms | 116 fps 2.6% late · p99 17.0 timed 9.2 ms | 120 fps 0.0% late · p99 10.3 timed 7.9 ms | 120 fps 0.0% late · p99 10.3 timed 5.5 ms | 120 fps 0.0% late · p99 10.3 timed 6.2 ms |
| Sutro 2.0M | 120 fps 0.0% late · p99 10.3 timed 4.5 ms | 120 fps 0.0% late · p99 10.3 timed 7.2 ms | 120 fps 0.0% late · p99 10.3 timed 6.4 ms | 120 fps 0.0% late · p99 10.4 timed 3.4 ms | 120 fps 0.0% late · p99 10.3 timed 4.1 ms |
| Bicycle 6.1M | 117 fps 2.2% late · p99 16.7 timed 8.9 ms | 96 fps 13.2% late · p99 39.3 timed 13.5 ms | 91 fps 14.2% late · p99 42.2 timed 12.9 ms | 120 fps 0.0% late · p99 10.3 timed 4.5 ms | 120 fps 0.1% late · p99 10.3 timed 9.4 ms |
| Grace interior 10.0M | 108 fps 11.0% late · p99 17.6 timed 9.2 ms | 78 fps 26.1% late · p99 50.7 timed 15.6 ms | 72 fps 29.6% late · p99 59.3 timed 16.5 ms | 120 fps 0.0% late · p99 10.3 timed 7.6 ms | 116 fps 3.3% late · p99 16.8 timed 11.3 ms |
| scene | Radix · WebGPU | Radix · WebGL2 | Spark · WebGL2 | PlayCanvas · WebGPU | PlayCanvas · WebGL2 |
|---|---|---|---|---|---|
| Butterfly 546k | 120 fps 0.0% late · p99 10.3 timed 3.6 ms | 120 fps 0.0% late · p99 10.3 timed 4.5 ms | 120 fps 0.0% late · p99 10.3 timed 3.8 ms | 120 fps 0.0% late · p99 10.3 timed 3.6 ms | 120 fps 0.0% late · p99 10.3 timed 3.5 ms |
| Ceramic (cave) 2.0M | 120 fps 0.0% late · p99 10.3 timed 6.0 ms | 118 fps 1.3% late · p99 16.0 timed 8.8 ms | 120 fps 0.0% late · p99 10.3 timed 7.8 ms | 120 fps 0.0% late · p99 10.3 timed 5.6 ms | 120 fps 0.0% late · p99 10.3 timed 6.1 ms |
| Sutro 2.0M | 120 fps 0.0% late · p99 10.3 timed 4.3 ms | 120 fps 0.0% late · p99 10.3 timed 7.0 ms | 120 fps 0.0% late · p99 10.3 timed 6.3 ms | 120 fps 0.0% late · p99 10.3 timed 4.1 ms | 120 fps 0.0% late · p99 10.3 timed 4.6 ms |
| Bicycle 6.1M | 118 fps 1.5% late · p99 16.5 timed 8.8 ms | 98 fps 11.6% late · p99 39.0 timed 13.2 ms | 91 fps 14.3% late · p99 41.7 timed 12.8 ms | 120 fps 0.0% late · p99 10.4 timed 7.5 ms | 120 fps 0.1% late · p99 10.3 timed 9.6 ms |
| Grace interior 10.0M | 110 fps 9.2% late · p99 17.0 timed 9.0 ms | 79 fps 24.9% late · p99 50.9 timed 15.5 ms | 72 fps 28.2% late · p99 59.3 timed 16.4 ms | 109 fps 9.5% late · p99 17.1 timed 9.3 ms | 110 fps 9.3% late · p99 18.3 timed 12.4 ms |
These are requestAnimationFrame intervals, not presented frames; Chrome's backpressure keeps the two close. The display caps the rate, so on light scenes every renderer reads 120 fps.
Each renderer with its default settings, so with different culling (section 02): PlayCanvas skips work the others do. Relative to Radix on WebGPU: Radix · WebGPU 1.00×, Radix · WebGL2 0.67×, Spark · WebGL2 0.73×, PlayCanvas · WebGPU 1.30×, PlayCanvas · WebGL2 0.98×; on WebGL2, relative to Radix on WebGL2: Radix · WebGL2 1.00×, Spark · WebGL2 1.09×, PlayCanvas · WebGL2 1.47×.
Mean frame time over the whole path (still, level turn, tilted turn), the median of the rounds; whiskers: the fastest and slowest round. Lower is better.
Radix with its defaults and with PlayCanvas's culling, against PlayCanvas with its defaults and without culling: how much of the difference at defaults is the culling.
Pixel size and contribution minimums at 0 and alpha 1/255 for all: every splat is drawn, each with its renderer's own dilation. Relative to Radix on WebGPU: Radix · WebGPU 1.00×, Radix · WebGL2 0.67×, Spark · WebGL2 0.72×, PlayCanvas · WebGPU 1.05×, PlayCanvas · WebGL2 0.92×; on WebGL2, relative to Radix on WebGL2: Radix · WebGL2 1.00×, Spark · WebGL2 1.08×, PlayCanvas · WebGL2 1.38×.
Mean frame time over the whole path (still, level turn, tilted turn), the median of the rounds; whiskers: the fastest and slowest round. Lower is better.
From the start of loading (the PLY file over local HTTP) to the end of the first frame, with default settings, the median over rounds. A renderer that sorts asynchronously may still show an empty or unsorted first frame.
Before every run the driver waited for macOS to report a nominal thermal state (ProcessInfo.thermalState): up to 90 seconds while "fair", which by itself is not throttling, and up to 5 minutes at "serious" or worse, then an idle gap. A canary (Radix on WebGPU, the butterfly, 120-frame parts) ran at the start of each round, every ten runs and at the end; when it came out more than 8% slower than its first measurement, the driver cooled down for a minute and measured it again, and measured the runs since the last good canary again. The configs ran in a different random order each round, so a slow period would spread over all of them rather than one.
Canary median frame time over the benchmark (pink: thermal state nominal; red: not). The shaded band is the 8% tolerance.
Mean frame time (ms) of each round of every config, the coefficient of variation between them (yellow over 5%), and the thermal state before each run.
| culling | scene | config | round 1 | round 2 | round 3 | round 4 | round 5 | CV | thermal before |
|---|---|---|---|---|---|---|---|---|---|
| Defaults | Butterfly | Radix · WebGPU | 3.69 | 3.73 | 3.67 | 3.70 | 3.65 | 0.9% | 0 0 0 0 0 |
| Defaults | Butterfly | Radix · WebGL2 | 4.59 | 4.59 | 4.57 | 4.57 | 4.55 | 0.3% | 0 0 0 0 0 |
| Defaults | Butterfly | Spark · WebGL2 | 3.81 | 3.84 | 3.82 | 3.83 | 3.81 | 0.4% | 0 0 0 0 0 |
| Defaults | Butterfly | PlayCanvas · WebGPU | 3.52 | 3.58 | 3.54 | 3.50 | 3.49 | 1.0% | 0 0 0 0 0 |
| Defaults | Butterfly | PlayCanvas · WebGL2 | 3.50 | 3.54 | 3.51 | 3.52 | 3.51 | 0.4% | 0 0 0 0 0 |
| Defaults | Ceramic (cave) | Radix · WebGPU | 6.32 | 6.28 | 6.25 | 6.27 | 6.29 | 0.4% | 0 0 0 0 0 |
| Defaults | Ceramic (cave) | Radix · WebGL2 | 9.24 | 9.18 | 9.07 | 9.15 | 9.20 | 0.7% | 0 0 0 0 0 |
| Defaults | Ceramic (cave) | Spark · WebGL2 | 7.86 | 7.91 | 7.84 | 7.89 | 7.89 | 0.3% | 0 0 0 0 0 |
| Defaults | Ceramic (cave) | PlayCanvas · WebGPU | 5.55 | 5.58 | 5.56 | 5.52 | 5.54 | 0.4% | 0 0 0 0 0 |
| Defaults | Ceramic (cave) | PlayCanvas · WebGL2 | 6.19 | 6.17 | 6.10 | 6.03 | 6.19 | 1.1% | 0 0 0 0 0 |
| Defaults | Sutro | Radix · WebGPU | 4.47 | 4.43 | 4.46 | 4.42 | 4.46 | 0.5% | 0 0 0 0 0 |
| Defaults | Sutro | Radix · WebGL2 | 7.18 | 7.25 | 7.19 | 7.22 | 7.23 | 0.4% | 0 0 0 0 0 |
| Defaults | Sutro | Spark · WebGL2 | 6.41 | 6.37 | 6.41 | 6.42 | 6.41 | 0.3% | 0 0 0 0 0 |
| Defaults | Sutro | PlayCanvas · WebGPU | 3.45 | 3.42 | 3.43 | 3.44 | 3.43 | 0.4% | 0 0 0 0 0 |
| Defaults | Sutro | PlayCanvas · WebGL2 | 4.08 | 4.11 | 4.08 | 4.07 | 4.06 | 0.5% | 0 0 0 0 0 |
| Defaults | Bicycle | Radix · WebGPU | 8.96 | 8.92 | 8.93 | 8.95 | 8.90 | 0.3% | 0 0 0 0 0 |
| Defaults | Bicycle | Radix · WebGL2 | 13.54 | 13.43 | 13.42 | 13.50 | 13.52 | 0.4% | 0 0 0 0 0 |
| Defaults | Bicycle | Spark · WebGL2 | 12.80 | 12.92 | 12.88 | 12.97 | 12.93 | 0.5% | 0 0 0 0 0 |
| Defaults | Bicycle | PlayCanvas · WebGPU | 4.56 | 4.54 | 4.61 | 4.51 | 4.47 | 1.2% | 0 0 0 0 0 |
| Defaults | Bicycle | PlayCanvas · WebGL2 | 9.34 | 9.36 | 9.40 | 9.30 | 9.44 | 0.6% | 0 0 0 0 0 |
| Defaults | Grace interior | Radix · WebGPU | 9.26 | 9.18 | 9.19 | 9.24 | 9.21 | 0.3% | 0 0 0 0 0 |
| Defaults | Grace interior | Radix · WebGL2 | 15.60 | 15.63 | 15.59 | 15.65 | 15.60 | 0.2% | 0 0 0 0 0 |
| Defaults | Grace interior | Spark · WebGL2 | 16.43 | 16.63 | 16.38 | 16.64 | 16.52 | 0.7% | 0 0 0 0 0 |
| Defaults | Grace interior | PlayCanvas · WebGPU | 7.55 | 7.44 | 7.73 | 7.56 | 7.54 | 1.4% | 0 0 0 0 0 |
| Defaults | Grace interior | PlayCanvas · WebGL2 | 11.36 | 11.30 | 11.04 | 11.29 | 11.31 | 1.1% | 0 0 0 0 0 |
| No culling | Butterfly | Radix · WebGPU | 3.64 | 3.65 | 3.64 | 3.61 | 3.59 | 0.7% | 0 0 0 0 0 |
| No culling | Butterfly | Radix · WebGL2 | 4.52 | 4.56 | 4.55 | 4.53 | 4.53 | 0.3% | 0 0 0 0 0 |
| No culling | Butterfly | Spark · WebGL2 | 3.80 | 3.82 | 3.79 | 3.82 | 3.76 | 0.6% | 0 0 0 0 0 |
| No culling | Butterfly | PlayCanvas · WebGPU | 3.68 | 3.63 | 3.63 | 3.64 | 3.64 | 0.5% | 0 0 0 0 0 |
| No culling | Butterfly | PlayCanvas · WebGL2 | 3.52 | 3.51 | 3.52 | 3.51 | 3.51 | 0.2% | 0 0 0 0 0 |
| No culling | Ceramic (cave) | Radix · WebGPU | 5.99 | 6.02 | 6.03 | 6.04 | 6.01 | 0.3% | 0 0 0 0 0 |
| No culling | Ceramic (cave) | Radix · WebGL2 | 8.80 | 8.93 | 8.82 | 8.82 | 8.80 | 0.6% | 0 0 0 0 0 |
| No culling | Ceramic (cave) | Spark · WebGL2 | 7.81 | 7.69 | 7.83 | 7.81 | 7.77 | 0.7% | 0 0 0 0 0 |
| No culling | Ceramic (cave) | PlayCanvas · WebGPU | 5.65 | 5.63 | 5.66 | 5.58 | 5.60 | 0.6% | 0 0 0 0 0 |
| No culling | Ceramic (cave) | PlayCanvas · WebGL2 | 6.08 | 6.07 | 6.08 | 6.07 | 6.21 | 1.0% | 0 0 0 0 0 |
| No culling | Sutro | Radix · WebGPU | 4.33 | 4.32 | 4.34 | 4.35 | 4.35 | 0.3% | 0 0 0 0 0 |
| No culling | Sutro | Radix · WebGL2 | 7.01 | 7.06 | 7.02 | 7.03 | 7.03 | 0.3% | 0 0 0 0 0 |
| No culling | Sutro | Spark · WebGL2 | 6.33 | 6.35 | 6.33 | 6.35 | 6.33 | 0.2% | 0 0 0 0 0 |
| No culling | Sutro | PlayCanvas · WebGPU | 4.06 | 4.13 | 4.15 | 4.09 | 4.10 | 0.8% | 0 0 0 0 0 |
| No culling | Sutro | PlayCanvas · WebGL2 | 4.58 | 4.65 | 4.56 | 4.58 | 4.56 | 0.8% | 0 0 0 0 0 |
| No culling | Bicycle | Radix · WebGPU | 8.81 | 8.89 | 8.86 | 8.79 | 8.80 | 0.5% | 0 0 0 0 0 |
| No culling | Bicycle | Radix · WebGL2 | 13.18 | 13.19 | 13.11 | 13.19 | 13.15 | 0.2% | 0 0 0 0 0 |
| No culling | Bicycle | Spark · WebGL2 | 12.77 | 12.71 | 12.75 | 12.84 | 12.89 | 0.6% | 0 0 0 0 0 |
| No culling | Bicycle | PlayCanvas · WebGPU | 7.53 | 7.47 | 7.44 | 7.50 | 7.48 | 0.4% | 0 0 0 0 0 |
| No culling | Bicycle | PlayCanvas · WebGL2 | 9.55 | 9.61 | 9.64 | 9.63 | 9.64 | 0.4% | 0 0 0 0 0 |
| No culling | Grace interior | Radix · WebGPU | 9.04 | 9.03 | 9.02 | 9.02 | 8.99 | 0.2% | 0 0 0 0 0 |
| No culling | Grace interior | Radix · WebGL2 | 15.47 | 15.55 | 15.45 | 15.58 | 15.54 | 0.4% | 0 0 0 0 0 |
| No culling | Grace interior | Spark · WebGL2 | 16.43 | 16.45 | 16.40 | 16.47 | 16.50 | 0.2% | 0 0 0 0 0 |
| No culling | Grace interior | PlayCanvas · WebGPU | 9.22 | 9.25 | 9.43 | 9.25 | 9.22 | 1.0% | 0 0 0 0 0 |
| No culling | Grace interior | PlayCanvas · WebGL2 | 12.29 | 12.40 | 12.29 | 12.37 | 12.36 | 0.4% | 0 0 0 0 0 |
| Matched culling | Butterfly | Radix · WebGPU | 3.44 | 3.45 | 3.44 | 3.40 | 3.45 | 0.6% | 0 0 0 0 0 |
| Matched culling | Butterfly | Radix · WebGL2 | 4.49 | 4.49 | 4.47 | 4.46 | 4.48 | 0.3% | 0 0 0 0 0 |
| Matched culling | Butterfly | Spark · WebGL2 | 3.69 | 3.69 | 3.70 | 3.69 | 3.68 | 0.3% | 0 0 0 0 0 |
| Matched culling | Ceramic (cave) | Radix · WebGPU | 5.81 | 5.82 | 5.85 | 5.84 | 5.79 | 0.4% | 0 0 0 0 0 |
| Matched culling | Ceramic (cave) | Radix · WebGL2 | 8.74 | 8.75 | 8.74 | 8.75 | 8.71 | 0.2% | 0 0 0 0 0 |
| Matched culling | Ceramic (cave) | Spark · WebGL2 | 7.71 | 7.76 | 7.79 | 7.76 | 7.70 | 0.5% | 0 0 0 0 0 |
| Matched culling | Sutro | Radix · WebGPU | 3.43 | 3.42 | 3.42 | 3.42 | 3.42 | 0.1% | 0 0 0 0 0 |
| Matched culling | Sutro | Radix · WebGL2 | 5.97 | 5.96 | 5.98 | 5.99 | 5.98 | 0.2% | 0 0 0 0 0 |
| Matched culling | Sutro | Spark · WebGL2 | 5.44 | 5.41 | 5.40 | 5.41 | 5.45 | 0.4% | 0 0 0 0 0 |
| Matched culling | Bicycle | Radix · WebGPU | 4.82 | 4.85 | 4.84 | 4.81 | 4.83 | 0.3% | 0 0 0 0 0 |
| Matched culling | Bicycle | Radix · WebGL2 | 12.48 | 12.49 | 12.46 | 12.47 | 12.56 | 0.3% | 0 0 0 0 0 |
| Matched culling | Bicycle | Spark · WebGL2 | 12.14 | 12.18 | 12.12 | 12.07 | 12.16 | 0.4% | 0 0 0 0 0 |
| Matched culling | Grace interior | Radix · WebGPU | 8.12 | 8.09 | 8.09 | 8.06 | 8.06 | 0.3% | 0 0 0 0 0 |
| Matched culling | Grace interior | Radix · WebGL2 | 15.46 | 15.01 | 15.09 | 15.14 | 15.13 | 1.1% | 0 0 0 0 0 |
| Matched culling | Grace interior | Spark · WebGL2 | 16.01 | 16.11 | 16.14 | 16.18 | 16.07 | 0.4% | 0 0 0 0 0 |
Each scan was decoded by Radix's loader and written as a standard degree-0 3DGS PLY (float32 position, log scales, rotation, logit opacity, DC color), so all three renderers load the same bytes and draw degree-0 color only: Radix has no spherical harmonics, and Spark and PlayCanvas would otherwise evaluate the degree-3 SH of the Grace, Sutro, and bicycle files. PlayCanvas reads no SPZ, so the SPZ scans needed converting anyway. The decode quantizes slightly (SPZ sources are quantized already; the PLY sources lose some color and scale precision), identically for every renderer.
| scene | splats | source | path | start view (upright frame) |
|---|---|---|---|---|
| Butterfly | 546,112 | /splats/bench/butterfly.ply | orbit the target | from (0.00, -0.02, 1.33) to (0.00, -0.02, 0.00), fov 50° |
| Ceramic (cave) | 2,000,000 | /splats/bench/ceramic.ply | look around while walking a small circle | from (-0.23, 0.55, -4.12) to (0.00, 0.00, 0.00), fov 50° |
| Sutro | 2,001,220 | /splats/bench/sutro.ply (SH3 dropped) | orbit the target | from (0.00, 1.50, -1.20) to (0.00, 1.50, 0.00), fov 60° |
| Bicycle | 6,131,954 | /splats/bench/bicycle.ply (SH3 dropped) | orbit the target | from (-3.50, 0.90, 0.00) to (0.00, -0.70, 0.00), fov 55° |
| Grace interior | 9,997,774 | /splats/bench/grace.ply (SH3 dropped) | look around while walking a small circle | from (-14.00, 8.00, -3.00) to (-4.00, 8.00, -3.00), fov 60° |
| renderer | version | setup | defaults that matter |
|---|---|---|---|
| Radix | as of the run, Oct 9, 2026 | createRenderer() (three.js 0.186 WebGPURenderer; ?gpu=webgpu-gl for its WebGL2 backend) and one SplatRenderer | WebGPU: the GPU sort every frame; WebGL2: autoSort (keys on the GPU, a worker sort, one in flight). ewa, 24-bit sort keys (float bits), blur 0.3 with opacity compensation, alpha 0.5/255, no size or contribution cull, radial sort |
| Spark | 2.3.1 | three.js 0.186 WebGLRenderer (antialias off), a SparkRenderer and one SplatMesh | WebGL2 only. A worker sort as the camera moves; 32-bit sort keys (fixed); no level of detail for plain files; blur 0.3 with opacity compensation; alpha 0.5/255; no size cull (minPixelRadius 0) and no contribution cull; radial sort |
| PlayCanvas | 2.23.1 | createGraphicsDevice (WebGPU or WebGL2, antialias off), an Application with a camera and one gsplat component (unified rendering) | WebGPU: GPU sort; WebGL2: CPU sort on a worker. minPixelSize 2 and minContribution 3 (GPU sort only), in units of twice the focal length (a 1 px radius at √8 σ, 0.75 px²), alpha 1/255, a 0.075 px² dilation without compensation, sort along the view direction |
Each renderer sorts with its default key width. Radix uses 24 bits: the distance's float bits without the low 7, so splats more than about 0.0015% of their distance apart are ordered. Spark always sorts 32-bit keys (the float distance). PlayCanvas picks its width from the splat count and spreads it linearly over the range of visible distances, with more of it near the camera; its GPU sort rounds the width up to whole 8-bit radix digits.
| scene | splats | Radix | Spark | PlayCanvas · WebGPU | PlayCanvas · WebGL2 |
|---|---|---|---|---|---|
| Butterfly | 546k | 24 | 32 | 24 (17 by count) | 17 |
| Ceramic (cave) | 2.0M | 24 | 32 | 24 (19 by count) | 19 |
| Sutro | 2.0M | 24 | 32 | 24 (19 by count) | 19 |
| Bicycle | 6.1M | 24 | 32 | 24 (20 by count) | 20 |
| Grace interior | 10.0M | 24 | 32 | 24 (20 by count) | 20 |
All renderers draw at 1920×1080 at pixel ratio 1 with MSAA off, onto a black background, in headless Chrome (WebGPU on Metal; WebGL2 through ANGLE on Metal). Each config runs in a fresh tab, so nothing is shared between renderers or scenes.
renderer.render; PlayCanvas: the application's tick, its whole update and render), then a wait until the GPU finished all submitted work (WebGPU: queue.onSubmittedWorkDone(); WebGL2: a one-pixel readPixels), then a yield to the event loop, so asynchronous sorts on workers land as they would in an app. A frame's time runs from its call to the GPU being done.requestAnimationFrame timing was tried and rejected: Chrome runs WebGL frames far ahead of the GPU without vsync (one renderer timed at 16,000 fps).requestAnimationFrame in headed Chrome with vsync on: one warm-up pass, then 2 measured passes. Thermal waits and the canary (a timed run) are as above.