mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Project player shadows onto baked world materials
This commit is contained in:
1 parent
ad992f2001
commit
464692f94c
5 files changed
+157
-15
No files matched your search
@@ -2,6 +2,7 @@
|
||||
use crate::retail_irradiance::Irradiance;
|
||||
use bevy::{
|
||||
asset::embedded_asset,
|
||||
camera::visibility::RenderLayers,
|
||||
gltf::GltfMaterialName,
|
||||
prelude::*,
|
||||
render::render_resource::{AsBindGroup, ShaderType},
|
||||
@@ -17,7 +18,7 @@ impl Plugin for CharacterLightingPlugin {
|
||||
embedded_asset!(app, "retail_character_depth.wgsl");
|
||||
app.add_plugins(MaterialPlugin::<CharacterMaterial>::default())
|
||||
.add_systems(Startup, load)
|
||||
.add_systems(Update, (bind, update).chain());
|
||||
.add_systems(Update, (bind, shadow_views, update).chain());
|
||||
}
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
@@ -36,6 +37,7 @@ struct Lighting {
|
||||
data: LightingData,
|
||||
probes: Irradiance,
|
||||
light: Vec4,
|
||||
shadow_floor: Option<Vec3>,
|
||||
}
|
||||
#[derive(Clone, ShaderType)]
|
||||
struct CharacterParams {
|
||||
@@ -135,16 +137,44 @@ fn load(mut commands: Commands, config: Res<crate::config::Config>) {
|
||||
light[i] = v as f32;
|
||||
}
|
||||
info!("SKATE_CHARACTER_LIGHTING: loaded authored {name} irradiance and character parameters");
|
||||
// Shadow visibility only. World shaders do not sample this map; zero lux
|
||||
// also prevents additive energy on any remaining StandardMaterials.
|
||||
spawn_shadow_sources(&mut commands, light.truncate());
|
||||
commands.insert_resource(Lighting {
|
||||
data,
|
||||
probes,
|
||||
light,
|
||||
shadow_floor: None,
|
||||
});
|
||||
}
|
||||
fn spawn_shadow_sources(commands: &mut Commands, light: Vec3) {
|
||||
// World + skater casters, sampled only by the character shader.
|
||||
commands.spawn((
|
||||
Name::new("Character shadow visibility"),
|
||||
DirectionalLight {
|
||||
illuminance: 0.,
|
||||
shadows_enabled: true,
|
||||
affects_lightmapped_mesh_diffuse: false,
|
||||
..default()
|
||||
},
|
||||
Transform::default().looking_to(-light.truncate(), Vec3::Y),
|
||||
Transform::default().looking_to(-light, Vec3::Y),
|
||||
bevy::light::CascadeShadowConfigBuilder {
|
||||
maximum_distance: 100.,
|
||||
first_cascade_far_bound: 10.,
|
||||
..default()
|
||||
}
|
||||
.build(),
|
||||
));
|
||||
// Only skinned player pieces inhabit layer 31. The world receiver samples
|
||||
// this separate map so baked building/terrain shadows are not re-applied.
|
||||
commands.spawn((
|
||||
Name::new("Player shadow onto baked world"),
|
||||
DirectionalLight {
|
||||
illuminance: 0.,
|
||||
shadows_enabled: true,
|
||||
affects_lightmapped_mesh_diffuse: true,
|
||||
..default()
|
||||
},
|
||||
RenderLayers::layer(31),
|
||||
Transform::default().looking_to(-light, Vec3::Y),
|
||||
bevy::light::CascadeShadowConfigBuilder {
|
||||
maximum_distance: 100.,
|
||||
first_cascade_far_bound: 10.,
|
||||
@@ -152,12 +182,8 @@ fn load(mut commands: Commands, config: Res<crate::config::Config>) {
|
||||
}
|
||||
.build(),
|
||||
));
|
||||
commands.insert_resource(Lighting {
|
||||
data,
|
||||
probes,
|
||||
light,
|
||||
});
|
||||
}
|
||||
|
||||
fn bind(
|
||||
mut commands: Commands,
|
||||
lighting: Option<Res<Lighting>>,
|
||||
@@ -215,13 +241,32 @@ fn bind(
|
||||
commands
|
||||
.entity(entity)
|
||||
.remove::<MeshMaterial3d<StandardMaterial>>()
|
||||
.insert(MeshMaterial3d(material));
|
||||
.insert((
|
||||
MeshMaterial3d(material),
|
||||
RenderLayers::from_layers(&[0, 31]),
|
||||
));
|
||||
}
|
||||
}
|
||||
fn shadow_views(
|
||||
mut commands: Commands,
|
||||
lighting: Option<Res<Lighting>>,
|
||||
cameras: Query<(Entity, Option<&RenderLayers>), With<Camera3d>>,
|
||||
) {
|
||||
if lighting.is_none() {
|
||||
return;
|
||||
}
|
||||
for (entity, layers) in &cameras {
|
||||
let layers = layers.cloned().unwrap_or_default();
|
||||
if !layers.intersects(&RenderLayers::layer(31)) {
|
||||
commands.entity(entity).insert(layers.with(31));
|
||||
}
|
||||
}
|
||||
}
|
||||
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>>,
|
||||
) {
|
||||
let (Some(mut lighting), Ok(root)) = (lighting, root.single()) else {
|
||||
return;
|
||||
@@ -234,4 +279,47 @@ fn update(
|
||||
for (_, material) in materials.iter_mut() {
|
||||
material.params.sh = sh;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn world_receiver_source_excludes_world_casters_and_emits_no_light() {
|
||||
let mut world = World::new();
|
||||
let mut queue = bevy::ecs::world::CommandQueue::default();
|
||||
spawn_shadow_sources(&mut Commands::new(&mut queue, &world), Vec3::Y);
|
||||
queue.apply(&mut world);
|
||||
let player = RenderLayers::from_layers(&[0, 31]);
|
||||
let terrain = RenderLayers::default();
|
||||
let mut receivers = 0;
|
||||
let mut character_sources = 0;
|
||||
for (light, layers) in world
|
||||
.query::<(&DirectionalLight, Option<&RenderLayers>)>()
|
||||
.iter(&world)
|
||||
{
|
||||
let layers = layers.cloned().unwrap_or_default();
|
||||
assert_eq!(light.illuminance, 0.);
|
||||
assert!(light.shadows_enabled);
|
||||
assert!(layers.intersects(&player));
|
||||
if light.affects_lightmapped_mesh_diffuse {
|
||||
assert!(!layers.intersects(&terrain));
|
||||
receivers += 1;
|
||||
} else {
|
||||
assert!(layers.intersects(&terrain));
|
||||
character_sources += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!((receivers, character_sources), (1, 1));
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,12 @@ fn fragment(i: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let ndl=dot(vn,p.light.xyz);
|
||||
let view_z=(frame::view.view_from_world*i.world_position).z;
|
||||
var shadow=1.0;
|
||||
if frame::lights.n_directional_lights>0u {
|
||||
shadow=fetch_directional_shadow(0u,i.world_position,vn,view_z);
|
||||
for (var light_id=0u; light_id<frame::lights.n_directional_lights; light_id+=1u) {
|
||||
// All-caster visibility light does not affect lightmapped world diffuse.
|
||||
if (frame::lights.directional_lights[light_id].flags & 5u)==1u {
|
||||
shadow=fetch_directional_shadow(light_id,i.world_position,vn,view_z);
|
||||
break;
|
||||
}
|
||||
}
|
||||
var lit=p.rows[1].rgb*saturate(ndl)*shadow+irr*p.rows[5].w;
|
||||
let fb=1.0-saturate(dot(vn,vd));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#import bevy_pbr::{forward_io::VertexOutput, mesh_view_bindings as frame}
|
||||
#import bevy_pbr::shadows::fetch_directional_shadow
|
||||
|
||||
struct WorldParams {
|
||||
mode: vec4<f32>, foliage_debug: vec4<f32>, surface: vec4<f32>, family: vec4<f32>,
|
||||
@@ -70,6 +71,18 @@ fn fragment(i: VertexOutput) -> @location(0) vec4<f32> {
|
||||
var d = a.rgb*a.rgb;
|
||||
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) {
|
||||
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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if fam == 9u || fam == 10u {
|
||||
lin = d * max(lm*lm,vec3<f32>(p.family.y)) * p.family.x;
|
||||
if fam == 9u { lin *= p.family.z; }
|
||||
@@ -102,7 +115,7 @@ fn fragment(i: VertexOutput) -> @location(0) vec4<f32> {
|
||||
wn = normalize(raw.x*tt+raw.y*bb+wn*max(raw.z,0.05));
|
||||
kd = (vnd.x*0.58*sign(dot(kt,sun))+vnd.y*0.62*sign(dot(kb,sun))+vnd.z*0.39)*2.39562;
|
||||
}
|
||||
var lml = lm*lm;
|
||||
var lml = baked;
|
||||
lin = lml*kd*d;
|
||||
if fam == 13u { lin = lml*d*a.a; }
|
||||
if (flags & 16u) != 0u {
|
||||
|
||||
@@ -105,3 +105,39 @@ colour, shadow and normal-depth shader variants pass offline Naga composition
|
||||
against the vendored Bevy 0.18 shader modules. Release compilation passes. The
|
||||
game/recomp was not launched. User visual checks should cover exposed terrain,
|
||||
sheltered spots, skin/clothing highlights, hair coverage and body/board shadows.
|
||||
|
||||
## Player shadow receiver, 2026-09-08
|
||||
|
||||
The baked-world receiver is now connected. Native `scene.hlsl` evaluates
|
||||
`min(lightmap * lightmap, shadow_visibility + shadow_colour)` before diffuse,
|
||||
normal/detail lighting and specular. This is a component-wise minimum cap, not
|
||||
an extra multiplicative darkening pass. The local native reference's caster path
|
||||
is corroborated by TU3 `0x827EBEF0` (CSM setup), `0x827F82F8` (CSM render) and
|
||||
the character-family shadow shader names in the executable. The animated mesh
|
||||
and board are the caster geometry; no ground decal or circular blob is used.
|
||||
|
||||
Two zero-lux shadow sources separate the receiver contracts. The existing source
|
||||
includes world and player casters, and is sampled by the character. The new
|
||||
source includes only the ten skinned retail player/board pieces on reserved
|
||||
render layer 31. Camera views include that layer; ordinary world geometry stays
|
||||
on layer zero. World families 1–8 and 13 sample the player-only source and apply
|
||||
the recovered minimum clamp. This prevents terrain/building shadows from being
|
||||
applied a second time over the baked map. The source's lightmapped-diffuse flag
|
||||
identifies its role in the GPU light table, so entity ordering does not determine
|
||||
which shadow map the shaders select. Both lights have zero illuminance.
|
||||
|
||||
This reproduces the projected-mesh and lightmap-combination method, **not full
|
||||
console parity**. Shadow map projection/filtering/biases still use Bevy cascades.
|
||||
The original per-frame c8 shadow-colour controller has not been recovered: the
|
||||
adapter currently uses the local irradiance probe's constant RGB coefficient as
|
||||
the ambient shadow floor. At University's starting point that is approximately
|
||||
(0.071711, 0.095925, 0.127945), read from the source sample, not a global preset.
|
||||
The floor updates when the selected probe changes. Native caster exclusions,
|
||||
atlas coverage encoding/blur, shadow colour and pixel matching remain separate
|
||||
parity work. Unsupported world material families still have no custom receiver.
|
||||
|
||||
Offline validation: world, character and character depth shaders pass Naga
|
||||
composition/validation; a windowless ECS regression test verifies the caster-layer
|
||||
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.
|
||||
@@ -89,8 +89,9 @@ selection and regional/streaming state also remain unverified.
|
||||
This is not a claim of pixel parity. Native frame constants are supplied by
|
||||
game hooks in the reference; this engine does not yet recover that controller.
|
||||
Scene exposure 2.5, world material multiplier 1, and tree/proxy values still use
|
||||
documented day-capture defaults. Imported maps have no dynamic directional sun or realtime
|
||||
sun shadows: their world lighting comes from the baked lightmaps. Normal-map
|
||||
documented day-capture defaults. Imported maps have no additive directional sun:
|
||||
their world lighting comes from baked lightmaps, with a separate player-only
|
||||
projected shadow receiver described in the character-lighting notes. Normal-map
|
||||
sign terms use the initial world's authored direction when new sky metadata
|
||||
is available; old packages retain the previous direction. Regional changes
|
||||
and the runtime environment controller are not implemented. Fog selection
|
||||
|
||||
Reference in new issue
Block a user