|
| 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' |
0 commit comments