Skip to content

Commit c1a335e

Browse files
authored
Added particle flow transition to examples (#131)
* Added particle flow transition to examples * Added particle flow transition to examples
1 parent 3a63eae commit c1a335e

1 file changed

Lines changed: 259 additions & 0 deletions

File tree

examples/splat-flow/index.html

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6+
<title>Spark • Splat Flow</title>
7+
<style>
8+
html, body { margin: 0; height: 100%; width: 100%; background-color: black; }
9+
#canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; outline: none; touch-action: none; }
10+
</style>
11+
</head>
12+
<body>
13+
<canvas id="canvas" tabindex="0"></canvas>
14+
<script type="importmap">
15+
{
16+
"imports": {
17+
"three": "/examples/js/vendor/three/build/three.module.js",
18+
"lil-gui": "/examples/js/vendor/lil-gui/dist/lil-gui.esm.js",
19+
"@sparkjsdev/spark": "/dist/spark.module.js"
20+
}
21+
}
22+
</script>
23+
<script type="module">
24+
import { dyno, SparkRenderer, SplatMesh } from "@sparkjsdev/spark";
25+
import * as THREE from "three";
26+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
27+
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
28+
import { GUI } from "lil-gui";
29+
30+
const splatFiles = ["woobles.spz", "dessert.spz", "robot-head.spz"];
31+
const skyFile = "dali-env.glb";
32+
const PARAMETERS = { speedMultiplier: 1.0, objectRotation: true, pause: false, fixedMinScale: false, waves: 0.5, cameraRotation: true };
33+
const PAUSE_SECONDS = 2.0;
34+
35+
function getTransitionState(t, fadeInTime, fadeOutTime, period) {
36+
const one = dyno.dynoFloat(1.0);
37+
const pauseTime = dyno.dynoFloat(PAUSE_SECONDS);
38+
const cycleTime = dyno.add(one, pauseTime);
39+
const total = dyno.mul(period, cycleTime);
40+
const wrapT = dyno.mod(t, total);
41+
const pos = dyno.mod(wrapT, cycleTime);
42+
const inPause = dyno.greaterThan(pos, one);
43+
const normT = dyno.select(inPause, one, pos);
44+
const fadeIn = dyno.and(
45+
dyno.greaterThan(wrapT, dyno.mul(fadeInTime, cycleTime)),
46+
dyno.lessThan(wrapT, dyno.mul(dyno.add(fadeInTime, one), cycleTime))
47+
);
48+
const fadeOut = dyno.and(
49+
dyno.greaterThan(wrapT, dyno.mul(fadeOutTime, cycleTime)),
50+
dyno.lessThan(wrapT, dyno.mul(dyno.add(fadeOutTime, one), cycleTime))
51+
);
52+
return { inTransition: dyno.or(fadeIn, fadeOut), isFadeIn: fadeIn, normT };
53+
}
54+
55+
function contractionDyno(centerGLSL) {
56+
return new dyno.Dyno({
57+
inTypes: { gsplat: dyno.Gsplat, inTransition: "bool", fadeIn: "bool", t: "float", gt: "float", objectIndex: "int", fixedMinScale: "bool", waves: "float" },
58+
outTypes: { gsplat: dyno.Gsplat },
59+
globals: () => [ dyno.unindent(`
60+
float hash13(vec3 p3) { p3 = fract(p3 * .1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); }
61+
float hash11(float p) { p = fract(p * .1031); p += dot(p, p + 33.33); return fract(p * p); }
62+
float fadeInOut(float t) { return abs(mix(-1., 1., t)); }
63+
${centerGLSL}
64+
float applyBrightness(float t) { return .5 + fadeInOut(t) * .5; }
65+
vec3 applyCenter(vec3 center, float t, float id, int idx, float waves) {
66+
int next = (idx + 1) % 3;
67+
vec3 cNext = getCenterOfMass(next);
68+
vec3 cOwn = getCenterOfMass(idx);
69+
float f = fadeInOut(t);
70+
float v = .5 + hash11(id) * 2.;
71+
vec3 p = t < .5 ? mix(cNext, center, pow(f, v)) : mix(cOwn, center, pow(f, v));
72+
return p + length(sin(p*2.5)) * waves * (1.-f)*smoothstep(0.5,0.,t) * 2.;
73+
}
74+
vec3 applyScale(vec3 s, float t, bool fixedMin) { return mix(fixedMin ? vec3(.02) : s * .2, s, pow(fadeInOut(t), 3.)); }
75+
float applyOpacity(float t, float gt, int idx) {
76+
float p = float(${PAUSE_SECONDS});
77+
float c = 1.0 + p;
78+
float tot = 3.0 * c;
79+
float w = mod(gt + p + .5, tot);
80+
int cur = int(floor(w / c));
81+
return cur == idx ? .1+fadeInOut(t) : 0.0;
82+
}
83+
`) ],
84+
statements: ({ inputs, outputs }) => dyno.unindentLines(`
85+
${outputs.gsplat} = ${inputs.gsplat};
86+
${outputs.gsplat}.center = applyCenter(${inputs.gsplat}.center, ${inputs.t}, float(${inputs.gsplat}.index), ${inputs.objectIndex}, ${inputs.waves});
87+
${outputs.gsplat}.scales = applyScale(${inputs.gsplat}.scales, ${inputs.t}, ${inputs.fixedMinScale});
88+
${outputs.gsplat}.rgba.a *= applyOpacity(${inputs.t}, ${inputs.gt}, ${inputs.objectIndex});
89+
${outputs.gsplat}.rgba.rgb *= applyBrightness(${inputs.t});
90+
`)
91+
});
92+
}
93+
94+
function getTransitionModifier(inTrans, fadeIn, t, idx, gt, centerGLSL, fixedMinScale, waves) {
95+
const dyn = contractionDyno(centerGLSL);
96+
return dyno.dynoBlock(
97+
{ gsplat: dyno.Gsplat },
98+
{ gsplat: dyno.Gsplat },
99+
({ gsplat }) => ({ gsplat: dyn.apply({ gsplat, inTransition: inTrans, fadeIn, t, gt, objectIndex: idx, fixedMinScale, waves }).gsplat })
100+
);
101+
}
102+
103+
async function loadGLB(file, isEnv = false) {
104+
const url = await getAssetFileURL(file);
105+
const loader = new GLTFLoader();
106+
const gltf = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
107+
gltf.scene.traverse(child => {
108+
if (child.isMesh && child.material) {
109+
const mat = new THREE.MeshBasicMaterial({ color: child.material.color, map: child.material.map });
110+
if (isEnv) {
111+
mat.side = THREE.BackSide;
112+
mat.map.mapping = THREE.EquirectangularReflectionMapping;
113+
mat.map.colorSpace = THREE.LinearSRGBColorSpace;
114+
mat.map.needsUpdate = true;
115+
}
116+
child.material = mat;
117+
}
118+
});
119+
return gltf.scene;
120+
}
121+
122+
const canvas = document.getElementById("canvas");
123+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: false });
124+
renderer.setPixelRatio(window.devicePixelRatio);
125+
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
126+
renderer.setClearColor(0x000000, 1);
127+
128+
const scene = new THREE.Scene();
129+
const spark = new SparkRenderer({ renderer });
130+
scene.add(spark);
131+
132+
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.01, 1000);
133+
camera.position.set(0, 5, 8);
134+
camera.lookAt(0, 0, 0);
135+
scene.add(camera);
136+
137+
window.addEventListener("resize", () => {
138+
const w = canvas.clientWidth;
139+
const h = canvas.clientHeight;
140+
renderer.setSize(w, h, false);
141+
camera.aspect = w / h;
142+
camera.updateProjectionMatrix();
143+
});
144+
145+
const time = dyno.dynoFloat(0.0);
146+
147+
async function loadAssets() {
148+
const env = await loadGLB(skyFile, true);
149+
scene.add(env);
150+
151+
const meshes = [];
152+
const period = dyno.dynoFloat(splatFiles.length);
153+
const positions = [
154+
new THREE.Vector3(-5, -2.2, -3),
155+
new THREE.Vector3(5, -2.5, 0),
156+
new THREE.Vector3(0, 1.5, 2)
157+
];
158+
159+
for (let i = 0; i < splatFiles.length; i++) {
160+
const url = await getAssetFileURL(splatFiles[i]);
161+
const m = new SplatMesh({ url });
162+
await m.initialized;
163+
m.position.copy(positions[i]);
164+
m.rotateX(Math.PI);
165+
scene.add(m);
166+
meshes.push(m);
167+
}
168+
169+
const centers = meshes.map(m => {
170+
const box = new THREE.Box3();
171+
m.packedSplats.forEachSplat((_, c) => {
172+
box.expandByPoint(c);
173+
});
174+
const localCenter = box.getCenter(new THREE.Vector3());
175+
localCenter.y = -localCenter.y;
176+
return localCenter.add(m.position);
177+
});
178+
179+
const centerGLSL = `
180+
vec3 getCenterOfMass(int idx) {
181+
if (idx == 0) return vec3(${centers[0].x}, ${centers[0].y}, ${centers[0].z});
182+
if (idx == 1) return vec3(${centers[1].x}, ${centers[1].y}, ${centers[1].z});
183+
if (idx == 2) return vec3(${centers[2].x}, ${centers[2].y}, ${centers[2].z});
184+
return vec3(0.0);
185+
}
186+
`;
187+
188+
meshes.forEach((m, i) => {
189+
const { inTransition, isFadeIn, normT } = getTransitionState(time, dyno.dynoFloat(i), dyno.dynoFloat((i+1)%splatFiles.length), period);
190+
m.worldModifier = getTransitionModifier(inTransition, isFadeIn, normT, dyno.dynoInt(i), time, centerGLSL, dyno.dynoBool(PARAMETERS.fixedMinScale), dyno.dynoFloat(PARAMETERS.waves));
191+
m.updateGenerator();
192+
});
193+
194+
return { splatMeshes: meshes, centerGLSL };
195+
}
196+
197+
const { splatMeshes, centerGLSL } = await loadAssets();
198+
const gui = new GUI();
199+
gui.add(PARAMETERS, "speedMultiplier", 0.25, 4.0, 0.01);
200+
gui.add(PARAMETERS, "objectRotation");
201+
gui.add(PARAMETERS, "pause");
202+
gui.add(PARAMETERS, "cameraRotation");
203+
gui.add(PARAMETERS, "fixedMinScale").onChange(() => {
204+
splatMeshes.forEach((m, i) => {
205+
const { inTransition, isFadeIn, normT } = getTransitionState(time, dyno.dynoFloat(i), dyno.dynoFloat((i+1)%splatFiles.length), dyno.dynoFloat(splatFiles.length));
206+
m.worldModifier = getTransitionModifier(inTransition, isFadeIn, normT, dyno.dynoInt(i), time, centerGLSL, dyno.dynoBool(PARAMETERS.fixedMinScale), dyno.dynoFloat(PARAMETERS.waves));
207+
m.updateGenerator();
208+
});
209+
});
210+
gui.add(PARAMETERS, "waves", 0, 1, 0.01).onChange(() => {
211+
splatMeshes.forEach((m, i) => {
212+
const { inTransition, isFadeIn, normT } = getTransitionState(time, dyno.dynoFloat(i), dyno.dynoFloat((i+1)%splatFiles.length), dyno.dynoFloat(splatFiles.length));
213+
m.worldModifier = getTransitionModifier(inTransition, isFadeIn, normT, dyno.dynoInt(i), time, centerGLSL, dyno.dynoBool(PARAMETERS.fixedMinScale), dyno.dynoFloat(PARAMETERS.waves));
214+
m.updateGenerator();
215+
});
216+
});
217+
218+
let last = 0;
219+
renderer.setAnimationLoop(rt => {
220+
const t = rt * 0.0005;
221+
const dt = t - last;
222+
last = t;
223+
updateCamera(splatMeshes);
224+
renderer.render(scene, camera);
225+
if (!PARAMETERS.pause) {
226+
time.value += dt * PARAMETERS.speedMultiplier;
227+
if (PARAMETERS.objectRotation) splatMeshes.forEach(m => m.rotation.y += dt * PARAMETERS.speedMultiplier * 2);
228+
}
229+
});
230+
231+
function updateCamera(meshes) {
232+
const cTime = 1 + PAUSE_SECONDS;
233+
const tot = meshes.length * cTime;
234+
const w = (time.value + PAUSE_SECONDS) % tot;
235+
const cur = Math.floor(w / cTime);
236+
const nxt = (cur + 1) % meshes.length;
237+
const tr = w / cTime - cur;
238+
const s = tr * tr * (3 - 2 * tr);
239+
const tgt = new THREE.Vector3().lerpVectors(meshes[cur].position, meshes[nxt].position, s**5);
240+
241+
const radius = 4 + (Math.abs(s-0.5)**2)*20;
242+
let x, z;
243+
244+
if (PARAMETERS.cameraRotation) {
245+
const angle = -time.value * 0.5; // Velocidad de rotación
246+
x = Math.cos(angle) * radius;
247+
z = Math.sin(angle) * radius;
248+
} else {
249+
x = 0;
250+
z = -radius;
251+
}
252+
253+
const frm = tgt.clone().add(new THREE.Vector3(x, 2, z));
254+
camera.position.copy(frm);
255+
camera.lookAt(tgt);
256+
}
257+
</script>
258+
</body>
259+
</html>

0 commit comments

Comments
 (0)