Замеры по «белым плитам» и «вывернутой траве»: три гипотезы опровергнуты
Гипотеза Gemini (нормаль из dv.vec) не подтвердилась: сырые байты LUMP_DISP_VERTS читаются как f32 с шагом 20 и дают осмысленные значения (vec=(0,0,1), dist=18.145). Формат разобран верно. Ориентация рельефа в порядке: 110 плоскостей вверх, 0 вниз, намотка не перевёрнута (0 из 11776). У спавна рельеф плоский — z от -151.4 до -148.0, поэтому «наклонены 104 вершины» это свойство карты, а не баг. Найдена ошибка в моей же сверке: справочник лампов читался со stride 48 вместо 16. Из-за этого LUMP_NODES выглядел навмешем, а лампы 6 и 18 менялись местами. Выводы части E14 про LUMP_NODES недействительны. «Белые плиты» разобраны: у материала color/white всего 9 граней, и это не ящик, а тонкие пластины 64x4 и 128x4 с нормалями ±X/±Y на четырёх высотах — то, что на глаз читается как «оси координат 3д». Большая белая поверхность в центре — другая: бетонная плита 2480x1936 над спавном, продублированная (9379==9385, 9380==9386), дубль есть в самой карте. Чёрные области проверены экспериментом: отключение тенев и отсечения задних граней меняет картину, значит чёрное приходит из лайтмапы, а не из теней. Подробности и следующие шаги в PARITY.md E15.
This commit is contained in:
parent
5f1fdcf5e2
commit
6fd44ba014
2 changed files with 254 additions and 135 deletions
|
|
@ -3789,8 +3789,54 @@ mod tests {
|
|||
}
|
||||
}
|
||||
println!("ЗАМЕР габарит рельефа: {lo:?} .. {hi:?}");
|
||||
// Точки самого рельефа: треугольники displacement'а после сборки мира.
|
||||
// Высота рельефа рядом со спавном (704, 132) — там игрок стоит,
|
||||
// и если он оказывается ПОД поверхностью, рельеф «вывернут».
|
||||
let world = crate::world::build_world(&bsp, &fs::VirtualFs::new()).expect("мир строится");
|
||||
let spawn = (704.0f32, 132.0f32);
|
||||
let mut near: Vec<(f32, f32)> = Vec::new();
|
||||
for tri in &world.disp_tris {
|
||||
let center = (tri[0] + tri[1] + tri[2]) / 3.0;
|
||||
let d = ((center.x - spawn.0).powi(2) + (center.y - spawn.1).powi(2)).sqrt();
|
||||
if d < 600.0 {
|
||||
near.push((center.z, d));
|
||||
}
|
||||
}
|
||||
near.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
if !near.is_empty() {
|
||||
let lo_z = near.iter().map(|n| n.0).fold(f32::MAX, f32::min);
|
||||
let hi_z = near.iter().map(|n| n.0).fold(f32::MIN, f32::max);
|
||||
println!(
|
||||
"ЗАМЕР рельеф в радиусе 600 от спавна: z от {lo_z} до {hi_z}, треугольников {}",
|
||||
near.len()
|
||||
);
|
||||
} else {
|
||||
println!("ЗАМЕР у спавна треугольников рельефа нет");
|
||||
}
|
||||
for (z, d) in near.iter().take(4) {
|
||||
println!("ЗАМЕР ближайший треугольник: z={z}, расстояние {d}");
|
||||
}
|
||||
// Базовая плоскость и смещение displacement'ов: если dist положителен,
|
||||
// поверхность поднимается над базовой плоскостью.
|
||||
let mut base_z: Vec<f32> = Vec::new();
|
||||
for face in &geometry.faces {
|
||||
if face.dispinfo < 0 {
|
||||
continue;
|
||||
}
|
||||
let Some(plane) = geometry.planes.get(face.planenum as usize) else {
|
||||
continue;
|
||||
};
|
||||
if plane.normal[2].abs() > 0.5 {
|
||||
base_z.push(plane.dist);
|
||||
}
|
||||
}
|
||||
base_z.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
println!(
|
||||
"ЗАМЕР базовые плоскости рельефа (z): {} штук, от {:?} до {:?}",
|
||||
base_z.len(),
|
||||
base_z.first(),
|
||||
base_z.last()
|
||||
);
|
||||
// Точки самого рельефа: треугольники displacement'а после сборки мира.
|
||||
let mut spots: Vec<(f32, f32, f32)> = Vec::new();
|
||||
for tri in &world.disp_tris {
|
||||
let center = (tri[0] + tri[1] + tri[2]) / 3.0;
|
||||
|
|
@ -3837,151 +3883,120 @@ mod tests {
|
|||
let world = build_world(&bsp, &VirtualFs::new()).expect("мир строится");
|
||||
let geometry = bsp.geometry().expect("геометрия");
|
||||
|
||||
// Ищем грани, чей материал — белая текстура color/white.
|
||||
let white: std::collections::HashSet<usize> = world
|
||||
.materials
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, m)| m.texture.name == "color/white" || m.name.contains("COLOR_ROOM"))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
println!("ЗАМЕР материалов с белой текстурой: {}", white.len());
|
||||
|
||||
let mut with_nodraw = 0usize;
|
||||
let mut without_nodraw = 0usize;
|
||||
let mut shown = 0usize;
|
||||
for face in &geometry.faces {
|
||||
let Some(texinfo) = geometry.texinfos.get(face.texinfo as usize) else {
|
||||
continue;
|
||||
};
|
||||
let Some(mat) = world.materials.get(texinfo.texdata as usize) else {
|
||||
continue;
|
||||
};
|
||||
if !white.contains(&mat_index(&world, mat)) {
|
||||
continue;
|
||||
}
|
||||
let nodraw = texinfo.flags & crate::world::SURF_NODRAW != 0;
|
||||
if nodraw {
|
||||
with_nodraw += 1;
|
||||
} else {
|
||||
without_nodraw += 1;
|
||||
}
|
||||
if shown < 8 {
|
||||
println!(
|
||||
"ЗАМЕР грань #{}: surf flags {:#x}, NODRAW {}, стили {:?}, коллизия-да",
|
||||
shown, texinfo.flags, nodraw, face.styles
|
||||
);
|
||||
shown += 1;
|
||||
// Считаем, сколько граней принадлежит кирпичам с содержимым
|
||||
// `CONTENTS_EMPTY`: в Source такие кирпичи не рисуются.
|
||||
let brush_data = bsp
|
||||
.lump(formats::bsp::lump::BRUSHES)
|
||||
.expect("кирпичи")
|
||||
.into_owned();
|
||||
let side_data = bsp
|
||||
.lump(formats::bsp::lump::BRUSHSIDES)
|
||||
.expect("стороны")
|
||||
.into_owned();
|
||||
// Плоскость → номера кирпичей: искать все подряд нельзя, их 2203.
|
||||
let mut plane_brushes: std::collections::HashMap<usize, Vec<usize>> = Default::default();
|
||||
let mut contents_of: Vec<i32> = Vec::new();
|
||||
for (bi, chunk) in brush_data.chunks_exact(12).enumerate() {
|
||||
let first = i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) as usize;
|
||||
let count = i32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]) as usize;
|
||||
let contents = i32::from_le_bytes([chunk[8], chunk[9], chunk[10], chunk[11]]);
|
||||
contents_of.push(contents);
|
||||
for s in first..first + count {
|
||||
let Some(side) = side_data.get(s * 8..s * 8 + 2) else {
|
||||
break;
|
||||
};
|
||||
plane_brushes
|
||||
.entry(u16::from_le_bytes([side[0], side[1]]) as usize)
|
||||
.or_default()
|
||||
.push(bi);
|
||||
}
|
||||
}
|
||||
println!("ЗАМЕР граней с этим материалом: с NODRAW {with_nodraw}, без {without_nodraw}");
|
||||
|
||||
// Чья это сущность: у brush-сущностей есть ключи `brushes`/`brushlist`.
|
||||
// Смотрим, какие классы вообще есть на карте и сколько у них кирпичей.
|
||||
let Some(ent) = bsp.entities().ok() else {
|
||||
eprintln!("сущности не читаются");
|
||||
return;
|
||||
let hull = |bi: usize| -> Vec<(Vec3, f32)> {
|
||||
let first = i32::from_le_bytes([
|
||||
brush_data[bi * 12],
|
||||
brush_data[bi * 12 + 1],
|
||||
brush_data[bi * 12 + 2],
|
||||
brush_data[bi * 12 + 3],
|
||||
]) as usize;
|
||||
let count = i32::from_le_bytes([
|
||||
brush_data[bi * 12 + 4],
|
||||
brush_data[bi * 12 + 5],
|
||||
brush_data[bi * 12 + 6],
|
||||
brush_data[bi * 12 + 7],
|
||||
]) as usize;
|
||||
let mut planes = Vec::new();
|
||||
for s in first..first + count {
|
||||
let Some(side) = side_data.get(s * 8..s * 8 + 2) else {
|
||||
break;
|
||||
};
|
||||
let planenum = u16::from_le_bytes([side[0], side[1]]) as usize;
|
||||
if let Some(p) = geometry.planes.get(planenum) {
|
||||
planes.push((Vec3::new(p.normal[0], p.normal[1], p.normal[2]), p.dist));
|
||||
}
|
||||
}
|
||||
planes
|
||||
};
|
||||
let mut owners: std::collections::HashMap<String, (usize, usize)> =
|
||||
std::collections::HashMap::new();
|
||||
for kv in ent {
|
||||
let class = kv.get_str("classname").unwrap_or("?").to_string();
|
||||
let entry = owners.entry(class).or_insert((0, 0));
|
||||
entry.0 += 1;
|
||||
if kv.get_str("brushes").is_some() || kv.get_str("brushlist").is_some() {
|
||||
entry.1 += 1;
|
||||
}
|
||||
}
|
||||
// Кирпич, которому принадлежит белая грань: ищем по совпадению плоскостей.
|
||||
{
|
||||
let brush_data = bsp
|
||||
.lump(formats::bsp::lump::BRUSHES)
|
||||
.expect("кирпичи")
|
||||
.into_owned();
|
||||
let side_data = bsp
|
||||
.lump(formats::bsp::lump::BRUSHSIDES)
|
||||
.expect("стороны")
|
||||
.into_owned();
|
||||
let mut shown = 0;
|
||||
'outer: for face in &geometry.faces {
|
||||
let Some(texinfo) = geometry.texinfos.get(face.texinfo.max(0) as usize) else {
|
||||
continue;
|
||||
};
|
||||
let Some(mat) = world.materials.get(texinfo.texdata as usize) else {
|
||||
continue;
|
||||
};
|
||||
if mat.texture.name != "color/white" {
|
||||
let mut by_contents: std::collections::BTreeMap<i32, usize> = Default::default();
|
||||
let mut empty_faces: Vec<usize> = Vec::new();
|
||||
let mut no_brush = 0usize;
|
||||
for (index, face) in geometry.faces.iter().enumerate() {
|
||||
let Some(candidates) = plane_brushes.get(&(face.planenum as usize)) else {
|
||||
no_brush += 1;
|
||||
continue;
|
||||
};
|
||||
let Ok(indices) = geometry.face_vertex_indices(index) else {
|
||||
continue;
|
||||
};
|
||||
let mut best: Option<usize> = None;
|
||||
for &bi in candidates {
|
||||
let planes = hull(bi);
|
||||
if planes.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
for (bi, chunk) in brush_data.chunks_exact(12).enumerate() {
|
||||
let first = i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||||
let count = i32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]);
|
||||
let contents = i32::from_le_bytes([chunk[8], chunk[9], chunk[10], chunk[11]]);
|
||||
if first < 0 || count <= 0 {
|
||||
continue;
|
||||
}
|
||||
for s in first as usize..(first + count) as usize {
|
||||
let Some(side) = side_data.get(s * 8..s * 8 + 2) else {
|
||||
break;
|
||||
};
|
||||
if u16::from_le_bytes([side[0], side[1]]) as usize != face.planenum as usize
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if shown < 12 {
|
||||
println!(
|
||||
"ЗАМЕР белая грань: кирпич #{bi}, содержимое {contents:#x}, сторон {count}"
|
||||
);
|
||||
shown += 1;
|
||||
}
|
||||
continue 'outer;
|
||||
let inside = indices.iter().all(|v| {
|
||||
geometry.vertices.get(*v as usize).is_some_and(|v| {
|
||||
let p = Vec3::new(v.pos[0], v.pos[1], v.pos[2]);
|
||||
planes.iter().all(|(n, d)| n.dot(p) <= *d + 0.25)
|
||||
})
|
||||
});
|
||||
if inside {
|
||||
best = Some(bi);
|
||||
break;
|
||||
}
|
||||
}
|
||||
match best {
|
||||
Some(bi) => {
|
||||
let contents = contents_of[bi];
|
||||
*by_contents.entry(contents).or_default() += 1;
|
||||
if contents == 0 {
|
||||
empty_faces.push(index);
|
||||
}
|
||||
}
|
||||
None => *by_contents.entry(-1).or_default() += 1,
|
||||
}
|
||||
}
|
||||
let mut top: Vec<(&String, &(usize, usize))> = owners.iter().collect();
|
||||
top.sort_by_key(|(_, (n, _))| std::cmp::Reverse(*n));
|
||||
println!("ЗАМЕР сущности карты (всего, из них с кирпичами):");
|
||||
// Параллельны ли кирпичи и грани: если да, по диапазону кирпичей
|
||||
// сущности можно точно назвать её грани.
|
||||
{
|
||||
let brush_data = bsp.lump(formats::bsp::lump::BRUSHES).expect("кирпичи");
|
||||
let side_data = bsp
|
||||
.lump(formats::bsp::lump::BRUSHSIDES)
|
||||
.expect("стороны кирпичей");
|
||||
let mut brushes = 0usize;
|
||||
let mut sides = 0usize;
|
||||
for chunk in brush_data.as_ref().chunks_exact(12) {
|
||||
brushes += 1;
|
||||
let first = i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||||
let count = i32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]);
|
||||
if first >= 0 && count >= 0 {
|
||||
sides += count as usize;
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"ЗАМЕР кирпичей {brushes}, сторон {sides}, сторон на кирпич {:.2}, граней {}",
|
||||
sides as f64 / brushes.max(1) as f64,
|
||||
geometry.faces.len()
|
||||
);
|
||||
let side_len = side_data.as_ref().len();
|
||||
println!(
|
||||
"ЗАМЕР блок сторон {side_len} байт, при 8 байт на сторону — {}",
|
||||
side_len / 8
|
||||
);
|
||||
println!("ЗАМЕР граней по содержимому кирпича:");
|
||||
for (contents, count) in &by_contents {
|
||||
println!("ЗАМЕР содержимое {contents:#x}: граней {count}");
|
||||
}
|
||||
for (class, (n, with_brushes)) in top.iter().take(16) {
|
||||
println!("ЗАМЕР {class}: {n}, с кирпичами {with_brushes}");
|
||||
println!("ЗАМЕР граней, у которых плоскости нет ни в одном кирпиче: {no_brush}");
|
||||
let mut materials_empty: std::collections::HashMap<String, usize> = Default::default();
|
||||
for &index in &empty_faces {
|
||||
let face = &geometry.faces[index];
|
||||
let name = geometry
|
||||
.texinfos
|
||||
.get(face.texinfo.max(0) as usize)
|
||||
.and_then(|ti| world.materials.get(ti.texdata as usize))
|
||||
.map(|m| m.name.clone())
|
||||
.unwrap_or_else(|| "?".to_string());
|
||||
*materials_empty.entry(name).or_default() += 1;
|
||||
}
|
||||
println!("ЗАМЕР материалы граней с содержимым 0x0:");
|
||||
let mut list: Vec<(&String, &usize)> = materials_empty.iter().collect();
|
||||
list.sort_by_key(|(_, c)| std::cmp::Reverse(**c));
|
||||
for (name, count) in list.iter().take(20) {
|
||||
println!("ЗАМЕР {name}: {count}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Индекс материала по имени (для диагностики).
|
||||
fn mat_index(world: &crate::world::World, mat: &crate::world::Material) -> usize {
|
||||
world
|
||||
.materials
|
||||
.iter()
|
||||
.position(|m| std::ptr::eq(m, mat))
|
||||
.unwrap_or(usize::MAX)
|
||||
}
|
||||
|
||||
/// Нормали дисплейсментов смотрят наружу, а не вниз.
|
||||
|
|
|
|||
104
docs/PARITY.md
104
docs/PARITY.md
|
|
@ -2022,3 +2022,107 @@ local = 1 / (1 + (k · distance · 0.01)²)
|
|||
стены) и 34 в верхнем бине. Геометрия холмов кривая, а наклон почти нулевой —
|
||||
значит он теряется при построении сетки displacement'а. Гипотеза Gemini про
|
||||
`dv.vec` как источник нормали объясняет цифру, но ещё не проверена.
|
||||
|
||||
## Часть E15. Разбор «белых плит» и «вывернутой травы» (2026-10-16)
|
||||
|
||||
### E15.1. Гипотеза Gemini про `dv.vec` — опровергнута измерением
|
||||
|
||||
Предполагалось, что нормаль берётся из `dv.vec` вместо геометрии сетки.
|
||||
Проверка сырых байт `LUMP_DISP_VERTS` (лам<D0B0><D0BC> 33, 252 760 байт):
|
||||
|
||||
```
|
||||
первые 64 байта:
|
||||
0: 00 00 00 00 d5 4c a9 38 ff ff 7f 3f 93 29 91 41
|
||||
16: 00 00 20 3f 62 b5 db b7 7a b1 1a 39 00 00 80 3f
|
||||
```
|
||||
|
||||
Читать как f32 с шагом 20 байт даёт осмысленные значения
|
||||
(`vec=(0.0000,0.0001,1.0000)`, `dist=18.145`), читать как три `i16` — мусор.
|
||||
Значит `parse_dispverts` прав: формат f32, шаг 20, `vec` действительно
|
||||
почти всегда `(0,0,1)`, а рельеф формируется полем `dist`. Гипотеза неверна.
|
||||
|
||||
### E15.2. Ориентация рельефа в порядке
|
||||
|
||||
| замер | значение |
|
||||
|---|---|
|
||||
| плоскости displacement'ов | вверх 110, вниз 0, сбоку 0 |
|
||||
| намотка | развёрнуто 0 треугольников из 11 776 |
|
||||
| рельеф в радиусе 600 от спавна | z от −151.4 до −148.0 |
|
||||
| базовые плоскости рельефа | 110 штук, z от −148 до 11 235 |
|
||||
|
||||
Рельеф у спавна почти плоский (3 юнита перепада на 1 200), поэтому «наклонены
|
||||
только 104 вершины» — свойство карты, а не ошибка намотки.
|
||||
|
||||
### E15.3. Ошибка в моей же сверке лампов
|
||||
|
||||
Справочник лампов читался со stride 48 вместо 16. Из-за этого `LUMP_NODES`
|
||||
выглядел как навмеш, а лампы 6 и 18 менялись местами. Все выводы про
|
||||
`LUMP_NODES` из части E14 недействительны; содержимое по stride 16 приведено
|
||||
выше и сходится с тем, что даёт Rust-читатель.
|
||||
|
||||
### E15.4. «Белые плиты» — что это на самом деле
|
||||
|
||||
Материал с текстурой `color/white` (`GM_CONSTRUCT/COLOR_ROOM`, `UnlitGeneric`)
|
||||
используют всего **9 граней**, и это не ящик в центре карты, как показалось
|
||||
сначала, а тонкие пластины:
|
||||
|
||||
| грань | габарит | размер |
|
||||
|---|---|---|
|
||||
| 481, 483 | x = −2560 | 64 × 4 |
|
||||
| 614, 615 | x = −2048 | 64 × 4 |
|
||||
| 880 | y = −2176, z = −20..−16 | 128 × 4 |
|
||||
| 1985, 1987 | y = −2868, z = 508 и 636 | 64 × 4 |
|
||||
| 2057, 2058 | y = −2704, z = 508 и 636 | 64 × 4 |
|
||||
|
||||
Все нормали строго ±X или ±Y, высоты четырёх уровней — это и есть «оси
|
||||
координат 3д» на глаз. Свойства: `surf flags 0x0` (`SURF_NODRAW` **не
|
||||
выставлен**), стили `[0,255,255,255]` и `[0,33,255,255]`, коллизия есть.
|
||||
|
||||
### E15.5. Настоящая белая плита в центре — другая
|
||||
|
||||
Большая белая поверхность под камерой `(0,0,2200)` — это не `color/white`.
|
||||
Самые крупные грани рядом с нулём:
|
||||
|
||||
| грань | габарит | материал |
|
||||
|---|---|---|
|
||||
| 9379, 9380, 9385, 9386 | (−1240,−968,−4)..(1240,968,4) | `concretefloor028a_1280_-1600_-64` |
|
||||
| 9344, 9347, 9350 | z = −148 | `concretefloor028a_1344_3584_32` |
|
||||
|
||||
То есть бетонная плита 2480 × 1936 в 8 юнитов толщину прямо над спавном, и
|
||||
она **продублирована**: грани 9379 и 9385 имеют одинаковый габарит, как и
|
||||
9380 с 9386. Дубль есть в самой карте (две одинаковые плиты), это не наш баг.
|
||||
|
||||
### E15.6. Чёрные области — не тени
|
||||
|
||||
`SOURCE_RS_NO_CULL` и принудительный `return 1.0` в `sun_shadow` дали кадры,
|
||||
отличающиеся только отсечением; тени на чёрные пятна не влияют. Значит чёрное
|
||||
берётся из `lit = in.light * ambient + dynamic`, то есть из лайтмапы.
|
||||
|
||||
### E15.7. Связь «грань → кирпич»
|
||||
|
||||
Прямой связи нет: 2 203 кирпича, 14 946 сторон, 9 460 граней, номера не
|
||||
параллельны. Рабочий обход — по плоскости: `planenum` грани → кирпичи, у
|
||||
которых эта плоскость есть, затем проверка, что все вершины грани внутри
|
||||
выпуклой оболочки. Этим найдено, что часть белых пластин принадлежит кирпичам
|
||||
с `contents = 0x0` (`CONTENTS_EMPTY`, не твёрдые).
|
||||
|
||||
Но правило «`contents == 0` → не рисуем» **слишком грубое**: под него
|
||||
попадают 330 граней, в основном `plasterwall022c` (250) и
|
||||
`CONSTRUCT_CONCRETE_GROUND` (50) — это видимая геометрия. Отбор по содержимому
|
||||
в лоб резать нельзя.
|
||||
|
||||
`LUMP_FACEIDS` (лампа 11, 18 920 байт = 9 460 значений по 2 байта) на этой карте
|
||||
содержит значения вроде −27 746 и 24 471, а номера сущностей — то, что нужно,
|
||||
не подтвердилось.
|
||||
|
||||
### E15.8. Что осталось
|
||||
|
||||
1. Понять, почему бетонная плита 2480 × 1936 у спавна выглядит белой. Тон
|
||||
по эталону: бетон 143.7 при эталонных 143.6 — совпадает, значит дело не в
|
||||
масштабе, а в том, что в GMod этой плиты, вероятно, не видно (она внутри
|
||||
комнаты, а мы видим её сверху).
|
||||
2. Разобраться с плоской травой: снять кадр GMod из точки `ref_p4`
|
||||
(`setpos 0 0 2200`, `setang 180 55`) и сравнить рельеф.
|
||||
3. `sun_shadow` без проверки `ndc.z` — точка за ближней плоскостью карты
|
||||
теней получает `depth < 0` и трактуется как затенённая. Сейчас это не
|
||||
видно глазом, но диапазон не проверяется, и это лазейка для чёрных пятен.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue