Files
fs2open.github.com/code/particle/effects/GenericShapeEffect.h
T
Asteroth 63df812d79 Add beam particles (#5330)
* add beam particles

* tweaks

* more tweaks

* fix warning

* dumb c++

* fix switch

* spelling
2023-04-29 08:45:31 -04:00

264 lines
7.9 KiB
C++

#ifndef GENERIC_SHAPE_EFFECT_H
#define GENERIC_SHAPE_EFFECT_H
#include "osapi/dialogs.h"
#include "parse/parselo.h"
#pragma once
#include "globalincs/pstypes.h"
#include "particle/ParticleEffect.h"
#include "particle/ParticleManager.h"
#include "particle/util/ParticleProperties.h"
#include "particle/util/EffectTiming.h"
#include "utils/RandomRange.h"
#include "weapon/beam.h"
namespace particle {
namespace effects {
enum class ConeDirection {
Incoming,
Normal,
Reflected,
Reverse
};
/**
* @brief A generic particle effect with a customizable shape
*
* @ingroup particleEffects
*/
template<typename TShape>
class GenericShapeEffect : public ParticleEffect {
private:
util::ParticleProperties m_particleProperties;
ConeDirection m_direction = ConeDirection::Incoming;
::util::UniformFloatRange m_velocity;
::util::UniformUIntRange m_particleNum;
float m_particleChance;
::util::UniformFloatRange m_particleRoll;
ParticleEffectHandle m_particleTrail = ParticleEffectHandle::invalid();
util::EffectTiming m_timing;
::util::UniformFloatRange m_vel_inherit;
TShape m_shape;
vec3d getNewDirection(const ParticleSource* source) const {
switch (m_direction) {
case ConeDirection::Incoming:
return source->getOrientation()->getDirectionVector(source->getOrigin());
case ConeDirection::Normal: {
vec3d normal;
if (!source->getOrientation()->getNormal(&normal)) {
mprintf(("Effect '%s' tried to use normal direction for source without a normal!\n", m_name.c_str()));
return source->getOrientation()->getDirectionVector(source->getOrigin());
}
return normal;
}
case ConeDirection::Reflected: {
vec3d out = source->getOrientation()->getDirectionVector(source->getOrigin());
vec3d normal;
if (!source->getOrientation()->getNormal(&normal)) {
mprintf(("Effect '%s' tried to use normal direction for source without a normal!\n", m_name.c_str()));
return out;
}
// Compute reflect vector as R = V - 2*(V dot N)*N where N
// is the normal and V is the incoming direction
auto dot = 2 * vm_vec_dot(&out, &normal);
vm_vec_scale(&normal, dot);
vm_vec_sub(&out, &out, &normal);
return out;
}
case ConeDirection::Reverse: {
vec3d out = source->getOrientation()->getDirectionVector(source->getOrigin());
vm_vec_scale(&out, -1.0f);
return out;
}
default:
Error(LOCATION, "Unhandled direction value!");
return vmd_zero_vector;
}
}
public:
explicit GenericShapeEffect(const SCP_string& name) : ParticleEffect(name) {
}
bool processSource(ParticleSource* source) override {
if (!m_timing.continueProcessing(source)) {
return false;
}
// This uses the internal features of the timing class for determining if and how many effects should be
// triggered this frame
util::EffectTiming::TimingState time_state;
while (m_timing.shouldCreateEffect(source, time_state)) {
auto num = m_particleNum.next();
if (source->getOrigin()->getType() == SourceOriginType::BEAM) {
// beam particle numbers are per km
object* b_obj = source->getOrigin()->getObjectHost();
float dist = vm_vec_dist(&Beams[b_obj->instance].last_start, &Beams[b_obj->instance].last_shot) / 1000.0f;
float km;
float remainder = modff(dist, &km);
uint old_num = num;
num = (uint)(old_num * km); // multiply by the number of kilometers
num += (uint)(old_num * remainder); // try to add any remainders if we have more than 1 per kilometer
// if we still have nothing let's give it one last shot
if (num < 1 && frand() < remainder * old_num)
num += 1;
}
vec3d dir = getNewDirection(source);
matrix dirMatrix;
vm_vector_2_matrix(&dirMatrix, &dir, nullptr, nullptr);
for (uint i = 0; i < num; ++i) {
if (m_particleChance < 1.0f) {
auto roll = m_particleRoll.next();
if (roll <= 0.0f)
continue;
}
matrix velRotation = m_shape.getDisplacementMatrix();
matrix rotatedVel;
vm_matrix_x_matrix(&rotatedVel, &dirMatrix, &velRotation);
particle_info info;
source->getOrigin()->applyToParticleInfo(info);
vec3d velocity = rotatedVel.vec.fvec;
if (TShape::scale_velocity_deviation()) {
// 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
// only achieved when the particle travels directly on the normal/reflect vector
vm_vec_scale(&velocity, vm_vec_dot(&velocity, &dir));
}
vm_vec_scale(&velocity, m_velocity.next());
info.vel *= m_vel_inherit.next();
info.vel += velocity;
if (m_particleTrail.isValid()) {
auto part = m_particleProperties.createPersistentParticle(info);
// 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.moveToParticle(part);
trailSource.finish();
}
} else {
// We don't have a trail so we don't need a persistent particle
m_particleProperties.createParticle(info);
}
}
}
return true;
}
void parseValues(bool nocreate) override {
m_particleProperties.parse(nocreate);
m_shape.parse(nocreate);
if (internal::required_string_if_new("+Velocity:", nocreate)) {
m_velocity = ::util::parseUniformRange<float>();
}
if (internal::required_string_if_new("+Number:", nocreate)) {
m_particleNum = ::util::parseUniformRange<uint>();
}
if (!nocreate) {
m_particleChance = 1.0f;
}
if (optional_string("+Chance:")) {
float chance;
stuff_float(&chance);
if (chance <= 0.0f) {
Warning(LOCATION,
"Particle %s tried to set +Chance: %f\nChances below 0 would result in no particles.",
m_name.c_str(), chance);
} else if (chance > 1.0f) {
Warning(LOCATION,
"Particle %s tried to set +Chance: %f\nChances above 1 are ignored, please use +Number: (min,max) "
"to spawn multiple particles.", m_name.c_str(), chance);
chance = 1.0f;
}
m_particleChance = chance;
}
m_particleRoll = ::util::UniformFloatRange(m_particleChance - 1.0f, m_particleChance);
if (optional_string("+Direction:")) {
SCP_string dirStr;
stuff_string(dirStr, F_NAME);
if (!stricmp(dirStr.c_str(), "Incoming")) {
m_direction = ConeDirection::Incoming;
}
else if (!stricmp(dirStr.c_str(), "Normal")) {
m_direction = ConeDirection::Normal;
}
else if (!stricmp(dirStr.c_str(), "Reflected")) {
m_direction = ConeDirection::Reflected;
}
else if (!stricmp(dirStr.c_str(), "Reverse")) {
m_direction = ConeDirection::Reverse;
}
else {
error_display(0, "Unknown direction name '%s'!", dirStr.c_str());
}
}
bool saw_deprecated_effect_location = false;
if (optional_string("+Trail effect:")) {
// This is the deprecated location since this introduces ambiguities in the parsing process
m_particleTrail = internal::parseEffectElement();
saw_deprecated_effect_location = true;
}
if (optional_string("+Parent Velocity Factor:")) {
m_vel_inherit = ::util::parseUniformRange<float>();
}
m_timing = util::EffectTiming::parseTiming();
if (optional_string("+Trail effect:")) {
// This is the new and correct location. This might create duplicate effects but the warning should be clear
// enough to avoid that
if (saw_deprecated_effect_location) {
error_display(0, "Found two trail effect options! Specifying '+Trail effect:' before '+Duration:' is "
"deprecated since that can cause issues with conflicting effect options.");
}
m_particleTrail = internal::parseEffectElement();
}
}
void initializeSource(ParticleSource& source) override {
m_timing.applyToSource(&source);
}
EffectType getType() const override { return m_shape.getType(); }
void pageIn() override {
m_particleProperties.pageIn();
}
};
}
}
#endif // GENERIC_SHAPE_EFFECT_H