flow.ts
examples/spark/splat-transitions/effects/flow.ts · raw · open the example · all examples · site home
Exports: init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
// Spark's splat-transitions Flow effect (the splat-flow example inside the
// transitions page): three scans take turns flowing out of the previous
// one's center of mass, with the camera flying from one to the next
import type GUI from "lil-gui";
import * as THREE from "three";
import { Splats, TSL, type TslNode } from "@sparkjsdev/radix";
import { loadGlb } from "../../js/assets.js";
import type { Effect, EffectContext } from "../main.js";
const { abs, float, floor, fract, length, mix, pow, select, sin, smoothstep, uniform, uniformArray, vec3 } = TSL;
const PAUSE_SECONDS = 2;
const CYCLE = 1 + PAUSE_SECONDS;
const hash11 = (x: TslNode): TslNode => {
const p = fract(x.mul(0.1031)).toVar();
const q = p.add(p.mul(p.add(33.33)));
return fract(q.mul(q));
};
export async function init({ camera, group, scans, loadScan }: EffectContext): Promise<Effect> {
const params = { speedMultiplier: 0.5, objectRotation: true, pause: false, fixedMinScale: false, cameraRotation: true };
const time = uniform(0);
const fixedMinScale = uniform(0);
const waves = uniform(0.5);
// Per object, set by bind
const nextIndex = uniform(0, "int");
const files = ["woobles.spz", "dessert.spz", "robot-head.spz"];
const total = CYCLE * files.length;
const centersOfMass = files.map(() => new THREE.Vector3());
const centers = uniformArray(centersOfMass, "vec3");
const models = await scans(files, (s, objectIndex) => {
const wrapT = time.sub(floor(time.div(total)).mul(total));
const pos = wrapT.sub(floor(wrapT.div(CYCLE)).mul(CYCLE));
const t = select(pos.greaterThan(1), float(1), pos).toVar();
const f = abs(t.mul(2).sub(1)).toVar();
const center = s.get("center").toVar();
const k = pow(f, hash11(float(s.get("index"))).mul(2).add(0.5));
const p = select(t.lessThan(0.5), mix(centers.element(nextIndex), center, k), mix(centers.element(objectIndex), center, k)).toVar();
s.get("center").assign(p.add(length(sin(p.mul(2.5))).mul(waves).mul(f.oneMinus()).mul(smoothstep(0, 0.5, t).oneMinus()).mul(2)));
const scales = s.get("scales").toVar();
s.get("scales").assign(mix(select(fixedMinScale.greaterThan(0.5), vec3(0.02), scales.mul(0.2)), scales, pow(f, 3)));
const w = time.add(PAUSE_SECONDS + 0.5).toVar();
const current = floor(w.sub(floor(w.div(total)).mul(total)).div(CYCLE));
s.get("opacity").assign(s.get("opacity").mul(select(current.equal(float(objectIndex)), f.add(0.1), float(0))));
s.get("rgb").assign(s.get("rgb").mul(f.mul(0.5).add(0.5)));
return s;
}, (data) => (nextIndex.value = (data.index + 1) % files.length));
// Each scan's place, and the center of its bounds there, as Spark
// computes it (only y flipped)
const positions = [new THREE.Vector3(-5, -2.2, -3), new THREE.Vector3(5, -2.5, 0), new THREE.Vector3(0, 1.5, 2)];
for (const [i, model] of models.entries()) {
model.position.copy(positions[i]);
const { data } = await loadScan(files[i]);
const { bundle } = Splats.get(data);
const box = new THREE.Box3();
const c = new THREE.Vector3();
for (let j = 0; j < data.count; j += 1) {
box.expandByPoint(Splats.loadCenter(bundle, j, c));
}
box.getCenter(c);
centersOfMass[i].set(c.x, -c.y, c.z).add(model.position);
}
group.add(await loadGlb("dali-env.glb", { unlit: "env" }));
// From one scan to the next, circling it
function updateCamera() {
const w = (time.value + PAUSE_SECONDS) % total;
const cur = Math.floor(w / CYCLE);
const tr = w / CYCLE - cur;
const s = tr * tr * (3 - 2 * tr);
const target = new THREE.Vector3().lerpVectors(models[cur].position, models[(cur + 1) % models.length].position, s ** 5);
const radius = 4 + Math.abs(s - 0.5) ** 2 * 20;
const angle = -time.value * 0.5;
const offset = params.cameraRotation ? new THREE.Vector3(Math.cos(angle) * radius, 2, Math.sin(angle) * radius) : new THREE.Vector3(0, 2, -radius);
camera.position.copy(target).add(offset);
camera.lookAt(target);
}
return {
update(dt) {
if (!params.pause) {
time.value += dt * params.speedMultiplier;
if (params.objectRotation) {
models.forEach((model) => (model.rotation.y += dt * params.speedMultiplier * 2));
}
}
updateCamera();
},
gui(folder: GUI) {
folder.add(params, "speedMultiplier", 0, 1, 0.01);
folder.add(params, "objectRotation");
folder.add(params, "pause");
folder.add(params, "cameraRotation");
folder.add(params, "fixedMinScale").onChange((value: boolean) => (fixedMinScale.value = value ? 1 : 0));
folder.add(waves, "value", 0, 1, 0.01).name("waves");
},
};
}