diff --git a/crates/engine/src/player.rs b/crates/engine/src/player.rs index 9500f35..0180e32 100644 --- a/crates/engine/src/player.rs +++ b/crates/engine/src/player.rs @@ -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 = 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 = 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> = Default::default(); + let mut contents_of: Vec = 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 = - 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 = Default::default(); + let mut empty_faces: Vec = 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 = 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 = 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) } /// Нормали дисплейсментов смотрят наружу, а не вниз. diff --git a/docs/PARITY.md b/docs/PARITY.md index 98e6910..85fe5f4 100644 --- a/docs/PARITY.md +++ b/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` (лам�� 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` и трактуется как затенённая. Сейчас это не + видно глазом, но диапазон не проверяется, и это лазейка для чёрных пятен.