diff --git a/crates/render_anim/src/occupancy/script_model.rs b/crates/render_anim/src/occupancy/script_model.rs index 3c64f8b..ae7cf6b 100644 --- a/crates/render_anim/src/occupancy/script_model.rs +++ b/crates/render_anim/src/occupancy/script_model.rs @@ -76,6 +76,7 @@ struct ScriptModelPoseLocals<'w, 's> { remap: Local<'s, Vec>>, lod_skinned: Res<'w, render_scene::LodRampSkinnedDvar>, gfx_scene: Res<'w, HostGfxScene>, + sun_casters: Res<'w, render_scene::SunShadowCasterVolume>, } #[derive(SystemParam)] @@ -588,6 +589,7 @@ fn pose_script_models( mut remap, lod_skinned, gfx_scene, + sun_casters, } = locals; live_assets.clear(); live_ids.clear(); @@ -634,7 +636,13 @@ fn pose_script_models( let entnum = owner.gentity_number.map(u32::from); let slot = entnum.map(|entnum| script_scene_slot(&gfx_scene.scene, entnum)); - if let Some(why) = slot.as_ref().and_then(|slot| slot.skip) { + // Out of the camera's walk, a model can still shade what the camera sees (laundry + // above the view on the wall below it): it stays, for the sun shadow only, while it is + // inside a sun partition. + let walk_hidden = entnum.is_some_and(|entnum| gfx_scene.scene.scene_ent_hidden(entnum)); + if let Some(why) = slot.as_ref().and_then(|slot| slot.skip) + && !walk_hidden + { if let Some(id) = focused_owner_id { focus.refuse(id, &owner.current_model.0, why); } @@ -647,6 +655,18 @@ fn pose_script_models( continue; }; let origin = transform.translation.to_array(); + let radius = skels + .first() + .and_then(|skel| skel.radius) + .unwrap_or(1.0) + .max(1.0); + let caster_bound = render_scene::XModelCasterBound { origin, radius }; + if walk_hidden && !sun_casters.admits(caster_bound) { + if let Some(id) = focused_owner_id { + focus.refuse(id, &owner.current_model.0, "scene_ent_hidden"); + } + continue; + } if lod_culled(&skels, origin, eye, skinned_ramp) { if let Some(id) = focused_owner_id { @@ -732,11 +752,6 @@ fn pose_script_models( }; let box_half = crate::script_model_lighting_box_half(&assets, &owner.dobj_state.composition.models); - let radius = skels - .first() - .and_then(|skel| skel.radius) - .unwrap_or(1.0) - .max(1.0); product.rows.push(ScriptOwnerRow { entity, asset_index, @@ -744,10 +759,10 @@ fn pose_script_models( model: owner.current_model.clone(), world_from_local: transform.to_matrix(), entnum, - camera_hidden: sphere_behind_frustum(origin, radius, &planes), + camera_hidden: walk_hidden || sphere_behind_frustum(origin, radius, &planes), lighting_origin: owner.lighting_origin, lookup_fallback: atpoint.fallback(owner.lighting_origin, box_half), - caster_bound: render_scene::XModelCasterBound { origin, radius }, + caster_bound, }); } persist.by_id.retain(|id, _| live_ids.contains(id)); diff --git a/crates/render_frontend/src/assemble/drawsurf/command_context.rs b/crates/render_frontend/src/assemble/drawsurf/command_context.rs index 7b956c0..3b3d631 100644 --- a/crates/render_frontend/src/assemble/drawsurf/command_context.rs +++ b/crates/render_frontend/src/assemble/drawsurf/command_context.rs @@ -618,27 +618,10 @@ pub(crate) fn update_command_context_code_sources( if let Some(scene) = scene.as_deref() && let Some(bounds) = scene.world_bounds { - let world_mid = [bounds[0], bounds[1], bounds[2]]; - let world_half = [bounds[3], bounds[4], bounds[5]]; - let shadow_forward = super::sun_shadow_forward_from_light_dir(light.direction); - let world = prepared.view_from_world.inverse(); - let forward = world.transform_vector3(Vec3::NEG_Z).normalize_or_zero(); - let right = world.transform_vector3(Vec3::X).normalize_or_zero(); - let up = world.transform_vector3(Vec3::Y).normalize_or_zero(); - let (tx, ty) = super::tan_half_fov_from_clip(clip_from_view); - let frame = super::forced_fallback_frame( - shadow_forward, - super::SunShadowCamera { - origin: eye.to_array(), - forward: forward.to_array(), - right: right.to_array(), - up: up.to_array(), - tan_half_fov_x: tx, - tan_half_fov_y: ty, - z_near: prepared.near, - }, - world_mid, - world_half, + let frame = super::sun_shadow_frame_for_view( + light.direction, + &prepared, + bounds, scene.sun_sample_size_near, ); produce_sun_shadow_receiver_constants(&mut mat_frame.code_sources, frame); diff --git a/crates/render_frontend/src/assemble/drawsurf/sun_shadow.rs b/crates/render_frontend/src/assemble/drawsurf/sun_shadow.rs index f75f1e5..a5f0f98 100644 --- a/crates/render_frontend/src/assemble/drawsurf/sun_shadow.rs +++ b/crates/render_frontend/src/assemble/drawsurf/sun_shadow.rs @@ -553,3 +553,60 @@ pub fn forced_fallback_frame( projection_offset_clip, } } + +/// The sun frame for the prepared view. The command context builds the frame with it, and so +/// does the caster volume published before the models are posed, so both see the same +/// partitions. +pub fn sun_shadow_frame_for_view( + light_direction: [f32; 3], + prepared: &crate::prepare::scene::view_parms::PreparedSceneView, + world_bounds: [f32; 6], + sample_near: f32, +) -> SunShadowForcedFrame { + let world = prepared.view_from_world.inverse(); + let axis = |local: bevy::math::Vec3| world.transform_vector3(local).normalize_or_zero(); + let (tan_half_fov_x, tan_half_fov_y) = tan_half_fov_from_clip(prepared.clip_from_view); + forced_fallback_frame( + sun_shadow_forward_from_light_dir(light_direction), + SunShadowCamera { + origin: prepared.eye.to_array(), + forward: axis(bevy::math::Vec3::NEG_Z).to_array(), + right: axis(bevy::math::Vec3::X).to_array(), + up: axis(bevy::math::Vec3::Y).to_array(), + tan_half_fov_x, + tan_half_fov_y, + z_near: prepared.near, + }, + [world_bounds[0], world_bounds[1], world_bounds[2]], + [world_bounds[3], world_bounds[4], world_bounds[5]], + sample_near, + ) +} + +/// Publishes this frame's sun caster volumes (see [`render_scene::SunShadowCasterVolume`]) +/// from the inputs the command context builds the sun frame from. +pub(crate) fn publish_sun_shadow_caster_volume( + dir_light: Option>, + prepared: bevy::prelude::Res, + scene: Option>, + mut volume: bevy::prelude::ResMut, +) { + let scene = scene.as_deref(); + let partitions = match ( + dir_light.as_deref(), + scene.and_then(|scene| scene.world_bounds), + ) { + (Some(light), Some(bounds)) if prepared.ready => { + let sample_near = scene.map_or(0.25, |scene| scene.sun_sample_size_near); + sun_shadow_frame_for_view(light.direction, &prepared, bounds, sample_near) + .partitions + .iter() + .map(|partition| partition.clip_planes.as_slice().to_vec()) + .collect() + } + _ => Vec::new(), + }; + if volume.partitions != partitions { + volume.partitions = partitions; + } +} diff --git a/crates/render_frontend/src/assemble/drawsurf/tess/xmodel.rs b/crates/render_frontend/src/assemble/drawsurf/tess/xmodel.rs index b968308..bb4f97e 100644 --- a/crates/render_frontend/src/assemble/drawsurf/tess/xmodel.rs +++ b/crates/render_frontend/src/assemble/drawsurf/tess/xmodel.rs @@ -176,6 +176,17 @@ fn scene_ent_admitted(scene: Option<&crate::GfxScene>, entnum: Option) -> b } } +/// A script model out of the camera's walk is drawn only into the sun shadow: its producer +/// keeps it, colour refused, while it is inside a sun partition. +#[must_use] +fn script_draw_admitted(scene: Option<&crate::GfxScene>, draw: &XModelSurfaceDraw) -> bool { + scene_ent_admitted(scene, draw.scene_entnum) + || (draw.colour_refusal.is_some() + && draw + .scene_entnum + .is_some_and(|n| scene.is_some_and(|s| s.scene_ent_hidden(n)))) +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct XModelTopologyKey { generation: [u64; 8], @@ -352,7 +363,7 @@ fn xmodel_merge_stamp( scene_ent_admitted(scene, draw.scene_entnum).hash(&mut occupancy); } for draw in scripts.draws() { - scene_ent_admitted(scene, draw.scene_entnum).hash(&mut occupancy); + script_draw_admitted(scene, draw).hash(&mut occupancy); } for draw in missiles.draws() { scene_ent_admitted(scene, draw.scene_entnum).hash(&mut occupancy); @@ -588,7 +599,7 @@ pub fn merge_xmodel_draw_plan( .draws() .iter() .copied() - .filter(|d| scene_ent_admitted(scene, d.scene_entnum)), + .filter(|d| script_draw_admitted(scene, d)), ); append_admitted_source( @@ -1014,7 +1025,7 @@ fn refresh_concat_draws( .draws() .iter() .copied() - .filter(|d| scene_ent_admitted(scene, d.scene_entnum)), + .filter(|d| script_draw_admitted(scene, d)), ); append_concat_owner_draws( merged, @@ -1091,7 +1102,7 @@ fn admitted_mask( if scripts .draws() .iter() - .any(|d| scene_ent_admitted(scene, d.scene_entnum)) + .any(|d| script_draw_admitted(scene, d)) { admitted |= ADMIT_SCRIPT; } diff --git a/crates/render_frontend/src/prepare/mod.rs b/crates/render_frontend/src/prepare/mod.rs index fb711db..30af02c 100644 --- a/crates/render_frontend/src/prepare/mod.rs +++ b/crates/render_frontend/src/prepare/mod.rs @@ -196,6 +196,13 @@ impl Plugin for RenderPreparePlugin { .after(crate::prepare::scene::sun_stage::update_active_sun_stage) .in_set(WorkerCmdSet::CellStatic), ) + .add_systems( + Update, + crate::assemble::drawsurf::publish_sun_shadow_caster_volume.in_set(frame::InMatch) + .after(stamp_prepared_scene_view) + .after(crate::prepare::scene::sun_stage::update_active_sun_stage) + .before(crate::adapters::anim::script_model::ScriptModelSkinSet), + ) .add_systems( Update, crate::prepare::scene::sun_stage::update_active_sun_stage.in_set(frame::InMatch) diff --git a/crates/render_scene/src/lib.rs b/crates/render_scene/src/lib.rs index 52d6db4..4e00195 100644 --- a/crates/render_scene/src/lib.rs +++ b/crates/render_scene/src/lib.rs @@ -52,4 +52,6 @@ pub use view_parms::{ pack_live_view_parms, prepare_scene_view, }; pub use world_instances::{WorldDynEntInstance, WorldScriptModelInstance}; -pub use xmodel_draw::{XModelCasterBound, XModelColourRefusal, XModelSurfaceDraw}; +pub use xmodel_draw::{ + SunShadowCasterVolume, XModelCasterBound, XModelColourRefusal, XModelSurfaceDraw, +}; diff --git a/crates/render_scene/src/plugin.rs b/crates/render_scene/src/plugin.rs index 00ed145..cbf3443 100644 --- a/crates/render_scene/src/plugin.rs +++ b/crates/render_scene/src/plugin.rs @@ -33,6 +33,7 @@ impl Plugin for RenderScenePlugin { .init_resource::() .init_resource::() .init_resource::() - .init_resource::(); + .init_resource::() + .init_resource::(); } } diff --git a/crates/render_scene/src/xmodel_draw.rs b/crates/render_scene/src/xmodel_draw.rs index 152109f..e1e891d 100644 --- a/crates/render_scene/src/xmodel_draw.rs +++ b/crates/render_scene/src/xmodel_draw.rs @@ -56,6 +56,22 @@ impl XModelCasterBound { } } +/// The volumes each sun partition collects its casters from this frame, published before the +/// models are posed (empty while sun shadows are off). A model the camera does not see still +/// shades what it does see while its bound is inside one of them, as IW4 culls scene entities +/// for each sun partition apart from the camera. +#[derive(Resource, Clone, Debug, Default, PartialEq)] +pub struct SunShadowCasterVolume { + pub partitions: Vec>, +} + +impl SunShadowCasterVolume { + #[must_use] + pub fn admits(&self, bound: XModelCasterBound) -> bool { + self.partitions.iter().any(|planes| !bound.outside(planes)) + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum XModelColourRefusal { CameraFrustum,