├── LICENSE ├── README.md ├── getLayer.js ├── getParticleSystem.js ├── img ├── circle.png ├── fire.png ├── rad-grad.png └── smoke.png ├── index.html └── index.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Bobby Roe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learn Three.js: Simple Particle System 2 | 3 | Learn three simple particle effects with Three.js! 4 | Quick, easy-to-create effects like Fire, Smoke and Sparkles. 5 | 6 | Watch the tutorial on [YouTube](https://youtu.be/h1UQdbuF204) 7 | 8 | Also, fork and create something cool! -------------------------------------------------------------------------------- /getLayer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | const loader = new THREE.TextureLoader(); 4 | 5 | function getSprite({ hasFog, color, opacity, path, pos, size }) { 6 | const spriteMat = new THREE.SpriteMaterial({ 7 | color, 8 | fog: hasFog, 9 | map: loader.load(path), 10 | transparent: true, 11 | opacity, 12 | }); 13 | spriteMat.color.offsetHSL(0, 0, Math.random() * 0.2 - 0.1); 14 | const sprite = new THREE.Sprite(spriteMat); 15 | sprite.position.set(pos.x, -pos.y, pos.z); 16 | size += Math.random() - 0.5; 17 | sprite.scale.set(size, size, size); 18 | sprite.material.rotation = 0; 19 | return sprite; 20 | } 21 | 22 | function getLayer({ 23 | hasFog = true, 24 | hue = 0.0, 25 | numSprites = 10, 26 | opacity = 1, 27 | path = "./img/rad-grad.png", 28 | radius = 1, 29 | sat = 0.5, 30 | size = 1, 31 | z = 0, 32 | }) { 33 | const layerGroup = new THREE.Group(); 34 | for (let i = 0; i < numSprites; i += 1) { 35 | let angle = (i / numSprites) * Math.PI * 2; 36 | const pos = new THREE.Vector3( 37 | Math.cos(angle) * Math.random() * radius, 38 | Math.sin(angle) * Math.random() * radius, 39 | z + Math.random() 40 | ); 41 | const length = new THREE.Vector3(pos.x, pos.y, 0).length(); 42 | // const hue = 0.0; // (0.9 - (radius - length) / radius) * 1; 43 | 44 | let color = new THREE.Color().setHSL(hue, 1, sat); 45 | const sprite = getSprite({ hasFog, color, opacity, path, pos, size }); 46 | layerGroup.add(sprite); 47 | } 48 | return layerGroup; 49 | } 50 | export default getLayer; -------------------------------------------------------------------------------- /getParticleSystem.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | const _VS = ` 4 | uniform float pointMultiplier; 5 | 6 | attribute float size; 7 | attribute float angle; 8 | attribute vec4 aColor; 9 | 10 | varying vec4 vColor; 11 | varying vec2 vAngle; 12 | 13 | void main() { 14 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); 15 | 16 | gl_Position = projectionMatrix * mvPosition; 17 | gl_PointSize = size * pointMultiplier / gl_Position.w; 18 | 19 | vAngle = vec2(cos(angle), sin(angle)); 20 | vColor = aColor; 21 | }`; 22 | 23 | const _FS = ` 24 | uniform sampler2D diffuseTexture; 25 | 26 | varying vec4 vColor; 27 | varying vec2 vAngle; 28 | 29 | void main() { 30 | vec2 coords = (gl_PointCoord - 0.5) * mat2(vAngle.x, vAngle.y, -vAngle.y, vAngle.x) + 0.5; 31 | gl_FragColor = texture2D(diffuseTexture, coords) * vColor; 32 | }`; 33 | 34 | 35 | function getLinearSpline(lerp) { 36 | 37 | const points = []; 38 | const _lerp = lerp; 39 | 40 | function addPoint(t, d) { 41 | points.push([t, d]); 42 | } 43 | 44 | function getValueAt(t) { 45 | let p1 = 0; 46 | 47 | for (let i = 0; i < points.length; i++) { 48 | if (points[i][0] >= t) { 49 | break; 50 | } 51 | p1 = i; 52 | } 53 | 54 | const p2 = Math.min(points.length - 1, p1 + 1); 55 | 56 | if (p1 == p2) { 57 | return points[p1][1]; 58 | } 59 | 60 | return _lerp( 61 | (t - points[p1][0]) / ( 62 | points[p2][0] - points[p1][0]), 63 | points[p1][1], points[p2][1]); 64 | } 65 | return { addPoint, getValueAt }; 66 | } 67 | 68 | function getParticleSystem(params) { 69 | const { camera, emitter, parent, rate, texture } = params; 70 | const uniforms = { 71 | diffuseTexture: { 72 | value: new THREE.TextureLoader().load(texture) 73 | }, 74 | pointMultiplier: { 75 | value: window.innerHeight / (2.0 * Math.tan(30.0 * Math.PI / 180.0)) 76 | } 77 | }; 78 | const _material = new THREE.ShaderMaterial({ 79 | uniforms: uniforms, 80 | vertexShader: _VS, 81 | fragmentShader: _FS, 82 | blending: THREE.AdditiveBlending, 83 | depthTest: true, 84 | depthWrite: false, 85 | transparent: true, 86 | vertexColors: true 87 | }); 88 | 89 | let _particles = []; 90 | 91 | const geometry = new THREE.BufferGeometry(); 92 | geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3)); 93 | geometry.setAttribute('size', new THREE.Float32BufferAttribute([], 1)); 94 | geometry.setAttribute('aColor', new THREE.Float32BufferAttribute([], 4)); 95 | geometry.setAttribute('angle', new THREE.Float32BufferAttribute([], 1)); 96 | 97 | const _points = new THREE.Points(geometry, _material); 98 | 99 | parent.add(_points); 100 | 101 | const alphaSpline = getLinearSpline((t, a, b) => { 102 | return a + t * (b - a); 103 | }); 104 | alphaSpline.addPoint(0.0, 0.0); 105 | alphaSpline.addPoint(0.6, 1.0); 106 | alphaSpline.addPoint(1.0, 0.0); 107 | 108 | const colorSpline = getLinearSpline((t, a, b) => { 109 | const c = a.clone(); 110 | return c.lerp(b, t); 111 | }); 112 | colorSpline.addPoint(0.0, new THREE.Color(0xFFFFFF)); 113 | colorSpline.addPoint(1.0, new THREE.Color(0xff8080)); 114 | 115 | const sizeSpline = getLinearSpline((t, a, b) => { 116 | return a + t * (b - a); 117 | }); 118 | sizeSpline.addPoint(0.0, 0.0); 119 | sizeSpline.addPoint(1.0, 1.0); 120 | // max point size = 512; => console.log(ctx.getParameter(ctx.ALIASED_POINT_SIZE_RANGE)); 121 | const radius = 0.5; 122 | const maxLife = 1.5; 123 | const maxSize = 3.0; 124 | let gdfsghk = 0.0; 125 | function _AddParticles(timeElapsed) { 126 | gdfsghk += timeElapsed; 127 | const n = Math.floor(gdfsghk * rate); 128 | gdfsghk -= n / rate; 129 | for (let i = 0; i < n; i += 1) { 130 | const life = (Math.random() * 0.75 + 0.25) * maxLife; 131 | _particles.push({ 132 | position: new THREE.Vector3( 133 | (Math.random() * 2 - 1) * radius, 134 | (Math.random() * 2 - 1) * radius, 135 | (Math.random() * 2 - 1) * radius).add(emitter.position), 136 | size: (Math.random() * 0.5 + 0.5) * maxSize, 137 | colour: new THREE.Color(), 138 | alpha: 1.0, 139 | life: life, 140 | maxLife: life, 141 | rotation: Math.random() * 2.0 * Math.PI, 142 | rotationRate: Math.random() * 0.01 - 0.005, 143 | velocity: new THREE.Vector3(0, 1.5, 0), 144 | }); 145 | } 146 | } 147 | 148 | function _UpdateGeometry() { 149 | const positions = []; 150 | const sizes = []; 151 | const colours = []; 152 | const angles = []; 153 | 154 | for (let p of _particles) { 155 | positions.push(p.position.x, p.position.y, p.position.z); 156 | colours.push(p.colour.r, p.colour.g, p.colour.b, p.alpha); 157 | sizes.push(p.currentSize); 158 | angles.push(p.rotation); 159 | } 160 | 161 | geometry.setAttribute( 162 | 'position', new THREE.Float32BufferAttribute(positions, 3)); 163 | geometry.setAttribute( 164 | 'size', new THREE.Float32BufferAttribute(sizes, 1)); 165 | geometry.setAttribute( 166 | 'aColor', new THREE.Float32BufferAttribute(colours, 4)); 167 | geometry.setAttribute( 168 | 'angle', new THREE.Float32BufferAttribute(angles, 1)); 169 | 170 | geometry.attributes.position.needsUpdate = true; 171 | geometry.attributes.size.needsUpdate = true; 172 | geometry.attributes.aColor.needsUpdate = true; 173 | geometry.attributes.angle.needsUpdate = true; 174 | } 175 | _UpdateGeometry(); 176 | 177 | function _UpdateParticles(timeElapsed) { 178 | for (let p of _particles) { 179 | p.life -= timeElapsed; 180 | } 181 | 182 | _particles = _particles.filter(p => { 183 | return p.life > 0.0; 184 | }); 185 | 186 | for (let p of _particles) { 187 | const t = 1.0 - p.life / p.maxLife; 188 | p.rotation += p.rotationRate; 189 | p.alpha = alphaSpline.getValueAt(t); 190 | p.currentSize = p.size * sizeSpline.getValueAt(t); 191 | p.colour.copy(colorSpline.getValueAt(t)); 192 | 193 | p.position.add(p.velocity.clone().multiplyScalar(timeElapsed)); 194 | 195 | const drag = p.velocity.clone(); 196 | drag.multiplyScalar(timeElapsed * 0.1); 197 | drag.x = Math.sign(p.velocity.x) * Math.min(Math.abs(drag.x), Math.abs(p.velocity.x)); 198 | drag.y = Math.sign(p.velocity.y) * Math.min(Math.abs(drag.y), Math.abs(p.velocity.y)); 199 | drag.z = Math.sign(p.velocity.z) * Math.min(Math.abs(drag.z), Math.abs(p.velocity.z)); 200 | p.velocity.sub(drag); 201 | } 202 | 203 | _particles.sort((a, b) => { 204 | const d1 = camera.position.distanceTo(a.position); 205 | const d2 = camera.position.distanceTo(b.position); 206 | 207 | if (d1 > d2) { 208 | return -1; 209 | } 210 | if (d1 < d2) { 211 | return 1; 212 | } 213 | return 0; 214 | }); 215 | } 216 | 217 | function update(timeElapsed) { 218 | _AddParticles(timeElapsed); 219 | _UpdateParticles(timeElapsed); 220 | _UpdateGeometry(); 221 | } 222 | return { update }; 223 | } 224 | 225 | export { getParticleSystem }; -------------------------------------------------------------------------------- /img/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobbyroe/Simple-Particle-Effects/2fb42f78ca2ffca5b420b5a1b4dbd974c9fdc511/img/circle.png -------------------------------------------------------------------------------- /img/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobbyroe/Simple-Particle-Effects/2fb42f78ca2ffca5b420b5a1b4dbd974c9fdc511/img/fire.png -------------------------------------------------------------------------------- /img/rad-grad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobbyroe/Simple-Particle-Effects/2fb42f78ca2ffca5b420b5a1b4dbd974c9fdc511/img/rad-grad.png -------------------------------------------------------------------------------- /img/smoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobbyroe/Simple-Particle-Effects/2fb42f78ca2ffca5b420b5a1b4dbd974c9fdc511/img/smoke.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |