Skip to content

Commit 2a022dc

Browse files
authored
feat: add Sparkles (#90)
Floating, glowing particles. https://drei.docs.pmnd.rs/staging/sparkles#sparkles
1 parent bf1e482 commit 2a022dc

File tree

4 files changed

+440
-0
lines changed

4 files changed

+440
-0
lines changed
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import * as THREE from 'three'
2+
import { Setup } from '../Setup'
3+
import GUI from 'lil-gui'
4+
import { Meta } from '@storybook/html'
5+
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
6+
import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js'
7+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
8+
import { Sparkles, SparklesProps } from '../../src/core/Sparkles'
9+
export default {
10+
title: 'Staging/Sparkles',
11+
} as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS
12+
13+
let gui: GUI
14+
let scene: THREE.Scene,
15+
camera: THREE.Camera,
16+
renderer: THREE.WebGLRenderer,
17+
animateLoop: (callback: (time: number) => void) => void,
18+
allSparkles: Sparkles[] = []
19+
20+
export const SparkleStory = async () => {
21+
const setupResult = Setup()
22+
scene = setupResult.scene
23+
camera = setupResult.camera
24+
renderer = setupResult.renderer
25+
animateLoop = setupResult.render
26+
27+
gui = new GUI({ title: SparkleStory.storyName })
28+
renderer.shadowMap.enabled = true
29+
renderer.toneMapping = THREE.ACESFilmicToneMapping
30+
camera.position.set(12, 12, 12)
31+
32+
const controls = new OrbitControls(camera, renderer.domElement)
33+
controls.target.set(0, 6, 0)
34+
controls.update()
35+
36+
const floor = new THREE.Mesh(
37+
new THREE.PlaneGeometry(60, 60).rotateX(-Math.PI / 2),
38+
new THREE.ShadowMaterial({ opacity: 0.3 })
39+
)
40+
floor.receiveShadow = true
41+
scene.add(floor)
42+
43+
const dirLight = new THREE.DirectionalLight(0xabcdef, 10)
44+
dirLight.position.set(1, 20, 1)
45+
dirLight.castShadow = true
46+
dirLight.shadow.mapSize.width = 1024
47+
dirLight.shadow.mapSize.height = 1024
48+
scene.add(dirLight)
49+
50+
setupEnvironment()
51+
setupSparkles()
52+
}
53+
54+
/**
55+
* Add scene.environment and groundProjected skybox
56+
*/
57+
const setupEnvironment = () => {
58+
const exrLoader = new EXRLoader()
59+
60+
// exr from polyhaven.com
61+
exrLoader.load('round_platform_1k.exr', (exrTex) => {
62+
exrTex.mapping = THREE.EquirectangularReflectionMapping
63+
scene.environment = exrTex
64+
scene.background = exrTex
65+
66+
const groundProjection = new GroundedSkybox(exrTex, 5, 20)
67+
groundProjection.position.set(0, 5, 0)
68+
scene.add(groundProjection)
69+
})
70+
}
71+
72+
function setupSparkles() {
73+
addSimpleSparkles()
74+
addRandomizedSparkles()
75+
76+
const timer = new THREE.Timer()
77+
78+
// runs on every frame
79+
animateLoop(() => {
80+
timer.update()
81+
const elapsedTime = timer.getElapsed()
82+
for (const sparkle of allSparkles) {
83+
sparkle.update(elapsedTime)
84+
}
85+
})
86+
}
87+
88+
function addSimpleSparkles() {
89+
const sphereMesh = new THREE.Mesh(
90+
new THREE.SphereGeometry(0.5, 16, 16),
91+
new THREE.MeshStandardMaterial({
92+
color: 0xffffff,
93+
emissive: 'lightpink',
94+
emissiveIntensity: 0.2,
95+
roughness: 0.25,
96+
})
97+
)
98+
sphereMesh.position.set(-3, 2, 0)
99+
sphereMesh.castShadow = true
100+
scene.add(sphereMesh)
101+
102+
const sparkleParameters: SparklesProps = {
103+
noise: 1,
104+
count: 100,
105+
speed: 1,
106+
opacity: 1,
107+
scale: 1,
108+
size: 5,
109+
color: new THREE.Color(0xffffff),
110+
}
111+
const sparkles = new Sparkles(sparkleParameters)
112+
sparkles.setPixelRatio(renderer.getPixelRatio())
113+
114+
allSparkles.push(sparkles)
115+
sphereMesh.add(sparkles)
116+
117+
// gui controls for sparkles
118+
const sFol = gui.addFolder('Simple Sparkles')
119+
sFol.add(sparkleParameters, 'count', 10, 1000, 100).onChange(() => sparkles.rebuildAttributes(sparkleParameters))
120+
sFol.add(sparkleParameters, 'size', 0, 50, 1).onChange(() => sparkles.rebuildAttributes(sparkleParameters))
121+
sFol.add(sparkleParameters, 'opacity', 0, 1, 1).onChange(() => sparkles.rebuildAttributes(sparkleParameters))
122+
sFol.add(sparkleParameters, 'speed', 0, 15, 1).onChange(() => sparkles.rebuildAttributes(sparkleParameters))
123+
sFol.add(sparkleParameters, 'noise', 0, 15, 1).onChange(() => sparkles.rebuildAttributes(sparkleParameters))
124+
sFol.add(sparkleParameters, 'scale', 0, 5, 0.5).onChange(() => sparkles.rebuildAttributes(sparkleParameters))
125+
sFol.addColor(sparkleParameters, 'color').onChange(() => sparkles.rebuildAttributes(sparkleParameters))
126+
}
127+
128+
function addRandomizedSparkles() {
129+
// parent mesh for the sparkles
130+
const octahedronMesh = new THREE.Mesh(
131+
new THREE.OctahedronGeometry(2),
132+
new THREE.MeshStandardMaterial({
133+
color: 'purple',
134+
roughness: 0.25,
135+
wireframe: true,
136+
})
137+
)
138+
octahedronMesh.position.set(3, 2, 0)
139+
octahedronMesh.castShadow = true
140+
scene.add(octahedronMesh)
141+
142+
const count = 100
143+
const sparkleParameters: SparklesProps = {
144+
count,
145+
noise: new Float32Array(count * 3),
146+
speed: new Float32Array(count),
147+
opacity: new Float32Array(count),
148+
scale: new THREE.Vector3(1, 3, 1), // bounds of the sparkles
149+
size: new Float32Array(count),
150+
color: new Float32Array(count * 3),
151+
}
152+
153+
const randomParams = {
154+
minNoise: 0.3,
155+
maxNoise: 10,
156+
157+
minSpeed: 0.3,
158+
maxSpeed: 10,
159+
160+
minSize: 0.3,
161+
maxSize: 10,
162+
163+
minColor: 0,
164+
maxColor: 1,
165+
}
166+
167+
// Helper function to randomize Float32Array values
168+
function randomizeArrayContent(array: Float32Array, min: number, max: number) {
169+
for (let i = 0; i < array.length; i++) {
170+
array[i] = THREE.MathUtils.randFloat(min, max)
171+
}
172+
}
173+
174+
// fill the arrays with random values
175+
function randomiseValues() {
176+
if (sparkleParameters.noise && sparkleParameters.noise instanceof Float32Array) {
177+
randomizeArrayContent(sparkleParameters.noise, randomParams.minNoise, randomParams.maxNoise)
178+
}
179+
if (sparkleParameters.speed instanceof Float32Array) {
180+
randomizeArrayContent(sparkleParameters.speed, randomParams.minSpeed, randomParams.maxSpeed)
181+
}
182+
if (sparkleParameters.size instanceof Float32Array) {
183+
randomizeArrayContent(sparkleParameters.size, randomParams.minSize, randomParams.maxSize)
184+
}
185+
if (sparkleParameters.opacity instanceof Float32Array) {
186+
randomizeArrayContent(sparkleParameters.opacity, 0.3, 1)
187+
}
188+
if (sparkleParameters.color instanceof Float32Array) {
189+
randomizeArrayContent(sparkleParameters.color, randomParams.minColor, randomParams.maxColor)
190+
}
191+
}
192+
193+
randomiseValues()
194+
195+
const advancedSparkles = new Sparkles(sparkleParameters)
196+
advancedSparkles.setPixelRatio(renderer.getPixelRatio())
197+
198+
allSparkles.push(advancedSparkles)
199+
octahedronMesh.add(advancedSparkles)
200+
201+
// gui controls for sparkles
202+
const updateSparkles = () => {
203+
randomiseValues()
204+
advancedSparkles.rebuildAttributes(sparkleParameters)
205+
}
206+
const sFol = gui.addFolder('Randomized Sparkles')
207+
sFol.add(randomParams, 'minNoise', 0, 100, 1).onChange(updateSparkles)
208+
sFol.add(randomParams, 'maxNoise', 0, 100, 1).onChange(updateSparkles)
209+
sFol.add(randomParams, 'minSpeed', 0, 50, 1).onChange(updateSparkles)
210+
sFol.add(randomParams, 'maxSpeed', 0, 50, 1).onChange(updateSparkles)
211+
sFol.add(randomParams, 'minSize', 0.1, 100, 1).onChange(updateSparkles)
212+
sFol.add(randomParams, 'maxSize', 0, 100, 1).onChange(updateSparkles)
213+
sFol.add(randomParams, 'minColor', 0, 1, 0.01).onChange(updateSparkles)
214+
sFol.add(randomParams, 'maxColor', 0, 1, 0.01).onChange(updateSparkles)
215+
}
216+
217+
SparkleStory.storyName = 'Two Sparkles'

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { pcss, ... } from '@pmndrs/vanilla'
5353
<li><a href="#caustics">Caustics</a></li>
5454
<li><a href="#cloud">Cloud</a></li>
5555
<li><a href="#camerashake">Camera Shake</a></li>
56+
<li><a href="#sparkles">Sparkles</a></li>
5657
</ul>
5758
<li><a href="#staging">Abstractions</a></li>
5859
<ul>
@@ -520,6 +521,54 @@ shake.decay= false, // should the intensity decay over time
520521
shake.decayRate= 0.65, // if decay = true this is the rate at which intensity will reduce at
521522
```
522523
524+
#### Sparkles
525+
526+
[![storybook](https://img.shields.io/badge/-storybook-%23ff69b4)](https://pmndrs.github.io/drei-vanilla/?path=/story/staging-sparkles--sparkle-story)
527+
528+
[drei counterpart](https://drei.docs.pmnd.rs/staging/sparkles#sparkles)
529+
530+
<p>
531+
<a href="https://codesandbox.io/s/0c5hv9"><img width="20%" src="https://codesandbox.io/api/v1/sandboxes/0c5hv9/screenshot.png" alt="Demo"/></a>
532+
</p>
533+
534+
Floating, glowing particles.
535+
536+
```ts
537+
SparklesProps = {
538+
/** Number of particles (default: 100) */
539+
count?: number
540+
/** Speed of particles (default: 1) */
541+
speed?: number | Float32Array
542+
/** Opacity of particles (default: 1) */
543+
opacity?: number | Float32Array
544+
/** Color of particles (default: 100) */
545+
color?: THREE.ColorRepresentation | Float32Array
546+
/** Size of particles (default: randomized between 0 and 1) */
547+
size?: number | Float32Array
548+
/** The space the particles occupy (default: 1) */
549+
scale?: number | [number, number, number] | THREE.Vector3
550+
/** Movement factor (default: 1) */
551+
noise?: number | [number, number, number] | THREE.Vector3 | Float32Array
552+
}
553+
```
554+
555+
Custom shaders are allowed. Sparkles will use the following attributes and uniforms:
556+
557+
Usage
558+
559+
```js
560+
const sparkles = new Sparkles(sparklesProps)
561+
sparkles.setPixelRatio(renderer.getPixelRatio())
562+
scene.add(sparkles)
563+
564+
// in the update loop
565+
function animate() {
566+
sparkles.update(elapsedTime)
567+
...
568+
}
569+
570+
```
571+
523572
#### Grid
524573
525574
[![storybook](https://img.shields.io/badge/-storybook-%23ff69b4)](https://pmndrs.github.io/drei-vanilla/?path=/story/gizmos-grid--grid-story)

0 commit comments

Comments
 (0)