mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* Make particles aware of the effect that spawned them * Allow modular curves to call global functions from submember inputs * Add more in and outputs for particle lifetime curves * Allow post-curve velocity as a curve input * fix warning and incorrect assig * Disable CheckTriviallyCopyableMove * Clang Tidy
435 lines
17 KiB
C++
435 lines
17 KiB
C++
#include <utility>
|
|
|
|
#include "particle/ParticleEffect.h"
|
|
#include "particle/ParticleManager.h"
|
|
|
|
#include "model/modelrender.h"
|
|
#include "render/3d.h"
|
|
|
|
#include <anl.h>
|
|
|
|
namespace particle {
|
|
|
|
ParticleEffect::ParticleEffect(SCP_string name)
|
|
: m_name(std::move(name)),
|
|
m_duration(Duration::ONETIME),
|
|
m_rotation_type(RotationType::DEFAULT),
|
|
m_direction(ShapeDirection::ALIGNED),
|
|
m_velocity_directional_scaling(VelocityScaling::NONE),
|
|
m_affectedByDetail(false),
|
|
m_parentLifetime(false),
|
|
m_parentScale(false),
|
|
m_hasLifetime(false),
|
|
m_parent_local(false),
|
|
m_keep_anim_length_if_available(false),
|
|
m_vel_inherit_absolute(false),
|
|
m_vel_inherit_from_position_absolute(false),
|
|
m_reverseAnimation(false),
|
|
m_ignore_velocity_inherit_if_has_parent(false),
|
|
m_bitmap_list({}),
|
|
m_bitmap_range(::util::UniformRange<size_t>(0)),
|
|
m_delayRange(::util::UniformFloatRange(0.0f)),
|
|
m_durationRange(::util::UniformFloatRange(0.0f)),
|
|
m_particlesPerSecond(::util::UniformFloatRange(-1.f)),
|
|
m_particleNum(::util::UniformFloatRange(1.0f)),
|
|
m_radius(::util::UniformFloatRange(0.0f, 1.0f)),
|
|
m_lifetime(::util::UniformFloatRange(0.0f)),
|
|
m_length(::util::UniformFloatRange(0.0f)),
|
|
m_vel_inherit(::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_manual_velocity_offset(std::nullopt),
|
|
m_particleTrail(ParticleEffectHandle::invalid()),
|
|
m_particleChance(1.f),
|
|
m_distanceCulled(-1.f)
|
|
{}
|
|
|
|
ParticleEffect::ParticleEffect(SCP_string name,
|
|
::util::ParsedRandomFloatRange particleNum,
|
|
Duration duration,
|
|
::util::ParsedRandomFloatRange durationRange,
|
|
::util::ParsedRandomFloatRange particlesPerSecond,
|
|
ShapeDirection direction,
|
|
::util::ParsedRandomFloatRange vel_inherit,
|
|
bool vel_inherit_absolute,
|
|
std::shared_ptr<::particle::ParticleVolume> velocityVolume,
|
|
::util::ParsedRandomFloatRange velocity_scaling,
|
|
VelocityScaling velocity_directional_scaling,
|
|
std::optional<::util::ParsedRandomFloatRange> vel_inherit_from_orientation,
|
|
std::optional<::util::ParsedRandomFloatRange> vel_inherit_from_position,
|
|
std::shared_ptr<::particle::ParticleVolume> spawnVolume,
|
|
ParticleEffectHandle particleTrail,
|
|
float particleChance,
|
|
bool affectedByDetail,
|
|
float distanceCulled,
|
|
bool disregardAnimationLength,
|
|
bool reverseAnimation,
|
|
bool parentLocal,
|
|
bool ignoreVelocityInheritIfParented,
|
|
bool velInheritFromPositionAbsolute,
|
|
std::optional<vec3d> velocityOffsetLocal,
|
|
std::optional<vec3d> offsetLocal,
|
|
::util::ParsedRandomFloatRange lifetime,
|
|
::util::ParsedRandomFloatRange radius,
|
|
int bitmap)
|
|
: m_name(std::move(name)),
|
|
m_duration(duration),
|
|
m_rotation_type(RotationType::DEFAULT),
|
|
m_direction(direction),
|
|
m_velocity_directional_scaling(velocity_directional_scaling),
|
|
m_affectedByDetail(affectedByDetail),
|
|
m_parentLifetime(false),
|
|
m_parentScale(false),
|
|
m_hasLifetime(true),
|
|
m_parent_local(parentLocal),
|
|
m_keep_anim_length_if_available(!disregardAnimationLength),
|
|
m_vel_inherit_absolute(vel_inherit_absolute),
|
|
m_vel_inherit_from_position_absolute(velInheritFromPositionAbsolute),
|
|
m_reverseAnimation(reverseAnimation),
|
|
m_ignore_velocity_inherit_if_has_parent(ignoreVelocityInheritIfParented),
|
|
m_bitmap_list({bitmap}),
|
|
m_bitmap_range(::util::UniformRange<size_t>(0)),
|
|
m_delayRange(::util::UniformFloatRange(0.0f)),
|
|
m_durationRange(durationRange),
|
|
m_particlesPerSecond(particlesPerSecond),
|
|
m_particleNum(particleNum),
|
|
m_radius(radius),
|
|
m_lifetime(lifetime),
|
|
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(offsetLocal),
|
|
m_manual_velocity_offset(velocityOffsetLocal),
|
|
m_particleTrail(particleTrail),
|
|
m_particleChance(particleChance),
|
|
m_distanceCulled(distanceCulled) {}
|
|
|
|
float ParticleEffect::getApproximatePixelSize(const vec3d& pos) const {
|
|
float distance_to_eye = vm_vec_dist(&Eye_position, &pos);
|
|
|
|
return convert_distance_and_diameter_to_pixel_size(
|
|
distance_to_eye,
|
|
m_radius.avg() * 2.f,
|
|
g3_get_hfov(Eye_fov),
|
|
gr_screen.max_w);
|
|
}
|
|
|
|
float ParticleEffect::getCurrentFrequencyMult(decltype(modular_curves_definition)::input_type_t source) const {
|
|
return m_modular_curves.get_output(ParticleEffect::ParticleCurvesOutput::PARTICLE_FREQ_MULT, source);
|
|
}
|
|
|
|
matrix ParticleEffect::getNewDirection(const matrix& hostOrientation, const std::optional<vec3d>& normal) const {
|
|
switch (m_direction) {
|
|
case ShapeDirection::ALIGNED:
|
|
return hostOrientation;
|
|
case ShapeDirection::HIT_NORMAL: {
|
|
if (!normal) {
|
|
return hostOrientation;
|
|
}
|
|
|
|
matrix normalOrient;
|
|
vm_vector_2_matrix_norm(&normalOrient, &*normal);
|
|
|
|
return normalOrient;
|
|
}
|
|
case ShapeDirection::REFLECTED: {
|
|
if (!normal) {
|
|
return hostOrientation;
|
|
}
|
|
|
|
// Compute reflect vector as R = V - 2*(V dot N)*N where N
|
|
// is the normal and V is the incoming direction
|
|
|
|
return hostOrientation * vm_matrix_new(1.f - 2.f * normal->xyz.x * normal->xyz.x, -2.f * normal->xyz.x * normal->xyz.y, -2.f * normal->xyz.x * normal->xyz.z,
|
|
-2.f * normal->xyz.x * normal->xyz.y, 1.f - 2.f * normal->xyz.y * normal->xyz.y, -2.f * normal->xyz.y * normal->xyz.z,
|
|
-2.f * normal->xyz.x * normal->xyz.z, -2.f * normal->xyz.y * normal->xyz.z, 1.f - 2.f * normal->xyz.z * normal->xyz.z);
|
|
}
|
|
case ShapeDirection::REVERSE: {
|
|
return vm_matrix_new(hostOrientation.vec.rvec * -1.f, hostOrientation.vec.uvec * -1.f, hostOrientation.vec.fvec * -1.f);
|
|
}
|
|
default:
|
|
Error(LOCATION, "Unhandled direction value!");
|
|
return vmd_identity_matrix;
|
|
}
|
|
}
|
|
|
|
void ParticleEffect::sampleNoise(vec3d& noiseTarget, const matrix* orientation, std::pair<anl::CKernel, anl::CInstructionIndex>& noise, decltype(modular_curves_definition)::input_type_t source, ParticleCurvesOutput noiseMult, ParticleCurvesOutput noiseTimeMult, ParticleCurvesOutput noiseSeed) const {
|
|
auto& [kernel, instruction] = noise;
|
|
anl::CNoiseExecutor executor(kernel);
|
|
const auto& color = executor.evaluateColor(
|
|
ParticleSource::getEffectRunningTime(std::forward_as_tuple(std::get<0>(source), std::get<1>(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);
|
|
}
|
|
|
|
/*
|
|
* In persistent mode (should only ever be used by scripting, really), this function returns pointers to the persistent particles
|
|
* In non-persistent mode, this function returns the multiplier for the next spawn time. This is because the source cannot know about the curve evaluation that is required to get this factor
|
|
*
|
|
* */
|
|
template<bool isPersistent>
|
|
auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& velParent, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const {
|
|
using persistentParticlesList = std::conditional_t<isPersistent, SCP_vector<WeakParticlePtr>, bool>;
|
|
persistentParticlesList createdParticles;
|
|
|
|
if constexpr (!isPersistent)
|
|
SCP_UNUSED(createdParticles);
|
|
|
|
if (m_affectedByDetail){
|
|
if (Detail.num_particles > 0)
|
|
particle_percent *= (0.5f + (0.25f * static_cast<float>(Detail.num_particles - 1)));
|
|
else {
|
|
//Will not emit on current detail settings, but may in the future.
|
|
if constexpr (isPersistent)
|
|
return createdParticles;
|
|
else {
|
|
const auto& [pos, hostOrientation] = source.m_host->getPositionAndOrientation(m_parent_local, interp, m_manual_offset);
|
|
auto modularCurvesInput = std::forward_as_tuple(source, effectNumber, pos);
|
|
return getCurrentFrequencyMult(modularCurvesInput);
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto& [pos, hostOrientation] = source.m_host->getPositionAndOrientation(m_parent_local, interp, m_manual_offset);
|
|
|
|
vec3d posGlobal = pos;
|
|
if (m_parent_local && parent >= 0) {
|
|
vm_vec_unrotate(&posGlobal, &posGlobal, &Objects[parent].orient);
|
|
vm_vec_add2(&posGlobal, &Objects[parent].pos);
|
|
}
|
|
|
|
auto modularCurvesInput = std::forward_as_tuple(source, effectNumber, posGlobal);
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
float lifetimeMultiplier = m_modular_curves.get_output(ParticleCurvesOutput::LIFETIME_MULT, modularCurvesInput);
|
|
float velocityVolumeMultiplier = m_modular_curves.get_output(ParticleCurvesOutput::VOLUME_VELOCITY_MULT, modularCurvesInput);
|
|
float inheritVelocityMultiplier = m_modular_curves.get_output(ParticleCurvesOutput::INHERIT_VELOCITY_MULT, modularCurvesInput);
|
|
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;
|
|
|
|
if (num >= 1.f){
|
|
num_spawn = static_cast<unsigned int>(num);
|
|
}
|
|
else {
|
|
num_spawn = (num >= frand() ? 1 : 0);
|
|
}
|
|
|
|
for (uint i = 0; i < num_spawn; ++i) {
|
|
float particleFraction = static_cast<float>(i) / static_cast<float>(num_spawn);
|
|
|
|
particle info;
|
|
|
|
info.reverse = m_reverseAnimation;
|
|
info.pos = pos;
|
|
info.velocity = velParent;
|
|
|
|
if (m_parent_local) {
|
|
info.attached_objnum = parent;
|
|
info.attached_sig = parent_sig;
|
|
}
|
|
else {
|
|
info.attached_objnum = -1;
|
|
info.attached_sig = -1;
|
|
}
|
|
|
|
if (m_vel_inherit_absolute)
|
|
vm_vec_normalize_safe(&info.velocity, true);
|
|
|
|
info.velocity *= (m_ignore_velocity_inherit_if_has_parent && parent >= 0) ? 0.f : m_vel_inherit.next() * inheritVelocityMultiplier;
|
|
|
|
vec3d localVelocity = velNoise;
|
|
vec3d localPos = posNoise;
|
|
|
|
if (m_spawnVolume != nullptr) {
|
|
localPos += m_spawnVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction);
|
|
}
|
|
|
|
if (m_velocityVolume != nullptr) {
|
|
localVelocity += m_velocityVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction) * (m_velocity_scaling.next() * velocityVolumeMultiplier);
|
|
}
|
|
|
|
if (m_manual_velocity_offset.has_value()) {
|
|
localVelocity += *m_manual_velocity_offset;
|
|
}
|
|
|
|
if (m_vel_inherit_from_orientation.has_value()) {
|
|
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);
|
|
localVelocity += velFromPos * (m_vel_inherit_from_position->next() * positionInheritVelocityMultiplier);
|
|
}
|
|
|
|
if (m_velocity_directional_scaling != VelocityScaling::NONE) {
|
|
// 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, &localVelocity);
|
|
float dot = vm_vec_dot(&normalizedVelocity, &orientation.vec.fvec);
|
|
vm_vec_scale(&localVelocity,
|
|
m_velocity_directional_scaling == VelocityScaling::DOT ? dot : 1.f / std::max(0.001f, dot));
|
|
}
|
|
|
|
info.velocity += localVelocity;
|
|
info.pos += localPos + info.velocity * (interp * f2fl(Frametime));
|
|
|
|
info.bitmap = m_bitmap_list[m_bitmap_range.next()];
|
|
|
|
if (m_parentScale)
|
|
// if we were spawned by a particle, parentRadius is the parent's radius and m_radius is a factor of that
|
|
info.radius = parentRadius * m_radius.next() * radiusMultiplier;
|
|
else
|
|
info.radius = m_radius.next() * radiusMultiplier;
|
|
|
|
info.length = m_length.next() * lengthMultiplier;
|
|
|
|
int fps = 1;
|
|
if (info.nframes < 0) {
|
|
Assertion(bm_is_valid(info.bitmap), "Invalid bitmap handle passed to particle create.");
|
|
bm_get_info(info.bitmap, nullptr, nullptr, nullptr, &info.nframes, &fps);
|
|
}
|
|
|
|
if (m_hasLifetime) {
|
|
if (m_keep_anim_length_if_available && info.nframes > 1) {
|
|
// Recalculate max life for ani's
|
|
info.max_life = i2fl(info.nframes) / i2fl(fps);
|
|
}
|
|
else {
|
|
if (m_parentLifetime)
|
|
// if we were spawned by a particle, parentLifetime is the parent's remaining lifetime and m_lifetime is a factor of that
|
|
info.max_life = parentLifetime * m_lifetime.next() * lifetimeMultiplier;
|
|
else
|
|
info.max_life = m_lifetime.next() * lifetimeMultiplier;
|
|
}
|
|
}
|
|
|
|
info.age = interp * f2fl(Frametime);
|
|
info.looping = false;
|
|
info.angle = frand_range(0.0f, PI2);
|
|
info.parent_effect = m_self;
|
|
|
|
switch (m_rotation_type) {
|
|
case RotationType::DEFAULT:
|
|
info.use_angle = Randomize_particle_rotation;
|
|
break;
|
|
case RotationType::RANDOM:
|
|
info.use_angle = true;
|
|
break;
|
|
case RotationType::SCREEN_ALIGNED:
|
|
info.use_angle = false;
|
|
break;
|
|
default:
|
|
UNREACHABLE("Rotation type not supported");
|
|
}
|
|
|
|
if (m_particleTrail.isValid()) {
|
|
auto part = createPersistent(std::move(info));
|
|
|
|
if constexpr (isPersistent)
|
|
createdParticles.push_back(part);
|
|
|
|
// There are some possibilities where we can get a null pointer back. Those are very rare but we
|
|
// still shouldn't crash in those circumstances.
|
|
if (!part.expired()) {
|
|
auto trailSource = ParticleManager::get()->createSource(m_particleTrail);
|
|
trailSource->setHost(std::make_unique<EffectHostParticle>(std::move(part)));
|
|
trailSource->finishCreation();
|
|
}
|
|
} else {
|
|
if constexpr (isPersistent){
|
|
auto part = createPersistent(std::move(info));
|
|
createdParticles.push_back(part);
|
|
}
|
|
else {
|
|
// We don't have a trail so we don't need a persistent particle
|
|
create(std::move(info));
|
|
}
|
|
}
|
|
}
|
|
|
|
if constexpr (isPersistent)
|
|
return createdParticles;
|
|
else
|
|
return getCurrentFrequencyMult(modularCurvesInput);
|
|
}
|
|
|
|
float 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 {
|
|
return processSourceInternal<false>(interp, source, effectNumber, velParent, parent, parent_sig, parentLifetime, parentRadius, particle_percent);
|
|
}
|
|
|
|
SCP_vector<WeakParticlePtr> ParticleEffect::processSourcePersistent(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& velParent, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const {
|
|
return processSourceInternal<true>(interp, source, effectNumber, velParent, parent, parent_sig, parentLifetime, parentRadius, particle_percent);
|
|
}
|
|
|
|
void ParticleEffect::pageIn() {
|
|
for (int bitmap : m_bitmap_list) {
|
|
bm_page_in_texture(bitmap);
|
|
}
|
|
}
|
|
|
|
std::pair<TIMESTAMP, TIMESTAMP> ParticleEffect::getEffectDuration() const {
|
|
std::pair<TIMESTAMP, TIMESTAMP> timing;
|
|
timing.first = _timestamp(fl2i(m_delayRange.next() * 1000.0f));
|
|
if (m_duration == Duration::ALWAYS)
|
|
timing.second = TIMESTAMP::never();
|
|
else
|
|
timing.second = timestamp_delta(timing.first, fl2i(m_durationRange.next() * 1000.0f));
|
|
return timing;
|
|
}
|
|
|
|
float ParticleEffect::getNextSpawnDelay() const {
|
|
return 1.0f / m_particlesPerSecond.next();
|
|
}
|
|
|
|
} |