[Gameplay] Reduce loom cost
[0ad.git] / source / graphics / ParticleEmitterType.cpp
blob581dd6161c58b2fb67be7fd855df93779beb00a6
1 /* Copyright (C) 2022 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "ParticleEmitterType.h"
22 #include "graphics/Color.h"
23 #include "graphics/ParticleEmitter.h"
24 #include "graphics/ParticleManager.h"
25 #include "graphics/TextureManager.h"
26 #include "maths/MathUtil.h"
27 #include "ps/CLogger.h"
28 #include "ps/Filesystem.h"
29 #include "ps/XML/Xeromyces.h"
30 #include "renderer/Renderer.h"
32 #include <boost/random/uniform_real_distribution.hpp>
35 /**
36 * Interface for particle state variables, which get evaluated for each newly
37 * constructed particle.
39 class IParticleVar
41 public:
42 IParticleVar() : m_LastValue(0) { }
43 virtual ~IParticleVar() {}
45 /// Computes and returns a new value.
46 float Evaluate(CParticleEmitter& emitter)
48 m_LastValue = Compute(*emitter.m_Type, emitter);
49 return m_LastValue;
52 /**
53 * Returns the last value that Evaluate returned.
54 * This is used for variables that depend on other variables (which is kind
55 * of fragile since it's very order-dependent), so they don't get re-randomised
56 * and don't have a danger of infinite recursion.
58 float LastValue() { return m_LastValue; }
60 /**
61 * Returns the minimum value that Evaluate might ever return,
62 * for computing bounds.
64 virtual float Min(CParticleEmitterType& type) = 0;
66 /**
67 * Returns the maximum value that Evaluate might ever return,
68 * for computing bounds.
70 virtual float Max(CParticleEmitterType& type) = 0;
72 protected:
73 virtual float Compute(CParticleEmitterType& type, CParticleEmitter& emitter) = 0;
75 private:
76 float m_LastValue;
79 /**
80 * Particle variable that returns a constant value.
82 class CParticleVarConstant : public IParticleVar
84 public:
85 CParticleVarConstant(float val) :
86 m_Value(val)
90 virtual float Compute(CParticleEmitterType& UNUSED(type), CParticleEmitter& UNUSED(emitter))
92 return m_Value;
95 virtual float Min(CParticleEmitterType& UNUSED(type))
97 return m_Value;
100 virtual float Max(CParticleEmitterType& UNUSED(type))
102 return m_Value;
105 private:
106 float m_Value;
110 * Particle variable that returns a uniformly-distributed random value.
112 class CParticleVarUniform : public IParticleVar
114 public:
115 CParticleVarUniform(float min, float max) :
116 m_Min(min), m_Max(max)
120 virtual float Compute(CParticleEmitterType& type, CParticleEmitter& UNUSED(emitter))
122 return boost::random::uniform_real_distribution<float>(m_Min, m_Max)(type.m_Manager.m_RNG);
125 virtual float Min(CParticleEmitterType& UNUSED(type))
127 return m_Min;
130 virtual float Max(CParticleEmitterType& UNUSED(type))
132 return m_Max;
135 private:
136 float m_Min;
137 float m_Max;
141 * Particle variable that returns the same value as some other variable
142 * (assuming that variable was evaluated before this one).
144 class CParticleVarCopy : public IParticleVar
146 public:
147 CParticleVarCopy(int from) :
148 m_From(from)
152 virtual float Compute(CParticleEmitterType& type, CParticleEmitter& UNUSED(emitter))
154 return type.m_Variables[m_From]->LastValue();
157 virtual float Min(CParticleEmitterType& type)
159 return type.m_Variables[m_From]->Min(type);
162 virtual float Max(CParticleEmitterType& type)
164 return type.m_Variables[m_From]->Max(type);
167 private:
168 int m_From;
172 * A terrible ad-hoc attempt at handling some particular variable calculation,
173 * which really needs to be cleaned up and generalised.
175 class CParticleVarExpr : public IParticleVar
177 public:
178 CParticleVarExpr(const CStr& from, float mul, float max) :
179 m_From(from), m_Mul(mul), m_Max(max)
183 virtual float Compute(CParticleEmitterType& UNUSED(type), CParticleEmitter& emitter)
185 return std::min(m_Max, emitter.m_EntityVariables[m_From] * m_Mul);
188 virtual float Min(CParticleEmitterType& UNUSED(type))
190 return 0.f;
193 virtual float Max(CParticleEmitterType& UNUSED(type))
195 return m_Max;
198 private:
199 CStr m_From;
200 float m_Mul;
201 float m_Max;
207 * Interface for particle effectors, which get evaluated every frame to
208 * update particles.
210 class IParticleEffector
212 public:
213 IParticleEffector() { }
214 virtual ~IParticleEffector() {}
216 /// Updates all particles.
217 virtual void Evaluate(std::vector<SParticle>& particles, float dt) = 0;
219 /// Returns maximum acceleration caused by this effector.
220 virtual CVector3D Max() = 0;
225 * Particle effector that applies a constant acceleration.
227 class CParticleEffectorForce : public IParticleEffector
229 public:
230 CParticleEffectorForce(float x, float y, float z) :
231 m_Accel(x, y, z)
235 virtual void Evaluate(std::vector<SParticle>& particles, float dt)
237 CVector3D dv = m_Accel * dt;
239 for (size_t i = 0; i < particles.size(); ++i)
240 particles[i].velocity += dv;
243 virtual CVector3D Max()
245 return m_Accel;
248 private:
249 CVector3D m_Accel;
255 CParticleEmitterType::CParticleEmitterType(const VfsPath& path, CParticleManager& manager) :
256 m_Manager(manager)
258 LoadXML(path);
259 // TODO: handle load failure
261 // Upper bound on number of particles depends on maximum rate and lifetime
262 m_MaxLifetime = m_Variables[VAR_LIFETIME]->Max(*this);
263 m_MaxParticles = ceil(m_Variables[VAR_EMISSIONRATE]->Max(*this) * m_MaxLifetime);
266 // Compute the worst-case bounds of all possible particles,
267 // based on the min/max values of positions and velocities and accelerations
268 // and sizes. (This isn't a guaranteed bound but it should be sufficient for
269 // culling.)
271 // Assuming constant acceleration,
272 // p = p0 + v0*t + 1/2 a*t^2
273 // => dp/dt = v0 + a*t
274 // = 0 at t = -v0/a
275 // max(p) is at t=0, or t=tmax, or t=-v0/a if that's between 0 and tmax
276 // => max(p) = max(p0, p0 + v0*tmax + 1/2 a*tmax, p0 - 1/2 v0^2/a)
278 // Compute combined acceleration (assume constant)
279 CVector3D accel;
280 for (size_t i = 0; i < m_Effectors.size(); ++i)
281 accel += m_Effectors[i]->Max();
283 CVector3D vmin(m_Variables[VAR_VELOCITY_X]->Min(*this), m_Variables[VAR_VELOCITY_Y]->Min(*this), m_Variables[VAR_VELOCITY_Z]->Min(*this));
284 CVector3D vmax(m_Variables[VAR_VELOCITY_X]->Max(*this), m_Variables[VAR_VELOCITY_Y]->Max(*this), m_Variables[VAR_VELOCITY_Z]->Max(*this));
286 // Start by assuming p0 = 0; compute each XYZ component individually
287 m_MaxBounds.SetEmpty();
288 // Lower/upper bounds at t=0, t=tmax
289 m_MaxBounds[0].X = std::min(0.f, vmin.X*m_MaxLifetime + 0.5f*accel.X*m_MaxLifetime*m_MaxLifetime);
290 m_MaxBounds[0].Y = std::min(0.f, vmin.Y*m_MaxLifetime + 0.5f*accel.Y*m_MaxLifetime*m_MaxLifetime);
291 m_MaxBounds[0].Z = std::min(0.f, vmin.Z*m_MaxLifetime + 0.5f*accel.Z*m_MaxLifetime*m_MaxLifetime);
292 m_MaxBounds[1].X = std::max(0.f, vmax.X*m_MaxLifetime + 0.5f*accel.X*m_MaxLifetime*m_MaxLifetime);
293 m_MaxBounds[1].Y = std::max(0.f, vmax.Y*m_MaxLifetime + 0.5f*accel.Y*m_MaxLifetime*m_MaxLifetime);
294 m_MaxBounds[1].Z = std::max(0.f, vmax.Z*m_MaxLifetime + 0.5f*accel.Z*m_MaxLifetime*m_MaxLifetime);
295 // Extend bounds to include position at t where dp/dt=0, if 0 < t < tmax
296 if (accel.X && 0 < -vmin.X/accel.X && -vmin.X/accel.X < m_MaxLifetime)
297 m_MaxBounds[0].X = std::min(m_MaxBounds[0].X, -0.5f*vmin.X*vmin.X / accel.X);
298 if (accel.Y && 0 < -vmin.Y/accel.Y && -vmin.Y/accel.Y < m_MaxLifetime)
299 m_MaxBounds[0].Y = std::min(m_MaxBounds[0].Y, -0.5f*vmin.Y*vmin.Y / accel.Y);
300 if (accel.Z && 0 < -vmin.Z/accel.Z && -vmin.Z/accel.Z < m_MaxLifetime)
301 m_MaxBounds[0].Z = std::min(m_MaxBounds[0].Z, -0.5f*vmin.Z*vmin.Z / accel.Z);
302 if (accel.X && 0 < -vmax.X/accel.X && -vmax.X/accel.X < m_MaxLifetime)
303 m_MaxBounds[1].X = std::max(m_MaxBounds[1].X, -0.5f*vmax.X*vmax.X / accel.X);
304 if (accel.Y && 0 < -vmax.Y/accel.Y && -vmax.Y/accel.Y < m_MaxLifetime)
305 m_MaxBounds[1].Y = std::max(m_MaxBounds[1].Y, -0.5f*vmax.Y*vmax.Y / accel.Y);
306 if (accel.Z && 0 < -vmax.Z/accel.Z && -vmax.Z/accel.Z < m_MaxLifetime)
307 m_MaxBounds[1].Z = std::max(m_MaxBounds[1].Z, -0.5f*vmax.Z*vmax.Z / accel.Z);
309 // Offset by the initial positions
310 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));
311 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));
314 int CParticleEmitterType::GetVariableID(const std::string& name)
316 if (name == "emissionrate") return VAR_EMISSIONRATE;
317 if (name == "lifetime") return VAR_LIFETIME;
318 if (name == "position.x") return VAR_POSITION_X;
319 if (name == "position.y") return VAR_POSITION_Y;
320 if (name == "position.z") return VAR_POSITION_Z;
321 if (name == "angle") return VAR_ANGLE;
322 if (name == "velocity.x") return VAR_VELOCITY_X;
323 if (name == "velocity.y") return VAR_VELOCITY_Y;
324 if (name == "velocity.z") return VAR_VELOCITY_Z;
325 if (name == "velocity.angle") return VAR_VELOCITY_ANGLE;
326 if (name == "size") return VAR_SIZE;
327 if (name == "size.growthRate") return VAR_SIZE_GROWTHRATE;
328 if (name == "color.r") return VAR_COLOR_R;
329 if (name == "color.g") return VAR_COLOR_G;
330 if (name == "color.b") return VAR_COLOR_B;
331 LOGWARNING("Particle sets unknown variable '%s'", name.c_str());
332 return -1;
335 bool CParticleEmitterType::LoadXML(const VfsPath& path)
337 // Initialise with sane defaults
338 m_Variables.clear();
339 m_Variables.resize(VAR__MAX);
340 m_Variables[VAR_EMISSIONRATE] = IParticleVarPtr(new CParticleVarConstant(10.f));
341 m_Variables[VAR_LIFETIME] = IParticleVarPtr(new CParticleVarConstant(3.f));
342 m_Variables[VAR_POSITION_X] = IParticleVarPtr(new CParticleVarConstant(0.f));
343 m_Variables[VAR_POSITION_Y] = IParticleVarPtr(new CParticleVarConstant(0.f));
344 m_Variables[VAR_POSITION_Z] = IParticleVarPtr(new CParticleVarConstant(0.f));
345 m_Variables[VAR_ANGLE] = IParticleVarPtr(new CParticleVarConstant(0.f));
346 m_Variables[VAR_VELOCITY_X] = IParticleVarPtr(new CParticleVarConstant(0.f));
347 m_Variables[VAR_VELOCITY_Y] = IParticleVarPtr(new CParticleVarConstant(1.f));
348 m_Variables[VAR_VELOCITY_Z] = IParticleVarPtr(new CParticleVarConstant(0.f));
349 m_Variables[VAR_VELOCITY_ANGLE] = IParticleVarPtr(new CParticleVarConstant(0.f));
350 m_Variables[VAR_SIZE] = IParticleVarPtr(new CParticleVarConstant(1.f));
351 m_Variables[VAR_SIZE_GROWTHRATE] = IParticleVarPtr(new CParticleVarConstant(0.f));
352 m_Variables[VAR_COLOR_R] = IParticleVarPtr(new CParticleVarConstant(1.f));
353 m_Variables[VAR_COLOR_G] = IParticleVarPtr(new CParticleVarConstant(1.f));
354 m_Variables[VAR_COLOR_B] = IParticleVarPtr(new CParticleVarConstant(1.f));
355 m_BlendMode = BlendMode::ADD;
356 m_StartFull = false;
357 m_UseRelativeVelocity = false;
358 m_Texture = g_Renderer.GetTextureManager().GetErrorTexture();
360 CXeromyces XeroFile;
361 PSRETURN ret = XeroFile.Load(g_VFS, path, "particle");
362 if (ret != PSRETURN_OK)
363 return false;
365 // Define all the elements and attributes used in the XML file
366 #define EL(x) int el_##x = XeroFile.GetElementID(#x)
367 #define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
368 EL(texture);
369 EL(blend);
370 EL(start_full);
371 EL(use_relative_velocity);
372 EL(constant);
373 EL(uniform);
374 EL(copy);
375 EL(expr);
376 EL(force);
377 AT(mode);
378 AT(name);
379 AT(value);
380 AT(min);
381 AT(max);
382 AT(mul);
383 AT(from);
384 AT(x);
385 AT(y);
386 AT(z);
387 #undef AT
388 #undef EL
390 XMBElement Root = XeroFile.GetRoot();
392 XERO_ITER_EL(Root, Child)
394 if (Child.GetNodeName() == el_texture)
396 CTextureProperties textureProps(Child.GetText().FromUTF8());
397 textureProps.SetAddressMode(
398 Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE);
399 m_Texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
401 else if (Child.GetNodeName() == el_blend)
403 const CStr mode = Child.GetAttributes().GetNamedItem(at_mode);
404 if (mode == "add")
405 m_BlendMode = BlendMode::ADD;
406 else if (mode == "subtract")
407 m_BlendMode = BlendMode::SUBTRACT;
408 else if (mode == "over")
409 m_BlendMode = BlendMode::OVERLAY;
410 else if (mode == "multiply")
411 m_BlendMode = BlendMode::MULTIPLY;
413 else if (Child.GetNodeName() == el_start_full)
415 m_StartFull = true;
417 else if (Child.GetNodeName() == el_use_relative_velocity)
419 m_UseRelativeVelocity = true;
421 else if (Child.GetNodeName() == el_constant)
423 int id = GetVariableID(Child.GetAttributes().GetNamedItem(at_name));
424 if (id != -1)
426 m_Variables[id] = IParticleVarPtr(new CParticleVarConstant(
427 Child.GetAttributes().GetNamedItem(at_value).ToFloat()
431 else if (Child.GetNodeName() == el_uniform)
433 int id = GetVariableID(Child.GetAttributes().GetNamedItem(at_name));
434 if (id != -1)
436 float min = Child.GetAttributes().GetNamedItem(at_min).ToFloat();
437 float max = Child.GetAttributes().GetNamedItem(at_max).ToFloat();
438 // To avoid hangs in the RNG, only use it if [min, max) is non-empty
439 if (min < max)
440 m_Variables[id] = IParticleVarPtr(new CParticleVarUniform(min, max));
441 else
442 m_Variables[id] = IParticleVarPtr(new CParticleVarConstant(min));
445 else if (Child.GetNodeName() == el_copy)
447 int id = GetVariableID(Child.GetAttributes().GetNamedItem(at_name));
448 int from = GetVariableID(Child.GetAttributes().GetNamedItem(at_from));
449 if (id != -1 && from != -1)
450 m_Variables[id] = IParticleVarPtr(new CParticleVarCopy(from));
452 else if (Child.GetNodeName() == el_expr)
454 int id = GetVariableID(Child.GetAttributes().GetNamedItem(at_name));
455 CStr from = Child.GetAttributes().GetNamedItem(at_from);
456 float mul = Child.GetAttributes().GetNamedItem(at_mul).ToFloat();
457 float max = Child.GetAttributes().GetNamedItem(at_max).ToFloat();
458 if (id != -1)
459 m_Variables[id] = IParticleVarPtr(new CParticleVarExpr(from, mul, max));
461 else if (Child.GetNodeName() == el_force)
463 float x = Child.GetAttributes().GetNamedItem(at_x).ToFloat();
464 float y = Child.GetAttributes().GetNamedItem(at_y).ToFloat();
465 float z = Child.GetAttributes().GetNamedItem(at_z).ToFloat();
466 m_Effectors.push_back(IParticleEffectorPtr(new CParticleEffectorForce(x, y, z)));
470 return true;
473 void CParticleEmitterType::UpdateEmitter(CParticleEmitter& emitter, float dt)
475 // If dt is very large, we should do the update in multiple small
476 // steps to prevent all the particles getting clumped together at
477 // low framerates
479 const float maxStepLength = 0.2f;
481 // Avoid wasting time by computing periods longer than the lifetime
482 // period of the particles
483 dt = std::min(dt, m_MaxLifetime);
485 while (dt > maxStepLength)
487 UpdateEmitterStep(emitter, maxStepLength);
488 dt -= maxStepLength;
491 UpdateEmitterStep(emitter, dt);
494 void CParticleEmitterType::UpdateEmitterStep(CParticleEmitter& emitter, float dt)
496 ENSURE(emitter.m_Type.get() == this);
498 if (emitter.m_Active)
500 float emissionRate = m_Variables[VAR_EMISSIONRATE]->Evaluate(emitter);
502 // Find how many new particles to spawn, and accumulate any rounding errors
503 // (to maintain a constant emission rate even if dt is very small)
504 int newParticles = floor(emitter.m_EmissionRoundingError + dt*emissionRate);
505 emitter.m_EmissionRoundingError += dt*emissionRate - newParticles;
507 // If dt was very large, there's no point spawning new particles that
508 // we'll immediately overwrite, so clamp it
509 newParticles = std::min(newParticles, (int)m_MaxParticles);
511 for (int i = 0; i < newParticles; ++i)
513 // Compute new particle state based on variables
514 SParticle particle;
516 particle.pos.X = m_Variables[VAR_POSITION_X]->Evaluate(emitter);
517 particle.pos.Y = m_Variables[VAR_POSITION_Y]->Evaluate(emitter);
518 particle.pos.Z = m_Variables[VAR_POSITION_Z]->Evaluate(emitter);
519 particle.pos += emitter.m_Pos;
521 if (m_UseRelativeVelocity)
523 float xVel = m_Variables[VAR_VELOCITY_X]->Evaluate(emitter);
524 float yVel = m_Variables[VAR_VELOCITY_Y]->Evaluate(emitter);
525 float zVel = m_Variables[VAR_VELOCITY_Z]->Evaluate(emitter);
526 CVector3D EmitterAngle = emitter.GetRotation().ToMatrix().Transform(CVector3D(xVel,yVel,zVel));
527 particle.velocity.X = EmitterAngle.X;
528 particle.velocity.Y = EmitterAngle.Y;
529 particle.velocity.Z = EmitterAngle.Z;
530 } else {
531 particle.velocity.X = m_Variables[VAR_VELOCITY_X]->Evaluate(emitter);
532 particle.velocity.Y = m_Variables[VAR_VELOCITY_Y]->Evaluate(emitter);
533 particle.velocity.Z = m_Variables[VAR_VELOCITY_Z]->Evaluate(emitter);
535 particle.angle = m_Variables[VAR_ANGLE]->Evaluate(emitter);
536 particle.angleSpeed = m_Variables[VAR_VELOCITY_ANGLE]->Evaluate(emitter);
538 particle.size = m_Variables[VAR_SIZE]->Evaluate(emitter);
539 particle.sizeGrowthRate = m_Variables[VAR_SIZE_GROWTHRATE]->Evaluate(emitter);
541 RGBColor color;
542 color.X = m_Variables[VAR_COLOR_R]->Evaluate(emitter);
543 color.Y = m_Variables[VAR_COLOR_G]->Evaluate(emitter);
544 color.Z = m_Variables[VAR_COLOR_B]->Evaluate(emitter);
545 particle.color = ConvertRGBColorTo4ub(color);
547 particle.age = 0.f;
548 particle.maxAge = m_Variables[VAR_LIFETIME]->Evaluate(emitter);
550 emitter.AddParticle(particle);
554 // Update particle states
555 for (size_t i = 0; i < emitter.m_Particles.size(); ++i)
557 SParticle& p = emitter.m_Particles[i];
559 // Don't bother updating particles already at the end of their life
560 if (p.age > p.maxAge)
561 continue;
563 p.pos += p.velocity * dt;
564 p.angle += p.angleSpeed * dt;
565 p.age += dt;
566 p.size += p.sizeGrowthRate * dt;
568 // Make alpha fade in/out nicely
569 // TODO: this should probably be done as a variable or something,
570 // instead of hardcoding
571 float ageFrac = p.age / p.maxAge;
572 float a = std::min(1.f - ageFrac, 5.f * ageFrac);
573 p.color.A = Clamp(static_cast<int>(a * 255.f), 0, 255);
576 for (size_t i = 0; i < m_Effectors.size(); ++i)
578 m_Effectors[i]->Evaluate(emitter.m_Particles, dt);
582 CBoundingBoxAligned CParticleEmitterType::CalculateBounds(CVector3D emitterPos, CBoundingBoxAligned emittedBounds)
584 CBoundingBoxAligned bounds = m_MaxBounds;
585 bounds[0] += emitterPos;
586 bounds[1] += emitterPos;
588 bounds += emittedBounds;
590 // The current bounds is for the particles' centers, so expand by
591 // sqrt(2) * max_size/2 to ensure any rotated billboards fit in
592 bounds.Expand(m_Variables[VAR_SIZE]->Max(*this)/2.f * sqrt(2.f));
594 return bounds;