gmod-rs/GEMINI.md
Niko Marmeladkov 4d9a8b580f
Some checks failed
CI / checks (push) Has been cancelled
Рельеф не рисовался: намотка displacement'ов была перевёрнута
Жалобы «дисплейсменты вывернуты» и «сквозь них видно только скайбокс»
оказались одной механикой. Меш 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 теста с картой зелёные.
2026-10-16 11:12:48 +03:00

12 KiB
Raw Permalink Blame History

Вопросы 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 белые плиты, которых нет в оригинале».

Что измерено и уже исключено

  1. LUMP_DISP_VERTS (33) читается верно — f32, шаг 20 байт. vec ≈ (0,0,1), dist 18.1 / 18.6 / 19.3 / 20.1 на соседних вершинах. Гипотеза «нормаль берётся из dv.vec» неверна.
  2. Ориентация рельефа в порядке — 110 базовых плоскостей смотрят вверх, 0 вниз; намотка не перевёрнута (0 из 11 776 треугольников). Рельеф в радиусе 600 от спавна: z от −151.4 до −148.0, то есть почти плоский.
  3. Чёрные области — не тени. Принудительный return 1.0 в sun_shadow даёт тот же кадр. Значит чёрное приходит из lit = in.light * ambient.
  4. Справочник лампов — 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 (не код, а порядок шагов), который восстанавливает поверхность:

  1. Откуда берутся 4 опорные точки сетки и в каком порядке (сравнение с startPosition как у нас, или иначе)?
  2. Какой знак и какая база у dv.dist — смещение от базовой плоскости грани или от плоскости DispInfo?
  3. Нужно ли учитывать DispInfo.axis/scale/subdiv?
  4. Почему у нас 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. Что подтвердилось, и новые вопросы

Проверено по вашему ответу — подтвердилось

  1. LUMP_FACES не сжат (fourCC = 0), 529 760 / 56 = 9 460 ровно. Все поля dface_t читаются по вашей таблице смещений байт-в-байт.
  2. Белые плиты в (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.
  3. LUMP_BRUSHES/LUMP_BRUSHSIDES для рендера не нужны. Правило «contents == 0 → не рисуем» удалено.
  4. Порядок углов и 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.
  5. Ваши вопросы сняты измерением: 110 = 89 (мир) + 21 (3D-скайбокс), потерь нет; у всех 110 numedges = 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. Резкие границы света на дисплейсментах

На траве видны резкие прямые границы светлого и тёмного с чётким углом — так, будто лайтмапа вырезана плитами, а не градиентом.

  1. Для displacement запекается своя лайтмапа, не та же, что у базового браша, верно? И dface_t.lightofs для displacement-грани указывает именно на её блок?
  2. Правильно ли мы берём размер блока как LightmapTextureSizeInLuxels, а ширину строки — size[0] + 1, и индекс light_offset + (y * (size[0]+1) + x) * 4?
  3. Нужно ли учитывать LightmapTextureMinsInLuxels для displacement (мы для displacement его не вычитаем, потому что lightofs уже указывает на начало блока этой грани)?
  4. Что делает поле ddispinfo_t.lightmapSamplePositionStart и участвует ли оно в раскладке люкселей? Мы его не читаем.

Вопрос 7. angles браш-сущностей

Мы переносим геометрию и коллизию браш-моделей только трансляцией на origin, без поворота по angles.

  1. Правильно ли, что dmodel_t хранит геометрию уже повёрнутую по angles сущности и отцентрированную вокруг origin, то есть поворот применять не нужно? Или в BSP лежит неповёрнутая геометрия, и поворот делает движок при рендере?
  2. Как в Source ведут себя func_illusionary и func_vehicleclip с точки зрения коллизии клиента? Мы для них коллизию отключаем, а для func_brush, func_reflective_glass и func_button — оставляем.