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