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>
36 * Interface for particle state variables, which get evaluated for each newly
37 * constructed particle.
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
);
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
; }
61 * Returns the minimum value that Evaluate might ever return,
62 * for computing bounds.
64 virtual float Min(CParticleEmitterType
& type
) = 0;
67 * Returns the maximum value that Evaluate might ever return,
68 * for computing bounds.
70 virtual float Max(CParticleEmitterType
& type
) = 0;
73 virtual float Compute(CParticleEmitterType
& type
, CParticleEmitter
& emitter
) = 0;
80 * Particle variable that returns a constant value.
82 class CParticleVarConstant
: public IParticleVar
85 CParticleVarConstant(float val
) :
90 virtual float Compute(CParticleEmitterType
& UNUSED(type
), CParticleEmitter
& UNUSED(emitter
))
95 virtual float Min(CParticleEmitterType
& UNUSED(type
))
100 virtual float Max(CParticleEmitterType
& UNUSED(type
))
110 * Particle variable that returns a uniformly-distributed random value.
112 class CParticleVarUniform
: public IParticleVar
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
))
130 virtual float Max(CParticleEmitterType
& UNUSED(type
))
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
147 CParticleVarCopy(int 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
);
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
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
))
193 virtual float Max(CParticleEmitterType
& UNUSED(type
))
207 * Interface for particle effectors, which get evaluated every frame to
210 class IParticleEffector
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
230 CParticleEffectorForce(float x
, float y
, float 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()
255 CParticleEmitterType::CParticleEmitterType(const VfsPath
& path
, CParticleManager
& manager
) :
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
271 // Assuming constant acceleration,
272 // p = p0 + v0*t + 1/2 a*t^2
273 // => dp/dt = v0 + a*t
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)
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());
335 bool CParticleEmitterType::LoadXML(const VfsPath
& path
)
337 // Initialise with sane defaults
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
;
357 m_UseRelativeVelocity
= false;
358 m_Texture
= g_Renderer
.GetTextureManager().GetErrorTexture();
361 PSRETURN ret
= XeroFile
.Load(g_VFS
, path
, "particle");
362 if (ret
!= PSRETURN_OK
)
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)
371 EL(use_relative_velocity
);
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
);
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
)
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
));
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
));
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
440 m_Variables
[id
] = IParticleVarPtr(new CParticleVarUniform(min
, max
));
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();
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
)));
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
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
);
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
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
;
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
);
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
);
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
)
563 p
.pos
+= p
.velocity
* dt
;
564 p
.angle
+= p
.angleSpeed
* 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
));