Source code for pi3d.util.PexParticles

from __future__ import absolute_import, division, print_function, unicode_literals

from xml.dom.minidom import parse
import numpy as np

from pi3d.shape.Points import Points
from pi3d.Shader import Shader
from pi3d.Texture import Texture
import time

[docs]class PexParticles(Points): def __init__(self, pex_file, emission_rate=10, scale=1.0, rot_rate=None, rot_var=0.0, new_batch=0.1, hardness=2.0, **kwargs): ''' has to be supplied with a pex xml type file to parse. The results are loaded into new attributes of the instance of this class with identifiers matching the Elements of the pex file. There is zero checking for the correct file format. pex_file: file name. if "circle" then lite option doesn't use texture lookup and used mat_pointsprite shader emission_rate: new particles per second scale: scale the point size and location rot_rate: UV mapping rotates rot_var: variance in rotation rate new_batch: proportion of emission_rate to batch (for efficiency) hardness: for lite version The following attributes are created from the pex file and can be subsequently altered. i.e. self.sourcePosition['x'] += 2.0 self.texture={name:'particle.png'} self.sourcePosition={x:160.00,y:369.01} self.sourcePositionVariance={x:60.00,y:0.00} self.speed=138.16 self.speedVariance=0.00 self.particleLifeSpan=0.7000 self.particleLifespanVariance=0.0000 self.angle=224.38 self.angleVariance=360.00 self.gravity={x:0.00,y:-1400.00} self.radialAcceleration=0.00 self.tangentialAcceleration=0.00 self.radialAccelVariance=0.00 self.tangentialAccelVariance=-0.00 self.startColor={red:0.15,green:0.06,blue:1.00,alpha:1.00} self.startColorVariance={red:0.00,green:0.00,blue:0.00,alpha:0.00} self.finishColor={red:0.00,green:0.14,blue:0.23,alpha:0.00} self.finishColorVariance={red:0.00,green:0.00,blue:0.00,alpha:0.00} self.maxParticles=300 self.startParticleSize=43.79 self.startParticleSizeVariance=0.00 self.finishParticleSize=138.11 self.FinishParticleSizeVariance=0.00 self.duration=-1.00 self.emitterType=0 self.maxRadius=100.00 self.maxRadiusVariance=0.00 self.minRadius=0.00 self.rotatePerSecond=0.00 self.rotatePerSecondVariance=0.00 self.blendFuncSource=770 self.blendFuncDestination=772 self.rotationStart=0.00 self.rotationStartVariance=0.00 self.rotationEnd=0.00 self.rotationEndVariance=0.00 ''' # first parse the pex file, json would have been nicer than xml! _config = parse(pex_file).childNodes[0].childNodes for c in _config: if c.localName is not None: key = c.localName val = {} for v in c.attributes.items(): try: v_tp = int(v[1]) # try int first except ValueError: try: v_tp = float(v[1]) # if not try float except ValueError: v_tp = v[1] # otherwise leave as string if v[0] == 'value': # single value 'value' don't use dictionary val = v_tp break else: val[v[0]] = v_tp # not just a value self.__setattr__(key, val) self._emission_rate = emission_rate # particles per second self._last_emission_time = None self._last_time = None self._new_batch = emission_rate * new_batch # to clump new particles self.scale = scale self.rot_rate = rot_rate self.rot_var = rot_var # make a flag to avoid this expensive operation if no accelerators self.any_acceleration = (self.gravity['x'] != 0.0 or self.gravity['y'] != 0.0 or self.radialAcceleration != 0.0 or self.tangentialAcceleration != 0.0) self.any_colorchange = any(self.startColor[i] != self.finishColor[i] for i in ('red','green','blue','alpha')) ''' Buffer.array_buffer holds [0] vertices[0] x position of centre of point relative to centre of screen in pixels [1] vertices[1] y position [2] vertices[2] z depth but fract(z) is used as a multiplier for point size [3] normals[0] rotation in radians [4] normals[1] red and green values to multiply with the texture [5] normals[2] blue and alph values to multiply with the texture. The values are packed into the whole number and fractional parts of the float i.e. where R and G are between 0.0 and 0.999 normals[:,2] = floor(999 * R) + G [6] tex_coords[0] distance of left side of sprite square from left side of texture in uv scale 0.0 to 1.0 [7] tex_coords[1] distance of top of sprite square from top of texture for lite version using the mat_pointsprite shader [3:7] hold RGBA in simple float form make additional numpy array to hold the particle info arr[0] x velocity arr[1] y velocity arr[2] lifespan arr[3] lifespan remaining arr[4:8] rgba target values arr[8:12] rgba difference arr[12] size delta (finish size - start size) / full_lifespan arr[13] radial acceleration arr[14] tangential acceleration ''' self.arr = np.zeros((self.maxParticles, 15), dtype='float32') self.point_size = max(self.startParticleSize + self.startParticleSizeVariance, self.finishParticleSize + self.FinishParticleSizeVariance) #NB capital F! super(PexParticles, self).__init__(vertices=np.zeros((self.maxParticles, 3), dtype='float32'), normals=np.zeros((self.maxParticles, 3), dtype='float32'), tex_coords=np.zeros((self.maxParticles, 2), dtype='float32'), point_size=self.point_size * self.scale, **kwargs) # pass to Points.__init__() if self.texture['name'] == 'circle': # TODO alternative geometries self.lite = True shader = Shader.create('mat_pointsprite') self.set_shader(shader) self.buf[0].unib[0] = hardness else: self.lite = False shader = Shader.create('uv_pointsprite') try: tex = Texture(self.texture['name']) # obvious first! except: import os tex = Texture(os.path.join( os.path.split(pex_file)[0], self.texture['name'])) self.set_draw_details(shader, [tex]) self.unif[48] = 1.0 # sprite uses whole image
[docs] def update(self): b = self.buf[0].array_buffer # shortcut to change Buffer.array_buffer in place # work out how many new particles to create tm = time.time() # first time round dt is 0.0 else time since last update dt = tm - self._last_time if self._last_time is not None else 0.0 self._last_time = tm if self._last_emission_time is None: n_new = self._emission_rate else: n_new = int(self._emission_rate * (tm - self._last_emission_time)) if n_new > self._new_batch: self._last_emission_time = tm self.arr[:-n_new] = self.arr[n_new:] # 'age' by moving along b[:-n_new] = b[n_new:] # generate ALL the varying values in one step new_vals = ( [self.sourcePosition['x'], self.sourcePosition['y'], # 0,1, self.speed, self.particleLifeSpan, self.angle, # 2,3,4, self.radialAcceleration, self.tangentialAcceleration, # 5,6 self.startColor['red'], self.startColor['green'], # 7,8 self.startColor['blue'], self.startColor['alpha'], # 9,10 self.finishColor['red'], self.finishColor['green'], # 11,12 self.finishColor['blue'], self.finishColor['alpha'], # 13,14 self.startParticleSize, self.finishParticleSize] # 15,16 + (np.random.random((n_new, 17)) * 2.0 - 1.0) * [self.sourcePositionVariance['x'], self.sourcePositionVariance['y'], self.speedVariance, self.particleLifespanVariance, self.angleVariance, self.radialAccelVariance, self.tangentialAccelVariance, self.startColorVariance['red'], self.startColorVariance['green'], self.startColorVariance['blue'], self.startColorVariance['alpha'], self.finishColorVariance['red'], self.finishColorVariance['green'], self.finishColorVariance['blue'], self.finishColorVariance['alpha'], self.startParticleSizeVariance, self.FinishParticleSizeVariance]) # NB capital F! # x, y locations b[-n_new:,0:2] = new_vals[:,0:2] * self.scale # velocities self.arr[-n_new:,0] = new_vals[:,2] * np.cos(np.radians(new_vals[:,4])) * self.scale self.arr[-n_new:,1] = new_vals[:,2] * np.sin(np.radians(new_vals[:,4])) * self.scale # lifeSpan self.arr[-n_new:,2] = new_vals[:,3] self.arr[-n_new:,3] = new_vals[:,3] # rgba target self.arr[-n_new:,4:8] = np.minimum(np.maximum(new_vals[:,11:15], 0.0), 0.999) # rgba difference self.arr[-n_new:,8:12] = (np.minimum(np.maximum(new_vals[:,11:15], 0.0), 0.999) - np.minimum(np.maximum(new_vals[:,7:11], 0.0), 0.999)) if self.lite: b[-n_new:,3:7] = new_vals[:,7:11] else: b[-n_new:,4:6] = np.floor(999.0 * self.arr[-n_new,4:7:2]) + 0.99 * self.arr[-n_new,5:8:2] # size b[-n_new:,2] = new_vals[:,15] * 0.999 / self.point_size # must not approx to 1.0 at medium precision # and reset the z distance part (unif[2] hold Shape z value) b[:,2] = np.arange(self.maxParticles + self.unif[2], self.unif[2], -1.0) + b[:,2] % 1.0 # size delta self.arr[-n_new:,12] = 0.95 * (new_vals[:,16] - new_vals[:,15]) / self.point_size / self.arr[-n_new:,2] # radial and tangential acc self.arr[-n_new:,13:15] = new_vals[:,5:7] alph_i = 6 if self.lite else 5 # see next line b[self.arr[:,3] <= 0.0, alph_i] = 0.0 # make alpha 0 for dead particles ix = np.where(self.arr[:,3] > 0.0)[0] # index of live particles radial_v = b[ix,0:2] - [self.sourcePosition['x'], self.sourcePosition['y']] # vector from emitter radial_v /= np.linalg.norm(radial_v, axis=1)[:, np.newaxis] # normalise b[ix,0:2] += self.arr[ix,0:2] * dt # location change if self.any_acceleration: self.arr[ix,0:2] += ([self.gravity['x'], self.gravity['y']] + # velocity change radial_v * self.arr[ix,13].reshape(-1,1) + # radial and tang acc radial_v[:,::-1] * [-1.0, 1.0] * self.arr[ix,14].reshape(-1,1)) * dt * self.scale if self.any_colorchange: if self.lite: b[ix,3:7] = self.arr[ix,4:8] - (self.arr[ix,8:12] * (self.arr[ix,3] / self.arr[ix,2]).reshape(-1,1))# colour change else: b[ix,4:6] = np.floor(999.0 * (self.arr[ix,4:7:2] - self.arr[ix,8:11:2] * (self.arr[ix,3] / self.arr[ix,2]).reshape(-1,1)))# rb change b[ix,4:6] += 0.99 * (self.arr[ix,5:8:2] - self.arr[ix,9:12:2] * (self.arr[ix,3] / self.arr[ix,2]).reshape(-1,1))# ga change b[ix,2] += self.arr[ix,12] * dt # size change self.arr[ix,3] -= dt # lifespan remaining if self.rot_rate is not None: # rotate if this is set b[ix,3] += (self.rot_rate + self.rot_var * (np.random.random(ix.shape) * 2.0 - 1.0)) * dt self.re_init() # re-init the buffers