Game effects: fx.js

game_open puts fx.js in the project root, next to geo.js. It holds a first-load setup (stage), canvas textures (tex), terrain and rule-based placement (field, place, instances), synthesized sound (sfx), particles (burst), camera shake (shake) and metre-scale texture coordinates (uv). Pure JS with no files to load. Nothing in it reads Math.random: the same seed gives the same pixels, points, particles and samples. Never edit it; this page lists its API.

import * as THREE from "three";
import { stage, tex, field, place, instances, sfx, burst, shake, uv } from "./fx.js";   // "../fx.js" from src/ in a vite project

stage

stage(container = document.body, { env, exposure, key, shadow, ground, background, fit }) returns { renderer, scene, camera, key, ground, fit }. It gives ACES tone mapping, sRGB output, a pixel ratio capped at 2, a studio environment, a key light with a shadow, a ground, a camera (a PerspectiveCamera with a 40° vertical fov at (3, 2.5, 5) looking at the origin, near 0.1 and far 1000 until fit) and a resize that follows the container: the setup faults game_look names are already fixed. It starts no render loop.

const { renderer, scene, camera, fit } = stage(document.body, { background: 0x1a2030, ground: false, key: 3 });
scene.environmentIntensity = 0.4;   // a game's own lit ground: the full studio light washes out the shadows
scene.add(level); fit(level);
renderer.setAnimationLoop(() => renderer.render(scene, camera));

Orbit controls are two lines of your own (the import map needs "three/addons/"): import { OrbitControls } from "three/addons/controls/OrbitControls.js"; new OrbitControls(camera, renderer.domElement).target.copy(fit(model));

tex

Each builder returns a THREE.CanvasTexture. Colour maps come tagged sRGB; normal is a data map and is not. noise, wood, bricks and windows tile (RepeatWrapping), so set map.repeat or use uv. Colours are anything THREE.Color takes.

const map = tex.bricks({ rows: 8, cols: 4 }); map.repeat.set(4, 2);
wall.material = new THREE.MeshStandardMaterial({ map, normalMap: tex.normal(map) });

field, place, instances

const land = field(100, { seed: 2, height: 5, basins: [[8, 6, 12, 7]] });   // a lake 7 deep at (8, 6)
scene.add(new THREE.Mesh(land.geometry(160), grassMat));
scene.add(instances(treeGeo, treeMat, place(150, { on: land, seed: 3, minHeight: 0, maxSlope: 30, scale: [0.8, 1.6], rotate: true }), "trees"));

sfx

Sound effects synthesized in code (a port of ZzFX), no audio files. Presets: coin, jump, hit, explode, shoot, laser, powerup, blip, bounce, thud, roll.

sfx.define("door", { frequency: 90, shape: 2, release: 0.3, slide: -1 }); sfx.play("door");
const r = sfx.play("roll"); if (r) { r.loop = true; r.playbackRate.value = speed; }   // a looping voice needs release: 0, as roll has

burst and shake

burst({ max = 500, seed = 1, size = 0.1, additive = true, ...defaults }) returns a Burst: one THREE.Points pool of soft round dots, positions in its own space. The other options are defaults for emit.

const sparks = burst({ max: 300, seed: 1, size: 0.1 }); sparks.name = "sparks"; scene.add(sparks);
sparks.emit(hit.point, { count: 40, speed: 6, spread: Math.PI / 4, color: 0xffaa33 });
const cam = shake(camera); cam.kick(0.3);
// each frame, after placing the camera: sparks.update(dt); cam.update(dt);

uv

uv(geometry, metresPerRepeat = 1, offset = 0) writes box-projected texture coordinates in the geometry's own units, so one tiling texture shows at the same real size on a floor, a wall, terrain and geo.js's boolean output (which has no UVs). It returns the geometry; call it before the first render. offset shifts u by that many metres, so buildings sharing a tex.windows tile do not show the same lit windows.

floor.geometry = uv(new THREE.BoxGeometry(6, 0.1, 5), 2);   // tex.wood repeats every 2 m

Pitfalls