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