├── .gitattributes ├── README.md ├── aframe-mesh-particles-component.js ├── assets ├── banana │ ├── CHAHIN_BANANA.bin │ ├── CHAHIN_BANANA.gltf │ ├── CHAHIN_BANANA_TEXTURE.jpg │ └── Poly by Goolgle.author ├── blob.png └── screenshot.jpg ├── index.html └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aframe-mesh-particles-component 2 | 3 | The **mesh-particles** component uses shader based geometry instancing to create a set of particles from an entities' mesh geometry. The particles start spawning once the component is created (and the mesh is available), and will continue until the **duration** expires. Properties can be used to define the position, velocity, acceleration, color, scale and rotation of the particles. 4 | 5 | [See a demo](https://harlyq.github.io/aframe-mesh-particles-component/) (banana asset from Poly by Google) 6 | 7 | ![Screenshot](assets/screenshot.jpg) 8 | 9 | ## Examples 10 | ```html 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ``` 23 | 24 | ## Values 25 | Some of the properties are listed as type *range*, which is a minimum and maximum value separated by `..` (the system will chose a value within that range for each particle) or just a single value. 26 | 27 | Some properties are listed as type *range array*. This provides different values over the life-time of the particle. The first value is for when the particle is created, linearly interpolating over values, until the last value is reached at the end of the particle's life. By default there are a maximum of 5 elements for each over-time array, but this can be changed by setting the **overTimeSlots** parameter. 28 | 29 | For example: 30 | 31 | `lifeTime: 1` - all particles have a life time of 1 (*number range*) 32 | 33 | `lifeTime: 2..4` - all particles have a life time between 2 and 4 inclusive (*number range*) 34 | 35 | `velocity: 1 1 .1 .. 2 3 5` - velocity value between 1 and 2 for x, 1 and 3 for y, .1 and 5 for z (*vec3 range*) 36 | 37 | `scale: 1..2,3,6,.5 .. 1,9` - there are 5 values so each value represents 0%, 25%, 50%, 75% 100% of the particles life time. at 0% scale is between 1 and 2, then blends to 3 at 25%, then up to 6 at 50%, a value between .5 and 1 at 75% then back up to 9 at 100% (*number range[]*) 38 | 39 | `rotation: 0 0 0,0 360 0` - there are 2 values, each particle starts at 0 0 0, and linearly interpolates counterclockwise to 0 360 0 (rotation about the Y axis) over the lifetime of the particle (*vec3 range[]*) 40 | 41 | ## Properties 42 | The list of properties in alphabetical order: 43 | 44 | **acceleration** - range for acceleration of each particle in local coordinates (*vec3 range*) default 0 0 0 45 | 46 | **angularVelocity** - range for velocity in degrees per second counterclockwise around the local origin about the X, Y and Z axis (*vec3 range*) default 0 0 0 47 | 48 | **color** - over-time ranges for the particle color, can use names e.g. `blue` or `color`, or hex strings e.g. `#ff0` or `#e7f890` (*color range array*) default white 49 | 50 | **direction** - the direction to play the particle effect. if playing backward the particle will start at the end of its maximum age and then get younger (*forward, backward*) default forward 51 | 52 | **duration** - no new particles will be generated after this duration (seconds). if negative, particles are generated forever. changing the duration will restart the particle system (number) defualt -1 53 | 54 | **editorObject** - if true, provide a bounding box which is viewable in the editor (does not work for world relative particles) (*boolean*) default false 55 | 56 | **enableInEditor** - if true, the particle system will run while the AFrame Inspector is active (*boolean*) default false 57 | 58 | **entity** - entity which contains the geometry for the particle. if no entity is specified use this component's entity (*selector*) default null 59 | 60 | **frustumCulled** - if false, then always render the particle system, even if outside of the camera view. This is useful for world relative particle systems that move around a lot, because the bounds are only associated with the current position, and not any prior positions (*boolean*) default true 61 | 62 | **geoName** - object3D name used for the particle geometry (*string*) default mesh 63 | 64 | **geoNumber** - each particle is based upon a single mesh, however there may be multiple meshes defined in an object3D. this number determines which mesh to use. 1 for the 1st mesh, 2 for the second etc (*number*) default 1 65 | 66 | **lifeTime** - range for maximum age of each particle (*number range*) default 1 67 | 68 | **position** - range for offseting the initial particle position in local coordinates (*vec3 range*) default 0 0 0 69 | 70 | **opacity** - over-time ranges for the particle opacity. `0` is transparent, `1` is opaque (*number range array*) default 1 71 | 72 | **overTimeSlots** - maximum number of slots for over-time attributes. if an attribute has more than **overTimeSlots** slots, then the remainder are ignored (cannot be changed at run-time) (*int*) default 5 73 | 74 | **radialAcceleration** - range for an acceleration from the local origin (*number range*) default 0 75 | 76 | **radialPosition** - range for offseting the start position from the local origin (*number range*) default 0 77 | 78 | **radialType** - shape for radial parameters, either a circle in XY or a sphere (*circle, sphere*) default circle 79 | 80 | **radialVelocity** - range for a radial speed from the local origin (*number range*) default 0 81 | 82 | **relative** - if local, all particles move relative to the entity. if world, new particles are spawned at the current entity position, but spawned particles are not affected by the entities' movement (cannot be changed at run-time) (*world, local*) default local 83 | 84 | **rotation** - over-time ranges for the particle rotation counterclockwise about the X, Y and Z axis. all rotations are from min range to max range, and in degrees (*vec3 range array*) default 0 0 0 85 | 86 | **scale** - over-time ranges for the particle scale (scaled equally in all dimensions) (*number range array*) default 1 87 | 88 | **seed** - initial seed for randomness. if negative, then there is no initial seed (*int*) default -1 89 | 90 | **spawnRate** - number of particles emitted per second (if **spawnType** is `continuous`) (*number*) default 10 91 | 92 | **spawnType** - continuous particles are emitted at the spawn rate, whilst burst particles are all emitted once the spawner is activated, and are re-emitted once all particles expire (*continuous, burst*) default continous 93 | 94 | ## Limitations 95 | 96 | The number of particles to spawn is the **spawnRate** multiplied by the maximum **lifeTime** (this also applies when **spawnType** is `burst`). 97 | 98 | If an entity contains multiple meshes, the **geoNumber** represent which mesh to use for the particles (1 for the first, 2 for the second etc). 99 | 100 | If an **entity** is not specified then the object3D with the **geoName** will be used as a basis for the particles, and the original geometry will be removed. 101 | 102 | If **entity** is empty, and the entity with the **mesh-particles** has **relative** set to `world`, and a **scale** component is on the entity, then **scale** component will be ignored (this does not affect the scale property in the mesh-particles). The recommended setup for scaling world relative particles is to use a separate entity, with visible set to false e.g. 103 | ```html 104 | 105 | 106 | ``` 107 | 108 | Both radial and non-radial values are applied to each particle. So a particle's position will be the sum of the **offset** and **radialOffset**, similarly for velocity and acceleration. 109 | 110 | The object3d name matches the attribute name used to define the component e.g. "mesh-particles" or "mesh-particles__fire". If the particle system is world relative, then the object3d is attached to the sceneEl, and will 111 | be the id name followed by the attribute name e.g. "box_mesh-particles" or "bonfire_mesh-particles__fire". If there is no id for the particles then a unique number will be used e.g. "mesh-particles2", "mesh-particles5". 112 | -------------------------------------------------------------------------------- /aframe-mesh-particles-component.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 harlyq 2 | // License MIT 3 | 4 | (function() { 5 | 6 | const TIME_PARAM = 0 // [0].x 7 | const ID_PARAM = 1 // [0].y 8 | const RADIAL_PARAM = 2 // [0].z 9 | const DURATION_PARAM = 3 // [0].w 10 | const SPAWN_TYPE_PARAM = 4 // [1].x 11 | const SPAWN_DELTA_PARAM = 5 // [1].y 12 | const SEED_PARAM = 6 // [1].z 13 | const PARTICLE_COUNT_PARAM = 7 // [1].w 14 | const MIN_AGE_PARAM = 8 // [2].x 15 | const MAX_AGE_PARAM = 9 // [2].y 16 | const DIRECTION_PARAM = 10 // [2].z 17 | 18 | const RANDOM_REPEAT_COUNT = 131072; // random numbers will start repeating after this number of particles 19 | 20 | const degToRad = THREE.Math.degToRad 21 | 22 | // Bring all sub-array elements into a single array e.g. [[1,2],[[3],4],5] => [1,2,3,4,5] 23 | const flattenDeep = arr1 => arr1.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []) 24 | 25 | // Convert a vector range string into an array of elements. def defines the default elements for each vector 26 | const parseVecRange = (str, def) => { 27 | let parts = str.split("..").map(a => a.trim().split(" ").map(b => { 28 | const num = Number(b) 29 | return isNaN(num) ? undefined : num 30 | })) 31 | if (parts.length === 1) parts[1] = parts[0] // if there is no second part then copy the first part 32 | parts.length = 2 33 | return flattenDeep( parts.map(a => def.map((x,i) => typeof a[i] === "undefined" ? x : a[i])) ) 34 | } 35 | 36 | // parse a ("," separated) list of vector range elements 37 | const parseVecRangeArray = (str, def) => { 38 | return flattenDeep( str.split(",").map(a => parseVecRange(a, def)) ) 39 | } 40 | 41 | // parse a ("," separated) list of color range elements 42 | const parseColorRangeArray = (str) => { 43 | return flattenDeep( str.split(",").map(a => { 44 | let parts = a.split("..") 45 | if (parts.length === 1) parts[1] = parts[0] // if there is no second part then copy the first part 46 | parts.length = 2 47 | return parts.map(b => new THREE.Color(b.trim())) 48 | }) ) 49 | } 50 | 51 | // find the first THREE.Mesh that is this either this object or one of it's descendants 52 | const getNthMesh = (object3D, n, i = 1) => { 53 | if (!object3D) { 54 | return 55 | } else if (object3D instanceof THREE.Mesh && i++ == n) { 56 | return object3D 57 | } 58 | 59 | for (let child of object3D.children) { 60 | let mesh = getNthMesh(child, n, i) 61 | if (mesh) return mesh 62 | } 63 | } 64 | 65 | const toLowerCase = x => x.toLowerCase() 66 | 67 | // console.assert(AFRAME.utils.deepEqual(parseVecRange("", [1,2,3]), [1,2,3,1,2,3])) 68 | // console.assert(AFRAME.utils.deepEqual(parseVecRange("5", [1,2,3]), [5,2,3,5,2,3])) 69 | // console.assert(AFRAME.utils.deepEqual(parseVecRange("5 6", [1,2,3]), [5,6,3,5,6,3])) 70 | // console.assert(AFRAME.utils.deepEqual(parseVecRange("5 6 7 8", [1,2,3]), [5,6,7,5,6,7])) 71 | // console.assert(AFRAME.utils.deepEqual(parseVecRange("8 9..10", [1,2,3]), [8,9,3,10,2,3])) 72 | // console.assert(AFRAME.utils.deepEqual(parseVecRange("..5 6 7", [1,2,3]), [1,2,3,5,6,7])) 73 | // console.assert(AFRAME.utils.deepEqual(parseVecRange("2 3 4..5 6 7", [1,2,3]), [2,3,4,5,6,7])) 74 | // console.assert(AFRAME.utils.deepEqual(parseVecRange("5 6 7..", [1,2,3]), [5,6,7,1,2,3])) 75 | 76 | // console.assert(AFRAME.utils.deepEqual(parseVecRangeArray("5 6 7..,9..10 11 12", [1,2,3]), [5,6,7,1,2,3,9,2,3,10,11,12])) 77 | // console.assert(AFRAME.utils.deepEqual(parseVecRangeArray("1,2,,,3", [10]), [1,1,2,2,10,10,10,10,3,3])) 78 | 79 | // console.assert(AFRAME.utils.deepEqual(parseColorRangeArray("black..red,blue,,#ff0..#00ffaa").map(a => a.getHexString()), ["000000","ff0000","0000ff","0000ff","ffffff","ffffff","ffff00","00ffaa"])) 80 | 81 | AFRAME.registerComponent("mesh-particles", { 82 | schema: { 83 | enableInEditor: { default: false }, 84 | entity: { type: "selector" }, 85 | duration: { default: -1 }, 86 | spawnType: { default: "continuous", oneOf: ["continuous", "burst"], parse: toLowerCase }, 87 | spawnRate: { default: 10 }, 88 | relative: { default: "local", oneOf: ["local", "world"], parse: toLowerCase }, 89 | 90 | lifeTime: { default: "1" }, 91 | position: { default: "0 0 0" }, 92 | velocity: { default: "0 0 0" }, 93 | acceleration: { default: "0 0 0" }, 94 | radialType: { default: "circle", oneOf: ["circle", "sphere"], parse: toLowerCase }, 95 | radialPosition: { default: "0" }, 96 | radialVelocity: { default: "0" }, 97 | radialAcceleration: { default: "0" }, 98 | angularVelocity: { default: "0 0 0" }, 99 | angularAcceleration: { default: "0 0 0" }, 100 | scale: { default: "1" }, 101 | color: { default: "white", parse: toLowerCase }, 102 | rotation: { default: "0 0 0" }, 103 | opacity: { default: "1" }, 104 | 105 | enable: { default: true }, 106 | direction: { default: "forward", oneOf: ["forward", "backward"], parse: toLowerCase }, 107 | seed: { type: "float", default: -1 }, 108 | overTimeSlots: { type: "int", default: 5 }, 109 | frustumCulled: { default: true }, 110 | geoName: { default: "mesh" }, 111 | geoNumber: { type: "int", min: 1, default: 1 }, 112 | }, 113 | multiple: true, 114 | help: "https://github.com/harlyq/aframe-mesh-particles-component", 115 | 116 | init() { 117 | this.pauseTick = this.pauseTick.bind(this) 118 | this.onBeforeCompile = this.onBeforeCompile.bind(this) 119 | 120 | this.count = 0 121 | this.overTimeArrayLength = this.data.overTimeSlots*2 + 1 // each slot represents 2 glsl array elements pluse one element for the length info 122 | this.emitterTime = 0 123 | this.lifeTime = [1,1] 124 | this.useTransparent = false 125 | this.offset = [0,0,0,0,0,0] 126 | this.radialOffset = [0,0] 127 | this.velocity = [0,0,0,0,0,0] 128 | this.radialVelocity = [0,0] 129 | this.acceleration = [0,0,0,0,0,0] 130 | this.radialAcceleration = [0,0] 131 | this.angularVelocity = [0,0,0,0,0,0] 132 | this.angularAcceleration = [0,0,0,0,0,0] 133 | this.colorOverTime = new Float32Array(4*this.overTimeArrayLength).fill(0) // color is xyz and opacity is w 134 | this.rotationScaleOverTime = new Float32Array(4*this.overTimeArrayLength).fill(0) // xyz is rotation, w is scale 135 | this.params = new Float32Array(4*3).fill(0) // see _PARAM constants 136 | this.nextID = 0 137 | this.nextTime = 0 138 | this.relative = this.data.relative // cannot be changed at run-time 139 | this.paused = false 140 | }, 141 | 142 | remove() { 143 | if (this.instancedMesh) { 144 | this.parentEl.removeObject3D(this.instancedMesh.name) 145 | } 146 | }, 147 | 148 | update(oldData) { 149 | const data = this.data 150 | let boundsDirty = false 151 | 152 | if (data.relative !== this.relative) { 153 | console.error("mesh-particles 'relative' cannot be changed at run-time") 154 | } 155 | 156 | if (data.overTimeSlots !== (this.overTimeArrayLength - 1)/2) { 157 | console.error("mesh-particles 'overTimeSlots' cannot be changed at run-time") 158 | } 159 | 160 | this.params[SPAWN_TYPE_PARAM] = data.spawnType === "burst" ? 0 : 1 161 | this.params[RADIAL_PARAM] = data.radialType === "circle" ? 0 : 1 162 | this.params[DIRECTION_PARAM] = data.direction === "forward" ? 0 : 1 163 | 164 | if (data.seed !== oldData.seed) { 165 | this.seed = data.seed 166 | this.params[SEED_PARAM] = data.seed >= 0 ? data.seed : Math.random() 167 | } 168 | 169 | if (this.instancedMesh && data.frustumCulled !== oldData.frustumCulled) { 170 | this.instancedMesh.frustumCulled = data.frustumCulled 171 | } 172 | 173 | if (data.position !== oldData.position || data.radialPosition !== oldData.radialPosition) { 174 | this.offset = parseVecRange(data.position, [0,0,0]) 175 | this.radialOffset = parseVecRange(data.radialPosition, [0]) 176 | boundsDirty = true 177 | } 178 | 179 | if (data.velocity !== oldData.velocity || data.radialVelocity !== oldData.radialVelocity) { 180 | this.velocity = parseVecRange(data.velocity, [0,0,0]) 181 | this.radialVelocity = parseVecRange(data.radialVelocity, [0]) 182 | boundsDirty = true 183 | } 184 | 185 | if (data.acceleration !== oldData.acceleration || data.radialAcceleration !== oldData.radialAcceleration) { 186 | this.acceleration = parseVecRange(data.acceleration, [0,0,0]) 187 | this.radialAcceleration = parseVecRange(data.radialAcceleration, [0]) 188 | boundsDirty = true 189 | } 190 | 191 | if (data.rotation !== oldData.rotation || data.scale !== oldData.scale) { 192 | this.updateRotationScaleOverTime() 193 | } 194 | 195 | if (data.color !== oldData.color || data.opacity !== oldData.opacity) { 196 | this.updateColorOverTime() 197 | } 198 | 199 | if (data.angularVelocity !== oldData.angularVelocity) { 200 | this.angularVelocity = parseVecRange(data.angularVelocity, [0,0,0]).map(degToRad) 201 | } 202 | 203 | if (data.angularAcceleration !== oldData.angularAcceleration) { 204 | this.angularAcceleration = parseVecRange(data.angularAcceleration, [0,0,0]).map(degToRad) 205 | } 206 | 207 | if (data.duration !== oldData.duration) { 208 | this.params[DURATION_PARAM] = data.duration 209 | this.emitterTime = 0 // if the duration is changed then restart the particles 210 | } 211 | 212 | if (data.spawnRate !== oldData.spawnRate || data.lifeTime !== oldData.lifeTime) { 213 | this.lifeTime = parseVecRange(data.lifeTime, [1]) 214 | this.params[SPAWN_DELTA_PARAM] = 1/data.spawnRate 215 | this.count = Math.max(1, Math.ceil(this.lifeTime[1]*data.spawnRate)) 216 | this.params[MIN_AGE_PARAM] = this.lifeTime[0] 217 | this.params[MAX_AGE_PARAM] = this.lifeTime[1] 218 | this.params[PARTICLE_COUNT_PARAM] = this.count 219 | this.updateAttributes() 220 | } 221 | 222 | if (data.enableInEditor !== oldData.enableInEditor) { 223 | this.enablePauseTick(data.enableInEditor) 224 | } 225 | 226 | if (boundsDirty && this.geometry) { 227 | this.updateBounds() 228 | } 229 | }, 230 | 231 | tick(time, deltaTime) { 232 | if (deltaTime > 100) deltaTime = 100 // ignore long pauses 233 | const dt = deltaTime/1000 // dt is in seconds 234 | 235 | // for models it may take some time before the original mesh is available, so keep trying 236 | if (!this.instancedMesh) { 237 | this.waitingForMeshDebug = (this.waitingForMesh || 0) + deltaTime 238 | if (this.waitingFroMeshDebug > 2000) { 239 | this.waitingFroMeshDebug -= 600000 240 | console.error("mesh-particles missing mesh geometry") 241 | } 242 | 243 | this.createMesh() 244 | } 245 | 246 | if (this.shader) { 247 | this.emitterTime += dt 248 | this.params[TIME_PARAM] = this.emitterTime 249 | 250 | this.updateWorldTransform(this.emitterTime) // before we update emitterTime 251 | } 252 | }, 253 | 254 | pause() { 255 | this.paused = true 256 | this.enablePauseTick(this.data.enableInEditor) 257 | }, 258 | 259 | play() { 260 | this.paused = false 261 | this.enablePauseTick(false) 262 | }, 263 | 264 | enablePauseTick(enable) { 265 | if (enable) { 266 | this.pauseRAF = requestAnimationFrame(this.pauseTick) 267 | } else { 268 | cancelAnimationFrame(this.pauseRAF) 269 | } 270 | }, 271 | 272 | pauseTick() { 273 | this.tick(0, 16) // time is not used 274 | this.enablePauseTick(true) 275 | }, 276 | 277 | createMesh() { 278 | const data = this.data 279 | 280 | // if there is no entity property then use the geo from our component 281 | let mesh = getNthMesh(data.entity ? data.entity.getObject3D(data.geoName) : this.el.getObject3D(data.geoName), data.geoNumber) 282 | 283 | if (!mesh || !mesh.geometry || !mesh.material) { 284 | return // mesh doesn't exist or not yet loaded 285 | } 286 | 287 | this.geometry = (new THREE.InstancedBufferGeometry()).copy(mesh.geometry) 288 | 289 | // If sourcing the particle from another entity, then bake that entities' 290 | // scale directly on the geo (i.e. any scale="..." applied to the entity will also be applied 291 | // to the particle) 292 | let entityScale = data.entity ? data.entity.object3D.scale : {x:1, y:1, z:1} 293 | this.geometry.scale(entityScale.x, entityScale.y, entityScale.z) 294 | 295 | this.updateAttributes() 296 | 297 | this.material = mesh.material.clone() 298 | this.wasOriginalMaterialTransparent = this.materialTransparent 299 | this.material.transparent = this.material.transparent || this.useTransparent 300 | 301 | this.material.defines = this.material.defines || {} 302 | this.material.defines.OVER_TIME_ARRAY_LENGTH = this.overTimeArrayLength 303 | this.material.defines.RANDOM_REPEAT_COUNT = RANDOM_REPEAT_COUNT 304 | 305 | // world relative particles use a set of new attributes, so only include the glsl code 306 | // if we are world relative 307 | if (this.relative === "world") { 308 | this.material.defines.WORLD_RELATIVE = true 309 | } else if (this.material.defines) { 310 | delete this.material.defines.WORLD_RELATIVE 311 | } 312 | 313 | this.material.onBeforeCompile = this.onBeforeCompile 314 | 315 | this.instancedMesh = new THREE.Mesh(this.geometry, this.material) 316 | this.instancedMesh.frustumCulled = data.frustumCulled 317 | 318 | if (!data.entity) { 319 | //mesh.visible = false // cannot just set the mesh because there may be multiple object3Ds under this geoname 320 | this.el.removeObject3D(data.geoName) 321 | } 322 | 323 | this.parentEl = this.relative === "world" ? this.el.sceneEl : this.el 324 | if (this.relative === "local") { 325 | this.instancedMesh.name = this.attrName 326 | } else if (this.el.id) { // world relative with id 327 | this.instancedMesh.name = this.el.id + "_" + this.attrName 328 | } else { // world relative, no id 329 | this.parentEl.meshParticleshUniqueID = (this.parentEl.meshParticleshUniqueID || 0) + 1 330 | this.instancedMesh.name = this.attrName + (this.parentEl.meshParticleshUniqueID > 1 ? this.parentEl.meshParticleshUniqueID.toString() : "") 331 | } 332 | // console.log(this.instancedMesh.name) 333 | 334 | this.parentEl.setObject3D(this.instancedMesh.name, this.instancedMesh) 335 | 336 | this.updateBounds() 337 | }, 338 | 339 | updateColorOverTime() { 340 | let color = parseColorRangeArray(this.data.color) 341 | let opacity = parseVecRangeArray(this.data.opacity, [1]) 342 | 343 | const maxSlots = this.data.overTimeSlots 344 | if (color.length > maxSlots*2) color.length = maxSlots*2 345 | if (opacity.length > maxSlots*2) opacity.length = maxSlots*2 346 | 347 | this.colorOverTime.fill(0) 348 | 349 | // first colorOverTime block contains length information 350 | // divide by 2 because each array contains min and max values 351 | this.colorOverTime[0] = color.length/2 // glsl colorOverTime[0].x 352 | this.colorOverTime[1] = opacity.length/2 // glsl colorOverTime[0].y 353 | 354 | // set k to 4 because the first vec4 of colorOverTime is use for the length params 355 | let n = color.length 356 | for (let i = 0, k = 4; i < n; i++, k += 4) { 357 | let col = color[i] 358 | this.colorOverTime[k] = col.r // glsl colorOverTime[1..].x 359 | this.colorOverTime[k+1] = col.g // glsl colorOverTime[1..].y 360 | this.colorOverTime[k+2] = col.b // glsl colorOverTime[1..].z 361 | } 362 | 363 | n = opacity.length 364 | for (let i = 0, k = 4; i < n; i++, k += 4) { 365 | let alpha = opacity[i] 366 | this.colorOverTime[k+3] = alpha // glsl colorOverTime[1..].w 367 | this.useTransparent = this.useTransparent || alpha < 1 368 | } 369 | 370 | if (this.material) { 371 | this.material.transparent = this.wasOriginalMaterialTransparent || this.useTransparent // material.needsUpdate = true??? 372 | } 373 | }, 374 | 375 | updateRotationScaleOverTime() { 376 | const maxSlots = this.data.overTimeSlots 377 | let rotation = parseVecRangeArray(this.data.rotation, [0,0,0]) 378 | let scale = parseVecRangeArray(this.data.scale, [1]) 379 | 380 | 381 | if (rotation.length/3 > maxSlots*2) rotation.length = maxSlots*2*3 // 3 numbers per rotation, 2 rotations per range 382 | if (scale.length > maxSlots*2) scale.length = maxSlots*2 // 2 scales per range 383 | 384 | // first vec4 contains the lengths of the rotation and scale vectors 385 | this.rotationScaleOverTime.fill(0) 386 | this.rotationScaleOverTime[0] = rotation.length/6 387 | this.rotationScaleOverTime[1] = scale.length/2 388 | 389 | // set k to 4 because the first vec4 of rotationScaleOverTime is use for the length params 390 | // update i by 3 becase rotation is 3 numbers per vector, and k by 4 because rotationScaleOverTime is 4 numbers per vector 391 | let n = rotation.length 392 | for (let i = 0, k = 4; i < n; i += 3, k += 4) { 393 | this.rotationScaleOverTime[k] = degToRad(rotation[i]) // glsl rotationScaleOverTime[1..].x 394 | this.rotationScaleOverTime[k+1] = degToRad(rotation[i+1]) // glsl rotationScaleOverTime[1..].y 395 | this.rotationScaleOverTime[k+2] = degToRad(rotation[i+2]) // glsl rotationScaleOverTime[1..].z 396 | } 397 | 398 | n = scale.length 399 | for (let i = 0, k = 4; i < n; i++, k += 4) { 400 | this.rotationScaleOverTime[k+3] = scale[i] // glsl rotationScaleOverTime[1..].w 401 | } 402 | }, 403 | 404 | random() { 405 | if (this.seed >= 0) { 406 | this.seed = (1664525*this.seed + 1013904223) % 0xffffffff 407 | return this.seed/0xffffffff 408 | } else { 409 | return Math.random() 410 | } 411 | }, 412 | 413 | randomNumber(min, max) { 414 | if (min === max) return min 415 | return this.random()*(max - min) + min 416 | }, 417 | 418 | randomDir(out) { 419 | const theta = this.randomNumber(0, 2*Math.PI) 420 | const omega = this.data.radialType === "sphere" ? this.randomNumber(0, 2*Math.PI) : 0 421 | 422 | const rc = Math.cos(theta) 423 | out.x = Math.cos(omega) * rc 424 | out.y = Math.sin(theta) 425 | out.z = Math.sin(omega) * rc 426 | }, 427 | 428 | randomVec3PlusRadial(vec3Range, wRange, dir, out) { 429 | const r = this.randomNumber(wRange[0], wRange[1]) 430 | out.x = this.randomNumber(vec3Range[0], vec3Range[3]) + dir.x*r 431 | out.y = this.randomNumber(vec3Range[1], vec3Range[4]) + dir.y*r 432 | out.z = this.randomNumber(vec3Range[2], vec3Range[5]) + dir.z*r 433 | }, 434 | 435 | randomVec3(vec3Range, out) { 436 | out.x = this.randomNumber(vec3Range[0], vec3Range[3]) 437 | out.y = this.randomNumber(vec3Range[1], vec3Range[4]) 438 | out.z = this.randomNumber(vec3Range[2], vec3Range[5]) 439 | }, 440 | 441 | updateAttributes() { 442 | if (this.geometry) { 443 | const n = this.count 444 | this.geometry.maxInstancedCount = n 445 | 446 | let instanceIDs = new Float32Array(n) 447 | for (let i = 0; i < n; i++) { 448 | instanceIDs[i] = i 449 | } 450 | 451 | this.geometry.addAttribute("instanceID", new THREE.InstancedBufferAttribute(instanceIDs, 1)) // gl_InstanceID is not supported, so make our own id 452 | this.geometry.addAttribute("instanceOffset", new THREE.InstancedBufferAttribute(new Float32Array(3*n).fill(0), 3)) 453 | this.geometry.addAttribute("instanceVelocity", new THREE.InstancedBufferAttribute(new Float32Array(3*n).fill(0), 3)) 454 | this.geometry.addAttribute("instanceAcceleration", new THREE.InstancedBufferAttribute(new Float32Array(3*n).fill(0), 3)) 455 | this.geometry.addAttribute("instanceAngularVelocity", new THREE.InstancedBufferAttribute(new Float32Array(3*n).fill(0), 3)) 456 | this.geometry.addAttribute("instanceAngularAcceleration", new THREE.InstancedBufferAttribute(new Float32Array(3*n).fill(0), 3)) 457 | 458 | if (this.relative === "world") { 459 | this.geometry.addAttribute("instancePosition", new THREE.InstancedBufferAttribute(new Float32Array(3*n).fill(0), 3)) 460 | this.geometry.addAttribute("instanceQuaternion", new THREE.InstancedBufferAttribute(new Float32Array(4*n).fill(0), 4)) 461 | } 462 | } 463 | }, 464 | 465 | updateBounds() { 466 | const data = this.data 467 | const maxAge = Math.max(this.lifeTime[0], this.lifeTime[1]) 468 | const STRIDE = 3 469 | let extent = [new Array(STRIDE).fill(0), new Array(STRIDE).fill(0)] // extent[0] = min values, extent[1] = max values 470 | let radialExtent = [0,0] 471 | 472 | const calcExtent = (offset, velocity, acceleration, t, compareFn) => { 473 | let extent = offset + (velocity + 0.5 * acceleration * t) * t 474 | extent = compareFn(extent, offset) 475 | 476 | const turningPoint = -velocity/acceleration 477 | if (turningPoint > 0 && turningPoint < t) { 478 | extent = compare(extent, offset - 0.5*velocity*velocity/acceleration) 479 | } 480 | 481 | return extent 482 | } 483 | 484 | // Use offset, velocity and acceleration to determine the extents for the particles 485 | for (let j = 0; j < 2; j++) { // index for extent 486 | const compareFn = j === 0 ? Math.min: Math.max 487 | 488 | for (let i = 0; i < STRIDE; i++) { // 0 = x, 1 = y, 2 = z, 3 = radial 489 | const offset = compareFn(this.offset[i], this.offset[i + STRIDE]) 490 | const velocity = compareFn(this.velocity[i], this.velocity[i + STRIDE]) 491 | const acceleration = compareFn(this.acceleration[i], this.acceleration[i + STRIDE]) 492 | 493 | extent[j][i] = calcExtent(offset, velocity, acceleration, maxAge, compareFn) 494 | } 495 | 496 | const radialOffset = compareFn(this.radialOffset[0], this.radialOffset[1]) 497 | const radialVelocity = compareFn(this.radialVelocity[0], this.radialVelocity[1]) 498 | const radialAcceleration = compareFn(this.radialAcceleration[0], this.radialAcceleration[1]) 499 | 500 | radialExtent[j] = calcExtent(radialOffset, radialVelocity, radialAcceleration, maxAge, compareFn) 501 | } 502 | 503 | // apply the radial extents to the XYZ extents 504 | const maxRadial = Math.max(Math.abs(radialExtent[0]), Math.abs(radialExtent[1])) 505 | extent[0][0] -= maxRadial 506 | extent[0][1] -= maxRadial 507 | extent[0][2] -= data.radialType === "sphere" ? maxRadial : 0 508 | extent[1][0] += maxRadial 509 | extent[1][1] += maxRadial 510 | extent[1][2] += data.radialType === "sphere" ? maxRadial : 0 511 | 512 | // TODO consider particle size 513 | 514 | const maxR = Math.max(...extent[0].map(Math.abs), ...extent[1].map(Math.abs)) 515 | if (!this.geometry.boundingSphere) { 516 | this.geometry.boundingSphere = new THREE.Sphere() 517 | } 518 | this.geometry.boundingSphere.radius = maxR 519 | 520 | if (!this.geometry.boundingBox) { 521 | this.geometry.boundingBox = new THREE.Box3() 522 | } 523 | this.geometry.boundingBox.min.set(...extent[0]) 524 | this.geometry.boundingBox.max.set(...extent[1]) 525 | }, 526 | 527 | updateWorldTransform: (function() { 528 | let position = new THREE.Vector3() 529 | let quaternion = new THREE.Quaternion() 530 | let scale = new THREE.Vector3() 531 | let dir = new THREE.Vector3() 532 | let offset = new THREE.Vector3() 533 | let velocity = new THREE.Vector3() 534 | let acceleration = new THREE.Vector3() 535 | let angularVelocity = new THREE.Vector3() 536 | let angularAcceleration = new THREE.Vector3() 537 | 538 | return function(emitterTime) { 539 | const data = this.data 540 | 541 | // the CPU provides the position, velocity, and acceleration parameters for each particle 542 | // (it is cheaper to do this on the CPU than the GPU because the values are set when 543 | // the particles spawn) 544 | if (this.geometry) { 545 | const isWorldRelative = this.relative === "world" 546 | const spawnRate = this.data.spawnRate 547 | const isBurst = data.spawnType === "burst" 548 | const spawnDelta = isBurst ? 0 : 1/spawnRate // for burst particles spawn everything at once 549 | 550 | let instancePosition 551 | let instanceQuaternion 552 | let instanceID = this.geometry.getAttribute("instanceID") 553 | let instanceOffset = this.geometry.getAttribute("instanceOffset") 554 | let instanceVelocity = this.geometry.getAttribute("instanceVelocity") 555 | let instanceAcceleration = this.geometry.getAttribute("instanceAcceleration") 556 | let instanceAngularVelocity = this.geometry.getAttribute("instanceAngularVelocity") 557 | let instanceAngularAcceleration = this.geometry.getAttribute("instanceAngularAcceleration") 558 | 559 | if (isWorldRelative) { 560 | instancePosition = this.geometry.getAttribute("instancePosition") 561 | instanceQuaternion = this.geometry.getAttribute("instanceQuaternion") 562 | this.el.object3D.matrixWorld.decompose(position, quaternion, scale) 563 | 564 | this.geometry.boundingSphere.center.copy(position) 565 | } 566 | 567 | let startID = this.nextID 568 | let numSpawned = 0 569 | let id = startID 570 | 571 | // the nextTime represents the startTime for each particle, so while the nextTime 572 | // is less than this frame's time, keep emitting particles. Note, if the spawnRate is 573 | // low, we may have to wait several frames before a particle is emitted, but if the 574 | // spawnRate is high we will emit several particles per frame 575 | while (this.nextTime <= emitterTime && numSpawned < this.count) { 576 | this.randomDir(dir) 577 | this.randomVec3PlusRadial(this.offset, this.radialOffset, dir, offset) 578 | this.randomVec3PlusRadial(this.velocity, this.radialVelocity, dir, velocity) 579 | this.randomVec3PlusRadial(this.acceleration, this.radialAcceleration, dir, acceleration) 580 | this.randomVec3(this.angularVelocity, angularVelocity) 581 | this.randomVec3(this.angularAcceleration, angularAcceleration) 582 | 583 | if (isWorldRelative) { 584 | instancePosition.setXYZ(id, position.x, position.y, position.z) 585 | instanceQuaternion.setXYZW(id, quaternion.x, quaternion.y, quaternion.z, quaternion.w) 586 | } 587 | 588 | id = this.nextID 589 | instanceID.setX(id, data.enable ? id : -1) 590 | instanceOffset.setXYZ(id, offset.x, offset.y, offset.z) 591 | instanceVelocity.setXYZ(id, velocity.x, velocity.y, velocity.z) 592 | instanceAcceleration.setXYZ(id, acceleration.x, acceleration.y, acceleration.z) 593 | instanceAngularVelocity.setXYZ(id, angularVelocity.x, angularVelocity.y, angularVelocity.z) 594 | instanceAngularAcceleration.setXYZ(id, angularAcceleration.x, angularAcceleration.y, angularAcceleration.z) 595 | 596 | numSpawned++ 597 | this.nextTime += spawnDelta 598 | this.nextID = (this.nextID + 1) % this.count // wrap around to 0 if we'd emitted the last particle in our stack 599 | } 600 | 601 | if (numSpawned > 0) { 602 | this.params[ID_PARAM] = id 603 | 604 | if (isBurst) { // if we did burst emit, then wait for maxAge before emitting again 605 | this.nextTime += this.lifeTime[1] 606 | } 607 | 608 | // if the buffer was wrapped, we cannot send just the end and beginning of a buffer, so submit everything 609 | if (this.nextID < startID) { 610 | startID = 0 611 | numSpawned = this.count 612 | } 613 | 614 | if (isWorldRelative) { 615 | instancePosition.updateRange.offset = startID 616 | instancePosition.updateRange.count = numSpawned 617 | instancePosition.needsUpdate = numSpawned > 0 618 | 619 | instanceQuaternion.updateRange.offset = startID 620 | instanceQuaternion.updateRange.count = numSpawned 621 | instanceQuaternion.needsUpdate = numSpawned > 0 622 | } 623 | 624 | instanceID.updateRange.offset = startID 625 | instanceID.updateRange.count = numSpawned 626 | instanceID.needsUpdate = numSpawned > 0 627 | 628 | instanceOffset.updateRange.offset = startID 629 | instanceOffset.updateRange.count = numSpawned 630 | instanceOffset.needsUpdate = numSpawned > 0 631 | 632 | instanceVelocity.updateRange.offset = startID 633 | instanceVelocity.updateRange.count = numSpawned 634 | instanceVelocity.needsUpdate = numSpawned > 0 635 | 636 | instanceAcceleration.updateRange.offset = startID 637 | instanceAcceleration.updateRange.count = numSpawned 638 | instanceAcceleration.needsUpdate = numSpawned > 0 639 | 640 | instanceAngularVelocity.updateRange.offset = startID 641 | instanceAngularVelocity.updateRange.count = numSpawned 642 | instanceAngularVelocity.needsUpdate = numSpawned > 0 643 | 644 | instanceAngularAcceleration.updateRange.offset = startID 645 | instanceAngularAcceleration.updateRange.count = numSpawned 646 | instanceAngularAcceleration.needsUpdate = numSpawned > 0 647 | } 648 | } 649 | } 650 | })(), 651 | 652 | onBeforeCompile(shader) { 653 | shader.uniforms.params = { value: this.params } 654 | shader.uniforms.colorOverTime = { value: this.colorOverTime } 655 | shader.uniforms.rotationScaleOverTime = { value: this.rotationScaleOverTime } 656 | 657 | // WARNING these shader replacements assume that the standard three.js shders are being used 658 | shader.vertexShader = shader.vertexShader.replace( "void main() {", MESH_PARTICLES_VERTEX_SHADER ) 659 | 660 | shader.vertexShader = shader.vertexShader.replace( "#include ", "" ) // transformed is calculated in MESH_PARTICLES_VERTEX_SHADER 661 | 662 | shader.fragmentShader = shader.fragmentShader.replace( "void main() {", ` 663 | varying vec4 vInstanceColor; 664 | 665 | void main() { 666 | `) 667 | 668 | shader.fragmentShader = shader.fragmentShader.replace( "#include ", ` 669 | #ifdef USE_COLOR 670 | diffuseColor.rgb *= vColor; 671 | #endif 672 | 673 | diffuseColor *= vInstanceColor; 674 | `) 675 | 676 | this.shader = shader 677 | }, 678 | }) 679 | 680 | const MESH_PARTICLES_VERTEX_SHADER = ` 681 | attribute float instanceID; 682 | attribute vec3 instanceOffset; 683 | attribute vec3 instanceVelocity; 684 | attribute vec3 instanceAcceleration; 685 | attribute vec3 instanceAngularVelocity; 686 | attribute vec3 instanceAngularAcceleration; 687 | 688 | #if defined(WORLD_RELATIVE) 689 | attribute vec3 instancePosition; 690 | attribute vec4 instanceQuaternion; 691 | #endif 692 | 693 | uniform vec4 params[3]; 694 | uniform vec4 colorOverTime[OVER_TIME_ARRAY_LENGTH]; 695 | uniform vec4 rotationScaleOverTime[OVER_TIME_ARRAY_LENGTH]; 696 | 697 | varying vec4 vInstanceColor; 698 | 699 | // each call to random will produce a different result by varying randI 700 | float randI = 0.0; 701 | float random( const float seed ) 702 | { 703 | randI += 0.001; 704 | return rand( vec2( seed, randI )); 705 | } 706 | 707 | vec3 randVec3Range( const vec3 range0, const vec3 range1, const float seed ) 708 | { 709 | vec3 lerps = vec3( random( seed ), random( seed ), random( seed ) ); 710 | return mix( range0, range1, lerps ); 711 | } 712 | 713 | float randFloatRange( const float range0, const float range1, const float seed ) 714 | { 715 | float lerps = random( seed ); 716 | return mix( range0, range1, lerps ); 717 | } 718 | 719 | // array lengths are stored in the first slot, followed by actual values from slot 1 onwards 720 | // colors are packed min,max,min,max,min,max,... 721 | // color is packed in xyz and opacity in w, and they may have different length arrays 722 | 723 | vec4 calcColorOverTime( const float r, const float seed ) 724 | { 725 | vec3 color = vec3(1.0); 726 | float opacity = 1.0; 727 | int colorN = int( colorOverTime[0].x ); 728 | int opacityN = int( colorOverTime[0].y ); 729 | 730 | if ( colorN == 1 ) 731 | { 732 | color = randVec3Range( colorOverTime[1].xyz, colorOverTime[2].xyz, seed ); 733 | } 734 | else if ( colorN > 1 ) 735 | { 736 | float ck = r * ( float( colorN ) - 1.0 ); 737 | float ci = floor( ck ); 738 | int i = int( ci )*2 + 1; 739 | vec3 sColor = randVec3Range( colorOverTime[i].xyz, colorOverTime[i + 1].xyz, seed ); 740 | vec3 eColor = randVec3Range( colorOverTime[i + 2].xyz, colorOverTime[i + 3].xyz, seed ); 741 | color = mix( sColor, eColor, ck - ci ); 742 | } 743 | 744 | if ( opacityN == 1 ) 745 | { 746 | opacity = randFloatRange( colorOverTime[1].w, colorOverTime[2].w, seed ); 747 | } 748 | else if ( opacityN > 1 ) 749 | { 750 | float ok = r * ( float( opacityN ) - 1.0 ); 751 | float oi = floor( ok ); 752 | int j = int( oi )*2 + 1; 753 | float sOpacity = randFloatRange( colorOverTime[j].w, colorOverTime[j + 1].w, seed ); 754 | float eOpacity = randFloatRange( colorOverTime[j + 2].w, colorOverTime[j + 3].w, seed ); 755 | opacity = mix( sOpacity, eOpacity, ok - oi ); 756 | } 757 | 758 | return vec4( color, opacity ); 759 | } 760 | 761 | // as per calcColorOverTime but euler rotation is packed in xyz and scale in w 762 | 763 | vec4 calcRotationScaleOverTime( const float r, const float seed ) 764 | { 765 | vec3 rotation = vec3(0.); 766 | float scale = 1.0; 767 | int rotationN = int( rotationScaleOverTime[0].x ); 768 | int scaleN = int( rotationScaleOverTime[0].y ); 769 | 770 | if ( rotationN == 1 ) 771 | { 772 | rotation = randVec3Range( rotationScaleOverTime[1].xyz, rotationScaleOverTime[2].xyz, seed ); 773 | } 774 | else if ( rotationN > 1 ) 775 | { 776 | float rk = r * ( float( rotationN ) - 1.0 ); 777 | float ri = floor( rk ); 778 | int i = int( ri )*2 + 1; // *2 because each range is 2 vectors, and +1 because the first vector is for the length info 779 | vec3 sRotation = randVec3Range( rotationScaleOverTime[i].xyz, rotationScaleOverTime[i + 1].xyz, seed ); 780 | vec3 eRotation = randVec3Range( rotationScaleOverTime[i + 2].xyz, rotationScaleOverTime[i + 3].xyz, seed ); 781 | rotation = mix( sRotation, eRotation, rk - ri ); 782 | } 783 | 784 | if ( scaleN == 1 ) 785 | { 786 | scale = randFloatRange( rotationScaleOverTime[1].w, rotationScaleOverTime[2].w, seed ); 787 | } 788 | else if ( scaleN > 1 ) 789 | { 790 | float sk = r * ( float( scaleN ) - 1.0 ); 791 | float si = floor( sk ); 792 | int j = int( si )*2 + 1; // *2 because each range is 2 vectors, and +1 because the first vector is for the length info 793 | float sScale = randFloatRange( rotationScaleOverTime[j].w, rotationScaleOverTime[j + 1].w, seed ); 794 | float eScale = randFloatRange( rotationScaleOverTime[j + 2].w, rotationScaleOverTime[j + 3].w, seed ); 795 | scale = mix( sScale, eScale, sk - si ); 796 | } 797 | 798 | return vec4( rotation, scale ); 799 | } 800 | 801 | // assumes euler order is YXZ (standard convention for AFrame) 802 | vec4 eulerToQuaternion( const vec3 euler ) 803 | { 804 | // from https://github.com/mrdoob/three.js/blob/master/src/math/Quaternion.js 805 | 806 | vec3 c = cos( euler * 0.5 ); 807 | vec3 s = sin( euler * 0.5 ); 808 | 809 | return vec4( 810 | s.x * c.y * c.z + c.x * s.y * s.z, 811 | c.x * s.y * c.z - s.x * c.y * s.z, 812 | c.x * c.y * s.z - s.x * s.y * c.z, 813 | c.x * c.y * c.z + s.x * s.y * s.z 814 | ); 815 | } 816 | 817 | vec3 applyQuaternion( const vec3 v, const vec4 q ) 818 | { 819 | return v + 2.0 * cross( q.xyz, cross( q.xyz, v ) + q.w * v ); 820 | } 821 | 822 | void main() { 823 | float time = params[0].x; 824 | float ID0 = params[0].y; 825 | float radialType = params[0].z; 826 | float duration = params[0].w; 827 | float spawnType = params[1].x; 828 | float spawnDelta = params[1].y; 829 | float baseSeed = params[1].z; 830 | float instanceCount = params[1].w; 831 | float minAge = params[2].x; 832 | float maxAge = params[2].y; 833 | float loopTime = instanceCount * spawnDelta; 834 | float direction = params[2].z; // 0 is forward, 1 is backward 835 | float age = -1.0; 836 | float ageRatio = -1.0; 837 | float seed = 0.0; 838 | 839 | if (instanceID >= 0.0) { 840 | // particles are either emitted in a burst (spawnType == 0) or spread evenly 841 | // throughout 0..loopTime (spawnType == 1). We calculate the ID of the last spawned particle ID0 842 | // for this frame, any instance IDs after ID0 are assumed to belong to the previous loop 843 | 844 | float loop = floor( time / loopTime ) - spawnType * (instanceID > ID0 ? 1.0 : 0.0); 845 | float startTime = loop * loopTime + instanceID * spawnDelta * spawnType; 846 | age = startTime >= 0.0 ? time - startTime : -1.0; // if age is -1 we won't show the particle 847 | 848 | // we use the id as a seed for the randomizer, but because the IDs are fixed in 849 | // the range 0..instanceCount we calculate a virtual ID by taking into account 850 | // the number of loops that have occurred (note, instanceIDs above ID0 are assumed 851 | // to be in the previous loop). We use the modoulo of the RANDOM_REPEAT_COUNT to 852 | // ensure that the virtualID doesn't exceed the floating point precision 853 | 854 | float virtualID = mod( instanceID + loop * instanceCount, float( RANDOM_REPEAT_COUNT ) ); 855 | seed = mod(1664525.*virtualID*(baseSeed*11.) + 1013904223., 4294967296.)/4294967296.; // we don't have enough precision in 32-bit float, but results look ok 856 | 857 | float lifeTime = randFloatRange( minAge, maxAge, seed ); 858 | 859 | // don't show particles that would be emitted after the duration 860 | if ( duration > 0.0 && time - age >= duration ) 861 | { 862 | age = -1.0; 863 | } 864 | else 865 | { 866 | age = age + direction * ( loopTime - 2.0 * age ); 867 | } 868 | 869 | // the ageRatio will be used for the lerps on over-time attributes 870 | ageRatio = age/lifeTime; 871 | } 872 | 873 | vec3 transformed = vec3(0.0); 874 | vInstanceColor = vec4(1.0); 875 | 876 | if ( ageRatio >= 0.0 && ageRatio <= 1.0 ) 877 | { 878 | vec4 rotScale = calcRotationScaleOverTime( ageRatio, seed ); 879 | vec4 rotationQuaternion = eulerToQuaternion( rotScale.xyz ); 880 | 881 | transformed = rotScale.w * position.xyz; 882 | transformed = applyQuaternion( transformed, rotationQuaternion ); 883 | 884 | vec3 velocity = ( instanceVelocity + 0.5 * instanceAcceleration * age ); 885 | vec3 rotationalVelocity = ( instanceAngularVelocity + 0.5 * instanceAngularAcceleration * age ); 886 | vec4 angularQuaternion = eulerToQuaternion( rotationalVelocity * age ); 887 | 888 | transformed += applyQuaternion( instanceOffset + velocity * age, angularQuaternion ); 889 | 890 | #if defined(WORLD_RELATIVE) 891 | 892 | transformed += 2.0 * cross( instanceQuaternion.xyz, cross( instanceQuaternion.xyz, transformed ) + instanceQuaternion.w * transformed ); 893 | transformed += instancePosition; 894 | 895 | #endif 896 | 897 | vInstanceColor = calcColorOverTime( ageRatio, seed ); // rgba format 898 | }` 899 | 900 | })() 901 | 902 | -------------------------------------------------------------------------------- /assets/banana/CHAHIN_BANANA.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harlyq/aframe-mesh-particles-component/353c9ee240ce711e0beb050663f44ab73cbec2db/assets/banana/CHAHIN_BANANA.bin -------------------------------------------------------------------------------- /assets/banana/CHAHIN_BANANA.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 0, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 432, 8 | "max": [ 9 | 431.0 10 | ], 11 | "min": [ 12 | 0.0 13 | ], 14 | "name": "buffer-0-accessor-indices-buffer-0-mesh-0", 15 | "type": "SCALAR" 16 | }, 17 | { 18 | "bufferView": 2, 19 | "byteOffset": 0, 20 | "componentType": 5126, 21 | "count": 432, 22 | "max": [ 23 | 0.14216899871826172, 24 | 0.3672190010547638, 25 | 0.662015974521637 26 | ], 27 | "min": [ 28 | -0.14216899871826172, 29 | -0.09007299691438675, 30 | -0.785847008228302 31 | ], 32 | "name": "buffer-0-accessor-position-buffer-0-mesh-0", 33 | "type": "VEC3" 34 | }, 35 | { 36 | "bufferView": 2, 37 | "byteOffset": 5184, 38 | "componentType": 5126, 39 | "count": 432, 40 | "max": [ 41 | 0.946399986743927, 42 | 0.9718000292778015, 43 | 1.0 44 | ], 45 | "min": [ 46 | -0.946399986743927, 47 | -0.9733999967575073, 48 | -1.0 49 | ], 50 | "name": "buffer-0-accessor-normal-buffer-0-mesh-0", 51 | "type": "VEC3" 52 | }, 53 | { 54 | "bufferView": 1, 55 | "byteOffset": 0, 56 | "componentType": 5126, 57 | "count": 432, 58 | "max": [ 59 | 0.8711000084877014, 60 | 0.0 61 | ], 62 | "min": [ 63 | 0.0, 64 | -0.8946999907493591 65 | ], 66 | "name": "buffer-0-accessor-texcoord-buffer-0-mesh-0", 67 | "type": "VEC2" 68 | }, 69 | { 70 | "bufferView": 3, 71 | "byteOffset": 0, 72 | "componentType": 5126, 73 | "count": 0, 74 | "max": [ 75 | 0.0, 76 | 0.0, 77 | 0.0, 78 | 0.0 79 | ], 80 | "min": [ 81 | 0.0, 82 | 0.0, 83 | 0.0, 84 | 0.0 85 | ], 86 | "name": "buffer-0-accessor-color-buffer-0-mesh-0", 87 | "type": "VEC4" 88 | } 89 | ], 90 | "asset": { 91 | "generator": "Obj2GltfConverter", 92 | "version": "2.0" 93 | }, 94 | "bufferViews": [ 95 | { 96 | "buffer": 0, 97 | "byteLength": 864, 98 | "byteOffset": 0, 99 | "byteStride": 0, 100 | "name": "buffer-0-bufferview-ushort", 101 | "target": 34963 102 | }, 103 | { 104 | "buffer": 0, 105 | "byteLength": 3456, 106 | "byteOffset": 864, 107 | "byteStride": 8, 108 | "name": "buffer-0-bufferview-vec2", 109 | "target": 34962 110 | }, 111 | { 112 | "buffer": 0, 113 | "byteLength": 10368, 114 | "byteOffset": 4320, 115 | "byteStride": 12, 116 | "name": "buffer-0-bufferview-vec3", 117 | "target": 34962 118 | }, 119 | { 120 | "buffer": 0, 121 | "byteLength": 0, 122 | "byteOffset": 14688, 123 | "byteStride": 16, 124 | "name": "buffer-0-bufferview-vec4", 125 | "target": 34962 126 | } 127 | ], 128 | "buffers": [ 129 | { 130 | "byteLength": 14688, 131 | "name": "buffer-0", 132 | "uri": "CHAHIN_BANANA.bin" 133 | } 134 | ], 135 | "images": [ 136 | { 137 | "mimeType": "image/jpeg", 138 | "name": "Material.050-image", 139 | "uri": "CHAHIN_BANANA_TEXTURE.jpg" 140 | } 141 | ], 142 | "materials": [ 143 | { 144 | "alphaMode": "OPAQUE", 145 | "doubleSided": true, 146 | "name": "Material.050", 147 | "pbrMetallicRoughness": { 148 | "baseColorFactor": [ 149 | 0.64, 150 | 0.64, 151 | 0.64, 152 | 1.0 153 | ], 154 | "baseColorTexture": { 155 | "index": 0, 156 | "texCoord": 0 157 | }, 158 | "metallicFactor": 0.0, 159 | "roughnessFactor": 0.7448017359246658 160 | } 161 | } 162 | ], 163 | "meshes": [ 164 | { 165 | "name": "buffer-0-mesh-0", 166 | "primitives": [ 167 | { 168 | "attributes": { 169 | "POSITION": 1, 170 | "NORMAL": 2, 171 | "TEXCOORD_0": 3 172 | }, 173 | "indices": 0, 174 | "material": 0, 175 | "mode": 4 176 | } 177 | ] 178 | } 179 | ], 180 | "nodes": [ 181 | { 182 | "mesh": 0, 183 | "name": "node-0" 184 | } 185 | ], 186 | "samplers": [ 187 | { 188 | "name": "Material.050-sampler", 189 | "wrapS": 10497, 190 | "wrapT": 10497 191 | } 192 | ], 193 | "scenes": [ 194 | { 195 | "name": "scene-0", 196 | "nodes": [ 197 | 0 198 | ] 199 | } 200 | ], 201 | "textures": [ 202 | { 203 | "name": "Material.050-texture", 204 | "sampler": 0, 205 | "source": 0 206 | } 207 | ] 208 | } -------------------------------------------------------------------------------- /assets/banana/CHAHIN_BANANA_TEXTURE.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harlyq/aframe-mesh-particles-component/353c9ee240ce711e0beb050663f44ab73cbec2db/assets/banana/CHAHIN_BANANA_TEXTURE.jpg -------------------------------------------------------------------------------- /assets/banana/Poly by Goolgle.author: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harlyq/aframe-mesh-particles-component/353c9ee240ce711e0beb050663f44ab73cbec2db/assets/banana/Poly by Goolgle.author -------------------------------------------------------------------------------- /assets/blob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harlyq/aframe-mesh-particles-component/353c9ee240ce711e0beb050663f44ab73cbec2db/assets/blob.png -------------------------------------------------------------------------------- /assets/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harlyq/aframe-mesh-particles-component/353c9ee240ce711e0beb050663f44ab73cbec2db/assets/screenshot.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-mesh-particles-component", 3 | "version": "0.5.0", 4 | "description": "aframe component for mesh particles", 5 | "main": "aframe-mesh-particles-component.js", 6 | "files": ["aframe-mesh-particles-component.js"], 7 | "repository": { 8 | "type": "git", 9 | "url": "github:harlyq/aframe-mesh-particles-component" 10 | }, 11 | "keywords": [ 12 | "aframe", 13 | "component", 14 | "particle", 15 | "particles", 16 | "mesh" 17 | ], 18 | "author": "harlyq", 19 | "license": "MIT", 20 | "peerDependencies": { 21 | "aframe": "^0.8.2" 22 | } 23 | } 24 | --------------------------------------------------------------------------------