1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "particles.h"
4
5#include <base/dbg.h>
6#include <base/math.h>
7#include <base/time.h>
8
9#include <engine/demo.h>
10#include <engine/graphics.h>
11
12#include <generated/client_data.h>
13
14#include <game/client/gameclient.h>
15
16CParticles::CParticles()
17{
18 OnReset();
19 m_RenderTrail.m_pParts = this;
20 m_RenderTrailExtra.m_pParts = this;
21 m_RenderExplosions.m_pParts = this;
22 m_RenderExtra.m_pParts = this;
23 m_RenderGeneral.m_pParts = this;
24}
25
26void CParticles::OnReset()
27{
28 // reset particles
29 for(int i = 0; i < MAX_PARTICLES; i++)
30 {
31 m_aParticles[i].m_PrevPart = i - 1;
32 m_aParticles[i].m_NextPart = i + 1;
33 }
34
35 m_aParticles[0].m_PrevPart = 0;
36 m_aParticles[MAX_PARTICLES - 1].m_NextPart = -1;
37 m_FirstFree = 0;
38
39 for(int &FirstPart : m_aFirstPart)
40 FirstPart = -1;
41}
42
43void CParticles::Add(int Group, CParticle *pPart, float TimePassed)
44{
45 if(GameClient()->IsWorldPaused() || GameClient()->IsDemoPlaybackPaused())
46 {
47 return;
48 }
49
50 if(m_FirstFree == -1)
51 return;
52
53 // remove from the free list
54 int Id = m_FirstFree;
55 m_FirstFree = m_aParticles[Id].m_NextPart;
56 if(m_FirstFree != -1)
57 m_aParticles[m_FirstFree].m_PrevPart = -1;
58
59 // copy data
60 m_aParticles[Id] = *pPart;
61
62 // insert to the group list
63 m_aParticles[Id].m_PrevPart = -1;
64 m_aParticles[Id].m_NextPart = m_aFirstPart[Group];
65 if(m_aFirstPart[Group] != -1)
66 m_aParticles[m_aFirstPart[Group]].m_PrevPart = Id;
67 m_aFirstPart[Group] = Id;
68
69 // set some parameters
70 m_aParticles[Id].m_Life = TimePassed;
71}
72
73void CParticles::Update(float TimePassed)
74{
75 if(TimePassed <= 0.0f)
76 return;
77
78 m_FrictionFraction += TimePassed;
79
80 if(m_FrictionFraction > 2.0f) // safety measure
81 m_FrictionFraction = 0;
82
83 int FrictionCount = 0;
84 while(m_FrictionFraction > 0.05f)
85 {
86 FrictionCount++;
87 m_FrictionFraction -= 0.05f;
88 }
89
90 for(int &FirstPart : m_aFirstPart)
91 {
92 int i = FirstPart;
93 while(i != -1)
94 {
95 int Next = m_aParticles[i].m_NextPart;
96 m_aParticles[i].m_Vel.y += m_aParticles[i].m_Gravity * TimePassed;
97
98 for(int f = 0; f < FrictionCount; f++) // apply friction
99 m_aParticles[i].m_Vel *= m_aParticles[i].m_Friction;
100
101 // move the point
102 vec2 Vel = m_aParticles[i].m_Vel * TimePassed;
103 if(m_aParticles[i].m_Collides)
104 {
105 Collision()->MovePoint(pInoutPos: &m_aParticles[i].m_Pos, pInoutVel: &Vel, Elasticity: random_float(min: 0.1f, max: 1.0f), pBounces: nullptr);
106 }
107 else
108 {
109 m_aParticles[i].m_Pos += Vel;
110 }
111 m_aParticles[i].m_Vel = Vel * (1.0f / TimePassed);
112
113 m_aParticles[i].m_Life += TimePassed;
114 m_aParticles[i].m_Rot += TimePassed * m_aParticles[i].m_Rotspeed;
115
116 // check particle death
117 if(m_aParticles[i].m_Life > m_aParticles[i].m_LifeSpan)
118 {
119 // remove it from the group list
120 if(m_aParticles[i].m_PrevPart != -1)
121 m_aParticles[m_aParticles[i].m_PrevPart].m_NextPart = m_aParticles[i].m_NextPart;
122 else
123 FirstPart = m_aParticles[i].m_NextPart;
124
125 if(m_aParticles[i].m_NextPart != -1)
126 m_aParticles[m_aParticles[i].m_NextPart].m_PrevPart = m_aParticles[i].m_PrevPart;
127
128 // insert to the free list
129 if(m_FirstFree != -1)
130 m_aParticles[m_FirstFree].m_PrevPart = i;
131 m_aParticles[i].m_PrevPart = -1;
132 m_aParticles[i].m_NextPart = m_FirstFree;
133 m_FirstFree = i;
134 }
135
136 i = Next;
137 }
138 }
139}
140
141void CParticles::OnRender()
142{
143 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
144 return;
145
146 set_new_tick();
147 const int64_t Now = time();
148 Update(TimePassed: (float)((Now - m_LastRenderTime) / (double)time_freq()) * GameClient()->GetAnimationPlaybackSpeed());
149 m_LastRenderTime = Now;
150}
151
152void CParticles::OnInit()
153{
154 Graphics()->QuadsSetRotation(Angle: 0);
155 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
156
157 m_ParticleQuadContainerIndex = Graphics()->CreateQuadContainer(AutomaticUpload: false);
158
159 for(int i = 0; i <= (SPRITE_PART9 - SPRITE_PART_SLICE); ++i)
160 {
161 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
162 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ParticleQuadContainerIndex, Size: 1.f);
163 }
164 Graphics()->QuadContainerUpload(ContainerIndex: m_ParticleQuadContainerIndex);
165
166 m_ExtraParticleQuadContainerIndex = Graphics()->CreateQuadContainer(AutomaticUpload: false);
167
168 for(int i = 0; i <= (SPRITE_PART_SPARKLE - SPRITE_PART_SNOWFLAKE); ++i)
169 {
170 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
171 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ExtraParticleQuadContainerIndex, Size: 1.f);
172 }
173
174 Graphics()->QuadContainerUpload(ContainerIndex: m_ExtraParticleQuadContainerIndex);
175}
176
177bool CParticles::ParticleIsVisibleOnScreen(const vec2 &CurPos, float CurSize)
178{
179 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
180 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
181
182 // for simplicity assume the worst case rotation, that increases the bounding box around the particle by its diagonal
183 const float SqrtOf2 = std::sqrt(x: 2);
184 CurSize = SqrtOf2 * CurSize;
185
186 // always uses the mid of the particle
187 float SizeHalf = CurSize / 2;
188
189 return CurPos.x + SizeHalf >= ScreenX0 && CurPos.x - SizeHalf <= ScreenX1 && CurPos.y + SizeHalf >= ScreenY0 && CurPos.y - SizeHalf <= ScreenY1;
190}
191
192void CParticles::RenderGroup(int Group)
193{
194 IGraphics::CTextureHandle *aParticles = GameClient()->m_ParticlesSkin.m_aSpriteParticles;
195 int FirstParticleOffset = SPRITE_PART_SLICE;
196 int ParticleQuadContainerIndex = m_ParticleQuadContainerIndex;
197 if(Group == GROUP_EXTRA || Group == GROUP_TRAIL_EXTRA)
198 {
199 aParticles = GameClient()->m_ExtrasSkin.m_aSpriteParticles;
200 FirstParticleOffset = SPRITE_PART_SNOWFLAKE;
201 ParticleQuadContainerIndex = m_ExtraParticleQuadContainerIndex;
202 }
203
204 // don't use the buffer methods here, else the old renderer gets many draw calls
205 if(Graphics()->IsQuadContainerBufferingEnabled())
206 {
207 int i = m_aFirstPart[Group];
208
209 static IGraphics::SRenderSpriteInfo s_aParticleRenderInfo[MAX_PARTICLES];
210
211 int CurParticleRenderCount = 0;
212
213 // batching makes sense for stuff like ninja particles
214 ColorRGBA LastColor;
215 int LastQuadOffset = 0;
216
217 if(i != -1)
218 {
219 float Alpha = m_aParticles[i].m_Color.a;
220 if(m_aParticles[i].m_UseAlphaFading)
221 {
222 float a = m_aParticles[i].m_Life / m_aParticles[i].m_LifeSpan;
223 Alpha = mix(a: m_aParticles[i].m_StartAlpha, b: m_aParticles[i].m_EndAlpha, amount: a);
224 }
225 LastColor.r = m_aParticles[i].m_Color.r;
226 LastColor.g = m_aParticles[i].m_Color.g;
227 LastColor.b = m_aParticles[i].m_Color.b;
228 LastColor.a = Alpha;
229
230 Graphics()->SetColor(
231 r: m_aParticles[i].m_Color.r,
232 g: m_aParticles[i].m_Color.g,
233 b: m_aParticles[i].m_Color.b,
234 a: Alpha);
235
236 LastQuadOffset = m_aParticles[i].m_Spr;
237 }
238
239 while(i != -1)
240 {
241 int QuadOffset = m_aParticles[i].m_Spr;
242 float a = m_aParticles[i].m_Life / m_aParticles[i].m_LifeSpan;
243 vec2 p = m_aParticles[i].m_Pos;
244 float Size = mix(a: m_aParticles[i].m_StartSize, b: m_aParticles[i].m_EndSize, amount: a);
245 float Alpha = m_aParticles[i].m_Color.a;
246 if(m_aParticles[i].m_UseAlphaFading)
247 {
248 Alpha = mix(a: m_aParticles[i].m_StartAlpha, b: m_aParticles[i].m_EndAlpha, amount: a);
249 }
250
251 // the current position, respecting the size, is inside the viewport, render it, else ignore
252 if(ParticleIsVisibleOnScreen(CurPos: p, CurSize: Size))
253 {
254 if((size_t)CurParticleRenderCount == GRAPHICS_MAX_PARTICLES_RENDER_COUNT || LastColor.r != m_aParticles[i].m_Color.r || LastColor.g != m_aParticles[i].m_Color.g || LastColor.b != m_aParticles[i].m_Color.b || LastColor.a != Alpha || LastQuadOffset != QuadOffset)
255 {
256 dbg_assert(LastQuadOffset >= FirstParticleOffset, "Invalid particle offsets: %d < %d", LastQuadOffset, FirstParticleOffset);
257 Graphics()->TextureSet(Texture: aParticles[LastQuadOffset - FirstParticleOffset]);
258 Graphics()->RenderQuadContainerAsSpriteMultiple(ContainerIndex: ParticleQuadContainerIndex, QuadOffset: LastQuadOffset - FirstParticleOffset, DrawCount: CurParticleRenderCount, pRenderInfo: s_aParticleRenderInfo);
259 CurParticleRenderCount = 0;
260 LastQuadOffset = QuadOffset;
261
262 Graphics()->SetColor(
263 r: m_aParticles[i].m_Color.r,
264 g: m_aParticles[i].m_Color.g,
265 b: m_aParticles[i].m_Color.b,
266 a: Alpha);
267
268 LastColor.r = m_aParticles[i].m_Color.r;
269 LastColor.g = m_aParticles[i].m_Color.g;
270 LastColor.b = m_aParticles[i].m_Color.b;
271 LastColor.a = Alpha;
272 }
273
274 s_aParticleRenderInfo[CurParticleRenderCount].m_Pos[0] = p.x;
275 s_aParticleRenderInfo[CurParticleRenderCount].m_Pos[1] = p.y;
276 s_aParticleRenderInfo[CurParticleRenderCount].m_Scale = Size;
277 s_aParticleRenderInfo[CurParticleRenderCount].m_Rotation = m_aParticles[i].m_Rot;
278
279 ++CurParticleRenderCount;
280 }
281
282 i = m_aParticles[i].m_NextPart;
283 }
284
285 if(CurParticleRenderCount > 0)
286 {
287 dbg_assert(LastQuadOffset >= FirstParticleOffset, "Invalid particle offsets: %d < %d", LastQuadOffset, FirstParticleOffset);
288 Graphics()->TextureSet(Texture: aParticles[LastQuadOffset - FirstParticleOffset]);
289 Graphics()->RenderQuadContainerAsSpriteMultiple(ContainerIndex: ParticleQuadContainerIndex, QuadOffset: LastQuadOffset - FirstParticleOffset, DrawCount: CurParticleRenderCount, pRenderInfo: s_aParticleRenderInfo);
290 }
291 }
292 else
293 {
294 int i = m_aFirstPart[Group];
295
296 Graphics()->WrapClamp();
297
298 while(i != -1)
299 {
300 float a = m_aParticles[i].m_Life / m_aParticles[i].m_LifeSpan;
301 vec2 p = m_aParticles[i].m_Pos;
302 float Size = mix(a: m_aParticles[i].m_StartSize, b: m_aParticles[i].m_EndSize, amount: a);
303 float Alpha = m_aParticles[i].m_Color.a;
304 if(m_aParticles[i].m_UseAlphaFading)
305 {
306 Alpha = mix(a: m_aParticles[i].m_StartAlpha, b: m_aParticles[i].m_EndAlpha, amount: a);
307 }
308
309 // the current position, respecting the size, is inside the viewport, render it, else ignore
310 if(ParticleIsVisibleOnScreen(CurPos: p, CurSize: Size))
311 {
312 Graphics()->TextureSet(Texture: aParticles[m_aParticles[i].m_Spr - FirstParticleOffset]);
313 Graphics()->QuadsBegin();
314
315 Graphics()->QuadsSetRotation(Angle: m_aParticles[i].m_Rot);
316
317 Graphics()->SetColor(
318 r: m_aParticles[i].m_Color.r,
319 g: m_aParticles[i].m_Color.g,
320 b: m_aParticles[i].m_Color.b,
321 a: Alpha);
322
323 IGraphics::CQuadItem QuadItem(p.x, p.y, Size, Size);
324 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
325 Graphics()->QuadsEnd();
326 }
327
328 i = m_aParticles[i].m_NextPart;
329 }
330 Graphics()->WrapNormal();
331 }
332}
333