01 The problem

An image with nothing to measure

Anyone buying a flat wants to check whether the wardrobe fits and how high the ceiling is. In a browser 3D scene that ruler is a textbook exercise: cast a ray from the cursor, hit a wall, subtract two points. We deliberately have no 3D scene in the browser.

GetFloorPlan tours are path-traced in Unreal Engine. A single frame takes tens of minutes on a farm of RTX machines, which no buyer's phone could reproduce, so the client receives a finished panorama of half a megabyte to a couple of megabytes. It loads fast and looks right, but it carries no geometry — there is nothing for the cursor to hit.

02 The decision

A second image that stores distance instead of colour

In the same engine pass we render a depth map next to the colour panorama, where each pixel holds the distance from the camera to the visible surface. Finding the point under the cursor then takes four steps: screen coordinates become a direction, the direction becomes coordinates on the unwrapped sphere, and the depth at that pixel turns it into a point in space.

A single point does not tell the user which surface they are on, so we estimate a normal from a small window of neighbouring pixels and slide a finder along the surface, turned to match it. The window size is a trade-off: a larger one steadies the finder on a flat wall but blends two surfaces at the edge of a cabinet. Occlusion by furniture happens in the shader — where the scene is closer than the ruler, the line fades.

Delivery was the hard part. The engine writes depth to EXR, which browsers cannot decode: parsing it in JavaScript took about 241 ms against 34 ms for a PNG, and had to move into a web worker. So we pack each value into the 24 bits of a PNG's three colour channels, which the browser decodes natively.

03 The outcome

A working ruler, and the bill for the PNG

The ruler has lived in our internal viewer since 2024 and now runs in the web widget, where it can be tried in a demo tour. At one to five metres the stored depth resolves to roughly 0.6–2.5 mm. The dominant error lies elsewhere — in how closely a model built from a floor plan matches the finished flat — and we have not yet measured the tool's end-to-end accuracy.

The format saved no bytes. The PNG came out 1.8 times heavier than the source EXR, and a brotli-compressed float16 array would have been 2.7 times lighter than the PNG. We chose PNG for the client work it saved and did not measure size at the time. The migration stretched over twenty months, and the fallback to EXR spent more than a year filing meaningless errors in Sentry.

04 Takeaways

The convention is as much a part of the system as the code

The trade-off between photoreal and measurable turned out to be false. But data packed into a format designed for something else brings an unwritten agreement about how to read it. Our packing multiplier lives in three systems written in three languages; change it in one and every distance silently shrinks tenfold, with no test or alert to notice.

Starting again, I would ship the packing parameters with the data and make the loader verify them, add a byte-exact round-trip test to CI, keep the source files, and measure the ruler against known dimensions from the floor plan before calling it accurate.