diff --git a/crates/renderide/shaders/materials/billboardunlit.wgsl b/crates/renderide/shaders/materials/billboardunlit.wgsl index e1c2ae24e..49b12407b 100644 --- a/crates/renderide/shaders/materials/billboardunlit.wgsl +++ b/crates/renderide/shaders/materials/billboardunlit.wgsl @@ -29,6 +29,7 @@ #import renderide::frame::fog as rfog #import renderide::draw::per_draw as pd #import renderide::draw::types as dt +#import renderide::lighting::diffuse as dl #import renderide::material::alpha as ma #import renderide::material::variant_bits as vb #import renderide::material::vertex_color as vc @@ -69,8 +70,9 @@ const BILLBOARDUNLIT_KW_VERTEX_LINEAR_COLOR: u32 = 1u << 13u; const BILLBOARDUNLIT_KW_VERTEX_SRGB_COLOR: u32 = 1u << 14u; const BILLBOARDUNLIT_KW_VERTEXCOLORS: u32 = 1u << 15u; const BILLBOARDUNLIT_KW_RENDER_BUFFER: u32 = 1u << 16u; -const BILLBOARDUNLIT_KW_UNLIT_MASK_TEXTURE_CLIP: u32 = 1u << 17u; -const BILLBOARDUNLIT_KW_UNLIT_MASK_TEXTURE_MUL: u32 = 1u << 18u; +const BILLBOARDUNLIT_KW_SIMPLE_LIT: u32 = 1u << 17u; +const BILLBOARDUNLIT_KW_UNLIT_MASK_TEXTURE_CLIP: u32 = 1u << 18u; +const BILLBOARDUNLIT_KW_UNLIT_MASK_TEXTURE_MUL: u32 = 1u << 19u; @group(1) @binding(0) var mat: BillboardUnlitMaterial; @group(1) @binding(1) var _Tex: texture_2d; @@ -152,12 +154,18 @@ fn kw_VERTEXCOLORS() -> bool { return bb_kw(BILLBOARDUNLIT_KW_VERTEXCOLORS); } +fn kw_SIMPLE_LIT() -> bool { + return bb_kw(BILLBOARDUNLIT_KW_SIMPLE_LIT); +} + struct VertexOutput { @builtin(position) clip_pos: vec4, @location(0) uv: vec2, @location(1) color: vec4, @location(2) @interpolate(flat) view_layer: u32, @location(3) fog_coord: f32, + @location(4) world_p: vec3, + @location(5) n: vec3, } struct RenderBufferBillboardBasis { @@ -364,6 +372,10 @@ fn vs_main( out.color = color; out.view_layer = layer; out.fog_coord = rfog::coord_from_world_pos(world_p, layer); + if (kw_SIMPLE_LIT()) { + out.world_p = world_p; + out.n = rmath::safe_normalize(cross(axes.right, axes.up), vec3(0.0, 0.0, 1.0)); + } return out; } @@ -422,9 +434,17 @@ fn vertex_color(color: vec4) -> vec4 { return color; } +fn two_sided_geometric_normal(world_n: vec3, front_facing: bool) -> vec3 { + let n = normalize(world_n); + return select(-n, n, front_facing); +} + //#pass type=forward name=forward_billboard blend=material_filter offset=0,0 @fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4 { +fn fs_main( + in: VertexOutput, + @builtin(front_facing) front_facing: bool, +) -> @location(0) vec4 { let use_texture = kw_TEXTURE(); let use_color = kw_COLOR(); @@ -473,5 +493,12 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { col = vec4(col.rgb, ma::alpha_intensity(col.a, col.rgb)); } + if (kw_SIMPLE_LIT()) { + let n = two_sided_geometric_normal(in.n, front_facing); + let base = saturate(col.rgb); + let lit = dl::shade_clustered_diffuse(in.clip_pos.xy, in.world_p, n, base, in.view_layer); + col = vec4(lit, col.a); + } + return rg::retain_globals_additive(rfog::apply_rgba(col, in.fog_coord)); } diff --git a/crates/renderide/src/materials.rs b/crates/renderide/src/materials.rs index 736b3cc2e..bdda97696 100644 --- a/crates/renderide/src/materials.rs +++ b/crates/renderide/src/materials.rs @@ -159,8 +159,7 @@ pub(crate) use pipeline_property_resolver::PipelinePropertyResolver; /// Shader route table, optional material asset registry, and WGSL composition patches. pub(crate) use registry::{MaterialPipelineResolution, MaterialRegistry}; pub(crate) use render_buffer_shader::{ - ensure_render_buffer_billboard_variant_bits, remap_unlit_variant_bits_for_billboard, - should_remap_unlit_variant_bits_for_billboard_draw, + ensure_render_buffer_billboard_variant_bits, remap_variant_bits_for_billboard, }; pub(crate) use router::{MaterialRouter, resolve_raster_pipeline}; diff --git a/crates/renderide/src/materials/render_buffer_shader.rs b/crates/renderide/src/materials/render_buffer_shader.rs index 06ef764e8..1c893c596 100644 --- a/crates/renderide/src/materials/render_buffer_shader.rs +++ b/crates/renderide/src/materials/render_buffer_shader.rs @@ -7,35 +7,45 @@ pub(crate) const BILLBOARD_RENDER_BUFFER_ABSOLUTE_SIZE_BIT: u32 = 1u32 << 16; /// Billboard/Unlit variant bit that enables per-particle color and alpha. pub(crate) const BILLBOARD_RENDER_BUFFER_VERTEX_COLORS_BIT: u32 = 1u32 << 15; +/// Billboard/Unlit variant bit that enables texture sampling. +pub(crate) const BILLBOARD_RENDER_BUFFER_TEXTURE_BIT: u32 = 1u32 << 10; +/// Billboard/Unlit variant bit that enables base color. +pub(crate) const BILLBOARD_RENDER_BUFFER_COLOR_BIT: u32 = 1u32 << 1; +/// Billboard variant bit that enables simple lighting for non-unlit source materials. +pub(crate) const BILLBOARD_RENDER_BUFFER_SIMPLE_LIT_BIT: u32 = 1u32 << 17; -/// Returns whether `stem` names an embedded Unlit-family shader other than Billboard/Unlit. -pub(crate) fn is_unlit_family_embedded_stem(stem: &str) -> bool { - let lower = stem.to_ascii_lowercase(); - (lower.starts_with("unlit") || lower.contains("_unlit") || lower.contains("unlit_")) - && !lower.contains("billboard") -} - -/// Returns whether an embedded draw should remap Unlit keyword bits to Billboard/Unlit bits. -pub(crate) fn should_remap_unlit_variant_bits_for_billboard_draw( - draw_stem: &str, - source_shader_stem: Option<&str>, -) -> bool { - draw_stem.starts_with("billboardunlit") - && source_shader_stem.is_some_and(is_unlit_family_embedded_stem) +pub(crate) fn remap_variant_bits_for_billboard(stem: &str, source_bits: u32) -> u32 { + if is_unlit_family_embedded_stem(stem) { + BILLBOARD_RENDER_BUFFER_VERTEX_COLORS_BIT + | remap_unlit_variant_bits_for_billboard(source_bits) + } else { + BILLBOARD_RENDER_BUFFER_TEXTURE_BIT + | BILLBOARD_RENDER_BUFFER_COLOR_BIT + | BILLBOARD_RENDER_BUFFER_SIMPLE_LIT_BIT + | map_billboard_vertex_color_variant_bits(stem, source_bits) + | map_billboard_alpha_clip_variant_bits(stem, source_bits) + } } /// Enables render-buffer sizing and particle color semantics for synthetic billboard draws. pub(crate) fn ensure_render_buffer_billboard_variant_bits(bits: u32) -> u32 { - bits | BILLBOARD_RENDER_BUFFER_ABSOLUTE_SIZE_BIT | BILLBOARD_RENDER_BUFFER_VERTEX_COLORS_BIT + bits | BILLBOARD_RENDER_BUFFER_ABSOLUTE_SIZE_BIT +} + +/// Returns whether `stem` names an embedded Unlit-family shader other than Billboard/Unlit. +fn is_unlit_family_embedded_stem(stem: &str) -> bool { + let lower = stem.to_ascii_lowercase(); + (lower.starts_with("unlit") || lower.contains("_unlit") || lower.contains("unlit_")) + && !lower.contains("billboard") } /// Remaps Froox Unlit keyword bits to Billboard/Unlit keyword bits for material binding. -pub(crate) fn remap_unlit_variant_bits_for_billboard(unlit_bits: u32) -> u32 { +fn remap_unlit_variant_bits_for_billboard(unlit_bits: u32) -> u32 { const PAIRS: &[(u32, u32)] = &[ (0, 0), (1, 1), - (2, 17), - (3, 18), + (2, 18), + (3, 19), (4, 2), (5, 3), (6, 4), @@ -55,12 +65,71 @@ pub(crate) fn remap_unlit_variant_bits_for_billboard(unlit_bits: u32) -> u32 { out } +/// Remaps Froox AlphaClip or Cutoff keyword bits to Billboard/Unlit keyword bits +/// for material binding with non-Unlit materials. +fn map_billboard_alpha_clip_variant_bits(stem: &str, source_bits: u32) -> u32 { + let lower = stem.to_ascii_lowercase(); + if lower.starts_with("xstoon2.0-cutout") { + return 1; + } + const ALPHA_CLIP_TWO: &[&str] = &["pbsmetallic", "pbsspecular"]; + if ALPHA_CLIP_TWO + .iter() + .any(|prefix| lower.starts_with(prefix)) + { + return (source_bits >> 2) & 1; + } + const ALPHA_CLIP_ONE: &[&str] = &[ + "pbsdisplace", + "pbsdualsided_", + "pbsdualsidedspecular_", + "pbslerp", + "pbsslice_", + "pbsslicespecular_", + "pbsvertexcolor", + "xstoon", + ]; + if ALPHA_CLIP_ONE + .iter() + .any(|prefix| lower.starts_with(prefix)) + { + return (source_bits >> 1) & 1; + } + const ALPHA_CLIP_ZERO: &[&str] = &["fresnel_", "pbsmultiuv", "reflection"]; + if ALPHA_CLIP_ZERO + .iter() + .any(|prefix| lower.starts_with(prefix)) + { + return source_bits & 1; + } + 0u32 +} + +/// Remaps Froox Vertex Colors keyword bits to Billboard/Unlit keyword bits +/// for material binding with non-Unlit materials. +fn map_billboard_vertex_color_variant_bits(stem: &str, source_bits: u32) -> u32 { + let lower = stem.to_ascii_lowercase(); + if lower.starts_with("fresnel_") { + return ((source_bits >> 10) & 1) << 17; + } + if lower.starts_with("pbsdualsidedtransparent") { + return ((source_bits >> 5) & 1) << 17; + } + if lower.starts_with("pbsvertexcolor") || lower.starts_with("pbsdualsided") { + return ((source_bits >> 6) & 1) << 17; + } + if lower.starts_with("xstoon") { + return ((source_bits >> 8) & 1) << 17; + } + 0u32 +} + #[cfg(test)] mod tests { use super::*; - const BILLBOARD_UNLIT_MASK_TEXTURE_CLIP_BIT: u32 = 1u32 << 17; - const BILLBOARD_UNLIT_MASK_TEXTURE_MUL_BIT: u32 = 1u32 << 18; + const BILLBOARD_UNLIT_MASK_TEXTURE_CLIP_BIT: u32 = 1u32 << 18; + const BILLBOARD_UNLIT_MASK_TEXTURE_MUL_BIT: u32 = 1u32 << 19; #[test] fn detects_unlit_family_stems() { @@ -72,7 +141,7 @@ mod tests { #[test] fn remaps_unlit_texture_and_color_bits() { let unlit = (1u32 << 1) | (1u32 << 9); - let billboard = remap_unlit_variant_bits_for_billboard(unlit); + let billboard = remap_variant_bits_for_billboard("unlit_default", unlit); assert_eq!(billboard & (1u32 << 1), 1u32 << 1); assert_eq!(billboard & (1u32 << 10), 1u32 << 10); @@ -96,11 +165,38 @@ mod tests { assert_eq!(billboard & (1u32 << 3), 0); } + #[test] + fn remaps_pbs_alphaclip_bits() { + let pbs = (1u32 << 2) | (1u32 << 7); + let billboard = remap_variant_bits_for_billboard("pbsmetallic_default", pbs); + + assert_eq!( + billboard, + BILLBOARD_RENDER_BUFFER_COLOR_BIT + | BILLBOARD_RENDER_BUFFER_TEXTURE_BIT + | BILLBOARD_RENDER_BUFFER_SIMPLE_LIT_BIT + | 1u32 + ); + } + + #[test] + fn remaps_pbs_no_alphaclip_bits() { + let pbs = 1u32 << 7; + let billboard = remap_variant_bits_for_billboard("pbsmetallic_default", pbs); + + assert_eq!( + billboard, + BILLBOARD_RENDER_BUFFER_COLOR_BIT + | BILLBOARD_RENDER_BUFFER_TEXTURE_BIT + | BILLBOARD_RENDER_BUFFER_SIMPLE_LIT_BIT + ); + } + #[test] fn render_buffer_variant_bit_is_reserved() { assert_eq!( ensure_render_buffer_billboard_variant_bits(0), - BILLBOARD_RENDER_BUFFER_ABSOLUTE_SIZE_BIT | BILLBOARD_RENDER_BUFFER_VERTEX_COLORS_BIT + BILLBOARD_RENDER_BUFFER_ABSOLUTE_SIZE_BIT ); } } diff --git a/crates/renderide/src/passes/world_mesh_forward/material_batch.rs b/crates/renderide/src/passes/world_mesh_forward/material_batch.rs index aa46b3c47..7fe2326d9 100644 --- a/crates/renderide/src/passes/world_mesh_forward/material_batch.rs +++ b/crates/renderide/src/passes/world_mesh_forward/material_batch.rs @@ -23,8 +23,7 @@ use crate::materials::{ MaterialBlendMode, MaterialPipelineDesc, MaterialPipelineResolution, MaterialPipelineSet, MaterialPipelineVariantSpec, MaterialRegistry, MaterialRenderState, MaterialShaderSpecializationKey, RasterFrontFace, RasterPipelineKind, RasterPrimitiveTopology, - ensure_render_buffer_billboard_variant_bits, remap_unlit_variant_bits_for_billboard, - should_remap_unlit_variant_bits_for_billboard_draw, + ensure_render_buffer_billboard_variant_bits, remap_variant_bits_for_billboard, }; use crate::passes::WorldMeshForwardEncodeRefs; use crate::render_graph::frame_upload_batch::GraphUploadSink; @@ -483,17 +482,17 @@ impl<'a> MaterialDrawResolver<'a> { let source_bits = self .registry .and_then(|registry| registry.variant_bits_for_shader_asset(batch_key.shader_asset_id)); - let mut bits = source_bits.unwrap_or(0); - if should_remap_unlit_variant_bits_for_billboard_draw( - stem, - self.registry - .and_then(|registry| registry.stem_for_shader_asset(batch_key.shader_asset_id)), - ) { - bits = remap_unlit_variant_bits_for_billboard(bits); + if !stem.starts_with("billboardunlit") { + return source_bits; } - if crate::particles::is_generated_billboard_mesh_asset_id(item.mesh_asset_id) - && stem.starts_with("billboardunlit") + let mut bits = source_bits.unwrap_or(0); + if let Some(source_stem) = self + .registry + .and_then(|registry| registry.stem_for_shader_asset(batch_key.shader_asset_id)) { + bits = remap_variant_bits_for_billboard(source_stem, bits); + } + if crate::particles::is_generated_billboard_mesh_asset_id(item.mesh_asset_id) { Some(ensure_render_buffer_billboard_variant_bits(bits)) } else { source_bits.map(|_| bits) diff --git a/crates/renderide/src/world_mesh/materials/resolve.rs b/crates/renderide/src/world_mesh/materials/resolve.rs index be1279105..a23a7df29 100644 --- a/crates/renderide/src/world_mesh/materials/resolve.rs +++ b/crates/renderide/src/world_mesh/materials/resolve.rs @@ -388,6 +388,12 @@ pub(crate) fn apply_render_buffer_mesh_pipeline_override( uses_blended_depth_write: features.uses_blended_depth_write, uses_two_sided_transparency: features.uses_two_sided_transparency, }); + if batch_key.blend_mode == MaterialBlendMode::StemDefault + && batch_key.transparent_class == TransparentMaterialClass::OrderedAlpha + { + // Enforce transparent blend mode to match source material transparency + batch_key.blend_mode = MaterialBlendMode::UnityBlend { src: 5, dst: 10 } + } } /// Assembles a [`MaterialDrawBatchKey`] from a pre-resolved [`ResolvedMaterialBatch`] entry. diff --git a/crates/renderide/tests/shader_module_audit.rs b/crates/renderide/tests/shader_module_audit.rs index 2cbbded04..167b57921 100644 --- a/crates/renderide/tests/shader_module_audit.rs +++ b/crates/renderide/tests/shader_module_audit.rs @@ -405,8 +405,12 @@ fn billboard_render_buffer_uses_indexed_corner_separate_from_sample_uv() -> io:: ); } assert!( - src.contains("const BILLBOARDUNLIT_KW_UNLIT_MASK_TEXTURE_CLIP: u32 = 1u << 17u;") - && src.contains("const BILLBOARDUNLIT_KW_UNLIT_MASK_TEXTURE_MUL: u32 = 1u << 18u;"), + src.contains("const BILLBOARDUNLIT_KW_SIMPLE_LIT: u32 = 1u << 17u;"), + "Non-Unlit shading support for render-buffer billboards must use compatibility bit after native Billboard/Unlit keywords" + ); + assert!( + src.contains("const BILLBOARDUNLIT_KW_UNLIT_MASK_TEXTURE_CLIP: u32 = 1u << 18u;") + && src.contains("const BILLBOARDUNLIT_KW_UNLIT_MASK_TEXTURE_MUL: u32 = 1u << 19u;"), "Unlit mask support for render-buffer billboards must use compatibility bits after native Billboard/Unlit keywords" ); assert!( @@ -440,6 +444,12 @@ fn billboard_render_buffer_uses_indexed_corner_separate_from_sample_uv() -> io:: && src.contains("rfog::apply_rgba(col, in.fog_coord)"), "Billboard/Unlit must preserve the source-authored UNITY_APPLY_FOG hook" ); + assert!( + src.contains("if (kw_SIMPLE_LIT())") + && src.contains("out.n = ") + && src.contains("dl::shade_clustered_diffuse"), + "Billboard/Unlit must offer simple shading capabilities for non-Unlit source materials" + ); Ok(()) }