main.ts
examples/spark/splat-transitions/main.ts · raw · open the example · all examples · site home
Exports: EffectContextEffect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
// Spark's splat-transitions: four transition effects picked from a menu
// (Spherical, Explosion, Flow, Morph), each a module in effects/ whose init
// builds the effect in a group of its own. Spark's per-mesh world modifiers
// become one generator per effect (js/worldEffect.js); scans load once and
// are shared between effects.
import GUI from "lil-gui";
import * as THREE from "three";
import type { WebGPURenderer } from "three/webgpu";
import { CameraControls } from "@sparkjsdev/radix";
import { asset } from "../js/assets.js";
import { SplatModel, SplatSet } from "../js/objects.js";
import { examplePage, windowRenderer } from "../js/page.js";
import { loadSplatFile, type LoadedFile } from "../js/files.js";
import { effectModel, worldEffectGenerator, type EffectPayload } from "../js/worldEffect.js";
type WorldEffect = Parameters<typeof worldEffectGenerator>[0];
// What an effect's init gets
export interface EffectContext {
renderer: WebGPURenderer;
// Placed by the effect (and animated by it, but in Explosion)
camera: THREE.PerspectiveCamera;
// Disabled: only the Explosion effect enables them
controls: CameraControls;
// In the scene while the effect is shown, with its splat models
group: THREE.Group;
// Scan i of `files` as object i of one world effect, upright, in the group
scans(files: string[], effect: WorldEffect, perObject?: (data: EffectPayload) => void): Promise<SplatModel[]>;
// A scan's table, GPU arrays and SH, loaded once
loadScan(file: string): Promise<LoadedFile>;
}
export interface Effect {
update(dt: number): void;
gui(folder: GUI): void;
dispose?(): void;
}
const page = examplePage("splat-transitions");
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.01, 2000);
// createRenderer, sized to the window and added to the page; it follows
// window resizes, the camera's aspect too (js/page.ts)
const renderer = await windowRenderer(camera, { antialias: true, directCanvas: false, pixelRatio: devicePixelRatio });
// Only the Explosion effect enables them
const controls = new CameraControls({ canvas: renderer.domElement, object: camera, storageKey: "spark-splat-transitions", settings: { moveSpeed: 3 } });
controls.enabled = false;
const splats = new SplatSet({ renderer, scene, animate: true });
const loaded = new Map<string, Promise<LoadedFile>>();
function loadScan(file: string) {
if (!loaded.has(file)) {
loaded.set(file, loadSplatFile(renderer, asset(file), { onProgress: page.progress(file) }));
}
return loaded.get(file)!;
}
function contextFor(group: THREE.Group): EffectContext {
return {
renderer, camera, controls, group, loadScan,
async scans(files, effect, perObject) {
const generator = worldEffectGenerator(effect, { perObject });
const scans = await Promise.all(files.map(loadScan));
return scans.map(({ arrays }, i) => {
const model = effectModel(generator, arrays, i);
// Spark's scans are upside down
model.rotation.x = Math.PI;
group.add(model);
return model;
});
},
};
}
const EFFECTS = {
Spherical: () => import("./effects/spheric.js"),
Explosion: () => import("./effects/explosion.js"),
Flow: () => import("./effects/flow.js"),
Morph: () => import("./effects/morph.js"),
};
type EffectName = keyof typeof EFFECTS;
const gui = new GUI();
let active: { effect: Effect; group: THREE.Group; folder: GUI } | undefined;
let switches = 0;
async function show(name: EffectName) {
const token = ++switches;
page.status(`Loading ${name}...`);
if (active) {
active.effect.dispose?.();
active.folder.destroy();
scene.remove(active.group);
splats.remove(...splats.models);
active = undefined;
}
const group = new THREE.Group();
const effect: Effect = await (await EFFECTS[name]()).init(contextFor(group));
if (token !== switches) {
effect.dispose?.();
return;
}
scene.add(group);
splats.add(...group.children.filter((child) => child instanceof SplatModel));
const folder = gui.addFolder(name);
effect.gui(folder);
active = { effect, group, folder };
page.status("");
}
gui.add({ Effect: "Spherical" }, "Effect", Object.keys(EFFECTS)).onChange((name: EffectName) => show(name));
await show("Spherical");
page.ready(renderer);
let last = 0;
renderer.setAnimationLoop((time) => {
const delta = last ? Math.min((time - last) / 1000, 0.1) : 0;
last = time;
controls.update();
active?.effect.update(delta);
renderer.render(scene, camera);
});