UNPKG

lume

Version:

Build next-level interactive web applications.

352 lines (305 loc) 13.2 kB
// Work started at https://discourse.threejs.org/t/12503/35 import {Scene as ThreeScene} from 'three/src/scenes/Scene.js' import {WebGLRenderer} from 'three/src/renderers/WebGLRenderer.js' import type {Camera, Object3D, Renderer} from 'three' import type * as THREE from 'three' /** * Allows rendering objects into one ore more visual layers that are stacked on * top of each other. Think of it like layers in Adobe Photoshop. */ export class VisualLayers { #layers: Array<Layer> = [] #renderer: Renderer #Scene: typeof ThreeScene /** * @param {THREE.Renderer} renderer The `THREE.Renderer` (f.e. `THREE.WebGLRenderer`) that * will be used to render the layers. * @param {typeof THREE.Scene} Scene The `THREE.Scene` class that will be used for each layer * (one per layer). If not provided, `THREE.Scene` is used by default. */ // IDEA: Optionally accept different Scene types per layer. constructor(renderer: Renderer, Scene = ThreeScene) { this.#renderer = renderer this.#Scene = Scene } /** * Deletes all defined layers -- hence un-references contained objects so * they can be garbage collected -- as if starting with a fresh new * VisualLayers. Generally you should call this if you are getting rid * of layering, or want to define a new set of layers, etc. */ dispose(): void { this.#layers.length = 0 } /** * Defines a new layer. * @param {LayerName} layerName The name to give the layer. * @param {number} order The order it will have. The newly-defined layer will * render above other layers that have lower numbers, and below other layers * that have higher order numbers. * @returns {Layer} The created object representing the layer. */ defineLayer(layerName: LayerName, order = 0): Layer { const layer = this.#getOrMakeLayer(layerName) const previousOrder = layer.order layer.order = order // Sort only if order changed. if (previousOrder !== layer.order) this.#layers.sort((a, b) => a.order - b.order) return layer } /** * Set the visibility of one or more layers. * @param {LayerNames} layerNames The name of a layer (or array of names of layers) that will have its (their) visibility set. * @param {boolean} visible A boolean indicating whether the layer or layers should be visible. */ setLayerVisible(layerNames: LayerNames, visible: boolean) { if (typeof layerNames == 'string') return this.#setLayerVisible(layerNames, visible) for (const name of layerNames) this.#setLayerVisible(name, visible) } #setLayerVisible(layerName: LayerName, visible: boolean) { const layer = this.#layers.find(l => l.name === layerName) if (!layer) throw new Error('Can not set visibility of layer that does not exist.') layer.visible = visible } /** Get a layer by name (if it doesn't exist, creates it with default order 0). */ #getOrMakeLayer(layerName: LayerName): Layer { let layer = this.#layers.find(l => l.name === layerName) if (!layer) { layer = {name: layerName, backingScene: new this.#Scene(), order: 0, visible: true} // @ts-expect-error legacy layer.backingScene.autoUpdate = false // three <0.144 layer.backingScene.matrixWorldAutoUpdate = false // three >=0.144 this.#layers.push(layer) } return layer } /** * Remove a layer. * @param {LayerName} layerName The name of the layer to remove. */ removeLayer(layerName: LayerName): void { const index = this.#layers.findIndex(l => { if (l.name === layerName) { l.backingScene.children.length = 0 return true } return false }) if (index >= 0) this.#layers.splice(index, 1) } /** * Check if a layer exists. * @param {LayerName} layerName The name of the layer to check existence of. * @returns {boolean} A boolean indicating if the layer exists. */ hasLayer(layerName: LayerName): boolean { return this.#layers.some(l => l.name === layerName) } /** * The number of layers. * @readonly * @type {number} */ get layerCount(): number { return this.#layers.length } /** * Add an object (anything that is or extends from THREE.Object3D) to the named layer (or named layers). * * @param {THREE.Object3D} obj The object to add. Must be an `instanceof THREE.Object3D`. * * @param {LayerNames} layerNames The name of a layer (or array of names of layers) that * the object will be added to. If an object is added to multiple layers, the * object will be rendered multiple times, once per layer. * * @param {boolean | undefined} withSubtree When true, this causes an object that was added into * specified layer(s) be rendered with its (grand)children, rather than it * being rendered only by itself without any of its (grand)children. * * It is useful for `withSubtree` to be set to `false` (the default) when you * want to have a hierarchy with different parts of the hierarchy rendered * in different layers * * On the other hand, sometimes you have a whole tree that you want to put in * a layer and don’t want to have to specify layers for all of the sub-nodes. * Set `withSubtree` to `true` in this case to add a root node to a layer * to render that whole subtree in that layer. * * It is easier to add a whole tree into a layer with `withSubtree` as * `true`. When `withSubtree` is `false` each node in a subtree would * need to be added to a layer manually, but this allows more fine grained control of * which parts of a tree will render in various layers. */ addObjectToLayer(obj: Object3D, layerNames: LayerNames, withSubtree = false): void { if (typeof layerNames == 'string') return this.#addObjectToLayer(obj, layerNames, withSubtree) for (const name of layerNames) this.#addObjectToLayer(obj, name, withSubtree) } /** * Similar to `addObjectToLayer`, but for adding multiple objects at once. * @param {THREE.Object3D[]} objects An array of objects that are `instanceof THREE.Object3D`. * @param {LayerNames} layerNames The layer or layers to add the objects to. * @param {boolean | undefined} withSubtree Whether rendering of the objects will also render their * children. See `withSubtree` of `addObjectToLayer` for more details. */ addObjectsToLayer(objects: Object3D[], layerNames: LayerNames, withSubtree = false): void { for (const obj of objects) this.addObjectToLayer(obj, layerNames, withSubtree) } /** * Add an object to all currently-defined layers. * @param {THREE.Object3D} obj The object to add. Must be an `instanceof THREE.Object3D`. * @param {boolean | undefined} withSubtree Whether rendering of the object will also render its * children. See `withSubtree` of `addObjectToLayer` for more details. */ addObjectToAllLayers(obj: Object3D, withSubtree = false): void { for (const layer of this.#layers) this.#addObjectToLayer(obj, layer.name, withSubtree) } /** * Add a set of objects to all currently-defined layers. * @param {THREE.Object3D[]} objects The list of `THREE.Object3D` instances to add. * @param {boolean | undefined} withSubtree Whether rendering of the objects will also render their * children. See `withSubtree` of `addObjectToLayer` for more details. */ addObjectsToAllLayers(objects: Object3D[], withSubtree = false): void { for (const obj of objects) this.addObjectToAllLayers(obj, withSubtree) } readonly #emptyArray = Object.freeze([]) #addObjectToLayer(obj: Object3D, layerName: LayerName, withSubtree: boolean): void { const layer = this.#getOrMakeLayer(layerName) if (!this.#layerHasObject(layer, obj)) { const proxy = Object.create(obj, withSubtree ? {} : {children: {get: () => this.#emptyArray}}) // We use `children.push()` here instead of `children.add()` so that the // added child will not be removed from its parent in its original scene. // This allows us to add an object to multiple layers, and to not // interfere with the user's original tree. layer.backingScene.children.push(proxy) } } #layerHasObject(layer: Layer, obj: Object3D): boolean { return layer.backingScene.children.some(proxy => (proxy as any).__proto__ === obj) } /** * Remove an object from a layer or set of layers. * @param {THREE.Object3D} obj The object to remove from the specified layer or layers. * @param {LayerNames} layerNames The layer or layers from which to remove the object from. */ removeObjectFromLayer(obj: Object3D, layerNames: LayerNames): void { if (typeof layerNames == 'string') { const layer = this.#layers.find(l => l.name === layerNames) return this.#removeObjectFromLayer(obj, layer) } for (const name of layerNames) { const layer = this.#layers.find(l => l.name === name) this.#removeObjectFromLayer(obj, layer) } } /** * Remove an object from a layer or set of layers. * @param {THREE.Object3D[]} objects The objects to remove from the specified layer or layers. * @param {LayerNames} layerNames The layer or layers from which to remove the object from. */ removeObjectsFromLayer(objects: Object3D[], layerNames: LayerNames): void { for (const obj of objects) this.removeObjectFromLayer(obj, layerNames) } /** * Remove the given object from all layers. * @param {THREE.Object3D} obj The object to remove. */ removeObjectFromAllLayers(obj: Object3D): void { for (const layer of this.#layers) this.#removeObjectFromLayer(obj, layer) } /** * Remove the given objects from all layers they may belong to. * @param {THREE.Object3D[]} objects The objects to remove. */ removeObjectsFromAllLayers(objects: Object3D[]): void { for (const obj of objects) this.removeObjectFromAllLayers(obj) } #removeObjectFromLayer(obj: Object3D, layer: Layer | undefined) { if (!layer) throw new Error('Can not remove object from layer that does not exist.') const children = layer.backingScene.children const index = children.findIndex(proxy => (proxy as any).__proto__ === obj) if (index >= 0) { children[index] = children[children.length - 1]! children.pop() } } /** * Render visible layers. * * @param {THREE.Camera} camera A THREE.Camera to render all the layers with. * * @param {BeforeAllCallback | undefined} beforeAll Optional: Called before rendering all layers. If not * supplied, the default value will turn off the rendere's auto clearing, so that * each layer can be manually drawn stacked on top of each other. * * @param {BeforeEachCallback | undefined} beforeEach Optional: When the layers are being rendered in the order they are * defined to be in, this callback will be called right before a layer is * rendered. It will be passed the name of the layer that is about to be * rendered. By default, this does nothing. * * @param {AfterEachCallback | undefined} afterEach Optional: When the layers are being rendered in the order * they are defined to be in, this callback will be called right after a * layer is rendered. It will be passed the name of the layer that was just * rendered. The default is that `clearDepth()` will be called on a * `WebGLRenderer` to ensure layers render on top of each other from low * order to high order. If you provide your own callback, you'll have to * remember to call `clearDepth` manually, unless you wish for layers to blend into * the same 3D space rather than appaering as separate scenes stacked on * top of each other. */ // IDEA: Allow different cameras per layer? It may not be common, but could // be useful for, for example, making background effects, etc. render( camera: Camera, beforeAll: BeforeAllCallback = this.#defaultBeforeAllCallback, beforeEach: BeforeEachCallback = this.#defaultBeforeEachCallback, afterEach: AfterEachCallback = this.#defaultAfterEachCallback, ) { beforeAll() for (const layer of this.#layers) { if (!layer.visible) continue beforeEach(layer.name) this.#renderer.render(layer.backingScene, camera) afterEach(layer.name) } } #defaultBeforeAllCallback = () => { if (this.#renderer instanceof WebGLRenderer) { this.#renderer.autoClear = false this.#renderer.clear() } } #defaultBeforeEachCallback = () => {} #defaultAfterEachCallback = () => { // By default, the depth of a WebGLRenderer is cleared, so that layers // render on top of each other in order from lowest to highest order value. if (this.#renderer instanceof WebGLRenderer) this.#renderer.clearDepth() } } /** @typedef {string} LayerName */ type LayerName = string /** @typedef {LayerName | LayerName[]} LayerNames */ type LayerNames = LayerName | LayerName[] /** @typedef {{name: LayerName; backingScene: THREE.Scene; order: number; visible: boolean}} Layer */ type Layer = {name: LayerName; backingScene: THREE.Scene; order: number; visible: boolean} /** * @typedef { * @function * @param {LayerName} layerName * } BeforeEachCallback */ type BeforeEachCallback = (layerName: LayerName) => void /** * @typedef { * @function * } BeforeAllCallback */ type BeforeAllCallback = () => void /** * @typedef { * @function * @param {LayerName} layerName * @returns {void} * } AfterEachCallback */ type AfterEachCallback = (layerName: LayerName) => void