Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions examples/splat-flow/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Spark • Splat Flow</title>
<style>
html, body { margin: 0; height: 100%; width: 100%; background-color: black; }
#canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; outline: none; touch-action: none; }
</style>
</head>
<body>
<canvas id="canvas" tabindex="0"></canvas>
<script type="importmap">
{
"imports": {
"three": "/examples/js/vendor/three/build/three.module.js",
"lil-gui": "/examples/js/vendor/lil-gui/dist/lil-gui.esm.js",
"@sparkjsdev/spark": "/dist/spark.module.js"
}
}
</script>
<script type="module">
import { dyno, SparkRenderer, SplatMesh } from "@sparkjsdev/spark";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
import { GUI } from "lil-gui";

const splatFiles = ["woobles.spz", "dessert.spz", "robot-head.spz"];
const skyFile = "dali-env.glb";
const PARAMETERS = { speedMultiplier: 1.0, objectRotation: true, pause: false, fixedMinScale: false, waves: 0.5, cameraRotation: true };
const PAUSE_SECONDS = 2.0;

function getTransitionState(t, fadeInTime, fadeOutTime, period) {
const one = dyno.dynoFloat(1.0);
const pauseTime = dyno.dynoFloat(PAUSE_SECONDS);
const cycleTime = dyno.add(one, pauseTime);
const total = dyno.mul(period, cycleTime);
const wrapT = dyno.mod(t, total);
const pos = dyno.mod(wrapT, cycleTime);
const inPause = dyno.greaterThan(pos, one);
const normT = dyno.select(inPause, one, pos);
const fadeIn = dyno.and(
dyno.greaterThan(wrapT, dyno.mul(fadeInTime, cycleTime)),
dyno.lessThan(wrapT, dyno.mul(dyno.add(fadeInTime, one), cycleTime))
);
const fadeOut = dyno.and(
dyno.greaterThan(wrapT, dyno.mul(fadeOutTime, cycleTime)),
dyno.lessThan(wrapT, dyno.mul(dyno.add(fadeOutTime, one), cycleTime))
);
return { inTransition: dyno.or(fadeIn, fadeOut), isFadeIn: fadeIn, normT };
}

function contractionDyno(centerGLSL) {
return new dyno.Dyno({
inTypes: { gsplat: dyno.Gsplat, inTransition: "bool", fadeIn: "bool", t: "float", gt: "float", objectIndex: "int", fixedMinScale: "bool", waves: "float" },
outTypes: { gsplat: dyno.Gsplat },
globals: () => [ dyno.unindent(`
float hash13(vec3 p3) { p3 = fract(p3 * .1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); }
float hash11(float p) { p = fract(p * .1031); p += dot(p, p + 33.33); return fract(p * p); }
float fadeInOut(float t) { return abs(mix(-1., 1., t)); }
${centerGLSL}
float applyBrightness(float t) { return .5 + fadeInOut(t) * .5; }
vec3 applyCenter(vec3 center, float t, float id, int idx, float waves) {
int next = (idx + 1) % 3;
vec3 cNext = getCenterOfMass(next);
vec3 cOwn = getCenterOfMass(idx);
float f = fadeInOut(t);
float v = .5 + hash11(id) * 2.;
vec3 p = t < .5 ? mix(cNext, center, pow(f, v)) : mix(cOwn, center, pow(f, v));
return p + length(sin(p*2.5)) * waves * (1.-f)*smoothstep(0.5,0.,t) * 2.;
}
vec3 applyScale(vec3 s, float t, bool fixedMin) { return mix(fixedMin ? vec3(.02) : s * .2, s, pow(fadeInOut(t), 3.)); }
float applyOpacity(float t, float gt, int idx) {
float p = float(${PAUSE_SECONDS});
float c = 1.0 + p;
float tot = 3.0 * c;
float w = mod(gt + p + .5, tot);
int cur = int(floor(w / c));
return cur == idx ? .1+fadeInOut(t) : 0.0;
}
`) ],
statements: ({ inputs, outputs }) => dyno.unindentLines(`
${outputs.gsplat} = ${inputs.gsplat};
${outputs.gsplat}.center = applyCenter(${inputs.gsplat}.center, ${inputs.t}, float(${inputs.gsplat}.index), ${inputs.objectIndex}, ${inputs.waves});
${outputs.gsplat}.scales = applyScale(${inputs.gsplat}.scales, ${inputs.t}, ${inputs.fixedMinScale});
${outputs.gsplat}.rgba.a *= applyOpacity(${inputs.t}, ${inputs.gt}, ${inputs.objectIndex});
${outputs.gsplat}.rgba.rgb *= applyBrightness(${inputs.t});
`)
});
}

function getTransitionModifier(inTrans, fadeIn, t, idx, gt, centerGLSL, fixedMinScale, waves) {
const dyn = contractionDyno(centerGLSL);
return dyno.dynoBlock(
{ gsplat: dyno.Gsplat },
{ gsplat: dyno.Gsplat },
({ gsplat }) => ({ gsplat: dyn.apply({ gsplat, inTransition: inTrans, fadeIn, t, gt, objectIndex: idx, fixedMinScale, waves }).gsplat })
);
}

async function loadGLB(file, isEnv = false) {
const url = await getAssetFileURL(file);
const loader = new GLTFLoader();
const gltf = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
gltf.scene.traverse(child => {
if (child.isMesh && child.material) {
const mat = new THREE.MeshBasicMaterial({ color: child.material.color, map: child.material.map });
if (isEnv) {
mat.side = THREE.BackSide;
mat.map.mapping = THREE.EquirectangularReflectionMapping;
mat.map.colorSpace = THREE.LinearSRGBColorSpace;
mat.map.needsUpdate = true;
}
child.material = mat;
}
});
return gltf.scene;
}

const canvas = document.getElementById("canvas");
const renderer = new THREE.WebGLRenderer({ canvas, antialias: false });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
renderer.setClearColor(0x000000, 1);

const scene = new THREE.Scene();
const spark = new SparkRenderer({ renderer });
scene.add(spark);

const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.01, 1000);
camera.position.set(0, 5, 8);
camera.lookAt(0, 0, 0);
scene.add(camera);

window.addEventListener("resize", () => {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
});

const time = dyno.dynoFloat(0.0);

async function loadAssets() {
const env = await loadGLB(skyFile, true);
scene.add(env);

const meshes = [];
const period = dyno.dynoFloat(splatFiles.length);
const positions = [
new THREE.Vector3(-5, -2.2, -3),
new THREE.Vector3(5, -2.5, 0),
new THREE.Vector3(0, 1.5, 2)
];

for (let i = 0; i < splatFiles.length; i++) {
const url = await getAssetFileURL(splatFiles[i]);
const m = new SplatMesh({ url });
await m.initialized;
m.position.copy(positions[i]);
m.rotateX(Math.PI);
scene.add(m);
meshes.push(m);
}

const centers = meshes.map(m => {
const box = new THREE.Box3();
m.packedSplats.forEachSplat((_, c) => {
box.expandByPoint(c);
});
const localCenter = box.getCenter(new THREE.Vector3());
localCenter.y = -localCenter.y;
return localCenter.add(m.position);
});

const centerGLSL = `
vec3 getCenterOfMass(int idx) {
if (idx == 0) return vec3(${centers[0].x}, ${centers[0].y}, ${centers[0].z});
if (idx == 1) return vec3(${centers[1].x}, ${centers[1].y}, ${centers[1].z});
if (idx == 2) return vec3(${centers[2].x}, ${centers[2].y}, ${centers[2].z});
return vec3(0.0);
}
`;

meshes.forEach((m, i) => {
const { inTransition, isFadeIn, normT } = getTransitionState(time, dyno.dynoFloat(i), dyno.dynoFloat((i+1)%splatFiles.length), period);
m.worldModifier = getTransitionModifier(inTransition, isFadeIn, normT, dyno.dynoInt(i), time, centerGLSL, dyno.dynoBool(PARAMETERS.fixedMinScale), dyno.dynoFloat(PARAMETERS.waves));
m.updateGenerator();
});

return { splatMeshes: meshes, centerGLSL };
}

const { splatMeshes, centerGLSL } = await loadAssets();
const gui = new GUI();
gui.add(PARAMETERS, "speedMultiplier", 0.25, 4.0, 0.01);
gui.add(PARAMETERS, "objectRotation");
gui.add(PARAMETERS, "pause");
gui.add(PARAMETERS, "cameraRotation");
gui.add(PARAMETERS, "fixedMinScale").onChange(() => {
splatMeshes.forEach((m, i) => {
const { inTransition, isFadeIn, normT } = getTransitionState(time, dyno.dynoFloat(i), dyno.dynoFloat((i+1)%splatFiles.length), dyno.dynoFloat(splatFiles.length));
m.worldModifier = getTransitionModifier(inTransition, isFadeIn, normT, dyno.dynoInt(i), time, centerGLSL, dyno.dynoBool(PARAMETERS.fixedMinScale), dyno.dynoFloat(PARAMETERS.waves));
m.updateGenerator();
});
});
gui.add(PARAMETERS, "waves", 0, 1, 0.01).onChange(() => {
splatMeshes.forEach((m, i) => {
const { inTransition, isFadeIn, normT } = getTransitionState(time, dyno.dynoFloat(i), dyno.dynoFloat((i+1)%splatFiles.length), dyno.dynoFloat(splatFiles.length));
m.worldModifier = getTransitionModifier(inTransition, isFadeIn, normT, dyno.dynoInt(i), time, centerGLSL, dyno.dynoBool(PARAMETERS.fixedMinScale), dyno.dynoFloat(PARAMETERS.waves));
m.updateGenerator();
});
});

let last = 0;
renderer.setAnimationLoop(rt => {
const t = rt * 0.0005;
const dt = t - last;
last = t;
updateCamera(splatMeshes);
renderer.render(scene, camera);
if (!PARAMETERS.pause) {
time.value += dt * PARAMETERS.speedMultiplier;
if (PARAMETERS.objectRotation) splatMeshes.forEach(m => m.rotation.y += dt * PARAMETERS.speedMultiplier * 2);
}
});

function updateCamera(meshes) {
const cTime = 1 + PAUSE_SECONDS;
const tot = meshes.length * cTime;
const w = (time.value + PAUSE_SECONDS) % tot;
const cur = Math.floor(w / cTime);
const nxt = (cur + 1) % meshes.length;
const tr = w / cTime - cur;
const s = tr * tr * (3 - 2 * tr);
const tgt = new THREE.Vector3().lerpVectors(meshes[cur].position, meshes[nxt].position, s**5);

const radius = 4 + (Math.abs(s-0.5)**2)*20;
let x, z;

if (PARAMETERS.cameraRotation) {
const angle = -time.value * 0.5; // Velocidad de rotación
x = Math.cos(angle) * radius;
z = Math.sin(angle) * radius;
} else {
x = 0;
z = -radius;
}

const frm = tgt.clone().add(new THREE.Vector3(x, 2, z));
camera.position.copy(frm);
camera.lookAt(tgt);
}
</script>
</body>
</html>
Loading