diff --git a/crates/asset_transport/src/discover.rs b/crates/asset_transport/src/discover.rs index c715197..cf35515 100644 --- a/crates/asset_transport/src/discover.rs +++ b/crates/asset_transport/src/discover.rs @@ -250,6 +250,28 @@ fn game_files(root: &Path) -> impl Iterator> { files_under(search_roots(root)) } +/// Folders the game never keeps zones in: cargo's output when the games root is the build's own +/// `target/play` (tens of thousands of files, walked on every discovery), and our own +/// artifacts. +const NOT_GAME_DIRS: [&str; 6] = [ + "incremental", + "deps", + "build", + ".fingerprint", + "examples", + "iw4l-artifacts", +]; + +fn is_not_game_dir(path: &Path) -> bool { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| { + NOT_GAME_DIRS + .iter() + .any(|skip| name.eq_ignore_ascii_case(skip)) + }) +} + fn files_under(roots: Vec) -> impl Iterator> { let mut pending: VecDeque<_> = roots.into_iter().map(Ok).collect(); let mut visited = HashSet::new(); @@ -259,6 +281,9 @@ fn files_under(roots: Vec) -> impl Iterator path, Err(error) => return Some(Err(error)), }; + if is_not_game_dir(&path) { + continue; + } let canonical = match std::fs::canonicalize(&path) { Ok(path) => path, Err(error) => { 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/retained_list/smodel.rs b/crates/render_frontend/src/assemble/drawsurf/retained_list/smodel.rs index bf94367..a7e31a6 100644 --- a/crates/render_frontend/src/assemble/drawsurf/retained_list/smodel.rs +++ b/crates/render_frontend/src/assemble/drawsurf/retained_list/smodel.rs @@ -366,7 +366,8 @@ pub(super) fn expand_smodel_destination( .lit_material_key .get(&(authored, placement.reflection_probe_index)) .copied() - .or_else(|| smodel_plan.unlit_material_key.get(&authored).copied()), + .or_else(|| smodel_plan.unlit_material_key.get(&authored).copied()) + .or_else(|| smodel_plan.shadow_material_key.get(&authored).copied()), }; let Some(material) = material else { continue; @@ -472,7 +473,8 @@ fn push_smodel_destination( .lit_material_key .get(&(authored, placement.reflection_probe_index)) .copied() - .or_else(|| smodel_plan.unlit_material_key.get(&authored).copied()), + .or_else(|| smodel_plan.unlit_material_key.get(&authored).copied()) + .or_else(|| smodel_plan.shadow_material_key.get(&authored).copied()), }; let Some(material) = material else { return stats; 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/smodel.rs b/crates/render_frontend/src/assemble/drawsurf/tess/smodel.rs index f5b5661..0e7c3a1 100644 --- a/crates/render_frontend/src/assemble/drawsurf/tess/smodel.rs +++ b/crates/render_frontend/src/assemble/drawsurf/tess/smodel.rs @@ -95,6 +95,10 @@ pub struct SmodelGpuPlan { pub lit_material_key: std::collections::HashMap<(Option, u8), u32>, pub unlit_material_key: std::collections::HashMap, u32>, + + /// Shadow-caster surfaces (a tree's shadow mesh): drawn into the shadow maps only, so kept + /// out of the colour keys above. + pub shadow_material_key: std::collections::HashMap, u32>, pub upload_pending: bool, pub packed_share: Option>>, pub index_share: Option>>, @@ -490,6 +494,45 @@ pub fn expand_smodel_cached_vertices( plan.cached_skip_n = 0; } +/// Lists a shadow-caster surface's material for the shadow passes, and says whether it was one. +/// MW2 trees cast their sun shadow through such a surface alone, so dropping it with the colour +/// pass left them shadowless (only the lightmap's blocky shadow under them stayed). +fn register_shadow_only_material( + plan: &mut SmodelGpuPlan, + authored: Option, + catalog: &render_material::RuntimeMaterialCatalog, + exact: &[Option>], + material_sorted_index: Option, +) -> bool { + let Some(derived) = authored.and_then(|id| catalog.derived(id)) else { + return false; + }; + if !derived.shadow_only { + return false; + } + if !plan.shadow_material_key.contains_key(&authored) { + let maps = runtime_maps(authored, catalog, exact); + let mat_idx = plan.materials.len() as u32; + plan.materials.push(SmodelPassMaterial { + model_lighting_required: false, + color: maps.color, + specular: None, + probe: None, + atlas: None, + alpha_mode: maps.alpha_mode, + draw_mode: maps.draw_mode, + cull_mode: maps.cull_mode, + env_map_parms: maps.env_map_parms, + lighting_lookup_scale: [0.0; 4], + atlas_lookup: [0.0; 4], + sort_key: derived.sort_key, + material_sorted_index, + }); + plan.shadow_material_key.insert(authored, mat_idx); + } + true +} + pub(crate) fn build_smodel_gpu_plan( mut scene: Option>, job: Res, @@ -635,10 +678,13 @@ pub(crate) fn build_smodel_gpu_plan( if plan.lit_material_key.contains_key(&key) { continue; } - if authored - .and_then(|id| scene.runtime_material_catalog.derived(id)) - .is_some_and(|m| m.shadow_only) - { + if register_shadow_only_material( + &mut plan, + authored, + &scene.runtime_material_catalog, + exact, + sorted_ordinal(authored), + ) { continue; } let maps = runtime_maps(authored, &scene.runtime_material_catalog, exact); @@ -694,10 +740,13 @@ pub(crate) fn build_smodel_gpu_plan( if plan.unlit_material_key.contains_key(&authored) { continue; } - if authored - .and_then(|id| scene.runtime_material_catalog.derived(id)) - .is_some_and(|m| m.shadow_only) - { + if register_shadow_only_material( + &mut plan, + authored, + &scene.runtime_material_catalog, + exact, + sorted_ordinal(authored), + ) { continue; } let maps = runtime_maps(authored, &scene.runtime_material_catalog, exact); 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,