diff --git a/examples/interactive-deform/index.html b/examples/interactive-deform/index.html
deleted file mode 100644
index 4845ad02..00000000
--- a/examples/interactive-deform/index.html
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
- Spark • Splat Experiment
-
-
-
-
- Click and drag on the penguin to deform it • Release to see elastic bounce • A/D to rotate • W/S to zoom • Adjust parameters with GUI controls
-
-
-
-
-
-
diff --git a/examples/interactive-deform/main.js b/examples/interactive-deform/main.js
deleted file mode 100644
index 778b4679..00000000
--- a/examples/interactive-deform/main.js
+++ /dev/null
@@ -1,318 +0,0 @@
-import { SparkRenderer, SplatMesh, dyno } from "@sparkjsdev/spark";
-import { GUI } from "lil-gui";
-import * as THREE from "three";
-import { getAssetFileURL } from "/examples/js/get-asset-url.js";
-
-const scene = new THREE.Scene();
-const camera = new THREE.PerspectiveCamera(
- 60,
- window.innerWidth / window.innerHeight,
- 0.1,
- 1000,
-);
-const renderer = new THREE.WebGLRenderer({ antialias: false });
-renderer.setSize(window.innerWidth, window.innerHeight);
-document.body.appendChild(renderer.domElement);
-
-const spark = new SparkRenderer({ renderer });
-scene.add(spark);
-
-window.addEventListener("resize", onWindowResize, false);
-function onWindowResize() {
- camera.aspect = window.innerWidth / window.innerHeight;
- camera.updateProjectionMatrix();
- renderer.setSize(window.innerWidth, window.innerHeight);
-}
-
-let rotationAngle = 0;
-let zoomDistance = 5.5;
-const minZoom = 1;
-const maxZoom = 20;
-const rotationSpeed = 0.02;
-const zoomSpeed = 0.1;
-
-camera.position.set(0, 3, zoomDistance);
-camera.lookAt(0, 1, 0);
-
-const keys = {};
-window.addEventListener("keydown", (event) => {
- keys[event.key.toLowerCase()] = true;
-});
-window.addEventListener("keyup", (event) => {
- keys[event.key.toLowerCase()] = false;
-});
-
-// Dyno uniforms for drag and bounce effects
-const dragPoint = dyno.dynoVec3(new THREE.Vector3(0, 0, 0));
-const dragDisplacement = dyno.dynoVec3(new THREE.Vector3(0, 0, 0));
-const dragRadius = dyno.dynoFloat(0.5);
-const dragActive = dyno.dynoFloat(0.0);
-const bounceTime = dyno.dynoFloat(0.0);
-const bounceBaseDisplacement = dyno.dynoVec3(new THREE.Vector3(0, 0, 0));
-const dragIntensity = dyno.dynoFloat(5.0);
-const bounceAmount = dyno.dynoFloat(0.5);
-const bounceSpeed = dyno.dynoFloat(0.5);
-let isBouncing = false;
-
-const gui = new GUI();
-const guiParams = {
- intensity: dragIntensity.value,
- radius: 0.5,
- bounceAmount: 0.5,
- bounceSpeed: 0.5,
-};
-gui
- .add(guiParams, "intensity", 0, 10.0, 0.1)
- .name("Deformation Strength")
- .onChange((value) => {
- dragIntensity.value = value;
- if (splatMesh) {
- splatMesh.updateVersion();
- }
- });
-gui
- .add(guiParams, "radius", 0.25, 1.0, 0.1)
- .name("Drag Radius")
- .onChange((value) => {
- dragRadius.value = value;
- if (splatMesh) {
- splatMesh.updateVersion();
- }
- });
-gui
- .add(guiParams, "bounceAmount", 0, 1.0, 0.1)
- .name("Bounce Strength")
- .onChange((value) => {
- bounceAmount.value = value;
- if (splatMesh) {
- splatMesh.updateVersion();
- }
- });
-gui
- .add(guiParams, "bounceSpeed", 0, 1.0, 0.01)
- .name("Bounce Speed")
- .onChange((value) => {
- bounceSpeed.value = value;
- if (splatMesh) {
- splatMesh.updateVersion();
- }
- });
-
-let isDragging = false;
-let dragStartPoint = null;
-let currentDragPoint = null;
-const raycaster = new THREE.Raycaster();
-raycaster.params.Points = { threshold: 0.5 };
-
-function createDragBounceDynoshader() {
- return dyno.dynoBlock(
- { gsplat: dyno.Gsplat },
- { gsplat: dyno.Gsplat },
- ({ gsplat }) => {
- const shader = new dyno.Dyno({
- inTypes: {
- gsplat: dyno.Gsplat,
- dragPoint: "vec3",
- dragDisplacement: "vec3",
- dragRadius: "float",
- dragActive: "float",
- bounceTime: "float",
- bounceBaseDisplacement: "vec3",
- dragIntensity: "float",
- bounceAmount: "float",
- bounceSpeed: "float",
- },
- outTypes: { gsplat: dyno.Gsplat },
- statements: ({ inputs, outputs }) =>
- dyno.unindentLines(`
- ${outputs.gsplat} = ${inputs.gsplat};
- vec3 originalPos = ${inputs.gsplat}.center;
-
- // Calculate influence based on distance from drag point
- float distToDrag = distance(originalPos, ${inputs.dragPoint});
- float dragInfluence = 1.0 - smoothstep(0.0, ${inputs.dragRadius}*2., distToDrag);
- float time = ${inputs.bounceTime};
-
- // Apply drag deformation
- if (${inputs.dragActive} > 0.5 && ${inputs.dragRadius} > 0.0) {
- vec3 dragOffset = ${inputs.dragDisplacement} * dragInfluence * ${inputs.dragIntensity} * 50.0;
- originalPos += dragOffset;
- }
-
- // Apply elastic bounce effect
- float bounceFrequency = 1.0 + ${inputs.bounceSpeed} * 8.0;
- vec3 bounceOffset = ${inputs.bounceBaseDisplacement} * dragInfluence * ${inputs.dragIntensity} * 50.0;
- originalPos += bounceOffset * cos(time*bounceFrequency) * exp(-time*2.0*(1.0-${inputs.bounceAmount}*.9));
-
- ${outputs.gsplat}.center = originalPos;
- `),
- });
-
- return {
- gsplat: shader.apply({
- gsplat,
- dragPoint: dragPoint,
- dragDisplacement: dragDisplacement,
- dragRadius: dragRadius,
- dragActive: dragActive,
- bounceTime: bounceTime,
- bounceBaseDisplacement: bounceBaseDisplacement,
- dragIntensity: dragIntensity,
- bounceAmount: bounceAmount,
- bounceSpeed: bounceSpeed,
- }).gsplat,
- };
- },
- );
-}
-
-let splatMesh = null;
-
-async function loadSplat() {
- const splatURL = await getAssetFileURL("penguin.spz");
- splatMesh = new SplatMesh({ url: splatURL });
- splatMesh.quaternion.set(1, 0, 0, 0);
- splatMesh.position.set(0, 0, 0);
- scene.add(splatMesh);
-
- await splatMesh.initialized;
-
- splatMesh.worldModifier = createDragBounceDynoshader();
- splatMesh.updateGenerator();
-}
-
-loadSplat().catch((error) => {
- console.error("Error loading splat:", error);
-});
-
-// Convert mouse coordinates to normalized device coordinates
-function getMouseNDC(event) {
- const rect = renderer.domElement.getBoundingClientRect();
- return new THREE.Vector2(
- ((event.clientX - rect.left) / rect.width) * 2 - 1,
- -((event.clientY - rect.top) / rect.height) * 2 + 1,
- );
-}
-
-// Raycast to find intersection point on splat
-function getHitPoint(ndc) {
- if (!splatMesh) return null;
- raycaster.setFromCamera(ndc, camera);
- const hits = raycaster.intersectObject(splatMesh, false);
- if (hits && hits.length > 0) {
- return hits[0].point.clone();
- }
- return null;
-}
-
-let dragStartNDC = null;
-let dragScale = 1.0;
-
-renderer.domElement.addEventListener("pointerdown", (event) => {
- if (!splatMesh) return;
-
- const ndc = getMouseNDC(event);
- const hitPoint = getHitPoint(ndc);
-
- if (hitPoint) {
- isDragging = true;
- dragStartNDC = ndc.clone();
- dragStartPoint = hitPoint.clone();
- currentDragPoint = hitPoint.clone();
-
- // Calculate scale factor for screen-to-world conversion
- const distanceToCamera = camera.position.distanceTo(hitPoint);
- const fov = camera.fov * (Math.PI / 180);
- const screenHeight = 2.0 * Math.tan(fov / 2.0) * distanceToCamera;
- dragScale = screenHeight / window.innerHeight;
-
- dragPoint.value.copy(hitPoint);
- dragActive.value = 1.0;
- dragRadius.value = guiParams.radius;
- dragDisplacement.value.set(0, 0, 0);
-
- bounceTime.value = -1.0;
- bounceBaseDisplacement.value.set(0, 0, 0);
- isBouncing = false;
- }
-});
-
-renderer.domElement.addEventListener("pointermove", (event) => {
- if (!isDragging || !splatMesh || !dragStartPoint || !dragStartNDC) return;
-
- const ndc = getMouseNDC(event);
-
- // Convert screen space movement to world space
- const mouseDelta = new THREE.Vector2(
- (ndc.x - dragStartNDC.x) * dragScale,
- (ndc.y - dragStartNDC.y) * dragScale,
- );
-
- const cameraRight = new THREE.Vector3();
- const cameraUp = new THREE.Vector3();
- camera.getWorldDirection(new THREE.Vector3());
- cameraRight.setFromMatrixColumn(camera.matrixWorld, 0).normalize();
- cameraUp.setFromMatrixColumn(camera.matrixWorld, 1).normalize();
-
- const worldDisplacement = new THREE.Vector3()
- .addScaledVector(cameraRight, mouseDelta.x)
- .addScaledVector(cameraUp, mouseDelta.y);
-
- currentDragPoint = dragStartPoint.clone().add(worldDisplacement);
- dragDisplacement.value.copy(worldDisplacement);
-});
-
-renderer.domElement.addEventListener("pointerup", (event) => {
- if (!isDragging) return;
-
- isDragging = false;
-
- // Start bounce animation with final displacement
- if (currentDragPoint && dragStartPoint) {
- const finalDisplacement = currentDragPoint.clone().sub(dragStartPoint);
- bounceBaseDisplacement.value.copy(dragDisplacement.value);
- bounceTime.value = 0.0;
- isBouncing = true;
- }
-
- dragActive.value = 0.0;
- dragDisplacement.value.set(0, 0, 0);
- dragStartNDC = null;
-});
-
-renderer.setAnimationLoop(() => {
- // Update bounce animation
- if (isBouncing) {
- bounceTime.value += 0.1;
- if (splatMesh) {
- splatMesh.updateVersion();
- }
- }
-
- // Keyboard controls
- if (keys.a) {
- rotationAngle -= rotationSpeed;
- }
- if (keys.d) {
- rotationAngle += rotationSpeed;
- }
-
- if (keys.w) {
- zoomDistance = Math.max(minZoom, zoomDistance - zoomSpeed);
- }
- if (keys.s) {
- zoomDistance = Math.min(maxZoom, zoomDistance + zoomSpeed);
- }
-
- // Update camera orbit
- camera.position.x = Math.sin(rotationAngle) * zoomDistance;
- camera.position.z = Math.cos(rotationAngle) * zoomDistance;
- camera.position.y = 3;
- camera.lookAt(0, 1.5, 0);
-
- if (splatMesh) {
- splatMesh.updateVersion();
- }
-
- renderer.render(scene, camera);
-});
diff --git a/examples/interactive-holes/index.html b/examples/interactive-holes/index.html
deleted file mode 100644
index 02cd637f..00000000
--- a/examples/interactive-holes/index.html
+++ /dev/null
@@ -1,461 +0,0 @@
-
-
-
-
-
-
- Spark • Interactive Holes (Click Radius)
-
-
-
-
- Mouse to look around • Click on a splat to create interactive holes within a radius
-
-
-
-
-
-
-
diff --git a/examples/interactive-ripples/index.html b/examples/interactive-ripples/index.html
deleted file mode 100644
index 94fbb812..00000000
--- a/examples/interactive-ripples/index.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
- spark | splat-shockwave
-
-
-
-
- Click to generate ripples • WASD + Mouse to move camera
-
-
-
diff --git a/examples/interactive-ripples/main.js b/examples/interactive-ripples/main.js
deleted file mode 100644
index 0dc848f2..00000000
--- a/examples/interactive-ripples/main.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import {
- SparkControls,
- SparkRenderer,
- SplatMesh,
- dyno,
-} from "@sparkjsdev/spark";
-import * as THREE from "three";
-import { getAssetFileURL } from "/examples/js/get-asset-url.js";
-
-const canvas = document.getElementById("canvas");
-const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
-renderer.setPixelRatio(window.devicePixelRatio);
-renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
-renderer.setClearColor(0x000000, 1);
-
-const scene = new THREE.Scene();
-const spark = new SparkRenderer({ renderer });
-scene.add(spark);
-
-const camera = new THREE.PerspectiveCamera(
- 50,
- canvas.clientWidth / canvas.clientHeight,
- 0.01,
- 2000,
-);
-camera.position.set(0, 0, 3);
-camera.lookAt(0, 0, 0);
-scene.add(camera);
-
-function handleResize() {
- const w = canvas.clientWidth;
- const h = canvas.clientHeight;
- renderer.setSize(w, h, false);
- camera.aspect = w / h;
- camera.updateProjectionMatrix();
-}
-window.addEventListener("resize", handleResize);
-
-// Camera controls with mouse and WASD enabled
-const controls = new SparkControls({ canvas: renderer.domElement });
-controls.fpsMovement.enable = true; // Enable WASD movement
-controls.pointerControls.enable = true; // Enable mouse controls
-
-// Dyno shader with time and shockwave function
-function passthroughDyno(timeUniform, hitpointUniform) {
- return dyno.dynoBlock(
- { gsplat: dyno.Gsplat },
- { gsplat: dyno.Gsplat },
- ({ gsplat }) => {
- const shader = new dyno.Dyno({
- inTypes: {
- gsplat: dyno.Gsplat,
- time: "float",
- hitpoint: "vec3",
- },
- outTypes: { gsplat: dyno.Gsplat },
- globals: () => [
- dyno.unindent(`
- vec3 shockwave(vec3 center, float t, vec3 hitpoint) {
- vec3 direction = center - hitpoint;
- float distance = length(direction);
- center += normalize(direction)*sin(t*4.-distance*5.)*exp(-t)*smoothstep(t*2.,0.,distance)*.5;
- return center;
- }
- vec4 shockwaveColor(vec4 rgba, vec3 center, float t, vec3 hitpoint) {
- vec3 direction = center - hitpoint;
- float distance = length(direction);
- float wave = sin(t*4.-distance*5.)*exp(-t*.7)*smoothstep(t*2.,0.,distance);
- float brightness = pow(abs(wave),3.) * 10.; // Increase brightness on wave crests
- rgba.rgb += brightness;
- return rgba;
- }
- `),
- ],
- statements: ({ inputs, outputs }) =>
- dyno.unindentLines(`
- ${outputs.gsplat} = ${inputs.gsplat};
- // Apply shockwave function to position
- ${outputs.gsplat}.center = shockwave(${inputs.gsplat}.center, ${inputs.time}, ${inputs.hitpoint});
- // Apply shockwave function to color
- ${outputs.gsplat}.rgba = shockwaveColor(${inputs.gsplat}.rgba, ${inputs.gsplat}.center, ${inputs.time}, ${inputs.hitpoint});
- `),
- });
- return {
- gsplat: shader.apply({
- gsplat,
- time: timeUniform,
- hitpoint: hitpointUniform,
- }).gsplat,
- };
- },
- );
-}
-
-async function run() {
- // Time and hitpoint uniforms for dyno shader
- const timeUniform = dyno.dynoFloat(0.0);
- const hitpointUniform = dyno.dynoVec3(new THREE.Vector3(0, 0, 1000)); // Initialize far away to avoid initial effect
-
- // Load valley.spz
- const splatURL = await getAssetFileURL("valley.spz");
- const valley = new SplatMesh({ url: splatURL });
- await valley.initialized;
-
- // Fix orientation - rotate 180 degrees around X axis
- valley.rotateX(Math.PI);
-
- // Apply dyno shader with time and hitpoint uniforms
- valley.objectModifier = passthroughDyno(timeUniform, hitpointUniform);
- valley.updateGenerator();
-
- scene.add(valley);
-
- // Raycaster for click detection
- const raycaster = new THREE.Raycaster();
- raycaster.params.Points = { threshold: 1.0 }; // Increased threshold for better hit detection
-
- // Simple time counter that resets on click
- let timeCounter = 0;
-
- // Click event listener to set hitpoint and reset time
- renderer.domElement.addEventListener("pointerdown", (event) => {
- const rect = renderer.domElement.getBoundingClientRect();
- const ndc = new THREE.Vector2(
- ((event.clientX - rect.left) / rect.width) * 2 - 1,
- -((event.clientY - rect.top) / rect.height) * 2 + 1,
- );
- raycaster.setFromCamera(ndc, camera);
- const hits = raycaster.intersectObject(valley, false);
- const hit = hits?.length ? hits[0] : null;
-
- if (!hit) {
- return;
- }
-
- const localPoint = valley.worldToLocal(hit.point.clone());
- // Don't invert Y or Z - keep original coordinates
-
- hitpointUniform.value.copy(localPoint);
- timeCounter = 0; // Reset time counter
- });
-
- renderer.setAnimationLoop((timeMs) => {
- // Increment time counter each frame
- timeCounter += 0.016; // ~60fps increment
- timeUniform.value = timeCounter;
-
- // Update dyno uniforms to propagate to the mesh each frame
- valley.updateVersion();
-
- controls.update(camera);
- renderer.render(scene, camera);
- });
-}
-
-run();
diff --git a/examples/splat-flow/index.html b/examples/splat-flow/index.html
deleted file mode 100644
index 4e237ff0..00000000
--- a/examples/splat-flow/index.html
+++ /dev/null
@@ -1,259 +0,0 @@
-
-
-
-
-
- Spark • Splat Flow
-
-
-
-
-
-
-
-
diff --git a/examples/splat-portal/index.html b/examples/splat-portal/index.html
deleted file mode 100644
index 86a24201..00000000
--- a/examples/splat-portal/index.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
- spark | splat-portal
-
-
-
-
- WASD + Mouse to move camera • Go through portal to switch between worlds
-
-
-
-
diff --git a/examples/splat-portal/main.js b/examples/splat-portal/main.js
deleted file mode 100644
index d9717f22..00000000
--- a/examples/splat-portal/main.js
+++ /dev/null
@@ -1,405 +0,0 @@
-import { SparkControls, SparkRenderer, SplatMesh } from "@sparkjsdev/spark";
-import * as THREE from "three";
-import { getAssetFileURL } from "/examples/js/get-asset-url.js";
-
-const canvas = document.getElementById("canvas");
-const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
-renderer.setPixelRatio(window.devicePixelRatio);
-renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
-renderer.setClearColor(0x000000, 1);
-
-// Two independent scenes (world A and world B)
-const sceneA = new THREE.Scene();
-const sceneB = new THREE.Scene();
-const sparkA = new SparkRenderer({ renderer });
-const sparkB = new SparkRenderer({ renderer });
-sceneA.add(sparkA);
-sceneB.add(sparkB);
-
-// Main camera (not parented to scenes)
-const camera = new THREE.PerspectiveCamera(
- 50,
- canvas.clientWidth / canvas.clientHeight,
- 0.01,
- 2000,
-);
-camera.position.set(0, 1, 3);
-camera.lookAt(0, 1, 0);
-
-// Offscreen render targets for portal views
-const rtAtoB = new THREE.WebGLRenderTarget(
- canvas.clientWidth,
- canvas.clientHeight,
- {
- depthBuffer: true,
- },
-);
-rtAtoB.texture.minFilter = THREE.LinearFilter;
-rtAtoB.texture.magFilter = THREE.LinearFilter;
-rtAtoB.texture.generateMipmaps = false;
-const rtBtoA = new THREE.WebGLRenderTarget(
- canvas.clientWidth,
- canvas.clientHeight,
- {
- depthBuffer: true,
- },
-);
-rtBtoA.texture.minFilter = THREE.LinearFilter;
-rtBtoA.texture.magFilter = THREE.LinearFilter;
-rtBtoA.texture.generateMipmaps = false;
-
-function resizeRenderTargets(width, height) {
- const dpr =
- typeof renderer.getPixelRatio === "function"
- ? renderer.getPixelRatio()
- : window.devicePixelRatio || 1;
- const w = Math.max(1, Math.floor(width * dpr));
- const h = Math.max(1, Math.floor(height * dpr));
- if (rtAtoB) {
- rtAtoB.setSize(w, h);
- }
- if (rtBtoA) {
- rtBtoA.setSize(w, h);
- }
-}
-
-function handleResize() {
- const w = canvas.clientWidth;
- const h = canvas.clientHeight;
- renderer.setSize(w, h, false);
- camera.aspect = w / h;
- camera.updateProjectionMatrix();
- resizeRenderTargets(w, h);
-}
-window.addEventListener("resize", handleResize);
-
-// Camera controls with mouse and WASD enabled
-const controls = new SparkControls({ canvas: renderer.domElement });
-controls.fpsMovement.enable = true;
-controls.pointerControls.enable = true;
-
-// Portal helpers
-function makePortalMaterial() {
- const material = new THREE.ShaderMaterial({
- uniforms: {
- tMap: { value: null },
- portalPV: { value: new THREE.Matrix4() },
- portalBridge: { value: new THREE.Matrix4() },
- worldMatrix: { value: new THREE.Matrix4() },
- circleRadius: { value: 0.6 },
- time: { value: 0.0 },
- waveStrength: { value: 0.001 },
- waveSpeed: { value: 10.0 },
- waveFrequency: { value: 50.0 },
- edgeSoftness: { value: 0.2 },
- },
- vertexShader: `
- varying vec3 vWorldPosition;
- varying vec2 vLocalXY;
- void main() {
- vec4 worldPos = modelMatrix * vec4(position, 1.0);
- vWorldPosition = worldPos.xyz;
- vLocalXY = position.xy;
- gl_Position = projectionMatrix * viewMatrix * worldPos;
- }
- `,
- fragmentShader: `
- uniform sampler2D tMap;
- uniform mat4 portalPV;
- uniform mat4 portalBridge;
- uniform float circleRadius;
- uniform float time;
- uniform float waveStrength;
- uniform float waveSpeed;
- uniform float waveFrequency;
- uniform float edgeSoftness;
- varying vec3 vWorldPosition;
- varying vec2 vLocalXY;
- void main() {
- float r = length(vLocalXY);
- if (r > circleRadius) discard;
-
- // Generate radial waves
- float wave = sin(r * waveFrequency - time * waveSpeed);
- float distortion = wave * waveStrength;
-
- // Deform UV radially based on wave
- vec2 direction = normalize(vLocalXY);
- vec2 offset = direction * distortion;
-
- vec4 targetPos = portalBridge * vec4(vWorldPosition, 1.0);
- vec4 clip = portalPV * targetPos;
- vec3 ndc = clip.xyz / max(clip.w, 1e-6);
- vec2 uv = ndc.xy * 0.5 + 0.5;
-
- // Apply wave distortion to UV
- uv += offset;
-
- // Clamp UVs instead of discarding to avoid black edges
- uv = clamp(uv, vec2(0.0), vec2(1.0));
- vec4 color = texture2D(tMap, uv);
- color+=offset.x*50.;
-
- // Edge fadeout by reducing brightness instead of alpha
- float edgeFade = 1.0 - smoothstep(circleRadius * (1.0 - edgeSoftness), circleRadius, r);
- color.rgb *= .1+edgeFade;
-
- gl_FragColor = color;
- }
- `,
- side: THREE.DoubleSide,
- transparent: false,
- depthWrite: true,
- depthTest: true,
- });
- return material;
-}
-
-function makePortal(radius) {
- const geom = new THREE.CircleGeometry(radius, 64);
- const mat = makePortalMaterial();
- const mesh = new THREE.Mesh(geom, mat);
- mesh.renderOrder = 1000;
- return mesh;
-}
-
-function buildPVMatrix(cameraObj) {
- const pv = new THREE.Matrix4();
- pv.multiplyMatrices(cameraObj.projectionMatrix, cameraObj.matrixWorldInverse);
- return pv;
-}
-
-function buildPortalBridgeMatrix(sourcePortal, targetPortal) {
- // Bridge = targetPortal * Ry(PI) * inverse(sourcePortal)
- const invSrc = new THREE.Matrix4().copy(sourcePortal.matrixWorld).invert();
- const rotY180 = new THREE.Matrix4().makeRotationY(Math.PI);
- const tmp = new THREE.Matrix4().multiplyMatrices(rotY180, invSrc);
- const bridge = new THREE.Matrix4().multiplyMatrices(
- targetPortal.matrixWorld,
- tmp,
- );
- return bridge;
-}
-
-function computeLinkedCamera(
- sourcePortal,
- targetPortal,
- fromCamera,
- outCamera,
- clampRadius = 0,
-) {
- // outCameraWorld = targetPortal * R_y(PI) * inverse(sourcePortal) * fromCameraWorld
- const invSrc = new THREE.Matrix4().copy(sourcePortal.matrixWorld).invert();
- const rotY180 = new THREE.Matrix4().makeRotationY(Math.PI);
- const tmp = new THREE.Matrix4().multiplyMatrices(
- invSrc,
- fromCamera.matrixWorld,
- );
- const withRot = new THREE.Matrix4().multiplyMatrices(rotY180, tmp);
- const dst = new THREE.Matrix4().multiplyMatrices(
- targetPortal.matrixWorld,
- withRot,
- );
-
- outCamera.matrixWorld.copy(dst);
- outCamera.matrixWorld.decompose(
- outCamera.position,
- outCamera.quaternion,
- outCamera.scale,
- );
-
- // Clamp camera position if radius is specified
- if (clampRadius > 0) {
- const targetPos = new THREE.Vector3();
- targetPortal.getWorldPosition(targetPos);
-
- const toCam = outCamera.position.clone().sub(targetPos);
- const distance = toCam.length();
-
- if (distance > clampRadius) {
- toCam.normalize().multiplyScalar(clampRadius);
- outCamera.position.copy(targetPos).add(toCam);
- outCamera.updateMatrixWorld(true);
- }
- }
-
- outCamera.projectionMatrix.copy(fromCamera.projectionMatrix);
- outCamera.updateMatrixWorld(true);
-}
-
-function transformPoseThroughPortal(sourcePortal, targetPortal, object3D) {
- const srcWorld = sourcePortal.matrixWorld;
- const dstWorld = targetPortal.matrixWorld;
- const invSrc = new THREE.Matrix4().copy(srcWorld).invert();
- const localMat = new THREE.Matrix4().multiplyMatrices(
- invSrc,
- object3D.matrixWorld,
- );
- const rotY180 = new THREE.Matrix4().makeRotationY(Math.PI);
- const dstMat = new THREE.Matrix4().multiplyMatrices(
- dstWorld,
- new THREE.Matrix4().multiplyMatrices(rotY180, localMat),
- );
- object3D.matrixWorld.copy(dstMat);
- object3D.matrixWorld.decompose(
- object3D.position,
- object3D.quaternion,
- object3D.scale,
- );
-}
-
-async function run() {
- // Load valley (world A)
- const valleyURL = await getAssetFileURL("valley.spz");
- const valley = new SplatMesh({ url: valleyURL });
- await valley.initialized;
- valley.rotateX(Math.PI);
- sceneA.add(valley);
-
- // Load sutro (world B)
- const sutroURL = await getAssetFileURL("sutro.zip");
- const sutro = new SplatMesh({ url: sutroURL });
- await sutro.initialized;
- sutro.rotateX(Math.PI); // Fix orientation
- sutro.position.set(0, 0, 0);
- sutro.scale.set(3.5, 3.5, 3.5);
- sceneB.add(sutro);
-
- // Portals in each world
- const portalRadius = 0.6;
- const portalA = makePortal(portalRadius);
- portalA.position.set(0, 1, -2);
- portalA.rotation.set(0, 0, 0);
- sceneA.add(portalA);
-
- const portalB = makePortal(portalRadius);
- portalB.position.set(-1, 1, -5);
- portalB.rotation.set(0, Math.PI, 0);
- sceneB.add(portalB);
-
- portalA.updateMatrixWorld(true);
- portalB.updateMatrixWorld(true);
-
- // Offscreen cameras
- const camAtoB = new THREE.PerspectiveCamera();
- const camBtoA = new THREE.PerspectiveCamera();
-
- // Teleportation tracking
- let activeWorld = "A";
- let lastPortalSide = -1; // -1 = behind portal, 1 = in front
- let teleportCooldown = 0;
- const TELEPORT_COOLDOWN_MS = 500; // 500ms cooldown
- const CROSSING_THRESHOLD = 0.3; // Distance threshold for crossing detection
-
- function getDistanceFromPortal(portal, pointWorld) {
- const local = pointWorld.clone();
- portal.worldToLocal(local);
- return local.z; // positive = in front, negative = behind
- }
-
- function withinRadius(portal, pointWorld, radius) {
- const local = pointWorld.clone();
- portal.worldToLocal(local);
- const d = Math.hypot(local.x, local.y);
- return d <= radius;
- }
-
- renderer.setAnimationLoop((timeMs) => {
- // Decrease cooldown timer
- if (teleportCooldown > 0) {
- teleportCooldown -= 16; // assuming ~60fps
- }
-
- // Controls and camera updates
- controls.update(camera);
- camera.updateMatrixWorld(true);
-
- // Update time for wave animation
- const time = timeMs * 0.001; // Convert to seconds
- portalA.material.uniforms.time.value = time;
- portalB.material.uniforms.time.value = time;
-
- // Prepare portal cameras
- portalA.updateMatrixWorld(true);
- portalB.updateMatrixWorld(true);
-
- computeLinkedCamera(portalA, portalB, camera, camAtoB, 3);
- computeLinkedCamera(portalB, portalA, camera, camBtoA, 3);
-
- // Render other worlds into targets (hide portals to avoid recursion/artifacts)
- const prevAVisible = portalA.visible;
- const prevBVisible = portalB.visible;
- portalA.visible = false;
- portalB.visible = false;
-
- renderer.setRenderTarget(rtAtoB);
- renderer.clear(true, true, true);
- renderer.render(sceneB, camAtoB);
- renderer.setRenderTarget(rtBtoA);
- renderer.clear(true, true, true);
- renderer.render(sceneA, camBtoA);
- renderer.setRenderTarget(null);
-
- portalA.visible = prevAVisible;
- portalB.visible = prevBVisible;
-
- // Update portal materials with PV matrices composed with portal bridges
- const pvA = buildPVMatrix(camAtoB);
- const pvB = buildPVMatrix(camBtoA);
- const bridgeAtoB = buildPortalBridgeMatrix(portalA, portalB);
- const bridgeBtoA = buildPortalBridgeMatrix(portalB, portalA);
- const matA = portalA.material;
- const matB = portalB.material;
- matA.uniforms.tMap.value = rtAtoB.texture;
- matA.uniforms.portalPV.value.copy(pvA);
- matA.uniforms.portalBridge.value.copy(bridgeAtoB);
- matB.uniforms.tMap.value = rtBtoA.texture;
- matB.uniforms.portalPV.value.copy(pvB);
- matB.uniforms.portalBridge.value.copy(bridgeBtoA);
-
- // Teleport logic when crossing the active world's portal
- if (teleportCooldown <= 0) {
- const camPos = camera.position.clone();
- const activePortal = activeWorld === "A" ? portalA : portalB;
- const distance = getDistanceFromPortal(activePortal, camPos);
- const currentSide = Math.sign(distance);
- const absDistance = Math.abs(distance);
-
- // Check if we're crossing the portal (from either side)
- const isCrossing =
- absDistance < CROSSING_THRESHOLD &&
- withinRadius(activePortal, camPos, portalRadius * 1.2);
-
- // Detect side change: going from front to back or back to front
- const sideChanged =
- lastPortalSide !== 0 && currentSide !== lastPortalSide;
-
- if (isCrossing && (sideChanged || absDistance < 0.1)) {
- const src = activeWorld === "A" ? portalA : portalB;
- const dst = activeWorld === "A" ? portalB : portalA;
-
- transformPoseThroughPortal(src, dst, camera);
- // Calculate forward direction relative to the destination portal
- const forward = new THREE.Vector3(0, 0, -1)
- .applyQuaternion(camera.quaternion)
- .multiplyScalar(0.2);
- camera.position.add(forward);
- camera.updateMatrixWorld(true);
- activeWorld = activeWorld === "A" ? "B" : "A";
- teleportCooldown = TELEPORT_COOLDOWN_MS;
- lastPortalSide = currentSide; // Update side tracking
- } else {
- lastPortalSide = currentSide;
- }
- }
-
- // Render active world to screen
- if (activeWorld === "A") {
- renderer.render(sceneA, camera);
- } else {
- renderer.render(sceneB, camera);
- }
- });
-}
-
-run();
diff --git a/examples/splat-reveal-effects/index.html b/examples/splat-reveal-effects/index.html
deleted file mode 100644
index ea127358..00000000
--- a/examples/splat-reveal-effects/index.html
+++ /dev/null
@@ -1,335 +0,0 @@
-
-
-
-
-
-
- Spark • Splat Reveal Effects
-
-
-
-
-
-
-
-
-
diff --git a/examples/splat-shader-effects/index.html b/examples/splat-shader-effects/index.html
deleted file mode 100644
index c51bef50..00000000
--- a/examples/splat-shader-effects/index.html
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
-
- Spark • GLSL Shaders
-
-
-
-
-
-
-
-
diff --git a/examples/splat-transitions/effects/explosion.js b/examples/splat-transitions/effects/explosion.js
deleted file mode 100644
index 8b137c9b..00000000
--- a/examples/splat-transitions/effects/explosion.js
+++ /dev/null
@@ -1,414 +0,0 @@
-import { SparkControls, SplatMesh, dyno, textSplats } 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;
-
- // Basic lights
- const ambient = new THREE.AmbientLight(0x404040, 0.6);
- group.add(ambient);
- const dir = new THREE.DirectionalLight(0xffffff, 1.0);
- dir.position.set(5, 10, 5);
- dir.castShadow = true;
- group.add(dir);
-
- renderer.shadowMap.enabled = true;
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
-
- // Camera baseline
- camera.position.set(0, 2.5, 7);
- camera.lookAt(0, 1, 0);
-
- // WASD + mouse controls
- const controls = new SparkControls({ canvas: renderer.domElement });
- controls.fpsMovement.moveSpeed = 3.0;
-
- // Uniforms
- const animationTime = dyno.dynoFloat(0.0);
- const uDropProgress = dyno.dynoFloat(0.0);
- const uGravity = dyno.dynoFloat(9.8);
- const uBounceDamping = dyno.dynoFloat(0.4);
- const uFloorLevel = dyno.dynoFloat(0.0);
- const uRandomFactor = dyno.dynoFloat(1.0);
- const uReformSpeed = dyno.dynoFloat(2.0);
- const uCycleDuration = dyno.dynoFloat(1.0);
- const uDropTime = dyno.dynoFloat(0.0);
- const uFriction = dyno.dynoFloat(0.98);
- const uShrinkSpeed = dyno.dynoFloat(5.0 - 3.0);
- const uExplosionStrength = dyno.dynoFloat(4.5);
- const uIsReforming = dyno.dynoFloat(0.0);
- const uReformTime = dyno.dynoFloat(0.0);
- const uReformDuration = dyno.dynoFloat(2.0);
-
- const uIsBirthing = dyno.dynoFloat(0.0);
- const uBirthTime = dyno.dynoFloat(0.0);
- const uBirthDuration = dyno.dynoFloat(0.5);
-
- function createDeathDynoshader() {
- return dyno.dynoBlock(
- { gsplat: dyno.Gsplat },
- { gsplat: dyno.Gsplat },
- ({ gsplat }) => {
- const physicsShader = new dyno.Dyno({
- inTypes: {
- gsplat: dyno.Gsplat,
- time: "float",
- dropTime: "float",
- dropProgress: "float",
- gravity: "float",
- bounceDamping: "float",
- floorLevel: "float",
- randomFactor: "float",
- reformSpeed: "float",
- cycleDuration: "float",
- friction: "float",
- shrinkSpeed: "float",
- explosionStrength: "float",
- isReforming: "float",
- reformTime: "float",
- reformDuration: "float",
- },
- outTypes: { gsplat: dyno.Gsplat },
- globals: () => [
- dyno.unindent(`
- mat2 rot(float angle) { float c = cos(angle); float s = sin(angle); return mat2(c, -s, s, c); }
- float hash(vec3 p) { return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453); }
- vec3 simulatePhysics(vec3 originalPos, float dropTime, float progress, float gravity, float damping, float floorLevel, float randomOffset, float friction, float explosionStrength) {
- if (progress <= 0.0) return originalPos;
- float timeVariation = hash(originalPos + vec3(42.0)) * 0.2 - 0.1;
- float t = max(0.0, dropTime + timeVariation);
- vec3 initialVelocity = vec3(
- (hash(originalPos + vec3(1.0)) - 0.5) * explosionStrength * (0.3 + hash(originalPos + vec3(10.0)) * 0.4),
- abs(hash(originalPos + vec3(3.0))) * explosionStrength * (0.8 + hash(originalPos + vec3(20.0)) * 0.4) + 0.5,
- (hash(originalPos + vec3(2.0)) - 0.5) * explosionStrength * (0.3 + hash(originalPos + vec3(30.0)) * 0.4)
- );
- float frictionDecay = pow(friction, t * 60.0);
- vec3 position = originalPos;
- position.x += initialVelocity.x * (1.0 - frictionDecay) / (1.0 - friction) / 60.0;
- position.z += initialVelocity.z * (1.0 - frictionDecay) / (1.0 - friction) / 60.0;
- position.y += initialVelocity.y * t - 0.5 * gravity * t * t;
- if (position.y <= floorLevel) {
- float bounceTime = t;
- float bounceCount = floor(bounceTime * 3.0);
- float timeSinceBounce = bounceTime - bounceCount / 3.0;
- float bounceHeight = initialVelocity.y * pow(damping, bounceCount) * max(0.0, 1.0 - timeSinceBounce * 3.0);
- if (bounceHeight > 0.1) {
- position.y = floorLevel + abs(sin(timeSinceBounce * 3.14159 * 3.0)) * bounceHeight;
- } else {
- position.y = floorLevel;
- float scatterFactor = hash(originalPos + vec3(50.0)) * 0.2;
- position.x += (hash(originalPos + vec3(60.0)) - 0.5) * scatterFactor;
- position.z += (hash(originalPos + vec3(70.0)) - 0.5) * scatterFactor;
- }
- }
- return position;
- }
- vec3 elegantReform(vec3 currentPos, vec3 originalPos, float reformTime, float duration) {
- if (reformTime <= 0.0) return currentPos;
- if (reformTime >= duration) return originalPos;
- float progress = reformTime / duration;
- return mix(currentPos, originalPos, progress);
- }
- vec3 reformScale(vec3 currentScale, vec3 originalScale, float reformTime, float duration) {
- if (reformTime <= 0.0) return currentScale;
- if (reformTime >= duration) return originalScale;
- float progress = reformTime / duration;
- float easeOut = 1.0 - pow(1.0 - progress, 2.0);
- return mix(currentScale, originalScale, easeOut);
- }
- `),
- ],
- statements: ({ inputs, outputs }) =>
- dyno.unindentLines(`
- ${outputs.gsplat} = ${inputs.gsplat};
- vec3 originalPos = ${inputs.gsplat}.center;
- vec3 originalScale = ${inputs.gsplat}.scales;
- vec3 physicsPos = originalPos;
- vec3 currentScale = originalScale;
- if (${inputs.dropProgress} > 0.0) {
- float randomOffset = hash(originalPos) * ${inputs.randomFactor};
- physicsPos = simulatePhysics(originalPos, ${inputs.dropTime}, ${inputs.dropProgress}, ${inputs.gravity}, ${inputs.bounceDamping}, ${inputs.floorLevel}, randomOffset, ${inputs.friction}, ${inputs.explosionStrength});
- float factor = exp(-${inputs.dropTime} * ${inputs.shrinkSpeed});
- currentScale = mix(originalScale, vec3(0.005), 1.0 - factor);
- }
- vec3 finalPos = physicsPos;
- vec3 finalScale = currentScale;
- if (${inputs.isReforming} > 0.5) {
- finalPos = elegantReform(physicsPos, originalPos, ${inputs.reformTime}, ${inputs.reformDuration});
- finalScale = reformScale(currentScale, originalScale, ${inputs.reformTime}, ${inputs.reformDuration});
- }
- ${outputs.gsplat}.center = finalPos;
- ${outputs.gsplat}.scales = finalScale;
- `),
- });
- gsplat = physicsShader.apply({
- gsplat,
- time: animationTime,
- dropTime: uDropTime,
- dropProgress: uDropProgress,
- gravity: uGravity,
- bounceDamping: uBounceDamping,
- floorLevel: uFloorLevel,
- randomFactor: uRandomFactor,
- reformSpeed: uReformSpeed,
- cycleDuration: uCycleDuration,
- friction: uFriction,
- shrinkSpeed: uShrinkSpeed,
- explosionStrength: uExplosionStrength,
- isReforming: uIsReforming,
- reformTime: uReformTime,
- reformDuration: uReformDuration,
- }).gsplat;
- return { gsplat };
- },
- );
- }
-
- function createBirthDynoshader() {
- return dyno.dynoBlock(
- { gsplat: dyno.Gsplat },
- { gsplat: dyno.Gsplat },
- ({ gsplat }) => {
- const birthShader = new dyno.Dyno({
- inTypes: {
- gsplat: dyno.Gsplat,
- time: "float",
- isBirthing: "float",
- birthTime: "float",
- birthDuration: "float",
- },
- outTypes: { gsplat: dyno.Gsplat },
- globals: () => [
- dyno.unindent(`
- float hash(vec3 p) { return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453); }
- `),
- ],
- statements: ({ inputs, outputs }) =>
- dyno.unindentLines(`
- ${outputs.gsplat} = ${inputs.gsplat};
- vec3 originalPos = ${inputs.gsplat}.center;
- vec3 originalScale = ${inputs.gsplat}.scales;
- if (${inputs.isBirthing} > 0.5 && ${inputs.birthTime} < ${inputs.birthDuration}) {
- float progress = ${inputs.birthTime} / ${inputs.birthDuration};
- float birthOffset = hash(originalPos) * 0.1;
- float adjusted = clamp((progress - birthOffset / ${inputs.birthDuration}) / (1.0 - birthOffset / ${inputs.birthDuration}), 0.0, 1.0);
- float ease = pow(adjusted * adjusted * (3.0 - 2.0 * adjusted), 0.6);
- vec3 birthPos = mix(vec3(0.0), originalPos, ease);
- vec3 birthScale = mix(vec3(0.0), originalScale, ease);
- ${outputs.gsplat}.center = birthPos;
- ${outputs.gsplat}.scales = birthScale;
- float alpha = ${inputs.gsplat}.rgba.a * ease;
- ${outputs.gsplat}.rgba.a = alpha;
- }
- `),
- });
- gsplat = birthShader.apply({
- gsplat,
- time: animationTime,
- isBirthing: uIsBirthing,
- birthTime: uBirthTime,
- birthDuration: uBirthDuration,
- }).gsplat;
- return { gsplat };
- },
- );
- }
-
- const splatMeshes = {};
- let currentSplatName = "penguin";
- let nextSplatName = "cat";
-
- async function loadSplats() {
- const splatNames = ["penguin.spz", "cat.spz", "woobles.spz"];
- for (const splatName of splatNames) {
- const splatURL = await getAssetFileURL(splatName);
- const mesh = new SplatMesh({ url: splatURL });
- await mesh.initialized;
- const nameKey = splatName.replace(".spz", "");
- mesh.worldModifier = createDeathDynoshader();
- mesh.updateGenerator();
- mesh.position.set(0, 0, 0);
- mesh.rotation.set(Math.PI, 0, 0);
- if (nameKey === "woobles") mesh.scale.set(1.7, 2.0, 1.7);
- else mesh.scale.set(1, 1, 1);
- mesh.visible = nameKey === currentSplatName;
- group.add(mesh);
- splatMeshes[nameKey] = mesh;
- }
- }
-
- async function loadTable() {
- const tableURL = await getAssetFileURL("table.glb");
- const loader = new GLTFLoader();
- await new Promise((resolve, reject) => {
- loader.load(
- tableURL,
- (gltf) => {
- const tableModel = gltf.scene;
- tableModel.position.set(0, -0.5, 0);
- tableModel.scale.set(5.5, 5.5, 5.5);
- tableModel.rotation.set(0, 0, 0);
- tableModel.traverse((child) => {
- if (child.isMesh) {
- child.castShadow = true;
- child.receiveShadow = true;
- }
- });
- group.add(tableModel);
- resolve(tableModel);
- },
- undefined,
- reject,
- );
- });
- }
-
- function _switchToSplat(name) {
- for (const m of Object.values(splatMeshes)) {
- if (m) m.visible = false;
- }
- if (splatMeshes[name]) {
- splatMeshes[name].visible = true;
- currentSplatName = name;
- }
- }
-
- function getNextSplatName(current) {
- const order = ["penguin", "cat", "woobles"];
- return order[(order.indexOf(current) + 1) % order.length];
- }
-
- const transitionState = {
- isTransitioning: false,
- transitionTime: 0.0,
- transitionDuration: 3.0,
- };
-
- function startExplosion() {
- if (transitionState.isTransitioning) return;
- transitionState.isTransitioning = true;
- transitionState.transitionTime = 0.0;
- for (const [name, mesh] of Object.entries(splatMeshes)) {
- if (mesh) mesh.visible = name === currentSplatName;
- }
- if (splatMeshes[currentSplatName]) {
- splatMeshes[currentSplatName].worldModifier = createDeathDynoshader();
- splatMeshes[currentSplatName].updateGenerator();
- }
- uDropProgress.value = 1.0;
- uDropTime.value = 0.0;
- uIsReforming.value = 0.0;
- }
-
- function startTransition() {
- startExplosion();
- nextSplatName = getNextSplatName(currentSplatName);
- if (splatMeshes[nextSplatName]) {
- for (const [name, mesh] of Object.entries(splatMeshes)) {
- if (!mesh) continue;
- if (name !== currentSplatName && name !== nextSplatName)
- mesh.visible = false;
- }
- splatMeshes[nextSplatName].worldModifier = createBirthDynoshader();
- splatMeshes[nextSplatName].updateGenerator();
- splatMeshes[nextSplatName].visible = true;
- uIsBirthing.value = 1.0;
- uBirthTime.value = 0.0;
- }
- }
-
- function completeTransition() {
- uIsBirthing.value = 0.0;
- uBirthTime.value = 0.0;
- currentSplatName = nextSplatName;
- transitionState.isTransitioning = false;
- transitionState.transitionTime = 0.0;
- }
-
- await Promise.all([loadSplats(), loadTable()]);
- if (disposed) return { group, update: () => {}, dispose, setupGUI };
-
- // Instructional text
- const instructionsText = textSplats({
- text: "WASD + mouse to move\nSPACEBAR: Explosion!",
- font: "Arial",
- fontSize: 24,
- color: new THREE.Color(0xffffff),
- textAlign: "center",
- lineHeight: 1.3,
- });
- instructionsText.scale.setScalar(0.15 / 24);
- instructionsText.position.set(0, 0.2, 2.5);
- group.add(instructionsText);
-
- let totalTime = 0;
- const transitionParams = { autoTransition: true };
-
- function onKeyDown(e) {
- if (e.code === "Space") {
- e.preventDefault();
- if (transitionState.isTransitioning) completeTransition();
- startTransition();
- totalTime = 0;
- }
- }
- window.addEventListener("keydown", onKeyDown);
-
- function update(dt, t) {
- animationTime.value = t;
- // Update camera controls
- controls.update(camera);
- if (transitionParams.autoTransition) {
- if (!transitionState.isTransitioning) totalTime += dt;
- if (!transitionState.isTransitioning && totalTime >= 1.0) {
- startTransition();
- totalTime = 0;
- }
- }
- if (transitionState.isTransitioning) {
- transitionState.transitionTime += dt;
- uBirthTime.value = transitionState.transitionTime;
- if (transitionState.transitionTime >= transitionState.transitionDuration)
- completeTransition();
- }
- uDropTime.value += dt;
- const dying = splatMeshes[currentSplatName];
- const birthing = splatMeshes[nextSplatName];
- if (transitionState.isTransitioning) {
- if (dying) dying.updateVersion();
- if (birthing) birthing.updateVersion();
- } else {
- if (dying) dying.updateVersion();
- }
- }
-
- function setupGUI(folder) {
- const params = { explosionStrength: uExplosionStrength.value };
- folder
- .add(params, "explosionStrength", 0.0, 10.0, 0.1)
- .name("Explosion Strength")
- .onChange((v) => {
- uExplosionStrength.value = v;
- });
- folder.add(transitionParams, "autoTransition").name("Auto Transition");
- folder
- .add({ explode: () => startTransition() }, "explode")
- .name("Trigger Explosion");
- return folder;
- }
-
- function dispose() {
- disposed = true;
- window.removeEventListener("keydown", onKeyDown);
- // Disable controls to avoid interfering with other effects
- controls.fpsMovement.enable = false;
- controls.pointerControls.enable = false;
- scene.remove(group);
- }
-
- return { group, update, dispose, setupGUI };
-}
diff --git a/examples/splat-transitions/effects/flow.js b/examples/splat-transitions/effects/flow.js
deleted file mode 100644
index 1cbf72da..00000000
--- a/examples/splat-transitions/effects/flow.js
+++ /dev/null
@@ -1,308 +0,0 @@
-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 };
-}
diff --git a/examples/splat-transitions/effects/morph.js b/examples/splat-transitions/effects/morph.js
deleted file mode 100644
index bbd2b607..00000000
--- a/examples/splat-transitions/effects/morph.js
+++ /dev/null
@@ -1,228 +0,0 @@
-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 };
-}
diff --git a/examples/splat-transitions/effects/spheric.js b/examples/splat-transitions/effects/spheric.js
deleted file mode 100644
index 040ba720..00000000
--- a/examples/splat-transitions/effects/spheric.js
+++ /dev/null
@@ -1,278 +0,0 @@
-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;
-
- // Params and uniforms
- const PARAMETERS = {
- splatCoverage: 1.0,
- spereRadius: 1.0,
- sphereHeight: 2.0,
- speedMultiplier: 1.0,
- rotation: true,
- pause: false,
- };
-
- const time = dyno.dynoFloat(0.0);
-
- // Camera baseline
- const _prevPos = camera.position.clone();
- const prevLook = new THREE.Vector3();
- camera.getWorldDirection(prevLook);
-
- const SPHERICAL_TARGET = new THREE.Vector3(0, 2, 0);
- camera.position.set(5, 4, 7);
- camera.lookAt(SPHERICAL_TARGET);
-
- const skyFile = "dali-env.glb";
- const sceneFile = "dali-table.glb";
- const splatFiles = ["penguin.spz", "dessert.spz", "woobles.spz"];
-
- async function loadDelitGLB(filename, isEnv = false) {
- const url = await getAssetFileURL(filename);
- const gltfLoader = new GLTFLoader();
- const gltf = await new Promise((resolve, reject) => {
- gltfLoader.load(url, resolve, undefined, reject);
- });
- const root = gltf.scene;
- root.traverse((child) => {
- if (child.isMesh && child.material) {
- const original = child.material;
- const basic = new THREE.MeshBasicMaterial();
- if (original.color) basic.color.copy(original.color);
- if (original.map) basic.map = original.map;
- if (isEnv) {
- basic.side = THREE.BackSide;
- if (basic.map) {
- basic.map.mapping = THREE.EquirectangularReflectionMapping;
- basic.map.colorSpace = THREE.LinearSRGBColorSpace;
- basic.map.needsUpdate = true;
- }
- }
- child.material = basic;
- }
- });
- return root;
- }
-
- function getTransitionState(t, fadeInTime, fadeOutTime, period) {
- 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() {
- 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);
- 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 });
- await splatMesh.initialized;
- 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();
- splatMesh.quaternion.set(1, 0, 0, 0);
- return splatMesh;
- }
-
- // Load env and assets
- const sky = await loadDelitGLB(skyFile, true);
- if (!disposed) group.add(sky);
- const table = await loadDelitGLB(sceneFile, false);
- const sceneScale = 3.5;
- table.scale.set(sceneScale, sceneScale, sceneScale);
- table.position.set(-1, 0, -0.8);
- if (!disposed) group.add(table);
-
- const period = dyno.dynoFloat(splatFiles.length);
- const spereRadiusDyno = dyno.dynoFloat(PARAMETERS.spereRadius);
- const splatCoverageDyno = dyno.dynoFloat(PARAMETERS.splatCoverage);
- const sphereHeightDyno = dyno.dynoFloat(PARAMETERS.sphereHeight);
-
- const meshes = [];
- for (let i = 0; i < splatFiles.length; i++) {
- const mesh = await morphableSplatMesh(
- splatFiles[i],
- time,
- dyno.dynoFloat(i),
- dyno.dynoFloat((i + 1) % splatFiles.length),
- period,
- splatCoverageDyno,
- spereRadiusDyno,
- sphereHeightDyno,
- );
- if (!disposed) group.add(mesh);
- meshes.push(mesh);
- }
-
- function update(dt, _t) {
- if (!PARAMETERS.pause) {
- time.value += dt * 0.5 * PARAMETERS.speedMultiplier;
- if (PARAMETERS.rotation) {
- for (const m of meshes) {
- m.rotation.y += dt * PARAMETERS.speedMultiplier;
- }
- }
- }
- // Keep camera centered on spherical target
- camera.lookAt(SPHERICAL_TARGET);
- }
-
- function setupGUI(folder) {
- folder.add(PARAMETERS, "spereRadius", 0.1, 8.0, 0.01).onChange((v) => {
- spereRadiusDyno.value = v;
- });
- folder.add(PARAMETERS, "sphereHeight", -1.0, 4.0, 0.01).onChange((v) => {
- sphereHeightDyno.value = v;
- });
- folder.add(PARAMETERS, "splatCoverage", 0.1, 2.0, 0.01).onChange((v) => {
- splatCoverageDyno.value = v;
- });
- folder.add(PARAMETERS, "speedMultiplier", 0.25, 4.0, 0.01);
- folder.add(PARAMETERS, "rotation");
- folder.add(PARAMETERS, "pause");
- return folder;
- }
-
- function dispose() {
- disposed = true;
- // Remove group
- scene.remove(group);
- // No global listeners here; controls are managed by main
- }
-
- return { group, update, dispose, setupGUI };
-}
diff --git a/examples/splat-transitions/index.html b/examples/splat-transitions/index.html
deleted file mode 100644
index 329202e0..00000000
--- a/examples/splat-transitions/index.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
- Spark • Splat Transitions
-
-
-
-
- Loading...
-
-
-
-
-
-
diff --git a/examples/splat-transitions/main.js b/examples/splat-transitions/main.js
deleted file mode 100644
index f33e97f0..00000000
--- a/examples/splat-transitions/main.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import { SparkRenderer } from "@sparkjsdev/spark";
-import { GUI } from "lil-gui";
-import * as THREE from "three";
-
-// Central renderer/scene/camera shared by effects
-const canvas = document.getElementById("canvas");
-const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
-renderer.setPixelRatio(window.devicePixelRatio);
-renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
-renderer.setClearColor(0x000000, 1);
-
-const scene = new THREE.Scene();
-const spark = new SparkRenderer({ renderer });
-scene.add(spark);
-
-const camera = new THREE.PerspectiveCamera(
- 50,
- canvas.clientWidth / canvas.clientHeight,
- 0.01,
- 2000,
-);
-camera.position.set(0, 3, 8);
-camera.lookAt(0, 0, 0);
-scene.add(camera);
-
-// Resize handling
-function handleResize() {
- const w = canvas.clientWidth;
- const h = canvas.clientHeight;
- renderer.setSize(w, h, false);
- camera.aspect = w / h;
- camera.updateProjectionMatrix();
-}
-window.addEventListener("resize", handleResize);
-
-// GUI
-const gui = new GUI();
-const params = { Effect: "Spherical" };
-const effectFiles = {
- Spherical: () => import("./effects/spheric.js"),
- Explosion: () => import("./effects/explosion.js"),
- Flow: () => import("./effects/flow.js"),
- Morph: () => import("./effects/morph.js"),
-};
-
-let active = null; // { api, group }
-let last = 0;
-let effectFolder = null; // GUI folder for current effect
-let switchCounter = 0; // guards concurrent effect switches
-
-async function switchEffect(name) {
- const myToken = ++switchCounter;
- const loading = document.getElementById("loading");
- loading.textContent = `Loading ${name}...`;
- loading.style.display = "block";
-
- // Dispose previous
- if (active) {
- try {
- active.api.dispose?.();
- } catch {}
- if (active.group) scene.remove(active.group);
- active = null;
- }
-
- // Destroy previous GUI folder to avoid accumulation
- if (effectFolder) {
- try {
- effectFolder.destroy();
- } catch {}
- effectFolder = null;
- }
-
- const loader = effectFiles[name];
- if (!loader) return;
- const preChildren = new Set(scene.children);
- const mod = await loader();
- if (myToken !== switchCounter) {
- // A newer switch started; ignore this one
- return;
- }
-
- const context = { THREE, scene, camera, renderer, spark };
- const api = await mod.init(context);
- if (myToken !== switchCounter) {
- try {
- api.dispose?.();
- } catch {}
- // Remove any children added during this init
- for (const child of [...scene.children]) {
- if (!preChildren.has(child)) scene.remove(child);
- }
- return;
- }
-
- if (api.group) scene.add(api.group);
- active = { api, group: api.group };
-
- // Setup a per-effect GUI folder if exposed
- if (api.setupGUI) {
- effectFolder = gui.addFolder(name);
- api.setupGUI(effectFolder);
- }
-
- loading.style.display = "none";
-
- // Give focus back to the canvas so keyboard controls work immediately
- try {
- canvas.focus();
- } catch {}
-}
-
-gui.add(params, "Effect", Object.keys(effectFiles)).onChange(switchEffect);
-
-// Animation loop
-renderer.setAnimationLoop((timeMs) => {
- const t = timeMs * 0.001;
- const dt = t - (last || t);
- last = t;
-
- if (active?.api?.update) active.api.update(dt, t);
- renderer.render(scene, camera);
-});
-
-// Kickoff
-switchEffect(params.Effect);