Skip to content

Commit beb0e64

Browse files
kali-shadedmarcos
andauthored
Splat reveal effects (#153)
* Add splat reveal effects (weather) * Update example title * Added 3 new reveal effects to examples --------- Co-authored-by: Diego Marcos <diego.marcos@gmail.com>
1 parent 0edfc8d commit beb0e64

1 file changed

Lines changed: 220 additions & 62 deletions

File tree

examples/splat-reveal-effects/index.html

Lines changed: 220 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -33,104 +33,236 @@
3333

3434
const scene = new THREE.Scene();
3535
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+
3641
const renderer = new THREE.WebGLRenderer();
3742
renderer.setSize(window.innerWidth, window.innerHeight);
3843
document.body.appendChild(renderer.domElement)
3944

45+
// Animation timing variables
4046
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;
4355

44-
// Effect parameters
56+
// Available visual effects configuration
4557
const effectParams = {
46-
effect: "Twister" // "Twister", "Rain"
58+
effect: "Magic"
4759
};
4860

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');
68118

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();
74135

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(
77141
{ gsplat: dyno.Gsplat },
78142
{ gsplat: dyno.Gsplat },
79143
({ gsplat }) => {
80144
const d = new dyno.Dyno({
81145
inTypes: { gsplat: dyno.Gsplat, t: "float", effectType: "int" },
82146
outTypes: { gsplat: dyno.Gsplat },
147+
// GLSL utility functions for effects
83148
globals: () => [
84149
dyno.unindent(`
150+
// Pseudo-random hash function
85151
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);
87181
}
88182
183+
// 2D rotation matrix
89184
mat2 rot(float a) {
90185
float s=sin(a),c=cos(a);
91186
return mat2(c,-s,s,c);
92187
}
93-
// Function that generates a twister effect
188+
// Twister weather effect
94189
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);
101196
}
197+
198+
// Rain weather effect
102199
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));
109208
}
110209
`)
111210
],
211+
// Main effect shader logic
112212
statements: ({ inputs, outputs }) => dyno.unindentLines(`
113213
${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;
114217
vec3 localPos = ${inputs.gsplat}.center;
115-
vec3 scale = ${inputs.gsplat}.scales;
218+
float l = length(localPos.xz);
116219
117-
vec4 effectResult;
118220
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));
124257
}
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));
129258
`),
130259
});
131260

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;
134266

135267
gsplat = d.apply({
136268
gsplat,
@@ -142,18 +274,44 @@
142274
}
143275
);
144276

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);
147283

284+
// Initialize camera controls and start render loop
148285
const controls = new SparkControls({ canvas: renderer.domElement });
286+
149287
renderer.setAnimationLoop(function animate(time) {
288+
// Update animation timing
150289
if (splatLoaded) {
151-
animateT.value = (time / 1000) - baseTime;
290+
baseTime += 1/60;
291+
animateT.value = baseTime;
152292
} else {
153293
animateT.value = 0;
154294
}
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+
157315
controls.update(camera);
158316
renderer.render(scene, camera);
159317
});

0 commit comments

Comments
 (0)