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 <base/math.h>
4#include <engine/demo.h>
5#include <engine/graphics.h>
6
7#include "particles.h"
8#include <game/client/render.h>
9#include <game/generated/client_data.h>
10
11#include <game/client/gameclient.h>
12
13CParticles::CParticles()
14{
15 OnReset();
16 m_RenderTrail.m_pParts = this;
17 m_RenderExplosions.m_pParts = this;
18 m_RenderExtra.m_pParts = this;
19 m_RenderGeneral.m_pParts = this;
20}
21
22void CParticles::OnReset()
23{
24 // reset particles
25 for(int i = 0; i < MAX_PARTICLES; i++)
26 {
27 m_aParticles[i].m_PrevPart = i - 1;
28 m_aParticles[i].m_NextPart = i + 1;
29 }
30
31 m_aParticles[0].m_PrevPart = 0;
32 m_aParticles[MAX_PARTICLES - 1].m_NextPart = -1;
33 m_FirstFree = 0;
34
35 for(int &FirstPart : m_aFirstPart)
36 FirstPart = -1;
37}
38
39void CParticles::Add(int Group, CParticle *pPart, float TimePassed)
40{
41 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
42 {
43 const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
44 if(pInfo->m_Paused)
45 return;
46 }
47 else
48 {
49 if(m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)
50 return;
51 }
52
53 if(m_FirstFree == -1)
54 return;
55
56 // remove from the free list
57 int Id = m_FirstFree;
58 m_FirstFree = m_aParticles[Id].m_NextPart;
59 if(m_FirstFree != -1)
60 m_aParticles[m_FirstFree].m_PrevPart = -1;
61
62 // copy data
63 m_aParticles[Id] = *pPart;
64
65 // insert to the group list
66 m_aParticles[Id].m_PrevPart = -1;
67 m_aParticles[Id].m_NextPart = m_aFirstPart[Group];
68 if(m_aFirstPart[Group] != -1)
69 m_aParticles[m_aFirstPart[Group]].m_PrevPart = Id;
70 m_aFirstPart[Group] = Id;
71
72 // set some parameters
73 m_aParticles[Id].m_Life = TimePassed;
74}
75
76void CParticles::Update(float TimePassed)
77{
78 if(TimePassed <= 0.0f)
79 return;
80
81 static float FrictionFraction = 0;
82 FrictionFraction += TimePassed;
83
84 if(FrictionFraction > 2.0f) // safety measure
85 FrictionFraction = 0;
86
87 int FrictionCount = 0;
88 while(FrictionFraction > 0.05f)
89 {
90 FrictionCount++;
91 FrictionFraction -= 0.05f;
92 }
93
94 for(int &FirstPart : m_aFirstPart)
95 {
96 int i = FirstPart;
97 while(i != -1)
98 {
99 int Next = m_aParticles[i].m_NextPart;
100 //m_aParticles[i].vel += flow_get(m_aParticles[i].pos)*time_passed * m_aParticles[i].flow_affected;
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), NULL);
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 static int64_t LastTime = 0;
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 - LastTime) / (double)time_freq()) * pInfo->m_Speed);
160 }
161 else
162 {
163 if(m_pClient->m_Snap.m_pGameInfoObj && !(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED))
164 Update(TimePassed: (float)((t - LastTime) / (double)time_freq()));
165 }
166
167 LastTime = 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 RenderTools()->QuadContainerAddSprite(QuadContainerIndex: m_ParticleQuadContainerIndex, Size: 1.f);
181 }
182 Graphics()->QuadContainerUpload(ContainerIndex: m_ParticleQuadContainerIndex);
183
184 m_ExtraParticleQuadContainerIndex = Graphics()->CreateQuadContainer(AutomaticUpload: false);
185
186 // TODO: Use a loop similar to the one for m_ParticleQuadContainerIndex if you add more additional particles
187 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
188 RenderTools()->QuadContainerAddSprite(QuadContainerIndex: m_ExtraParticleQuadContainerIndex, Size: 1.f);
189 Graphics()->QuadContainerUpload(ContainerIndex: m_ExtraParticleQuadContainerIndex);
190}
191
192bool CParticles::ParticleIsVisibleOnScreen(const vec2 &CurPos, float CurSize)
193{
194 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
195 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
196
197 // for simplicity assume the worst case rotation, that increases the bounding box around the particle by its diagonal
198 const float SqrtOf2 = std::sqrt(x: 2);
199 CurSize = SqrtOf2 * CurSize;
200
201 // always uses the mid of the particle
202 float SizeHalf = CurSize / 2;
203
204 return CurPos.x + SizeHalf >= ScreenX0 && CurPos.x - SizeHalf <= ScreenX1 && CurPos.y + SizeHalf >= ScreenY0 && CurPos.y - SizeHalf <= ScreenY1;
205}
206
207void CParticles::RenderGroup(int Group)
208{
209 IGraphics::CTextureHandle *aParticles = GameClient()->m_ParticlesSkin.m_aSpriteParticles;
210 int FirstParticleOffset = SPRITE_PART_SLICE;
211 int ParticleQuadContainerIndex = m_ParticleQuadContainerIndex;
212 if(Group == GROUP_EXTRA)
213 {
214 aParticles = GameClient()->m_ExtrasSkin.m_aSpriteParticles;
215 FirstParticleOffset = SPRITE_PART_SNOWFLAKE;
216 ParticleQuadContainerIndex = m_ExtraParticleQuadContainerIndex;
217 }
218
219 // don't use the buffer methods here, else the old renderer gets many draw calls
220 if(Graphics()->IsQuadContainerBufferingEnabled())
221 {
222 int i = m_aFirstPart[Group];
223
224 static IGraphics::SRenderSpriteInfo s_aParticleRenderInfo[MAX_PARTICLES];
225
226 int CurParticleRenderCount = 0;
227
228 // batching makes sense for stuff like ninja particles
229 ColorRGBA LastColor;
230 int LastQuadOffset = 0;
231
232 if(i != -1)
233 {
234 float Alpha = m_aParticles[i].m_Color.a;
235 if(m_aParticles[i].m_UseAlphaFading)
236 {
237 float a = m_aParticles[i].m_Life / m_aParticles[i].m_LifeSpan;
238 Alpha = mix(a: m_aParticles[i].m_StartAlpha, b: m_aParticles[i].m_EndAlpha, amount: a);
239 }
240 LastColor.r = m_aParticles[i].m_Color.r;
241 LastColor.g = m_aParticles[i].m_Color.g;
242 LastColor.b = m_aParticles[i].m_Color.b;
243 LastColor.a = Alpha;
244
245 Graphics()->SetColor(
246 r: m_aParticles[i].m_Color.r,
247 g: m_aParticles[i].m_Color.g,
248 b: m_aParticles[i].m_Color.b,
249 a: Alpha);
250
251 LastQuadOffset = m_aParticles[i].m_Spr;
252 }
253
254 while(i != -1)
255 {
256 int QuadOffset = m_aParticles[i].m_Spr;
257 float a = m_aParticles[i].m_Life / m_aParticles[i].m_LifeSpan;
258 vec2 p = m_aParticles[i].m_Pos;
259 float Size = mix(a: m_aParticles[i].m_StartSize, b: m_aParticles[i].m_EndSize, amount: a);
260 float Alpha = m_aParticles[i].m_Color.a;
261 if(m_aParticles[i].m_UseAlphaFading)
262 {
263 Alpha = mix(a: m_aParticles[i].m_StartAlpha, b: m_aParticles[i].m_EndAlpha, amount: a);
264 }
265
266 // the current position, respecting the size, is inside the viewport, render it, else ignore
267 if(ParticleIsVisibleOnScreen(CurPos: p, CurSize: Size))
268 {
269 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)
270 {
271 Graphics()->TextureSet(Texture: aParticles[LastQuadOffset - FirstParticleOffset]);
272 Graphics()->RenderQuadContainerAsSpriteMultiple(ContainerIndex: ParticleQuadContainerIndex, QuadOffset: LastQuadOffset - FirstParticleOffset, DrawCount: CurParticleRenderCount, pRenderInfo: s_aParticleRenderInfo);
273 CurParticleRenderCount = 0;
274 LastQuadOffset = QuadOffset;
275
276 Graphics()->SetColor(
277 r: m_aParticles[i].m_Color.r,
278 g: m_aParticles[i].m_Color.g,
279 b: m_aParticles[i].m_Color.b,
280 a: Alpha);
281
282 LastColor.r = m_aParticles[i].m_Color.r;
283 LastColor.g = m_aParticles[i].m_Color.g;
284 LastColor.b = m_aParticles[i].m_Color.b;
285 LastColor.a = Alpha;
286 }
287
288 s_aParticleRenderInfo[CurParticleRenderCount].m_Pos[0] = p.x;
289 s_aParticleRenderInfo[CurParticleRenderCount].m_Pos[1] = p.y;
290 s_aParticleRenderInfo[CurParticleRenderCount].m_Scale = Size;
291 s_aParticleRenderInfo[CurParticleRenderCount].m_Rotation = m_aParticles[i].m_Rot;
292
293 ++CurParticleRenderCount;
294 }
295
296 i = m_aParticles[i].m_NextPart;
297 }
298
299 Graphics()->TextureSet(Texture: aParticles[LastQuadOffset - FirstParticleOffset]);
300 Graphics()->RenderQuadContainerAsSpriteMultiple(ContainerIndex: ParticleQuadContainerIndex, QuadOffset: LastQuadOffset - FirstParticleOffset, DrawCount: CurParticleRenderCount, pRenderInfo: s_aParticleRenderInfo);
301 }
302 else
303 {
304 int i = m_aFirstPart[Group];
305
306 Graphics()->BlendNormal();
307 Graphics()->WrapClamp();
308
309 while(i != -1)
310 {
311 float a = m_aParticles[i].m_Life / m_aParticles[i].m_LifeSpan;
312 vec2 p = m_aParticles[i].m_Pos;
313 float Size = mix(a: m_aParticles[i].m_StartSize, b: m_aParticles[i].m_EndSize, amount: a);
314 float Alpha = m_aParticles[i].m_Color.a;
315 if(m_aParticles[i].m_UseAlphaFading)
316 {
317 Alpha = mix(a: m_aParticles[i].m_StartAlpha, b: m_aParticles[i].m_EndAlpha, amount: a);
318 }
319
320 // the current position, respecting the size, is inside the viewport, render it, else ignore
321 if(ParticleIsVisibleOnScreen(CurPos: p, CurSize: Size))
322 {
323 Graphics()->TextureSet(Texture: aParticles[m_aParticles[i].m_Spr - FirstParticleOffset]);
324 Graphics()->QuadsBegin();
325
326 Graphics()->QuadsSetRotation(Angle: m_aParticles[i].m_Rot);
327
328 Graphics()->SetColor(
329 r: m_aParticles[i].m_Color.r,
330 g: m_aParticles[i].m_Color.g,
331 b: m_aParticles[i].m_Color.b,
332 a: Alpha);
333
334 IGraphics::CQuadItem QuadItem(p.x, p.y, Size, Size);
335 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
336 Graphics()->QuadsEnd();
337 }
338
339 i = m_aParticles[i].m_NextPart;
340 }
341 Graphics()->WrapNormal();
342 Graphics()->BlendNormal();
343 }
344}
345