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