From 79ea829ca968a067de239d926f5f62ebd228dd39 Mon Sep 17 00:00:00 2001 From: chazmm Date: Tue, 8 Sep 2026 19:27:12 +0100 Subject: [PATCH] Reduce weathering decal strength and mip-filter clamped decals --- crates/skate-game/src/retail_depth.wgsl | 2 +- crates/skate-game/src/retail_render.rs | 30 +++++++++++++++++++++++-- crates/skate-game/src/retail_world.wgsl | 4 ++-- crates/skate-game/src/skate_world.rs | 4 ++-- docs/dynamic-props-and-grime.md | 20 +++++++++++++++++ 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/crates/skate-game/src/retail_depth.wgsl b/crates/skate-game/src/retail_depth.wgsl index 28417943..5d59ee0f 100644 --- a/crates/skate-game/src/retail_depth.wgsl +++ b/crates/skate-game/src/retail_depth.wgsl @@ -5,7 +5,7 @@ #ifdef MOTION_VECTOR_PREPASS #import bevy_pbr::pbr_prepass_functions::calculate_motion_vector #endif -struct WorldParams { mode: vec4, foliage_debug: vec4, surface: vec4, family: vec4, fog_ramp: vec4, fog_color: vec4, shadow_color: vec4, sun_direction: vec4, water: array, 4> } +struct WorldParams { mode: vec4, foliage_debug: vec4, surface: vec4, family: vec4, fog_ramp: vec4, fog_color: vec4, shadow_color: vec4, sun_direction: vec4, decal: vec4, water: array, 4> } @group(#{MATERIAL_BIND_GROUP}) @binding(0) var p: WorldParams; @group(#{MATERIAL_BIND_GROUP}) @binding(1) var diffuse: texture_2d; @group(#{MATERIAL_BIND_GROUP}) @binding(2) var diffuse_sampler: sampler; diff --git a/crates/skate-game/src/retail_render.rs b/crates/skate-game/src/retail_render.rs index 94b523a1..347bf9e1 100644 --- a/crates/skate-game/src/retail_render.rs +++ b/crates/skate-game/src/retail_render.rs @@ -47,7 +47,7 @@ fn compare_weathering( 1 => "Repeating grime OFF; decals ON", 2 => "Repeating grime ON; decals OFF", 3 => "Repeating grime OFF; decals OFF", - _ => "Authored grime and decals ON", + _ => "Normal grime and decals ON", }; for (_, material) in materials.iter_mut() { let family = material.params.mode.x as u32; @@ -98,6 +98,8 @@ pub(crate) struct WorldParams { pub fog_color: Vec4, pub shadow_color: Vec4, pub sun_direction: Vec4, + // Wear-only visual tuning; artwork retains unit opacity. + pub decal: Vec4, pub water: [Vec4; 4], } @@ -270,7 +272,7 @@ impl Definition { texture( b.map_or(fallback, |b| b.texture), if clamp || b.is_some_and(|b| b.u == 1 && b.v == 1) { - 4 + if role == "decal" { 6 } else { 4 } } else { 3 }, @@ -339,6 +341,7 @@ impl Definition { fog_color: Vec4::ZERO, shadow_color: Vec4::ZERO, sun_direction: Vec3::new(4., 7., 4.).normalize().extend(0.), + decal: Vec4::new(stain_opacity(self.parameters.get("decal").and_then(|v| v.first()).map(String::as_str).unwrap_or("")), 0., 0., 0.), water, }, diffuse, @@ -356,6 +359,29 @@ impl Definition { } } +// The source labels distinguish weathering from graphics. This is an explicit +// visual tuning choice, not a recovered native material constant. Keep arrows, +// logos, paint, scratches and edge wear at their authored alpha. +fn stain_opacity(texture_label: &str) -> f32 { + let label = texture_label.to_ascii_lowercase(); + if ["grime", "grunge", "stain", "oildirt", "drainage", "ground_decals"] + .iter().any(|word| label.contains(word)) { 0.35 } else { 1. } +} + +#[cfg(test)] +mod stain_tests { + use super::stain_opacity; + #[test] + fn weathering_tuning_preserves_artwork() { + for name in ["decal_WEAR_WaterStain_01", "subway_grunge03", "decal_GrimePuddle", "OT_Ground_decals"] { + assert_eq!(stain_opacity(name), 0.35); + } + for name in ["decal_Graphic_SP_UN_Shark_01", "decal_other_sp_arrowramps_01", "decal_Graphic_SP_UN_MegaRmp_01", "decal_Wear_GL_UN_MPedge_01", "decal_skateboardscratcheswood01", ""] { + assert_eq!(stain_opacity(name), 1.); + } + } +} + /// Native renderer box-filters decoded UNORM cube faces down to 1x1. /// The same chain prevents aliasing of repeating world maps at distance. pub(crate) fn mip_chain(rgba: &[u8], width: u32, height: u32, layers: u32) -> (Vec, u32) { diff --git a/crates/skate-game/src/retail_world.wgsl b/crates/skate-game/src/retail_world.wgsl index 5584c5c0..a62f79a2 100644 --- a/crates/skate-game/src/retail_world.wgsl +++ b/crates/skate-game/src/retail_world.wgsl @@ -3,7 +3,7 @@ struct WorldParams { mode: vec4, foliage_debug: vec4, surface: vec4, family: vec4, - fog_ramp: vec4, fog_color: vec4, shadow_color: vec4, sun_direction: vec4, water: array, 4>, + fog_ramp: vec4, fog_color: vec4, shadow_color: vec4, sun_direction: vec4, decal: vec4, water: array, 4>, } @group(#{MATERIAL_BIND_GROUP}) @binding(0) var p: WorldParams; @group(#{MATERIAL_BIND_GROUP}) @binding(1) var diffuse: texture_2d; @@ -174,7 +174,7 @@ fn fragment(i: VertexOutput) -> @location(0) vec4 { lin = d; if fam == 11u { lin *= p.family.w; } } else { - if (fam == 3u || fam == 4u) && (flags & 8u) != 0u && (flags & 512u) == 0u { d = mix(d,art.rgb*art.rgb,art.a); } + if (fam == 3u || fam == 4u) && (flags & 8u) != 0u && (flags & 512u) == 0u { d = mix(d,art.rgb*art.rgb,art.a*p.decal.x); } if (flags & 4u) != 0u && fam < 13u && (flags & 256u) == 0u { d *= saturate((overlay_sample-0.5)*p.surface.y+0.5); } var kd = 0.93429; if (fam <= 6u || fam == 13u) && (flags & 1u) != 0u { diff --git a/crates/skate-game/src/skate_world.rs b/crates/skate-game/src/skate_world.rs index 5d6906a7..5cd8eb13 100644 --- a/crates/skate-game/src/skate_world.rs +++ b/crates/skate-game/src/skate_world.rs @@ -481,7 +481,7 @@ pub(crate) fn spawn( format, RenderAssetUsages::RENDER_WORLD, ); - if role == 3 || cube { + if role == 3 || role == 6 || cube { let (bytes, levels) = crate::retail_render::mip_chain(&source.rgba, source.width, height, layers); image.data = Some(bytes); image.texture_descriptor.mip_level_count = levels; @@ -492,7 +492,7 @@ pub(crate) fn spawn( }); } let mut sampler = bevy::image::ImageSamplerDescriptor::linear(); - if role != 1 && role != 4 && !cube { + if role != 1 && role != 4 && role != 6 && !cube { sampler.address_mode_u = bevy::image::ImageAddressMode::Repeat; sampler.address_mode_v = bevy::image::ImageAddressMode::Repeat; } diff --git a/docs/dynamic-props-and-grime.md b/docs/dynamic-props-and-grime.md index 7f129418..49f4a00d 100644 --- a/docs/dynamic-props-and-grime.md +++ b/docs/dynamic-props-and-grime.md @@ -1,5 +1,25 @@ # Authored props and grime follow-up +## Targeted decal correction after layer test + +The user rechecked the comparison and identified decals as the unwanted layer. +Repeating grime is unchanged. Wear/stain decals now use 35% of their texture's +alpha; source UVs, dimensions, texture RGB and placement are retained. This is +explicit visual tuning based on the user's feedback, not proof of the original +game's intended opacity and not a recovered native shader constant. + +Classification uses the material's authored `decal` texture label for grime, +grunge, stain, oil-dirt, drainage and ground-decal entries. Arrows, logos, +scratches and ramp edge paint retain full authored opacity. A regression test +covers this separation. This is not a blanket fade on every decal material. + +Clamped decals also now receive a mip chain, independently of lightmaps, which +remain bilinear mip zero. Previously the shared clamp texture role omitted mips +for both, leaving fine decal alpha detail unfiltered at distance. The new decal +role retains clamp addressing while permitting mip filtering. No map re-export +or owned texture modification is needed. GPU visual verification remains for +the user; source and shader checks cannot confirm the preferred visual strength. + ## Grime investigation after user retest The UV-scale correction did not resolve the user's reported visual mismatch.