From c66abdeb276b3bd10c6e3ae24cfc8f8d32315757 Mon Sep 17 00:00:00 2001 From: kali-shade Date: Wed, 8 Apr 2026 23:00:04 -0300 Subject: [PATCH] deleted example effects that are now in effects folder --- examples/interactive-deform/index.html | 49 -- examples/interactive-deform/main.js | 318 ------------ examples/interactive-holes/index.html | 461 ------------------ examples/interactive-ripples/index.html | 30 -- examples/interactive-ripples/main.js | 156 ------ examples/splat-flow/index.html | 259 ---------- examples/splat-portal/index.html | 31 -- examples/splat-portal/main.js | 405 --------------- examples/splat-reveal-effects/index.html | 335 ------------- examples/splat-shader-effects/index.html | 242 --------- .../splat-transitions/effects/explosion.js | 414 ---------------- examples/splat-transitions/effects/flow.js | 308 ------------ examples/splat-transitions/effects/morph.js | 228 --------- examples/splat-transitions/effects/spheric.js | 278 ----------- examples/splat-transitions/index.html | 30 -- examples/splat-transitions/main.js | 126 ----- 16 files changed, 3670 deletions(-) delete mode 100644 examples/interactive-deform/index.html delete mode 100644 examples/interactive-deform/main.js delete mode 100644 examples/interactive-holes/index.html delete mode 100644 examples/interactive-ripples/index.html delete mode 100644 examples/interactive-ripples/main.js delete mode 100644 examples/splat-flow/index.html delete mode 100644 examples/splat-portal/index.html delete mode 100644 examples/splat-portal/main.js delete mode 100644 examples/splat-reveal-effects/index.html delete mode 100644 examples/splat-shader-effects/index.html delete mode 100644 examples/splat-transitions/effects/explosion.js delete mode 100644 examples/splat-transitions/effects/flow.js delete mode 100644 examples/splat-transitions/effects/morph.js delete mode 100644 examples/splat-transitions/effects/spheric.js delete mode 100644 examples/splat-transitions/index.html delete mode 100644 examples/splat-transitions/main.js 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);