From cf36b24e8db60ff7e5588295a98a944cc49e65ec Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 20 Oct 2026 18:00:51 +0200 Subject: [PATCH] Let the smoke leave the exhaust as a widening jet that fades in --- docs/opengl46-renderer.md | 4 ++++ rendering/opengl46/opengl46gpuparticles.cpp | 4 ++-- rendering/opengl46/opengl46particles.cpp | 2 +- rendering/particles.cpp | 12 ++++++++++++ rendering/particles.h | 13 +++++++++++++ shaders/particle_common.glsl | 4 ++-- shaders/particles_smoke_prepare.comp | 6 +++++- shaders/particles_smoke_simulate.comp | 3 +++ 8 files changed, 42 insertions(+), 6 deletions(-) diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index 70e8eb10..a8409dad 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -274,6 +274,10 @@ are no longer loaded. of a particle goes over to `v` (straight up or down, besides the drift with the wind), the faster the larger `drag: d` (per second, default 1: about 63% of the way in a second). It takes the place of the old rules of the rise (slower in cold and humid air); a template without it behaves as before. The particles don't stop at the ground. + Two more keys make the smoke come out of the exhaust as a jet instead of puffs swelling above it: `spread: s` — the + particles grow by `s` metres of size for each metre they go through the air (a jet widens by about 0.3–0.5 per metre), + on top of `size_change`, so they leave the exhaust as small as `min_size`/`max_size` (the size of the pipe) and widen + as they rise; `fade_in: t` — they take `t` seconds to show up after they're born, instead of popping up at full opacity. - **Rain and snow** are particles in three boxes around the camera (10, 24 and 60 metres), more of them the heavier the rain or snow (the overcast above 1). The drops and flakes fall, drift with the wind, the flakes flutter, and each is drawn as a streak of its motion against the camera: they slant when the train moves. They are lit like the smoke, so the diff --git a/rendering/opengl46/opengl46gpuparticles.cpp b/rendering/opengl46/opengl46gpuparticles.cpp index 13b9b784..862f9fde 100644 --- a/rendering/opengl46/opengl46gpuparticles.cpp +++ b/rendering/opengl46/opengl46gpuparticles.cpp @@ -246,10 +246,10 @@ void opengl46_gpu_particles::simulate_smoke(particle_manager &Particles, glm::dv record.previous = glm::vec4{glm::vec3{state.previous - m_origin}, 0.f}; record.launch = glm::vec4{state.inclination[0], state.inclination[1], state.velocity[0], state.velocity[1]}; record.initial = glm::vec4{state.size[0], state.size[1], state.opacity[0], state.opacity[1]}; - record.sizechange = glm::vec4{state.sizechange[0], state.sizechange[1], state.sizechange[2], 0.f}; + record.sizechange = glm::vec4{state.sizechange[0], state.sizechange[1], state.sizechange[2], state.spread}; record.opacitychange = glm::vec4{state.opacitychange[0], state.opacitychange[1], state.opacitychange[2], 0.f}; record.motion = glm::vec4{state.rise, state.damping, state.risemin, 0.f}; - record.settling = glm::vec4{state.settling, state.drag, state.settles ? 1.f : 0.f, 0.f}; + record.settling = glm::vec4{state.settling, state.drag, state.settles ? 1.f : 0.f, state.fadein}; record.color = glm::vec4{glm::clamp(state.color, glm::vec3{0.f}, glm::vec3{1.f}), 0.f}; record.oldcolor = glm::vec4{glm::clamp(state.color * oldlight, glm::vec3{0.f}, glm::vec3{1.f}), 0.f}; if (pool.size > 0) diff --git a/rendering/opengl46/opengl46particles.cpp b/rendering/opengl46/opengl46particles.cpp index 1de26ff2..c9a78e21 100644 --- a/rendering/opengl46/opengl46particles.cpp +++ b/rendering/opengl46/opengl46particles.cpp @@ -67,7 +67,7 @@ opengl46_particles::update( opengl_camera const &Camera ) { vertex.color[ 0 ] = particlecolor.r; vertex.color[ 1 ] = particlecolor.g; vertex.color[ 2 ] = particlecolor.b; - vertex.color.a = std::clamp(particle.opacity, 0.0f, 1.0f); + vertex.color.a = std::clamp(particle.opacity, 0.0f, 1.0f) * sourcedata.fade_in(particle.age); auto const offset { glm::vec3{ particle.position - Camera.position() } }; auto const rotation { glm::angleAxis( particle.rotation, glm::vec3{ 0.f, 0.f, 1.f } ) }; diff --git a/rendering/particles.cpp b/rendering/particles.cpp index 95107665..f67bf5a3 100644 --- a/rendering/particles.cpp +++ b/rendering/particles.cpp @@ -122,6 +122,12 @@ smoke_source::deserialize_mapping( cParser &Input ) { else if( key == "drag:" ) { m_drag = std::max( 0.f, Input.getToken( true, "\n\r\t ,;[]" ) ); } + else if( key == "spread:" ) { + m_spread = std::max( 0.f, Input.getToken( true, "\n\r\t ,;[]" ) ); + } + else if( key == "fade_in:" ) { + m_fadein = std::max( 0.f, Input.getToken( true, "\n\r\t ,;[]" ) ); + } return true; // return value marks a [ key: value ] pair was extracted, nothing about whether it's recognized } @@ -385,6 +391,10 @@ smoke_source::update( smoke_particle &Particle, bounding_box &Boundingbox, doubl if( Particle.opacity <= 0.f ) { return false; } // ... otherwise proceed with full update m_sizemodifier.update( Particle.size, Timedelta ); + if( m_spread > 0.f ) { + // the jet widens with the way it goes + Particle.size = std::min( Particle.size + m_spread * glm::length( Particle.velocity ) * static_cast( Timedelta ), m_sizemodifier.limit( value_limit::max ) ); + } if( m_settles ) { // the drag of the air takes the particle towards the velocity it settles at @@ -463,6 +473,8 @@ smoke_source::update_gpu( double const Timedelta, bool const Onlydespawn ) { m_gpu.damping = static_cast( 0.050 * Global.Overcast ); m_gpu.risemin = static_cast( 0.25 * ( 2.f - Global.Overcast ) ); } + m_gpu.spread = m_spread; + m_gpu.fadein = m_fadein; m_gpu.settles = m_settles; m_gpu.settling = m_settling; m_gpu.drag = m_drag; diff --git a/rendering/particles.h b/rendering/particles.h index a8979d85..07b9e98a 100644 --- a/rendering/particles.h +++ b/rendering/particles.h @@ -105,6 +105,8 @@ public: bool settles { false }; float settling { 0.f }; float drag { 1.f }; + float spread { 0.f }; // see smoke_source::m_spread + float fadein { 0.f }; // see smoke_source::m_fadein glm::vec3 color { 0.f }; }; // methods @@ -128,6 +130,12 @@ public: std::size_t max_particles() const { return m_max_particles; } + // share of the opacity of a particle the age has let through so far, 0-1 (fade_in:) + float + fade_in( float const Age ) const { + if( m_fadein <= 0.f ) { return 1.f; } + auto const share { std::clamp( Age / m_fadein, 0.f, 1.f ) }; + return share * share * ( 3.f - 2.f * share ); } glm::vec3 const & color() const { return m_emitter.color; } @@ -199,6 +207,11 @@ private: bool m_settles { false }; float m_settling { 0.f }; // terminal_velocity: float m_drag { 1.f }; // drag: + // optional growth of the particles with the way they go through the air, metres of size per metre: the jet out of the + // exhaust widens into a cone as it takes in the air around, instead of the puffs swelling in one place + float m_spread { 0.f }; // spread: + // optional time the particles take to show up after they're born, seconds, so they don't pop up at the exhaust + float m_fadein { 0.f }; // fade_in: // texture_handle m_texture { -1 }; // texture assigned to particle billboards // current state float m_spawncount { 0.f }; // number of particles to spawn during next update diff --git a/shaders/particle_common.glsl b/shaders/particle_common.glsl index 03fe200c..dc68c0a4 100644 --- a/shaders/particle_common.glsl +++ b/shaders/particle_common.glsl @@ -19,10 +19,10 @@ struct smoke_source_s vec4 previous; // xyz: emitter at the update before, from the origin vec4 launch; // xy: inclination, radians, zw: speed, m/s vec4 initial; // xy: size, zw: opacity - vec4 sizechange; // x: per second, y: min, z: max + vec4 sizechange; // x: per second, y: min, z: max, w: per metre the particle goes through the air (spread:) vec4 opacitychange; // x: per second, y: min, z: max vec4 motion; // x: rise, y: damping (per second, of the vertical velocity), z: least vertical velocity - vec4 settling; // x: velocity the particles settle at, m/s upwards, y: drag, per second, z: 1 when they settle (in place of motion) + vec4 settling; // x: velocity the particles settle at, m/s upwards, y: drag, per second, z: 1 when they settle (in place of motion), w: seconds they take to show up (fade_in:) vec4 color; // rgb: colour of the smoke, lit by the lights of the pass vec4 oldcolor; // rgb: colour lit as the cpu does it, without the lights (gfx.smoke.lighting off) }; diff --git a/shaders/particles_smoke_prepare.comp b/shaders/particles_smoke_prepare.comp index 3a1b23df..1ed8ec8e 100644 --- a/shaders/particles_smoke_prepare.comp +++ b/shaders/particles_smoke_prepare.comp @@ -48,5 +48,9 @@ void main() vec3 color = source.oldcolor.rgb; if (options.x > 0.5) color = source.color.rgb * particle_light((view * vec4(world, 1.0)).xyz, world, options.y); - particle_emit(world, size, vec3(particle.position.w, 0.0, 0.0), color, clamp(particle.state.x, 0.0, 1.0), uint(options.z)); + float opacity = clamp(particle.state.x, 0.0, 1.0); + // showing up after the birth (smoke_source::fade_in()) + if (source.settling.w > 0.0) + opacity *= smoothstep(0.0, source.settling.w, particle.state.y); + particle_emit(world, size, vec3(particle.position.w, 0.0, 0.0), color, opacity, uint(options.z)); } diff --git a/shaders/particles_smoke_simulate.comp b/shaders/particles_smoke_simulate.comp index 936b79b7..ebc64cd2 100644 --- a/shaders/particles_smoke_simulate.comp +++ b/shaders/particles_smoke_simulate.comp @@ -37,6 +37,9 @@ bool advance(inout smoke_particle_s particle, smoke_source_s source, float secon if (particle.state.x <= 0.0) return false; particle.velocity.w = clamp(particle.velocity.w + source.sizechange.x * seconds, source.sizechange.y, source.sizechange.z); + // the jet widens with the way it goes + if (source.sizechange.w > 0.0) + particle.velocity.w = min(particle.velocity.w + source.sizechange.w * length(particle.velocity.xyz) * seconds, source.sizechange.z); if (source.settling.z > 0.5) { // the drag of the air takes the particle towards the velocity it settles at