1
0
forked from 0ad/0ad
0ad/source/graphics/ParticleEmitterType.cpp

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;
}