Skip to content

Commit 1ecabe0

Browse files
authored
Interactive ripples effect (#194)
1 parent f035f4a commit 1ecabe0

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>spark | splat-shockwave</title>
7+
<style>
8+
html, body, #canvas { width: 100%; height: 100%; margin: 0; padding: 0; display: block; }
9+
body { background: #000; overflow: hidden; }
10+
.legend {
11+
position: absolute;
12+
top: 20px;
13+
left: 20px;
14+
color: white;
15+
font-family: Arial, sans-serif;
16+
font-size: 16px;
17+
background: rgba(0, 0, 0, 0.7);
18+
padding: 10px 15px;
19+
border-radius: 5px;
20+
pointer-events: none;
21+
z-index: 100;
22+
}
23+
</style>
24+
</head>
25+
<body>
26+
<canvas id="canvas"></canvas>
27+
<div class="legend">Click to generate ripples • WASD + Mouse to move camera</div>
28+
<script type="module" src="./main.js"></script>
29+
</body>
30+
</html>
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import {
2+
SparkControls,
3+
SparkRenderer,
4+
SplatMesh,
5+
dyno,
6+
} from "@sparkjsdev/spark";
7+
import * as THREE from "three";
8+
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
9+
10+
const canvas = document.getElementById("canvas");
11+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
12+
renderer.setPixelRatio(window.devicePixelRatio);
13+
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
14+
renderer.setClearColor(0x000000, 1);
15+
16+
const scene = new THREE.Scene();
17+
const spark = new SparkRenderer({ renderer });
18+
scene.add(spark);
19+
20+
const camera = new THREE.PerspectiveCamera(
21+
50,
22+
canvas.clientWidth / canvas.clientHeight,
23+
0.01,
24+
2000,
25+
);
26+
camera.position.set(0, 0, 3);
27+
camera.lookAt(0, 0, 0);
28+
scene.add(camera);
29+
30+
function handleResize() {
31+
const w = canvas.clientWidth;
32+
const h = canvas.clientHeight;
33+
renderer.setSize(w, h, false);
34+
camera.aspect = w / h;
35+
camera.updateProjectionMatrix();
36+
}
37+
window.addEventListener("resize", handleResize);
38+
39+
// Camera controls with mouse and WASD enabled
40+
const controls = new SparkControls({ canvas: renderer.domElement });
41+
controls.fpsMovement.enable = true; // Enable WASD movement
42+
controls.pointerControls.enable = true; // Enable mouse controls
43+
44+
// Dyno shader with time and shockwave function
45+
function passthroughDyno(timeUniform, hitpointUniform) {
46+
return dyno.dynoBlock(
47+
{ gsplat: dyno.Gsplat },
48+
{ gsplat: dyno.Gsplat },
49+
({ gsplat }) => {
50+
const shader = new dyno.Dyno({
51+
inTypes: {
52+
gsplat: dyno.Gsplat,
53+
time: "float",
54+
hitpoint: "vec3",
55+
},
56+
outTypes: { gsplat: dyno.Gsplat },
57+
globals: () => [
58+
dyno.unindent(`
59+
vec3 shockwave(vec3 center, float t, vec3 hitpoint) {
60+
vec3 direction = center - hitpoint;
61+
float distance = length(direction);
62+
center += normalize(direction)*sin(t*4.-distance*5.)*exp(-t)*smoothstep(t*2.,0.,distance)*.5;
63+
return center;
64+
}
65+
vec4 shockwaveColor(vec4 rgba, vec3 center, float t, vec3 hitpoint) {
66+
vec3 direction = center - hitpoint;
67+
float distance = length(direction);
68+
float wave = sin(t*4.-distance*5.)*exp(-t*.7)*smoothstep(t*2.,0.,distance);
69+
float brightness = pow(abs(wave),3.) * 10.; // Increase brightness on wave crests
70+
rgba.rgb += brightness;
71+
return rgba;
72+
}
73+
`),
74+
],
75+
statements: ({ inputs, outputs }) =>
76+
dyno.unindentLines(`
77+
${outputs.gsplat} = ${inputs.gsplat};
78+
// Apply shockwave function to position
79+
${outputs.gsplat}.center = shockwave(${inputs.gsplat}.center, ${inputs.time}, ${inputs.hitpoint});
80+
// Apply shockwave function to color
81+
${outputs.gsplat}.rgba = shockwaveColor(${inputs.gsplat}.rgba, ${inputs.gsplat}.center, ${inputs.time}, ${inputs.hitpoint});
82+
`),
83+
});
84+
return {
85+
gsplat: shader.apply({
86+
gsplat,
87+
time: timeUniform,
88+
hitpoint: hitpointUniform,
89+
}).gsplat,
90+
};
91+
},
92+
);
93+
}
94+
95+
async function run() {
96+
// Time and hitpoint uniforms for dyno shader
97+
const timeUniform = dyno.dynoFloat(0.0);
98+
const hitpointUniform = dyno.dynoVec3(new THREE.Vector3(0, 0, 1000)); // Initialize far away to avoid initial effect
99+
100+
// Load valley.spz
101+
const splatURL = await getAssetFileURL("valley.spz");
102+
const valley = new SplatMesh({ url: splatURL });
103+
await valley.initialized;
104+
105+
// Fix orientation - rotate 180 degrees around X axis
106+
valley.rotateX(Math.PI);
107+
108+
// Apply dyno shader with time and hitpoint uniforms
109+
valley.objectModifier = passthroughDyno(timeUniform, hitpointUniform);
110+
valley.updateGenerator();
111+
112+
scene.add(valley);
113+
114+
// Raycaster for click detection
115+
const raycaster = new THREE.Raycaster();
116+
raycaster.params.Points = { threshold: 1.0 }; // Increased threshold for better hit detection
117+
118+
// Simple time counter that resets on click
119+
let timeCounter = 0;
120+
121+
// Click event listener to set hitpoint and reset time
122+
renderer.domElement.addEventListener("pointerdown", (event) => {
123+
const rect = renderer.domElement.getBoundingClientRect();
124+
const ndc = new THREE.Vector2(
125+
((event.clientX - rect.left) / rect.width) * 2 - 1,
126+
-((event.clientY - rect.top) / rect.height) * 2 + 1,
127+
);
128+
raycaster.setFromCamera(ndc, camera);
129+
const hits = raycaster.intersectObject(valley, false);
130+
const hit = hits?.length ? hits[0] : null;
131+
132+
if (!hit) {
133+
return;
134+
}
135+
136+
const localPoint = valley.worldToLocal(hit.point.clone());
137+
// Don't invert Y or Z - keep original coordinates
138+
139+
hitpointUniform.value.copy(localPoint);
140+
timeCounter = 0; // Reset time counter
141+
});
142+
143+
renderer.setAnimationLoop((timeMs) => {
144+
// Increment time counter each frame
145+
timeCounter += 0.016; // ~60fps increment
146+
timeUniform.value = timeCounter;
147+
148+
// Update dyno uniforms to propagate to the mesh each frame
149+
valley.updateVersion();
150+
151+
controls.update(camera);
152+
renderer.render(scene, camera);
153+
});
154+
}
155+
156+
run();

0 commit comments

Comments
 (0)