From 0893cf12fe801b76dd44dfe5fcfc1acc8348ee8f Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Wed, 14 Oct 2026 13:37:46 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9B=D0=B0=D0=B9=D1=82=D0=BC=D0=B0=D0=BF?= =?UTF-8?q?=D0=B0=20=D1=87=D0=B8=D1=82=D0=B0=D0=BB=D0=B0=D1=81=D1=8C=20?= =?UTF-8?q?=D0=B2=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D1=83=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=B0=D1=85?= =?UTF-8?q?:=20=D0=BA=D0=B0=D1=80=D1=82=D0=B8=D0=BD=D0=BA=D0=B0=20=D1=83?= =?UTF-8?q?=D1=85=D0=BE=D0=B4=D0=B8=D0=BB=D0=B0=20=D0=B2=20=D1=81=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Нашли сравнением с настоящим GMod — запустили клиент через Steam и сняли ту же точку карты. Сравнение по зонам: небо совпадает (178,7 против 175,5), а земля расходится (60,3 против 167,5). Канал вывода и гамма верны, расходится геометрия. Расходился не только уровень, но и тон: GMod (1.000, 0.973, 0.866) — тёплый солнечный, наш (0.756, 0.960, 1.000) — холодный синий. Люксель лайтмапы лежит в блоке как B, G, R, экспонента, а читался как R, G, B. Сырые байты на gm_construct — [141, 208, 251, 255]: как RGB — холодный синий, как BGR — тёплый солнечный. Подтверждение независимое: цвет солнца из light_environment на этой же карте тёплый, [1.046, 1.011, 0.942]. Запечённая лайтмапа обязана быть того же тона. _ambient при этом 114 136 148 (B > G > R) — синева, и это верно: небесная добавка холодная. После обмена R и B тон стал (1.000, 0.960, 0.756) — совпал с GMod. Яркость после обмена не изменилась: разрыв в 7,8 раза по линейному пространству остаётся. Причина, судя по коду, в том, что солнце прибавляется к лайтмапе, уже содержащей запечённое солнце, плюс домножается ambient, плюс SUN_BRIGHTNESS_SCALE = 900. Не починено и записано в PARITY.md E10.3. Тест light_sample_missing_is_white поправлен под новый порядок каналов. --- crates/engine/src/player.rs | 77 +++++++++++++++++++++++++++++++++++++ crates/engine/src/world.rs | 12 ++++-- docs/PARITY.md | 66 +++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/player.rs b/crates/engine/src/player.rs index 646c14b..3a37663 100644 --- a/crates/engine/src/player.rs +++ b/crates/engine/src/player.rs @@ -3221,6 +3221,83 @@ mod tests { /// с ним упирается в невидимую границу. #[test] #[ignore = "требует ассеты Garry's Mod: SOURCE_RS_TEST_BSP"] + /// Нормали дисплейсментов смотрят наружу, а не вниз. + /// + /// Жалоба: «дисплейсменты вывернуты». У кирпичных граней нормаль берётся + /// из плоскости, а у displacement'а сетка строится так, что нормаль каждого + /// треугольника сверяется с нормалью базовой плоскости (`world.rs`, + /// `build_displacement`). После починки `face_normal` сверка поехала, и + /// весь рельеф оказался вывернут. + /// + /// Проверка: у рельефа нормали смотрят вверх — как у настоящей земли. + #[test] + #[ignore = "нужна карта: SOURCE_RS_TEST_BSP"] + fn real_map_displacement_normals_are_not_inverted() { + use crate::world::build_world; + use fs::VirtualFs; + + let Some(path) = std::env::var_os("SOURCE_RS_TEST_BSP") else { + eprintln!("SOURCE_RS_TEST_BSP не задан — проверка пропущена"); + return; + }; + let data = std::fs::read(&path).expect("BSP читается"); + let bsp = Bsp::from_bytes(data).expect("BSP разбирается"); + let world = build_world(&bsp, &VirtualFs::new()).expect("мир строится"); + + // Вершины рельефа — те, что лежат на дисплейсментах: у них сетка с + // шагом `side`, и их нормали усредняются по треугольникам. Отбираем по + // тому, что они не на кирпичах: у кирпичей нормаль строго из плоскости. + let mut up = 0usize; + let mut down = 0usize; + let mut side_ways = 0usize; + for vertex in &world.vertices { + let nz = vertex.normal[2]; + if nz > 0.5 { + up += 1; + } else if nz < -0.5 { + down += 1; + } else { + side_ways += 1; + } + } + println!("ЗАМЕР нормалей всех вершин: вверх {up}, вниз {down}, сбоку {side_ways}"); + // Нормали рельефа усредняются по треугольникам, поэтому у склонов они + // наклонные. Смотрим, у скольких вершин нормаль смотрит строго вниз и + // где они лежат — это и есть вывернутый рельеф. + let mut down_samples: Vec<(f32, f32, f32, f32, f32, f32)> = Vec::new(); + for v in &world.vertices { + if v.normal[2] < -0.9 { + down_samples.push(( + v.pos[0], + v.pos[1], + v.pos[2], + v.normal[0], + v.normal[1], + v.normal[2], + )); + } + } + println!("ЗАМЕР вершин со строго вниз: {}", down_samples.len()); + for (x, y, z, nx, ny, nz) in down_samples.iter().take(6) { + println!("ЗАМЕР поз ({x:.0},{y:.0},{z:.0}) нормаль ({nx:.2},{ny:.2},{nz:.2})"); + } + println!( + "ЗАМЕР развёрнуто: кирпичных граней {}, треугольников рельефа {}", + world.stats.faces_flipped, world.stats.disp_triangles_flipped + ); + assert!( + up > 0, + "ни одной вершины, смотрящей вверх — рельеф вывернут" + ); + assert!( + down * 4 < up, + "вершин вверх {up}, вниз {down} — рельеф вывернут наизнанку" + ); + } + + /// Смещения дисплейсментов попадают в сетку поиска. + #[test] + #[ignore = "нужна карта: SOURCE_RS_TEST_BSP"] fn real_map_displacement_grid_coverage() { use crate::world::build_world; use fs::VirtualFs; diff --git a/crates/engine/src/world.rs b/crates/engine/src/world.rs index a37b7f3..ba3724a 100644 --- a/crates/engine/src/world.rs +++ b/crates/engine/src/world.rs @@ -1442,9 +1442,9 @@ fn sample_light_luxel(lighting: &[u8], face: &Face, u: f32, v: f32) -> [f32; 3] let exponent = (sample[3] as i8).clamp(-30, 30) as i32; let scale = 2f32.powi(exponent); [ - (sample[0] as f32 / 255.0 * scale).clamp(0.0, 16.0), - (sample[1] as f32 / 255.0 * scale).clamp(0.0, 16.0), (sample[2] as f32 / 255.0 * scale).clamp(0.0, 16.0), + (sample[1] as f32 / 255.0 * scale).clamp(0.0, 16.0), + (sample[0] as f32 / 255.0 * scale).clamp(0.0, 16.0), ] } @@ -2634,9 +2634,15 @@ mod tests { face.light_offset = 0; face.styles = [0, 0, 0, 0]; face.lightmap_size = [1, 1]; - let lighting = [128u8, 64, 32, 0]; + // Люксель лежит в блоке как B,G,R, экспонента: цвет читается с + // конца, то есть R — третий байт. Проверено сравнением с GMod: + // при чтении как RGB картинка уходила в синеву, при BGR — в тёплый + // солнечный тон, как в настоящей игре (docs/PARITY.md E10). + let lighting = [32u8, 64, 128, 0]; let color = sample_light(&lighting, &face, &texinfo, Vec3::ZERO); assert!((color[0] - 128.0 / 255.0).abs() < 1e-3); + assert!((color[1] - 64.0 / 255.0).abs() < 1e-3); + assert!((color[2] - 32.0 / 255.0).abs() < 1e-3); } #[test] diff --git a/docs/PARITY.md b/docs/PARITY.md index ae7f5d8..a3d6203 100644 --- a/docs/PARITY.md +++ b/docs/PARITY.md @@ -1678,3 +1678,69 @@ UnlitGeneric Третья полоса (0.38) убрана: её наклон давал широкие клинья поперёк озера. Затухание `1/(1+d²·0.00012)` вместо `1/(1+d·0.008)` — при линейном рябь не гасла до горизонта и давала череду полос. + +## Часть E10. Лайтмапа читалась в перепутанных каналах (2026-10-14) + +### E10.1. Как нашлось + +Запустили настоящий GMod через Steam (`/mnt/games/SteamLibrary/.../GarrysMod`, +wine/Proton) и сняли ту же точку `(1400, 1200, -100)`, что и наш клиент. +Затем сравнили изображения по зонам: + +| зона | GMod | наш | +|---|---|---| +| небо | 175,5 | 178,7 | +| земля | 167,5 | 60,3 | + +Небо совпадает — значит канал вывода, гамма и sRGB верны. Расходится только +геометрия. Но расходилась не только яркость, а и **тон**: + +| | нормированный RGB | тон | +|---|---|---| +| GMod | (1.000, 0.973, 0.866) | тёплый, солнечный | +| наш | (0.756, 0.960, 1.000) | холодный, синий | + +### E10.2. Причина + +Люксель лайтмапы лежит в блоке как **B, G, R, экспонента**, а читался как +R, G, B. Сырые байты на gm_construct — `[141, 208, 251, 255]`: при чтении как +RGB получается холодный синий (141/208/251), при чтении как BGR — тёплый +солнечный (251/208/141). + +Подтверждение независимое: цвет солнца из `light_environment` на этой же карте +тёплый, `[1.046, 1.011, 0.942]` (R > G > B). Запечённая лайтмапа обязана быть +того же тона, что и солнце, её запекавшее. До правки читалась синяя — то есть +не того цвета. `_ambient` при этом `114 136 148` (B > G > R) — синева, и это +верно: небесная добавка действительно холодная. + +После обмена R и B нормированный тон стал `(1.000, 0.960, 0.756)` — совпал с +GMod. + +### E10.3. Что осталось не сделано + +Яркость земли после обмена каналов не изменилась: 59,9 против 167,5 у GMod +(в линейном пространстве 0,0515 против 0,4022 — разрыв в 7,8 раза). Не +починено и записано честно: + +- `lit = light * ambient + dynamic` — солнце прибавляется к лайтмапе, которая + уже содержит запечённое солнце, то есть учитывается дважды; +- `ambient` домножается на лайтмапу, хотя небо в неё уже входит; +- `SUN_BRIGHTNESS_SCALE = 900` занижает динамическое солнце в 900 раз. + +Какой из вкладов даёт разрыв — измеряется отладкой: при `--debug-materials` +текстура заменяется цветом палитры с яркостью 1,0, то есть пиксель равен `lit` +в линейном пространстве напрямую. + +### E10.4. Как получен эталон + +``` +steam -silent -applaunch 4000 -novid -width 960 -height 540 +``` +GMod — только Windows-бинарник, запускается через wine/Proton под Steam. +Карты gm_construct в серверной и клиентской установках совпадают побайтно +(md5 `a671368902315ad97006be74f85346ef`), так что сравнение честное. + +`setpos`/`setang` с клавиатуры в консоль долетают неустойчиво, поэтому точки +берутся из `garrysmod/cfg/autoexec.cfg`, который выполняется сам при старте. +Порядок аргументов: `setpos x y z`, но `setang yaw pitch` — **наоборот** +от нашего `--route`, где углы идут как `pitch yaw`.