├── .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 | 
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 |
--------------------------------------------------------------------------------