Last week I promised a look under the hood of the fork. Instead of a tour, here's one small piece that cost me a week and taught me more than any tour would: the roof map.
The problem
In the Two Worlds port, the first lighting switch was crude. Walk into a house and the whole frame switched to interior light. Fine for caves. Wrong for a hut with an open door: the land outside went dark too, and the picture got darker in general. What I wanted was simple to say: dark under a roof, bright everywhere else, in the same frame, per pixel.
For that, every pixel needs to know one thing: is there a roof above me?
Version 1: rays (0.19.0)
The first answer was a map of 128 x 128 m around the viewer. For each cell I cast one ray upward from about a metre over the viewer's height. If the ray hits a face that points down (a ceiling, a roof's underside), the cell is under a roof. If it hits a face pointing up, the ray started inside a solid, like a rock or a thick wall that runs on under the landscape. Those cells are left out. Tree crowns count as sky.
The result goes to the shaders as a texture. Under a roof the ambient light becomes the interior's, and the sun is switched off. A blur over 3 x 3 cells keeps the edge soft. A hut looked right with that; a wider blur left it half lit.
It worked. It also had a price.
The price
I measured before touching anything, walking eight seconds each at the start hall, the portico, the back of the hall and a hut:
- the ray map cost up to 6 ms per frame on the CPU, with spikes well above that, mostly at the portico
- giving the house halves their own triangle trees cut the worst case of the ray map from 54 ms to 15 ms
- and a second problem showed up in the screenshots: the portico's open steps were marked as roof, because a ray from the middle of a cell happily hits a ceiling above the columns
That last one matters more than the milliseconds. The columns are open on the sides. Standing between them, you are not indoors.
Version 2: draw it, don't ray it
The second answer is how a human would do it: look up. An orthographic depth pass from one metre above the viewer, looking up 60 m, over 128 m at 0.25 m per texel (512 x 512). The first downward face over a texel is the underside of its roof. A small compute shader turns that into the same texture the shaders already knew, with a roof weight from how much sky is visible.
On the CPU this cost 0.002 ms per frame, essentially nothing. In the pictures, the hall and the hut looked the same as before, and the portico steps were no longer roof. The columns were out.
The new price
The cost just moved. Each drawing took 1.8 to 3.5 ms on the GPU, and 12 ms once right after a teleport. I redraw about every two seconds while walking. A 3 ms spike every two seconds is the kind of thing you feel as a hitch, even if the average looks fine.
Version 3: bake it (0.26.0)
A roof does not move. Walk over the same ground twice and the answer is the same. So I stopped redrawing and started remembering.
- the world is cut into chunks of 32 m, and the map window is 5 x 5 chunks, 160 m, with its corner on the chunk grid so chunks line up between frames
- every drawing is read back to the CPU through a small readback ring, and the result is kept per chunk and per camera height (in 2 m steps)
- each kept chunk stores a fingerprint of the boxes of the objects drawn over it. If a house streams in or changes, the fingerprint no longer matches and the chunk is drawn again
- when all 25 chunks of the window are kept and still match, the map is assembled on the CPU the way the shader would, and uploaded as a new texture. No drawing at all.
- the chunks are written to disk, at most once a minute, so the second visit to a place starts warm
The first walk through an area still pays the GPU cost. Every later one doesn't. The CPU-side map also gets filled from the readback, so the hero's own roof test works with the camera map too.
What I'd do differently
I trusted the first version because it looked right in one hut. The portico showed me that "looks right in the test case" and "is right" are different things. The measurement script I wrote for the start hall found both the cost and the portico error in one run. Measure at the ugly place first.
Also: the ray version is still in the fork, switchable from the console (roofmap source cpu). It is the reference. When the picture looks wrong, I flip to it and compare.
Common pitfalls
- A ray from a cell centre is a point sample. Open structures like porticos and bridges need an area answer, not a point one.
- Caching by position alone is not enough. Without a fingerprint of what stands there, streamed-in houses leave stale roofs behind.
- Reading back GPU results stalls if you wait for them. Use a ring and take the result a few frames later.
- Average cost hides hitches. Look at the worst frame, not the mean.
30-second recap
- A per-pixel roof test needs a map of "roof or sky" around the viewer.
- Rays from the CPU worked, but cost up to 6 ms per frame and wrongly covered an open portico.
- An orthographic depth pass from the camera fixed the portico and removed the CPU cost, but cost 1.8-3.5 ms of GPU per drawing.
- Baking the result per 32 m chunk, with a fingerprint of the objects above it, removes most of those drawings.
- Keep the simple version around as a reference.
Next time: what happens when the GPU disappears in the middle of a frame.
— Marco
Comments
Leave a comment