|
33 | 33 |
|
34 | 34 | const scene = new THREE.Scene(); |
35 | 35 | const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); |
| 36 | + |
| 37 | + // Initialize camera with elevated perspective |
| 38 | + camera.position.set(0, 2, 2.5); |
| 39 | + camera.lookAt(0, 0, 0); |
| 40 | + |
36 | 41 | const renderer = new THREE.WebGLRenderer(); |
37 | 42 | renderer.setSize(window.innerWidth, window.innerHeight); |
38 | 43 | document.body.appendChild(renderer.domElement) |
39 | 44 |
|
| 45 | + // Animation timing variables |
40 | 46 | const animateT = dyno.dynoFloat(0); |
41 | | - let baseTime = 0; // Variable to reset time |
42 | | - let splatLoaded = false; // Flag to know if splat has loaded |
| 47 | + let baseTime = 0; |
| 48 | + let splatLoaded = false; |
| 49 | + |
| 50 | + // Camera orbit parameters |
| 51 | + let cameraAngle = 0; |
| 52 | + const cameraRadius = 3; |
| 53 | + const cameraHeight = 2; |
| 54 | + const rotationSpeed = 0.2; |
43 | 55 |
|
44 | | - // Effect parameters |
| 56 | + // Available visual effects configuration |
45 | 57 | const effectParams = { |
46 | | - effect: "Twister" // "Twister", "Rain" |
| 58 | + effect: "Magic" |
47 | 59 | }; |
48 | 60 |
|
49 | | - const splatURL = await getAssetFileURL("sutro.zip"); |
50 | | - const splatMesh = new SplatMesh({ url: splatURL }); |
51 | | - splatMesh.quaternion.set(1, 0, 0, 0); |
52 | | - splatMesh.position.set(0, 0, -1.5); |
53 | | - splatMesh.scale.set(.5,.5,.5); |
54 | | - scene.add(splatMesh); |
55 | | - |
56 | | - // Wait for splat to load completely |
57 | | - await splatMesh.loaded; |
58 | | - splatLoaded = true; |
59 | | - baseTime = performance.now() / 1000; // Initialize base time when loaded |
60 | | - |
61 | | - // Create GUI |
62 | | - const gui = new GUI({ title: "Weather Effects" }); |
63 | | - gui.add(effectParams, "effect", ["Twister", "Rain"]).name("Effect Type").onChange(() => { |
64 | | - // Reset time when effect changes |
65 | | - baseTime = performance.now() / 1000; |
66 | | - splatMesh.updateGenerator(); |
67 | | - }); |
| 61 | + let splatMesh = null; |
| 62 | + |
| 63 | + /** |
| 64 | + * Loads and configures splat mesh based on selected effect |
| 65 | + * @param {string} effect - The effect type (Magic, Spread, Unroll, Twister, or Rain) |
| 66 | + */ |
| 67 | + async function loadSplatForEffect(effect) { |
| 68 | + // Clean up existing splat mesh |
| 69 | + if (splatMesh) { |
| 70 | + scene.remove(splatMesh); |
| 71 | + splatMesh = null; |
| 72 | + } |
| 73 | + |
| 74 | + // Configure splat file and positioning per effect |
| 75 | + let splatFileName, position; |
| 76 | + if (effect === "Magic") { |
| 77 | + splatFileName = "primerib-tamos.spz"; |
| 78 | + position = [0, 0, 0]; |
| 79 | + } else if (effect === "Spread") { |
| 80 | + splatFileName = "valley.spz"; |
| 81 | + position = [0, 1, 1]; |
| 82 | + } else if (effect === "Unroll") { |
| 83 | + splatFileName = "burger-from-amboy.spz"; |
| 84 | + position = [0, 0, 0]; |
| 85 | + } else if (effect === "Twister" || effect === "Rain") { |
| 86 | + splatFileName = "sutro.zip"; |
| 87 | + position = [0, -1, 1]; |
| 88 | + } |
| 89 | + |
| 90 | + // Load and initialize new splat mesh |
| 91 | + const splatURL = await getAssetFileURL(splatFileName); |
| 92 | + splatMesh = new SplatMesh({ url: splatURL }); |
| 93 | + splatMesh.quaternion.set(1, 0, 0, 0); |
| 94 | + splatMesh.position.set(position[0], position[1], position[2]); |
| 95 | + |
| 96 | + // Apply special scaling for Unroll effect |
| 97 | + if (effect === "Unroll") { |
| 98 | + splatMesh.scale.set(1.5, 1.5, 1.5); |
| 99 | + } else if (effect === "Twister" || effect === "Rain") { |
| 100 | + splatMesh.scale.set(0.8, 0.8, 0.8); |
| 101 | + } |
| 102 | + |
| 103 | + scene.add(splatMesh); |
| 104 | + |
| 105 | + // Wait for asset loading and reset animation timing |
| 106 | + splatLoaded = false; |
| 107 | + await splatMesh.loaded; |
| 108 | + splatLoaded = true; |
| 109 | + baseTime = 0; |
| 110 | + |
| 111 | + // Apply visual effects to the loaded splat |
| 112 | + setupSplatModifier(); |
| 113 | + } |
| 114 | + |
| 115 | + // Initialize user interface |
| 116 | + const gui = new GUI(); |
| 117 | + const effectFolder = gui.addFolder('Effects'); |
68 | 118 |
|
69 | | - // Add reset time button |
70 | | - gui.add({ resetTime: () => { |
71 | | - baseTime = performance.now() / 1000; |
72 | | - splatMesh.updateGenerator(); |
73 | | - }}, "resetTime").name("Reset Time"); |
| 119 | + // Effect selector dropdown |
| 120 | + effectFolder.add(effectParams, 'effect', ['Magic', 'Spread', 'Unroll', 'Twister', 'Rain']) |
| 121 | + .name('Effect Type') |
| 122 | + .onChange(async () => { |
| 123 | + await loadSplatForEffect(effectParams.effect); |
| 124 | + }); |
| 125 | + |
| 126 | + // Animation controls |
| 127 | + const guiControls = { |
| 128 | + resetTime: () => { |
| 129 | + baseTime = 0; |
| 130 | + animateT.value = 0; |
| 131 | + } |
| 132 | + }; |
| 133 | + effectFolder.add(guiControls, 'resetTime').name('Reset Time'); |
| 134 | + effectFolder.open(); |
74 | 135 |
|
75 | | - // Modified shader: weather effects on splat mesh |
76 | | - splatMesh.objectModifier = dyno.dynoBlock( |
| 136 | + /** |
| 137 | + * Configures visual effects shader for the current splat mesh |
| 138 | + */ |
| 139 | + function setupSplatModifier() { |
| 140 | + splatMesh.objectModifier = dyno.dynoBlock( |
77 | 141 | { gsplat: dyno.Gsplat }, |
78 | 142 | { gsplat: dyno.Gsplat }, |
79 | 143 | ({ gsplat }) => { |
80 | 144 | const d = new dyno.Dyno({ |
81 | 145 | inTypes: { gsplat: dyno.Gsplat, t: "float", effectType: "int" }, |
82 | 146 | outTypes: { gsplat: dyno.Gsplat }, |
| 147 | + // GLSL utility functions for effects |
83 | 148 | globals: () => [ |
84 | 149 | dyno.unindent(` |
| 150 | + // Pseudo-random hash function |
85 | 151 | vec3 hash(vec3 p) { |
86 | | - return fract(sin(p*123.456)*123.456); |
| 152 | + p = fract(p * 0.3183099 + 0.1); |
| 153 | + p *= 17.0; |
| 154 | + return fract(vec3(p.x * p.y * p.z, p.x + p.y * p.z, p.x * p.y + p.z)); |
| 155 | + } |
| 156 | +
|
| 157 | + // 3D Perlin-style noise function |
| 158 | + vec3 noise(vec3 p) { |
| 159 | + vec3 i = floor(p); |
| 160 | + vec3 f = fract(p); |
| 161 | + f = f * f * (3.0 - 2.0 * f); |
| 162 | + |
| 163 | + vec3 n000 = hash(i + vec3(0,0,0)); |
| 164 | + vec3 n100 = hash(i + vec3(1,0,0)); |
| 165 | + vec3 n010 = hash(i + vec3(0,1,0)); |
| 166 | + vec3 n110 = hash(i + vec3(1,1,0)); |
| 167 | + vec3 n001 = hash(i + vec3(0,0,1)); |
| 168 | + vec3 n101 = hash(i + vec3(1,0,1)); |
| 169 | + vec3 n011 = hash(i + vec3(0,1,1)); |
| 170 | + vec3 n111 = hash(i + vec3(1,1,1)); |
| 171 | + |
| 172 | + vec3 x0 = mix(n000, n100, f.x); |
| 173 | + vec3 x1 = mix(n010, n110, f.x); |
| 174 | + vec3 x2 = mix(n001, n101, f.x); |
| 175 | + vec3 x3 = mix(n011, n111, f.x); |
| 176 | + |
| 177 | + vec3 y0 = mix(x0, x1, f.y); |
| 178 | + vec3 y1 = mix(x2, x3, f.y); |
| 179 | + |
| 180 | + return mix(y0, y1, f.z); |
87 | 181 | } |
88 | 182 |
|
| 183 | + // 2D rotation matrix |
89 | 184 | mat2 rot(float a) { |
90 | 185 | float s=sin(a),c=cos(a); |
91 | 186 | return mat2(c,-s,s,c); |
92 | 187 | } |
93 | | - // Function that generates a twister effect |
| 188 | + // Twister weather effect |
94 | 189 | vec4 twister(vec3 pos, vec3 scale, float t) { |
95 | | - float h=hash(pos).x+.1; |
96 | | - float s=smoothstep(0.,8.,t*t*.1-length(pos.xz)*2.); |
97 | | - if (length(scale)<.05) pos.y=mix(-10.,pos.y,pow(s,2.*h)); |
98 | | - pos.xz=mix(pos.xz*.5,pos.xz,pow(s,2.*h)); |
99 | | - pos.xz*=rot(t*.2+pos.y*20.*(1.-s)*exp(-1.*length(pos.xz))); |
100 | | - return vec4(pos,s*s*s*s); |
| 190 | + float h = hash(pos).x + .1; |
| 191 | + float s = smoothstep(0., 8., t*t*.1 - length(pos.xz)*2.+2.); |
| 192 | + if (length(scale) < .05) pos.y = mix(-10., pos.y, pow(s, 2.*h)); |
| 193 | + pos.xz = mix(pos.xz*.5, pos.xz, pow(s, 2.*h)); |
| 194 | + pos.xz *= rot(t*.2 + pos.y*20.*(1.-s)*exp(-1.*length(pos.xz))); |
| 195 | + return vec4(pos, s*s*s*s); |
101 | 196 | } |
| 197 | +
|
| 198 | + // Rain weather effect |
102 | 199 | vec4 rain(vec3 pos, vec3 scale, float t) { |
103 | | - vec3 h=hash(pos); |
104 | | - float s=pow(smoothstep(0.,5.,t*t*.1-length(pos.xz)*2.+1.),.5+h.x); |
105 | | - float y=pos.y; |
106 | | - pos.y=min(-10.+s*15.,pos.y); |
107 | | - pos.xz=mix(pos.xz*.3,pos.xz,s); |
108 | | - return vec4(pos,smoothstep(-10.,y,pos.y)); |
| 200 | + vec3 h = hash(pos); |
| 201 | + float s = pow(smoothstep(0., 5., t*t*.1 - length(pos.xz)*2. + 1.), .5 + h.x); |
| 202 | + float y = pos.y; |
| 203 | + pos.y = min(-10. + s*15., pos.y); |
| 204 | + pos.x += pos.y*.2; |
| 205 | + pos.xz = mix(pos.xz*.3, pos.xz, s); |
| 206 | + pos.xz *= rot(t*.3); |
| 207 | + return vec4(pos, smoothstep(-10., y, pos.y)); |
109 | 208 | } |
110 | 209 | `) |
111 | 210 | ], |
| 211 | + // Main effect shader logic |
112 | 212 | statements: ({ inputs, outputs }) => dyno.unindentLines(` |
113 | 213 | ${outputs.gsplat} = ${inputs.gsplat}; |
| 214 | + float t = ${inputs.t}; |
| 215 | + float s = smoothstep(0.,10.,t-4.5)*10.; |
| 216 | + vec3 scales = ${inputs.gsplat}.scales; |
114 | 217 | vec3 localPos = ${inputs.gsplat}.center; |
115 | | - vec3 scale = ${inputs.gsplat}.scales; |
| 218 | + float l = length(localPos.xz); |
116 | 219 | |
117 | | - vec4 effectResult; |
118 | 220 | if (${inputs.effectType} == 1) { |
119 | | - // Twister effect |
120 | | - effectResult = twister(localPos, scale, ${inputs.t}); |
121 | | - } else { |
122 | | - // Rain effect |
123 | | - effectResult = rain(localPos, scale, ${inputs.t}); |
| 221 | + // Magic Effect: Complex twister with noise and radial reveal |
| 222 | + float border = abs(s-l-.5); |
| 223 | + localPos *= 1.-.2*exp(-20.*border); |
| 224 | + vec3 finalScales = mix(scales,vec3(0.002),smoothstep(s-.5,s,l+.5)); |
| 225 | + ${outputs.gsplat}.center = localPos + .1*noise(localPos.xyz*2.+t*.5)*smoothstep(s-.5,s,l+.5); |
| 226 | + ${outputs.gsplat}.scales = finalScales; |
| 227 | + float at = atan(localPos.x,localPos.z)/3.1416; |
| 228 | + ${outputs.gsplat}.rgba *= step(at,t-3.1416); |
| 229 | + ${outputs.gsplat}.rgba += exp(-20.*border) + exp(-50.*abs(t-at-3.1416))*.5; |
| 230 | + |
| 231 | + } else if (${inputs.effectType} == 2) { |
| 232 | + // Spread Effect: Gentle radial emergence with scaling |
| 233 | + float tt = t*t*.4+.5; |
| 234 | + localPos.xz *= min(1.,.3+max(0.,tt*.05)); |
| 235 | + ${outputs.gsplat}.center = localPos; |
| 236 | + ${outputs.gsplat}.scales = max(mix(vec3(0.0),scales,min(tt-7.-l*2.5,1.)),mix(vec3(0.0),scales*.2,min(tt-1.-l*2.,1.))); |
| 237 | + ${outputs.gsplat}.rgba = mix(vec4(.3),${inputs.gsplat}.rgba,clamp(tt-l*2.5-3.,0.,1.)); |
| 238 | + |
| 239 | + } else if (${inputs.effectType} == 3) { |
| 240 | + // Unroll Effect: Rotating helix with vertical reveal |
| 241 | + localPos.xz *= rot((localPos.y*50.-20.)*exp(-t)); |
| 242 | + ${outputs.gsplat}.center = localPos * (1.-exp(-t)*2.); |
| 243 | + ${outputs.gsplat}.scales = mix(vec3(0.002),scales,smoothstep(.3,.7,t+localPos.y-2.)); |
| 244 | + ${outputs.gsplat}.rgba = ${inputs.gsplat}.rgba*step(0.,t*.5+localPos.y-.5); |
| 245 | + } else if (${inputs.effectType} == 4) { |
| 246 | + // Twister Effect: swirling weather reveal |
| 247 | + vec4 effectResult = twister(localPos, scales, t); |
| 248 | + ${outputs.gsplat}.center = effectResult.xyz; |
| 249 | + ${outputs.gsplat}.scales = mix(vec3(.005), scales, pow(effectResult.w, 30.)); |
| 250 | + ${outputs.gsplat}.rgba.a = pow(effectResult.w, .7) * smoothstep(30., 0., length(localPos.xz)); |
| 251 | + } else if (${inputs.effectType} == 5) { |
| 252 | + // Rain Effect: falling streaks |
| 253 | + vec4 effectResult = rain(localPos, scales, t); |
| 254 | + ${outputs.gsplat}.center = effectResult.xyz; |
| 255 | + ${outputs.gsplat}.scales = mix(vec3(.005), scales, pow(effectResult.w, 30.)); |
| 256 | + ${outputs.gsplat}.rgba.a = pow(effectResult.w, .7) * smoothstep(30., 0., length(localPos.xz)); |
124 | 257 | } |
125 | | - |
126 | | - ${outputs.gsplat}.center = effectResult.xyz; |
127 | | - ${outputs.gsplat}.scales = mix(vec3(.005),scale,pow(effectResult.w,30.)); |
128 | | - ${outputs.gsplat}.rgba.a = pow(effectResult.w,.7)*smoothstep(30.,0.,length(localPos.xz)); |
129 | 258 | `), |
130 | 259 | }); |
131 | 260 |
|
132 | | - // Convert effect name to integer for shader |
133 | | - const effectType = effectParams.effect === "Twister" ? 1 : 2; |
| 261 | + // Map effect names to shader integer constants |
| 262 | + const effectType = effectParams.effect === "Magic" ? 1 : |
| 263 | + effectParams.effect === "Spread" ? 2 : |
| 264 | + effectParams.effect === "Unroll" ? 3 : |
| 265 | + effectParams.effect === "Twister" ? 4 : 5; |
134 | 266 |
|
135 | 267 | gsplat = d.apply({ |
136 | 268 | gsplat, |
|
142 | 274 | } |
143 | 275 | ); |
144 | 276 |
|
145 | | - // Update generator |
146 | | - splatMesh.updateGenerator(); |
| 277 | + // Apply shader modifications to splat mesh |
| 278 | + splatMesh.updateGenerator(); |
| 279 | + } |
| 280 | + |
| 281 | + // Initialize with default effect |
| 282 | + await loadSplatForEffect(effectParams.effect); |
147 | 283 |
|
| 284 | + // Initialize camera controls and start render loop |
148 | 285 | const controls = new SparkControls({ canvas: renderer.domElement }); |
| 286 | + |
149 | 287 | renderer.setAnimationLoop(function animate(time) { |
| 288 | + // Update animation timing |
150 | 289 | if (splatLoaded) { |
151 | | - animateT.value = (time / 1000) - baseTime; |
| 290 | + baseTime += 1/60; |
| 291 | + animateT.value = baseTime; |
152 | 292 | } else { |
153 | 293 | animateT.value = 0; |
154 | 294 | } |
155 | | - splatMesh.updateVersion(); |
156 | | - splatMesh.position.set(0,-.7,-2.5); |
| 295 | + |
| 296 | + // Orbit camera only for non-weather effects |
| 297 | + cameraAngle += rotationSpeed * (1/60); |
| 298 | + if (effectParams.effect == "Twister" || effectParams.effect == "Rain") cameraAngle = 0; |
| 299 | + camera.position.x = Math.cos(cameraAngle) * cameraRadius; |
| 300 | + camera.position.z = Math.sin(cameraAngle) * cameraRadius; |
| 301 | + camera.position.y = cameraHeight; |
| 302 | + |
| 303 | + // Adjust camera target based on current effect |
| 304 | + if (effectParams.effect === "Spread") { |
| 305 | + camera.lookAt(0, 1, 0); |
| 306 | + } else { |
| 307 | + camera.lookAt(0, 0, 0); |
| 308 | + } |
| 309 | + |
| 310 | + // Update splat rendering if available |
| 311 | + if (splatMesh) { |
| 312 | + splatMesh.updateVersion(); |
| 313 | + } |
| 314 | + |
157 | 315 | controls.update(camera); |
158 | 316 | renderer.render(scene, camera); |
159 | 317 | }); |
|
0 commit comments