Skip to content
Advertisement

Three.js – depthWrite vs depthTest for transparent canvas texture map on THREE.Points

Question

Is there a significant difference between depthWrite: false and depthTest: false? Does using depthTest offer a performance advantage? Is there any sacrifice in functionality choosing one or the other?

Original problem

I wanted to render a THREE.Points object with translucent circles as each point. I used a THREE.Texture loaded from a canvas element and passed it to the map property on the THREE.PointsMaterial.

The transparency did not completely work, some circles overlapped fine but others behaved as if they were solid.

I fixed it after learning about depthWrite: false and depthTest: false on the THREE.PointsMaterial.

Where I’m at

I have a code example (embedded at bottom) that shows the overlapping points error, and can use depthTest or depthWrite to fix it:

var points = new THREE.Points(
    new THREE.Geometry(),
    new THREE.PointsMaterial({
        //depthTest: false,
        //depthWrite: false,
        map: circleTexture,
        size: circleDiameter,
        transparent: true
    })
);

I’m new to all this, but I tried reading up on the subject, and from what I can tell (correct me if I’m wrong) the depth buffer is used to determine what fragments are occluded and do not need rendering. Turning off either depthWrite or depthTest will exempt an object from this process. They differ in that:

  • depthWrite: false still calculates depth, but renders the entire object regardless

  • depthTest: false does not even calculate depth

So it sounds like I would lose some object qualities by turning off depthTest instead of depthWrite, but possibly get a performance boost by skipping the calculation altogether? But, what qualities would I be losing? And is there actually a performance difference? Here my ignorance shines through.

// Sizes
var sceneWidth = 200;
var sceneHeight = 200;
var lineLength = 50;
var circleRadius = 32;
var circleDiameter = circleRadius * 2;

// Renderer
var renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true
});
renderer.setSize(sceneWidth, sceneHeight);
document.body.appendChild(renderer.domElement);

// Scene
var scene = new THREE.Scene();

// Camera
var d = 100;
var aspect = sceneWidth / sceneHeight;
var camera = new THREE.OrthographicCamera(
    -d * aspect,
    d * aspect,
    d,
    -d,
    1,
    12000
);
camera.position.set(140, 140, 140);
scene.add(camera);

// Controls
var controls = new THREE.OrthographicTrackballControls(
    camera,
    renderer.domElement
);
controls.rotateSpeed = 0.2;
controls.addEventListener('change', function () {
    renderer.render(scene, camera);
});
window.addEventListener('resize', function() {
    controls.handleResize();
});

// Circle texture
var canvasEl = document.createElement('canvas');
var context = canvasEl.getContext('2d');
canvasEl.width = circleDiameter;
canvasEl.height = circleDiameter;
context.fillStyle = 'rgba(255, 255, 255, 0.5)';
context.beginPath();
context.arc(circleRadius, circleRadius, circleRadius, 0, Math.PI * 2);
context.fill();
var circleTexture = new THREE.Texture(canvasEl);
circleTexture.needsUpdate = true;

// Points
var points = new THREE.Points(
    new THREE.Geometry(),
    new THREE.PointsMaterial({
        //depthTest: false,
        //depthWrite: false,
        map: circleTexture,
        size: circleDiameter,
        transparent: true
    })
);
points.geometry.vertices.push(new THREE.Vector3(0, 0, 0));
points.geometry.vertices.push(new THREE.Vector3(0, lineLength, 0));
points.geometry.vertices.push(new THREE.Vector3(0, lineLength, lineLength));
points.geometry.vertices.push(new THREE.Vector3(0, 0, lineLength));
scene.add(points);

// Lines
var lines = new THREE.Line(
    new THREE.Geometry(),
    new THREE.LineBasicMaterial({
        linewidth: 1.2,
        color: 0xffffff,
        transparent: true,
        opacity: 0.25
    })
);
lines.geometry.vertices.push(new THREE.Vector3(0, 0, 0));
lines.geometry.vertices.push(new THREE.Vector3(0, lineLength, 0));
lines.geometry.vertices.push(new THREE.Vector3(0, lineLength, 0));
lines.geometry.vertices.push(new THREE.Vector3(0, lineLength, lineLength));
lines.geometry.vertices.push(new THREE.Vector3(0, lineLength, lineLength));
lines.geometry.vertices.push(new THREE.Vector3(0, 0, lineLength));
lines.geometry.vertices.push(new THREE.Vector3(0, 0, lineLength));
lines.geometry.vertices.push(new THREE.Vector3(0, 0, 0));
scene.add(lines);

// Render
function render() {
    window.requestAnimationFrame(render);
    renderer.render(scene, camera);
    controls.update();
}

render();
* { margin: 0; padding: 0; }
body { background-color: #333; }
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Document</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r76/three.min.js"></script>
<script src="http://threejs.org/examples/js/controls/OrthographicTrackballControls.js"></script>
</body>
</html>

Advertisement

Answer

Depth test off means to turn off depth testing all together. (reading/testing and writing)

Depth write off means to prevent the depth buffer from being written.

So first of all, what is depth test? Suppose if you are to draw 2 identical shapes directly in front of you but of different distance to you. In real life, you expect to only see the shape that is closer to you, correct?

Well if you were to try to do this without a depth test, you will only get the desired effect half the time: if the distant object is drawn before the closer object, no problem, same as real life; but if the closer object is drawn before the distance object, oh-oh, the distant object is visible when it should be not. Problematic.

Depth test is a tool built in today’s GPUs to allow to get the desired draw output regardless of the order which the objects are drawn. This is normally very useful but it comes with a critical weakness: depth and blending(transparency) do not work together. Why is this the case? Well what depth test does is that for every pixel that is drawn, the distance(depth) of that pixel to the camera is compared to the depth value stored in that the pixel. If the distance is less that the stored depth value, the pixel is drawn, otherwise that pixel is discarded.

This explains why you sometimes see the black quads in your demo. When those quads are drawn first, their depth values are written into the depth buffer. Then when the more distant quads are drawn, their depth value are greater than the depth in the buffer and thus those pixels are discarded. In other viewing angles it just so happens that distant quads are drawn first and then the closer quads, so no pixels are discarded due to depth testing.

Hopefully its clear now that there are two aspects of depth testing: the comparison of depth values and the writing of depth values to the depth buffer. DepthTest and depthWrite gives you fine control over how to archive the desired effect.

Turning off depth testing all together would be faster than just depth writing. However, sometimes you just want to prevent new pixels to write to the depth buffer but still with the depth testing enabled. For example, in your demo if you were to draw a totally opaque cube in the center; you still want pixels with further depth than the pixels representing the opaque cube to be hidden (the depth testing aspect), but also want to prevent pixels from the transparent circles from blocking each other (the writing aspect). A common draw configuration you see is to draw all the opaque object with depth testing on, turn depth write off, then draw the transparent objects in a back to front order.

Advertisement