mirror of
https://github.com/storytold/spark.git
synced 2026-10-14 23:52:59 +00:00
Transition Shader Example (#69)
* v0 transition shader * cleanup hardcoded constants, add multi-object transitions * switch to fixed canvas * update assets * envmap draft * update assets * Move assets to CDN * Replace underscore variable naming with camelCase for consistency --------- Co-authored-by: Diego Marcos Segura <diego.marcos@gmail.com>
This commit is contained in:
@@ -95,10 +95,30 @@
|
||||
"url": "https://sparkjs.dev/assets/splats/cat.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"dessert.spz": {
|
||||
"url": "https://sparkjs.dev/assets/splats/dessert.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"penguin.spz": {
|
||||
"url": "https://sparkjs.dev/assets/splats/penguin.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"woobles.spz": {
|
||||
"url": "https://sparkjs.dev/assets/splats/woobles.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"arrow.glb": {
|
||||
"url": "https://sparkjs.dev/assets/models/arrow.glb",
|
||||
"directory": "models"
|
||||
},
|
||||
"dali-env.glb": {
|
||||
"url": "https://sparkjs.dev/assets/models/dali-env.glb",
|
||||
"directory": "models"
|
||||
},
|
||||
"dali-table.glb": {
|
||||
"url": "https://sparkjs.dev/assets/models/dali-table.glb",
|
||||
"directory": "models"
|
||||
},
|
||||
"valley.spz": {
|
||||
"url": "https://sparkjs.dev/assets/splats/valley.spz",
|
||||
"directory": "splats"
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Spark • Splat Transitions</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; /* Remove default focus outline */
|
||||
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 { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
import { GUI } from "lil-gui";
|
||||
|
||||
const splatFiles = [
|
||||
"penguin.spz",
|
||||
"dessert.spz",
|
||||
"woobles.spz",
|
||||
];
|
||||
const sceneFile = "dali-table.glb"
|
||||
const skyFile = "dali-env.glb"
|
||||
|
||||
const PARAMETERS = {
|
||||
splatCoverage: 1.0,
|
||||
spereRadius: 1.0,
|
||||
sphereHeight: 2.0,
|
||||
speedMultiplier: 1.0,
|
||||
rotation: true,
|
||||
pause: false,
|
||||
};
|
||||
|
||||
function getTransitionState(t, fadeInTime, fadeOutTime, period) {
|
||||
// inputs:
|
||||
// unnormalized time t
|
||||
// fade in and fade out (assumed to take 1.0 unnormalized time units)
|
||||
// period (assumed to be an integer number of unnormalized time units)
|
||||
// returns:
|
||||
// dynobool for whether transition is active
|
||||
// dynobool for whether transition is fading in or out
|
||||
// dynofloat for the normalized time of the transition
|
||||
const dynoOne = dyno.dynoFloat(1.0);
|
||||
const wrapT = dyno.mod(t, period);
|
||||
const normT= dyno.mod(t, dynoOne);
|
||||
const isFadeIn = dyno.and(
|
||||
dyno.greaterThan(wrapT, fadeInTime),
|
||||
dyno.lessThan(wrapT, dyno.add(fadeInTime, dynoOne)),
|
||||
);
|
||||
const isFadeOut = dyno.and(
|
||||
dyno.greaterThan(wrapT, fadeOutTime),
|
||||
dyno.lessThan(wrapT, dyno.add(fadeOutTime, dynoOne)),
|
||||
);
|
||||
const inTransition = dyno.or(isFadeIn, isFadeOut);
|
||||
return { inTransition, isFadeIn, normT};
|
||||
}
|
||||
|
||||
function contractionDyno() {
|
||||
// this is a looping shader that periodically contracts and expands the splat.
|
||||
// t is assumed to be in normalized time [0, 1], any retiming should be done before this function.
|
||||
// fadeIn is a boolean that indicates whether the splat is fading in or out.
|
||||
return new dyno.Dyno({
|
||||
inTypes: {
|
||||
gsplat: dyno.Gsplat,
|
||||
inTransition: "bool",
|
||||
fadeIn: "bool",
|
||||
t: "float",
|
||||
splatScale: "float",
|
||||
spereRadius: "float",
|
||||
sphereHeight: "float",
|
||||
},
|
||||
outTypes: { gsplat: dyno.Gsplat },
|
||||
globals: () => [
|
||||
dyno.unindent(`
|
||||
vec3 applyCenter(vec3 center, float t, float spereRadius, float sphereHeight) {
|
||||
float heightModifier = 0.5 + 0.5 * pow(abs(1.0 - 2.0*t), 0.2);
|
||||
vec3 targetCenter = vec3(0.0, heightModifier * sphereHeight, 0.0);
|
||||
vec3 dir = normalize(center - targetCenter);
|
||||
vec3 targetPoint = targetCenter + dir * spereRadius;
|
||||
if (t < 0.25 || t > 0.75) {
|
||||
return center;
|
||||
} else if (t < 0.45) {
|
||||
return mix(center, targetPoint, pow((t - 0.25) * 5.0, 4.0));
|
||||
} else if (t < 0.55) {
|
||||
float churn = 0.1;
|
||||
float transitionT = (t - 0.45) * 10.0;
|
||||
float angle = transitionT * 2.0 * PI;
|
||||
vec3 rotvec = vec3(sin(angle), 0.0, cos(angle));
|
||||
float strength = sin(transitionT * PI);
|
||||
return targetPoint + cross(dir, rotvec) * churn * strength;
|
||||
} else {
|
||||
return mix(targetPoint, center, pow((t - 0.55) * 5.0, 4.0));
|
||||
}
|
||||
}
|
||||
|
||||
vec3 applyScale(vec3 scales, float t, float targetScale) {
|
||||
vec3 targetScales = targetScale * vec3(1.0, 1.0, 1.0);
|
||||
if (t < 0.25) {
|
||||
return scales;
|
||||
} else if (t < 0.45) {
|
||||
return mix(scales, targetScales, pow((t - 0.25) * 5.0, 2.0));
|
||||
} else if (t < 0.55) {
|
||||
return targetScales;
|
||||
} else if (t < 0.75) {
|
||||
return mix(targetScales, scales, pow((t - 0.55) * 5.0, 2.0));
|
||||
} else {
|
||||
return scales;
|
||||
}
|
||||
}
|
||||
|
||||
float applyOpacity(float opacity, float t, bool fadeIn) {
|
||||
if (fadeIn) {
|
||||
if (t < 0.4) {
|
||||
return 0.0;
|
||||
} else if (t < 0.6) {
|
||||
return mix(0.0, opacity, pow((t - 0.4) * 5.0, 2.0));
|
||||
} else {
|
||||
return opacity;
|
||||
}
|
||||
} else {
|
||||
if (t < 0.4) {
|
||||
return opacity;
|
||||
} else if (t < 0.6) {
|
||||
return mix(opacity, 0.0, pow((t - 0.4) * 5.0, 2.0));
|
||||
} else {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
],
|
||||
statements: ({ inputs, outputs }) => dyno.unindentLines(`
|
||||
${outputs.gsplat} = ${inputs.gsplat};
|
||||
${outputs.gsplat}.center = applyCenter(${inputs.gsplat}.center, ${inputs.t}, ${inputs.spereRadius}, ${inputs.sphereHeight});
|
||||
${outputs.gsplat}.scales = applyScale(${inputs.gsplat}.scales, ${inputs.t}, ${inputs.splatScale});
|
||||
if (${inputs.inTransition}) {
|
||||
${outputs.gsplat}.rgba.a = applyOpacity(${inputs.gsplat}.rgba.a, ${inputs.t}, ${inputs.fadeIn});
|
||||
} else {
|
||||
${outputs.gsplat}.rgba.a = 0.0;
|
||||
}
|
||||
`),
|
||||
});
|
||||
}
|
||||
|
||||
function getTransitionModifier(
|
||||
inTransition,
|
||||
fadeIn,
|
||||
t,
|
||||
splatScale,
|
||||
spereRadius,
|
||||
sphereHeight
|
||||
) {
|
||||
const contraction = contractionDyno();
|
||||
return dyno.dynoBlock(
|
||||
{ gsplat: dyno.Gsplat },
|
||||
{ gsplat: dyno.Gsplat },
|
||||
({ gsplat }) => {
|
||||
gsplat = contraction.apply({
|
||||
gsplat, inTransition, fadeIn, t, splatScale, spereRadius, sphereHeight
|
||||
}).gsplat;
|
||||
return { gsplat };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function morphableSplatMesh(
|
||||
assetName,
|
||||
time,
|
||||
fadeInTime,
|
||||
fadeOutTime,
|
||||
period,
|
||||
splatCoverage,
|
||||
spereRadius,
|
||||
sphereHeight,
|
||||
) {
|
||||
|
||||
const url = await getAssetFileURL(assetName);
|
||||
const splatMesh = new SplatMesh({
|
||||
url: url,
|
||||
onFrame: ({ mesh, time }) => {
|
||||
mesh.needsUpdate = true;
|
||||
}
|
||||
});
|
||||
await splatMesh.initialized; // wait to get splatCount
|
||||
const splatScale = dyno.div(dyno.mul(splatCoverage, spereRadius),
|
||||
dyno.dynoFloat(splatMesh.packedSplats.numSplats / 1000.0)
|
||||
);
|
||||
|
||||
const { inTransition, isFadeIn, normT} =
|
||||
getTransitionState(time, fadeInTime, fadeOutTime, period);
|
||||
|
||||
splatMesh.worldModifier = getTransitionModifier(
|
||||
inTransition,
|
||||
isFadeIn,
|
||||
normT,
|
||||
splatScale,
|
||||
spereRadius,
|
||||
sphereHeight
|
||||
);
|
||||
splatMesh.updateGenerator();
|
||||
return splatMesh;
|
||||
}
|
||||
|
||||
async function loadDelitGLB(filename, isEnv = false) {
|
||||
const url = await getAssetFileURL(filename);
|
||||
console.log("Loading GLB", url);
|
||||
const gltfLoader = new GLTFLoader();
|
||||
const gltf = await new Promise((resolve, reject) => {
|
||||
gltfLoader.load(url, resolve, undefined, reject);
|
||||
});
|
||||
const scene = gltf.scene;
|
||||
// Switch all materials to albedo slot
|
||||
scene.traverse((child) => {
|
||||
if (child.isMesh && child.material) {
|
||||
const originalMaterial = child.material;
|
||||
const basicMaterial = new THREE.MeshBasicMaterial();
|
||||
if (originalMaterial.color) {
|
||||
basicMaterial.color.copy(originalMaterial.color);
|
||||
}
|
||||
if (originalMaterial.map) {
|
||||
basicMaterial.map = originalMaterial.map;
|
||||
}
|
||||
if (isEnv) { // sky sphere handling
|
||||
basicMaterial.side = THREE.BackSide;
|
||||
basicMaterial.map.mapping = THREE.EquirectangularReflectionMapping;
|
||||
basicMaterial.map.colorSpace = THREE.LinearSRGBColorSpace;
|
||||
basicMaterial.map.needsUpdate = true;
|
||||
}
|
||||
child.material = basicMaterial;
|
||||
}
|
||||
});
|
||||
return scene;
|
||||
}
|
||||
|
||||
|
||||
const canvas = document.getElementById("canvas");
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: false });
|
||||
renderer.setClearColor(new THREE.Color(0x000000), 1);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const spark = new SparkRenderer({
|
||||
renderer,
|
||||
});
|
||||
scene.add(spark);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
50,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.01,
|
||||
1000,
|
||||
);
|
||||
camera.position.set(5, 4, 7);
|
||||
camera.lookAt(0, 4, 0);
|
||||
scene.add(camera);
|
||||
|
||||
const orbitControls = new OrbitControls(camera, renderer.domElement);
|
||||
orbitControls.target.set(0, 2, 0);
|
||||
function handleResize() {
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight;
|
||||
renderer.setSize(width, height, false);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
const time = dyno.dynoFloat(0.0);
|
||||
|
||||
async function loadAssets(splatCoverage, spereRadius, sphereHeight) {
|
||||
console.log("Loading initial scene...");
|
||||
|
||||
// Load sky sphere
|
||||
const skySphere = await loadDelitGLB(skyFile, true);
|
||||
scene.add(skySphere);
|
||||
|
||||
// Load table GLB
|
||||
const sceneAssets = await loadDelitGLB(sceneFile);
|
||||
const sceneScale = 3.5;
|
||||
sceneAssets.scale.set(sceneScale, sceneScale, sceneScale);
|
||||
sceneAssets.position.set(-1, 0, -0.8);
|
||||
scene.add(sceneAssets);
|
||||
|
||||
const splatMeshes = [];
|
||||
const period = dyno.dynoFloat(splatFiles.length);
|
||||
for (let i=0; i<splatFiles.length; i++) {
|
||||
console.log(splatFiles[i], (i+1) % splatFiles.length);
|
||||
const splatMesh = await morphableSplatMesh(
|
||||
splatFiles[i],
|
||||
time,
|
||||
dyno.dynoFloat(i), //fadeInTime
|
||||
dyno.dynoFloat((i+1) % splatFiles.length), //fadeOutTime
|
||||
period,
|
||||
splatCoverage,
|
||||
spereRadius,
|
||||
sphereHeight,
|
||||
);
|
||||
splatMesh.quaternion.set(1, 0, 0, 0);
|
||||
scene.add(splatMesh);
|
||||
splatMeshes.push(splatMesh);
|
||||
}
|
||||
return splatMeshes;
|
||||
}
|
||||
|
||||
const spereRadiusDyno = dyno.dynoFloat(PARAMETERS.spereRadius)
|
||||
const splatCoverageDyno = dyno.dynoFloat(PARAMETERS.splatCoverage)
|
||||
const sphereHeightDyno = dyno.dynoFloat(PARAMETERS.sphereHeight)
|
||||
const splatMeshes = await loadAssets(splatCoverageDyno, spereRadiusDyno, sphereHeightDyno);
|
||||
|
||||
const gui = new GUI();
|
||||
gui.add(PARAMETERS, "spereRadius").min(0.1).max(8.0).step(0.01).onChange((value) => {
|
||||
spereRadiusDyno.value = value;
|
||||
});
|
||||
gui.add(PARAMETERS, "sphereHeight").min(-1.0).max(4.0).step(0.01).onChange((value) => {
|
||||
sphereHeightDyno.value = value;
|
||||
});
|
||||
gui.add(PARAMETERS, "splatCoverage").min(0.1).max(2.0).step(0.01).onChange((value) => {
|
||||
splatCoverageDyno.value = value;
|
||||
});
|
||||
gui.add(PARAMETERS, "speedMultiplier").min(0.25).max(4.0).step(0.01);
|
||||
gui.add(PARAMETERS, "rotation");
|
||||
gui.add(PARAMETERS, "pause");
|
||||
|
||||
console.log("Starting render loop");
|
||||
|
||||
// Animation loop
|
||||
let lastTime = 0;
|
||||
renderer.setAnimationLoop((rawTime) => {
|
||||
rawTime *= 0.0005;
|
||||
const deltaTime = rawTime - (lastTime ?? rawTime);
|
||||
lastTime = rawTime;
|
||||
orbitControls.update();
|
||||
renderer.render(scene, camera);
|
||||
|
||||
if (!PARAMETERS.pause) {
|
||||
time.value +=deltaTime * PARAMETERS.speedMultiplier;
|
||||
if (PARAMETERS.rotation) {
|
||||
for (const splatMesh of splatMeshes) {
|
||||
splatMesh.rotation.y += deltaTime * PARAMETERS.speedMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -142,6 +142,7 @@
|
||||
<li><a href="/examples/dynamic-lighting/">Dynamic Lighting</a></li>
|
||||
<li><a href="/examples/particle-animation/">Particle Animation</a></li>
|
||||
<li><a href="/examples/particle-simulation/">Particle Simulation</a></li>
|
||||
<li><a href="/examples/splat-transitions/">Splat Transitions</a></li>
|
||||
<li><a href="/examples/webxr/">WebXR</a></li>
|
||||
<li><a href="/examples/glsl/">GLSL Shaders</a></li>
|
||||
<li><a href="/examples/debug-color/">Debug Coloring</a></li>
|
||||
|
||||
Reference in New Issue
Block a user