Files
spark/examples/interactive-ripples/main.js
T
2025-10-16 11:19:20 -07:00

157 lines
5.0 KiB
JavaScript

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();