Четыре жалобы из игры, все закрыты измерением.
Константы движения (gmodwiki.com/ConVars_In_Garrysmod, не source-sdk-2013):
sv_friction 4 -> 8 торможение 250 -> 10 за 0,40 с, путь 26 юнитов
sv_stopspeed 100 -> 10
sv_gravity 800 -> 600 прыжок 45,6 -> 60,8 юнита, как в GMod
sv_airaccelerate 12 -> 10
sv_maxvelocity 350 -> 3500 падение 585/589/590 -> 600/1200/1800/2400/3000
«В воздухе управление слабее»: в AirAccelerate кап sv_air_max_wishspeed
ограничивает ЦЕЛЬ, а прибавка считается по некапленной wishspeed. Мы
ограничивали и то и другое — прибавка была в 8,3 раза слабее. Замер после
правки: рывок вбок даёт ровно 30,0 юнитов/с, как в Source. Поворот при
полёте вперёд по-прежнему заблокирован (проекция 177 >> капа 30) — это
верное поведение оригинала.
«Высота шага терялась единица»: подъём ровно на sv_stepsime ставит низ
габарита точно на грань ступени, грань считается касанием, и подшаг
блокирует сам себя. Замерено: брались уступы до 17 и застревали на 18.
В оригинале подъём на flStepSize + DIST_EPSILON. После правки — все до 18
включительно, 19 нет.
«Невидимые стены, рампа как параллелипипед»: грань кирпича ограничивает
плоскость, а не кирпич, и останавливает шаг где угодно на её уровне.
Замерено: игрок останавливался за 3,4 юнита до настоящей преграды, блокировала
верхняя грань соседнего кирпича. Правка: при t около нуля проверяется, что
точка касания лежит на самом кирпиче. Замер после правки: преград «из
воздуха» 0 (было 2), упоров за 30 с ходьбы 0.
Проверено и отброшено измерением: кисти не работают как ящик (аудит 2126
кирпичей, 0 расхождений из 129684 точек); displacement не выпадает из сетки
(11776 треугольников, выпало 0); скорость вдоль склона 239-263 против 250.
Пара sv_falldistance/fall_speed НЕ введена осознанно: в таблице cvar'ов GMod
её нет, а формула HL2 при отрицательной velocity[2] даёт замедление падения,
а не разгон. Ставить непроверенные константы значило бы гадать.
228 тестов зелёные, clippy -D warnings чист.
Два оставшихся пункта из B6.7, сверенные с оригиналом SDK 2013
(src/game/shared/gamemovement.cpp).
- StartGravity / FinishGravity берут по половине гравитации до и после
движения: «yes, this 0.5 looks wrong, but it's not» — комментарий Valve.
Замер высоты прыжка на gm_construct: 42,7 -> 44,9 юнита при ~45 в Source.
Заодно проверено, что TryPlayerMove гравитацию не применяет вовсе.
- CheckVelocity: sv_maxvelocity = 350 попоосно. Без него шаг за кадр на
большой скорости становится длиннее стены, и отсечение её не видит.
- Место вызова важнее самого ограничения: в оригинале CheckVelocity стоит
прямо перед TryPlayerMove (внутри Friction). Вызов только в конце кадра
позволял одной нечислице уйти в смещение и сделать позицию нечислицей
навсегда — прогон тестов занимал 42 с вместо 0,01. Теперь вызов есть
и перед движением, и в конце.
- Позиция проверяется на нечислицу так же, как скорость.
Тесты: speed_is_capped_at_max_velocity, non_finite_velocity_is_zeroed.
Жалоба: «если игрок выходит за карту, то скайбокс и 3D-скайбокс не должны
рендериться (иначе не понятны границы карты)».
Причина видна в шейдере 3D-скайбокса:
world = camera.pos.xyz + (in.pos - sky_camera) * scale
Геометрия неба размещается относительно камеры, поэтому небо всегда заливает
весь фон — пределы карты на нём не видно.
Замер на gm_construct:
габариты геометрии (-5640, -4576, -960) .. (2168, 6464, 2976)
габариты коллизии (-15360, -15360, -1024) .. (15872, 15360, 15359)
sky_camera (-1428, 1645, 10991), масштаб 1/16
2D-скайбокса нет, только 3D
Габариты коллизии не годятся — включают и 3D-небо, и далёкие террасы,
поэтому основание — габариты геометрии карты.
- world::inside_bounds — одна реализация предиката, её используют и движок,
и рендерер (рендерер копирует габариты при загрузке).
- WorldRenderer::update_sky_visibility пересчитывает видимость каждый кадр:
игрок уходит за край во время игры. Проверка по всем трём осям сразу,
под картой небо тоже исчезает.
- SKY_BOUNDS_MARGIN = 128: габариты считаются по вершинам, а камера стоит
на высоте глаз над опорой, так что стоящий на краю игрок формально за ними.
- Гасятся оба прохода неба: иначе за краем остался бы виден 3D-неб, а это
ровно то, что мешало увидеть границу.
Тесты: sky_is_visible_only_inside_the_map (юнит) и real_map_sky_bounds
(замер на настоящей карте).
Замеры на gm_construct до и после — шесть жалоб из игры закрыты.
Исправлено по измерениям, а не по догадкам:
- vertices_of брал пересечение двух граней (прямую) и выбирал точку вне
кирпича: 1846 кирпичей из 2126 (87%) считались «без объёма», то есть
не твёрдыми. Это корень «сквозь рампу прошёл» и «падаю сквозь карту».
- clip_fraction отбрасывал грань при касании (guard `start <= limit`):
шаг 8 юнитов — стоп у стены, шаг 16 — на 8 внутрь. Guard убран,
условие `!(0.0..1.0).contains(&t)` — верно и для -0.0, и для NaN.
- Полуплоскость кирпича действовала на всю плоскость: игрок зависал
над краем площадки. Добавлен swept-bounds early-out.
- Движение шло одним общим процентом шага, из-за чего игрок «запинкой»
полз по полу. Заменено на цикл касаний TryPlayerMove (numbumps = 4).
- step_move всегда выбирал путь «вверх», а он уводил назад по склону:
игрок сползал с x = -256 до x = -1066 за 5 с. Переписано по оригиналу —
побеждает путь, ушедший дальше по горизонтали, z из движения вниз.
- stay_on_ground падал всем шагом, вминался в кирпич и выталкивался вдоль
нормали, то есть вбок по склону. Теперь зондирует вниз по 1 юниту.
- Трение было только без ввода. FullWalkMove зовёт Friction() безусловно —
именно оно удерживает игрока на рампе.
- Линейная интерполяция ускорения замедляет не хуже, чем ускоряет:
равновесие на 179 юнитах/с. Заменена квото-формула Accelerate;
sv_air_max_wishspeed = 30 действует только в воздухе.
Замер на настоящей карте (real_map_movement, --ignored):
скорость 165 -> 224,9 (среднее с разгона; Source 250)
дёрганье назад 0,00 юнита за 3 с
прыжок 42,7 юнита (Source ~45)
падение сквозь мир и проход сквозь рампу устранены
Старый real_map_player_rests_walks_and_does_not_sink требовал пройти
2000 юнитов вслепую вдоль +x, где на карте стена на x = 992 — ожидание
теста было неверно, а не игрок. Тест удалён, его корректная часть
покрыта real_map_movement.
222 теста зелёные, clippy -D warnings чист.
Главный дефект батча 2 был не в кинематике, а в проверке пересечения AABB с
кирпичом. Симптомы были «поверхность превратилась в лёд» и «выталкивает сквозь
пол и стены».
Глубина проникновения считалась как `dist - project_min(aabb, n)` — насколько
тело зашло за грань с внутренней стороны. Для игрока, стоящего на полу (низ на
1 юнит ниже верха кирпича высотой 100), это даёт 172 юнита проникновения в
нижнюю грань — на 172 юнита выше неё. Тело выталкивало вниз сквозь пол.
Отсюда сразу все жалобы: улетев вниз, игрок терял `on_ground`, трение не
применялось — это и был «лёд».
Заменено на SAT по разделяющим осям: длина перекрытия интервалов проекций.
Для того же игрока — 1 юнит, выход вверх. Чтобы у кирпича был второй конец
интервала, находятся его вершины и хранится проекция противоположного края.
Отдельно: минимальный вектор выхода не годится для тонкой стены — у стены
шириной 16 при теле шириной 32 выход вперёд всегда короче, и тело, дошедшее до
её середины, выталкивалось сквозь (замерено ровно до x = 132.1 при стене на
x = 100). Добавлена swept-трассировка: тело останавливается у грани и никогда
не оказывается по другую сторону.
По `CGameMovement` из Source SDK 2013:
- `StayOnGround()` — функции не было вообще. После ходьбы игрок
притягивается к поверхности; без этого он висит над землёй на спуске.
- порог опоры `sv_walkable_normal` = 0.7, а не 0.6;
- `StepMove` работает только когда игрок был на земле, в прыжке на ступеньку
не взбираются;
- в пути отсечения выставляется опора — без этого игрок замирал намертво на
45° рампе: гашение скорости контактом уравновешивало гравитацию.
Замер на gm_construct (30 секунд ходьбы): пройдено 6372 юнита вместо 1072,
проваливания и замирания нет. 81 тест зелёный, clippy чистый.
Четыре жалобы (проталкивает на расстояние, метания вдоль стены, сползание по
рампам, bhop) оказались одной ошибкой: гравитация применялась КАЖДЫЙ кадр,
в том числе стоя на земле. Игрок вдавливался в геометрию, разрешение
столкновений выталкивало обратно.
Source делает наоборот: при FL_ONGROUND вертикальная скорость
обнуляется, а ApplyGravity в земле не вызывается вовсе.
Что изменено:
- гравитация только в воздухе;
- трение всегда на земле (Source зовёт Friction() каждый кадр) — именно
оно держит игрока на рампе; sv_friction = 4, sv_stopspeed = 100;
- прыжок по фронту нажатия, защёлка держится пока пробел зажат;
- подшаг PM_StepSlideMove: полное движение, при блокировке повтор с
подъёмом на 18 юнитов и спуском; при падении отключён, иначе игрёт в
потолок;
- MoveResult::blocked — признак блокировки для подшага.
Три мои ошибки пойманы тестами:
1. jump_latched = jump — защёлка вставала ДО первого прыжка, прыгать было
нельзя вообще;
2. jump_latched = jump && on_ground — флаг сбрасывался тем же кадром, когда
прыжок уже отработал, bhop возвращался;
3. тест на рамку мерил первоначальное падение (40 -> 6), а не сползание, и
падал с «сносом 34»; после отделения приземления снос ноль.
Также: пропы добавлены в коллизию игрока (Collision::boxes) и в is_on_ground
- иначе игрок стоял на ящике и считал себя в воздухе.
Тестов 73 в engine (было 70), 23 набора по воркспейсу, clippy чист.
Две версии про проваливание проверены тестами и обе оказались неверными.
1. «Пропов нет в коллизии игрока» — пробел реальный и устранён, но игрок
ходил по брашам, это его не объясняло.
2. «На низком FPS шаг подшага прошивает пол» — опровергнуто: при
terminal-скорости 2000 юнитов/с и dt = 1/60 игрок садится на площадку
толщиной 4 юнита, on_ground = true.
Тест оставлен как регрессионный сторож: если подшаги когда-нибудь
ослабнут, он сразу это покажет.
Замер: стояние на спавне gm_construct стабильно 180 кадров (z = -143.9,
on_ground = true). Дефект локальный и требует указания места.
Причина не найдена — фиксирую это в docs/PARITY.md B3, а не подгоняю
тест под желаемый результат.
Главная жалоба: игрок проваливался при хождении и стоянии.
Измерение сузило область: 180 кадров стояния на спавне стабильны
(z = -143.9, on_ground = true, провала нет). Значит дефект локальный.
Причина найдена архитектурно: коллизия игрока содержала ТОЛЬКО браши
карты и треугольники displacement. Пропы живут в мире Rapier, а игрок ходит
своей кодовой базой — то есть встать на ящик означало шагнуть в пустоту.
Это два непересекающихся мира коллизий.
- Collision::boxes + сетка по X/Y: габариты подвижных тел;
- set_boxes() перестраивает сетку целиком каждый кадр — тел десятки, а
пересборка убирает риск «забыть обновить» (источник тихих провалов);
- aabb_penetration() выталкивает по оси наименьшего проникновения, чтобы тело
не застревало в углу;
- клиент каждый кадр берёт габариты из живого мира, вместе с падающими
пропами.
Три теста: игрок стоит на теле, стена из тел не пропускает, и отдельный тест
фиксирует исходное поведение «без габаритов — проваливается», чтобы
регрессия была видна явно.
Тестов 69 в engine, 23 набора по воркспейсу, clippy чист.
Батч 3, шаг 4. Соединения между телами по классам GMod: phys_ballsocket,
phys_hinge, phys_slider, phys_weld, rope. Плюс add_force (база для
thruster/hover) и set_angular_velocity_target (мотор шарнира).
Главная тонкость переноса: скрипт задаёт якорь в мировых координатах, а
Rapier ждёт локальные в каждом теле. Пересчёт через обратное вращение.
Ошибка не бросает исключение — соединение просто «приклеивается» к другой
точке и уезжает при первом движении.
Четыре вывода из измерений, а не из рассуждений:
1. Констрейн не «стягивает» тела, и это правильно: якорь в мировой точке
совпадает у обоих тел в момент создания, ограничение выполнено сразу и
включается, только когда тела расходятся. Пять первых тестов проверяли
расстояние между телами и падали именно поэтому. Правильный инвариант —
совпадение ТОЧЕК КРЕПЛЕНИЯ под действием силы.
2. Сила в Rapier действует один шаг и сбрасывается: add_force надо вызывать
каждый кадр. Тест с однократной силой измерял не силу, а её отсутствие,
и падал одинаково при 8000 и при 40000.
3. Спящее тело честно сообщает нулевую скорость, поэтому эффект силы
проверяется сравнением с контрольным прогоном, а не по скорости.
4. ПОЛЗУН НЕ УДЕРЖИВАЕТ. Сустав сообщает enabled = true и 5 заблокированных
осей из 6, шар/шарнир/сварка в том же прогоне держат, но ползун пропускает
расхождение по высоте. Тест помечен #[ignore] с причиной: зелёная галочка
на неработающем коде хуже явного отказа. Гипотеза к проверке записана в
docs/PARITY.md B2.6.
Тестов 67 в engine (было 56), 23 набора по воркспейсу, clippy чист.
Батч 3, шаг 2. Крейт glua не зависит от engine: поведение отдаётся через
трейт Host, реализация которого живёт в клиенте. Иначе был бы цикл
зависимостей и Lua-слой нельзя было бы тестировать в одиночку.
Мир переведён в Rc<RefCell<World>> — скрипт обращается к нему из хуков в
любой момент. Все обращения через try_borrow: пересечение заимствований
даёт panic, а падать из-за скрипта клиент не должен.
Проверено измерением на gm_construct: ents.GetAll() возвращает 1227
сущностей, ents.GetByClass('prop_physics') читает позицию.
Найден и исправлен рассинхрон: move_prop обновлял позу экземпляра пропа,
но не запись в пуле сущностей. Lua читал z = -64, пока физика уже опустила
проп на z = -84 — «данные» и «картинка» разошлись, и заметить это можно
было только глазами. Теперь поза пишется в оба места всегда, даже без
модели: проп без модели всё равно стоит правильно, он просто не рисуется.
Возвращаемое значение move_prop теперь означает именно «геометрия
пересчитана».
После правки Lua читает z = -84.15848, физика даёт -84.15847778320313 —
совпадение до последнего бита.
Тест на паникующий хост: скрипт получает «невалидно» вместо падения.
Тестов 24 в glua, 56 в engine, 23 набора по воркспейсу, clippy чист.
Батч 2. Мир не переезжает в Rapier целиком: геометрия карты остаётся
брашевой, Rapier обслуживает только динамические тела.
Четыре дефекта найдены измерением на gm_construct, а не догадкой — каждый
следующий был невидим, пока не закрыт предыдущий:
1. Рельеф раздувался в 15 277 коллайдеров (сетка LAND_CELL дублировала
каждый треугольник в каждую ячейку). Заменено одним объединённым
trimesh с дедупликацией вершин: 11 776 треугольников -> 6 148 вершин,
1 коллайдер. Точнее и на порядки дешевле.
2. Уровень gm_construct построен брашами, а не displacement'ами: без них
все пропы падали насквозь. add_brushes восстанавливает вершины
перебором троек плоскостей (Крамер). Замер: 2121 построен, 5 пропущено.
3. Broad-phase Rapier наполняется только на step(): после вставки статики
луч её не видел. Лечится явным broad_phase.set_aabb().
4. setup_physics начинался с PhysicsWorld::new() и стирал построенные браши.
Тест поймал баг, который замер бы не увидел: add_brushes создавал
коллайдеры, но не увеличивал счётчик, и лог врал (static = 1 при 2121 браше).
Проверка двумя независимыми системами коллизий: и Rapier, и брашевая
Collision находят пол под пропами на z = -96. После 400 кадров все 4
prop_physics осели: speed = 0.0, sleeping = true.
Ограничения зафиксированы в docs/PARITY.md: коллайдер пропа — AABB модели,
а не vcollide; физика только у prop_physics*; FPS просел до 10-21 и требует
отдельного замера.
Тестов 55 в engine (было 42), 23 набора по воркспейсу, clippy чист.
Шаг 1 батча пропов. Раньше проп был запечён в буфер мира навсегда:
физика могла двигать сущность, а на экране ничего не менялось.
- PropMesh хранит позиции и нормали моделей в локальных координатах;
- PropInstance ссылается на модель по индексу, а не по имени;
- World::move_prop перезаписывает вершины экземпляра на месте из
локальных координат. Бати и материалы остаются валидными, пересборка
мира не нужна;
- update_prop_bounds расширяет AABB мира, иначе уехавший проп отсечётся
при отрисовке.
Тесты поймали две собственные ошибки в тестах: локальная позиция (1,2,3)
плюс origin даёт (101,2,3), а поворот задаётся в ГРАДУСАХ — с pi в
rad вместо 90 градусов поворот выходил 1.57 вместо 90. Заодно уточнено,
что roll вращает вокруг оси «вперёд» и её не меняет, поэтому для проверки
поворота нормали нужен yaw.
Карта грузится и рисуется без регрессии: 9 экземпляров, 43212 вершин.
Раньше пропы порождались напрямую из сырого лумпа ENTITIES, минуя
EntityWorld. Из-за этого физика и Lua видели бы одну картину мира, а
рендер — другую.
Теперь build_world берёт пропы из World::entities по индексу и для
каждого заводит PropInstance: ссылка на сущность, класс, модель,
трансформ и диапазон вершин в буфере. Это то, что нужно физике, чтобы
двигать проп, и Lua, чтобы получить ссылку через Entity().
На gm_construct: 9 экземпляров, 5 уникальных моделей. Добавлен тест,
что ссылка экземпляра протухает вместе с сущностью, а не указывает на
переиспользованный слот.
Фундамент для пропов, физики и Lua: пока сущности не было, пропы
запекались прямо в геометрию мира при загрузке, и их нельзя было ни
толкнуть, ни выбрать, ни отдать в Lua.
- EntityIndex со слотом и поколением: удалённая сущность распознаётся
как умершая, даже если слот уже переиспользован (тот же приём, что
EHANDLE в Source);
- EntityWorld: пул со свободными слотами, spawn/remove, поиск по
классу, по префиксу и в сфере (основа ents.FindInSphere);
- Entity: classname, ключ-значения, origin/angles, movetype из карты,
флаги solid/render/trigger, модель (браш-модели с '*' отбрасываются);
- лимиты на всё, что приходит из файла карты (правило #1 AGENTS.md):
2048 сущностей, 128 пар, 256 байт на ключ и значение.
build_world теперь спавнит все сущности карты в единый пул: на
gm_construct это 1227 сущностей (9 пропов, 13 функций). Источники
света тоже берутся из того же списка.
9 новых тестов, включая протухание ссылки после переиспользования
слота и устойчивость к мусору в origin.
Бег: поле Player::sprint, скорость 250 -> 520 (+speed в Source).
Ускоряет и боковое движение, как в Source.
Импульс: раньше переключение noclip обнуляло скорость, и выход из
полёта выглядел как мгновенная остановка. Теперь fly() пишет скорость
в velocity, а set_noclip() её не трогает (вертикальную обнуляет, её
задаёт гравитация). Вход в noclip на бегу продолжает движение.
Тест sprint_respects_max_speed сначала падал на 2066 — оказалось, игрок
убегает за край пола и падает с vz=-2000. +maxspeed в Source
ограничивает горизонтальную составляющую, поэтому тест меряет её.
collect_lights() разбирает light_environment (солнце), light, light_spot.
На gm_construct: 65 источников, 1 солнце, 22 прожектора. Шейдер получил
uniform-блок с солнцем, точечными источниками и конусами прожекторов.
Ключевые находки при подборе нормировки (проверено рендерами):
- четвертая компонента _light — это РАДИУС у ламп и ЯРКОСТЬ у солнца;
- яркость нельзя домножать в цвет, иначе возводится в квадрат;
- лайтмапа уже содержит запеченное солнце, динамическое добавляется слабо.
Также добавлен разбор GAME_LUMP в formats (нужен для деталь-текстур) и
заодно выяснено: в gm_construct деталь-текстур нет, под-лумпы prps/prpd —
это геометрия пропов. Текстура GRASS-SAND_13 есть, но не используется ни
одним texinfo — вопрос песка остается открытым (docs/PARITY.md A5).
A4 из docs/PARITY.md. Раньше cull_mode был None во всех пайплайнах, и
изнанка брашей рисовалась поверх настоящих поверхностей (на gm_construct
трава была видна сквозь бетонную плиту).
Намотку нельзя задать «как в карте»: в данных она непоследовательна.
Замер на gm_construct показал, что у обычных граней и у displacement'ов
противоположный порядок обхода, а нормаль плоскости у них трактуется
с разным знаком. Поэтому:
- face_normal: нормаль плоскости смотрит внутрь тела, лицевая сторона —
обратная (side == 0 => -normal);
- face_winding: геометрическая нормаль каждой грани приводится к нормали
грани, треугольник разворачивается при несовпадении;
- build_displacement: то же для рельефа, причём «наружу» — это нормаль
БАЗОВОЙ плоскости (на склонах геометрическая нормаль наклонная).
После этого инвариант «геометрическая нормаль = нормаль освещения» держится
везде, и правильная фронт-грань одна: Ccw в NDC, то есть Cw в wgpu/Vulkan
(Source использует левую систему координат).
Проверено рендерами на gm_construct: рельеф виден сверху целиком, плита и
трава вместе, трава корректно перекрыта плитой. Диагностические флаги
SOURCE_RS_NO_CULL и SOURCE_RS_FRONT_FACE=ccw оставлены для сверки.
Два дефекта коллизии из docs/PARITY.md A1 и A2.
A1. move_aabb возвращал только (Aabb, on_ground) и не трогал скорость:
игрок каждый кадр двигался в стену, получал выталкивание и визуально
«нырял» в неё (симптом: подойти в стену, отпустить клавиши — входит
внутрь и выталкивает). Теперь возвращается MoveResult с нормалями
контакта, а Player::update гасит компоненту скорости внутрь каждой
нормали — как ClipVelocity в Source. Скольжение вдоль стены сохраняется.
A2. Твёрдость браша определялась сравнением contents == 0x40000000,
то есть твёрдым считалось ВСЁ остальное, включая contents == 0 и воду.
По спецификации твёрдость задаёт бит CONTENTS_SOLID (0x1). На
gm_construct это давало 77 лишних стен из 2203 брашей: невидимые стены
в проходах и твёрдая вода озера.
Проверено на реальной карте: total=2203 solid=2126 non_solid=77 liquid=1 —
совпадает с независимым замером по bsp. Добавлено 6 тестов, включая
регрессию на «ныряние» и на отбор не-твёрдых брашей.
- Вода: детект Water-шейдера (/), отдельный полупрозрачный
проход с френелем; поверхность/изнанка по нормали. Белая дыра на месте воды
(2D-скайбокс «вниз») устранена.
- 3D-скайбокс: выбор граней по area листа sky_camera (движок: area bits), а не по
AABB. Disp-грани не попадают в leaf_faces — выбор по листу центра грани; теперь
в скайбоксе есть большая плоскость flatgrass_2 (пропадала — торчала «стена»
бетонного пола). Защита >30% граней = скайбокс не выделяем (gm_flatgrass).
- Порядок: 2D-скайбокс фоном -> 3D-меш (depth Always, без записи) -> мир ->
вода. Скайбокс больше не проступает сквозь дальние стены карты.
- Лайтмапы: блок грани (size+1)^2 с индексом v*width+u; для displacement'ов —
плотная сетка по UV поверхности (CalcDispSurfCoords), а не проекция мира:
убрана «шахматка» теней на рельефе. Скайбокс-меш тоже сэмплит лайтмапы.
- Тесты: gm_flatgrass-диспы живут в скайбоксе (z≈-16000), пороги обновлены.
- docs/RECON.md §12: форматы листьев/граней, лайтмапы, скайбокс, вода.
- engine::model: сборка мешей MDL+VVD+VTX (bind pose, LOD 0), материалы через
cd-каталоги модели, поворот углами Source
- build_world: экземпляры prop_* моделей в общий меш мира (9 инстансов на
gm_construct), кэш моделей и текстур
- 3D-скайбокс: выделение скайбокс-граней (AABB вокруг sky_camera, <30% карты),
сжатие 1/scale вокруг камеры, отдельный пайплайн с глубиной у дальней
плоскости; 2D-скайбокс рисуется только в незакрытых пикселях
- тесты: sky_mesh на gm_construct, пороги дисплейсментов обновлены
- AABB игрока: ступни на position.z (было — центр), фикс проекций на нормали
(учёт знака компонент) — игрок больше не проваливается сквозь пол
- коллизия displacement'ов: SAT AABB-треугольник (13 осей, минимальный сдвиг
для тонких поверхностей) + пространственная сетка 128 юнитов
- ориентация сетки displacement'ов: стартовый угол по startPosition, далее по
порядку обхода грани (VBSP AdjustSurfPointData); индексы a*side+b; проверено
на gm_construct — 332 совпадающих ребра соседних displacement'ов
- нормали displacement'ов наружу (порядок обхода как у граней BSP)
- тесты игрока: падение на пол, стояние на треугольнике, направления стрейфа
- engine::player: Collision (BRUSHES/BRUSHSIDES, твёрдые содержимые), AABB-разрешение
проникновений с подшагами, Player (800 грав., 268 прыжок, 250 ходьба, присед)
- map: спавн на info_player_start, режим ходьбы по умолчанию, V — noclip,
камера на уровне глаз, обзор мышью
- gm_construct: 2129 брашей коллизии, полёт/ходьба без падений
- мышь: движение вправо уменьшает yaw (Source: +Y — влево)
- displacement: (0,0) сетки — угол, ближайший к startPosition; оси по текстурным осям
- далее: игрок (коллизия с BSP-брашами, гравитация, ходьба), вода, модели