Is it posible in Three.js to have a mesh always rendered on top of the scene, even if its position is behind all objects? I'm implementing a lasso selection with a mesh and I need to render the selecting box on top of the rest of the scene.
4 Answers
Yes.
First do this:
renderer.autoClear = false;
Then create a second scene that contains just the objects you want to be on top. Then, in your render loop:
renderer.clear(); // clear buffers
renderer.render( scene, camera ); // render scene 1
renderer.clearDepth(); // clear depth buffer
renderer.render( scene2, camera ); // render scene 2
three.js r.152

- 102,557
- 10
- 276
- 276
-
1Is this the only way? I was trying to achieve the same effect with mesh.renderDepth, material.depthTest and material.depthWrite but to no avail. – Andrew May 23 '14 at 23:10
-
@Andrew Please make a post if you have a question. – WestLangley May 24 '14 at 00:04
-
@Andrew trying to do the same right now, looking at http://stackoverflow.com/questions/16247280/force-a-sprite-object-to-always-be-in-front-of-skydome-object-in-three-js – sixFingers Jan 06 '15 at 17:47
-
@trusktr The answer is still valid. In your codepen, do `object2.renderOrder = 999; object2.onBeforeRender = function( renderer ) { renderer.clearDepth(); };` Please create a new issue if you have problems. – WestLangley Oct 08 '20 at 20:08
-
@WestLangley Thanks! I had a mistake. The answer works. Deleted my comment. – trusktr Oct 09 '20 at 03:34
-
I prefer the approach to use one scene and `renderOrder` with `onBeforeRender` – Gangula May 01 '23 at 13:18
If you want render only one mesh on front you can also manage by setting depthTest
for the material of that object to false
:
var onTopMaterial = new THREE.MeshStandardMaterial({
color: 'red',
depthTest: false
});
mesh.material = onTopMaterial;
Check a demonstation in this fiddle
Note: Make sure you have renderer.sortObjects
set to the default true
value.

- 41,477
- 12
- 152
- 203
-
1This is _not_ a good idea. There are many pitfalls. For one, set `transparent: true` on the blue cubes. But the biggest problem is that by setting `depthTest = false`, the so-called "on top mesh" will no longer depth-test with itself, which can lead to rendering artifacts. – WestLangley Oct 10 '16 at 23:27
-
@WestLangley Thanks for your feedback. That it conflicts with transparent settings is acceptable in my case, but about the artifacts I am not sure. How do I test this? I just need to temporarily move an object in front of all others on `mouseover` and back on `mousout`. Seems to me that adding it temporarily to a different scene is not desirable in such a case. – Wilt Oct 11 '16 at 07:50
-
I would follow the approach in the accepted answer, unless you have a very restricted use case. – WestLangley Oct 11 '16 at 15:57
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<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/tweakpane@1.5.5/dist/tweakpane.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/stats.js/r11/Stats.min.js"></script>
<script src="//unpkg.com/three@0.121.1/build/three.min.js"></script>
<script src="//unpkg.com/postprocessing@6.17.4/build/postprocessing.js"></script>

- 44,284
- 53
- 191
- 263
On top of setting object.renderOrder you have to set material.depthTest to false on the relevant objects.
var spriteMaterial = new THREE.SpriteMaterial( { map: texture1, depthTest: false} );
this.context1 = context1;
this.texture1 = texture1;
var sprite1 = new THREE.Sprite( spriteMaterial );
sprite1.scale.set(30,15,1);
sprite1.center.x=0;
sprite1.center.y=0;
sprite1.position.set( 0, 0, 0 );
this.scene.add( sprite1 );

- 2,556
- 1
- 19
- 23