mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-26 02:11:39 -04:00
Sun shadows: script models out of view still cast into it
Favela's hanging laundry is animated script models. They were posed only when the camera's scene-entity walk reached them, so once the clothes left the screen their shadows on the wall below vanished (only the static rope's stayed). IW4 culls scene entities for each sun partition apart from the camera; here a script model the walk hid is kept for the sun shadow only (colour refused, no lighting request) while its bound is inside one of the sun partitions. The partitions are published before the models are posed, from one shared builder the command context now uses as well, so both see the same planes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,7 @@ struct ScriptModelPoseLocals<'w, 's> {
|
||||
remap: Local<'s, Vec<Option<usize>>>,
|
||||
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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<bevy::prelude::Res<crate::prepare::scene::world::MapDirPrimaryLight>>,
|
||||
prepared: bevy::prelude::Res<crate::prepare::scene::view_parms::PreparedSceneView>,
|
||||
scene: Option<bevy::prelude::Res<crate::prepare::scene::world::WorldScene>>,
|
||||
mut volume: bevy::prelude::ResMut<render_scene::SunShadowCasterVolume>,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,17 @@ fn scene_ent_admitted(scene: Option<&crate::GfxScene>, entnum: Option<u32>) -> 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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ impl Plugin for RenderScenePlugin {
|
||||
.init_resource::<RSubwindowDvar>()
|
||||
.init_resource::<RLockPvs>()
|
||||
.init_resource::<SmEnableDvar>()
|
||||
.init_resource::<SmSunEnableDvar>();
|
||||
.init_resource::<SmSunEnableDvar>()
|
||||
.init_resource::<crate::SunShadowCasterVolume>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vec<[f32; 4]>>,
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user