1
# ----------------------------------------------------------------------------
3
# Copyright (c) 2008 Daniel Moisset, Ricardo Quesada, Rayentray Tappa, Lucio Torre
6
# Redistribution and use in source and binary forms, with or without
7
# modification, are permitted provided that the following conditions are met:
9
# * Redistributions of source code must retain the above copyright
10
# notice, this list of conditions and the following disclaimer.
11
# * Redistributions in binary form must reproduce the above copyright
12
# notice, this list of conditions and the following disclaimer in
13
# the documentation and/or other materials provided with the
15
# * Neither the name of cocos2d nor the names of its
16
# contributors may be used to endorse or promote products
17
# derived from this software without specific prior written
20
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31
# POSSIBILITY OF SUCH DAMAGE.
32
# ----------------------------------------------------------------------------
33
'''Particle system engine'''
37
from pyglet.gl import *
43
from cocosnode import CocosNode
44
from euclid import Point2
46
rand = lambda: random.random() * 2 - 1
48
# PointerToNumpy by Gary Herron
49
# from pyglet's user list
50
def PointerToNumpy(a, ptype=ctypes.c_float):
51
a = numpy.ascontiguousarray(a) # Probably a NO-OP, but perhaps not
52
return a.ctypes.data_as(ctypes.POINTER(ptype)) # Ugly and undocumented!
54
class Color( object ):
55
def __init__( self, r,g,b,a ):
62
return self.r, self.g, self.b, self.a
65
class ParticleSystem( CocosNode ):
68
POSITION_FREE, POSITION_GROUPED = range(2)
70
#: is the particle system active ?
73
#: duration in seconds of the system. -1 is infinity
76
#: time elapsed since the start of the system (in seconds)
79
#: Gravity of the particles
80
gravity = Point2(0.0, 0.0)
82
#: position is from "superclass" CocosNode
84
pos_var = Point2(0.0, 0.0)
86
#: The angle (direction) of the particles measured in degrees
88
#: Angle variance measured in degrees;
91
#: The speed the particles will have.
96
#: Tangential acceleration
97
tangential_accel = 0.0
98
#: Tangential acceleration variance
99
tangential_accel_var = 0.0
101
#: Radial acceleration
103
#: Radial acceleration variance
104
radial_accel_var = 0.0
106
#: Size of the particles
111
#: How many seconds will the particle live
116
#: Start color of the particles
117
start_color = Color(0.0,0.0,0.0,0.0)
118
#: Start color variance
119
start_color_var = Color(0.0,0.0,0.0,0.0)
120
#: End color of the particles
121
end_color = Color(0.0,0.0,0.0,0.0)
122
#: End color variance
123
end_color_var = Color(0.0,0.0,0.0,0.0)
128
#:texture of the particles
129
texture = pyglet.resource.image('fire.png').texture
132
blend_additive = False
135
color_modulate = True
138
position_type = POSITION_GROUPED
141
super(ParticleSystem,self).__init__()
145
self.particle_pos = numpy.zeros( (self.total_particles, 2), numpy.float32 )
147
self.particle_dir = numpy.zeros( (self.total_particles, 2), numpy.float32 )
149
self.particle_rad = numpy.zeros( (self.total_particles, 1), numpy.float32 )
151
self.particle_tan = numpy.zeros( (self.total_particles, 1), numpy.float32 )
153
self.particle_grav = numpy.zeros( (self.total_particles, 2), numpy.float32 )
155
self.particle_color = numpy.zeros( (self.total_particles, 4), numpy.float32 )
157
self.particle_delta_color = numpy.zeros( (self.total_particles, 4), numpy.float32 )
159
self.particle_life = numpy.zeros( (self.total_particles, 1), numpy.float32 )
160
self.particle_life.fill(-1.0)
162
self.particle_size = numpy.zeros( (self.total_particles, 1), numpy.float32 )
164
self.start_pos = numpy.zeros( (self.total_particles, 2), numpy.float32 )
166
#: How many particles can be emitted per second
167
self.emit_counter = 0
169
#: Count of particles
170
self.particle_count = 0
172
#: auto remove when particle finishes
173
self.auto_remove_on_finish = False
175
self.schedule( self.step )
177
def on_enter( self ):
178
super( ParticleSystem, self).on_enter()
185
glPointSize( self.size )
187
glEnable(GL_TEXTURE_2D)
188
glBindTexture(GL_TEXTURE_2D, self.texture.id )
190
glEnable(GL_POINT_SPRITE)
191
glTexEnvi( GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE )
194
glEnableClientState(GL_VERTEX_ARRAY)
195
vertex_ptr = PointerToNumpy( self.particle_pos )
196
glVertexPointer(2,GL_FLOAT,0,vertex_ptr);
198
glEnableClientState(GL_COLOR_ARRAY)
199
color_ptr = PointerToNumpy( self.particle_color)
200
glColorPointer(4,GL_FLOAT,0,color_ptr);
202
glPushAttrib(GL_COLOR_BUFFER_BIT)
204
if self.blend_additive:
205
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
207
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
210
# glTexEnviv( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, mode )
212
# if self.color_modulate:
213
# glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE )
215
# glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE )
218
glDrawArrays(GL_POINTS, 0, self.total_particles);
224
# glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, mode)
227
glDisableClientState(GL_COLOR_ARRAY);
228
glDisableClientState(GL_VERTEX_ARRAY);
229
glDisable(GL_POINT_SPRITE);
230
glDisable(GL_TEXTURE_2D);
235
def step( self, delta ):
237
# update particle count
238
self.particle_count = numpy.sum( self.particle_life >= 0 )
241
rate = 1.0 / self.emission_rate
242
self.emit_counter += delta
244
# if random.random() < 0.01:
247
while self.particle_count < self.total_particles and self.emit_counter > rate:
249
self.emit_counter -= rate
251
self.elapsed += delta
253
if self.duration != -1 and self.duration < self.elapsed:
256
self.update_particles( delta )
258
if self.particle_count == 0 and self.auto_remove_on_finish == True:
259
self.unschedule( self.step )
260
self.parent.remove( self )
262
def add_particle( self ):
264
self.particle_count += 1
266
def stop_system( self ):
268
self.elapsed= self.duration
269
self.emit_counter = 0
271
def reset_system( self ):
272
self.elapsed= self.duration
273
self.emit_counter = 0
275
def update_particles( self, delta ):
276
# radial: posx + posy
277
norm = numpy.sqrt( self.particle_pos[:,0] ** 2 + self.particle_pos[:,1] ** 2 )
278
# XXX prevent div by 0
279
norm = numpy.select( [norm==0], [0.0000001], default=norm )
280
posx = self.particle_pos[:,0] / norm
281
posy = self.particle_pos[:,1] / norm
283
radial = numpy.array( [posx, posy] )
284
tangential = numpy.array( [-posy, posx] )
287
radial = numpy.swapaxes(radial,0,1)
288
radial *= self.particle_rad
289
tangential = numpy.swapaxes(tangential,0,1)
290
tangential *= self.particle_tan
292
self.particle_dir += (tangential + radial + self.particle_grav) * delta
294
# update pos with updated dir
295
self.particle_pos += self.particle_dir * delta
298
self.particle_life -= delta
301
# position: free or grouped
302
if self.position_type == self.POSITION_FREE:
303
tuple = numpy.array( [self.x, self.y] )
304
tmp = tuple - self.start_pos
305
self.particle_pos -= tmp
309
self.particle_color += self.particle_delta_color * delta
311
# if life < 0, set alpha in 0
312
self.particle_color[:,3] = numpy.select( [self.particle_life[:,0] < 0], [0], default=self.particle_color[:,3] )
314
# print self.particles[0]
315
# print self.pas[0,0:4]
317
def init_particle( self ):
319
# p=self.particles[idx]
321
a = self.particle_life < 0
329
raise Exception("No empty particle")
332
self.particle_pos[idx][0] = self.pos_var.x * rand()
333
self.particle_pos[idx][1] = self.pos_var.y * rand()
336
self.start_pos[idx][0] = self.x
337
self.start_pos[idx][1] = self.y
339
a = math.radians( self.angle + self.angle_var * rand() )
340
v = Point2( math.cos( a ), math.sin( a ) )
341
s = self.speed + self.speed_var * rand()
346
self.particle_dir[idx][0] = dir.x
347
self.particle_dir[idx][1] = dir.y
350
self.particle_rad[idx] = self.radial_accel + self.radial_accel_var * rand()
354
self.particle_tan[idx] = self.tangential_accel + self.tangential_accel_var * rand()
357
life = self.particle_life[idx] = self.life + self.life_var * rand()
361
sr = self.start_color.r + self.start_color_var.r * rand()
362
sg = self.start_color.g + self.start_color_var.g * rand()
363
sb = self.start_color.b + self.start_color_var.b * rand()
364
sa = self.start_color.a + self.start_color_var.a * rand()
366
self.particle_color[idx][0] = sr
367
self.particle_color[idx][1] = sg
368
self.particle_color[idx][2] = sb
369
self.particle_color[idx][3] = sa
372
er = self.end_color.r + self.end_color_var.r * rand()
373
eg = self.end_color.g + self.end_color_var.g * rand()
374
eb = self.end_color.b + self.end_color_var.b * rand()
375
ea = self.end_color.a + self.end_color_var.a * rand()
377
delta_color_r = (er - sr) / life
378
delta_color_g = (eg - sg) / life
379
delta_color_b = (eb - sb) / life
380
delta_color_a = (ea - sa) / life
382
self.particle_delta_color[idx][0] = delta_color_r
383
self.particle_delta_color[idx][1] = delta_color_g
384
self.particle_delta_color[idx][2] = delta_color_b
385
self.particle_delta_color[idx][3] = delta_color_a
388
self.particle_size[idx] = self.size + self.size_var * rand()
391
self.particle_grav[idx][0] = self.gravity.x
392
self.particle_grav[idx][1] = self.gravity.y