use crate::resources::mesh::mesh_store::MeshId;
use crate::resources::{DeviceResources, DualPipeline};
use wgpu::util::DeviceExt as _;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub(crate) struct DecalUniformRaw {
pub inv_transform: [[f32; 4]; 4], pub blend_mode: u32, pub alpha: f32, pub normal_blend_strength: f32, pub has_normal: u32, pub roughness: f32, pub metallic: f32, pub has_roughness_tex: u32, pub has_metallic_tex: u32, pub uv_offset: [f32; 2], pub uv_scale: [f32; 2], pub emissive: f32, pub has_emissive_tex: u32, pub edge_fade: f32, pub _pad: u32, pub projection: u32, pub tri_blend_sharpness: f32, pub _pad2: u32, pub _pad3: u32, }
#[derive(Clone)]
pub(crate) struct DecalGpuItem {
pub blend_mode: crate::renderer::DecalBlendMode,
pub _uniform_buf: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
pub model: glam::Mat4,
pub selected: bool,
}
pub(crate) struct DecalExcludeGpuItem {
pub mesh_id: MeshId,
pub _uniform_buf: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
}
pub(crate) fn decal_uniform_raw(item: &crate::renderer::DecalItem) -> DecalUniformRaw {
let model = glam::Mat4::from_cols_array_2d(&item.transform);
let inv_transform = model.inverse().to_cols_array_2d();
let blend_mode_u32 = match item.blend_mode {
crate::renderer::DecalBlendMode::Replace => 0u32,
crate::renderer::DecalBlendMode::Multiply => 1u32,
crate::renderer::DecalBlendMode::Additive => 0u32,
};
let (projection_u32, tri_blend_sharpness) = match item.projection {
crate::renderer::DecalProjection::Planar => (0u32, 1.0f32),
crate::renderer::DecalProjection::TriPlanar { blend_sharpness } => {
(1u32, blend_sharpness.max(0.1))
}
crate::renderer::DecalProjection::Cylindrical { facing } => {
let code = match facing {
crate::renderer::CylindricalFacing::Outward => 2u32,
crate::renderer::CylindricalFacing::Inward => 3u32,
};
(code, 0.0f32)
}
};
let has_normal = item.normal_texture_id.is_some() as u32;
let has_roughness_tex = item.roughness_texture_id.is_some() as u32;
let has_metallic_tex = item.metallic_texture_id.is_some() as u32;
let has_emissive_tex = item.emissive_texture_id.is_some() as u32;
DecalUniformRaw {
inv_transform,
blend_mode: blend_mode_u32,
alpha: item.alpha,
normal_blend_strength: if has_normal != 0 {
item.normal_blend_strength
} else {
0.0
},
has_normal,
roughness: item.roughness,
metallic: item.metallic,
has_roughness_tex,
has_metallic_tex,
uv_offset: item.uv_offset,
uv_scale: item.uv_scale,
emissive: item.emissive,
has_emissive_tex,
edge_fade: item.edge_fade.clamp(0.0, 0.5),
_pad: 0,
projection: projection_u32,
tri_blend_sharpness,
_pad2: 0,
_pad3: 0,
}
}
pub(crate) fn hash_decal_item(item: &crate::renderer::DecalItem) -> u64 {
use std::hash::Hasher as _;
let raw = decal_uniform_raw(item);
let mut h = std::collections::hash_map::DefaultHasher::new();
h.write(bytemuck::bytes_of(&raw));
h.write_u8(item.blend_mode as u8);
h.write_u64(item.texture_id.raw());
for id in [
item.normal_texture_id,
item.roughness_texture_id,
item.metallic_texture_id,
item.emissive_texture_id,
] {
h.write_u8(id.is_some() as u8);
h.write_u64(id.map(|t| t.raw()).unwrap_or(0));
}
h.finish()
}
#[derive(Default)]
pub(crate) struct DecalResources {
pub(crate) replace_pipeline: Option<DualPipeline>,
pub(crate) multiply_pipeline: Option<DualPipeline>,
pub(crate) additive_pipeline: Option<DualPipeline>,
pub(crate) depth_bgl: Option<wgpu::BindGroupLayout>,
pub(crate) item_bgl: Option<wgpu::BindGroupLayout>,
pub(crate) sampler: Option<wgpu::Sampler>,
pub(crate) exclude_pipeline: Option<wgpu::RenderPipeline>,
pub(crate) exclude_obj_bgl: Option<wgpu::BindGroupLayout>,
pub(crate) outline_mask_pipeline: Option<wgpu::RenderPipeline>,
pub(crate) outline_edge_pipeline: Option<wgpu::RenderPipeline>,
pub(crate) outline_edge_bgl: Option<wgpu::BindGroupLayout>,
pub(crate) outline_targets: Option<DecalOutlineTargets>,
}
pub(crate) struct DecalOutlineTargets {
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) mask_view: wgpu::TextureView,
pub(crate) _mask_tex: wgpu::Texture,
pub(crate) edge_uniform_buf: wgpu::Buffer,
pub(crate) edge_bind_group: wgpu::BindGroup,
}
impl DeviceResources {
pub(crate) fn ensure_decal_pipeline(&mut self, device: &wgpu::Device) {
if self.decal.replace_pipeline.is_some() {
return;
}
let shader = crate::resources::builders::wgsl_module(
device,
"decal_shader",
crate::resources::builders::wgsl_source!("decal"),
);
let tex2d_entry = |binding: u32| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
};
let item_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("decal_item_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
tex2d_entry(1),
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
tex2d_entry(3), tex2d_entry(4), tex2d_entry(5), tex2d_entry(6), ],
});
let depth_bgl = self
.decal
.depth_bgl
.as_ref()
.expect("decal_depth_bgl must exist before ensure_decal_pipeline");
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("decal_pipeline_layout"),
bind_group_layouts: &[&self.camera_bind_group_layout, depth_bgl, &item_bgl],
push_constant_ranges: &[],
});
let make = |fmt: wgpu::TextureFormat, blend: wgpu::BlendState| {
crate::resources::builders::build_fullscreen_pipeline(
device,
"decal_pipeline",
&layout,
&shader,
fmt,
Some(blend),
)
};
let replace_blend = wgpu::BlendState::ALPHA_BLENDING;
let multiply_blend = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::Src,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
};
let additive_blend = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
};
self.decal.item_bgl = Some(item_bgl);
self.decal.replace_pipeline = Some(DualPipeline {
ldr: make(self.target_format, replace_blend),
hdr: make(wgpu::TextureFormat::Rgba16Float, replace_blend),
});
self.decal.multiply_pipeline = Some(DualPipeline {
ldr: make(self.target_format, multiply_blend),
hdr: make(wgpu::TextureFormat::Rgba16Float, multiply_blend),
});
self.decal.additive_pipeline = Some(DualPipeline {
ldr: make(self.target_format, additive_blend),
hdr: make(wgpu::TextureFormat::Rgba16Float, additive_blend),
});
}
pub(crate) fn ensure_decal_outline_pipelines(&mut self, device: &wgpu::Device) {
if self.decal.outline_mask_pipeline.is_some() {
return;
}
let (Some(depth_bgl), Some(item_bgl)) =
(self.decal.depth_bgl.as_ref(), self.decal.item_bgl.as_ref())
else {
return;
};
let mask_shader = crate::resources::builders::wgsl_module(
device,
"decal_outline_mask_shader",
crate::resources::builders::wgsl_source!("decal_outline_mask"),
);
let mask_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("decal_outline_mask_layout"),
bind_group_layouts: &[&self.camera_bind_group_layout, depth_bgl, item_bgl],
push_constant_ranges: &[],
});
let mask_pipeline = crate::resources::builders::build_fullscreen_pipeline(
device,
"decal_outline_mask_pipeline",
&mask_layout,
&mask_shader,
crate::resources::MASK_COLOR_FORMAT,
None,
);
let edge_shader = crate::resources::builders::wgsl_module(
device,
"decal_outline_edge_shader",
crate::resources::builders::wgsl_source!("outline_edge"),
);
let edge_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("decal_outline_edge_bgl"),
entries: &[
crate::resources::builders::texture_entry(0, wgpu::ShaderStages::FRAGMENT),
crate::resources::builders::sampler_entry(1, wgpu::ShaderStages::FRAGMENT),
crate::resources::builders::uniform_entry(2, wgpu::ShaderStages::FRAGMENT),
],
});
let edge_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("decal_outline_edge_layout"),
bind_group_layouts: &[&edge_bgl],
push_constant_ranges: &[],
});
let edge_pipeline = crate::resources::builders::build_fullscreen_pipeline(
device,
"decal_outline_edge_pipeline",
&edge_layout,
&edge_shader,
wgpu::TextureFormat::Rgba16Float,
Some(wgpu::BlendState::ALPHA_BLENDING),
);
self.decal.outline_mask_pipeline = Some(mask_pipeline);
self.decal.outline_edge_pipeline = Some(edge_pipeline);
self.decal.outline_edge_bgl = Some(edge_bgl);
}
pub(crate) fn ensure_decal_outline_targets(&mut self, device: &wgpu::Device, w: u32, h: u32) {
if let Some(t) = &self.decal.outline_targets {
if t.width == w && t.height == h {
return;
}
}
let (Some(edge_bgl), Some(sampler)) = (
self.decal.outline_edge_bgl.as_ref(),
self.decal.sampler.as_ref(),
) else {
return;
};
let mask_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("decal_outline_mask_tex"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: crate::resources::MASK_COLOR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let mask_view = mask_tex.create_view(&wgpu::TextureViewDescriptor::default());
let edge_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("decal_outline_edge_uniform_buf"),
size: std::mem::size_of::<crate::resources::OutlineEdgeUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let edge_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("decal_outline_edge_bg"),
layout: edge_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&mask_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: edge_uniform_buf.as_entire_binding(),
},
],
});
self.decal.outline_targets = Some(DecalOutlineTargets {
width: w,
height: h,
mask_view,
_mask_tex: mask_tex,
edge_uniform_buf,
edge_bind_group,
});
}
pub(crate) fn create_decal_depth_bg(
&self,
device: &wgpu::Device,
depth_only_view: &wgpu::TextureView,
stencil_only_view: &wgpu::TextureView,
) -> wgpu::BindGroup {
let bgl = self
.decal
.depth_bgl
.as_ref()
.expect("decal_depth_bgl not created");
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("decal_depth_bg"),
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(depth_only_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(stencil_only_view),
},
],
})
}
pub(crate) fn upload_decal_item(
&self,
device: &wgpu::Device,
item: &crate::renderer::DecalItem,
) -> DecalGpuItem {
let model = glam::Mat4::from_cols_array_2d(&item.transform);
let raw = decal_uniform_raw(item);
let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("decal_uniform_buf"),
contents: bytemuck::bytes_of(&raw),
usage: wgpu::BufferUsages::UNIFORM,
});
let resolve_tex = |id: Option<crate::resources::TextureId>| -> &wgpu::TextureView {
id.and_then(|i| self.content.textures.get(i))
.map(|t| &t.view)
.unwrap_or(&self.fallback_texture.view)
};
let tex_view = self
.content
.textures
.get(item.texture_id)
.map(|t| &t.view)
.unwrap_or(&self.fallback_texture.view);
let normal_view = resolve_tex(item.normal_texture_id);
let roughness_view = resolve_tex(item.roughness_texture_id);
let metallic_view = resolve_tex(item.metallic_texture_id);
let emissive_view = resolve_tex(item.emissive_texture_id);
let bgl = self
.decal
.item_bgl
.as_ref()
.expect("ensure_decal_pipeline not called");
let sampler = self
.decal
.sampler
.as_ref()
.expect("decal_sampler not created");
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("decal_item_bg"),
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(tex_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(normal_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(roughness_view),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(metallic_view),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::TextureView(emissive_view),
},
],
});
DecalGpuItem {
blend_mode: item.blend_mode,
_uniform_buf: uniform_buf,
bind_group,
model,
selected: item.settings.selected,
}
}
pub(crate) fn ensure_decal_exclude_pipeline(&mut self, device: &wgpu::Device) {
if self.decal.exclude_pipeline.is_some() {
return;
}
let shader = crate::resources::builders::wgsl_module(
device,
"decal_exclude_shader",
crate::resources::builders::wgsl_source!("decal_exclude"),
);
let obj_bgl = crate::resources::builders::uniform_bgl(
device,
"decal_exclude_obj_bgl",
wgpu::ShaderStages::VERTEX,
);
let layout = crate::resources::builders::standard_scene_layout(
device,
"decal_exclude_pipeline_layout",
&self.camera_bind_group_layout,
&obj_bgl,
);
let vertex_layout = wgpu::VertexBufferLayout {
array_stride: 64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 0,
shader_location: 0,
}],
};
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("decal_exclude_pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[vertex_layout],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::LessEqual,
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Replace,
},
back: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Replace,
},
read_mask: 0xff,
write_mask: 0xff,
},
bias: wgpu::DepthBiasState {
constant: -2,
slope_scale: 0.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
self.decal.exclude_obj_bgl = Some(obj_bgl);
self.decal.exclude_pipeline = Some(pipeline);
}
pub(crate) fn upload_decal_exclude_item(
&self,
device: &wgpu::Device,
mesh_id: MeshId,
model: [[f32; 4]; 4],
) -> DecalExcludeGpuItem {
let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("decal_exclude_uniform_buf"),
contents: bytemuck::cast_slice(&model),
usage: wgpu::BufferUsages::UNIFORM,
});
let bgl = self
.decal
.exclude_obj_bgl
.as_ref()
.expect("ensure_decal_exclude_pipeline not called");
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("decal_exclude_obj_bg"),
layout: bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buf.as_entire_binding(),
}],
});
DecalExcludeGpuItem {
mesh_id,
_uniform_buf: uniform_buf,
bind_group,
}
}
}
#[cfg(test)]
mod tests {
use super::hash_decal_item;
use crate::renderer::{DecalBlendMode, DecalItem};
#[test]
fn identical_decals_hash_equal() {
let a = DecalItem {
texture_id: crate::resources::TextureId(3),
..DecalItem::default()
};
let b = a.clone();
assert_eq!(hash_decal_item(&a), hash_decal_item(&b));
}
#[test]
fn blend_mode_changes_hash() {
let replace = DecalItem {
blend_mode: DecalBlendMode::Replace,
..DecalItem::default()
};
let additive = DecalItem {
blend_mode: DecalBlendMode::Additive,
..DecalItem::default()
};
assert_ne!(hash_decal_item(&replace), hash_decal_item(&additive));
}
#[test]
fn texture_and_transform_change_hash() {
let base = DecalItem::default();
let tex = DecalItem {
texture_id: crate::resources::TextureId(7),
..DecalItem::default()
};
assert_ne!(hash_decal_item(&base), hash_decal_item(&tex));
let mut moved = DecalItem::default();
moved.transform[3][0] = 5.0;
assert_ne!(hash_decal_item(&base), hash_decal_item(&moved));
}
}