Is it posible in Three.js to have a mesh always rendered on top of the scene, even if it\'s position is behind all objects? I\'m implementing a lasso selection with a mesh a
The following is a working example that has a VisualLayers class for managing any number of layers, and it uses the renderer.autoClear = false and clearing-depth technique like West Langley mentioned in his answer.
This approach is nice because renderOrder of objects is not being modified (that's another approach) and thus will not introduce other different issues, and you can save it for other use cases decoupled from layering.
Try playing with the options in the UI to see what it does:
// @ts-check
////////////////////////
// LAYER SYSTEM
////////////////////////
/** @typedef {{name: string, backingScene: THREE.Scene, order: number}} Layer */
class VisualLayers {
/**
* @type {Array}
* @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;
}
(example on codepen)