mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-25 18:05:19 -04:00
Merge PR #95: restore Favela sun shadows and speed up game discovery
This commit is contained in:
@@ -250,6 +250,28 @@ fn game_files(root: &Path) -> impl Iterator<Item = Result<PathBuf, String>> {
|
||||
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<PathBuf>) -> impl Iterator<Item = Result<PathBuf, String>> {
|
||||
let mut pending: VecDeque<_> = roots.into_iter().map(Ok).collect();
|
||||
let mut visited = HashSet::new();
|
||||
@@ -259,6 +281,9 @@ fn files_under(roots: Vec<PathBuf>) -> impl Iterator<Item = Result<PathBuf, Stri
|
||||
Ok(path) => 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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,10 @@ pub struct SmodelGpuPlan {
|
||||
pub lit_material_key: std::collections::HashMap<(Option<assets::MaterialIndex>, u8), u32>,
|
||||
|
||||
pub unlit_material_key: std::collections::HashMap<Option<assets::MaterialIndex>, 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<Option<assets::MaterialIndex>, u32>,
|
||||
pub upload_pending: bool,
|
||||
pub packed_share: Option<Arc<Vec<[u8; asset_iw4::size::GFX_PACKED_VERTEX]>>>,
|
||||
pub index_share: Option<Arc<Vec<u32>>>,
|
||||
@@ -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<assets::MaterialIndex>,
|
||||
catalog: &render_material::RuntimeMaterialCatalog,
|
||||
exact: &[Option<Handle<Image>>],
|
||||
material_sorted_index: Option<u32>,
|
||||
) -> 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<ResMut<crate::prepare::scene::world::WorldScene>>,
|
||||
job: Res<crate::prepare::scene::spawn::WorldSpawnJob>,
|
||||
@@ -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);
|
||||
|
||||
@@ -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