mirror of
https://github.com/storytold/spark.git
synced 2026-10-20 08:04:33 +00:00
Interactive ripples effect (#194)
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>spark | splat-shockwave</title>
|
||||||
|
<style>
|
||||||
|
html, body, #canvas { width: 100%; height: 100%; margin: 0; padding: 0; display: block; }
|
||||||
|
body { background: #000; overflow: hidden; }
|
||||||
|
.legend {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
<div class="legend">Click to generate ripples • WASD + Mouse to move camera</div>
|
||||||
|
<script type="module" src="./main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
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();
|
||||||
Reference in New Issue
Block a user