Лайтмапа читалась в перепутанных каналах: картинка уходила в синеву

Нашли сравнением с настоящим 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 поправлен под новый порядок каналов.
This commit is contained in:
Niko Marmeladkov 2026-10-14 13:37:46 +03:00
parent e91a60d40d
commit 0893cf12fe
3 changed files with 152 additions and 3 deletions

View file

@ -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;

View file

@ -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]

View file

@ -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`.