Skip to content

Allow using seeded noise for particle velocity and spawn position offset #6675

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

Merged
merged 4 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions code/math/vecmat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -500,18 +500,26 @@ float vm_vec_normalize(vec3d *v)
// If vector is 0,0,0, return 1.0f, and change v to 1,0,0.
// Otherwise return the magnitude.
// No warning() generated for null vector, as it is expected that some vectors may be null.
float vm_vec_copy_normalize_safe(vec3d *dest, const vec3d *src)
float vm_vec_copy_normalize_safe(vec3d *dest, const vec3d *src, bool fallbackToZeroVec)
{
float m;

m = vm_vec_mag(src);

// Mainly here to trap attempts to normalize a null vector.
if (fl_near_zero(m)) {
dest->xyz.x = 1.0f;
dest->xyz.y = 0.0f;
dest->xyz.z = 0.0f;
return 1.0f;
if (fallbackToZeroVec) {
dest->xyz.x = 0.0f;
dest->xyz.y = 0.0f;
dest->xyz.z = 0.0f;
return 0.0f;
}
else {
dest->xyz.x = 1.0f;
dest->xyz.y = 0.0f;
dest->xyz.z = 0.0f;
return 1.0f;
}
}

float im = 1.0f / m;
Expand All @@ -527,9 +535,9 @@ float vm_vec_copy_normalize_safe(vec3d *dest, const vec3d *src)
// If vector is 0,0,0, return 1.0f, and change v to 1,0,0.
// Otherwise return the magnitude.
// No warning() generated for null vector.
float vm_vec_normalize_safe(vec3d *v)
float vm_vec_normalize_safe(vec3d *v, bool fallbackToZeroVec)
{
return vm_vec_copy_normalize_safe(v,v);
return vm_vec_copy_normalize_safe(v,v, fallbackToZeroVec);
}

//return the normalized direction vector between two points
Expand Down
6 changes: 3 additions & 3 deletions code/math/vecmat.h
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,9 @@ float vm_vec_copy_normalize(vec3d *dest, const vec3d *src);
float vm_vec_normalize(vec3d *v);

// This version of vector normalize checks for the null vector before normalization.
// If it is detected, it returns the vector 1, 0, 0.
float vm_vec_copy_normalize_safe(vec3d *dest, const vec3d *src);
float vm_vec_normalize_safe(vec3d *v);
// If it is detected, it returns the vector 1, 0, 0 (or 0, 0, 0 if fallbackToZeroVec is true)..
float vm_vec_copy_normalize_safe(vec3d *dest, const vec3d *src, bool fallbackToZeroVec = false);
float vm_vec_normalize_safe(vec3d *v, bool fallbackToZeroVec = false);

//return the normalized direction vector between two points
//dest = normalized(end - start). Returns mag of direction vector
Expand Down
73 changes: 55 additions & 18 deletions code/particle/ParticleEffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

#include "render/3d.h"

#include <anl.h>

namespace particle {

ParticleEffect::ParticleEffect(SCP_string name)
Expand All @@ -31,11 +33,15 @@ ParticleEffect::ParticleEffect(SCP_string name)
m_lifetime(::util::UniformFloatRange(0.0f)),
m_length(::util::UniformFloatRange(0.0f)),
m_vel_inherit(::util::UniformFloatRange(0.0f)),
m_velocity_scaling(::util::UniformFloatRange(0.0f)),
m_velocity_scaling(::util::UniformFloatRange(1.0f)),
m_velocity_noise_scaling(::util::UniformFloatRange(1.0f)),
m_position_noise_scaling(::util::UniformFloatRange(1.0f)),
m_vel_inherit_from_orientation(std::nullopt),
m_vel_inherit_from_position(std::nullopt),
m_velocityVolume(nullptr),
m_spawnVolume(nullptr),
m_velocityNoise(nullptr),
m_spawnNoise(nullptr),
m_manual_offset (std::nullopt),
m_particleTrail(ParticleEffectHandle::invalid()),
m_size_lifetime_curve(-1),
Expand Down Expand Up @@ -87,10 +93,14 @@ ParticleEffect::ParticleEffect(SCP_string name,
m_length(::util::UniformFloatRange(0.0f)),
m_vel_inherit(vel_inherit),
m_velocity_scaling(velocity_scaling),
m_velocity_noise_scaling(::util::UniformFloatRange(1.0f)),
m_position_noise_scaling(::util::UniformFloatRange(1.0f)),
m_vel_inherit_from_orientation(vel_inherit_from_orientation),
m_vel_inherit_from_position(vel_inherit_from_position),
m_velocityVolume(std::move(velocityVolume)),
m_spawnVolume(std::move(spawnVolume)),
m_velocityNoise(nullptr),
m_spawnNoise(nullptr),
m_manual_offset (std::nullopt),
m_particleTrail(particleTrail),
m_size_lifetime_curve(-1),
Expand Down Expand Up @@ -134,26 +144,41 @@ matrix ParticleEffect::getNewDirection(const matrix& hostOrientation, const std:
}
}

void ParticleEffect::processSource(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& vel, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const {
const auto& [pos, hostOrientation] = source.m_host->getPositionAndOrientation(m_parent_local, interp, m_manual_offset);
void ParticleEffect::sampleNoise(vec3d& noiseTarget, const matrix* orientation, std::pair<anl::CKernel, anl::CInstructionIndex>& noise, const std::tuple<const ParticleSource&, const size_t&>& source, ParticleCurvesOutput noiseMult, ParticleCurvesOutput noiseTimeMult, ParticleCurvesOutput noiseSeed) const {
auto& [kernel, instruction] = noise;
anl::CNoiseExecutor executor(kernel);
const auto& color = executor.evaluateColor(
ParticleSource::getEffectRunningTime(source)
* m_modular_curves.get_output(noiseTimeMult, source)
, m_modular_curves.get_output(noiseSeed, source), instruction);

vec3d noiseSampleLocal{{{ color.r, color.g, color.b }}};
noiseSampleLocal *= m_modular_curves.get_output(noiseMult, source);

vm_vec_unrotate(&noiseTarget, &noiseSampleLocal, orientation);
}

void ParticleEffect::processSource(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& velParent, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const {
if (m_affectedByDetail){
if (Detail.num_particles > 0)
particle_percent *= (0.5f + (0.25f * static_cast<float>(Detail.num_particles - 1)));
else
return; //Will not emit on current detail settings, but may in the future.
}

auto modularCurvesInput = std::forward_as_tuple(source, effectNumber);

const auto& [pos, hostOrientation] = source.m_host->getPositionAndOrientation(m_parent_local, interp, m_manual_offset);

const auto& orientation = getNewDirection(hostOrientation, source.m_normal);

if (m_distanceCulled > 0.f) {
float min_dist = 125.0f;
float dist = vm_vec_dist_quick(&pos, &Eye_position) / m_distanceCulled;
if (dist > min_dist)
particle_percent *= min_dist / dist;
}

const auto& orientation = getNewDirection(hostOrientation, source.m_normal);

auto modularCurvesInput = std::forward_as_tuple(source, effectNumber);
particle_percent *= m_particleChance * m_modular_curves.get_output(ParticleCurvesOutput::PARTICLE_NUM_MULT, modularCurvesInput);
float radiusMultiplier = m_modular_curves.get_output(ParticleCurvesOutput::RADIUS_MULT, modularCurvesInput);
float lengthMultiplier = m_modular_curves.get_output(ParticleCurvesOutput::LENGTH_MULT, modularCurvesInput);
Expand All @@ -163,6 +188,18 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s
float positionInheritVelocityMultiplier = m_modular_curves.get_output(ParticleCurvesOutput::POSITION_INHERIT_VELOCITY_MULT, modularCurvesInput);
float orientationInheritVelocityMultiplier = m_modular_curves.get_output(ParticleCurvesOutput::ORIENTATION_INHERIT_VELOCITY_MULT, modularCurvesInput);

vec3d velNoise = ZERO_VECTOR;
if (m_velocityNoise != nullptr) {
sampleNoise(velNoise, &orientation, *m_velocityNoise, modularCurvesInput, ParticleCurvesOutput::VELOCITY_NOISE_MULT, ParticleCurvesOutput::VELOCITY_NOISE_TIME_MULT, ParticleCurvesOutput::VELOCITY_NOISE_SEED);
velNoise *= m_velocity_noise_scaling.next();
}

vec3d posNoise = ZERO_VECTOR;
if (m_spawnNoise != nullptr) {
sampleNoise(posNoise, &orientation, *m_spawnNoise, modularCurvesInput, ParticleCurvesOutput::SPAWN_POSITION_NOISE_MULT, ParticleCurvesOutput::SPAWN_POSITION_NOISE_TIME_MULT, ParticleCurvesOutput::SPAWN_POSITION_NOISE_SEED);
posNoise *= m_position_noise_scaling.next();
}

float num = m_particleNum.next() * particle_percent;
unsigned int num_spawn;

Expand All @@ -177,7 +214,7 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s
particle_info info;

info.pos = pos;
info.vel = vel;
info.vel = velParent;

if (m_parent_local) {
info.attached_objnum = parent;
Expand All @@ -189,45 +226,45 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s
}

if (m_vel_inherit_absolute)
vm_vec_normalize_quick(&info.vel);
vm_vec_normalize_safe(&info.vel, true);

info.vel *= m_vel_inherit.next() * inheritVelocityMultiplier;

vec3d velocity = ZERO_VECTOR;
vec3d localPos = ZERO_VECTOR;
vec3d localVelocity = velNoise;
vec3d localPos = posNoise;

if (m_spawnVolume != nullptr) {
localPos += m_spawnVolume->sampleRandomPoint(orientation, modularCurvesInput);
}

if (m_velocityVolume != nullptr) {
velocity += m_velocityVolume->sampleRandomPoint(orientation, modularCurvesInput) * (m_velocity_scaling.next() * velocityVolumeMultiplier);
localVelocity += m_velocityVolume->sampleRandomPoint(orientation, modularCurvesInput) * (m_velocity_scaling.next() * velocityVolumeMultiplier);
}

if (m_vel_inherit_from_orientation.has_value()) {
velocity += orientation.vec.fvec * (m_vel_inherit_from_orientation->next() * orientationInheritVelocityMultiplier);
localVelocity += orientation.vec.fvec * (m_vel_inherit_from_orientation->next() * orientationInheritVelocityMultiplier);
}

if (m_vel_inherit_from_position.has_value()) {
vec3d velFromPos = localPos;
if (m_vel_inherit_from_position_absolute)
vm_vec_normalize_safe(&velFromPos);
velocity += velFromPos * (m_vel_inherit_from_position->next() * positionInheritVelocityMultiplier);
localVelocity += velFromPos * (m_vel_inherit_from_position->next() * positionInheritVelocityMultiplier);
}

if (m_velocity_directional_scaling != VelocityScaling::NONE) {
// Scale the vector with a random velocity sample and also multiply that with cos(angle between
// info.vel and sourceDir) That should produce good looking directions where the maximum velocity is
// Scale the vector with a random localVelocity sample and also multiply that with cos(angle between
// info.vel and sourceDir) That should produce good looking directions where the maximum localVelocity is
// only achieved when the particle travels directly on the normal/reflect vector
vec3d normalizedVelocity;
vm_vec_copy_normalize_safe(&normalizedVelocity, &velocity);
vm_vec_copy_normalize_safe(&normalizedVelocity, &localVelocity);
float dot = vm_vec_dot(&normalizedVelocity, &orientation.vec.fvec);
vm_vec_scale(&velocity,
vm_vec_scale(&localVelocity,
m_velocity_directional_scaling == VelocityScaling::DOT ? dot : 1.f / std::max(0.001f, dot));
}

info.pos += localPos;
info.vel += velocity;
info.vel += localVelocity;

info.bitmap = m_bitmap_list[m_bitmap_range.next()];

Expand Down
28 changes: 26 additions & 2 deletions code/particle/ParticleEffect.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ class EffectHost;
//Due to parsing shenanigans in weapons, this needs a forward-declare here
int parse_weapon(int, bool, const char*);

namespace anl {
class CKernel;
class CInstructionIndex;
}

namespace particle {

/**
Expand Down Expand Up @@ -64,6 +69,12 @@ class ParticleEffect {
INHERIT_VELOCITY_MULT,
POSITION_INHERIT_VELOCITY_MULT,
ORIENTATION_INHERIT_VELOCITY_MULT,
VELOCITY_NOISE_MULT,
VELOCITY_NOISE_TIME_MULT,
VELOCITY_NOISE_SEED,
SPAWN_POSITION_NOISE_MULT,
SPAWN_POSITION_NOISE_TIME_MULT,
SPAWN_POSITION_NOISE_SEED,

NUM_VALUES
};
Expand Down Expand Up @@ -101,13 +112,18 @@ class ParticleEffect {
::util::ParsedRandomFloatRange m_length;
::util::ParsedRandomFloatRange m_vel_inherit;
::util::ParsedRandomFloatRange m_velocity_scaling;
::util::ParsedRandomFloatRange m_velocity_noise_scaling;
::util::ParsedRandomFloatRange m_position_noise_scaling;

std::optional<::util::ParsedRandomFloatRange> m_vel_inherit_from_orientation;
std::optional<::util::ParsedRandomFloatRange> m_vel_inherit_from_position;

std::shared_ptr<::particle::ParticleVolume> m_velocityVolume;
std::shared_ptr<::particle::ParticleVolume> m_spawnVolume;

std::shared_ptr<std::pair<anl::CKernel, anl::CInstructionIndex>> m_velocityNoise;
std::shared_ptr<std::pair<anl::CKernel, anl::CInstructionIndex>> m_spawnNoise;

std::optional<vec3d> m_manual_offset;

ParticleEffectHandle m_particleTrail;
Expand All @@ -119,6 +135,7 @@ class ParticleEffect {
float m_distanceCulled; //Kinda deprecated. Only used by the oldest of legacy effects.

matrix getNewDirection(const matrix& hostOrientation, const std::optional<vec3d>& normal) const;
void sampleNoise(vec3d& noiseTarget, const matrix* orientation, std::pair<anl::CKernel, anl::CInstructionIndex>& noise, const std::tuple<const ParticleSource&, const size_t&>& source, ParticleCurvesOutput noiseMult, ParticleCurvesOutput noiseTimeMult, ParticleCurvesOutput noiseSeed) const;
public:
/**
* @brief Initializes the base ParticleEffect
Expand Down Expand Up @@ -173,7 +190,13 @@ class ParticleEffect {
std::pair {"Velocity Volume Mult", ParticleCurvesOutput::VOLUME_VELOCITY_MULT},
std::pair {"Velocity Inherit Mult", ParticleCurvesOutput::INHERIT_VELOCITY_MULT},
std::pair {"Velocity Position Inherit Mult", ParticleCurvesOutput::POSITION_INHERIT_VELOCITY_MULT},
std::pair {"Velocity Orientation Inherit Mult", ParticleCurvesOutput::ORIENTATION_INHERIT_VELOCITY_MULT}
std::pair {"Velocity Orientation Inherit Mult", ParticleCurvesOutput::ORIENTATION_INHERIT_VELOCITY_MULT},
std::pair {"Velocity Noise Mult", ParticleCurvesOutput::VELOCITY_NOISE_MULT},
std::pair {"Velocity Noise Time Mult", ParticleCurvesOutput::VELOCITY_NOISE_TIME_MULT},
std::pair {"Velocity Noise Seed", ParticleCurvesOutput::VELOCITY_NOISE_SEED},
std::pair {"Spawn Position Noise Mult", ParticleCurvesOutput::SPAWN_POSITION_NOISE_MULT},
std::pair {"Spawn Position Noise Time Mult", ParticleCurvesOutput::SPAWN_POSITION_NOISE_TIME_MULT},
std::pair {"Spawn Position Noise Seed", ParticleCurvesOutput::SPAWN_POSITION_NOISE_SEED}
},
std::pair {"Trigger Radius", modular_curves_submember_input<&ParticleSource::m_triggerRadius>{}},
std::pair {"Trigger Velocity", modular_curves_submember_input<&ParticleSource::m_triggerVelocity>{}},
Expand All @@ -185,7 +208,8 @@ class ParticleEffect {
modular_curves_submember_input<&ParticleSource::getEffect, &SCP_vector<ParticleEffect>::size>,
ModularCurvesMathOperators::division>{}})
.derive_modular_curves_input_only_subset<size_t>(
std::pair {"Spawntime Left", modular_curves_functional_full_input<&ParticleSource::getEffectRemainingTime>{}}
std::pair {"Spawntime Left", modular_curves_functional_full_input<&ParticleSource::getEffectRemainingTime>{}},
std::pair {"Time Running", modular_curves_functional_full_input<&ParticleSource::getEffectRunningTime>{}}
);

MODULAR_CURVE_SET(m_modular_curves, modular_curves_definition);
Expand Down
43 changes: 41 additions & 2 deletions code/particle/ParticleParse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include "particle/volumes/ConeVolume.h"
#include "particle/volumes/SpheroidVolume.h"

#include <anl.h>

namespace particle {

//
Expand Down Expand Up @@ -141,9 +143,30 @@ namespace particle {
}
}

static void parseVelocityNoise(ParticleEffect &effect) {
if (optional_string("+Velocity Noise:")) {
SCP_string func;
stuff_string(func, F_RAW);
anl::CKernel kernel;
anl::CExpressionBuilder builder(kernel);
anl::CInstructionIndex instruction = builder.eval(func);
effect.m_velocityNoise = std::make_shared<std::pair<anl::CKernel, anl::CInstructionIndex>>(std::move(kernel), std::move(instruction));
}
if (optional_string("+Velocity Noise Scale:")) {
effect.m_velocity_noise_scaling = ::util::ParsedRandomFloatRange::parseRandomRange();
}
}

template<bool modern = true> static void parseVelocityVolumeScale(ParticleEffect &effect) {
if (internal::required_string_if_new(modern ? "+Velocity Volume Scale:" : "+Velocity:", false)) {
effect.m_velocity_scaling = ::util::ParsedRandomFloatRange::parseRandomRange();
if constexpr (modern) {
if (optional_string("+Velocity Volume Scale:")) {
effect.m_velocity_scaling = ::util::ParsedRandomFloatRange::parseRandomRange();
}
}
else {
if (internal::required_string_if_new("+Velocity:", false)) {
effect.m_velocity_scaling = ::util::ParsedRandomFloatRange::parseRandomRange();
}
}
}

Expand All @@ -170,6 +193,20 @@ namespace particle {
}
}

static void parsePositionNoise(ParticleEffect &effect) {
if (optional_string("+Spawn Position Noise:")) {
SCP_string func;
stuff_string(func, F_RAW);
anl::CKernel kernel;
anl::CExpressionBuilder builder(kernel);
anl::CInstructionIndex instruction = builder.eval(func);
effect.m_spawnNoise = std::make_shared<std::pair<anl::CKernel, anl::CInstructionIndex>>(std::move(kernel), std::move(instruction));
}
if (optional_string("+Spawn Position Noise Scale:")) {
effect.m_position_noise_scaling = ::util::ParsedRandomFloatRange::parseRandomRange();
}
}

static void parseVelocityInheritFromPosition(ParticleEffect &effect) {
if (optional_string("+Velocity From Position:")) {
effect.m_vel_inherit_from_position.emplace(::util::ParsedRandomFloatRange::parseRandomRange());
Expand Down Expand Up @@ -283,8 +320,10 @@ namespace particle {
parseDirection(effect);
parseOffset(effect);
parsePositionVolume(effect);
parsePositionNoise(effect);
parseVelocityVolume(effect);
parseVelocityVolumeScale(effect);
parseVelocityNoise(effect);
parseVelocityDirectionScale(effect);
parseVelocityInheritFromPosition(effect);
parseVelocityInheritFromOrientation(effect);
Expand Down
6 changes: 5 additions & 1 deletion code/particle/ParticleSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ void ParticleSource::finishCreation() {

for (const auto& effect : ParticleManager::get()->getEffect(m_effect)) {
const auto& [begin, end] = effect.getEffectDuration();
m_timing.emplace_back(SourceTiming{begin, end});
m_timing.emplace_back(SourceTiming{timestamp_delta(begin, 0), begin, end});
}
}

Expand Down Expand Up @@ -101,4 +101,8 @@ void ParticleSource::setHost(std::unique_ptr<EffectHost> host) {
float ParticleSource::getEffectRemainingTime(const std::tuple<const ParticleSource&, const size_t&>& source) {
return i2fl(timestamp_until(std::get<0>(source).m_timing[std::get<1>(source)].m_endTimestamp)) / i2fl(MILLISECONDS_PER_SECOND);
}

float ParticleSource::getEffectRunningTime(const std::tuple<const ParticleSource&, const size_t&>& source) {
return i2fl(timestamp_since(std::get<0>(source).m_timing[std::get<1>(source)].m_startTimestamp)) / i2fl(MILLISECONDS_PER_SECOND);
}
}
Loading
Loading