diff --git a/crates/client/src/menu.rs b/crates/client/src/menu.rs index dcab340..4308dff 100644 --- a/crates/client/src/menu.rs +++ b/crates/client/src/menu.rs @@ -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, diff --git a/crates/engine/src/world.rs b/crates/engine/src/world.rs index c22c342..9474bbf 100644 --- a/crates/engine/src/world.rs +++ b/crates/engine/src/world.rs @@ -64,6 +64,8 @@ struct BrushModels { /// Диапазоны граней: `(firstface, numfaces, смещение)`. Смещение `None` /// означает, что модель не рисуется. ranges: Vec<(u32, u32, Option)>, + /// Диапазоны `func_reflective_glass`: эти грани рисуются отражением. + mirror_ranges: Vec<(u32, u32, Vec3)>, } impl BrushModels { @@ -78,6 +80,7 @@ impl BrushModels { "func_clip_vphysics", ]; let mut offsets: HashMap> = HashMap::new(); + let mut mirror_numbers: HashMap = 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)> = 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| -> 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 { 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 = ""; + 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, + /// Плоскости зеркал карты (`func_reflective_glass`). + pub mirrors: Vec, /// Запечённый свет по центрам граней: `(точка, линейный цвет)`. /// /// Модели берут отсюда ambient в своей точке — тёмный угол затемняет проп, @@ -675,6 +720,7 @@ pub fn build_world(bsp: &Bsp, vfs: &VirtualFs) -> Result { 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 { // со смещением на `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 = None; let mut resolved: Vec> = 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 { &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(), diff --git a/crates/engine/tests/world_real.rs b/crates/engine/tests/world_real.rs index a7bef7d..0583957 100644 --- a/crates/engine/tests/world_real.rs +++ b/crates/engine/tests/world_real.rs @@ -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(), "сущностей нет"); } diff --git a/crates/render/src/world.wgsl b/crates/render/src/world.wgsl index c2d6790..a266a1f 100644 --- a/crates/render/src/world.wgsl +++ b/crates/render/src/world.wgsl @@ -8,10 +8,16 @@ struct Camera { pos: vec4, /// Секунды с запуска — используется шейдером воды. time: vec4, + /// Направление на солнце (заполняется в uniform, шейдер мира его не + /// использует — источник света идёт из `Lights`, но слот обязан быть). + sun: vec4, /// Цвет тумана карты. fog_color: vec4, /// (`start`, `end`, `max_density`, признак «туман есть»). fog_params: vec4, + /// Плоскость отсечения прохода отражения: xyz — нормаль, w — смещение. + /// `x == 0` — отсечения нет (основной проход). + mirror_clip: vec4, }; const MAX_SUNS: u32 = 4u; @@ -162,6 +168,13 @@ fn attenuation(dist: f32, radius: f32) -> f32 { /// Общий расчёт освещения: `baked` — запечённый свет (из атласа или вершин). fn shade(in: VsOut, baked: vec3) -> vec4 { + // Проход отражения: всё, что за плоскостью зеркала, не рисуем — иначе + // отражение показывает стену, на которой висит зеркало. + 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 { fn fs_main_vertex(in: VsOut) -> @location(0) vec4 { return shade(in, in.light); } + +/// Грань зеркала: берёт отражение по экранным координатам. +/// +/// Текстура отражения нарисована отражённой камерой с той же проекцией, что и +/// кадр. У зеркала на слоте лайтмапы (группа 3) привязана текстура отражения, +/// поэтому сэмплируем её через те же binding'и 0/1 — отдельная группа не нужна +/// (у устройства лимит 4 группы, и все заняты). +@fragment +fn fs_mirror(in: VsOut) -> @location(0) vec4 { + let dims = vec2(textureDimensions(lightmap_texture)); + let uv = in.clip.xy / dims; + return vec4(textureSample(lightmap_texture, lightmap_sampler, uv).rgb, 1.0); +} diff --git a/crates/render/src/world_renderer.rs b/crates/render/src/world_renderer.rs index 88211a0..d08780f 100644 --- a/crates/render/src/world_renderer.rs +++ b/crates/render/src/world_renderer.rs @@ -91,6 +91,17 @@ pub struct WorldRenderer { fog: Option, /// Отладочный слой коллизий (создаётся, только если его включат). debug_lines: Option, + /// Плоскость зеркала карты (если есть `func_reflective_glass`). + mirror: Option, + /// Текстура отражения: в неё рисуется мир отражённой камерой. + mirror_texture: Option, + mirror_view: Option, + /// Конвейер грани зеркала: сэмплирует отражение по экранным координатам. + mirror_pipeline: wgpu::RenderPipeline, + mirror_bind_group_layout: wgpu::BindGroupLayout, + mirror_bind_group: Option, + /// Батчи-зеркала (материал-метка ``), по индексу батча. + mirror_batches: Vec, /// 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 = 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) +} diff --git a/docs/DIARY.md b/docs/DIARY.md index edec67c..7f30323 100644 --- a/docs/DIARY.md +++ b/docs/DIARY.md @@ -111,6 +111,28 @@ HL2: туда залит только `garrysmod/`, без `sourceengine/`. Пр `env_sprite`/glow нет, `env_skypaint` не читается (небо из sky-материалов), `env_tonemap_controller` не используется (экспозиция своя). +## 0.5. Зеркало (`func_reflective_glass`) — сделано + +- в сборке мира модель зеркала помечается материалом-меткой `` + (грани рисуются отдельным конвейером); у `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** (его слова): «быстрее дойти до того, как смогу