mirror of
https://github.com/storytold/spark.git
synced 2026-10-18 07:40:38 +00:00
Transition Effects (#172)
* Transition effects * Added two transition effects and refactored code into separate .js filesAdded two transition effects and refactorized code for effects in separate .js files * Deleted unused html file * Fixed a GUI bug * Fixed focus given to effect when dropdown changes * Fixed github verification errors * fix lints in splat-transitions and tweak defaults * lint fix on splat-transitions * lint fix on splat-transitions * Morph transition * Morph transition - better random function * Morph transition - better morphing of dishes * fixed lint errors
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
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);
|
||||
|
||||
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);
|
||||
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);
|
||||
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() {
|
||||
scene.remove(group);
|
||||
}
|
||||
|
||||
return { group, update, dispose, setupGUI };
|
||||
}
|
||||
Reference in New Issue
Block a user