import { SplatMesh, dyno } from "@sparkjsdev/spark"; import * as THREE from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { getAssetFileURL } from "/examples/js/get-asset-url.js"; export async function init({ THREE: _THREE, scene, camera, renderer, spark }) { const group = new THREE.Group(); scene.add(group); let disposed = false; const PARAMETERS = { speedMultiplier: 0.5, 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; if (mat.map) { mat.map.mapping = THREE.EquirectangularReflectionMapping; mat.map.colorSpace = THREE.LinearSRGBColorSpace; mat.map.needsUpdate = true; } } child.material = mat; } }); return gltf.scene; } const time = dyno.dynoFloat(0.0); const splatFiles = ["woobles.spz", "dessert.spz", "robot-head.spz"]; const skyFile = "dali-env.glb"; const env = await loadGLB(skyFile, true); if (!disposed) group.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); if (!disposed) group.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(); }); function updateCamera() { 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; let z; if (PARAMETERS.cameraRotation) { const angle = -time.value * 0.5; 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); } function update(dt, _t) { if (!PARAMETERS.pause) { time.value += dt * PARAMETERS.speedMultiplier; if (PARAMETERS.objectRotation) { for (const m of meshes) { m.rotation.y += dt * PARAMETERS.speedMultiplier * 2; } } } updateCamera(); } function setupGUI(folder) { folder.add(PARAMETERS, "speedMultiplier", 0, 1, 0.01); folder.add(PARAMETERS, "objectRotation"); folder.add(PARAMETERS, "pause"); folder.add(PARAMETERS, "cameraRotation"); folder.add(PARAMETERS, "fixedMinScale").onChange(() => { meshes.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(); }); }); folder.add(PARAMETERS, "waves", 0, 1, 0.01).onChange(() => { meshes.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(); }); }); return folder; } function dispose() { disposed = true; scene.remove(group); } return { group, update, dispose, setupGUI }; }