Зеркало func_reflective_glass: отражённая камера и клип по плоскости
Some checks are pending
CI / checks (push) Waiting to run
Some checks are pending
CI / checks (push) Waiting to run
- сборка мира: модель зеркала помечается материалом-меткой `<mirror>` до резолва материала (у reflectiveglass001 нет VTF, иначе грань выпадала), из её граней строится плоскость MirrorPlane (origin, normal); - рендер: проход отражённой камерой в текстуру Rgba16Float, клип по плоскости зеркала (vec4 в uniform камеры, discard в шейдере), грань зеркала сэмплирует отражение по экранным координатам; - отражение привязано на слот лайтмапы (группа 3) отдельным bind group: у устройства max_bind_groups = 4, пятая группа не создаётся; - обычный проход пропускает зеркальные батчи (иначе чекер-заглушка перекрывала отражение), зеркало рисуется после непрозрачного мира; - найден и исправлен пропуск слота `sun` в Camera у world.wgsl (из-за него fog_color читался со сдвигом). Проверено: 25 сьютов и 29 картовых тестов зелёные; UV-градиент на грани подтвердил плоскость и шейдер, отражение показывает стену и потолок.
This commit is contained in:
parent
fc2e981bfc
commit
b564de5b25
6 changed files with 420 additions and 15 deletions
|
|
@ -49,6 +49,7 @@ fn empty_world() -> World {
|
|||
prop_meshes: Vec::new(),
|
||||
physics: engine::physics::PhysicsWorld::new(),
|
||||
home_origins: Vec::new(),
|
||||
mirrors: Vec::new(),
|
||||
light_samples: Vec::new(),
|
||||
spawn: Vec3::ZERO,
|
||||
spawn_yaw: 0.0,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ struct BrushModels {
|
|||
/// Диапазоны граней: `(firstface, numfaces, смещение)`. Смещение `None`
|
||||
/// означает, что модель не рисуется.
|
||||
ranges: Vec<(u32, u32, Option<Vec3>)>,
|
||||
/// Диапазоны `func_reflective_glass`: эти грани рисуются отражением.
|
||||
mirror_ranges: Vec<(u32, u32, Vec3)>,
|
||||
}
|
||||
|
||||
impl BrushModels {
|
||||
|
|
@ -78,6 +80,7 @@ impl BrushModels {
|
|||
"func_clip_vphysics",
|
||||
];
|
||||
let mut offsets: HashMap<usize, Option<Vec3>> = HashMap::new();
|
||||
let mut mirror_numbers: HashMap<usize, Vec3> = HashMap::new();
|
||||
for entity in entities {
|
||||
let Some(model) = entity.get_str("model") else {
|
||||
continue;
|
||||
|
|
@ -88,6 +91,9 @@ impl BrushModels {
|
|||
else {
|
||||
continue;
|
||||
};
|
||||
let is_mirror = entity
|
||||
.get_str("classname")
|
||||
.is_some_and(|class| class.eq_ignore_ascii_case("func_reflective_glass"));
|
||||
let visible = entity.get_str("classname").is_some_and(|class| {
|
||||
!class.starts_with("trigger_")
|
||||
&& !HIDDEN.iter().any(|h| h.eq_ignore_ascii_case(class))
|
||||
|
|
@ -102,15 +108,20 @@ impl BrushModels {
|
|||
.and_then(parse_vec3)
|
||||
.unwrap_or(Vec3::ZERO);
|
||||
offsets.insert(number, visible.then_some(offset));
|
||||
if is_mirror {
|
||||
mirror_numbers.insert(number, offset);
|
||||
}
|
||||
}
|
||||
|
||||
let Ok(models) = bsp.lump(formats::bsp::lump::MODELS) else {
|
||||
return BrushModels {
|
||||
ranges: vec![(0, u32::MAX, Some(Vec3::ZERO))],
|
||||
mirror_ranges: Vec::new(),
|
||||
};
|
||||
};
|
||||
let models = models.into_owned();
|
||||
let mut ranges: Vec<(u32, u32, Option<Vec3>)> = Vec::new();
|
||||
let mut mirror_ranges: Vec<(u32, u32, Vec3)> = Vec::new();
|
||||
for chunk in models.chunks_exact(48) {
|
||||
let read = |range: core::ops::Range<usize>| -> u32 {
|
||||
let mut bytes = [0u8; 4];
|
||||
|
|
@ -125,6 +136,12 @@ impl BrushModels {
|
|||
.copied()
|
||||
.unwrap_or(Some(Vec3::ZERO));
|
||||
ranges.push((first, num, offset));
|
||||
let number = ranges.len() - 1;
|
||||
if let Some(mirror_offset) = mirror_numbers.get(&number)
|
||||
&& offset.is_some()
|
||||
{
|
||||
mirror_ranges.push((first, num, *mirror_offset));
|
||||
}
|
||||
}
|
||||
if ranges.is_empty() {
|
||||
// Нет данных о моделях — ведём себя как раньше, весь `LUMP_FACES`.
|
||||
|
|
@ -135,11 +152,22 @@ impl BrushModels {
|
|||
visible = ranges.iter().filter(|r| r.2.is_some()).count(),
|
||||
"модели карты: статический мир + браш-сущности со своим смещением"
|
||||
);
|
||||
BrushModels { ranges }
|
||||
BrushModels {
|
||||
ranges,
|
||||
mirror_ranges,
|
||||
}
|
||||
}
|
||||
|
||||
/// Смещение для грани: `Some(offset)` — рисуем (ноль для мира), `None` —
|
||||
/// сущность невидима. Грани вне любых диапазонов считаем частью мира.
|
||||
/// Грань принадлежит `func_reflective_glass` — рисуется отражением.
|
||||
fn is_mirror_face(&self, face_index: usize) -> bool {
|
||||
let index = face_index as u32;
|
||||
self.mirror_ranges
|
||||
.iter()
|
||||
.any(|(first, num, _)| index >= *first && index < *first + *num)
|
||||
}
|
||||
|
||||
fn offset_for(&self, face_index: usize) -> Option<Vec3> {
|
||||
let index = face_index as u64;
|
||||
for (first, num, offset) in &self.ranges {
|
||||
|
|
@ -150,6 +178,9 @@ impl BrushModels {
|
|||
Some(Vec3::ZERO)
|
||||
}
|
||||
}
|
||||
/// Имя материала-метки зеркала (рендер рисует такие батчи отражением).
|
||||
pub const MIRROR_MATERIAL: &str = "<mirror>";
|
||||
|
||||
const SURF_HINT: i32 = 0x100;
|
||||
const SURF_SKIP: i32 = 0x200;
|
||||
|
||||
|
|
@ -210,6 +241,18 @@ pub struct WorldVertex {
|
|||
pub lightmap_uv: [f32; 2],
|
||||
}
|
||||
|
||||
/// Плоскость зеркала (`func_reflective_glass`).
|
||||
///
|
||||
/// Рендер строит отражённую камеру относительно неё и рисует грань зеркала
|
||||
/// текстурой отражения (A27).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct MirrorPlane {
|
||||
/// Точка на плоскости (центр зеркала).
|
||||
pub origin: Vec3,
|
||||
/// Нормаль плоскости (наружу, в сторону комнаты).
|
||||
pub normal: Vec3,
|
||||
}
|
||||
|
||||
/// Атлас запечённого света карты.
|
||||
///
|
||||
/// Раньше свет брался в вершинах: на больших гранях (пол тоннеля, бетонная
|
||||
|
|
@ -604,6 +647,8 @@ pub struct World {
|
|||
pub physics: crate::physics::PhysicsWorld,
|
||||
/// Исходные позиции пропов из карты — куда их возвращать при падении.
|
||||
pub home_origins: Vec<Vec3>,
|
||||
/// Плоскости зеркал карты (`func_reflective_glass`).
|
||||
pub mirrors: Vec<MirrorPlane>,
|
||||
/// Запечённый свет по центрам граней: `(точка, линейный цвет)`.
|
||||
///
|
||||
/// Модели берут отсюда ambient в своей точке — тёмный угол затемняет проп,
|
||||
|
|
@ -675,6 +720,7 @@ pub fn build_world(bsp: &Bsp, vfs: &VirtualFs) -> Result<World, WorldError> {
|
|||
prop_meshes: Vec::new(),
|
||||
physics: crate::physics::PhysicsWorld::new(),
|
||||
home_origins: Vec::new(),
|
||||
mirrors: Vec::new(),
|
||||
light_samples: Vec::new(),
|
||||
};
|
||||
|
||||
|
|
@ -781,6 +827,43 @@ pub fn build_world(bsp: &Bsp, vfs: &VirtualFs) -> Result<World, WorldError> {
|
|||
// со смещением на `origin`, невидимые (клипы, триггеры, отключённые) — нет.
|
||||
let brush_models = BrushModels::collect(bsp, &entities);
|
||||
|
||||
// Плоскости зеркал: берём самую большую грань каждого
|
||||
// `func_reflective_glass` (у кисти зеркала она и есть отражающая).
|
||||
for (first, num, offset) in &brush_models.mirror_ranges {
|
||||
let mut best: Option<(i16, Vec3, Vec3)> = None;
|
||||
let end = (*first + *num).min(geometry.faces.len() as u32);
|
||||
for face_index in *first..end {
|
||||
let face = &geometry.faces[face_index as usize];
|
||||
if face.num_edges < 3 {
|
||||
continue;
|
||||
}
|
||||
let Ok(indices) = geometry.face_vertex_indices(face_index as usize) else {
|
||||
continue;
|
||||
};
|
||||
let mut center = Vec3::ZERO;
|
||||
let mut count = 0.0f32;
|
||||
for &index in &indices {
|
||||
if let Some(vertex) = geometry.vertices.get(index as usize) {
|
||||
center += Vec3::new(vertex.pos[0], vertex.pos[1], vertex.pos[2]) + *offset;
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
if count == 0.0 {
|
||||
continue;
|
||||
}
|
||||
if best.is_none_or(|(edges, _, _)| face.num_edges > edges) {
|
||||
best = Some((face.num_edges, center / count, face_normal(face, &geometry)));
|
||||
}
|
||||
}
|
||||
if let Some((_, origin, normal)) = best {
|
||||
world.mirrors.push(MirrorPlane { origin, normal });
|
||||
}
|
||||
}
|
||||
if !world.mirrors.is_empty() {
|
||||
tracing::info!(mirrors = world.mirrors.len(), "зеркала карты");
|
||||
}
|
||||
|
||||
let mut mirror_material: Option<usize> = None;
|
||||
let mut resolved: Vec<Option<Resolved>> = vec![None; texdata.len()];
|
||||
let mut texture_budget_left = TEXTURE_BUDGET;
|
||||
let checker = checker_texture("<заглушка>");
|
||||
|
|
@ -815,9 +898,31 @@ pub fn build_world(bsp: &Bsp, vfs: &VirtualFs) -> Result<World, WorldError> {
|
|||
&checker,
|
||||
&mut world.stats,
|
||||
);
|
||||
let Resolved::Material(material) = resolved else {
|
||||
world.stats.skipped += 1;
|
||||
continue;
|
||||
// Грань зеркала рисуется отражением: помечаем её отдельным материалом,
|
||||
// рендер узнаёт его по имени. Делаем это ДО проверки материала: у
|
||||
// `reflectiveglass001` в игре нет VTF, и материал не резолвится —
|
||||
// иначе грань зеркала просто выпадала бы из мира.
|
||||
let material = if brush_models.is_mirror_face(face_index) {
|
||||
match mirror_material {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
let index = world.materials.len();
|
||||
world.materials.push(Material {
|
||||
name: MIRROR_MATERIAL.to_string(),
|
||||
texture: checker.clone(),
|
||||
fallback: false,
|
||||
water: None,
|
||||
});
|
||||
mirror_material = Some(index);
|
||||
index
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let Resolved::Material(material) = resolved else {
|
||||
world.stats.skipped += 1;
|
||||
continue;
|
||||
};
|
||||
material
|
||||
};
|
||||
|
||||
let vertex_indices = geometry
|
||||
|
|
@ -5037,6 +5142,7 @@ mod tests {
|
|||
bounds_half_extents: Vec3::ZERO,
|
||||
}],
|
||||
model_instances: Vec::new(),
|
||||
mirrors: Vec::new(),
|
||||
moved_ranges: Vec::new(),
|
||||
moved_index_ranges: Vec::new(),
|
||||
props: vec![PropInstance {
|
||||
|
|
@ -5103,6 +5209,7 @@ mod tests {
|
|||
bounds_half_extents: Vec3::ZERO,
|
||||
}],
|
||||
model_instances: Vec::new(),
|
||||
mirrors: Vec::new(),
|
||||
moved_ranges: Vec::new(),
|
||||
moved_index_ranges: Vec::new(),
|
||||
props: vec![PropInstance {
|
||||
|
|
@ -5166,6 +5273,7 @@ mod tests {
|
|||
lightmap: None,
|
||||
entities: crate::entity::EntityWorld::new(),
|
||||
model_instances: Vec::new(),
|
||||
mirrors: Vec::new(),
|
||||
moved_ranges: Vec::new(),
|
||||
moved_index_ranges: Vec::new(),
|
||||
props: Vec::new(),
|
||||
|
|
@ -5222,6 +5330,7 @@ mod tests {
|
|||
lightmap: None,
|
||||
entities: crate::entity::EntityWorld::new(),
|
||||
model_instances: Vec::new(),
|
||||
mirrors: Vec::new(),
|
||||
moved_ranges: Vec::new(),
|
||||
moved_index_ranges: Vec::new(),
|
||||
props: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -1455,5 +1455,11 @@ fn real_map_visual_entities() {
|
|||
eprintln!("color_correction: filename={filename}");
|
||||
}
|
||||
}
|
||||
for mirror in &world.mirrors {
|
||||
eprintln!(
|
||||
"зеркало: точка {:?}, нормаль {:?}",
|
||||
mirror.origin, mirror.normal
|
||||
);
|
||||
}
|
||||
assert!(!classes.is_empty(), "сущностей нет");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,16 @@ struct Camera {
|
|||
pos: vec4<f32>,
|
||||
/// Секунды с запуска — используется шейдером воды.
|
||||
time: vec4<f32>,
|
||||
/// Направление на солнце (заполняется в uniform, шейдер мира его не
|
||||
/// использует — источник света идёт из `Lights`, но слот обязан быть).
|
||||
sun: vec4<f32>,
|
||||
/// Цвет тумана карты.
|
||||
fog_color: vec4<f32>,
|
||||
/// (`start`, `end`, `max_density`, признак «туман есть»).
|
||||
fog_params: vec4<f32>,
|
||||
/// Плоскость отсечения прохода отражения: xyz — нормаль, w — смещение.
|
||||
/// `x == 0` — отсечения нет (основной проход).
|
||||
mirror_clip: vec4<f32>,
|
||||
};
|
||||
|
||||
const MAX_SUNS: u32 = 4u;
|
||||
|
|
@ -162,6 +168,13 @@ fn attenuation(dist: f32, radius: f32) -> f32 {
|
|||
|
||||
/// Общий расчёт освещения: `baked` — запечённый свет (из атласа или вершин).
|
||||
fn shade(in: VsOut, baked: vec3<f32>) -> vec4<f32> {
|
||||
// Проход отражения: всё, что за плоскостью зеркала, не рисуем — иначе
|
||||
// отражение показывает стену, на которой висит зеркало.
|
||||
if (camera.mirror_clip.x != 0.0
|
||||
&& dot(in.world_pos, camera.mirror_clip.xyz) < camera.mirror_clip.w)
|
||||
{
|
||||
discard;
|
||||
}
|
||||
let tex = textureSample(base_texture, base_sampler, in.uv);
|
||||
let n = normalize(in.normal);
|
||||
|
||||
|
|
@ -257,3 +270,16 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|||
fn fs_main_vertex(in: VsOut) -> @location(0) vec4<f32> {
|
||||
return shade(in, in.light);
|
||||
}
|
||||
|
||||
/// Грань зеркала: берёт отражение по экранным координатам.
|
||||
///
|
||||
/// Текстура отражения нарисована отражённой камерой с той же проекцией, что и
|
||||
/// кадр. У зеркала на слоте лайтмапы (группа 3) привязана текстура отражения,
|
||||
/// поэтому сэмплируем её через те же binding'и 0/1 — отдельная группа не нужна
|
||||
/// (у устройства лимит 4 группы, и все заняты).
|
||||
@fragment
|
||||
fn fs_mirror(in: VsOut) -> @location(0) vec4<f32> {
|
||||
let dims = vec2<f32>(textureDimensions(lightmap_texture));
|
||||
let uv = in.clip.xy / dims;
|
||||
return vec4<f32>(textureSample(lightmap_texture, lightmap_sampler, uv).rgb, 1.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,17 @@ pub struct WorldRenderer {
|
|||
fog: Option<engine::world::Fog>,
|
||||
/// Отладочный слой коллизий (создаётся, только если его включат).
|
||||
debug_lines: Option<crate::debug_lines::DebugLines>,
|
||||
/// Плоскость зеркала карты (если есть `func_reflective_glass`).
|
||||
mirror: Option<engine::world::MirrorPlane>,
|
||||
/// Текстура отражения: в неё рисуется мир отражённой камерой.
|
||||
mirror_texture: Option<wgpu::Texture>,
|
||||
mirror_view: Option<wgpu::TextureView>,
|
||||
/// Конвейер грани зеркала: сэмплирует отражение по экранным координатам.
|
||||
mirror_pipeline: wgpu::RenderPipeline,
|
||||
mirror_bind_group_layout: wgpu::BindGroupLayout,
|
||||
mirror_bind_group: Option<wgpu::BindGroup>,
|
||||
/// Батчи-зеркала (материал-метка `<mirror>`), по индексу батча.
|
||||
mirror_batches: Vec<bool>,
|
||||
/// HDR-конвейер: мир в Rgba16Float, затем экспозиция, ACES и bloom.
|
||||
post: crate::post::PostProcess,
|
||||
}
|
||||
|
|
@ -393,7 +404,7 @@ impl WorldRenderer {
|
|||
label: Some("camera uniform"),
|
||||
// mat4 (64) + vec4 позиции + vec4 времени + vec4 солнца + vec4 тумана
|
||||
// + vec4 параметров тумана
|
||||
size: 144,
|
||||
size: 160,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
|
@ -739,6 +750,47 @@ impl WorldRenderer {
|
|||
cache: None,
|
||||
});
|
||||
|
||||
// Грань зеркала: сэмплирует текстуру отражения по экранным координатам.
|
||||
let mirror_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("mirror pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Some(vertex_layout.clone())],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_mirror"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: crate::post::HDR_FORMAT,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Cw,
|
||||
cull_mode: None,
|
||||
unclipped_depth: false,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: DEPTH_FORMAT,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::Less),
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
// Полупрозрачная вода (Water-шейдер): после мира, с блендингом.
|
||||
let water_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("water shader"),
|
||||
|
|
@ -1202,6 +1254,50 @@ impl WorldRenderer {
|
|||
|
||||
let post = crate::post::PostProcess::new(device, queue, gfx.color_format(), width, height);
|
||||
|
||||
// Зеркало: текстура отражения по размеру экрана и bind group.
|
||||
// Раскладка та же, что у лайтмапы (группа 3, texture+sampler): у
|
||||
// устройства лимит 4 bind-группы, и все уже заняты.
|
||||
let mirror_bind_group_layout = lightmap_layout.clone();
|
||||
let mirror = world.mirrors.first().copied();
|
||||
let (mirror_texture, mirror_view, mirror_bind_group) = match mirror {
|
||||
Some(_) => {
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("mirror reflection"),
|
||||
size: wgpu::Extent3d {
|
||||
width: width.max(1),
|
||||
height: height.max(1),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: crate::post::HDR_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let bind = mirror_bind_group(device, &mirror_bind_group_layout, &view, &sampler);
|
||||
(Some(texture), Some(view), Some(bind))
|
||||
}
|
||||
None => (None, None, None),
|
||||
};
|
||||
let mirror_batches: Vec<bool> = world
|
||||
.batches
|
||||
.iter()
|
||||
.map(|batch| {
|
||||
world
|
||||
.materials
|
||||
.get(batch.material)
|
||||
.is_some_and(|material| material.name == engine::world::MIRROR_MATERIAL)
|
||||
})
|
||||
.collect();
|
||||
|
||||
tracing::info!(
|
||||
mirrors = world.mirrors.len(),
|
||||
mirror_batches = mirror_batches.iter().filter(|m| **m).count(),
|
||||
"зеркало: ресурсы"
|
||||
);
|
||||
Ok(Self {
|
||||
pipeline,
|
||||
water_pipeline,
|
||||
|
|
@ -1244,6 +1340,13 @@ impl WorldRenderer {
|
|||
camera_layout: camera_layout.clone(),
|
||||
color_format: gfx.color_format(),
|
||||
debug_lines: None,
|
||||
mirror,
|
||||
mirror_texture,
|
||||
mirror_view,
|
||||
mirror_pipeline,
|
||||
mirror_bind_group_layout,
|
||||
mirror_bind_group,
|
||||
mirror_batches,
|
||||
post,
|
||||
})
|
||||
}
|
||||
|
|
@ -1382,6 +1485,16 @@ impl WorldRenderer {
|
|||
}
|
||||
self.batches = world.batches.clone();
|
||||
self.water_materials = world.materials.iter().map(|m| m.water.is_some()).collect();
|
||||
self.mirror_batches = world
|
||||
.batches
|
||||
.iter()
|
||||
.map(|batch| {
|
||||
world
|
||||
.materials
|
||||
.get(batch.material)
|
||||
.is_some_and(|material| material.name == engine::world::MIRROR_MATERIAL)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Растит буферы мира под новые вершины и индексы.
|
||||
|
|
@ -1494,6 +1607,32 @@ impl WorldRenderer {
|
|||
self.depth_view = view;
|
||||
self.post.resize(&gfx.device, &gfx.queue, width, height);
|
||||
self.size = (width, height);
|
||||
if self.mirror.is_some() {
|
||||
let texture = gfx.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("mirror reflection"),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: crate::post::HDR_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
self.mirror_bind_group = Some(mirror_bind_group(
|
||||
&gfx.device,
|
||||
&self.mirror_bind_group_layout,
|
||||
&view,
|
||||
&self.material_sampler,
|
||||
));
|
||||
self.mirror_texture = Some(texture);
|
||||
self.mirror_view = Some(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// Число индексов и батчей (для логов).
|
||||
|
|
@ -1505,10 +1644,16 @@ impl WorldRenderer {
|
|||
)
|
||||
}
|
||||
|
||||
fn update_camera(&self, queue: &wgpu::Queue, view_proj: &Mat4, pos: math::Vec3) {
|
||||
// 144 байта: матрица (64) + позиция (16) + время (16) + солнце (16)
|
||||
// + туман (16) + параметры тумана (16).
|
||||
let mut bytes = [0u8; 144];
|
||||
fn update_camera(
|
||||
&self,
|
||||
queue: &wgpu::Queue,
|
||||
view_proj: &Mat4,
|
||||
pos: math::Vec3,
|
||||
mirror_clip: Option<(math::Vec3, f32)>,
|
||||
) {
|
||||
// 160 байт: матрица (64) + позиция (16) + время (16) + солнце (16)
|
||||
// + туман (16) + параметры тумана (16) + плоскость отражения (16).
|
||||
let mut bytes = [0u8; 160];
|
||||
for (i, value) in view_proj.0.iter().enumerate() {
|
||||
bytes[i * 4..i * 4 + 4].copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
|
@ -1552,6 +1697,12 @@ impl WorldRenderer {
|
|||
let offset = 128 + i * 4;
|
||||
bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
if let Some((normal, distance)) = mirror_clip {
|
||||
for (i, value) in [normal.x, normal.y, normal.z, distance].iter().enumerate() {
|
||||
let offset = 144 + i * 4;
|
||||
bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
}
|
||||
queue.write_buffer(&self.camera_buffer, 0, &bytes);
|
||||
}
|
||||
|
||||
|
|
@ -1666,7 +1817,12 @@ impl WorldRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
fn draw(&self, encoder: &mut wgpu::CommandEncoder, color_view: &wgpu::TextureView) {
|
||||
fn draw(
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
color_view: &wgpu::TextureView,
|
||||
mirror_pass: bool,
|
||||
) {
|
||||
// Карта теней считается до основного пасса: вершины читают её в шейдере.
|
||||
self.draw_shadow(encoder);
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
|
|
@ -1786,12 +1942,16 @@ impl WorldRenderer {
|
|||
pass.set_bind_group(3, &self.lightmap_bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
|
||||
pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||
for batch in &self.batches {
|
||||
for (index, batch) in self.batches.iter().enumerate() {
|
||||
if let Some(only) = self.only_material {
|
||||
if batch.material != only {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Грань зеркала рисуется отдельным конвейером ниже.
|
||||
if self.mirror_batches.get(index).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let Some(bind_group) = self.material_bind_groups.get(batch.material) else {
|
||||
continue;
|
||||
};
|
||||
|
|
@ -1809,6 +1969,30 @@ impl WorldRenderer {
|
|||
);
|
||||
}
|
||||
|
||||
// Грань зеркала: в основном проходе рисуем отражение, в проходе
|
||||
// отражения — пропускаем (иначе зеркало отражает само себя).
|
||||
if !mirror_pass && self.mirror_view.is_some() {
|
||||
if let Some(bind) = &self.mirror_bind_group {
|
||||
pass.set_pipeline(&self.mirror_pipeline);
|
||||
// На слоте лайтмапы — текстура отражения (см. fs_mirror).
|
||||
pass.set_bind_group(3, bind, &[]);
|
||||
for (index, batch) in self.batches.iter().enumerate() {
|
||||
if !self.mirror_batches.get(index).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
pass.draw_indexed(
|
||||
batch.first_index..batch.first_index + batch.index_count,
|
||||
0,
|
||||
0..1,
|
||||
);
|
||||
}
|
||||
// Возвращаем конвейер мира и атлас лайтмапы: следующий код
|
||||
// (вода) ждёт их.
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(3, &self.lightmap_bind_group, &[]);
|
||||
}
|
||||
}
|
||||
|
||||
// Вода — полупрозрачно, поверх непрозрачного мира.
|
||||
pass.set_pipeline(&self.water_pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
|
|
@ -1901,7 +2085,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_camera(&gfx.queue, &camera.view_proj(aspect), camera.pos, None);
|
||||
self.update_lights(&gfx.queue, camera.pos);
|
||||
self.upload_text(&gfx.queue, width, height);
|
||||
|
||||
|
|
@ -1913,7 +2097,17 @@ impl WorldRenderer {
|
|||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("world frame"),
|
||||
});
|
||||
self.draw(&mut encoder, self.post.hdr_view());
|
||||
if let (Some(mirror), Some(mirror_view)) = (self.mirror, self.mirror_view.as_ref()) {
|
||||
let (view_proj, position) = mirror_camera(camera, mirror, aspect);
|
||||
let normal = mirror.normal.normalized();
|
||||
// Отсекаем всё за зеркалом: чуть впереди плоскости, чтобы не
|
||||
// поймать саму грань.
|
||||
let clip = Some((normal, normal.dot(mirror.origin) + 0.1));
|
||||
self.update_camera(&gfx.queue, &view_proj, position, clip);
|
||||
self.draw(&mut encoder, mirror_view, true);
|
||||
self.update_camera(&gfx.queue, &camera.view_proj(aspect), camera.pos, None);
|
||||
}
|
||||
self.draw(&mut encoder, self.post.hdr_view(), false);
|
||||
self.post.resolve(&mut encoder, &view);
|
||||
self.draw_text(&mut encoder, &view, wgpu::LoadOp::Load);
|
||||
gfx.queue.submit(std::iter::once(encoder.finish()));
|
||||
|
|
@ -1948,7 +2142,7 @@ impl WorldRenderer {
|
|||
let color_view = color.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let aspect = width as f32 / height.max(1) as f32;
|
||||
self.update_camera(&gfx.queue, &camera.view_proj(aspect), camera.pos);
|
||||
self.update_camera(&gfx.queue, &camera.view_proj(aspect), camera.pos, None);
|
||||
self.update_lights(&gfx.queue, camera.pos);
|
||||
self.upload_text(&gfx.queue, width, height);
|
||||
|
||||
|
|
@ -1956,7 +2150,15 @@ impl WorldRenderer {
|
|||
label: Some("capture"),
|
||||
});
|
||||
// HDR-проход мира, затем тон-маппинг в sRGB-текстуру захвата и оверлей.
|
||||
self.draw(&mut encoder, self.post.hdr_view());
|
||||
if let (Some(mirror), Some(mirror_view)) = (self.mirror, self.mirror_view.as_ref()) {
|
||||
let (view_proj, position) = mirror_camera(camera, mirror, aspect);
|
||||
let normal = mirror.normal.normalized();
|
||||
let clip = Some((normal, normal.dot(mirror.origin) + 0.1));
|
||||
self.update_camera(&gfx.queue, &view_proj, position, clip);
|
||||
self.draw(&mut encoder, mirror_view, true);
|
||||
self.update_camera(&gfx.queue, &camera.view_proj(aspect), camera.pos, None);
|
||||
}
|
||||
self.draw(&mut encoder, self.post.hdr_view(), false);
|
||||
self.post.resolve(&mut encoder, &color_view);
|
||||
self.draw_text(&mut encoder, &color_view, wgpu::LoadOp::Load);
|
||||
|
||||
|
|
@ -2078,3 +2280,42 @@ fn create_buffer(
|
|||
fn grow_capacity(capacity: usize, needed: usize) -> usize {
|
||||
capacity.max(4096).saturating_mul(2).max(needed).max(4096)
|
||||
}
|
||||
|
||||
/// Bind group отражения для грани зеркала.
|
||||
fn mirror_bind_group(
|
||||
device: &wgpu::Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
view: &wgpu::TextureView,
|
||||
sampler: &wgpu::Sampler,
|
||||
) -> wgpu::BindGroup {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("mirror bind group"),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// Камера, отражённая относительно плоскости зеркала.
|
||||
fn mirror_camera(
|
||||
camera: &FlyCamera,
|
||||
mirror: engine::world::MirrorPlane,
|
||||
aspect: f32,
|
||||
) -> (Mat4, Vec3) {
|
||||
let normal = mirror.normal.normalized();
|
||||
let reflect = |v: Vec3| v - normal * (2.0 * v.dot(normal));
|
||||
let position = camera.pos - normal * (2.0 * (camera.pos - mirror.origin).dot(normal));
|
||||
let forward = reflect(camera.forward());
|
||||
let up = reflect(Vec3::new(0.0, 0.0, 1.0));
|
||||
let view = Mat4::look_at_rh(position, position + forward, up);
|
||||
let proj = Mat4::perspective(camera.fov, aspect, camera.near, camera.far);
|
||||
(proj.mul(&view), position)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,28 @@ HL2: туда залит только `garrysmod/`, без `sourceengine/`. Пр
|
|||
`env_sprite`/glow нет, `env_skypaint` не читается (небо из sky-материалов),
|
||||
`env_tonemap_controller` не используется (экспозиция своя).
|
||||
|
||||
## 0.5. Зеркало (`func_reflective_glass`) — сделано
|
||||
|
||||
- в сборке мира модель зеркала помечается материалом-меткой `<mirror>`
|
||||
(грани рисуются отдельным конвейером); у `reflectiveglass001` в игре нет
|
||||
VTF, поэтому пометка ставится **до** резолва материала, иначе грань
|
||||
выпадала из мира;
|
||||
- из граней модели строится плоскость (`MirrorPlane { origin, normal }`):
|
||||
у gm_construct — точка (-2064, -2067, -286), нормаль (0,1,0);
|
||||
- рендер: отдельный проход отражённой камерой в текстуру Rgba16Float, клип
|
||||
по плоскости зеркала (плоскость едет в uniform камеры, `discard` в шейдере);
|
||||
грань зеркала сэмплирует отражение по экранным координатам. Группа 4 для
|
||||
отражения не подошла: `max_bind_groups = 4`, поэтому текстура отражения
|
||||
биндится на слот лайтмапы (группа 3) в отдельном bind group;
|
||||
- зеркало рисуется после непрозрачного мира (обычный проход пропускает
|
||||
зеркальные батчи, иначе чекер перекрывал отражение), тени от зеркала не
|
||||
считаются.
|
||||
|
||||
Проверено кадрами: UV-градиент на грани показал, что зеркальная плоскость
|
||||
занимает всю стену и шейдер работает; отражение показывает противоположную
|
||||
кирпичную стену и потолок. Дальше — сглаживание границы отражения и
|
||||
исключение зеркала из прохода теней.
|
||||
|
||||
## 1. Что просит пользователь (приоритеты)
|
||||
|
||||
**Ускоренный путь до Sandbox-UX** (его слова): «быстрее дойти до того, как смогу
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue