Прыжок 200 вместо 270 и исправлены флаги граней — пропало 100 дырок в картинке
«Дырки в крыше, сквозь которые нельзя упасть»: граней в картинке было меньше, чем в коллизии. Причина — константы SURF_* в world.rs были перепутаны. По public/bspflags.h Source настоящие значения: SURF_SKY2D 0x0002 SURF_NOPORTAL 0x0020 SURF_SKY 0x0004 SURF_TRIGGER 0x0040 SURF_WARP 0x0008 SURF_NODRAW 0x0080 SURF_HINT 0x0100 SURF_SKIP 0x0200 SURF_NOPORTAL означает «нельзя поставить портал», SURF_TRIGGER — служебный флаг; ни то, ни другое не про рисование, а мы их пропускали. Замер на gm_construct: старая маска скрывала 2511 граней, новая 2411 — в картинке появилось 100 граней, не пропало ни одной. Тесты surface_flags_match_source_bspflags и skip_mask_covers_only_faces_that_are_not_drawn не дадут значениям снова разойтись. «Прыжки марсианские»: скорость прыжка бралась как 270 из Source SDK, а у GMod она своя — вики GMod в примере Player:GetJumpPower() печатает 200. Прыжок был выше в 1,8 раза. высота 60,8 -> 33,3 юнита до вершины 0,45 -> 0,33 с полёт 0,90 -> 0,67 с sv_gravity = 600 подтверждён вики отдельно и остаётся. Попутно исправлены две ошибки в самих измерениях: брался максимальный z над точкой вместо высоты поверхности на месте (крыша на z=1296 заслоняла крышу на z=240), и зазор между габаритами считался без клампа по нулю. На этих артефактах диагностика рапортовала чужие дефекты.
This commit is contained in:
parent
7b99c58bd9
commit
229372ffa1
3 changed files with 324 additions and 30 deletions
|
|
@ -1105,8 +1105,12 @@ const GRAVITY: f32 = 600.0;
|
|||
/// поднят на порядок, иначе падение упирается в него и перестаёт расти.
|
||||
const MAX_VELOCITY: f32 = 3500.0;
|
||||
|
||||
/// Скорость прыжка (`m_flJumpSpeed` в GMod = 270).
|
||||
const JUMP_SPEED: f32 = 270.0;
|
||||
/// Скорость прыжка — `Player:GetJumpPower()` в GMod по умолчанию **200**.
|
||||
///
|
||||
/// Вики GMod в примере `GetJumpPower` печатает 200; величина 270 из Source SDK
|
||||
/// к GMod отношения не имеет, и была взята по привычке. Отсюда прыжок был
|
||||
/// 60,8 юнита вместо 200²/1200 = **33,3** — «слишком высокий».
|
||||
const JUMP_SPEED: f32 = 200.0;
|
||||
/// Скорость шага (`+forward speed` в Source = 250).
|
||||
const WALK_SPEED: f32 = 250.0;
|
||||
/// Скорость бега (`+speed` в Source = 520).
|
||||
|
|
@ -2295,8 +2299,9 @@ mod tests {
|
|||
let before = player.velocity.z;
|
||||
player.update(&collision, 0.0, 0.0, true, false, 0.0, 1.0 / 60.0);
|
||||
// Прыжок — единственный кадр, где вертикальная скорость взлетает
|
||||
// до значения прыжка.
|
||||
if player.velocity.z > 250.0 && before <= 0.0 {
|
||||
// до значения прыжка. Порог от `JUMP_SPEED`, а не число: прыжок в
|
||||
// GMod это 200 (`GetJumpPower`), а не 270 из Source SDK.
|
||||
if player.velocity.z > JUMP_SPEED * 0.9 && before <= 0.0 {
|
||||
jumps += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -3059,6 +3064,106 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// Есть ли в рендере поверхность под заданной точкой.
|
||||
///
|
||||
/// Диагностика жалобы «сквозь дырки в крыше упасть нельзя»: ищем точки,
|
||||
/// где игрок стоит на твёрдом кирпиче, а нарисованной поверхности над ним
|
||||
/// нет — то есть ровно то место, где игрок видит дырку и не может
|
||||
/// провалиться.
|
||||
#[test]
|
||||
#[ignore = "требует ассеты Garry's Mod: SOURCE_RS_TEST_BSP и SOURCE_RS_TEST_BSP_AT"]
|
||||
fn probe_drawn_surface_under() {
|
||||
use crate::world::build_world;
|
||||
use fs::VirtualFs;
|
||||
|
||||
let Some(path) = std::env::var_os("SOURCE_RS_TEST_BSP") else {
|
||||
return;
|
||||
};
|
||||
let Ok(at) = std::env::var("SOURCE_RS_TEST_BSP_AT") else {
|
||||
return;
|
||||
};
|
||||
let parts: Vec<f32> = at
|
||||
.split(',')
|
||||
.map(|v| v.trim().parse::<f32>().unwrap_or(0.0))
|
||||
.collect();
|
||||
let center = Vec3::new(parts[0], parts[1], parts[2]);
|
||||
let data = std::fs::read(&path).expect("BSP читается");
|
||||
let bsp = Bsp::from_bytes(data).expect("BSP разбирается");
|
||||
let world = build_world(&bsp, &VirtualFs::new()).expect("мир строится");
|
||||
let collision = Collision::from_bsp(&bsp).expect("коллизия");
|
||||
|
||||
/// Точка внутри треугольника по его проекции на XY.
|
||||
fn point_in_tri(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Option<f32> {
|
||||
let d = (b.z - c.z) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y);
|
||||
if d.abs() < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let w0 = ((b.z - c.z) * (p.x - c.x) + (c.x - b.x) * (p.y - c.y)) / d;
|
||||
let w1 = ((c.z - a.z) * (p.x - c.x) + (a.x - c.x) * (p.y - c.y)) / d;
|
||||
let w2 = 1.0 - w0 - w1;
|
||||
if w0 < -1e-3 || w1 < -1e-3 || w2 < -1e-3 {
|
||||
return None;
|
||||
}
|
||||
Some(a.z * w0 + b.z * w1 + c.z * w2)
|
||||
}
|
||||
|
||||
let step = 24.0f32;
|
||||
let mut solid = 0usize;
|
||||
let mut invisible = 0usize;
|
||||
let mut samples: Vec<(f32, f32, f32, Option<f32>)> = Vec::new();
|
||||
let mut x = center.x - 160.0;
|
||||
while x <= center.x + 160.0 {
|
||||
let mut y = center.y - 160.0;
|
||||
while y <= center.y + 160.0 {
|
||||
// Куда игрок встаёт, если его сбросить сверху.
|
||||
let mut drop = Player::new(Vec3::new(x, y, center.z + 200.0));
|
||||
for _ in 0..300 {
|
||||
drop.update(&collision, 0.0, 0.0, false, false, 0.0, 1.0 / 60.0);
|
||||
}
|
||||
if !drop.on_ground {
|
||||
y += step;
|
||||
continue;
|
||||
}
|
||||
solid += 1;
|
||||
// Что нарисовано прямо НА МЕСТЕ, где он стоит: берём все
|
||||
// треугольники, накрывающие точку по XY, и ищем тот, что
|
||||
// совпадает с местом приземления.
|
||||
//
|
||||
// Раньше здесь брался максимальный z над точкой — это ошибка:
|
||||
// крыша тысячу юнитов выше заслоняла собой крышу под ногами, и
|
||||
// все 196 точек объявлялись «без рисунка».
|
||||
let probe = Vec3::new(x, y, 0.0);
|
||||
let mut covers = false;
|
||||
for tri in world.indices.chunks_exact(3) {
|
||||
let v = |i: usize| {
|
||||
let v = world.vertices[tri[i] as usize].pos;
|
||||
Vec3::new(v[0], v[1], v[2])
|
||||
};
|
||||
if let Some(z) = point_in_tri(probe, v(0), v(1), v(2)) {
|
||||
if (z - drop.position.z).abs() < 8.0 {
|
||||
covers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !covers {
|
||||
invisible += 1;
|
||||
if samples.len() < 8 {
|
||||
samples.push((x, y, drop.position.z, None));
|
||||
}
|
||||
}
|
||||
y += step;
|
||||
}
|
||||
x += step;
|
||||
}
|
||||
println!("ЗАМЕР крыша вокруг {center:?}: мест {solid}, твёрдо без рисунка {invisible}");
|
||||
for (x, y, z, drawn) in samples.iter().take(8) {
|
||||
println!(
|
||||
"ЗАМЕР точка ({x:.0}, {y:.0}): игрок стоит на z={z:.1}, нарисовано {drawn:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Что за геометрия вокруг заданной точки: кирпичи и содержимое.
|
||||
///
|
||||
/// Диагностика жалобы «сквозь дырки в крыше упасть нельзя». Точка и радиус
|
||||
|
|
@ -3116,42 +3221,111 @@ mod tests {
|
|||
}
|
||||
println!("ЗАМЕР рядом: кирпичей {brushes}, треугольников {tris}");
|
||||
|
||||
// Сколько граней рендер теперь пропускает: было 2451 до исправления
|
||||
// флагов, должно стать заметно меньше.
|
||||
{
|
||||
use crate::world::build_world;
|
||||
use fs::VirtualFs;
|
||||
let world = build_world(&bsp, &VirtualFs::new()).expect("мир строится");
|
||||
println!(
|
||||
"ЗАМЕР мир: граней пропущено {}, треугольников {}",
|
||||
world.stats.skipped, world.stats.triangles
|
||||
);
|
||||
}
|
||||
|
||||
// Флаги граней рядом: рисуется грань или нет. Кирпич может быть твёрдым
|
||||
// и при этом не иметь ни одной нарисованной грани — это и есть
|
||||
// невидимая стена.
|
||||
// Флаги граней, лежащих ровно на уровне, где стоит игрок: если у них
|
||||
// стоит «запрет рисования», то крыша твёрдая, но невидимая.
|
||||
let geometry = bsp.geometry().expect("геометрия");
|
||||
let mut flagged: Vec<(i32, Vec3)> = Vec::new();
|
||||
let texdata = bsp.texdata().expect("texdata");
|
||||
let names = bsp.texture_names().expect("имена текстур");
|
||||
let mut plain = 0usize;
|
||||
for (index, face) in geometry.faces.iter().enumerate() {
|
||||
let mut noportal = 0usize;
|
||||
let mut flagged: Vec<(i32, String, f32)> = Vec::new();
|
||||
for face in geometry.faces.iter() {
|
||||
let Some(plane) = geometry.planes.get(face.planenum as usize) else {
|
||||
continue;
|
||||
};
|
||||
let normal = Vec3::new(plane.normal[0], plane.normal[1], plane.normal[2]);
|
||||
let dist = plane.dist;
|
||||
// Плоскость должна проходить рядом с точкой.
|
||||
if (normal.dot(center) - dist).abs() > 300.0 {
|
||||
if (normal.dot(center) - dist).abs() > 8.0 {
|
||||
continue;
|
||||
}
|
||||
let Some(texinfo) = geometry.texinfos.get(face.texinfo as usize) else {
|
||||
continue;
|
||||
};
|
||||
let skip = texinfo.flags & (0x20 | 0x80 | 0x40 | 0x200) != 0;
|
||||
if skip {
|
||||
// `SURF_NOPORTAL` = 0x20: грань пропускает порталы, но рисуется как
|
||||
// обычно. У нас же 0x20 считался за «не рисовать», и такие грани
|
||||
// просто исчезали из картинки.
|
||||
if texinfo.flags & 0x20 != 0 {
|
||||
noportal += 1;
|
||||
}
|
||||
// Маска рендерера из world.rs.
|
||||
if texinfo.flags & (0x4 | 0x8 | 0x20 | 0x40 | 0x80) != 0 {
|
||||
let name = texdata
|
||||
.get(texinfo.texdata.max(0) as usize)
|
||||
.map(|t| {
|
||||
names
|
||||
.get(t.name_id.max(0) as usize)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if flagged.len() < 20 {
|
||||
flagged.push((texinfo.flags, Vec3::new(index as f32, dist, 0.0)));
|
||||
flagged.push((texinfo.flags, name, dist));
|
||||
}
|
||||
} else {
|
||||
plain += 1;
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"ЗАМЕР граней рядом: рисуемых {plain}, с запретом рисования {}",
|
||||
"ЗАМЕР граней на уровне игрока: рисуемых {plain}, пропущенных {}, из них с SURF_NOPORTAL (0x20) {noportal}",
|
||||
flagged.len()
|
||||
);
|
||||
for (flags, _) in flagged.iter().take(20) {
|
||||
println!("ЗАМЕР грань с флагами {flags:#x}");
|
||||
for (flags, name, dist) in flagged.iter().take(20) {
|
||||
println!("ЗАМЕР грань z={dist:.1} флаги {flags:#x} текстура {name:?}");
|
||||
}
|
||||
|
||||
// Флаги по всей карте: что скрывает маска рендерера.
|
||||
//
|
||||
// Настоящие значения из `public/bspflags.h` Source:
|
||||
// SURF_SKY 0x4, SURF_WARP 0x8, SURF_NOPORTAL 0x20,
|
||||
// SURF_TRIGGER 0x40, SURF_NODRAW 0x80, SURF_HINT 0x100,
|
||||
// SURF_SKIP 0x200, SURF_NOLIGHT 0x400.
|
||||
//
|
||||
// У нас стояло `NODRAW = 0x20`, `HINT = 0x40`, `SKIP = 0x80` — то есть
|
||||
// под «не рисуем» попадали `NOPORTAL` и `TRIGGER`, а настоящие `NODRAW`,
|
||||
// `HINT` и `SKIP` проходили в рендер.
|
||||
let mut old_mask = 0usize;
|
||||
let mut new_mask = 0usize;
|
||||
let mut newly_hidden = 0usize;
|
||||
let mut newly_shown = 0usize;
|
||||
for face in geometry.faces.iter() {
|
||||
let Some(texinfo) = geometry.texinfos.get(face.texinfo as usize) else {
|
||||
continue;
|
||||
};
|
||||
let flags = texinfo.flags;
|
||||
let old = flags & (0x4 | 0x8 | 0x20 | 0x40 | 0x80) != 0;
|
||||
let new = flags & (0x2 | 0x4 | 0x80 | 0x100 | 0x200) != 0;
|
||||
if old {
|
||||
old_mask += 1;
|
||||
}
|
||||
if new {
|
||||
new_mask += 1;
|
||||
}
|
||||
if new && !old {
|
||||
newly_hidden += 1;
|
||||
}
|
||||
if old && !new {
|
||||
newly_shown += 1;
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"ЗАМЕР флаги граней: старая маска скрывала {old_mask}, новая {new_mask}, появилось в картинке {newly_shown}, пропало из картинки {newly_hidden}"
|
||||
);
|
||||
|
||||
// Расклад содержимого по всей карте: что вообще попадает в коллизию.
|
||||
let brush_data = bsp.lump(lump::BRUSHES).expect("BRUSHES");
|
||||
let mut hist: HashMap<i32, usize> = HashMap::new();
|
||||
|
|
@ -3459,8 +3633,8 @@ mod tests {
|
|||
println!("ЗАМЕР дёрганье назад за 1с разгона: {jitter:.2} юнитов");
|
||||
assert!(jitter < 1.0, "игрок дёргается назад: {jitter:.2} юнитов");
|
||||
|
||||
// 4. Прыжок: 270 юнитов/с при `sv_gravity` = 600 даёт 270²/1200 ≈ 60,8
|
||||
// юнита высоты — как в GMod (в Source гравитация 800 и высота 45,6).
|
||||
// 4. Прыжок: `GetJumpPower` = 200 при `sv_gravity` = 600 даёт
|
||||
// 200²/1200 = 33,3 юнита высоты — как в GMod.
|
||||
let mut player = Player::new(Vec3::new(spawn.x, spawn.y, rest_z));
|
||||
for _ in 0..10 {
|
||||
player.update(&collision, 0.0, 0.0, false, false, 0.0, dt);
|
||||
|
|
@ -3476,8 +3650,8 @@ mod tests {
|
|||
peak = peak.max(player.position.z);
|
||||
}
|
||||
let height = peak - ground_z;
|
||||
// Дуга прыжка целиком: до вершины, обратно. В GMod при скорости 270
|
||||
// и `sv_gravity` = 600 это ровно 0,45 с вверх и 0,9 с полёта.
|
||||
// Дуга прыжка целиком: до вершины, обратно. В GMod при скорости прыжка
|
||||
// 200 и `sv_gravity` = 600 это ровно 0,333 с вверх и 0,667 с полёта.
|
||||
{
|
||||
let mut arc = Player::new(Vec3::new(spawn.x, spawn.y, rest_z));
|
||||
for _ in 0..10 {
|
||||
|
|
@ -3505,21 +3679,21 @@ mod tests {
|
|||
apex - ground
|
||||
);
|
||||
assert!(
|
||||
(apex_frame as f32 * dt - 0.45).abs() < 0.05,
|
||||
"до вершины должно быть 0,45 с, а вышло {:.2}",
|
||||
(apex_frame as f32 * dt - 1.0 / 3.0).abs() < 0.04,
|
||||
"до вершины должно быть 0,33 с, а вышло {:.2}",
|
||||
apex_frame as f32 * dt
|
||||
);
|
||||
assert!(
|
||||
(land_frame as f32 * dt - 0.9).abs() < 0.06,
|
||||
"полёт должен длиться 0,9 с, а вышел {:.2}",
|
||||
(land_frame as f32 * dt - 2.0 / 3.0).abs() < 0.05,
|
||||
"полёт должен длиться 0,67 с, а вышел {:.2}",
|
||||
land_frame as f32 * dt
|
||||
);
|
||||
}
|
||||
println!("ЗАМЕР прыжок: оторвался={left_ground} высота={height:.1} юнита (в GMod ~60,8)");
|
||||
println!("ЗАМЕР прыжок: оторвался={left_ground} высота={height:.1} юнита (в GMod ~33,3)");
|
||||
assert!(left_ground, "прыжок не оторвал игрока от земли");
|
||||
assert!(
|
||||
height > 50.0,
|
||||
"прыжок слишком низкий: {height:.1} юнита при ~60,8 в GMod"
|
||||
height > 28.0,
|
||||
"прыжок слишком низкий: {height:.1} юнита при ~33,3 в GMod"
|
||||
);
|
||||
|
||||
// 5. Падение сквозз мир: за 30 секунд ходьбы игрок либо на земле, либо
|
||||
|
|
|
|||
|
|
@ -10,13 +10,32 @@ use fs::{FsError, VirtualFs};
|
|||
use math::Vec3;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Флаги `texinfo.flags` (SURF_*).
|
||||
/// Флаги `texinfo.flags` (`SURF_*`) — значения из `public/bspflags.h` Source.
|
||||
///
|
||||
/// Раньше здесь стояли другие числа, и это стоило 100 граней на gm_construct:
|
||||
/// под «не рисуем» попадали `SURF_NOPORTAL` и `SURF_TRIGGER`, которые рисуются
|
||||
/// как обычные, а настоящие `SURF_NODRAW`, `SURF_HINT` и `SURF_SKIP` в рендер
|
||||
/// проходили. Итог — дырки в картинке при твёрдой коллизии: в дырку не видно,
|
||||
/// а сквозь неё не упасть.
|
||||
///
|
||||
/// ```text
|
||||
/// SURF_SKY2D 0x0002 // не рисуем, но рисуем 2D-небо
|
||||
/// SURF_SKY 0x0004 // не рисуем, но отдаём в скайбокс
|
||||
/// SURF_WARP 0x0008 // вода
|
||||
/// SURF_NOPORTAL 0x0020 // порталы не ставим — грань рисуется как обычно
|
||||
/// SURF_TRIGGER 0x0040 // служебный флаг, не про «не рисовать»
|
||||
/// SURF_NODRAW 0x0080 // не рисуем вовсе
|
||||
/// SURF_HINT 0x0100 // грань-подсказка для разбиения BSP
|
||||
/// SURF_SKIP 0x0200 // игнорируем, допускает незамкнутые кирпичи
|
||||
/// SURF_NOLIGHT 0x0400 // без освещения
|
||||
/// ```
|
||||
const SURF_SKY2D: i32 = 0x2;
|
||||
const SURF_SKY: i32 = 0x4;
|
||||
/// SURF_WARP — вода (шейдер Water).
|
||||
const SURF_WARP: i32 = 0x8;
|
||||
const SURF_NODRAW: i32 = 0x20;
|
||||
const SURF_HINT: i32 = 0x40;
|
||||
const SURF_SKIP: i32 = 0x80;
|
||||
const SURF_NODRAW: i32 = 0x80;
|
||||
const SURF_HINT: i32 = 0x100;
|
||||
const SURF_SKIP: i32 = 0x200;
|
||||
|
||||
/// Специальные материалы, которые не рендерим.
|
||||
const SKIP_MATERIALS: &[&str] = &[
|
||||
|
|
@ -954,7 +973,10 @@ fn resolve_material(
|
|||
checker: &TextureData,
|
||||
stats: &mut WorldStats,
|
||||
) -> Resolved {
|
||||
if texinfo.flags & (SURF_NODRAW | SURF_HINT | SURF_SKIP | SURF_SKY) != 0 {
|
||||
// `SURF_NOPORTAL` и `SURF_TRIGGER` в маску **не входят**: обе грань
|
||||
// рисуются как обычная. `SURF_SKY2D` тоже пропускаем — двумерное небо
|
||||
// рисует отдельный проход.
|
||||
if texinfo.flags & (SURF_NODRAW | SURF_HINT | SURF_SKIP | SURF_SKY | SURF_SKY2D) != 0 {
|
||||
return Resolved::Skip;
|
||||
}
|
||||
if texinfo.texdata < 0 {
|
||||
|
|
@ -2328,6 +2350,46 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// Флаги граней совпадают с `public/bspflags.h` Source.
|
||||
///
|
||||
/// Значения были перепутаны местами, и это стоило 100 граней на gm_construct:
|
||||
/// под «не рисуем» попадали `SURF_NOPORTAL` и `SURF_TRIGGER`, которые рисуются
|
||||
/// как обычные. Итог — дырки в картинке при твёрдой коллизии: в дырку не
|
||||
/// видно, а сквозь неё не упасть.
|
||||
#[test]
|
||||
fn surface_flags_match_source_bspflags() {
|
||||
assert_eq!(SURF_SKY2D, 0x0002, "SURF_SKY2D");
|
||||
assert_eq!(SURF_SKY, 0x0004, "SURF_SKY");
|
||||
assert_eq!(SURF_WARP, 0x0008, "SURF_WARP");
|
||||
assert_eq!(SURF_NODRAW, 0x0080, "SURF_NODRAW");
|
||||
assert_eq!(SURF_HINT, 0x0100, "SURF_HINT");
|
||||
assert_eq!(SURF_SKIP, 0x0200, "SURF_SKIP");
|
||||
}
|
||||
|
||||
/// Грань рисуется, если на ней нет ни одного «скрывающего» флага.
|
||||
///
|
||||
/// Проверяется именно то, из-за чего появились дырки: `SURF_NOPORTAL`
|
||||
/// (0x20) и `SURF_TRIGGER` (0x40) в маску пропуска не входят.
|
||||
#[test]
|
||||
fn skip_mask_covers_only_faces_that_are_not_drawn() {
|
||||
let hidden = SURF_SKY2D | SURF_SKY | SURF_NODRAW | SURF_HINT | SURF_SKIP;
|
||||
for flags in [0x20, 0x40, 0x400, 0x1000, 0x10, 0] {
|
||||
assert!(
|
||||
flags & hidden == 0,
|
||||
"грань с флагами {flags:#x} не должна скрываться"
|
||||
);
|
||||
}
|
||||
for flags in [SURF_SKY2D, SURF_SKY, SURF_NODRAW, SURF_HINT, SURF_SKIP] {
|
||||
assert_ne!(
|
||||
flags & hidden,
|
||||
0,
|
||||
"грань с флагом {flags:#x} обязана скрываться"
|
||||
);
|
||||
}
|
||||
// Вода рисуется отдельным проходом: SURF_WARP в маску не входит.
|
||||
assert_eq!(SURF_WARP & hidden, 0, "вода рисуется отдельным проходом");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checker_is_opaque() {
|
||||
let t = checker_texture("test");
|
||||
|
|
|
|||
|
|
@ -1380,3 +1380,61 @@ GMod (замерено 60,8). В Source с гравитацией 800 было 4
|
|||
|
||||
Чтобы довести до причины, нужны координаты конкретной дырки: без них
|
||||
единственный способ — гадать, а это прямо запрещено правилом проекта.
|
||||
|
||||
## Часть B10. Флаги граней и высота прыжка (сделано)
|
||||
|
||||
### B10.1. «Дырки в крыше, сквозь которые нельзя упасть»: неверные флаги граней
|
||||
|
||||
Проверено по указанной точке (−1388, −1503, 240): игрок там стоит на твёрдом
|
||||
кирпиче, но **нарисованной поверхности на месте нет**.
|
||||
|
||||
Причина — константы `SURF_*` в `world.rs` были перепутаны. Настоящие значения
|
||||
из `public/bspflags.h` Source:
|
||||
|
||||
| флаг | правильно | было у нас |
|
||||
|---|---|---|
|
||||
| `SURF_SKY2D` | 0x0002 | — |
|
||||
| `SURF_SKY` | 0x0004 | 0x4 |
|
||||
| `SURF_WARP` | 0x0008 | 0x8 |
|
||||
| `SURF_NOPORTAL` | 0x0020 | **0x20 — и считался за «не рисовать»** |
|
||||
| `SURF_TRIGGER` | 0x0040 | **0x40 — и считался за «не рисовать»** |
|
||||
| `SURF_NODRAW` | 0x0080 | **0x80 — но назывался SKIP** |
|
||||
| `SURF_HINT` | 0x0100 | 0x40 — не скрывался вовсе |
|
||||
| `SURF_SKIP` | 0x0200 | 0x80 — не скрывался вовсе |
|
||||
|
||||
`SURF_NOPORTAL` означает «на этой грани нельзя поставить портал», а
|
||||
`SURF_TRIGGER` — служебный флаг. Ни то, ни другое не имеет отношения к
|
||||
рисованию, а мы их пропускали. Итог — **граней в картинке меньше, чем в
|
||||
коллизии**: дырка, в которую не видно, но сквозь которую не упасть.
|
||||
|
||||
Замер на gm_construct: старая маска скрывала 2511 граней, новая — 2411;
|
||||
**появилось в картинке 100 граней, пропало 0**.
|
||||
|
||||
Тесты: `surface_flags_match_source_bspflags` (значения закреплены по
|
||||
первоисточнику) и `skip_mask_covers_only_faces_that_are_not_drawn` (в маску не
|
||||
входят `NOPORTAL` и `TRIGGER`).
|
||||
|
||||
### B10.2. «Прыжки марсианские» — они были выше GMod в 1,8 раза
|
||||
|
||||
Дуга прыжка снималась и совпадала с расчётом — но расчёт был неверен. Скорость
|
||||
прыжка бралась как 270 из Source SDK, а у GMod она своя: вики GMod в примере
|
||||
`Player:GetJumpPower()` печатает **200**.
|
||||
|
||||
| | было (270) | GMod (200) | замерено после |
|
||||
|---|---|---|---|
|
||||
| высота | 60,8 юнита | **33,3** | 33,3 |
|
||||
| до вершины | 0,45 с | **0,33 с** | 0,33 |
|
||||
| полёт | 0,90 с | **0,67 с** | 0,67 |
|
||||
|
||||
`sv_gravity` = 600 остаётся — он подтверждён вики GMod отдельно.
|
||||
|
||||
### B10.3. Ошибки измерений, которые пришлось исправить
|
||||
|
||||
Две, обе нашли чужие грани и на них строились выводы:
|
||||
|
||||
- **Максимальный z над точкой** вместо высоты поверхности **на месте**: крыша на
|
||||
z = 1296 заслоняла крышу на z = 240, и все 196 проб объявлялись «без рисунка».
|
||||
- **Зазор между габаритами без клампа по нулю**: по пересекающейся оси
|
||||
`max` двух отрицательных давал отрицательное число, а длина вектора —
|
||||
«912 юнитов зазора» там, где габариты соприкасаются. На этом артефакте
|
||||
диагностика рапортовала 35 невидимых стен при реальном нуле.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue