import { make_box_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_box_geometry.js";
import { make_octahedron_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_octahedron_geometry.js";
import { createScene } from "./scene.js";
import { flatGrid, tiles } from "./procedural.js";
const WIDTH = 7, LENGTH = 4.5, DEPTH = 1.2, LEVEL = -.1; // the pool, in metres
const STONES = 8, DROP_HEIGHT = 2.6, GRAVITY = 9.81, SINK = .5;
const scene = await createScene({ distance: 6.2, yaw: Math.PI + .2, pitch: .34, focus: [0, -.3, -.2], sun: [-.45, -.55, -.7] });
// A deep-blue pool, so the sky it reflects outlines every ripple.
const stone = scene.material("#b9ae9b", { roughness: .9 });
const deck = 14;
for (const [w, d, x, z] of [
[deck, LENGTH + 2 * deck, -(WIDTH + deck) / 2, 0], [deck, LENGTH + 2 * deck, (WIDTH + deck) / 2, 0],
[WIDTH, deck, 0, (LENGTH + deck) / 2], [WIDTH, deck, 0, -(LENGTH + deck) / 2],
]) scene.solid(make_box_geometry(w, DEPTH + .1, d), stone, x, -(DEPTH + .1) / 2, z);
const tile = scene.material("#ffffff", { texture: await scene.canvasTexture(tiles(2, 2, "#1f5d85", "#174866")), roughness: .4 });
scene.solid(flatGrid(WIDTH, LENGTH, 1, undefined, .5), tile, 0, -DEPTH, 0);
for (const [w, d, x, z] of [
[.02, LENGTH, -WIDTH / 2 + .01, 0], [.02, LENGTH, WIDTH / 2 - .01, 0],
[WIDTH, .02, 0, LENGTH / 2 - .01], [WIDTH, .02, 0, -LENGTH / 2 + .01],
]) scene.solid(make_box_geometry(w, DEPTH, d), tile, x, -DEPTH / 2, z);
const pool = scene.waterBody(flatGrid(WIDTH, LENGTH, 8), { at: [0, LEVEL, 0], preset: "swimming_pool", shoreReflection: .6 });
const inside = (x, z, margin = 0) => Math.abs(x) < WIDTH / 2 - margin && Math.abs(z) < LENGTH / 2 - margin;
const splash = (x, z, radius, kick) => scene.water.splash(pool.entity, [x, LEVEL, z], radius, kick);
const input = id => document.getElementById(id);
const size = () => Number(input("size").value), kick = () => Number(input("kick").value);
for (const [id, digits, apply] of [
["size", 2], ["kick", 1],
["reflection", 2, value => pool.waves.shoreReflection.set(value)],
]) {
input(id).addEventListener("input", () => {
input(`${id}-value`).value = Number(input(id).value).toFixed(digits);
apply?.(Number(input(id).value));
});
}
// A click splashes where it meets the water.
scene.onTap(position => {
const hit = scene.hitWater(position, [pool], (body, point) => inside(point[0], point[2]));
if (hit) splash(hit.point[0], hit.point[2], size(), kick());
});
// Stones fall from above the pool, splash as they enter and sink to the floor.
const pebble = scene.material("#6f7680", { roughness: .7 });
const stones = Array.from({ length: STONES }, () => ({
...scene.solid(make_octahedron_geometry(.07, 3), pebble, 0, -10, 0), state: "idle", x: 0, y: -10, z: 0, speed: 0, rest: 0,
}));
let nextStone = 0, rainDebt = 0;
scene.onFrame(dt => {
if (dt === 0) return;
nextStone -= dt;
if (input("stones").checked && nextStone <= 0) {
const free = stones.find(s => s.state === "idle");
if (free) {
Object.assign(free, { state: "falling", x: (Math.random() - .5) * (WIDTH - 1), y: DROP_HEIGHT, z: (Math.random() - .5) * (LENGTH - 1), speed: 0 });
}
nextStone = .9;
}
for (const s of stones) {
if (s.state === "falling") {
s.speed += GRAVITY * dt;
s.y -= s.speed * dt;
if (s.y <= LEVEL) {
// the faster it enters, the harder it kicks
splash(s.x, s.z, .25, Math.min(3, s.speed * .3));
s.state = "sinking";
}
} else if (s.state === "sinking") {
s.y = Math.max(-DEPTH + .07, s.y - SINK * dt);
if (s.y <= -DEPTH + .07) { s.state = "resting"; s.rest = 3; }
} else if (s.state === "resting" && (s.rest -= dt) <= 0) {
// back out of sight under the floor
s.state = "idle";
s.y = -10;
} else if (s.state === "idle") {
continue;
}
scene.move(s, s.x, s.y, s.z);
}
if (input("rain").checked) {
// drops a few centimetres across, forty a second
for (rainDebt += 40 * dt; rainDebt >= 1; rainDebt--) {
splash((Math.random() - .5) * WIDTH * .98, (Math.random() - .5) * LENGTH * .98, .05, .5);
}
}
});
document.querySelector("fieldset").disabled = false;
import { Attribute } from "@woosh/meep-engine/src/shade/renderer/geometry/Attribute.js";
import { Geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/Geometry.js";
import { StandardAttributes } from "@woosh/meep-engine/src/shade/renderer/geometry/StandardAttributes.js";
const FLAT = () => 0;
/**
* A flat grid in local XZ facing up. Water uses it as the outline of a body; floors use it with a
* texture, whose coordinates run 0..1 across the grid, or repeat every `texel` metres when given.
* @param {number|number[]} [segments] cells along the shorter side, with the longer side getting
* as many more as keeps them square; or [across the width, along the length]
* @param {function(number, number): number} [height] y at (x, z)
*/
export function flatGrid(width, length, segments = 1, height = FLAT, texel = 0) {
const [sx, sz] = Array.isArray(segments) ? segments : [
Math.max(1, Math.round(segments * Math.max(1, width / length))),
Math.max(1, Math.round(segments * Math.max(1, length / width))),
];
const position = [], normal = [], uv = [], index = [];
for (let j = 0; j <= sz; j++) {
for (let i = 0; i <= sx; i++) {
const x = (i / sx - .5) * width, z = (j / sz - .5) * length;
position.push(x, height(x, z), z);
normal.push(0, 1, 0);
uv.push(texel > 0 ? x / texel : i / sx, texel > 0 ? z / texel : j / sz);
}
}
for (let j = 0; j < sz; j++) {
for (let i = 0; i < sx; i++) {
const a = j * (sx + 1) + i, b = a + 1, c = a + sx + 1, d = c + 1;
index.push(a, c, b, b, c, d); // counter-clockwise seen from above
}
}
const geometry = new Geometry();
geometry.index = Attribute.from(new Uint32Array(index), 1, "index");
geometry.addAttribute(Attribute.from(new Float32Array(position), 3, StandardAttributes.Position));
geometry.addAttribute(Attribute.from(new Float32Array(normal), 3, StandardAttributes.Normal));
geometry.addAttribute(Attribute.from(new Float32Array(uv), 2, StandardAttributes.TextureCoordinates0));
if (height !== FLAT) recomputeNormals(geometry);
return geometry;
}
function recomputeNormals(geometry) {
const p = geometry.getAttribute(StandardAttributes.Position).data;
const n = geometry.getAttribute(StandardAttributes.Normal).data;
const index = geometry.index.data;
n.fill(0);
for (let t = 0; t < index.length; t += 3) {
const a = index[t] * 3, b = index[t + 1] * 3, c = index[t + 2] * 3;
const ux = p[b] - p[a], uy = p[b + 1] - p[a + 1], uz = p[b + 2] - p[a + 2];
const vx = p[c] - p[a], vy = p[c + 1] - p[a + 1], vz = p[c + 2] - p[a + 2];
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
for (const v of [a, b, c]) { n[v] += nx; n[v + 1] += ny; n[v + 2] += nz; }
}
for (let i = 0; i < n.length; i += 3) {
const l = Math.hypot(n[i], n[i + 1], n[i + 2]) || 1;
n[i] /= l; n[i + 1] /= l; n[i + 2] /= l;
}
}
/** A disk in local XZ facing up, with texture coordinates 0..1 across its square. */
export function disk(radius, segments = 48) {
const position = [0, 0, 0], normal = [0, 1, 0], uv = [.5, .5], index = [];
for (let i = 0; i < segments; i++) {
const angle = i / segments * Math.PI * 2, x = Math.cos(angle), z = Math.sin(angle);
position.push(x * radius, 0, z * radius);
normal.push(0, 1, 0);
uv.push(.5 + x / 2, .5 + z / 2);
index.push(0, 1 + (i + 1) % segments, 1 + i); // counter-clockwise seen from above
}
const geometry = new Geometry();
geometry.index = Attribute.from(new Uint32Array(index), 1, "index");
geometry.addAttribute(Attribute.from(new Float32Array(position), 3, StandardAttributes.Position));
geometry.addAttribute(Attribute.from(new Float32Array(normal), 3, StandardAttributes.Normal));
geometry.addAttribute(Attribute.from(new Float32Array(uv), 2, StandardAttributes.TextureCoordinates0));
return geometry;
}
/** Square tiles with grout lines; `lane` adds a dark stripe down the middle, as a pool floor has. */
export function tiles(across, down, face, grout, lane = false) {
const cell = 64, canvas = document.createElement("canvas");
canvas.width = across * cell;
canvas.height = down * cell;
const g = canvas.getContext("2d");
g.fillStyle = grout;
g.fillRect(0, 0, canvas.width, canvas.height);
g.fillStyle = face;
for (let y = 0; y < down; y++) for (let x = 0; x < across; x++) g.fillRect(x * cell + 3, y * cell + 3, cell - 6, cell - 6);
if (lane) {
g.fillStyle = "#1d4f78";
g.fillRect(cell, (down / 2 - .5) * cell + 8, (across - 2) * cell, cell - 16);
}
return canvas;
}
/** Bands of colour across v, or across u when `vertical`. */
export function stripes(colours, count, vertical = false) {
const canvas = document.createElement("canvas");
canvas.width = vertical ? 512 : 8;
canvas.height = vertical ? 8 : 512;
const g = canvas.getContext("2d"), size = 512 / count;
for (let i = 0; i < count; i++) {
g.fillStyle = colours[i % colours.length];
if (vertical) g.fillRect(i * size, 0, size, 8);
else g.fillRect(0, i * size, 8, size);
}
return canvas;
}
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import TopDownCameraController from "@woosh/meep-engine/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js";
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { System } from "@woosh/meep-engine/src/engine/ecs/System.js";
import { Transform64 } from "@woosh/meep-engine/src/engine/ecs/transform/Transform64.js";
import { t64_announce_change } from "@woosh/meep-engine/src/engine/ecs/transform/t64_announce_change.js";
import { ShadedGeometry } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics3/ShadedGeometrySystem.js";
import Water from "@woosh/meep-engine/src/engine/graphics/ecs/water/Water.js";
import WaterWaves from "@woosh/meep-engine/src/engine/graphics/ecs/water/WaterWaves.js";
import { WaterSystem } from "@woosh/meep-engine/src/engine/graphics3/WaterSystem.js";
import { GPUFluidSystem } from "@woosh/meep-engine/src/engine/graphics3/GPUFluidSystem.js";
import { FluidEffectorsComponent } from "@woosh/meep-engine/src/engine/physics/fluid/ecs/FluidEffectorsComponent.js";
import { FluidEffectorsSystem } from "@woosh/meep-engine/src/engine/physics/fluid/ecs/FluidEffectorsSystem.js";
import { GlobalFluidEffector } from "@woosh/meep-engine/src/engine/physics/fluid/effector/GlobalFluidEffector.js";
import { water_medium_preset } from "@woosh/meep-engine/src/shade/renderer/medium/water/water_presets.js";
import { StandardShadeMaterial } from "@woosh/meep-engine/src/shade/renderer/material/StandardShadeMaterial.js";
import { ColorSpace } from "@woosh/meep-engine/src/shade/renderer/texture/ColorSpace.js";
import { ShadeImage } from "@woosh/meep-engine/src/shade/renderer/texture/source/ShadeImage.js";
import { ShadeTexture } from "@woosh/meep-engine/src/shade/renderer/texture/ShadeTexture.js";
import { meshlet_geometry_build_from_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.js";
import { meshlet_geometry_from_bytes } from "@woosh/meep-engine/src/shade/renderer/geometry/meshlet_geometry_transfer.js";
import { make_sky_hosek } from "@woosh/meep-engine/src/engine/graphics/sh3/path_tracer/make_sky_hosek.js";
import { Sampler2D } from "@woosh/meep-engine/src/engine/graphics/texture/sampler/Sampler2D.js";
import { sampler2d_to_f16 } from "@woosh/meep-engine/src/engine/graphics/texture/sampler/sampler2d_to_f16.js";
import { octahedral_uv_to_direction } from "@woosh/meep-engine/src/shade/renderer/light/environment/octahedral_uv_to_direction.js";
import { Color } from "@woosh/meep-engine/src/core/color/Color.js";
import Vector2 from "@woosh/meep-engine/src/core/geom/Vector2.js";
import Vector3 from "@woosh/meep-engine/src/core/geom/Vector3.js";
/**
* Engine, sky and water system for a water example.
* @param {object} options
* @param {number[]} options.sun direction the sunlight travels
*/
export async function createScene({ distance = 12, yaw = .4, pitch = .45, focus = [0, 0, 0], sun = [-.45, -.5, -.75], sunIntensity = 5 } = {}) {
let water, frame = () => {}, playing = true;
// Pause hands the water and the example a zero time step; the camera keeps moving.
class PausableWaterSystem extends WaterSystem {
update(dt) { super.update(playing ? dt : 0); }
}
class FrameSystem extends System {
dependencies = [Transform64];
update(dt) { frame(playing ? dt : 0); }
}
const engine = await EngineHarness.bootstrap({
configuration(config, engine) {
const scene = EngineHarness.shadeScene(engine);
config.addSystem(new ShadedGeometrySystem(engine.graphics, scene));
water = new PausableWaterSystem(engine.graphics, scene);
config.addSystem(water);
// The scene's wind, which raises the waves: GlobalFluidEffectors folded into a far
// field and published for the GPU. With no GPUFluidComponent the far field is all of it.
config.addSystem(new FluidEffectorsSystem());
config.addSystem(new GPUFluidSystem(engine.graphics, scene));
config.addSystem(new FrameSystem());
},
});
await EngineHarness.buildBasics({
engine, enableTerrain: false, enableWater: false, enableLights: false,
focus: new Vector3(...focus), distance, pitch, yaw,
// Shade spreads the sun's shadow cascades over the camera's whole near..far range, so
// far is three opening distances and zooming out stops at two: the scene stays inside
// far at any zoom, and the cascades are not stretched over empty ground.
cameraFarDistance: distance * 3, showFps: false,
});
engine.entityManager.dataset.getAnyComponent(TopDownCameraController).component.distanceMax = distance * 2;
await EngineHarness.buildLights({
engine, sunIntensity, sunDirection: new Vector3(...sun).normalize(), shadowmapResolution: 2048, castShadow: true,
});
engine.graphics.renderer.feature_automatic_exposure_enabled = false;
engine.graphics.set_environment_map(skyTexture(sun.map(v => -v)));
const ecd = engine.entityManager.dataset;
const weather = new GlobalFluidEffector();
weather.drag = 1;
const effectors = new FluidEffectorsComponent();
effectors.addEffector(weather);
new Entity().add(effectors).build(ecd);
/** The scene's wind 10 m up, metres per second, blowing `direction` radians from +X towards +Z. */
function wind(speed, direction = 0) {
weather.wind = [speed * Math.cos(direction), 0, speed * Math.sin(direction)];
}
function material(hex, { texture, roughness = .8 } = {}) {
const m = new StandardShadeMaterial();
m.diffuse_color.copy(Color.from_sRGB_to_linear(Color.parse(hex)));
m.roughness_factor = roughness;
if (texture) m.texture_albedo = texture;
return m;
}
async function canvasTexture(canvas) {
const image = ShadeImage.fromImageBitmap(await createImageBitmap(canvas));
image.color_space = ColorSpace.SRGB;
return ShadeTexture.from(image);
}
// A Geometry is built into meshlets here; a MeshletGeometry baked offline is used as it is.
function solid(geometry, surface, x = 0, y = 0, z = 0) {
const transform = new Transform64();
transform.setTranslation(x, y, z);
const meshlets = geometry.isMeshletGeometry ? geometry : meshlet_geometry_build_from_geometry(geometry);
const entity = new Entity().add(transform)
.add(ShadedGeometry.from(meshlets, typeof surface === "string" ? material(surface) : surface))
.build(ecd);
return { entity, transform };
}
function move(object, x, y, z) {
object.transform.setTranslation(x, y, z);
object.transform.updateMatrix();
t64_announce_change(ecd, object.entity);
}
/** A body of water: its mesh is the outline, the transform places it, the medium fills it. */
function waterBody(geometry, { at = [0, 0, 0], preset = "clear_ocean", roughness = .02, shoreReflection = .5 } = {}) {
const component = new Water();
component.medium = water_medium_preset({ preset });
component.roughness.set(roughness);
const waves = new WaterWaves();
waves.shoreReflection.set(shoreReflection);
const transform = new Transform64();
transform.setTranslation(...at);
transform.updateMatrix();
const entity = new Entity().add(transform).add(meshlet_geometry_build_from_geometry(geometry)).add(component).add(waves).build(ecd);
return { entity, water: component, waves, level: at[1] };
}
// Where a click meets a body's surface at rest, or nothing.
const tap = new Vector2(), ndc = new Vector2(), origin = new Vector3(), direction = new Vector3();
function hitWater(position, bodies, inside) {
tap.set(position.x, position.y);
engine.graphics.normalizeViewportPoint(tap, ndc);
engine.graphics.viewportProjectionRay(ndc.x, ndc.y, origin, direction);
let best = null;
for (const body of bodies) {
if (Math.abs(direction.y) < 1e-6) continue;
const t = (body.level - origin.y) / direction.y;
if (!(t > 0) || (best && t >= best.t)) continue;
const point = [origin.x + direction.x * t, body.level, origin.z + direction.z * t];
if (inside(body, point)) best = { body, point, t };
}
return best;
}
const pause = document.getElementById("pause");
pause?.addEventListener("click", () => {
playing = !playing;
pause.textContent = playing ? "Pause" : "Resume";
pause.setAttribute("aria-pressed", String(!playing));
});
return {
engine, ecd, water, wind, material, canvasTexture, solid, move, waterBody, hitWater,
onFrame: callback => { frame = callback; },
onTap: callback => engine.devices.pointer.on.tap.add(callback),
};
}
async function fetchOk(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`${url}: ${response.status} ${response.statusText}`);
return response;
}
/** A gzipped `MeshletGeometry` baked offline; loading it is an inflate and a copy, with no geometry work. */
export async function fetchMeshlets(url) {
const response = await fetchOk(url);
return meshlet_geometry_from_bytes(await new Response(response.body.pipeThrough(new DecompressionStream("gzip"))).arrayBuffer());
}
/** An image file as a colour texture; the browser decodes it off the main thread. */
export async function fetchTexture(url) {
const image = ShadeImage.fromImageBitmap(await createImageBitmap(await (await fetchOk(url)).blob(), { premultiplyAlpha: "none" }));
image.color_space = ColorSpace.SRGB;
return ShadeTexture.from(image);
}
// A clear afternoon sky (Hosek-Wilkie) as the environment, so the water has a sky to reflect.
function skyTexture(toSun) {
const size = 256, sky = make_sky_hosek(toSun, 2.5, 0, [.25, .23, .2]);
const sampler = Sampler2D.float32(4, size, size), direction = [0, 0, 0];
for (let v = 0; v < size; v++) {
for (let u = 0; u < size; u++) {
octahedral_uv_to_direction(direction, (u + .5) / size, (v + .5) / size);
const offset = (v * size + u) * 4;
sky(sampler.data, offset, direction, 0);
sampler.data[offset + 3] = 1;
}
}
const image = ShadeImage.fromSampler2D(sampler2d_to_f16(sampler));
image.color_space = ColorSpace.LinearSRGB;
return ShadeTexture.from(image);
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<link rel="icon" href="data:,">
<title>Splashes · Meep</title>
<style>
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #080c16; color: #edf3ff; font: 14px system-ui, sans-serif; }
.panel { position: fixed; z-index: 100; left: 20px; bottom: 20px; width: 290px; padding: 16px; background: #080c16de; border: 1px solid #29344b; border-radius: 14px; backdrop-filter: blur(12px); }
fieldset { border: 0; padding: 0; margin: 0; }
fieldset > :first-child { margin-top: 0; }
label { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 12px 0 6px; }
label.toggle { justify-content: flex-start; margin: 8px 0; }
output { color: #6bedc2; font-variant-numeric: tabular-nums; }
input { accent-color: #6bedc2; }
input[type=range] { width: 100%; }
button { margin-top: 12px; background: #23314a; color: inherit; border: 1px solid #465675; padding: 7px 16px; border-radius: 8px; cursor: pointer; }
button:focus-visible, input:focus-visible { outline: 2px solid #6bedc2; outline-offset: 3px; }
:disabled { opacity: .5; }
.hint { position: fixed; z-index: 100; bottom: 20px; right: 20px; margin: 0; color: #a8b6cd; font-size: 12px; pointer-events: none; }
@media (max-width: 650px) { .panel { left: 10px; bottom: 10px; width: 250px; padding: 12px; } .hint { display: none; } }
</style>
</head>
<body>
<section class="panel" aria-label="Splash controls">
<fieldset disabled>
<label for="size">Splash radius, m <output id="size-value">0.20</output></label>
<input id="size" type="range" min="0.05" max="0.6" step="0.01" value="0.2">
<label for="kick">Kick, m/s <output id="kick-value">1.5</output></label>
<input id="kick" type="range" min="0.2" max="4" step="0.1" value="1.5">
<label for="reflection">Wall reflection <output id="reflection-value">0.60</output></label>
<input id="reflection" type="range" min="0" max="1" step="0.05" value="0.6">
<label class="toggle"><input id="stones" type="checkbox" checked>Drop stones</label>
<label class="toggle"><input id="rain" type="checkbox">Rain</label>
<button id="pause" type="button" aria-pressed="false">Pause</button>
</fieldset>
</section>
<p class="hint">Click water to splash · Drag to orbit · Scroll to zoom</p>
<script type="module" src="./src/main.js"></script>
</body>
</html>
{
"name": "@meep-examples/water-splash",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Click a pool to splash it; stones and rain make wave rings that spread and reflect off the walls.",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@woosh/meep-engine": "3.55.0"
},
"devDependencies": {
"@rollup/plugin-strip": "^3.0.4",
"vite": "^8.0.13"
}
}
# Splashes
A calm pool. Click the water and a ring of waves spreads out, fades and bounces off the walls. Stones drop in on their own, and rain can be switched on.
Run `npm ci` and `npm run dev`, then open `/demo.html`. Build with `npm run build`.
Every splash is one call, `WaterSystem.splash(entity, position, radius, kick)`: a world position, a radius that is half the wavelength of the ring, and a vertical kick in m/s. The engine turns it into a ring of wave particles on the GPU. Each particle carries a short packet of waves outward at the group speed of its wavelength, so a wide splash sends long waves that travel fast, and a raindrop sends short, slow ones that fade quickly. At the mesh's outline a particle reflects; `WaterWaves.shoreReflection`, the wall-reflection slider, sets how much of its amplitude comes back.
The stones fall under gravity and splash where they enter, with a kick from their speed, then sink to the floor. The pool has no wind, so the rings are the only waves.
`vite.config.js` keeps the asserts in `WaterSystem.js` when it strips the rest: `splash` computes its world-to-local matrix inside one.
`src/main.js` builds the pool, the stones and the rain; `src/scene.js` boots the engine, the sky and `WaterSystem`; `src/procedural.js` makes the grids and canvas textures.
import { defineConfig } from "vite";
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
import strip from "@rollup/plugin-strip";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
root: __dirname,
publicDir: false,
// Keep the engine's imported asset URLs intact during development.
optimizeDeps: {
exclude: ["@woosh/meep-engine"],
include: ["dat.gui", "opentype.js"],
},
plugins: [
{
name: "copy-thumbnail",
apply: "build",
closeBundle() {
const thumb = resolve(__dirname, "thumbnail.png");
const dst = resolve(__dirname, "../../public/examples/water-splash");
if (existsSync(thumb)) {
mkdirSync(dst, { recursive: true });
copyFileSync(thumb, resolve(dst, "thumbnail.png"));
}
},
},
],
base: "./",
build: {
outDir: resolve(__dirname, "../../public/examples/water-splash"),
emptyOutDir: false,
rollupOptions: {
input: resolve(__dirname, "demo.html"),
plugins: [
{
// WaterSystem.splash computes its world-to-local matrix inside an assert, so stripping
// that module's asserts would put every splash at the body's origin.
...strip({ exclude: ["**/engine/graphics3/WaterSystem.js"] }),
apply: 'build'
}
],
},
target: "es2022",
},
});
thumbnail.png · 216.1 KB