mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-26 10:19:50 -04:00
Draw effect model pieces such as cash register bills
Effect model pieces (the bills and halves of a shot cash register, food,
luggage contents, plant pots, glass pieces) have not drawn since 69cf9ff
moved the effect model geometry into the Plan phase of the world spawn.
That phase runs before world_occupancy::place builds the model lighting
atlas, so prepare_fx_model_geometry found no atlas and quietly returned an
empty plan, and every model elem that passed its LOD and cull checks was
dropped for want of geometry.
The geometry is now built in spawn_world_finish straight after
world_occupancy::place. The Plan phase still installs an empty plan so its
readers find the resource. A missing atlas is now a warning with the model
count, and one line reports the models and LODs prepared and refused.
The bills had a second fault. mp_terminal stores `,fx_cash01` as a
placeholder for common_mp's fx_cash01, and the effect catalog kept the
comma in the model hint, so the edge never bound. Model hints now use the
bare name (AssetRef::bare_name), so fx_cash01 resolves against common_mp.
One model whose packed rows do not match its decoded vertices would make
the packed payload of all effect model geometry Unavailable, and the merged
xmodel lane then drops every xmodel draw (viewmodel, bodies, script models)
for the frame. Such a LOD is now refused and logged when the geometry is
built, and fill_fx_model_plan only draws effect models from geometry whose
Iw4 packed rows cover every decoded vertex.
The player's own first-person shell casings are still skipped by the
render-flag check in fill_fx_model_plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1670,7 +1670,8 @@ fn sound_visual(name: String) -> OwnedFxVisual {
|
||||
}
|
||||
|
||||
fn model_visual(name: Option<String>) -> OwnedFxVisual {
|
||||
match name.filter(|n| !n.is_empty()) {
|
||||
let hint = name.map(|name| crate::AssetRef::bare_name(&name).to_owned());
|
||||
match hint.filter(|n| !n.is_empty()) {
|
||||
Some(hint) => OwnedFxVisual::Model {
|
||||
edge: AssetEdge::Unresolved(AssetEdgeReason::CatalogMiss),
|
||||
hint: Some(hint),
|
||||
|
||||
@@ -1214,6 +1214,11 @@ fn fill_fx_model_plan(
|
||||
continue;
|
||||
};
|
||||
let asset_surfaces = asset.surfaces.clone();
|
||||
// A draw without exact packed rows drops the whole merged xmodel lane.
|
||||
if !plan.packed_exact() {
|
||||
plan.skipped_no_material = plan.skipped_no_material.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
let box_half = entry.skel.radius.and_then(|radius| {
|
||||
crate::prepare::scene::model_lighting_cache::lighting_box_half(
|
||||
|
||||
@@ -185,10 +185,9 @@ pub(crate) fn spawn_world(
|
||||
prepared: (
|
||||
Res<render_anim::PreparedFpv>,
|
||||
Res<render_anim::PreparedModelMaterials>,
|
||||
Option<Res<render_fx::PreparedFxModels>>,
|
||||
),
|
||||
) {
|
||||
let (fpv, model_materials, fx_models) = prepared;
|
||||
let (fpv, model_materials) = prepared;
|
||||
if scene.readiness.generation != job.spawn || job.spawn.0.is_none() {
|
||||
return;
|
||||
}
|
||||
@@ -697,8 +696,7 @@ pub(crate) fn spawn_world(
|
||||
tracers.as_deref(),
|
||||
fx_catalog.as_deref(),
|
||||
);
|
||||
let fx_geometry =
|
||||
super::world_plan::prepare_fx_model_geometry(&scene, fx_models.as_deref());
|
||||
let fx_geometry = super::world_plan::empty_fx_model_geometry(&scene);
|
||||
commands.insert_resource(fx_geometry.0.clone());
|
||||
commands.insert_resource(fx_geometry);
|
||||
tess.material_images = std::sync::Arc::new(job.images.exact_handles().to_vec());
|
||||
@@ -726,6 +724,7 @@ pub(crate) fn spawn_world_finish(
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut job: ResMut<WorldSpawnJob>,
|
||||
mut tess: ResMut<render_scene::TessMaterials>,
|
||||
fx_models: Option<Res<render_fx::PreparedFxModels>>,
|
||||
) {
|
||||
if scene.readiness.generation != job.spawn
|
||||
|| job.spawn.0.is_none()
|
||||
@@ -747,6 +746,10 @@ pub(crate) fn spawn_world_finish(
|
||||
job.images.probe_handles().to_vec(),
|
||||
job.images.lightmap_handles().to_vec(),
|
||||
);
|
||||
// Needs the model lighting atlas, which world_occupancy::place builds.
|
||||
let fx_geometry = super::world_plan::prepare_fx_model_geometry(&scene, fx_models.as_deref());
|
||||
commands.insert_resource(fx_geometry.0.clone());
|
||||
commands.insert_resource(fx_geometry);
|
||||
tess.material_images = std::sync::Arc::new(job.images.exact_handles().to_vec());
|
||||
job.last_work_ms = frame_started.elapsed().as_secs_f32() * 1000.0;
|
||||
if paced {
|
||||
|
||||
@@ -236,24 +236,39 @@ pub(crate) fn world_color_images(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn empty_fx_model_geometry(scene: &WorldScene) -> render_fx::PreparedFxModelGeometry {
|
||||
let mut plan = render_fx::FxModelDrawPlan::default();
|
||||
plan.generation = scene.runtime_material_catalog.generation_id().get();
|
||||
render_fx::PreparedFxModelGeometry(plan)
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_fx_model_geometry(
|
||||
scene: &WorldScene,
|
||||
models: Option<&render_fx::PreparedFxModels>,
|
||||
) -> render_fx::PreparedFxModelGeometry {
|
||||
let mut plan = render_fx::FxModelDrawPlan::default();
|
||||
plan.generation = scene.runtime_material_catalog.generation_id().get();
|
||||
let render_fx::PreparedFxModelGeometry(mut plan) = empty_fx_model_geometry(scene);
|
||||
let Some(models) = models else {
|
||||
diag::info!(
|
||||
World,
|
||||
"FX model geometry: no effect model catalog, nothing prepared"
|
||||
);
|
||||
return render_fx::PreparedFxModelGeometry(plan);
|
||||
};
|
||||
let models = &models.0;
|
||||
let Some((image, dims)) = scene
|
||||
.model_lighting_image
|
||||
.clone()
|
||||
.zip(scene.model_lighting_dims)
|
||||
else {
|
||||
diag::warn!(
|
||||
World,
|
||||
"FX model geometry refused: no model lighting atlas ({} models; the map has no light grid, or this ran before world occupancy); effect model pieces will not draw",
|
||||
models.len()
|
||||
);
|
||||
return render_fx::PreparedFxModelGeometry(plan);
|
||||
};
|
||||
let atlas = super::model_lighting_atlas::WorldModelLightingAtlas { image, dims };
|
||||
let models = &models.0;
|
||||
let (mut prepared, mut refused, mut lods_prepared, mut lods_refused) = (0u32, 0u32, 0u32, 0u32);
|
||||
for index in 0..models.len() {
|
||||
let Some(entry) = models.get_at(index) else {
|
||||
continue;
|
||||
@@ -270,8 +285,10 @@ pub(crate) fn prepare_fx_model_geometry(
|
||||
})();
|
||||
let Some((dobj, request, skin)) = binding else {
|
||||
diag::warn!(World, "FX model `{name}` refused: bind pose unavailable");
|
||||
refused += 1;
|
||||
continue;
|
||||
};
|
||||
let lods_before = lods_prepared;
|
||||
for lod in 0..entry
|
||||
.skel
|
||||
.lod
|
||||
@@ -303,8 +320,19 @@ pub(crate) fn prepare_fx_model_geometry(
|
||||
World,
|
||||
"FX model `{name}` LOD {lod} refused: geometry unavailable"
|
||||
);
|
||||
lods_refused += 1;
|
||||
continue;
|
||||
};
|
||||
if let Some((surface, packed, decoded)) =
|
||||
render_fx::fx_model_packed_mismatch(&lod_surfaces)
|
||||
{
|
||||
diag::warn!(
|
||||
World,
|
||||
"FX model `{name}` LOD {lod} refused: surface {surface} has {packed} packed vertices for {decoded} decoded"
|
||||
);
|
||||
lods_refused += 1;
|
||||
continue;
|
||||
}
|
||||
let materials: Vec<_> = lod_surfaces
|
||||
.iter()
|
||||
.map(|surface| {
|
||||
@@ -320,11 +348,24 @@ pub(crate) fn prepare_fx_model_geometry(
|
||||
World,
|
||||
"FX model `{name}` LOD {lod} refused: material binding unavailable"
|
||||
);
|
||||
lods_refused += 1;
|
||||
continue;
|
||||
}
|
||||
render_fx::append_fx_model_asset(&mut plan, index, lod, &lod_surfaces, &materials);
|
||||
lods_prepared += 1;
|
||||
}
|
||||
if lods_prepared > lods_before {
|
||||
prepared += 1;
|
||||
} else {
|
||||
refused += 1;
|
||||
}
|
||||
}
|
||||
diag::info!(
|
||||
World,
|
||||
"FX model geometry: models prepared={prepared} refused={refused} (LODs prepared={lods_prepared} refused={lods_refused}) vertices={} packed_exact={}",
|
||||
plan.vertices().len(),
|
||||
plan.packed_exact()
|
||||
);
|
||||
render_fx::PreparedFxModelGeometry(plan)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ pub use host::{
|
||||
PreparedFxElemInfos, PreparedFxModels, PreparedImpactFx, PresentedVehicleFx,
|
||||
PresentedVehicleFxRow,
|
||||
};
|
||||
pub use model_append::append_fx_model_asset;
|
||||
pub use model_append::{append_fx_model_asset, fx_model_packed_mismatch};
|
||||
pub use model_draw::{FxModelAssetDraw, FxModelDrawPlan, XMODEL_OBJECT_ID_FX_BASE};
|
||||
pub use plugin::RenderFxPlugin;
|
||||
pub use product::{
|
||||
|
||||
@@ -7,6 +7,19 @@ pub const XMODEL_PACKED_UNAVAILABLE: &str =
|
||||
"xmodel merge GfxPackedVertex missing or count-mismatched; decoded float is not packed VB";
|
||||
pub const XMODEL_PACKED_EMPTY_PLAN: &str = "xmodel plan has no vertices";
|
||||
|
||||
pub fn fx_model_packed_mismatch(
|
||||
surfaces: &[render_anim::fpv_pose::PosedModelSurface],
|
||||
) -> Option<(usize, usize, usize)> {
|
||||
surfaces.iter().find_map(|surface| {
|
||||
let decoded = surface
|
||||
.mesh
|
||||
.attribute(bevy::prelude::Mesh::ATTRIBUTE_POSITION)
|
||||
.map_or(0, |positions| positions.len());
|
||||
let packed = surface.packed_vertices.len();
|
||||
(packed != decoded).then_some((surface.surface_index, packed, decoded))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn append_fx_model_asset(
|
||||
plan: &mut FxModelDrawPlan,
|
||||
model_index: usize,
|
||||
|
||||
@@ -74,6 +74,13 @@ impl FxModelDrawPlan {
|
||||
&self.geometry.packed_vertices
|
||||
}
|
||||
|
||||
pub fn packed_exact(&self) -> bool {
|
||||
matches!(
|
||||
&self.geometry.packed_vertices,
|
||||
asset_world::PackedVertexPayload::Iw4(rows) if rows.len() == self.geometry.vertices.len()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.draws.clear();
|
||||
self.generated = 0;
|
||||
|
||||
Reference in New Issue
Block a user