// @ts-check
////////////////////////
// LAYER SYSTEM
////////////////////////
/** @typedef {{name: string, backingScene: THREE.Scene, order: number}} Layer */
class VisualLayers {
/**
* @type {Array<Layer>}
* @private
*/
__layers = [];
constructor(
/** @private @type {THREE.WebGLRenderer} */ __renderer,
/** @private @type {typeof THREE.Scene} */ __Scene = THREE.Scene
) {
this.__renderer = __renderer;
this.__Scene = __Scene;
}
defineLayer(/** @type {string} */ name, /** @type {number=} */ order = 0) {
const layer = this.__getLayer(name);
// The default layer always has order 0.
const previousOrder = layer.order;
layer.order = name === "default" ? 0 : order;
// Sort only if order changed.
if (previousOrder !== layer.order)
this.__layers.sort((a, b) => a.order - b.order);
return layer;
}
/**
* Get a layer by name (if it doesn't exist, creates it with default order 0).
* @private
*/
__getLayer(/** @type {string} */ name) {
let layer = this.__layers.find((l) => l.name === name);
if (!layer) {
layer = { name, backingScene: new this.__Scene(), order: 0 };
layer.backingScene.autoUpdate = false;
this.__layers.push(layer);
}
return layer;
}
removeLayer(/** @type {string} */ name) {
const index = this.__layers.findIndex((l) => {
if (l.name === name) {
l.backingScene.children.length = 0;
return true;
}
return false;
});
if (index >= 0) this.__layers.splice(index, 1);
}
hasLayer(/** @type {string} */ name) {
return this.__layers.some((l) => l.name === name);
}
/** @readonly */
get layerCount() {
return this.__layers.length;
}
addObjectToLayer(
/** @type {THREE.Object3D} */ obj,
/** @type {string | string[]} */ layers
) {
if (Array.isArray(layers)) {
for (const name of layers) this.__addObjectToLayer(obj, name);
return;
}
this.__addObjectToLayer(obj, layers);
}
addObjectsToLayer(
/** @type {THREE.Object3D[]} */ objects,
/** @type {string | string[]} */ layers
) {
for (const obj of objects) {
this.addObjectToLayer(obj, layers);
}
}
/** @private @readonly */
__emptyArray = Object.freeze([]);
/** @private */
__addObjectToLayer(
/** @type {THREE.Object3D} */ obj,
/** @type {string} */ name
) {
const layer = this.__getLayer(name);
const proxy = Object.create(obj, {
children: { get: () => this.__emptyArray }
});
layer.backingScene.children.push(proxy);
}
removeObjectFromLayer(
/** @type {THREE.Object3D} */ obj,
/** @type {string | string[]} */ nameOrNames
) {
if (Array.isArray(nameOrNames)) {
for (const name of nameOrNames) {
const layer = this.__layers.find((l) => l.name === name);
if (!layer) continue;
this.__removeObjectFromLayer(obj, layer);
}
return;
}
const layer = this.__layers.find((l) => l.name === nameOrNames);
if (!layer) return;
this.__removeObjectFromLayer(obj, layer);
}
/** @private */
__removeObjectFromLayer(
/** @type {THREE.Object3D} */ obj,
/** @type {Layer} */ layer
) {
const children = layer.backingScene.children;
const index = children.findIndex(
(proxy) => /** @type {any} */ (proxy).__proto__ === obj
);
if (index >= 0) {
children[index] = children[children.length - 1];
children.pop();
}
}
removeObjectsFromAllLayers(/** @type {THREE.Object3D[]} */ objects) {
for (const layer of this.__layers) {
for (const obj of objects) {
this.__removeObjectFromLayer(obj, layer);
}
}
}
render(
/** @type {THREE.Camera} */ camera,
/** @type {(layerName: string) => void} */ beforeEach,
/** @type {(layerName: string) => void} */ afterEach
) {
for (const layer of this.__layers) {
beforeEach(layer.name);
this.__renderer.render(layer.backingScene, camera);
afterEach(layer.name);
}
}
}
//////////////////////
// VARS
//////////////////////
let camera, stats, geometry, material, object, object2, root;
let time = 0;
/** @type {THREE.Scene} */
let scene;
/** @type {THREE.WebGLRenderer} */
let renderer;
/** @type {VisualLayers} */
let visualLayers;
const clock = new THREE.Clock();
const greenColor = "#27ae60";
const options = {
useLayers: true,
showMiddleBox: true,
rotate: true,
layer2Order: 2
};
//////////////////////
// INIT
//////////////////////
~(function init() {
setup3D();
renderLoop();
})();
////////////////////////////////
// SETUP 3D
////////////////////////////////
function setup3D() {
const container = document.createElement("div");
container.id = "container";
document.body.appendChild(container);
// CAMERA
camera = new THREE.PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
1,
10000
);
camera.position.x = 0;
camera.position.z = 500;
camera.position.y = 0;
scene = new THREE.Scene();
// RENDERERS
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setClearColor(0x111111);
container.appendChild(renderer.domElement);
// LAYERS
visualLayers = new VisualLayers(renderer);
// Layers don't have to be defined. Adding an object to a layer will
// automatically create the layer with order 0. But let's define layers with
// order values.
visualLayers.defineLayer("layer1", 1);
visualLayers.defineLayer("layer2", 2);
visualLayers.defineLayer("layer3", 3);
// LIGHTS
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(300, 0, 300);
scene.add(directionalLight);
visualLayers.addObjectToLayer(directionalLight, [
"layer1",
"layer2",
"layer3"
]);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
visualLayers.addObjectToLayer(ambientLight, ["layer1", "layer2", "layer3"]);
// GEOMETRY
root = new THREE.Object3D();
scene.add(root);
geometry = new THREE.BoxGeometry(100, 100, 100);
material = new THREE.MeshPhongMaterial({
color: greenColor,
transparent: false,
opacity: 1
});
object = new THREE.Mesh(geometry, material);
root.add(object);
visualLayers.addObjectToLayer(object, "layer1");
object.position.y = 80;
object.position.z = -20;
// object.rotation.y = -Math.PI / 5
object2 = new THREE.Mesh(geometry, material);
object.add(object2);
visualLayers.addObjectToLayer(object2, "layer2");
object2.position.y -= 80;
object2.position.z = -20;
object2.rotation.y = -Math.PI / 5;
const object3 = new THREE.Mesh(geometry, material);
object2.add(object3);
visualLayers.addObjectToLayer(object3, "layer3");
object3.position.y -= 80;
object3.position.z = -20;
object3.rotation.y = -Math.PI / 5;
// GUI
const pane = new Tweakpane({
title: "VisualLayers"
});
pane.addInput(options, "useLayers", { label: "use layers" });
pane.addInput(options, "showMiddleBox", { label: "show middle box" });
pane.addInput(options, "rotate");
pane
.addInput(options, "layer2Order", {
label: "layer2 order",
options: {
0: 0,
2: 2,
4: 4
}
})
.on("change", () => visualLayers.defineLayer("layer2", options.layer2Order));
// STATS
// SEE: https://github.com/mrdoob/stats.js
stats = new Stats();
stats.domElement.style.position = "absolute";
stats.domElement.style.left = "0px";
stats.domElement.style.top = "0px";
stats.setMode(0);
document.body.appendChild(stats.domElement);
}
//////////////////////
// RESIZE
//////////////////////
(window.onresize = function (event) {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
})();
//////////////////////
// RAF RENDER LOOP
//////////////////////
function render() {
stats.begin();
if (options.rotate) {
time += clock.getDelta();
object.rotation.y += 0.02;
root.rotation.y = Math.PI / 2 + (Math.PI / 6) * Math.sin(time * 0.001);
}
object2.visible = options.showMiddleBox;
if (options.useLayers) {
scene.updateWorldMatrix(true, true);
renderer.autoClear = false;
renderer.clear();
visualLayers.render(camera, beforeEachLayerRender, afterEachLayerRender);
} else {
renderer.autoClear = true;
renderer.render(scene, camera);
}
stats.end();
}
function renderLoop() {
render();
requestAnimationFrame(renderLoop);
}
function beforeEachLayerRender(layer) {}
function afterEachLayerRender(layer) {
renderer.clearDepth();
}
html,
body,
#container {
margin: 0px;
padding: 0px;
width: 100%;
height: 100%;
}
canvas {
background: transparent;
display: block;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/tweakpane.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/stats.js/r11/Stats.min.js"></script>
<script src="//unpkg.com/[email protected]/build/three.min.js"></script>
<script src="//unpkg.com/[email protected]/build/postprocessing.js"></script>