Skip to content

Instantly share code, notes, and snippets.

@colelawrence
Last active November 16, 2023 10:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save colelawrence/ae9cd23f62b7412520c00e0c2e5829c2 to your computer and use it in GitHub Desktop.
Save colelawrence/ae9cd23f62b7412520c00e0c2e5829c2 to your computer and use it in GitHub Desktop.
import {
Object3D, PerspectiveCamera, Scene,
WebGLRenderer
} from "three";
import { CleanupFn } from "./CleanupFn";
import { DisposePool, isDisposable } from "./DisposePool";
import { merge } from "./merge";
import { ISheet } from "@theatre/core";
import { TheatreMergeable, theatreMerge } from "./theatreMerge";
import { EventsPool } from "./EventsPool";
type SceneItemMutHelper<T> = {
self: T;
cleanup: CleanupFn;
};
type SceneAddOverrides<T extends object> = ({ theatreSheet: ISheet; name: string; } |
{ theatreSheet?: never; }) &
TheatreMergeable<T>;
type FrameFn = (deltaMs: number) => void;
export class SceneManager {
/** This scene needs to be rendered */
#invalid = false;
#pool: DisposePool;
#lastFrame = Date.now();
#onFrame: FrameFn[] = [];
#mutCleanup = (callback: () => void) => {
this.#pool.addfn(callback);
};
public readonly scene = new Scene();
public readonly camera: PerspectiveCamera;
constructor(
public readonly mount: HTMLElement,
public readonly renderer: WebGLRenderer,
camera: SceneAddOverrides<PerspectiveCamera>
) {
const pool = (this.#pool = new DisposePool());
this.render = this.render.bind(this);
pool.add(this.renderer);
this.camera = this.add(new PerspectiveCamera(), camera);
merge(this.renderer.domElement.style, {
position: "absolute",
inset: "0",
});
mount.appendChild(this.renderer.domElement);
this.#pool.addfn(mount.removeChild.bind(mount, this.renderer.domElement));
}
dispose() {
this.#pool.dispose();
}
events<T extends EventTarget>(target: T): EventsPool<T> {
const pool = new EventsPool(target);
this.#pool.add(pool);
return pool;
}
onFrame(fn: FrameFn) {
this.#onFrame.push(fn);
return () => {
const idx = this.#onFrame.indexOf(fn);
if (idx >= 0) {
this.#onFrame.splice(idx, 1);
}
};
}
add<T extends object>(
self: T,
overrides?: SceneAddOverrides<T>,
mut?: (helpers: SceneItemMutHelper<T>) => void
) {
if (overrides) {
const { theatreSheet, ...nonTheatreOverrides } = overrides;
// @ts-ignore
const name = nonTheatreOverrides.name;
const { groups } = theatreMerge(self, nonTheatreOverrides as any);
if (theatreSheet) {
for (const { keys, target, compoundProps } of groups) {
const obj = theatreSheet.object(
[name, keys.join(".")].filter(Boolean).join(" / "),
compoundProps,
{ reconfigure: true }
);
this.#pool.addfn(
obj.onValuesChange(
// create the function directly and bind the values for the on values change
// I'm not sure this is faster, but I'm pretty sure it is to compile it since
// we always know the exact values that will be updated.
new Function(
"t",
"v",
[
...Object.keys(compoundProps).map((k) => `t.${k} = v.${k}`),
target instanceof PerspectiveCamera
? "t.updateProjectionMatrix()"
: "",
"this.invalidate()",
].join(";")
).bind(this, target)
)
);
}
} else if (groups.length > 0) {
console.error(
"No theatreSheet provided for creating object theatre props",
groups
);
}
}
if (self instanceof Object3D) {
this.scene.add(self);
this.#pool.addfn(this.scene.remove.bind(this.scene, self));
}
if (isDisposable(self)) {
this.#pool.add(self);
}
mut?.({
self,
cleanup: this.#mutCleanup,
});
return self;
}
render = () => {
if (this.#pool.disposed || !this.#invalid) return;
const last = this.#lastFrame;
const now = Date.now();
const delta = now - last;
this.#lastFrame = now;
for (let i = 0; i < this.#onFrame.length; i++) {
this.#onFrame[i](delta);
}
this.#invalid = false;
this.renderer.render(this.scene, this.camera);
};
/** Schedule a render due to changed values */
invalidate() {
if (this.#invalid) return;
this.#invalid = true;
requestAnimationFrame(this.render);
}
/** Handle window resize event. */
onWindowResize() {
const width = this.mount.clientWidth;
const height = this.mount.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
this.render();
}
}
import { ISheet, getProject, types } from "@theatre/core";
const tn = (def: number, nudgeMultiplier = 0.01) =>
types.number(def, { nudgeMultiplier });
const t0n = (def: number, max: number) =>
types.number(def, { range: [0, max] });
const t01 = (def: number) => t0n(def, 1);
const tpos = (x: number, y: number, z: number) => ({
x: tn(x),
y: tn(y),
z: tn(z),
});
const pos = (x: number, y: number, z: number) => ({ x, y, z });
const trot = (x: number, y: number, z: number) => ({
x: tn(x, 0.01),
y: tn(y, 0.01),
z: tn(z, 0.01),
});
function addBoxes(ctx: SceneManager, sheet: ISheet) {
const geom = ctx.add(new BoxGeometry());
const mat = ctx.add(new MeshStandardMaterial(), {
name: "Box Material",
theatreSheet: sheet,
color: {
r: t0n(255, 255),
g: t0n(128, 255),
b: t0n(255, 255),
},
});
ctx.add(new Mesh(geom, mat), {
name: "box",
theatreSheet: sheet,
position: tpos(0, 0, 0),
rotation: tpos(-5, 0, 0),
castShadow: false,
receiveShadow: false,
});
ctx.add(new Mesh(geom, mat), {
name: "box2",
theatreSheet: sheet,
position: tpos(0, 0, 0),
rotation: tpos(-5, 0, 0),
castShadow: false,
receiveShadow: false,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment