Небо гасится за пределами карты: и 2D-скайбокс, и 3D-скайбокс
Жалоба: «если игрок выходит за карту, то скайбокс и 3D-скайбокс не должны
рендериться (иначе не понятны границы карты)».
Причина видна в шейдере 3D-скайбокса:
world = camera.pos.xyz + (in.pos - sky_camera) * scale
Геометрия неба размещается относительно камеры, поэтому небо всегда заливает
весь фон — пределы карты на нём не видно.
Замер на gm_construct:
габариты геометрии (-5640, -4576, -960) .. (2168, 6464, 2976)
габариты коллизии (-15360, -15360, -1024) .. (15872, 15360, 15359)
sky_camera (-1428, 1645, 10991), масштаб 1/16
2D-скайбокса нет, только 3D
Габариты коллизии не годятся — включают и 3D-небо, и далёкие террасы,
поэтому основание — габариты геометрии карты.
- world::inside_bounds — одна реализация предиката, её используют и движок,
и рендерер (рендерер копирует габариты при загрузке).
- WorldRenderer::update_sky_visibility пересчитывает видимость каждый кадр:
игрок уходит за край во время игры. Проверка по всем трём осям сразу,
под картой небо тоже исчезает.
- SKY_BOUNDS_MARGIN = 128: габариты считаются по вершинам, а камера стоит
на высоте глаз над опорой, так что стоящий на краю игрок формально за ними.
- Гасятся оба прохода неба: иначе за краем остался бы виден 3D-неб, а это
ровно то, что мешало увидеть границу.
Тесты: sky_is_visible_only_inside_the_map (юнит) и real_map_sky_bounds
(замер на настоящей карте).
This commit is contained in:
parent
512f3d0394
commit
b96b06ccdb
4 changed files with 208 additions and 3 deletions
|
|
@ -15,4 +15,5 @@ pub use model::{Model, ModelVertex, load_model};
|
|||
pub use player::{Collision, Player};
|
||||
pub use world::{
|
||||
Batch, Material, Skybox, TextureData, World, WorldError, WorldStats, WorldVertex, build_world,
|
||||
inside_bounds,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1693,7 +1693,31 @@ fn find_spawn(entities: &[formats::KvDocument], world: &World) -> (Vec3, f32) {
|
|||
/// Предел числа динамических источников на кадр (данные из файла карты).
|
||||
pub const MAX_LIGHTS: usize = 64;
|
||||
|
||||
/// Находится ли точка внутри габаритов карты с допуском `margin` наружу.
|
||||
///
|
||||
/// За пределами габаритов геометрии небо рисовать нельзя: 3D-скайбокс
|
||||
/// привязан к камере (`world = cam + (v - sky_camera) * scale`) и всегда заливает
|
||||
/// весь фон. Из-за этого, вылетев за край карты, игрок видит бесконечное небо и
|
||||
/// не может понять, где карта кончилась — «границы карты» не видно.
|
||||
///
|
||||
/// `margin` — небольшой допуск наружу: габариты считаются по вершинам
|
||||
/// геометрии, и стоящий на самом краю игрок находится за ними на половину
|
||||
/// своего роста.
|
||||
pub fn inside_bounds(min: Vec3, max: Vec3, pos: Vec3, margin: f32) -> bool {
|
||||
pos.x >= min.x - margin
|
||||
&& pos.x <= max.x + margin
|
||||
&& pos.y >= min.y - margin
|
||||
&& pos.y <= max.y + margin
|
||||
&& pos.z >= min.z - margin
|
||||
&& pos.z <= max.z + margin
|
||||
}
|
||||
|
||||
impl World {
|
||||
/// Находится ли точка внутри проходимой части карты.
|
||||
pub fn inside_bounds(&self, pos: Vec3, margin: f32) -> bool {
|
||||
inside_bounds(self.bounds_min, self.bounds_max, pos, margin)
|
||||
}
|
||||
|
||||
/// Строит мир физики: рельеф карты и динамические пропы.
|
||||
///
|
||||
/// В мире физики живут только `prop_physics*`. У `prop_static` физики в
|
||||
|
|
@ -2220,6 +2244,90 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::entity::EntityWorld;
|
||||
|
||||
/// Небо гасится только за пределами карты.
|
||||
#[test]
|
||||
fn sky_is_visible_only_inside_the_map() {
|
||||
let min = Vec3::new(-1024.0, -1024.0, -256.0);
|
||||
let max = Vec3::new(1024.0, 1024.0, 512.0);
|
||||
let margin = 64.0;
|
||||
assert!(inside_bounds(min, max, Vec3::ZERO, margin));
|
||||
assert!(inside_bounds(min, max, Vec3::new(1024.0, 0.0, 0.0), margin));
|
||||
// На самом краю с допуском — ещё карта.
|
||||
assert!(inside_bounds(min, max, Vec3::new(1088.0, 0.0, 0.0), margin));
|
||||
// За краем — уже нет: небо гаснет, граница карты видна.
|
||||
assert!(!inside_bounds(
|
||||
min,
|
||||
max,
|
||||
Vec3::new(1089.0, 0.0, 0.0),
|
||||
margin
|
||||
));
|
||||
// По любой оси: под картой небо тоже исчезает.
|
||||
assert!(!inside_bounds(
|
||||
min,
|
||||
max,
|
||||
Vec3::new(0.0, 0.0, -321.0),
|
||||
margin
|
||||
));
|
||||
assert!(!inside_bounds(
|
||||
min,
|
||||
max,
|
||||
Vec3::new(0.0, -1089.0, 0.0),
|
||||
margin
|
||||
));
|
||||
assert!(!inside_bounds(min, max, Vec3::new(0.0, 0.0, 577.0), margin));
|
||||
}
|
||||
|
||||
/// Замер границ карты на настоящей карте.
|
||||
///
|
||||
/// Требует локальной установки Garry's Mod:
|
||||
///
|
||||
/// ```text
|
||||
/// SOURCE_RS_TEST_BSP=~/gmod_server/server/garrysmod/maps/gm_construct.bsp \\
|
||||
/// cargo test -p engine -- --ignored real_map_sky_bounds --nocapture
|
||||
/// ```
|
||||
#[test]
|
||||
#[ignore = "требует ассеты Garry's Mod: SOURCE_RS_TEST_BSP"]
|
||||
fn real_map_sky_bounds() {
|
||||
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("мир строится");
|
||||
println!(
|
||||
"ЗАМЕР границы карты {:?} .. {:?}, спавн {:?}",
|
||||
world.bounds_min, world.bounds_max, world.spawn
|
||||
);
|
||||
// Спавн внутри карты — небо есть.
|
||||
assert!(
|
||||
world.inside_bounds(world.spawn, 0.0),
|
||||
"спавн должен быть внутри карты"
|
||||
);
|
||||
// Далеко за краем по любой оси — неба нет.
|
||||
for dir in [
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
Vec3::new(-1.0, 0.0, 0.0),
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
Vec3::new(0.0, -1.0, 0.0),
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
Vec3::new(0.0, 0.0, -1.0),
|
||||
] {
|
||||
let far = world.spawn + dir * 100_000.0;
|
||||
assert!(
|
||||
!world.inside_bounds(far, 0.0),
|
||||
"небо должно гаснуть в {dir:?}: {far:?}"
|
||||
);
|
||||
}
|
||||
// Даже чуть за краем небо уже не рисуется.
|
||||
let just_outside =
|
||||
world.spawn + Vec3::new(0.0, 0.0, -1.0) * (world.spawn.z - world.bounds_min.z + 1.0);
|
||||
assert!(
|
||||
!world.inside_bounds(just_outside, 0.0),
|
||||
"под нижней гранью карты неба быть не должно: {just_outside:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checker_is_opaque() {
|
||||
let t = checker_texture("test");
|
||||
|
|
|
|||
|
|
@ -4,10 +4,19 @@ use crate::gfx::Gfx;
|
|||
use crate::text::{TextItem, TextVertex, build_atlas, build_vertices};
|
||||
use crate::texture::upload_texture;
|
||||
use engine::{Batch, FlyCamera, World, WorldVertex};
|
||||
use math::Mat4;
|
||||
use math::{Mat4, Vec3};
|
||||
|
||||
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||
|
||||
/// Допуск за границы карты, за которыми небо гасится.
|
||||
///
|
||||
/// Габариты считаются по вершинам геометрии, а камера стоит на высоте глаз
|
||||
/// (около 64 юнитов) над опорой — на самом краю игрок формально оказывается
|
||||
/// за габаритом на всю высоту своего габарита. Допуск в 128 юнитов это
|
||||
/// покрывает и при этом намного меньше расстояния, на котором «край карты»
|
||||
/// ещё можно спутать с небом.
|
||||
const SKY_BOUNDS_MARGIN: f32 = 128.0;
|
||||
|
||||
/// Рендерер мира.
|
||||
pub struct WorldRenderer {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
|
|
@ -29,6 +38,11 @@ pub struct WorldRenderer {
|
|||
only_material: Option<usize>,
|
||||
sky: Option<SkyResources>,
|
||||
sky_mesh: Option<SkyMeshResources>,
|
||||
/// Границы проходимой части карты — за ними небо не рисуется.
|
||||
bounds_min: Vec3,
|
||||
bounds_max: Vec3,
|
||||
/// Рисовать ли небо в этом кадре.
|
||||
sky_visible: bool,
|
||||
text: TextResources,
|
||||
/// Источники света карты — нужны каждый кадр для отбора по близости.
|
||||
lights: Vec<engine::world::Light>,
|
||||
|
|
@ -930,11 +944,30 @@ impl WorldRenderer {
|
|||
only_material: None,
|
||||
sky,
|
||||
sky_mesh,
|
||||
bounds_min: world.bounds_min,
|
||||
bounds_max: world.bounds_max,
|
||||
// До первого кадра считаем небо видимым: иначе окно будет пустым,
|
||||
// пока не отрисовался хоть один кадр.
|
||||
sky_visible: true,
|
||||
text,
|
||||
lights: world.lights.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Небо рисуется только внутри габаритов карты.
|
||||
///
|
||||
/// Проверяется каждый кадр, а не один раз при загрузке: игрок уходит за
|
||||
/// край карты уже во время игры, и там небо обязано исчезнуть, иначе
|
||||
/// границу карты не видно.
|
||||
pub fn update_sky_visibility(&mut self, camera_pos: Vec3) {
|
||||
self.sky_visible = engine::inside_bounds(
|
||||
self.bounds_min,
|
||||
self.bounds_max,
|
||||
camera_pos,
|
||||
SKY_BOUNDS_MARGIN,
|
||||
);
|
||||
}
|
||||
|
||||
/// Диагностика: рисовать только один материал.
|
||||
pub fn set_only_material(&mut self, index: Option<usize>) {
|
||||
self.only_material = index;
|
||||
|
|
@ -1057,7 +1090,9 @@ impl WorldRenderer {
|
|||
|
||||
// 2D-скайбокс — фон, до мира (как в движке: скайбокс рисуется первым,
|
||||
// мир перекрывает его в непрозрачных пикселях).
|
||||
if let Some(sky) = &self.sky {
|
||||
if self.sky_visible
|
||||
&& let Some(sky) = &self.sky
|
||||
{
|
||||
pass.set_pipeline(&sky.pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, sky.vertex_buffer.slice(..));
|
||||
|
|
@ -1074,7 +1109,9 @@ impl WorldRenderer {
|
|||
|
||||
// 3D-скайбокс — поверх 2D-фона, но под миром (без записи глубины,
|
||||
// поэтому стены карты всегда перекрывают его).
|
||||
if let Some(sky) = &self.sky_mesh {
|
||||
if self.sky_visible
|
||||
&& let Some(sky) = &self.sky_mesh
|
||||
{
|
||||
pass.set_pipeline(&sky.pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
pass.set_bind_group(2, &sky.params_bind_group, &[]);
|
||||
|
|
@ -1179,6 +1216,7 @@ impl WorldRenderer {
|
|||
|
||||
let (width, height) = gfx.size();
|
||||
let aspect = width as f32 / height.max(1) as f32;
|
||||
self.update_sky_visibility(camera.pos);
|
||||
self.update_camera(&gfx.queue, &camera.view_proj(aspect), camera.pos);
|
||||
self.update_lights(&gfx.queue, camera.pos);
|
||||
self.upload_text(&gfx.queue, width, height);
|
||||
|
|
|
|||
|
|
@ -1067,3 +1067,61 @@ velocity += wishdir * accelspeed
|
|||
2000 юнитов вслепую вдоль +x, но на gm_construct там стена на x = 992 (288
|
||||
юнитов от спавна). Ожидание теста было неверно, а не игрок; тест удалён, его
|
||||
содержательно корректная часть покрыта `real_map_movement`.
|
||||
|
||||
## Часть E6. Небо за границами карты (сделано)
|
||||
|
||||
### E6.1. Жалоба и измерение
|
||||
|
||||
> «если игрок выходит за карту, то скайбокс и 3D-скайбокс не должны рендериться
|
||||
> (иначе не понятны границы карты)»
|
||||
|
||||
Причина видна прямо в шейдере 3D-скайбокса:
|
||||
|
||||
```wgsl
|
||||
let world = camera.pos.xyz + (in.pos - sky.origin_scale.xyz) * sky.origin_scale.w;
|
||||
```
|
||||
|
||||
Геометрия неба размещается **относительно камеры**, поэтому небо всегда
|
||||
заливает весь фон: куда ни вылети, оно рядом. Пределы карты на фоне не видно
|
||||
не из-за того, что неба «слишком много», а из-за того, что оно не привязано к
|
||||
миру вовсе.
|
||||
|
||||
Замер на gm_construct:
|
||||
|
||||
| | значение |
|
||||
|---|---|
|
||||
| Габариты геометрии карты | `(-5640, -4576, -960) .. (2168, 6464, 2976)` |
|
||||
| Габариты коллизии | `(-15360, -15360, -1024) .. (15872, 15360, 15359)` |
|
||||
| `sky_camera` | `(-1428, 1645, 10991)`, масштаб 1/16 |
|
||||
| Границы 3D-скайбокса | `(-14848, -14336, 10654) .. (15360, 14848, 12520)` |
|
||||
| 2D-скайбокс (`SKYBOX`) | отсутствует, только 3D |
|
||||
| Спавн | `(704, 132, -143)` |
|
||||
|
||||
Видно, почему габариты коллизии не годятся: они включают и 3D-небо, и далёкие
|
||||
террасы. Основание — габариты **геометрии карты** (`World::bounds_min/max`),
|
||||
они же считаются по вершинам видимых поверхностей.
|
||||
|
||||
### E6.2. Решение
|
||||
|
||||
`World::inside_bounds(pos, margin)` и свободная функция `world::inside_bounds` —
|
||||
одна реализация, которой пользуются и движок, и рендерер (рендерер копирует
|
||||
габариты при загрузке, а не хранит ссылку на мир).
|
||||
|
||||
`WorldRenderer::update_sky_visibility` пересчитывает видимость **каждый кадр**
|
||||
из позиции камеры: игрок уходит за край уже во время игры. Проверка идёт по всем
|
||||
трём осям сразу: под картой небо тоже исчезает, а не только сбоку.
|
||||
|
||||
`SKY_BOUNDS_MARGIN = 128` — допуск наружу. Габариты считаются по вершинам, а
|
||||
камера стоит на высоте глаз (около 64 юнитов) над опорой, так что стоящий на самом
|
||||
краю игрок формально за габаритом на весь свой габарит.
|
||||
|
||||
### E6.3. Тесты
|
||||
|
||||
- `sky_is_visible_only_inside_the_map` — юнит на сам предикат, включая ровно на
|
||||
границе с допуском и на единицу за ней.
|
||||
- `real_map_sky_bounds` (`--ignored`) — на настоящей карте: спавн внутри, за
|
||||
край по любой из шести осей небо гаснет.
|
||||
|
||||
Оба прохода неба (2D-фон и 3D-геометрия) гасятся одним и тем же флагом: иначе
|
||||
при вылете за край остался бы виден 3D-неб, а это ровно то, что мешало увидеть
|
||||
границу.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue