Жалобы «дисплейсменты вывернуты» и «сквозь них видно только скайбокс» оказались одной механикой. Меш 3D-скайбокса рисуется без записи глубины и без отсечения, поэтому всегда заполнял то, что мир не нарисовал, и маскировал пропуск: вместо травяных холмов gm_construct был виден чужой ландшафт скайбокса, а из-под карты изнанка рельефа читалась как «трава сверху». Замер: геометрия рельефа в буфере здоровая (0 вырожденных, 0 нефинитных, габариты — земля карты), батч выдаётся, но `--only-material 36` даёт пусто и только `SOURCE_RS_NO_CULL=1` возвращает `#93ff26` на экран. Замер верхней поверхности над точкой: пол под спавном (z=-144) намотан с кросс-произведением вниз, трава (z=-148) — вверх. Видимая сверху поверхность — та, у которой кросс-произведение смотрит внутрь тела; браш-грани приводятся к этому в `face_winding`, а `build_displacement` подгонял намотку вдоль нормали плоскости, то есть наоборот. Порядок индексов в `emit` перевёрнут; нормали вершин оставлены наружу, чтобы динамический свет не поехал. Коллизия не задета — `add_displacement_tris` строит нормали по рёбрам, порядок вершин для неё неважен. Новая диагностика: переключатель `SOURCE_RS_NO_SKY_MESH` (гасит только меш скайбокса), лог «топ-материал по объёму геометрии» и предупреждение о батчах с индексом вне списка материалов — рендер пропускает такие молча. Отмечу: в тестах мир строится с пустым VirtualFs, из-за чего 189 материалов против 54 в рантайме и индексы расходятся, поэтому диагностика печатается из сборки мира. Тесты: real_map_top_surface_orientation (замер), real_map_displacement_is_visible_from_above (держит правку). Все 22 теста с картой зелёные.
12 KiB
Вопросы Gemini по Source BSP — gmod-rs
Репозиторий открыт: git@git.n1ko.dev:Niko/gmod-rs.git
Ключевые файлы: crates/engine/src/world.rs, crates/formats/src/bsp.rs,
crates/render/src/world.wgsl.
Ниже — только то, что уже измерено. Прежние гипотезы помечены как опровергнутые, чтобы не тратили время.
Контекст
Клиент Garry's Mod на Rust. Карта gm_construct (BSP v20, 36 МБ).
Жалоба игрока: «трава вывернута, видно комнату под землёй, в 0 0 белые плиты,
которых нет в оригинале».
Что измерено и уже исключено
LUMP_DISP_VERTS(33) читается верно — f32, шаг 20 байт.vec ≈ (0,0,1),dist18.1 / 18.6 / 19.3 / 20.1 на соседних вершинах. Гипотеза «нормаль берётся изdv.vec» неверна.- Ориентация рельефа в порядке — 110 базовых плоскостей смотрят вверх, 0 вниз; намотка не перевёрнута (0 из 11 776 треугольников). Рельеф в радиусе 600 от спавна: z от −151.4 до −148.0, то есть почти плоский.
- Чёрные области — не тени. Принудительный
return 1.0вsun_shadowдаёт тот же кадр. Значит чёрное приходит изlit = in.light * ambient. - Справочник лампов — stride 16, не 48.
LUMP_NODES(5) — это дерево BSP.
Вопрос 1. Главный
Кадр с камеры (0, 0, 2200), pitch 55, yaw 180, fov 100 показывает:
- трава — ровная оливковая плоскость без рельефа и без вариаций лайтмапы; при этом габарит рельефа по z от −148 до 11 235, то есть холмы в карте есть;
- в центре — бетонная плита 2480 × 1936 × 8 с габаритом
(−1240,−968,−4)..(1240,968,4), материалmaps/gm_construct/concrete/concretefloor028a_1280_-1600_-64, продублированная (грани 9379 == 9385, 9380 == 9386); - лампа
LUMP_DISP_INFO(26) — 19 360 байт; в рендер попадает 89 displacement-граней при 110 плоскостях.
Почему рельеф читается плоским, если геометрия изогнута, и куда деваются 21 displacement-грань из 110?
Конкретно: правильно ли в build_displacement (world.rs, ~строка 1676)
восстанавливать позицию вершины как base + dv.vec * dv.dist, где base —
билинейная интерполяция угла грани, и брать углы грани из
geometry.face_vertex_indices? Или углы надо брать из DispInfo.start_position
и DispInfo иначе, и из-за этого сетка «съезжает»?
Вопрос 2. Как в Source грувится displacement-поверхность
Нужен алгоритм движка Source (не код, а порядок шагов), который восстанавливает поверхность:
- Откуда берутся 4 опорные точки сетки и в каком порядке (сравнение с
startPositionкак у нас, или иначе)? - Какой знак и какая база у
dv.dist— смещение от базовой плоскости грани или от плоскостиDispInfo? - Нужно ли учитывать
DispInfo.axis/scale/subdiv? - Почему у нас
vecпочти всегда(0,0,1)— это нормально для Source или мы читаем не то поле?
Вопрос 3. Отсев невидимых кирпичей
На карте 2 203 кирпича и 9 460 граней, номера не параллельны
(14 946 сторон). Ни одна сущность не имеет ключа brushlist.
Связь «грань → кирпич» находится только через planenum + проверку, что все
вершины грани внутри выпуклой оболочки кирпича.
В Source BSP v1 dbrushside_t занимает ровно 8 байт и не хранит номер
грани. Значит ли это, что в движке действительно нет прямой связи, и он
определяет принадлежность иначе — например, по LUMP_LEAFBRUSHES (17)
или по тому, что не-твёрдые кирпичи (contents == 0) вообще не получают
граней?
Как в Source клиент решает, что грань не рисуется, если у кирпича
CONTENTS_EMPTY, а SURF_NODRAW не выставлен?
У нас под правило «contents == 0 → не рисуем» попадают 330 граней, из них
250 — plasterwall022c, 50 — CONSTRUCT_CONCRETE_GROUND, и это явно видимая
геометрия. Значит правило в лоб неверно.
Вопрос 4. dface_t в BSP v20
LUMP_FACES (7) — 529 760 байт, версия 1 (LZMA). После распаковки сколько
байт на грань? У Source BSP v20 dface_t = 56 байт с полями
planenum, side, firstedge, numedges, texinfo, styles[4], lightofs.
Наш парсер даёт 9 460 граней. Совпадает ли 56 × 9460 = 529 360 с
распакованным размером? Если да — значит dface_t у нас разбирается
правильно; если нет — у нас сдвиг на какое-то поле, и это может быть
причиной и плоского рельефа, и белых плит.
Часть 2. Что подтвердилось, и новые вопросы
Проверено по вашему ответу — подтвердилось
LUMP_FACESне сжат (fourCC = 0),529 760 / 56 = 9 460ровно. Все поляdface_tчитаются по вашей таблице смещений байт-в-байт.- Белые плиты в (0,0,0) — это браш-сущности без
origin. ЗамерLUMP_MODELS: 624 / 48 = 13 моделей, модель 0 — грани 0..9363, модели 1..12 — грани 9363..9460. Модели 1, 8 лежат на YZ, 2 и 5 на XZ, 3 и 4 на XY — это и есть «оси координат 3д» из жалобы. Исправлено в рендере, плиты уехали наorigin. LUMP_BRUSHES/LUMP_BRUSHSIDESдля рендера не нужны. Правило «contents == 0→ не рисуем» удалено.- Порядок углов и UV лайтмапы displacement у нас совпадают с вашим
алгоритмом:
L(v)=lerp(C0,C1,v),R(v)=lerp(C3,C2,v),base=lerp(L,R,u),vert_idx = row*N+col,luxel = 0.5 + param * LightmapTextureSizeInLuxels. - Ваши вопросы сняты измерением:
110 = 89 (мир) + 21 (3D-скайбокс), потерь нет; у всех 110numedges = 4; все 110 вmodels[0];startPositionу всех 110 совпадает с углом грани (худшее расстояние 0.053 юнита);powerиDispVertStartсходятся (сумма сеток 12 638 =LUMP_DISP_VERTS).
Что мы дополнительно нашли про коллизию (вам это может быть интересно)
В dmodel_t поле по смещению 36 оказалось корнем поддерева BSP
(headnode), а не visleafs: обход из этого узла по LUMP_LEAFBRUSHES
даёт для модели 0 — 2 168 кирпичей, для моделей 1..5 — по 1..5 кирпичей с
габаритами, в точности совпадающими с локальными габаритами моделей.
14 кирпичей не принадлежат ни одной модели.
Кирпичи 2184 и 2185 (оба CONTENTS_SOLID, габарит
(−1240,−968,−4)..(1240,968,4)) — это те самые две плиты, их коллизия
стояла в (0,0,0).
Вопрос 5. Порядок dDispVert — строка или столбец
Снято измерением (см. docs/PARITY.md E18): «вывернутость» была не в
порядке индексов, а в намотке. Контрольный кадр сверху с погашенным мешем
3D-скайбокса (SOURCE_RS_NO_SKY_MESH=1) показал, что рельеф не рисуется
вообще, с SOURCE_RS_NO_CULL=1 — рисуется; замер верхней поверхности дал:
пол под спавном (z=−144) намотан с кросс-произведением вниз, трава
(z=−148) — вверх. build_displacement подгонял намотку вдоль нормали
плоскости, то есть ровно наоборот тому, как намотаны браш-грани (face_winding).
Порядок индексов перевёрнут — рельеф виден сверху, row/col оставили как
есть, рёбра соседних displacement'ов по-прежнему совпадают.
Остаётся вопрос для понимания формата, но он больше не влияет на картинку:
Мы индексируем verts[a * side + b], где a идёт вдоль ребра
C0 → C1 (это v, строка сетки), b — вдоль C0 → C3 (это u, столбец).
То есть index = row * (N) + col.
Верно ли это для LUMP_DISP_VERTS, или vbsp раскладывает записи
по-другому?
Вопрос 6. Резкие границы света на дисплейсментах
На траве видны резкие прямые границы светлого и тёмного с чётким углом — так, будто лайтмапа вырезана плитами, а не градиентом.
- Для displacement запекается своя лайтмапа, не та же, что у базового
браша, верно? И
dface_t.lightofsдля displacement-грани указывает именно на её блок? - Правильно ли мы берём размер блока как
LightmapTextureSizeInLuxels, а ширину строки —size[0] + 1, и индексlight_offset + (y * (size[0]+1) + x) * 4? - Нужно ли учитывать
LightmapTextureMinsInLuxelsдля displacement (мы для displacement его не вычитаем, потому чтоlightofsуже указывает на начало блока этой грани)? - Что делает поле
ddispinfo_t.lightmapSamplePositionStartи участвует ли оно в раскладке люкселей? Мы его не читаем.
Вопрос 7. angles браш-сущностей
Мы переносим геометрию и коллизию браш-моделей только трансляцией на
origin, без поворота по angles.
- Правильно ли, что
dmodel_tхранит геометрию уже повёрнутую поanglesсущности и отцентрированную вокругorigin, то есть поворот применять не нужно? Или в BSP лежит неповёрнутая геометрия, и поворот делает движок при рендере? - Как в Source ведут себя
func_illusionaryиfunc_vehicleclipс точки зрения коллизии клиента? Мы для них коллизию отключаем, а дляfunc_brush,func_reflective_glassиfunc_button— оставляем.