mirror of
https://github.com/storytold/spark.git
synced 2026-10-09 00:09:53 +00:00
229 lines
6.4 KiB
JavaScript
229 lines
6.4 KiB
JavaScript
import { SplatMesh, dyno } from "@sparkjsdev/spark";
|
|
import * as THREE from "three";
|
|
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;
|
|
|
|
// Camera baseline for Morph effect
|
|
camera.position.set(0, 2.2, 6.5);
|
|
camera.lookAt(0, 1.0, 0);
|
|
|
|
const PARAMETERS = {
|
|
speedMultiplier: 1.0,
|
|
rotation: true,
|
|
pause: false,
|
|
staySeconds: 1.5,
|
|
transitionSeconds: 2.0,
|
|
randomRadius: 1.3,
|
|
};
|
|
|
|
const time = dyno.dynoFloat(0.0);
|
|
|
|
// Tres splats de comida
|
|
const splatFiles = [
|
|
"branzino-amarin.spz",
|
|
"pad-thai.spz",
|
|
"primerib-tamos.spz",
|
|
];
|
|
|
|
function morphDyno() {
|
|
return new dyno.Dyno({
|
|
inTypes: {
|
|
gsplat: dyno.Gsplat,
|
|
gt: "float",
|
|
objectIndex: "int",
|
|
stay: "float",
|
|
trans: "float",
|
|
numObjects: "int",
|
|
randomRadius: "float",
|
|
offsetY: "float",
|
|
},
|
|
outTypes: { gsplat: dyno.Gsplat },
|
|
globals: () => [
|
|
dyno.unindent(`
|
|
vec3 hash3(int n) {
|
|
float x = float(n);
|
|
return fract(sin(vec3(x, x + 1.0, x + 2.0)) * 43758.5453123);
|
|
}
|
|
float ease(float x) { return x*x*(3.0 - 2.0*x); }
|
|
vec3 randPos(int splatIndex, float radius) {
|
|
// Uniform disk sampling on XZ plane
|
|
vec3 h = hash3(splatIndex);
|
|
float theta = 6.28318530718 * h.x;
|
|
float r = radius * sqrt(h.y);
|
|
return vec3(r * cos(theta), 0.0, r * sin(theta));
|
|
}
|
|
`),
|
|
],
|
|
statements: ({ inputs, outputs }) =>
|
|
dyno.unindentLines(`
|
|
${outputs.gsplat} = ${inputs.gsplat};
|
|
float stay = ${inputs.stay};
|
|
float trans = ${inputs.trans};
|
|
float cycle = stay + trans;
|
|
float tot = float(${inputs.numObjects}) * cycle;
|
|
float w = mod(${inputs.gt}, tot);
|
|
int cur = int(floor(w / cycle));
|
|
int nxt = (cur + 1) % ${inputs.numObjects};
|
|
float local = mod(w, cycle);
|
|
bool inTrans = local > stay;
|
|
float uPhase = inTrans ? clamp((local - stay) / trans, 0.0, 1.0) : 0.0;
|
|
bool phaseScatter = uPhase < 0.5;
|
|
float s = phaseScatter ? (uPhase / 0.5) : ((uPhase - 0.5) / 0.5);
|
|
int idx = ${inputs.objectIndex};
|
|
|
|
vec3 rp = randPos(int(${inputs.gsplat}.index), ${inputs.randomRadius});
|
|
rp.y -= ${inputs.offsetY};
|
|
vec3 rpMid = mix(${inputs.gsplat}.center, rp, 0.7);
|
|
|
|
float alpha = 0.0;
|
|
vec3 pos = ${inputs.gsplat}.center;
|
|
vec3 origScale = ${inputs.gsplat}.scales;
|
|
vec3 small = ${inputs.gsplat}.scales*.2;
|
|
if (idx == cur) {
|
|
if (!inTrans) {
|
|
alpha = 1.0;
|
|
pos = ${inputs.gsplat}.center;
|
|
${outputs.gsplat}.scales = origScale;
|
|
} else if (phaseScatter) {
|
|
alpha = 1.0 - ease(s)*.5;
|
|
pos = mix(${inputs.gsplat}.center, rpMid, ease(s));
|
|
${outputs.gsplat}.scales = mix(origScale, small, ease(s));
|
|
} else {
|
|
alpha = 0.0;
|
|
pos = rpMid;
|
|
${outputs.gsplat}.scales = small;
|
|
}
|
|
} else if (idx == nxt) {
|
|
if (!inTrans) {
|
|
alpha = 0.0;
|
|
pos = rpMid;
|
|
${outputs.gsplat}.scales = small;
|
|
} else if (phaseScatter) {
|
|
alpha = 0.0;
|
|
pos = rpMid;
|
|
${outputs.gsplat}.scales = small;
|
|
} else {
|
|
alpha = max(ease(s), 0.5);
|
|
pos = mix(rpMid, ${inputs.gsplat}.center, ease(s));
|
|
${outputs.gsplat}.scales = mix(small, origScale, ease(s));
|
|
}
|
|
} else {
|
|
alpha = 0.0;
|
|
pos = ${inputs.gsplat}.center;
|
|
${outputs.gsplat}.scales = origScale;
|
|
}
|
|
pos.y += ${inputs.offsetY};
|
|
${outputs.gsplat}.center = pos;
|
|
${outputs.gsplat}.rgba.a = ${inputs.gsplat}.rgba.a * alpha;
|
|
`),
|
|
});
|
|
}
|
|
|
|
function getMorphModifier(
|
|
gt,
|
|
idx,
|
|
stay,
|
|
trans,
|
|
numObjects,
|
|
randomRadius,
|
|
offsetY,
|
|
) {
|
|
const dyn = morphDyno();
|
|
return dyno.dynoBlock(
|
|
{ gsplat: dyno.Gsplat },
|
|
{ gsplat: dyno.Gsplat },
|
|
({ gsplat }) => ({
|
|
gsplat: dyn.apply({
|
|
gsplat,
|
|
gt,
|
|
objectIndex: idx,
|
|
stay,
|
|
trans,
|
|
numObjects,
|
|
randomRadius,
|
|
offsetY,
|
|
}).gsplat,
|
|
}),
|
|
);
|
|
}
|
|
|
|
const meshes = [];
|
|
const numObjectsDyn = dyno.dynoInt(splatFiles.length);
|
|
const stayDyn = dyno.dynoFloat(PARAMETERS.staySeconds);
|
|
const transDyn = dyno.dynoFloat(PARAMETERS.transitionSeconds);
|
|
const radiusDyn = dyno.dynoFloat(PARAMETERS.randomRadius);
|
|
const OFFSETS_Y = [
|
|
dyno.dynoFloat(0.0),
|
|
dyno.dynoFloat(0.3),
|
|
dyno.dynoFloat(0.0),
|
|
];
|
|
|
|
for (let i = 0; i < splatFiles.length; i++) {
|
|
const url = await getAssetFileURL(splatFiles[i]);
|
|
const mesh = new SplatMesh({ url });
|
|
await mesh.initialized;
|
|
// Orientación base similar a otros efectos
|
|
mesh.rotateX(Math.PI);
|
|
mesh.position.set(0, 0, 0);
|
|
mesh.scale.set(1.5, 1.5, 1.5);
|
|
if (!disposed) group.add(mesh);
|
|
meshes.push(mesh);
|
|
}
|
|
|
|
// Asignar modificadores de morph (hold → scatter → morph)
|
|
meshes.forEach((m, i) => {
|
|
m.worldModifier = getMorphModifier(
|
|
time,
|
|
dyno.dynoInt(i),
|
|
stayDyn,
|
|
transDyn,
|
|
numObjectsDyn,
|
|
radiusDyn,
|
|
OFFSETS_Y[i] ?? dyno.dynoFloat(0.0),
|
|
);
|
|
m.updateGenerator();
|
|
});
|
|
|
|
function update(dt, _t) {
|
|
if (!PARAMETERS.pause) {
|
|
time.value += dt * PARAMETERS.speedMultiplier;
|
|
for (const m of meshes) {
|
|
if (PARAMETERS.rotation) {
|
|
m.rotation.y += dt * PARAMETERS.speedMultiplier;
|
|
}
|
|
// Ensure dyno uniform updates are applied even without rotation
|
|
m.updateVersion();
|
|
}
|
|
}
|
|
}
|
|
|
|
function setupGUI(folder) {
|
|
folder.add(PARAMETERS, "speedMultiplier", 0.1, 3.0, 0.01);
|
|
folder.add(PARAMETERS, "rotation");
|
|
folder.add(PARAMETERS, "pause");
|
|
folder.add(PARAMETERS, "staySeconds", 0.2, 5.0, 0.05).onChange((v) => {
|
|
stayDyn.value = v;
|
|
});
|
|
folder
|
|
.add(PARAMETERS, "transitionSeconds", 1.0, 3.0, 0.05)
|
|
.onChange((v) => {
|
|
transDyn.value = v;
|
|
});
|
|
folder.add(PARAMETERS, "randomRadius", 1, 5.0, 0.1).onChange((v) => {
|
|
radiusDyn.value = v;
|
|
});
|
|
return folder;
|
|
}
|
|
|
|
function dispose() {
|
|
disposed = true;
|
|
scene.remove(group);
|
|
}
|
|
|
|
return { group, update, dispose, setupGUI };
|
|
}
|