What a shard costs, measured
Every piece in a shatter is evaluated at every pixel, so the cost is linear in piece count over the whole screen and everyone knows to be nervous about it. Nobody seems to know the constant. Here it is, measured rather than reasoned about.
The measurement
A WebGL2 fragment shader, 960×540, one piece being a circle intersected with two half planes, which is the same construction the destruction notes use. Median of 90 frames after a 40 frame warm-up, on an Apple M4 Max.
| Pieces | No bound | Bounding test | Saved |
|---|---|---|---|
| 1 | 0.112 ms | 0.100 ms | n/a |
| 4 | 0.100 ms | 0.112 ms | n/a |
| 8 | 0.125 ms | 0.125 ms | 0% |
| 16 | 0.162 ms | 0.125 ms | 23% |
| 32 | 0.213 ms | 0.163 ms | 23% |
| 64 | 0.350 ms | 0.250 ms | 29% |
| 128 | 0.563 ms | 0.438 ms | 22% |
Three things in that table
1. There is a floor, and below it the piece count is irrelevant
One piece and four pieces measure the same, around 0.10ms. That is not the shader, it is the cost of issuing a full-screen draw at all. Anything under about eight pieces is free in the sense that removing them buys you nothing measurable.
Which is a useful thing to know before optimizing. The first instinct on seeing a shatter cost 0.11ms is to reduce the piece count, and at that end of the curve reducing it to zero would save 0.01ms.
2. Above the floor it is exactly linear, at 0.007 ms per piece per megapixel
From 8 pieces to 128 the cost rises 0.438ms over 120 pieces: 0.00365ms per piece at 0.518 megapixels. The fit is close enough to be boring, which is what you want from a cost model. Predicted 32 pieces: 0.213ms. Measured: 0.213ms.
Normalized, 0.0070ms per piece per megapixel. That number travels, which is the point of measuring it:
| Resolution | Per piece | Pieces in a 2ms budget |
|---|---|---|
| 1280×720 | 0.0065 ms | ~310 |
| 1920×1080 | 0.0146 ms | ~137 |
| 2560×1440 | 0.0259 ms | ~77 |
So a hundred shards at 1080p is about 1.5ms, which is a real cost inside a 16.7ms frame and an entirely affordable one for something that happens for half a second. A thousand shards is 15ms and is not a thing you can do this way.
3. The bounding test buys 22%, and that is the surprising one
The standard advice is to reject each piece with a cheap bounding circle before evaluating its terms, and the mental model behind that advice is that most pixels are far from most pieces, so most of the work disappears. Measured, it removes about a fifth.
Two reasons, and both are worth internalising because they generalise past this case:
-
The test is most of the piece. Rejecting needs
length(p − pos), and the piece's body islength(rot(p − pos)) − R. You skip a rotate, two dot products and two maxes, having already paid for the expensive part. - A fragment shader branches per group, not per pixel. The hardware runs many pixels in lockstep, so the piece's cost is skipped only when every pixel in the group rejects it. Near any piece's edge, all of them pay.
That does not make it useless. A fifth off is a fifth off, it costs three lines, and it grows with piece count as more pieces become uniformly distant. It does mean the optimization is not the thing standing between you and a thousand shards.
What actually raises the ceiling
In order of how much they buy:
- Draw the pieces as geometry instead of evaluating them per pixel. One quad per piece, the field evaluated only inside it. This changes the complexity class: cost becomes proportional to the area the pieces actually cover rather than to pieces times the whole screen. It is the real answer above a few hundred.
- Shrink the pieces as they multiply. A thousand-piece shatter has tiny pieces, and with geometry that means tiny quads. The cost of a shatter is roughly its covered area, which stays constant as you subdivide.
- Render at half resolution. Cost is linear in pixels, so this is an exact 4x, and debris in motion is the single most forgiving thing to blur.
- Lower the piece count over time. Nobody is counting shards two seconds in. Fading and merging late pieces is invisible and free.
Rules of thumb
- Budget 0.007ms per piece per megapixel for a per-pixel loop, and verify it on your own hardware in an afternoon.
- Under about eight pieces, the piece count is not what you are paying for.
- A hundred pieces at 1080p is roughly 1.5ms. A thousand is not a per-pixel loop.
- A bounding test buys about a fifth, not an order of magnitude, because the test is most of the piece and the branch is per group.
- Past a few hundred pieces, switch to one quad per piece. That is a complexity change, not a constant-factor one.
- Measure with the queue forced to complete. Timing a draw call without a flush measures how fast commands were queued.
One email when something new goes up. No newsletter, no schedule, nothing else.
Double opt-in, so watch for a confirmation email. Unsubscribe any time.