mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Stabilize player shadow state without rebuilding world materials
This commit is contained in:
1 parent
464692f94c
commit
b5a00ede43
6 files changed
+153
-16
No files matched your search
@@ -37,7 +37,6 @@ struct Lighting {
|
||||
data: LightingData,
|
||||
probes: Irradiance,
|
||||
light: Vec4,
|
||||
shadow_floor: Option<Vec3>,
|
||||
}
|
||||
#[derive(Clone, ShaderType)]
|
||||
struct CharacterParams {
|
||||
@@ -142,7 +141,6 @@ fn load(mut commands: Commands, config: Res<crate::config::Config>) {
|
||||
data,
|
||||
probes,
|
||||
light,
|
||||
shadow_floor: None,
|
||||
});
|
||||
}
|
||||
fn spawn_shadow_sources(commands: &mut Commands, light: Vec3) {
|
||||
@@ -171,13 +169,16 @@ fn spawn_shadow_sources(commands: &mut Commands, light: Vec3) {
|
||||
illuminance: 0.,
|
||||
shadows_enabled: true,
|
||||
affects_lightmapped_mesh_diffuse: true,
|
||||
// Receiver-only map: no self-shadow acne to hide with large bias.
|
||||
shadow_depth_bias: 0.002,
|
||||
shadow_normal_bias: 0.0,
|
||||
..default()
|
||||
},
|
||||
RenderLayers::layer(31),
|
||||
Transform::default().looking_to(-light, Vec3::Y),
|
||||
bevy::light::CascadeShadowConfigBuilder {
|
||||
maximum_distance: 100.,
|
||||
first_cascade_far_bound: 10.,
|
||||
num_cascades: 1,
|
||||
maximum_distance: 24.,
|
||||
..default()
|
||||
}
|
||||
.build(),
|
||||
@@ -266,7 +267,8 @@ fn update(
|
||||
lighting: Option<ResMut<Lighting>>,
|
||||
root: Query<&Transform, With<crate::world::PlayerRoot>>,
|
||||
mut materials: ResMut<Assets<CharacterMaterial>>,
|
||||
mut world_materials: ResMut<Assets<crate::retail_render::RetailWorldMaterial>>,
|
||||
mut shadow: ResMut<crate::retail_render::ShadowState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let (Some(mut lighting), Ok(root)) = (lighting, root.single()) else {
|
||||
return;
|
||||
@@ -281,13 +283,7 @@ fn update(
|
||||
}
|
||||
// Adapter floor: the local probe's direction-independent ambient term.
|
||||
// The native per-frame c8 shadow-colour controller remains unrecovered.
|
||||
let floor = sh[0].truncate().clamp(Vec3::ZERO, Vec3::ONE);
|
||||
if lighting.shadow_floor != Some(floor) {
|
||||
for (_, material) in world_materials.iter_mut() {
|
||||
material.params.shadow_color = floor.extend(1.);
|
||||
}
|
||||
lighting.shadow_floor = Some(floor);
|
||||
}
|
||||
shadow.approach(sh[0].truncate(), time.delta_secs());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -304,8 +300,8 @@ mod tests {
|
||||
let terrain = RenderLayers::default();
|
||||
let mut receivers = 0;
|
||||
let mut character_sources = 0;
|
||||
for (light, layers) in world
|
||||
.query::<(&DirectionalLight, Option<&RenderLayers>)>()
|
||||
for (light, layers, cascades) in world
|
||||
.query::<(&DirectionalLight, Option<&RenderLayers>, &bevy::light::CascadeShadowConfig)>()
|
||||
.iter(&world)
|
||||
{
|
||||
let layers = layers.cloned().unwrap_or_default();
|
||||
@@ -314,6 +310,9 @@ mod tests {
|
||||
assert!(layers.intersects(&player));
|
||||
if light.affects_lightmapped_mesh_diffuse {
|
||||
assert!(!layers.intersects(&terrain));
|
||||
assert_eq!(cascades.bounds, vec![24.]);
|
||||
assert_eq!(light.shadow_normal_bias, 0.);
|
||||
assert!(light.shadow_depth_bias < 0.005);
|
||||
receivers += 1;
|
||||
} else {
|
||||
assert!(layers.intersects(&terrain));
|
||||
|
||||
@@ -18,6 +18,7 @@ use std::collections::BTreeMap;
|
||||
pub(crate) struct RetailRenderPlugin;
|
||||
impl Plugin for RetailRenderPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
shadow::install(app);
|
||||
app.add_plugins(crate::retail_character::CharacterLightingPlugin);
|
||||
if std::env::var_os("SKATE_DEBUG_FOLIAGE").is_some_and(|v| v == "1") {
|
||||
eprintln!("SKATE_FOLIAGE_DEBUG: solid cyan tree-wall cards, magenta other foliage; alpha rejection disabled for foliage only");
|
||||
@@ -34,6 +35,10 @@ impl Plugin for RetailRenderPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
#[path = "retail_shadow.rs"]
|
||||
mod shadow;
|
||||
pub(crate) use shadow::ShadowState;
|
||||
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct RetailScene(pub bool);
|
||||
|
||||
@@ -106,6 +111,8 @@ pub(crate) struct RetailWorldMaterial {
|
||||
pub specular: Option<Handle<Image>>,
|
||||
#[texture(15, dimension = "cube")]
|
||||
pub environment: Option<Handle<Image>>,
|
||||
#[storage(16, read_only)]
|
||||
pub shadow_state: Handle<bevy::render::storage::ShaderStorageBuffer>,
|
||||
pub alpha: AlphaMode,
|
||||
pub two_sided: bool,
|
||||
}
|
||||
@@ -305,6 +312,7 @@ impl Definition {
|
||||
decal,
|
||||
specular,
|
||||
environment,
|
||||
shadow_state: shadow::BUFFER,
|
||||
alpha,
|
||||
two_sided: self.flags & 4 != 0,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Shared, in-place shadow state: moving probes must not rebuild world materials.
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
render::{
|
||||
Render, RenderApp, RenderSystems,
|
||||
extract_resource::{ExtractResource, ExtractResourcePlugin},
|
||||
render_asset::RenderAssets,
|
||||
render_resource::BufferUsages,
|
||||
renderer::RenderQueue,
|
||||
storage::{GpuShaderStorageBuffer, ShaderStorageBuffer},
|
||||
},
|
||||
};
|
||||
|
||||
pub(super) const BUFFER: Handle<ShaderStorageBuffer> =
|
||||
bevy::asset::uuid_handle!("cd736f89-4882-4a5d-8fcb-32273beeaaf4");
|
||||
|
||||
#[derive(Resource, Clone, Default, ExtractResource)]
|
||||
pub(crate) struct ShadowState(pub Vec4);
|
||||
|
||||
impl ShadowState {
|
||||
pub(crate) fn approach(&mut self, target: Vec3, dt: f32) {
|
||||
let target = target.clamp(Vec3::ZERO, Vec3::ONE);
|
||||
let value = if self.0.w == 0. {
|
||||
target
|
||||
} else {
|
||||
// Adapter smoothing, not a recovered native constant. Cap a hitch's
|
||||
// contribution so one long frame cannot cause a darkness step.
|
||||
self.0
|
||||
.truncate()
|
||||
.lerp(target, 1. - (-dt.clamp(0., 0.05) / 0.35).exp())
|
||||
};
|
||||
self.0 = value.extend(1.);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn install(app: &mut App) {
|
||||
app.init_resource::<ShadowState>()
|
||||
.add_plugins(ExtractResourcePlugin::<ShadowState>::default())
|
||||
.add_systems(Startup, initialize);
|
||||
if let Some(render) = app.get_sub_app_mut(RenderApp) {
|
||||
render.add_systems(Render, upload.in_set(RenderSystems::PrepareResources));
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize(mut buffers: ResMut<Assets<ShaderStorageBuffer>>) {
|
||||
let mut buffer = ShaderStorageBuffer::from(Vec4::ZERO);
|
||||
buffer.buffer_description.usage |= BufferUsages::COPY_DST;
|
||||
buffers
|
||||
.insert(BUFFER.id(), buffer)
|
||||
.expect("reserved shadow state asset");
|
||||
}
|
||||
|
||||
fn upload(
|
||||
state: Res<ShadowState>,
|
||||
buffers: Res<RenderAssets<GpuShaderStorageBuffer>>,
|
||||
queue: Res<RenderQueue>,
|
||||
) {
|
||||
if let Some(buffer) = buffers.get(BUFFER.id()) {
|
||||
let bytes: Vec<u8> = state
|
||||
.0
|
||||
.to_array()
|
||||
.into_iter()
|
||||
.flat_map(f32::to_le_bytes)
|
||||
.collect();
|
||||
// Keep the buffer and every material bind group alive; only 16 bytes change.
|
||||
queue.write_buffer(&buffer.buffer, 0, &bytes);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn probe_transition_is_gradual_and_hitches_are_bounded() {
|
||||
let mut state = ShadowState::default();
|
||||
state.approach(Vec3::splat(0.1), 0.016);
|
||||
assert_eq!(state.0, Vec3::splat(0.1).extend(1.));
|
||||
state.approach(Vec3::splat(0.3), 0.016);
|
||||
assert!(state.0.x > 0.1 && state.0.x < 0.12);
|
||||
let mut hitch = state.clone();
|
||||
state.approach(Vec3::splat(0.3), 0.05);
|
||||
hitch.approach(Vec3::splat(0.3), 10.);
|
||||
assert_eq!(state.0, hitch.0);
|
||||
for _ in 0..240 {
|
||||
state.approach(Vec3::splat(0.3), 1. / 60.);
|
||||
}
|
||||
assert!((state.0.x - 0.3).abs() < 0.0001);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ struct WorldParams {
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(14) var specular_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(15) var environment_map: texture_cube<f32>;
|
||||
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(16) var<storage, read> shadow_state: vec4<f32>;
|
||||
|
||||
@fragment
|
||||
fn fragment(i: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let fam = u32(p.mode.x);
|
||||
@@ -72,13 +74,13 @@ fn fragment(i: VertexOutput) -> @location(0) vec4<f32> {
|
||||
var alpha = 1.0;
|
||||
var lin = vec3<f32>(0.0);
|
||||
var baked = lm*lm;
|
||||
if p.shadow_color.w>0.0 && (fam<=8u || fam==13u) {
|
||||
if shadow_state.w>0.0 && (fam<=8u || fam==13u) {
|
||||
let view_z=(frame::view.view_from_world*i.world_position).z;
|
||||
for (var light_id=0u; light_id<frame::lights.n_directional_lights; light_id+=1u) {
|
||||
// The lightmapped receiver source contains only player/board casters.
|
||||
if (frame::lights.directional_lights[light_id].flags & 5u)==5u {
|
||||
let visibility=fetch_directional_shadow(light_id,i.world_position,wn,view_z);
|
||||
baked=min(baked,vec3<f32>(visibility)+p.shadow_color.rgb);
|
||||
baked=min(baked,vec3<f32>(visibility)+shadow_state.rgb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,3 +141,36 @@ composition/validation; a windowless ECS regression test verifies the caster-lay
|
||||
separation and zero illuminance. Release compilation passes. The prepared user
|
||||
launcher retains the texture-band repairs and prior character lighting. No game
|
||||
or recomp was launched, and no physics/animation implementation changed.
|
||||
|
||||
## Shadow stability follow-up, 2026-09-08
|
||||
|
||||
User testing reported thin/warped limbs in the shadow, heavy lag, a crash, and
|
||||
abrupt changes in darkness. The previous implementation changed every world
|
||||
material when nearest-probe selection changed. This both stepped the ambient
|
||||
floor and invalidated all those material assets. It is a confirmed code defect
|
||||
and a plausible source of the reported stalls; no contemporaneous crash dump or
|
||||
Application error record was found, so the crash itself is not diagnosed.
|
||||
|
||||
World materials now bind one persistent 16-byte storage buffer. An extracted
|
||||
resource supplies the shadow floor, and the render queue writes it in place
|
||||
after asset preparation. Neither the storage asset nor world materials are
|
||||
mutated during probe changes; existing bind groups remain valid. The floor uses
|
||||
an exponential 0.35-second transition, with each frame's contribution capped at
|
||||
50 ms to avoid a jump after a hitch. These are adapter settings, not recovered
|
||||
native constants. The initial sample is applied directly.
|
||||
|
||||
The player-only source now uses one 24-metre cascade instead of four extending
|
||||
to 100 metres. This removes its internal cascade transitions and three shadow
|
||||
views. Its receiver normal offset is zero and depth bias is 0.002 metres; the
|
||||
previous defaults were 1.8 texels and 0.02 metres. World-plus-character self
|
||||
shadowing retains its original source and bias. The new settings target the
|
||||
reported silhouette distortion, but visible improvement is not confirmed offline.
|
||||
A single cascade trades distant coverage for stability; shadows outside its
|
||||
24-metre camera range are intentionally unavailable. Exact console parity is
|
||||
still pending the native colour controller and filtering work above.
|
||||
|
||||
Offline world/character/depth shader validation and six filtered unit tests pass,
|
||||
including smooth probe transitions, hitch limiting, caster isolation and cascade
|
||||
configuration. Release compilation passes. The updated private University
|
||||
launcher records stdout/stderr, backtraces and the exit code in
|
||||
`logs/renderer-work/player-shadow-runtime.log`. It has not been executed.
|
||||
@@ -227,3 +227,9 @@ using both scalar and NumPy paths. All 16 asset-pipeline tests pass. The game wa
|
||||
not launched; visual confirmation remains user-run. Existing exported maps need
|
||||
their textures regenerated from RX2, since changing the decoder cannot repair
|
||||
already-baked PNG/RGBA cache files. Fresh conversions use the fix across maps.
|
||||
|
||||
Player-shadow stability follow-up: the ambient floor is now a smoothed shared
|
||||
16-byte GPU buffer, updated without rebuilding world materials. The player-only
|
||||
source uses one 24 m cascade and reduced receiver bias. Native colour/filter
|
||||
parity and runtime visual/performance verification remain outstanding; see the
|
||||
character lighting investigation. The private launcher now captures crash output.
|
||||
Reference in new issue
Block a user