Лайтмапа читалась в перепутанных каналах: картинка уходила в синеву
Нашли сравнением с настоящим 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:
parent
e91a60d40d
commit
0893cf12fe
3 changed files with 152 additions and 3 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue