From a2f3f830e6a853af4c4f3547fccaf2a6c3879c82 Mon Sep 17 00:00:00 2001 From: JohnM <234386938+treuenten@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:13:29 +0800 Subject: [PATCH] Draw spark fountains where they were sprayed, at their own size Shooting a vending machine on mp_terminal drew large flat yellow, white and red sheets next to it and made the game stutter. A spark fountain that runs relative to its effect was sprayed from the effect-local spawn origin, and the custom draw then placed the cells with the cloud matrix T(pos) R(quat) S(vis scale). Prop sparks (props/credit_card_touchpad_exp on the card touchpads, and explosions/powerlines_* from the vending machine's tv_explosion) carry a vis scale of 150, so every spark was drawn 150 times too big; barrelexp and small_vehicle_explosion (scale 10) were 10 times too big. New cells also had every sample time at 0, which ends the ribbon at origins[3]: about 1,780 untraced cells stacked one 150-unit quad on the effect point until the tracer, 16 cells per update, reached them. The spray now gets the world spawn point (spawn_origin_world), untraced cells start as an unobstructed arc (spark_fountain_integrate_miss_cell), and the custom draw is submitted with a unit placement (spark_fountain_draw_cloud). The cells are already in world space and the fountain shader only applies view and projection, so the vis scale stays a draw gate and no longer scales the vertices. Bounce traces now run in the same space as world collision. The elem keeps drifting on its velocity graph (0.4 units/ms on most fountains) while the sparks stay where they were sprayed, so the fountain is now culled around the spray point, with the radius grown by how far a spark can have flown by then (max speed * t + |gravity| * t^2 / 2, times the bounce fraction when it is above 1). Sparks no longer follow an effect that moves after spraying them. Co-Authored-By: Claude Opus 5.5 --- crates/fx/src/draw.rs | 19 +++++---- crates/fx/src/spark_fountain.rs | 42 ++++++++++++++----- crates/fx/src/spawn.rs | 3 +- crates/fx_iw4/src/lib.rs | 2 +- crates/fx_iw4/src/spark_fountain.rs | 12 +++++- .../src/drawsurf/tess/particle_cloud.rs | 3 +- crates/render_fx/src/present.rs | 2 +- 7 files changed, 60 insertions(+), 23 deletions(-) diff --git a/crates/fx/src/draw.rs b/crates/fx/src/draw.rs index 0377f8f..69f91c2 100644 --- a/crates/fx/src/draw.rs +++ b/crates/fx/src/draw.rs @@ -17,6 +17,7 @@ pub struct FxDrawElemContext<'a> { pub def_index: u8, pub elem_type: u8, pub origin: [f32; 3], + pub cull_reach: f32, pub axis: [[f32; 3]; 3], pub norm_time: f32, pub age_msec: i32, @@ -393,6 +394,7 @@ fn draw_one_elem( def_index: elem.def_index, elem_type: elem.elem_type, origin, + cull_reach: 0.0, axis: effect.axis, norm_time: norm, age_msec: age, @@ -472,19 +474,19 @@ fn draw_one_elem( let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); let elem_random_seed = elem.random_seed; - let origin = crate::spark::spark_elem_world_origin( - elem.origin, - elem.flags, - &effect.frame_now(), - &effect.frame_when_played(), - Some(elem.orient_spawn_params(elem_random_seed)), - ); + let Some((origin, cull_reach)) = + crate::spark_fountain::spark_fountain_reach(host, elem.spark_cloud_handle, age) + else { + out.skipped_spark_fountain = out.skipped_spark_fountain.saturating_add(1); + return; + }; let ctx = FxDrawElemContext { def_name: effect.def_name.as_str(), catalog_index: effect.catalog_index, def_index: elem.def_index, elem_type: elem.elem_type, origin, + cull_reach, axis: effect.axis, norm_time: norm, age_msec: age, @@ -550,6 +552,7 @@ fn draw_one_elem( def_index: elem.def_index, elem_type: elem.elem_type, origin, + cull_reach: 0.0, axis: effect.axis, norm_time: norm, age_msec: age, @@ -589,6 +592,7 @@ fn draw_one_elem( def_index: elem.def_index, elem_type: elem.elem_type, origin, + cull_reach: 0.0, axis: effect.axis, norm_time: norm, age_msec: age, @@ -640,6 +644,7 @@ fn draw_one_elem( def_index: elem.def_index, elem_type: elem.elem_type, origin, + cull_reach: 0.0, axis: effect.axis, norm_time: norm, age_msec: age, diff --git a/crates/fx/src/spark_fountain.rs b/crates/fx/src/spark_fountain.rs index 5e90a4d..4c17884 100644 --- a/crates/fx/src/spark_fountain.rs +++ b/crates/fx/src/spark_fountain.rs @@ -3,9 +3,9 @@ use fx_iw4::{ FX_SPARK_FOUNTAIN_INTEGRATE_BUDGET, FX_SPARK_FOUNTAIN_INTEGRATE_CELLS, msvcrt_rand, spark_fountain_accel_from_gravity, spark_fountain_cone_dir, spark_fountain_handle_for_slot, spark_fountain_integrate_cell, spark_fountain_integrate_cell_begin, - spark_fountain_isotropic_dir, spark_fountain_mark_ready, spark_fountain_slot_for_handle, - spark_fountain_spark_n_clamped, spark_fountain_speed, spark_fountain_spray_dir, - spark_fountain_update_keyframe_cursor, + spark_fountain_integrate_miss_cell, spark_fountain_isotropic_dir, spark_fountain_mark_ready, + spark_fountain_slot_for_handle, spark_fountain_spark_n_clamped, spark_fountain_speed, + spark_fountain_spray_dir, spark_fountain_update_keyframe_cursor, }; use crate::system::FxSystemHost; @@ -32,6 +32,8 @@ pub struct FxSparkFountainClusterSlot { pub loop_time: f32, pub boost_time: f32, pub boost_factor: f32, + pub origin: [f32; 3], + pub max_speed: f32, pub mesh_idx: [u16; FX_SPARK_FOUNTAIN_CLUSTER_MESH_MAX as usize], } @@ -51,6 +53,8 @@ impl Default for FxSparkFountainClusterSlot { loop_time: 0.0, boost_time: 0.0, boost_factor: 0.0, + origin: [0.0; 3], + max_speed: 0.0, mesh_idx: [FX_SPARK_FOUNTAIN_HANDLE_NONE; FX_SPARK_FOUNTAIN_CLUSTER_MESH_MAX as usize], } } @@ -215,15 +219,14 @@ pub(crate) fn spray_spark_fountain( let rx = msvcrt_rand(&mut hold); let dir = spark_fountain_spray_dir(cone_axis, [rx, ry, rz], vel_cone_frac); let speed = spark_fountain_speed(msvcrt_rand(&mut hold), vel_min, vel_max); + let (times, origins, vels) = spark_fountain_integrate_miss_cell( + origin, + [dir[0] * speed, dir[1] * speed, dir[2] * speed], + ); host.spark_fountain_meshes[mesh].cells[cell as usize] = FxSparkFountainCell { - times: [0.0; 4], - origins: [origin, [0.0; 3], [0.0; 3], [0.0; 3]], - vels: [ - [dir[0] * speed, dir[1] * speed, dir[2] * speed], - [0.0; 3], - [0.0; 3], - [0.0; 3], - ], + times, + origins, + vels, }; cell = cell.saturating_add(1); } @@ -243,9 +246,26 @@ pub(crate) fn spray_spark_fountain( host.spark_fountains[dense].loop_time = loop_time; host.spark_fountains[dense].boost_time = boost_time; host.spark_fountains[dense].boost_factor = boost_factor; + host.spark_fountains[dense].origin = origin; + host.spark_fountains[dense].max_speed = vel_min.abs().max(vel_max.abs()); FountainSpray::Ready } +pub(crate) fn spark_fountain_reach( + host: &FxSystemHost, + handle: u16, + age_msec: i32, +) -> Option<([f32; 3], f32)> { + use fx_iw4::{spark_fountain_boost, spark_fountain_wrap_loop_time}; + let dense = spark_fountain_slot_for_handle(handle)?; + let cluster = host.spark_fountains.get(dense).filter(|c| c.occupied)?; + let (warped, _) = + spark_fountain_boost(cluster.boost_time, cluster.boost_factor, age_msec as f32); + let t = spark_fountain_wrap_loop_time(warped, cluster.loop_time).max(0.0); + let path = cluster.max_speed * t + 0.5 * cluster.gravity.abs() * t * t; + Some((cluster.origin, path * cluster.bounce_frac.abs().max(1.0))) +} + pub(crate) fn emit_spark_fountain_custom_cells( host: &FxSystemHost, handle: u16, diff --git a/crates/fx/src/spawn.rs b/crates/fx/src/spawn.rs index 1da5917..0c492a4 100644 --- a/crates/fx/src/spawn.rs +++ b/crates/fx/src/spawn.rs @@ -529,10 +529,11 @@ fn spawn_elem( match crate::spark_fountain::alloc_spark_fountain(host) { Some(fountain_handle) => { host.elems[elem_slot].spark_cloud_handle = fountain_handle; + // Fountain cells are world space in every run mode. match crate::spark_fountain::spray_spark_fountain( host, fountain_handle, - origin, + spawn_origin_world(elem_def, effect_origin, effect_axis, life_idx), fx_iw4::get_elem_angles_axis( elem_def.spawn_angles, elem_def.angular_velocity, diff --git a/crates/fx_iw4/src/lib.rs b/crates/fx_iw4/src/lib.rs index 6151a24..b4cdc35 100644 --- a/crates/fx_iw4/src/lib.rs +++ b/crates/fx_iw4/src/lib.rs @@ -297,7 +297,7 @@ pub use spark_fountain::{ spark_fountain_accel_from_gravity, spark_fountain_atlas_uv, spark_fountain_ballistic, spark_fountain_boost, spark_fountain_bounce_vel, spark_fountain_cell_indices, spark_fountain_cell_verts, spark_fountain_cluster_draw_allows, spark_fountain_cone_dir, - spark_fountain_cone_is_isotropic, spark_fountain_def_allows_draw, + spark_fountain_cone_is_isotropic, spark_fountain_def_allows_draw, spark_fountain_draw_cloud, spark_fountain_draw_clouds_allows, spark_fountain_generate_ribbon, spark_fountain_handle_for_slot, spark_fountain_hit_time, spark_fountain_hit_time_abs, spark_fountain_index_count, spark_fountain_integrate_cell, spark_fountain_integrate_cell_begin, diff --git a/crates/fx_iw4/src/spark_fountain.rs b/crates/fx_iw4/src/spark_fountain.rs index 1e2c8ce..02a9d77 100644 --- a/crates/fx_iw4/src/spark_fountain.rs +++ b/crates/fx_iw4/src/spark_fountain.rs @@ -1,4 +1,4 @@ -use crate::particle_cloud::{GfxPosTexVertex, particle_spark_cell_indices}; +use crate::particle_cloud::{GfxParticleCloud, GfxPosTexVertex, particle_spark_cell_indices}; use crate::vec::vec3_normalize; pub const FX_SPARK_FOUNTAIN_CLUSTER_STRIDE: usize = 0x40; @@ -578,6 +578,16 @@ pub fn spark_fountain_atlas_uv(col_bits: u8, row_bits: u8, cell: u32) -> [f32; 4 [u0, u_span, v0, v_span] } +#[inline] +pub fn spark_fountain_draw_cloud(cloud: GfxParticleCloud) -> GfxParticleCloud { + GfxParticleCloud { + quat: [0.0, 0.0, 0.0, 1.0], + pos: [0.0; 3], + placement_scale: 1.0, + ..cloud + } +} + pub const R_PARTICLE_CLOUD_CUSTOM_CAP: u32 = 0x40; #[inline] diff --git a/crates/render_fx/src/drawsurf/tess/particle_cloud.rs b/crates/render_fx/src/drawsurf/tess/particle_cloud.rs index 54419a3..f5b3d56 100644 --- a/crates/render_fx/src/drawsurf/tess/particle_cloud.rs +++ b/crates/render_fx/src/drawsurf/tess/particle_cloud.rs @@ -171,12 +171,13 @@ impl FxParticleCloudPlan { sort_key, material_sorted_index, }); + let placed = fx_iw4::spark_fountain_draw_cloud(cloud); let empty = fx_iw4::empty_particle_cloud(); self.draws.push(FxParticleCloudDraw { material, index_start, index_count: spark_fountain_index_count(cells.len() as u32), - clouds: [cloud, empty, empty], + clouds: [placed, empty, empty], }); self.custom_live = self.custom_live.saturating_add(1); Some(self.draws.len() as u32 - 1) diff --git a/crates/render_fx/src/present.rs b/crates/render_fx/src/present.rs index 69887ad..5fe39eb 100644 --- a/crates/render_fx/src/present.rs +++ b/crates/render_fx/src/present.rs @@ -1359,7 +1359,7 @@ pub fn build_fx_verts( ctx.origin, size0, size1, - scale, + scale + ctx.cull_reach, ) { return None; }