01 Проблема

Картинка, в которой нечем мерить

Покупателю квартиры хочется проверить, встанет ли шкаф и какой высоты потолок. В браузерной 3D-сцене такая линейка — почти учебная задача: луч от курсора пересекает стену, расстояние считается одной строкой. Но 3D-сцены в браузере у нас нет, и это сознательное решение.

Туры GetFloorPlan рендерятся в Unreal Engine с трассировкой пути: один кадр считается десятки минут на ферме с видеокартами RTX, и повторить это на телефоне покупателя невозможно. Поэтому клиенту уходит готовая панорама весом от половины мегабайта до пары. Она быстро грузится и хорошо выглядит, но геометрии в ней нет — курсору не во что попасть.

02 Решение

Вторая картинка, где вместо цвета — расстояние

Тем же прогоном движка рядом с цветной панорамой рендерится карта глубины: в каждом пикселе хранится дистанция от камеры до видимой поверхности. Точка под курсором находится за четыре шага: экранные координаты превращаются в направление, направление — в координаты на развёртке сферы, а глубина в этом пикселе даёт точку в пространстве.

Одной точки мало, чтобы пользователь понимал, на какую плоскость навёлся. Поэтому по небольшому окну соседних пикселей считается нормаль, и по поверхности скользит «искатель», повёрнутый вместе с ней. Размер окна — компромисс: на гладкой стене большое окно убирает дрожание, а на ребре шкафа смешивает две поверхности. Перекрытие мебелью считается прямо в шейдере: если сцена в этом направлении ближе линейки, линия становится полупрозрачной.

Сложнее всего оказалось довезти данные до браузера. Движок пишет глубину в EXR, который браузер не понимает: разбор на JavaScript занимал около 241 мс против 34 мс у PNG, и его пришлось увести в web worker. Поэтому мы упаковали каждое значение в 24 бита трёх цветовых каналов PNG — такой файл браузер декодирует нативно.

03 Результат

Линейка работает — и счёт за PNG

Линейка живёт во внутреннем вьюере с 2024 года, а теперь и в веб-виджете, где её можно попробовать в демо-туре. На дистанциях от одного до пяти метров шаг хранения глубины — примерно 0,6–2,5 мм. Основная погрешность при этом не в формате, а в том, насколько модель по чертежу совпадает с реальной квартирой; сквозную точность инструмента мы пока не измеряли.

Экономии в байтах не случилось: PNG вышел в 1,8 раза тяжелее исходного EXR, а сжатый brotli массив float16 был бы в 2,7 раза легче PNG. Формат выбирали по объёму работы на клиенте, размер тогда не измеряли. Миграция растянулась на двадцать месяцев, а резервный путь на EXR больше года сыпал в Sentry ошибками, которые ничего не значили.

04 Выводы

Соглашение — такая же часть системы, как код

Компромисс «красиво или измеримо» оказался ложным. Но у данных, упакованных в формат, придуманный для другого, появляется незаписанное соглашение о том, как их читать. Множитель упаковки у нас живёт в трёх системах на трёх языках, и если поменять его в одном месте, все расстояния молча станут в десять раз меньше — ни тесты, ни мониторинг этого не заметят.

Начиная заново, я положил бы параметры упаковки рядом с данными и заставил загрузчик их сверять, добавил бы в CI круговой тест на побайтовую сохранность, не удалял бы исходники и измерил бы точность линейки по известным размерам из чертежа, прежде чем называть её точной.