Декали infodecal: квады на ближайшей грани и альфа-проход
Some checks are pending
CI / checks (push) Waiting to run
Some checks are pending
CI / checks (push) Waiting to run
- сборка мира: для каждого infodecal берётся ближайшая плоскость грани (замер: у всех 19 декалей расстояние 0), центр проецируется на неё, квадрат строится по осям текстуры грани (32×32 юнита), материал — `decals/<имя>`; - рендер: материалы с префиксом `decals/` идут отдельным проходом с ALPHA_BLENDING, без записи глубины, после непрозрачного мира; новый шейдер `fs_decal` берёт цвет из обычного освещения, альфу — из текстуры; - декаль освещается ambient'ом в своей точке (`model_light_at`). Проверено кадром: граффити с прозрачностью лежит на кирпичной стене. 25 сьютов и 29 картовых тестов зелёные.
This commit is contained in:
parent
b564de5b25
commit
b347bc380f
4 changed files with 232 additions and 1 deletions
|
|
@ -182,6 +182,12 @@ impl BrushModels {
|
|||
pub const MIRROR_MATERIAL: &str = "<mirror>";
|
||||
|
||||
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<World, WorldError> {
|
|||
"источники света собраны"
|
||||
);
|
||||
|
||||
// --- декали (infodecal) ---
|
||||
//
|
||||
// Source проецирует декаль на поверхность по трассировке. У нас есть
|
||||
// геометрия граней, поэтому берём ближайшую плоскость, проецируем на неё
|
||||
// центр декали и строим квадрат по осям текстуры грани. Декали рисуются
|
||||
// с альфа-блендингом (материалы `decals/...` — отдельный проход рендера).
|
||||
{
|
||||
let mut decal_budget = DECAL_TEXTURE_BUDGET;
|
||||
let mut decal_materials: HashMap<String, usize> = 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 видели бы другую
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
"зеркало: точка {:?}, нормаль {:?}",
|
||||
|
|
|
|||
|
|
@ -283,3 +283,16 @@ fn fs_mirror(in: VsOut) -> @location(0) vec4<f32> {
|
|||
let uv = in.clip.xy / dims;
|
||||
return vec4<f32>(textureSample(lightmap_texture, lightmap_sampler, uv).rgb, 1.0);
|
||||
}
|
||||
|
||||
/// Декаль: цвет как у мира, альфа — из текстуры (трафареты надписей).
|
||||
@fragment
|
||||
fn fs_decal(in: VsOut) -> @location(0) vec4<f32> {
|
||||
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<f32>(color.rgb, alpha);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ pub struct WorldRenderer {
|
|||
mirror_bind_group: Option<wgpu::BindGroup>,
|
||||
/// Батчи-зеркала (материал-метка `<mirror>`), по индексу батча.
|
||||
mirror_batches: Vec<bool>,
|
||||
/// Батчи-декали (материалы `decals/...`) — рисуются с альфа-блендингом.
|
||||
decal_batches: Vec<bool>,
|
||||
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<bool> = world
|
||||
.batches
|
||||
.iter()
|
||||
.map(|batch| {
|
||||
world
|
||||
.materials
|
||||
.get(batch.material)
|
||||
.is_some_and(|material| material.name.starts_with("decals/"))
|
||||
})
|
||||
.collect();
|
||||
let mirror_batches: Vec<bool> = 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() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue