A13 (первая половина): туман карты читается в мир
Some checks are pending
CI / checks (push) Waiting to run
Some checks are pending
CI / checks (push) Waiting to run
Начал пункт «туман» с чтения данных: в Source параметры тумана лежат в env_fog_controller (её можно менять в игре), а если её нет — в worldspawn. Разобраны fogcolor (R G B, 0..255 либо 0..1), fogstart, fogend, fogmaxdensity и farz; приоритет у контроллера. Туман хранится в World как Option<Fog> и печатается в лог сборки мира; у Fog есть fraction(distance) — линейная доля от start к end с потолком плотности, как в движке. При end <= start тумана нет вовсе (None) — шейдер его тогда не рисует. Замер на gm_construct: туман есть — цвет (0,557 0,643 0,698), start 1024, end 250000, max_density 1,0, farz -1. То есть дальний серо-синий оттенок у карты задан, и после подключения шейдера он появится. Тест fog_parses_controller_and_worldspawn: приоритет контроллера, разбор worldspawn, поведение fraction, отсутствие тумана. Все 23 сьюта и 28 картовых тестов зелёные. Следующий шаг — отдать туман в шейдер мира.
This commit is contained in:
parent
72bad60d8c
commit
d35efef711
2 changed files with 132 additions and 0 deletions
|
|
@ -50,6 +50,7 @@ fn empty_world() -> World {
|
|||
bounds_min: Vec3::splat(-1.0),
|
||||
bounds_max: Vec3::splat(1.0),
|
||||
stats: Default::default(),
|
||||
fog: None,
|
||||
ambient: [0.62, 0.66, 0.74],
|
||||
skybox: None,
|
||||
disp_tris: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -287,6 +287,8 @@ pub struct World {
|
|||
///
|
||||
/// Задаёт нижнюю границу освещённости: у теней есть цвет, а не чёрнота.
|
||||
pub ambient: [f32; 3],
|
||||
/// Туман карты (`env_fog_controller` или `worldspawn`), если задан.
|
||||
pub fog: Option<Fog>,
|
||||
/// Скайбокс (грани в порядке ft, bk, rt, lf, up, dn).
|
||||
pub skybox: Option<Skybox>,
|
||||
/// Треугольники displacement'ов в мировых координатах (для коллизии).
|
||||
|
|
@ -357,6 +359,7 @@ pub fn build_world(bsp: &Bsp, vfs: &VirtualFs) -> Result<World, WorldError> {
|
|||
bounds_min: Vec3::splat(f32::MAX),
|
||||
bounds_max: Vec3::splat(f32::MIN),
|
||||
stats: WorldStats::default(),
|
||||
fog: None,
|
||||
ambient: [0.62, 0.66, 0.74],
|
||||
skybox: None,
|
||||
disp_tris: Vec::new(),
|
||||
|
|
@ -813,6 +816,10 @@ pub fn build_world(bsp: &Bsp, vfs: &VirtualFs) -> Result<World, WorldError> {
|
|||
// задаёт цвет теней. Без неё тени были чисто чёрными, и мир выглядел
|
||||
// вырезанным из бумаги.
|
||||
world.ambient = map_ambient(&entities);
|
||||
world.fog = map_fog(&entities);
|
||||
if let Some(fog) = world.fog {
|
||||
tracing::info!(?fog, "туман карты");
|
||||
}
|
||||
let suns = world
|
||||
.lights
|
||||
.iter()
|
||||
|
|
@ -2580,6 +2587,94 @@ fn parse_angles(text: &str) -> (f32, f32, f32) {
|
|||
/// (на gm_construct `_ambient 114 136 148 420`). Нормируем по среднему, иначе
|
||||
/// сумма источников выбьет карту в белое; ограничиваем сверху единицей, потому
|
||||
/// что это добавка, а не основной свет.
|
||||
/// Туман карты — `env_fog_controller` или ключи `worldspawn`.
|
||||
///
|
||||
/// Source держит параметры тумана в сущности `env_fog_controller` (у неё их
|
||||
/// можно менять в игре), а если её нет — в `worldspawn`. Значения: `fogcolor`
|
||||
/// («R G B», обычно 0..255), `fogstart`/`fogend` в юнитах, `fogmaxdensity`
|
||||
/// (0..1) и `farz` (дальняя плоскость).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Fog {
|
||||
/// Цвет тумана (0..1).
|
||||
pub color: [f32; 3],
|
||||
/// Начало и конец тумана в юнитах.
|
||||
pub start: f32,
|
||||
pub end: f32,
|
||||
/// Максимальная плотность (0..1).
|
||||
pub max_density: f32,
|
||||
/// Дальняя плоскость отсечения (`farz`), 0 — обычная.
|
||||
pub far_z: f32,
|
||||
}
|
||||
|
||||
impl Fog {
|
||||
/// Туман включён: движок рисует его только при `end > start`.
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.end > self.start
|
||||
}
|
||||
|
||||
/// Доля тумана на расстоянии — линейно от `start` к `end`, с потолком
|
||||
/// плотности. Как в движке: за `end` дальше не густеет.
|
||||
pub fn fraction(&self, distance: f32) -> f32 {
|
||||
if !self.is_enabled() || distance <= self.start {
|
||||
return 0.0;
|
||||
}
|
||||
let span = (self.end - self.start).max(1e-3);
|
||||
((distance - self.start) / span).clamp(0.0, 1.0) * self.max_density
|
||||
}
|
||||
}
|
||||
|
||||
/// Цвет «R G B» из ключей карты: 0..255, но принимаем и 0..1.
|
||||
fn parse_fog_color(text: &str) -> Option<[f32; 3]> {
|
||||
let parts: Vec<f32> = text
|
||||
.split_whitespace()
|
||||
.filter_map(|value| value.parse::<f32>().ok())
|
||||
.collect();
|
||||
if parts.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let scale = if parts[0] > 1.5 || parts[1] > 1.5 || parts[2] > 1.5 {
|
||||
1.0 / 255.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
Some([parts[0] * scale, parts[1] * scale, parts[2] * scale])
|
||||
}
|
||||
|
||||
fn fog_key(entity: &formats::KvDocument, name: &str) -> Option<f32> {
|
||||
entity.get_str(name)?.trim().parse::<f32>().ok()
|
||||
}
|
||||
|
||||
/// Разбирает туман карты: приоритет у `env_fog_controller`, иначе `worldspawn`.
|
||||
///
|
||||
/// `None` — туман не задан: тогда шейдер его не рисует вовсе.
|
||||
pub fn map_fog(entities: &[formats::KvDocument]) -> Option<Fog> {
|
||||
let controller = entities
|
||||
.iter()
|
||||
.find(|entity| entity.get_str("classname") == Some("env_fog_controller"));
|
||||
let source = controller.or_else(|| {
|
||||
entities
|
||||
.iter()
|
||||
.find(|entity| entity.get_str("classname") == Some("worldspawn"))
|
||||
})?;
|
||||
let start = fog_key(source, "fogstart").unwrap_or(0.0);
|
||||
let end = fog_key(source, "fogend").unwrap_or(0.0);
|
||||
if end <= start {
|
||||
return None;
|
||||
}
|
||||
Some(Fog {
|
||||
color: source
|
||||
.get_str("fogcolor")
|
||||
.and_then(parse_fog_color)
|
||||
.unwrap_or([0.5, 0.6, 0.7]),
|
||||
start,
|
||||
end,
|
||||
max_density: fog_key(source, "fogmaxdensity")
|
||||
.unwrap_or(1.0)
|
||||
.clamp(0.0, 1.0),
|
||||
far_z: fog_key(source, "farz").unwrap_or(0.0),
|
||||
})
|
||||
}
|
||||
|
||||
fn map_ambient(entities: &[formats::KvDocument]) -> [f32; 3] {
|
||||
const FALLBACK: [f32; 3] = [0.62, 0.66, 0.74];
|
||||
for kv in entities {
|
||||
|
|
@ -3510,6 +3605,38 @@ mod tests {
|
|||
/// Отбрасывать её нельзя: сравнение с GMod показало, что с плоским
|
||||
/// коэффициентом тоннель выходил 165,8 при эталонных 92,9, а с экспонентой
|
||||
/// 86,8.
|
||||
#[test]
|
||||
fn fog_parses_controller_and_worldspawn() {
|
||||
// Приоритет у `env_fog_controller`, иначе — ключи `worldspawn`.
|
||||
let text = "{\"classname\" \"worldspawn\"\n\"fogcolor\" \"100 120 140\"\n\
|
||||
\"fogstart\" \"500\"\n\"fogend\" \"4000\"}\n\
|
||||
{\"classname\" \"env_fog_controller\"\n\"fogcolor\" \"200 210 220\"\n\
|
||||
\"fogstart\" \"1000\"\n\"fogend\" \"2000\"\n\
|
||||
\"fogmaxdensity\" \"0.5\"}\n";
|
||||
let kvs = formats::KvDocument::parse_blocks(text).expect("KV");
|
||||
let fog = map_fog(&kvs).expect("туман есть");
|
||||
assert!((fog.color[0] - 200.0 / 255.0).abs() < 1e-3);
|
||||
assert!((fog.start - 1000.0).abs() < 0.01 && (fog.end - 2000.0).abs() < 0.01);
|
||||
assert!((fog.max_density - 0.5).abs() < 0.01);
|
||||
assert!(fog.is_enabled());
|
||||
// До начала — ноль, в середине — половина от плотности, дальше — потолок.
|
||||
assert_eq!(fog.fraction(500.0), 0.0);
|
||||
assert!((fog.fraction(1500.0) - 0.25).abs() < 0.01);
|
||||
assert!((fog.fraction(9999.0) - 0.5).abs() < 0.01);
|
||||
|
||||
// Контроллера нет — берём worldspawn.
|
||||
let only_world = "{\"classname\" \"worldspawn\"\n\"fogcolor\" \"10 20 30\"\n\
|
||||
\"fogstart\" \"100\"\n\"fogend\" \"900\"}\n";
|
||||
let kvs = formats::KvDocument::parse_blocks(only_world).expect("KV");
|
||||
let fog = map_fog(&kvs).expect("туман из worldspawn");
|
||||
assert!(fog.start == 100.0 && fog.end == 900.0);
|
||||
assert!((fog.max_density - 1.0).abs() < 1e-6);
|
||||
|
||||
// Туман не задан — `None`: шейдер его не рисует вовсе.
|
||||
let none = formats::KvDocument::parse_blocks("{\"classname\" \"worldspawn\"}").expect("KV");
|
||||
assert!(map_fog(&none).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lightmap_exponent_byte_makes_shadow_darker() {
|
||||
use formats::bsp::geometry::{Face, TexInfo};
|
||||
|
|
@ -3672,6 +3799,7 @@ mod tests {
|
|||
}],
|
||||
bounds_min: Vec3::splat(-100.0),
|
||||
bounds_max: Vec3::splat(100.0),
|
||||
fog: None,
|
||||
ambient: [0.62, 0.66, 0.74],
|
||||
prop_meshes: vec![PropMesh {
|
||||
positions: vec![[1.0, 2.0, 3.0]],
|
||||
|
|
@ -3727,6 +3855,7 @@ mod tests {
|
|||
}],
|
||||
bounds_min: Vec3::splat(-100.0),
|
||||
bounds_max: Vec3::splat(100.0),
|
||||
fog: None,
|
||||
ambient: [0.62, 0.66, 0.74],
|
||||
prop_meshes: vec![PropMesh {
|
||||
positions: vec![[1.0, 0.0, 0.0]],
|
||||
|
|
@ -3784,6 +3913,7 @@ mod tests {
|
|||
bounds_min: Vec3::ZERO,
|
||||
bounds_max: Vec3::ZERO,
|
||||
stats: WorldStats::default(),
|
||||
fog: None,
|
||||
ambient: [0.62, 0.66, 0.74],
|
||||
skybox: None,
|
||||
disp_tris: Vec::new(),
|
||||
|
|
@ -3834,6 +3964,7 @@ mod tests {
|
|||
bounds_min: Vec3::ZERO,
|
||||
bounds_max: Vec3::ZERO,
|
||||
stats: WorldStats::default(),
|
||||
fog: None,
|
||||
ambient: [0.62, 0.66, 0.74],
|
||||
skybox: None,
|
||||
disp_tris: Vec::new(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue