From b347bc380f06d0eee8b52eb9dbb3a3c9507e90b1 Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Sat, 24 Oct 2026 11:50:32 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=B5=D0=BA=D0=B0=D0=BB=D0=B8=20infodeca?= =?UTF-8?q?l:=20=D0=BA=D0=B2=D0=B0=D0=B4=D1=8B=20=D0=BD=D0=B0=20=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D0=B6=D0=B0=D0=B9=D1=88=D0=B5=D0=B9=20=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=20=D0=B8=20=D0=B0=D0=BB=D1=8C=D1=84=D0=B0-?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=85=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - сборка мира: для каждого infodecal берётся ближайшая плоскость грани (замер: у всех 19 декалей расстояние 0), центр проецируется на неё, квадрат строится по осям текстуры грани (32×32 юнита), материал — `decals/<имя>`; - рендер: материалы с префиксом `decals/` идут отдельным проходом с ALPHA_BLENDING, без записи глубины, после непрозрачного мира; новый шейдер `fs_decal` берёт цвет из обычного освещения, альфу — из текстуры; - декаль освещается ambient'ом в своей точке (`model_light_at`). Проверено кадром: граффити с прозрачностью лежит на кирпичной стене. 25 сьютов и 29 картовых тестов зелёные. --- crates/engine/src/world.rs | 129 ++++++++++++++++++++++++++++ crates/engine/tests/world_real.rs | 9 ++ crates/render/src/world.wgsl | 13 +++ crates/render/src/world_renderer.rs | 82 +++++++++++++++++- 4 files changed, 232 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/world.rs b/crates/engine/src/world.rs index 9474bbf..a5777ea 100644 --- a/crates/engine/src/world.rs +++ b/crates/engine/src/world.rs @@ -182,6 +182,12 @@ impl BrushModels { pub const MIRROR_MATERIAL: &str = ""; const SURF_HINT: i32 = 0x100; + +/// Полразмера квадрата декали (юниты). В Source размер задаёт материал; +/// у нас общий разумный размер для всех трафаретов. +const DECAL_HALF_SIZE: f32 = 16.0; +/// Бюджет текстур декалей (доля от общего бюджета мира). +const DECAL_TEXTURE_BUDGET: usize = 64 * 1024 * 1024; const SURF_SKIP: i32 = 0x200; /// Специальные материалы, которые не рендерим. @@ -1284,6 +1290,129 @@ pub fn build_world(bsp: &Bsp, vfs: &VirtualFs) -> Result { "источники света собраны" ); + // --- декали (infodecal) --- + // + // Source проецирует декаль на поверхность по трассировке. У нас есть + // геометрия граней, поэтому берём ближайшую плоскость, проецируем на неё + // центр декали и строим квадрат по осям текстуры грани. Декали рисуются + // с альфа-блендингом (материалы `decals/...` — отдельный проход рендера). + { + let mut decal_budget = DECAL_TEXTURE_BUDGET; + let mut decal_materials: HashMap = HashMap::new(); + let mut placed = 0usize; + for entity in &entities { + if !entity + .get_str("classname") + .is_some_and(|class| class.eq_ignore_ascii_case("infodecal")) + { + continue; + } + let Some(name) = entity.get_str("texture") else { + continue; + }; + let Some(origin) = entity.get_str("origin").and_then(parse_vec3) else { + continue; + }; + let name = name.replace('\\', "/"); + // Ближайшая грань по плоскости. + let mut best: Option<(f32, usize, Vec3, f32)> = None; + for (face_index, face) in geometry.faces.iter().enumerate() { + if face.num_edges < 3 || face.dispinfo >= 0 { + continue; + } + 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 distance = (origin.dot(normal) - plane.dist).abs(); + if best.is_none_or(|(best_distance, _, _, _)| distance < best_distance) { + best = Some((distance, face_index, normal, plane.dist)); + } + } + let Some((distance, face_index, normal, plane_dist)) = best else { + continue; + }; + // Декаль далеко от поверхности — не наша геометрия, пропускаем. + if distance > 8.0 || !normal.is_finite() { + continue; + } + let Some(face) = geometry.faces.get(face_index) else { + continue; + }; + let Some(texinfo) = geometry.texinfos.get(face.texinfo.max(0) as usize) else { + continue; + }; + // Оси квадрата — оси текстуры грани, спроецированные на плоскость. + let axis = |v: [f32; 4]| Vec3::new(v[0], v[1], v[2]); + let project = |v: Vec3| (v - normal * v.dot(normal)).normalized(); + let mut u = project(axis(texinfo.texture_vecs[0])); + let mut v = project(axis(texinfo.texture_vecs[1])); + if u.length_squared() < 1e-6 || v.length_squared() < 1e-6 { + u = project(Vec3::new(1.0, 0.0, 0.0)); + v = project(Vec3::new(0.0, 1.0, 0.0)); + if u.length_squared() < 1e-6 { + u = project(Vec3::new(0.0, 1.0, 0.0)); + v = project(Vec3::new(1.0, 0.0, 0.0)); + } + } + // Центр — проекция точки на плоскость, чуть наружу от поверхности. + let centre = origin - normal * (origin.dot(normal) - plane_dist) + normal * 0.2; + let half = DECAL_HALF_SIZE; + let corners = [ + centre - u * half - v * half, + centre + u * half - v * half, + centre + u * half + v * half, + centre - u * half + v * half, + ]; + let light = world.model_light_at(centre); + let first_vertex = world.vertices.len() as u32; + for (i, corner) in corners.iter().enumerate() { + let uv = match i { + 0 => [0.0, 1.0], + 1 => [1.0, 1.0], + 2 => [1.0, 0.0], + _ => [0.0, 0.0], + }; + world.vertices.push(WorldVertex { + pos: [corner.x, corner.y, corner.z], + normal: [normal.x, normal.y, normal.z], + uv, + light, + lightmap_uv: [-1.0, -1.0], + }); + } + let material = *decal_materials.entry(name.clone()).or_insert_with(|| { + let texture = match resolve_basetexture(vfs, &name, 0) { + BaseTexture::Texture(base) => load_texture(vfs, &base, &mut decal_budget), + _ => None, + }; + let texture = texture.unwrap_or_else(|| checker.clone()); + let index = world.materials.len(); + world.materials.push(Material { + name: name.clone(), + texture, + fallback: false, + water: None, + }); + index + }); + let first_index = world.indices.len() as u32; + world.indices.extend_from_slice(&[ + first_vertex, + first_vertex + 1, + first_vertex + 2, + first_vertex, + first_vertex + 2, + first_vertex + 3, + ]); + push_batch(&mut world, material, first_index); + placed += 1; + } + if placed > 0 { + tracing::info!(decals = placed, "декали карты"); + } + } + // --- экземпляры пропов --- // Пропы берутся ИЗ ПУЛА СУЩНОСТЕЙ, а не из сырого лумпа: сущность — // единственный источник истины, иначе физика и Lua видели бы другую diff --git a/crates/engine/tests/world_real.rs b/crates/engine/tests/world_real.rs index 0583957..16fd56c 100644 --- a/crates/engine/tests/world_real.rs +++ b/crates/engine/tests/world_real.rs @@ -1455,6 +1455,15 @@ fn real_map_visual_entities() { eprintln!("color_correction: filename={filename}"); } } + for entity in world.entities.iter() { + if entity.classname.eq_ignore_ascii_case("infodecal") { + eprintln!( + "infodecal: origin {:?}, texture {:?}", + entity.origin, + entity.key("texture") + ); + } + } for mirror in &world.mirrors { eprintln!( "зеркало: точка {:?}, нормаль {:?}", diff --git a/crates/render/src/world.wgsl b/crates/render/src/world.wgsl index a266a1f..1aa1677 100644 --- a/crates/render/src/world.wgsl +++ b/crates/render/src/world.wgsl @@ -283,3 +283,16 @@ fn fs_mirror(in: VsOut) -> @location(0) vec4 { let uv = in.clip.xy / dims; return vec4(textureSample(lightmap_texture, lightmap_sampler, uv).rgb, 1.0); } + +/// Декаль: цвет как у мира, альфа — из текстуры (трафареты надписей). +@fragment +fn fs_decal(in: VsOut) -> @location(0) vec4 { + let baked = select( + textureSample(lightmap_texture, lightmap_sampler, in.lightmap_uv).rgb, + in.light, + in.lightmap_uv.x < 0.0, + ); + let color = shade(in, baked); + let alpha = textureSample(base_texture, base_sampler, in.uv).a; + return vec4(color.rgb, alpha); +} diff --git a/crates/render/src/world_renderer.rs b/crates/render/src/world_renderer.rs index d08780f..0539037 100644 --- a/crates/render/src/world_renderer.rs +++ b/crates/render/src/world_renderer.rs @@ -102,6 +102,9 @@ pub struct WorldRenderer { mirror_bind_group: Option, /// Батчи-зеркала (материал-метка ``), по индексу батча. mirror_batches: Vec, + /// Батчи-декали (материалы `decals/...`) — рисуются с альфа-блендингом. + decal_batches: Vec, + decal_pipeline: wgpu::RenderPipeline, /// HDR-конвейер: мир в Rgba16Float, затем экспозиция, ACES и bloom. post: crate::post::PostProcess, } @@ -791,6 +794,47 @@ impl WorldRenderer { cache: None, }); + // Декали: тот же шейдер, но с альфа-блендингом и без записи глубины. + let decal_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("decal 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_decal"), + targets: &[Some(wgpu::ColorTargetState { + format: crate::post::HDR_FORMAT, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + 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(false), + depth_compare: Some(wgpu::CompareFunction::LessEqual), + 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"), @@ -1282,6 +1326,16 @@ impl WorldRenderer { } None => (None, None, None), }; + let decal_batches: Vec = world + .batches + .iter() + .map(|batch| { + world + .materials + .get(batch.material) + .is_some_and(|material| material.name.starts_with("decals/")) + }) + .collect(); let mirror_batches: Vec = world .batches .iter() @@ -1347,6 +1401,8 @@ impl WorldRenderer { mirror_bind_group_layout, mirror_bind_group, mirror_batches, + decal_batches, + decal_pipeline, post, }) } @@ -1948,10 +2004,13 @@ impl WorldRenderer { continue; } } - // Грань зеркала рисуется отдельным конвейером ниже. + // Грань зеркала и декали рисуются отдельными конвейерами ниже. if self.mirror_batches.get(index).copied().unwrap_or(false) { continue; } + if self.decal_batches.get(index).copied().unwrap_or(false) { + continue; + } let Some(bind_group) = self.material_bind_groups.get(batch.material) else { continue; }; @@ -1969,6 +2028,27 @@ impl WorldRenderer { ); } + // Декали: поверх непрозрачного мира, с альфа-блендингом, без записи + // глубины (иначе прозрачные края перекрывали бы друг друга). + if !mirror_pass { + pass.set_pipeline(&self.decal_pipeline); + for (index, batch) in self.batches.iter().enumerate() { + if !self.decal_batches.get(index).copied().unwrap_or(false) { + continue; + } + let Some(bind_group) = self.material_bind_groups.get(batch.material) else { + continue; + }; + pass.set_bind_group(1, bind_group, &[]); + pass.draw_indexed( + batch.first_index..batch.first_index + batch.index_count, + 0, + 0..1, + ); + } + pass.set_pipeline(&self.pipeline); + } + // Грань зеркала: в основном проходе рисуем отражение, в проходе // отражения — пропускаем (иначе зеркало отражает само себя). if !mirror_pass && self.mirror_view.is_some() {