Repository navigation
Feature/speed streaks #5891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Feature/speed streaks #5891
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| // SuperTuxKart - a fun racing game with go-kart | ||
| // Copyright (C) 2026 the SuperTuxKart team | ||
| // Copyright (C) 2026 sbilliet | ||
| // | ||
| // This program is free software; you can redistribute it and/or | ||
| // modify it under the terms of the GNU General Public License | ||
| // as published by the Free Software Foundation; either version 3 | ||
| // of the License, or (at your option) any later version. | ||
| // | ||
| // This program is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU General Public License | ||
| // along with this program; if not, write to the Free Software | ||
| // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | ||
|
|
||
|
|
||
| // speed_streaks.frag | ||
|
|
||
| uniform float u_intensity; // 0.0 to 1.0, how strong the effect is (derived from getBoostLevel) | ||
| uniform float u_radial_scroll_offset; // Integrated uv offset: encodes speed/time/direction. | ||
| uniform float u_time; // Raw world->getTime() specifically needed for color change over time. | ||
|
|
||
| uniform sampler2D noise_texture; | ||
|
|
||
| out vec4 FragColor; | ||
|
|
||
| const float WISP_DENSITY = 0.60; // Density <=> how many wisps should survive the density smoothstep. | ||
| const float WISP_SOFTNESS = 0.25; // Cutoff for noise range, determines how soft the wisps appear. | ||
| const float WISP_LENGTH = 0.10; // How much should we stretch the UV space radially. | ||
| const float WISP_AMOUNT = 6.0; // Amount of discrete sectors to divide the polar coordinates in. | ||
| const float WISP_RIPPLE = 0.003; // Strength of the distortion applied to the noise_texture.y coordinates. | ||
| const float WISP_ACTIVATION = 0.05; // Threshold boost level at which the effect starts being fully visible. | ||
|
|
||
| const float RADIAL_MASK_SOFTNESS = 1.0; | ||
| const float RADIAL_MASK_SIZE_MIN = 0.75; | ||
| const float RADIAL_MASK_SIZE_MAX = 0.25; | ||
|
|
||
| const float COLOR_CHANGE_SPEED = 8.0; // At what speed should the color change clockwise over time. | ||
| const float COLOR_SATURATION_MIN = 0.1; | ||
| const float COLOR_SATURATION_MAX = 1.0; | ||
|
|
||
| const float PI = 3.14159; | ||
|
|
||
| void main() { | ||
| // -- COORDS SETUP -- // | ||
| // Normalize UVs [0, 1] | ||
| vec2 uv = gl_FragCoord.xy / u_screen; | ||
|
|
||
| // Centre UVs [-0.5f, 0.5f] | ||
| uv = uv - 0.5f; | ||
|
|
||
| // Correct for aspect ratio (e.g., 16/9 -> 1.77) | ||
| uv.x *= u_screen.x / u_screen.y; | ||
|
|
||
| // Convert Cartesian coords to Polar coords (angle & distance) | ||
| // Calculate the angle (-PI to PI) and distance around/from the center point. | ||
| const float polar_angle_radians = atan(uv.y, uv.x); | ||
| const float polar_distance = length(uv); | ||
|
|
||
| // Normalize the angle to the [0, 1] range | ||
| const float normalized_angle = polar_angle_radians / (2 * PI); | ||
|
|
||
| const float wisp_sector_index = normalized_angle * WISP_AMOUNT; | ||
|
|
||
| // Radial coordinate (y portion of noise_texture UVs) used for scrolling the texture outwards. | ||
| const float base_radial_scroll = (polar_distance - u_radial_scroll_offset) * WISP_LENGTH; | ||
|
|
||
| // -- NOISE SAMPLE(s) -- // | ||
| // Wisp noise distortion | ||
| const float ripple_angle_frequency = polar_angle_radians * WISP_AMOUNT; | ||
| const float radial_ripple = sin(ripple_angle_frequency) * WISP_RIPPLE; | ||
| const float distorted_radial_scroll = base_radial_scroll + radial_ripple; | ||
|
|
||
| // First layer of simplex noise (base noise layer) | ||
| const vec2 noise_uv_primary = vec2(wisp_sector_index, distorted_radial_scroll); | ||
| const float noise_sample_primary = texture(noise_texture, noise_uv_primary).r; | ||
|
|
||
| // Second layer of simplex noise | ||
| // Scale and shift coords at different frequencies to create parallax between noise layers. | ||
| const vec2 secondary_noise_modifiers = vec2(1.571, 1.350); | ||
| const vec2 noise_uv_secondary = noise_uv_primary * secondary_noise_modifiers; | ||
|
|
||
| const float noise_sample_secondary = texture(noise_texture, noise_uv_secondary).r; | ||
|
|
||
| // Intersection of both noise layers, final noise data. | ||
| float wisp_noise_mask = min(noise_sample_primary, noise_sample_secondary); | ||
|
|
||
| // Threshold wisp mask using density parameters (e.g. discard range of noise frequencies) | ||
| wisp_noise_mask = smoothstep(WISP_DENSITY, WISP_DENSITY + WISP_SOFTNESS, wisp_noise_mask); | ||
|
|
||
| // -- RADIAL MASK -- // | ||
| const float radial_mask_size = mix(RADIAL_MASK_SIZE_MIN, RADIAL_MASK_SIZE_MAX, u_intensity); | ||
| const float radial_mask = smoothstep(radial_mask_size, radial_mask_size + RADIAL_MASK_SOFTNESS, polar_distance); | ||
| wisp_noise_mask = wisp_noise_mask * radial_mask; | ||
|
|
||
| // -- ACTIVATION FADE -- // | ||
| // Threshold the visibility of the effect at low boost levels and smoothly fade in at target edge. | ||
| float wisp_activation_fade = smoothstep(0.0, WISP_ACTIVATION, u_intensity); | ||
| wisp_noise_mask = wisp_noise_mask * wisp_activation_fade; | ||
|
|
||
| //-------------// | ||
| // -- COLOR -- // | ||
| //-------------// | ||
| // Inigo Quilez cosine palette technique to generate smooth procedural gradient based on input factor. | ||
| const float color_change_offset = u_time * COLOR_CHANGE_SPEED; // Rotate/Offset through color clockwise over time. | ||
| const float base_hue_shift = normalized_angle + color_change_offset; | ||
| const float color_saturation = mix(COLOR_SATURATION_MIN, COLOR_SATURATION_MAX, u_intensity); | ||
|
|
||
| const vec3 spectral_hue = 0.5 + 0.5 * cos(6.28318 * (base_hue_shift + vec3(0.0, 0.333, 0.667))); | ||
| const vec3 palette_output_color = mix(vec3(1.0), spectral_hue, color_saturation); | ||
|
|
||
| // -- FINAL OUTPUT -- / | ||
| FragColor = vec4(palette_output_color * wisp_noise_mask, wisp_noise_mask); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,8 +35,10 @@ | |
| #include "graphics/sp/sp_dynamic_draw_call.hpp" | ||
| #include "graphics/sp/sp_shader.hpp" | ||
| #include "graphics/sp/sp_uniform_assigner.hpp" | ||
| #include "graphics/speed_streaks.hpp" | ||
| #include "io/file_manager.hpp" | ||
| #include "karts/abstract_kart.hpp" | ||
| #include "karts/kart.hpp" | ||
| #include "karts/kart_model.hpp" | ||
| #include "modes/world.hpp" | ||
| #include "physics/physics.hpp" | ||
|
|
@@ -576,6 +578,23 @@ class MotionBlurShader : public TextureShader<MotionBlurShader, 2, | |
| } // render | ||
| }; // MotionBlurShader | ||
|
|
||
| class SpeedStreaksShader : public TextureShader<SpeedStreaksShader, 1, float, float, float> | ||
| { | ||
| public: | ||
| SpeedStreaksShader() | ||
| { | ||
| loadProgram(OBJECT, GL_VERTEX_SHADER, "screenquad.vert", | ||
| GL_FRAGMENT_SHADER, "speed_streaks.frag"); | ||
| assignUniforms("u_intensity", "u_radial_scroll_offset", "u_time"); | ||
| assignSamplerNames(0, "noise_texture", ST_BILINEAR_FILTERED); | ||
| } //SpeedStreaksShader | ||
| void render(GLuint noise_texture, float intensity, float radial_scroll_offset, float time) | ||
| { | ||
| setTextureUnits(noise_texture); | ||
| drawFullScreenEffect(intensity, radial_scroll_offset, time); | ||
| } // render | ||
| }; // SpeedStreaksShader | ||
|
|
||
| // ============================================================================ | ||
| class GodFadeShader : public TextureShader<GodFadeShader, 1, video::SColorf> | ||
| { | ||
|
|
@@ -724,9 +743,25 @@ PostProcessing::PostProcessing() | |
| STKTexManager::getInstance()->addTexture(m_areamap); | ||
| areamap->drop(); | ||
|
|
||
| // Load the simplex noise texture | ||
| const std::string noiseTexturePath = file_manager->getAsset( | ||
| FileManager::AssetType::TEXTURE, | ||
| "stk_noise_simplex.png" | ||
| ); | ||
|
|
||
| //TODO: implement single_channel path, currently loads rgba even though noise_texture is single channel only. | ||
| m_noise_texture = GE::createTexture(noiseTexturePath); | ||
| if (!m_noise_texture || m_noise_texture->getTextureHandler() == 0) | ||
| { | ||
| Log::fatal("postprocessing", "Failed to load %s", noiseTexturePath.c_str()); | ||
| return; | ||
| } | ||
| STKTexManager::getInstance()->addTexture(m_noise_texture); | ||
|
|
||
| // For preloading shaders | ||
| MotionBlurShader::getInstance(); | ||
| LightningShader::getInstance(); | ||
| SpeedStreaksShader::getInstance(); | ||
| } // PostProcessing | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
|
|
@@ -1019,6 +1054,40 @@ void PostProcessing::renderMotionBlur(const FrameBuffer &in_fbo, | |
| MotionBlurShader::getInstance()->render(in_fbo, boost_time, depth_stencil_texture); | ||
| } // renderMotionBlur | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| void PostProcessing::renderSpeedStreaks() | ||
| { | ||
| World* world = World::getWorld(); | ||
| Camera* cam = Camera::getActiveCamera(); | ||
| const Kart* kart = cam ? dynamic_cast<const Kart*>(cam->getKart()) : nullptr; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test is absolutely insufficient to know if the camera should actually have speed streaks displayed. For example, the free camera (ctrl+F2) will still have speed streaks displayed. So does the top view camera. So do the various side of view cameras. Most of these cameras are logically tied to a kart, too. Some kind of white list of camera modes where speed streaks make sense is probably the way to go. |
||
| SpeedStreaks* speed_streaks = kart ? kart->getSpeedStreaks() : nullptr; | ||
|
|
||
| float radial_scroll_offset = 0.0f; | ||
| float intensity = 0.0f; | ||
|
|
||
| if (speed_streaks) | ||
| { | ||
| radial_scroll_offset = speed_streaks->getRadialScrollOffset(); | ||
| intensity = speed_streaks->getBoostIntensity(); | ||
|
|
||
| auto direction = SpeedStreaks::Direction::FORWARD; | ||
| if(cam->getMode() == Camera::CM_REVERSE) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fact that you have the speedlines move in the right direction in reverse camera is a nice touch. You can probably handle the assignment of direction with a simple ternary, it's cleaner. |
||
| { | ||
| direction = SpeedStreaks::Direction::REVERSE; | ||
| } | ||
| speed_streaks->setDirection(direction); | ||
| } | ||
|
|
||
|
|
||
| glEnable(GL_BLEND); | ||
| glBlendEquation(GL_FUNC_ADD); | ||
| glBlendFunc(GL_ONE, GL_ONE); | ||
|
|
||
| SpeedStreaksShader::getInstance()->render(m_noise_texture->getTextureHandler(), intensity, radial_scroll_offset, world->getTime()); | ||
|
|
||
| glDisable(GL_BLEND); | ||
| } | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| void PostProcessing::renderDoF(const FrameBuffer &framebuffer, GLuint color_texture, GLuint depth_stencil_texture) | ||
| { | ||
|
|
@@ -1304,6 +1373,18 @@ FrameBuffer *PostProcessing::render(scene::ICameraSceneNode * const camnode, | |
| in_fbo = &rtts->getFBO(FBO_RGBA_1); | ||
| out_fbo = &rtts->getFBO(FBO_RGBA_2); | ||
|
|
||
| { | ||
| PROFILER_PUSH_CPU_MARKER("- Speed streaks", 0xFF, 0x00, 0x00); | ||
| ScopedGPUTimer Timer(irr_driver->getGPUTimer(Q_SPEEDSTREAKS)); | ||
| if (isRace && World::getWorld() && UserConfigParams::m_speed_streaks) | ||
| { | ||
| in_fbo->bind(); | ||
| renderSpeedStreaks(); | ||
| // additive blend, specifically no ping/pong std::swap | ||
| } | ||
| PROFILER_POP_CPU_MARKER(); | ||
| } | ||
|
|
||
| { | ||
| PROFILER_PUSH_CPU_MARKER("- Motion blur", 0xFF, 0x00, 0x00); | ||
| ScopedGPUTimer Timer(irr_driver->getGPUTimer(Q_MOTIONBLUR)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| // SuperTuxKart - a fun racing game with go-kart | ||
| // Copyright (C) 2026 the SuperTuxKart-Team | ||
| // Copyright (C) 2026 sbilliet | ||
| // | ||
| // This program is free software; you can redistribute it and/or | ||
| // modify it under the terms of the GNU General Public License | ||
| // as published by the Free Software Foundation; either version 3 | ||
| // of the License, or (at your option) any later version. | ||
| // | ||
| // This program is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU General Public License | ||
| // along with this program; if not, write to the Free Software | ||
| // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | ||
|
|
||
| #include "graphics/speed_streaks.hpp" | ||
| #include "karts/abstract_kart.hpp" | ||
| #include <irrMath.h> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A blank line is typically left in STK's codebase between the local STK includes and includes from libraries. |
||
|
|
||
| using namespace irr; | ||
|
|
||
| SpeedStreaks::SpeedStreaks(const AbstractKart& kart) | ||
| : m_kart(kart), | ||
| m_filtered_boost_intensity(0.0f), | ||
| m_radial_scroll_offset(0.0f), | ||
| m_direction(Direction::FORWARD) | ||
| { | ||
| } | ||
|
|
||
| // Calculate the boost intensity value. | ||
| // See comments on s_boost_intensity_lower_ratio and s_boost_intensity_upper_ratio for parameters. | ||
| float SpeedStreaks::calcBoostIntensity(float lower_bound_ratio, float upper_bound_ratio) const | ||
| { | ||
| assert(lower_bound_ratio >= 0.0f && upper_bound_ratio > lower_bound_ratio); | ||
| float speed_ratio = m_kart.getCurrentAdditionalSpeedRatio(); | ||
|
|
||
| const float result = (speed_ratio - lower_bound_ratio) / (upper_bound_ratio - lower_bound_ratio); | ||
| return core::clamp(result, 0.0f, 1.0f); | ||
| } | ||
|
|
||
| // Set the direction in which the streaks should move on screen (inwards/outwards). | ||
| void SpeedStreaks::setDirection(Direction dir) | ||
| { | ||
| m_direction = dir; | ||
| } | ||
|
|
||
| void SpeedStreaks::reset() | ||
| { | ||
| m_filtered_boost_intensity = 0.0f; | ||
| m_radial_scroll_offset = 0.0f; | ||
| m_direction = Direction::FORWARD; | ||
| } | ||
|
|
||
| void SpeedStreaks::update(float dt) | ||
| { | ||
| // Filter the kart's boost intensity to avoid abrupt visual changes. | ||
| // - Uses the last level and smoothly approaches the new value over time. | ||
| // - The speed at which it does this is dictated by s_boost_smoothing_rate. | ||
| const float interpolant = 1.0f - std::exp(-s_boost_intensity_smoothing_rate * dt); //exponential decay | ||
| m_filtered_boost_intensity = core::lerp( | ||
| m_filtered_boost_intensity, | ||
| calcBoostIntensity(s_boost_intensity_lower_ratio, s_boost_intensity_upper_ratio), | ||
| interpolant | ||
| ); | ||
|
|
||
| // Accumulate UV scroll offset over time. | ||
| // Accumulated on CPU to ensure a continuous offset over time; | ||
| // Passing per-frame speed(dictated by intensity) and time values directly | ||
| // to the shader would produce discontinuous UV scrolling. | ||
| // | ||
| // Encodes speed/time/ and direction together in a single uniform to be passed to the shader. | ||
| const float streak_speed = s_streaks_speed_min + s_streaks_speed_max * m_filtered_boost_intensity; | ||
| m_radial_scroll_offset += dt * streak_speed * m_direction; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The closing parenthesis on its own line isn't really something done in STK's codebase.