anim lod: a far on-screen rig parks, so a city crowd costs no per-bone work
`animLodDistance` (0..=300 yd, default 60, `0` off) adds a distance leg to the animation-LOD gate: a rig past it parks its pose evaluation even when it is in frame and in a PVS-visible room, joining the frustum and room legs. The gate reads the world camera's `GlobalTransform` for the position. `0` restores the shipped frustum-and-room-only behaviour.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! The animation-LOD gate: a rig off the frustum, or in a room the portal PVS does not reach,
|
||||
//! parks its pose evaluation ([`AnimParked`]) while its clocks run on; every [`RigPose`] is
|
||||
//! covered, GameObject rigs included. The reference elects every scene object each frame
|
||||
//! The animation-LOD gate: a rig off the frustum, past `animLodDistance` yards, or in a room the
|
||||
//! portal PVS does not reach, parks its pose evaluation ([`AnimParked`]) while its clocks run on;
|
||||
//! every [`RigPose`] is covered, GameObject rigs included. The reference elects every scene object each frame
|
||||
//! (`0x683dd0`): a pass-2 unit is neither drawn, animated nor event-ticked, except a
|
||||
//! `MORE_AUDIBLE` creature, re-linked for the tick (`0x607da0`). So a parked rig fires no event
|
||||
//! keys unless flagged, and wakes to the pose its absolute clock dictates (`0x714260` samples the
|
||||
@@ -38,14 +38,16 @@ const RADIUS_SCALE: f32 = 2.0;
|
||||
const RADIUS_PAD: f32 = 4.0;
|
||||
const FALLBACK_RADIUS: f32 = 6.0;
|
||||
|
||||
/// Park or wake each rig by its padded frustum sphere and its room, before [`AnimationSystems`] so
|
||||
/// a wake samples the same frame. The body we drive and its mount never park: the camera rides its
|
||||
/// attachment 17 and it animates faded out in first person. `WOW_NO_ANIM_LOD=1` never parks and
|
||||
/// `WOW_NO_ROOM_LOD=1` drops the room leg.
|
||||
/// Park or wake each rig by its padded frustum sphere, its distance from the camera and its room,
|
||||
/// before [`AnimationSystems`] so a wake samples the same frame. The body we drive and its mount
|
||||
/// never park: the camera rides its attachment 17 and it animates faded out in first person.
|
||||
/// `WOW_NO_ANIM_LOD=1` never parks, `WOW_NO_ROOM_LOD=1` drops the room leg and `animLodDistance 0`
|
||||
/// drops the distance leg.
|
||||
#[allow(clippy::type_complexity)] // one Bevy system's full input set
|
||||
pub(super) fn gate_rig_animation(
|
||||
time: Res<Time>,
|
||||
cam: Query<&Frustum, With<WorldCamera>>,
|
||||
video: Res<crate::video::VideoConfig>,
|
||||
cam: Query<(&Frustum, &GlobalTransform), With<WorldCamera>>,
|
||||
rigs: Query<
|
||||
(
|
||||
Entity,
|
||||
@@ -83,9 +85,12 @@ pub(super) fn gate_rig_animation(
|
||||
// the PVS rejects, even under `WOW_NO_ROOM_LOD`.
|
||||
let cost_on = benilla_world::rig_palette::rig_cost_enabled();
|
||||
let (mut n_rigs, mut n_parked, mut room_out) = (0u32, 0u32, 0u32);
|
||||
let Ok(frustum) = cam.single() else {
|
||||
let Ok((frustum, cam_tf)) = cam.single() else {
|
||||
return;
|
||||
};
|
||||
let cam_pos = cam_tf.translation();
|
||||
let max_dist = video.anim_lod_distance;
|
||||
let max_dist2 = max_dist * max_dist;
|
||||
let now = time.elapsed_secs();
|
||||
for (entity, tf, radius, parked, is_self, mount, room) in &rigs {
|
||||
let exempt =
|
||||
@@ -101,15 +106,19 @@ pub(super) fn gate_rig_animation(
|
||||
},
|
||||
false,
|
||||
);
|
||||
// The room leg: only a rig the frustum leg kept can be room-parked.
|
||||
let room_in = !sphere_in || (no_room && !cost_on) || {
|
||||
// The distance leg: on-screen but past `animLodDistance` parks too. `0` disables it.
|
||||
let dist_in =
|
||||
max_dist <= 0.0 || tf.translation().distance_squared(cam_pos) <= max_dist2;
|
||||
let keep = sphere_in && dist_in;
|
||||
// The room leg: only a rig the frustum + distance legs kept can be room-parked.
|
||||
let room_in = !keep || (no_room && !cost_on) || {
|
||||
let ok = room_pvs_visible(room, &instances, &wmos);
|
||||
if !ok {
|
||||
room_out += 1;
|
||||
}
|
||||
ok || no_room
|
||||
};
|
||||
sphere_in && room_in
|
||||
keep && room_in
|
||||
};
|
||||
if cost_on {
|
||||
n_rigs += 1;
|
||||
@@ -268,8 +277,11 @@ mod tests {
|
||||
/// at +Z are behind it.
|
||||
fn spawn_camera(app: &mut App) {
|
||||
let clip_from_world = Mat4::perspective_rh(1.2, 1.0, 0.1, 1000.0);
|
||||
app.world_mut()
|
||||
.spawn((WorldCamera, Frustum::from_clip_from_world(&clip_from_world)));
|
||||
app.world_mut().spawn((
|
||||
WorldCamera,
|
||||
Frustum::from_clip_from_world(&clip_from_world),
|
||||
GlobalTransform::default(),
|
||||
));
|
||||
}
|
||||
|
||||
fn app() -> App {
|
||||
@@ -281,6 +293,9 @@ mod tests {
|
||||
));
|
||||
app.init_asset::<WmoModel>();
|
||||
app.add_message::<AnimSoundEvent>();
|
||||
// The gate reads `animLodDistance`; the default (60 yd) leaves the frustum tests' rigs
|
||||
// (20 and 50 yd out) inside, so only the frustum/room legs decide them.
|
||||
app.insert_resource(crate::video::VideoConfig::default());
|
||||
// Empty: every parked rig reads unflagged.
|
||||
app.init_resource::<crate::names::NameCache>();
|
||||
// The live order: the gate, the pose evaluation, then the event scan.
|
||||
@@ -563,6 +578,36 @@ mod tests {
|
||||
assert_eq!(frozen, bone(&app, joint), "the parked GO rig's bones hold");
|
||||
}
|
||||
|
||||
/// An in-frame rig past `animLodDistance` parks on the distance leg, and wakes when it comes
|
||||
/// inside — the on-screen-but-far crowd a city needs parked.
|
||||
#[test]
|
||||
fn a_far_in_frame_rig_parks_on_the_distance_leg() {
|
||||
let mut app = app();
|
||||
spawn_camera(&mut app);
|
||||
app.world_mut()
|
||||
.resource_mut::<crate::video::VideoConfig>()
|
||||
.anim_lod_distance = 30.0;
|
||||
let (far, joint) = spawn_rig(&mut app, -Vec3::Z * 50.0); // in frame, 50 yd out
|
||||
advance(&mut app, PARK_AFTER_SECS + 0.2, 4);
|
||||
assert!(
|
||||
app.world().entity(far).contains::<AnimParked>(),
|
||||
"in-frame but past animLodDistance ⇒ parked"
|
||||
);
|
||||
let frozen = bone(&app, joint);
|
||||
advance(&mut app, DUR * 0.35, 2);
|
||||
assert_eq!(frozen, bone(&app, joint), "the distance-parked bone holds");
|
||||
|
||||
// Inside the distance: wake instantly and sample again.
|
||||
app.world_mut()
|
||||
.entity_mut(far)
|
||||
.insert(GlobalTransform::from_translation(-Vec3::Z * 20.0));
|
||||
app.update();
|
||||
assert!(
|
||||
!app.world().entity(far).contains::<AnimParked>(),
|
||||
"inside animLodDistance ⇒ instant wake"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_self_avatar_is_exempt() {
|
||||
let mut app = app();
|
||||
|
||||
@@ -685,6 +685,11 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
"1",
|
||||
"benilla's own: multiplier on the shadow caster-collection reach, 0.25..2 (1 = unchanged)",
|
||||
),
|
||||
ours(
|
||||
"animLodDistance",
|
||||
"60",
|
||||
"benilla's own: park a rig's pose evaluation past this many yards, 0..300 (0 = frustum/room only)",
|
||||
),
|
||||
// MONKEY (moon shadows): how dark a MOON-shadowed fragment is allowed to get at night — the
|
||||
// dial over the night lane's own shadow term (`benilla_world::lighting::MoonShadowStrength`,
|
||||
// bridged by `dynamic_interior` beside the spell gain). `0` is the FAITHFUL null: every
|
||||
|
||||
@@ -68,6 +68,11 @@ pub(crate) enum DisplayMode {
|
||||
/// The windowed size a fresh config gets.
|
||||
pub(crate) const DEFAULT_WINDOWED: UVec2 = UVec2::new(1600, 900);
|
||||
|
||||
/// `animLodDistance`: yards past which a rig's pose evaluation parks. 60 is past the range a
|
||||
/// city-crowd member reads as more than a speck; `0` disables the distance leg.
|
||||
pub(crate) const ANIM_LOD_DISTANCE_DEFAULT: f32 = 60.0;
|
||||
pub(crate) const ANIM_LOD_DISTANCE_RANGE: std::ops::RangeInclusive<f32> = 0.0..=300.0;
|
||||
|
||||
/// `gxWindow`'s value to the mode, by the reference's 0/1 parse (int, `!= 0`); shared by
|
||||
/// [`crate::cvars`]'s arm and the boot read.
|
||||
pub(crate) fn display_from_flag(v: f32) -> DisplayMode {
|
||||
@@ -199,6 +204,12 @@ pub(crate) struct VideoConfig {
|
||||
/// proxies. Trimming it is the direct dial on caster POPULATION — the input to both lanes'
|
||||
/// per-rebuild cost — at the risk of a tall caster's shadow popping in as you approach.
|
||||
pub(crate) shadow_caster_reach: f32,
|
||||
/// MONKEY (anim LOD): park a rig's pose evaluation past this distance in yards
|
||||
/// (`animLodDistance`, 0..=300, default 60). Off-frustum rigs and dark-room rigs already park
|
||||
/// (`creature_anim::lod`); this adds the on-screen-but-far leg, so a city crowd past the
|
||||
/// horizon of interest costs no per-bone evaluation, no palette rebuild and no transform
|
||||
/// propagation. `0` disables the distance leg (frustum + room only — the shipped behaviour).
|
||||
pub(crate) anim_lod_distance: f32,
|
||||
/// MONKEY (moon shadows): how dark a MOON-shadowed fragment is allowed to get at night
|
||||
/// (`moonShadowStrength`, 0..1, default 0.35 = the fragment keeps 65 % of the night
|
||||
/// directional term). Bridged to [`benilla_world::lighting::MoonShadowStrength`] by
|
||||
@@ -359,6 +370,7 @@ impl Default for VideoConfig {
|
||||
character_shadow_rate: crate::shadow_core::DEFAULT_SHADOW_RATE,
|
||||
world_shadow_rate: crate::shadow_core::DEFAULT_SHADOW_RATE,
|
||||
shadow_caster_reach: 1.0,
|
||||
anim_lod_distance: ANIM_LOD_DISTANCE_DEFAULT,
|
||||
// MONKEY (moon shadows): 0.35 — a hint of a silhouette, not a daylight-hard shadow.
|
||||
moon_shadow_strength: 0.35,
|
||||
// The cvar defaults are the source of truth at load; these only stand in until then.
|
||||
@@ -535,6 +547,13 @@ pub(crate) fn on_cvar(
|
||||
*crate::shadow_core::CASTER_REACH_RANGE.end(),
|
||||
)
|
||||
}
|
||||
// `0` is meaningful: the distance leg off, frustum + room only (the shipped behaviour).
|
||||
"animloddistance" => {
|
||||
cfg.anim_lod_distance = v.clamp(
|
||||
*ANIM_LOD_DISTANCE_RANGE.start(),
|
||||
*ANIM_LOD_DISTANCE_RANGE.end(),
|
||||
)
|
||||
}
|
||||
// MONKEY (moon shadows): the night lane's shadow darkness. 0 IS meaningful (the lane off,
|
||||
// bit-identical to the pre-feature night), so it floors at 0 rather than at a
|
||||
// minimum-useful value; 1 is a daylight-hard silhouette by moonlight.
|
||||
|
||||
Reference in New Issue
Block a user