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 <noreply@anthropic.com>
This commit is contained in:
JohnM
2026-10-10 00:13:29 +08:00
parent 2036c3653e
commit a2f3f830e6
7 changed files with 60 additions and 23 deletions
+12 -7
View File
@@ -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,
+31 -11
View File
@@ -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,
+2 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+11 -1
View File
@@ -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]
@@ -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)
+1 -1
View File
@@ -1359,7 +1359,7 @@ pub fn build_fx_verts(
ctx.origin,
size0,
size1,
scale,
scale + ctx.cull_reach,
) {
return None;
}