1
0
forked from 0ad/0ad
0ad/source/graphics/ParticleEmitterType.cpp
bb 157c6af18e Make the space in 0 A.D. non-breaking throughout the codebase.
Avoid cases of filenames
Update years in terms and other legal(ish) documents
Don't update years in license headers, since change is not meaningful

Will add linter rule in seperate commit

Happy recompiling everyone!

Original Patch By: Nescio
Comment By: Gallaecio
Differential Revision: D2620
This was SVN commit r27786.
2023-07-27 20:54:46 +00:00

596 lines
18 KiB
C++

/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. 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 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "ParticleEmitterType.h"
#include "graphics/Color.h"
#include "graphics/ParticleEmitter.h"
#include "graphics/ParticleManager.h"
#include "graphics/TextureManager.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/XML/Xeromyces.h"
#include "renderer/Renderer.h"
#include <boost/random/uniform_real_distribution.hpp>
/**
* Interface for particle state variables, which get evaluated for each newly
* constructed particle.
*/
class IParticleVar
{
public:
IParticleVar() : m_LastValue(0) { }
virtual ~IParticleVar() {}
/// Computes and returns a new value.
float Evaluate(CParticleEmitter& emitter)
{
m_LastValue = Compute(*emitter.m_Type, emitter);
return m_LastValue;
}
/**
* Returns the last value that Evaluate returned.
* This is used for variables that depend on other variables (which is kind
* of fragile since it's very order-dependent), so they don't get re-randomised
* and don't have a danger of infinite recursion.
*/
float LastValue() { return m_LastValue; }
/**
* Returns the minimum value that Evaluate might ever return,
* for computing bounds.
*/
virtual float Min(CParticleEmitterType& type) = 0;
/**
* Returns the maximum value that Evaluate might ever return,
* for computing bounds.
*/
virtual float Max(CParticleEmitterType& type) = 0;
protected:
virtual float Compute(CParticleEmitterType& type, CParticleEmitter& emitter) = 0;
private:
float m_LastValue;
};
/**
* Particle variable that returns a constant value.
*/
class CParticleVarConstant : public IParticleVar
{
public:
CParticleVarConstant(float val) :
m_Value(val)
{
}
virtual float Compute(CParticleEmitterType& UNUSED(type), CParticleEmitter& UNUSED(emitter))
{
return m_Value;
}
virtual float Min(CParticleEmitterType& UNUSED(type))
{
return m_Value;
}
virtual float Max(CParticleEmitterType& UNUSED(type))
{
return m_Value;
}
private:
float m_Value;
};
/**
* Particle variable that returns a uniformly-distributed random value.
*/
class CParticleVarUniform : public IParticleVar
{
public:
CParticleVarUniform(float min, float max) :
m_Min(min), m_Max(max)
{
}
virtual float Compute(CParticleEmitterType& type, CParticleEmitter& UNUSED(emitter))
{
return boost::random::uniform_real_distribution<float>(m_Min, m_Max)(type.m_Manager.m_RNG);
}
virtual float Min(CParticleEmitterType& UNUSED(type))
{
return m_Min;
}
virtual float Max(CParticleEmitterType& UNUSED(type))
{
return m_Max;
}
private:
float m_Min;
float m_Max;
};
/**
* Particle variable that returns the same value as some other variable
* (assuming that variable was evaluated before this one).
*/
class CParticleVarCopy : public IParticleVar
{
public:
CParticleVarCopy(int from) :
m_From(from)
{
}
virtual float Compute(CParticleEmitterType& type, CParticleEmitter& UNUSED(emitter))
{
return type.m_Variables[m_From]->LastValue();
}
virtual float Min(CParticleEmitterType& type)
{
return type.m_Variables[m_From]->Min(type);
}
virtual float Max(CParticleEmitterType& type)
{
return type.m_Variables[m_From]->Max(type);
}
private:
int m_From;
};
/**
* A terrible ad-hoc attempt at handling some particular variable calculation,
* which really needs to be cleaned up and generalised.
*/
class CParticleVarExpr : public IParticleVar
{
public:
CParticleVarExpr(const CStr& from, float mul, float max) :
m_From(from), m_Mul(mul), m_Max(max)
{
}
virtual float Compute(CParticleEmitterType& UNUSED(type), CParticleEmitter& emitter)
{
return std::min(m_Max, emitter.m_EntityVariables[m_From] * m_Mul);
}
virtual float Min(CParticleEmitterType& UNUSED(type))
{
return 0.f;
}
virtual float Max(CParticleEmitterType& UNUSED(type))
{
return m_Max;
}
private:
CStr m_From;
float m_Mul;
float m_Max;
};
/**
* Interface for particle effectors, which get evaluated every frame to
* update particles.
*/
class IParticleEffector
{
public:
IParticleEffector() { }
virtual ~IParticleEffector() {}
/// Updates all particles.
virtual void Evaluate(std::vector<SParticle>& particles, float dt) = 0;
/// Returns maximum acceleration caused by this effector.
virtual CVector3D Max() = 0;
};
/**
* Particle effector that applies a constant acceleration.
*/
class CParticleEffectorForce : public IParticleEffector
{
public:
CParticleEffectorForce(float x, float y, float z) :
m_Accel(x, y, z)
{
}
virtual void Evaluate(std::vector<SParticle>& particles, float dt)
{
CVector3D dv = m_Accel * dt;
for (size_t i = 0; i < particles.size(); ++i)
particles[i].velocity += dv;
}
virtual CVector3D Max()
{
return m_Accel;
}
private:
CVector3D m_Accel;
};
CParticleEmitterType::CParticleEmitterType(const VfsPath& path, CParticleManager& manager) :
m_Manager(manager)
{
LoadXML(path);
// TODO: handle load failure
// Upper bound on number of particles depends on maximum rate and lifetime
m_MaxLifetime = m_Variables[VAR_LIFETIME]->Max(*this);
m_MaxParticles = ceil(m_Variables[VAR_EMISSIONRATE]->Max(*this) * m_MaxLifetime);
// Compute the worst-case bounds of all possible particles,
// based on the min/max values of positions and velocities and accelerations
// and sizes. (This isn't a guaranteed bound but it should be sufficient for
// culling.)
// Assuming constant acceleration,
// p = p0 + v0*t + 1/2 a*t^2
// => dp/dt = v0 + a*t
// = 0 at t = -v0/a
// max(p) is at t=0, or t=tmax, or t=-v0/a if that's between 0 and tmax
// => max(p) = max(p0, p0 + v0*tmax + 1/2 a*tmax, p0 - 1/2 v0^2/a)
// Compute combined acceleration (assume constant)
CVector3D accel;
for (size_t i = 0; i < m_Effectors.size(); ++i)
accel += m_Effectors[i]->Max();
CVector3D vmin(m_Variables[VAR_VELOCITY_X]->Min(*this), m_Variables[VAR_VELOCITY_Y]->Min(*this), m_Variables[VAR_VELOCITY_Z]->Min(*this));
CVector3D vmax(m_Variables[VAR_VELOCITY_X]->Max(*this), m_Variables[VAR_VELOCITY_Y]->Max(*this), m_Variables[VAR_VELOCITY_Z]->Max(*this));
// Start by assuming p0 = 0; compute each XYZ component individually
m_MaxBounds.SetEmpty();
// Lower/upper bounds at t=0, t=tmax
m_MaxBounds[0].X = std::min(0.f, vmin.X*m_MaxLifetime + 0.5f*accel.X*m_MaxLifetime*m_MaxLifetime);
m_MaxBounds[0].Y = std::min(0.f, vmin.Y*m_MaxLifetime + 0.5f*accel.Y*m_MaxLifetime*m_MaxLifetime);
m_MaxBounds[0].Z = std::min(0.f, vmin.Z*m_MaxLifetime + 0.5f*accel.Z*m_MaxLifetime*m_MaxLifetime);
m_MaxBounds[1].X = std::max(0.f, vmax.X*m_MaxLifetime + 0.5f*accel.X*m_MaxLifetime*m_MaxLifetime);
m_MaxBounds[1].Y = std::max(0.f, vmax.Y*m_MaxLifetime + 0.5f*accel.Y*m_MaxLifetime*m_MaxLifetime);
m_MaxBounds[1].Z = std::max(0.f, vmax.Z*m_MaxLifetime + 0.5f*accel.Z*m_MaxLifetime*m_MaxLifetime);
// Extend bounds to include position at t where dp/dt=0, if 0 < t < tmax
if (accel.X && 0 < -vmin.X/accel.X && -vmin.X/accel.X < m_MaxLifetime)
m_MaxBounds[0].X = std::min(m_MaxBounds[0].X, -0.5f*vmin.X*vmin.X / accel.X);
if (accel.Y && 0 < -vmin.Y/accel.Y && -vmin.Y/accel.Y < m_MaxLifetime)
m_MaxBounds[0].Y = std::min(m_MaxBounds[0].Y, -0.5f*vmin.Y*vmin.Y / accel.Y);
if (accel.Z && 0 < -vmin.Z/accel.Z && -vmin.Z/accel.Z < m_MaxLifetime)
m_MaxBounds[0].Z = std::min(m_MaxBounds[0].Z, -0.5f*vmin.Z*vmin.Z / accel.Z);
if (accel.X && 0 < -vmax.X/accel.X && -vmax.X/accel.X < m_MaxLifetime)
m_MaxBounds[1].X = std::max(m_MaxBounds[1].X, -0.5f*vmax.X*vmax.X / accel.X);
if (accel.Y && 0 < -vmax.Y/accel.Y && -vmax.Y/accel.Y < m_MaxLifetime)
m_MaxBounds[1].Y = std::max(m_MaxBounds[1].Y, -0.5f*vmax.Y*vmax.Y / accel.Y);
if (accel.Z && 0 < -vmax.Z/accel.Z && -vmax.Z/accel.Z < m_MaxLifetime)
m_MaxBounds[1].Z = std::max(m_MaxBounds[1].Z, -0.5f*vmax.Z*vmax.Z / accel.Z);
// Offset by the initial positions
m_MaxBounds[0] += CVector3D(m_Variables[VAR_POSITION_X]->Min(*this), m_Variables[VAR_POSITION_Y]->Min(*this), m_Variables[VAR_POSITION_Z]->Min(*this));
m_MaxBounds[1] += CVector3D(m_Variables[VAR_POSITION_X]->Max(*this), m_Variables[VAR_POSITION_Y]->Max(*this), m_Variables[VAR_POSITION_Z]->Max(*this));
}
int CParticleEmitterType::GetVariableID(const std::string& name)
{
if (name == "emissionrate") return VAR_EMISSIONRATE;
if (name == "lifetime") return VAR_LIFETIME;
if (name == "position.x") return VAR_POSITION_X;
if (name == "position.y") return VAR_POSITION_Y;
if (name == "position.z") return VAR_POSITION_Z;
if (name == "angle") return VAR_ANGLE;
if (name == "velocity.x") return VAR_VELOCITY_X;
if (name == "velocity.y") return VAR_VELOCITY_Y;
if (name == "velocity.z") return VAR_VELOCITY_Z;
if (name == "velocity.angle") return VAR_VELOCITY_ANGLE;
if (name == "size") return VAR_SIZE;
if (name == "size.growthRate") return VAR_SIZE_GROWTHRATE;
if (name == "color.r") return VAR_COLOR_R;
if (name == "color.g") return VAR_COLOR_G;
if (name == "color.b") return VAR_COLOR_B;
LOGWARNING("Particle sets unknown variable '%s'", name.c_str());
return -1;
}
bool CParticleEmitterType::LoadXML(const VfsPath& path)
{
// Initialise with sane defaults
m_Variables.clear();
m_Variables.resize(VAR__MAX);
m_Variables[VAR_EMISSIONRATE] = IParticleVarPtr(new CParticleVarConstant(10.f));
m_Variables[VAR_LIFETIME] = IParticleVarPtr(new CParticleVarConstant(3.f));
m_Variables[VAR_POSITION_X] = IParticleVarPtr(new CParticleVarConstant(0.f));
m_Variables[VAR_POSITION_Y] = IParticleVarPtr(new CParticleVarConstant(0.f));
m_Variables[VAR_POSITION_Z] = IParticleVarPtr(new CParticleVarConstant(0.f));
m_Variables[VAR_ANGLE] = IParticleVarPtr(new CParticleVarConstant(0.f));
m_Variables[VAR_VELOCITY_X] = IParticleVarPtr(new CParticleVarConstant(0.f));
m_Variables[VAR_VELOCITY_Y] = IParticleVarPtr(new CParticleVarConstant(1.f));
m_Variables[VAR_VELOCITY_Z] = IParticleVarPtr(new CParticleVarConstant(0.f));
m_Variables[VAR_VELOCITY_ANGLE] = IParticleVarPtr(new CParticleVarConstant(0.f));
m_Variables[VAR_SIZE] = IParticleVarPtr(new CParticleVarConstant(1.f));
m_Variables[VAR_SIZE_GROWTHRATE] = IParticleVarPtr(new CParticleVarConstant(0.f));
m_Variables[VAR_COLOR_R] = IParticleVarPtr(new CParticleVarConstant(1.f));
m_Variables[VAR_COLOR_G] = IParticleVarPtr(new CParticleVarConstant(1.f));
m_Variables[VAR_COLOR_B] = IParticleVarPtr(new CParticleVarConstant(1.f));
m_BlendMode = BlendMode::ADD;
m_StartFull = false;
m_UseRelativeVelocity = false;
m_Texture = g_Renderer.GetTextureManager().GetErrorTexture();
CXeromyces XeroFile;
PSRETURN ret = XeroFile.Load(g_VFS, path, "particle");
if (ret != PSRETURN_OK)
return false;
// Define all the elements and attributes used in the XML file
#define EL(x) int el_##x = XeroFile.GetElementID(#x)
#define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
EL(texture);
EL(blend);
EL(start_full);
EL(use_relative_velocity);
EL(constant);
EL(uniform);
EL(copy);
EL(expr);
EL(force);
AT(mode);
AT(name);
AT(value);
AT(min);
AT(max);
AT(mul);
AT(from);
AT(x);
AT(y);
AT(z);
#undef AT
#undef EL
XMBElement Root = XeroFile.GetRoot();
XERO_ITER_EL(Root, Child)
{
if (Child.GetNodeName() == el_texture)
{
CTextureProperties textureProps(Child.GetText().FromUTF8());
textureProps.SetAddressMode(
Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE);
m_Texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
}
else if (Child.GetNodeName() == el_blend)
{
const CStr mode = Child.GetAttributes().GetNamedItem(at_mode);
if (mode == "add")
m_BlendMode = BlendMode::ADD;
else if (mode == "subtract")
m_BlendMode = BlendMode::SUBTRACT;
else if (mode == "over")
m_BlendMode = BlendMode::OVERLAY;
else if (mode == "multiply")
m_BlendMode = BlendMode::MULTIPLY;
}
else if (Child.GetNodeName() == el_start_full)
{
m_StartFull = true;
}
else if (Child.GetNodeName() == el_use_relative_velocity)
{
m_UseRelativeVelocity = true;
}
else if (Child.GetNodeName() == el_constant)
{
int id = GetVariableID(Child.GetAttributes().GetNamedItem(at_name));
if (id != -1)
{
m_Variables[id] = IParticleVarPtr(new CParticleVarConstant(
Child.GetAttributes().GetNamedItem(at_value).ToFloat()
));
}
}
else if (Child.GetNodeName() == el_uniform)
{
int id = GetVariableID(Child.GetAttributes().GetNamedItem(at_name));
if (id != -1)
{
float min = Child.GetAttributes().GetNamedItem(at_min).ToFloat();
float max = Child.GetAttributes().GetNamedItem(at_max).ToFloat();
// To avoid hangs in the RNG, only use it if [min, max) is non-empty
if (min < max)
m_Variables[id] = IParticleVarPtr(new CParticleVarUniform(min, max));
else
m_Variables[id] = IParticleVarPtr(new CParticleVarConstant(min));
}
}
else if (Child.GetNodeName() == el_copy)
{
int id = GetVariableID(Child.GetAttributes().GetNamedItem(at_name));
int from = GetVariableID(Child.GetAttributes().GetNamedItem(at_from));
if (id != -1 && from != -1)
m_Variables[id] = IParticleVarPtr(new CParticleVarCopy(from));
}
else if (Child.GetNodeName() == el_expr)
{
int id = GetVariableID(Child.GetAttributes().GetNamedItem(at_name));
CStr from = Child.GetAttributes().GetNamedItem(at_from);
float mul = Child.GetAttributes().GetNamedItem(at_mul).ToFloat();
float max = Child.GetAttributes().GetNamedItem(at_max).ToFloat();
if (id != -1)
m_Variables[id] = IParticleVarPtr(new CParticleVarExpr(from, mul, max));
}
else if (Child.GetNodeName() == el_force)
{
float x = Child.GetAttributes().GetNamedItem(at_x).ToFloat();
float y = Child.GetAttributes().GetNamedItem(at_y).ToFloat();
float z = Child.GetAttributes().GetNamedItem(at_z).ToFloat();
m_Effectors.push_back(IParticleEffectorPtr(new CParticleEffectorForce(x, y, z)));
}
}
return true;
}
void CParticleEmitterType::UpdateEmitter(CParticleEmitter& emitter, float dt)
{
// If dt is very large, we should do the update in multiple small
// steps to prevent all the particles getting clumped together at
// low framerates
const float maxStepLength = 0.2f;
// Avoid wasting time by computing periods longer than the lifetime
// period of the particles
dt = std::min(dt, m_MaxLifetime);
while (dt > maxStepLength)
{
UpdateEmitterStep(emitter, maxStepLength);
dt -= maxStepLength;
}
UpdateEmitterStep(emitter, dt);
}
void CParticleEmitterType::UpdateEmitterStep(CParticleEmitter& emitter, float dt)
{
ENSURE(emitter.m_Type.get() == this);
if (emitter.m_Active)
{
float emissionRate = m_Variables[VAR_EMISSIONRATE]->Evaluate(emitter);
// Find how many new particles to spawn, and accumulate any rounding errors
// (to maintain a constant emission rate even if dt is very small)
int newParticles = floor(emitter.m_EmissionRoundingError + dt*emissionRate);
emitter.m_EmissionRoundingError += dt*emissionRate - newParticles;
// If dt was very large, there's no point spawning new particles that
// we'll immediately overwrite, so clamp it
newParticles = std::min(newParticles, (int)m_MaxParticles);
for (int i = 0; i < newParticles; ++i)
{
// Compute new particle state based on variables
SParticle particle;
particle.pos.X = m_Variables[VAR_POSITION_X]->Evaluate(emitter);
particle.pos.Y = m_Variables[VAR_POSITION_Y]->Evaluate(emitter);
particle.pos.Z = m_Variables[VAR_POSITION_Z]->Evaluate(emitter);
particle.pos += emitter.m_Pos;
if (m_UseRelativeVelocity)
{
float xVel = m_Variables[VAR_VELOCITY_X]->Evaluate(emitter);
float yVel = m_Variables[VAR_VELOCITY_Y]->Evaluate(emitter);
float zVel = m_Variables[VAR_VELOCITY_Z]->Evaluate(emitter);
CVector3D EmitterAngle = emitter.GetRotation().ToMatrix().Transform(CVector3D(xVel,yVel,zVel));
particle.velocity.X = EmitterAngle.X;
particle.velocity.Y = EmitterAngle.Y;
particle.velocity.Z = EmitterAngle.Z;
} else {
particle.velocity.X = m_Variables[VAR_VELOCITY_X]->Evaluate(emitter);
particle.velocity.Y = m_Variables[VAR_VELOCITY_Y]->Evaluate(emitter);
particle.velocity.Z = m_Variables[VAR_VELOCITY_Z]->Evaluate(emitter);
}
particle.angle = m_Variables[VAR_ANGLE]->Evaluate(emitter);
particle.angleSpeed = m_Variables[VAR_VELOCITY_ANGLE]->Evaluate(emitter);
particle.size = m_Variables[VAR_SIZE]->Evaluate(emitter);
particle.sizeGrowthRate = m_Variables[VAR_SIZE_GROWTHRATE]->Evaluate(emitter);
RGBColor color;
color.X = m_Variables[VAR_COLOR_R]->Evaluate(emitter);
color.Y = m_Variables[VAR_COLOR_G]->Evaluate(emitter);
color.Z = m_Variables[VAR_COLOR_B]->Evaluate(emitter);
particle.color = ConvertRGBColorTo4ub(color);
particle.age = 0.f;
particle.maxAge = m_Variables[VAR_LIFETIME]->Evaluate(emitter);
emitter.AddParticle(particle);
}
}
// Update particle states
for (size_t i = 0; i < emitter.m_Particles.size(); ++i)
{
SParticle& p = emitter.m_Particles[i];
// Don't bother updating particles already at the end of their life
if (p.age > p.maxAge)
continue;
p.pos += p.velocity * dt;
p.angle += p.angleSpeed * dt;
p.age += dt;
p.size += p.sizeGrowthRate * dt;
// Make alpha fade in/out nicely
// TODO: this should probably be done as a variable or something,
// instead of hardcoding
float ageFrac = p.age / p.maxAge;
float a = std::min(1.f - ageFrac, 5.f * ageFrac);
p.color.A = Clamp(static_cast<int>(a * 255.f), 0, 255);
}
for (size_t i = 0; i < m_Effectors.size(); ++i)
{
m_Effectors[i]->Evaluate(emitter.m_Particles, dt);
}
}
CBoundingBoxAligned CParticleEmitterType::CalculateBounds(CVector3D emitterPos, CBoundingBoxAligned emittedBounds)
{
CBoundingBoxAligned bounds = m_MaxBounds;
bounds[0] += emitterPos;
bounds[1] += emitterPos;
bounds += emittedBounds;
// The current bounds is for the particles' centers, so expand by
// sqrt(2) * max_size/2 to ensure any rotated billboards fit in
bounds.Expand(m_Variables[VAR_SIZE]->Max(*this)/2.f * sqrt(2.f));
return bounds;
}