6

I need to compute 3D coordinates from a screen-space position using a rendered depth-map. Unfortunately, using the regular raytracing is not an option for me because I am dealing with a single geometry containing something on the order of 5M faces.

So I figured I will do the following:

  • render a depth-map with RGBADepthPacking into a renderTarget
  • use a regular unproject-call to compute a ray from the mouse-position (exactly as I would do when using raycasting)
  • lookup the depth from the depth-map at the mouse-coordinates and compute a point along the ray using that distance.

This kind of works, but somehow the located point is always slightly behind the object, so there is probably something wrong with my depth-calculations.

Now some details about the steps above

Rendering the depth-map is pretty much straight-forward:

const depthTarget = new THREE.WebGLRenderTarget(w, h);
const depthMaterial = new THREE.MeshDepthMaterial({
  depthPacking: THREE.RGBADepthPacking
});

// in renderloop
renderer.setClearColor(0xffffff, 1);
renderer.clear();
scene.overrideMaterial = depthMaterial;
renderer.render(scene, camera, depthTarget);

Lookup the stored color-value at the mouse-position with:

renderer.readRenderTargetPixels(
  depthTarget, x, h - y, 1, 1, rgbaBuffer
);

And convert back to float using (adapted from the GLSL-Version in packing.glsl):

const v4 = new THREE.Vector4()
const unpackDownscale = 255 / 256;
const unpackFactors = new THREE.Vector4(
  unpackDownscale / (256 * 256 * 256),
  unpackDownscale / (256 * 256),
  unpackDownscale / 256,
  unpackDownscale
);

function unpackRGBAToDepth(rgbaBuffer) {
  return v4.fromArray(rgbaBuffer)
    .multiplyScalar(1 / 255)
    .dot(unpackFactors);
}

and finally computing the depth-value (I found corresponding code in readDepth() in examples/js/shaders/SSAOShader.js which I ported to JS):

function computeDepth() {
  const cameraFarPlusNear = cameraFar + cameraNear;
  const cameraFarMinusNear = cameraFar - cameraNear;
  const cameraCoef = 2.0 * cameraNear;

  let z = unpackRGBAToDepth(rgbaBuffer);
  return cameraCoef / (cameraFarPlusNear - z * cameraFarMinusNear);
}

Now, as this function returns values in range 0..1 I think it is the depth in clip-space coordinates, so I convert them into "real" units using:

const depth = camera.near + depth * (camera.far - camera.near);

There is obviously something slightly off with these calculations and I didn't figure out the math and details about how depth is stored yet. Can someone please point me to the mistake I made?

Addition: other things I tried

First I thought it should be possible to just use the unpacked depth-value as value for z in my unproject-call like this:

const x = mouseX/w * 2 - 1;
const y = -mouseY/h * 2 + 1;
const v = new THREE.Vector3(x, y, depth).unproject(camera);

However, this also doesn't get the coordinates right.

[EDIT 1 2017-05-23 11:00CEST]

As per @WestLangleys comment I found the perspectiveDepthToViewZ() function which sounds like it should help. Written in JS that function is

function perspectiveDepthToViewZ(invClipZ, near, far) {
  return (near * far) / ((far - near) * invClipZ - far);
}

However, when called with unpacked values from the depth-map, results are several orders of magnitude off. See here.

Martin Schuhfuß
  • 6,814
  • 1
  • 36
  • 44
  • Maybe these will help: https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/packing.glsl, https://github.com/mrdoob/three.js/blob/master/examples/webgl_depth_texture.html – WestLangley May 22 '17 at 23:45
  • Are you assuming that your depth buffer is linear? I'm not entirely sure if you already considered that, but it may be whats throwing off your math. – pailhead May 23 '17 at 01:00
  • @WestLangley thanks! I didn't notice the `perspectiveDepthToViewZ()`-function there, but unfortunately a) I still don't quite understand the math and b) the results, when supplied a depth-buffer value don't make any sense to me. – Martin Schuhfuß May 23 '17 at 08:57
  • @pailhead Already considered that, which is why I originally wanted to just reverse the perspective transform. At this point I'm just trying things until I get numbers that make sense :/ – Martin Schuhfuß May 23 '17 at 08:59
  • @WestLangley: Turns out, I was doing it wrong and the solution with `perspectiveDepthToViewZ()` now has convincing results. If you add that as an answer I will happily accept it. Otherwise I will add it myself tomorrow... – Martin Schuhfuß May 23 '17 at 10:28
  • That's OK, you can post it. :) – WestLangley May 28 '17 at 14:48
  • 1
    @user5515 I have updated the original https://jsfiddle.net/mpspb7jn/1/ with current r152 THREE version: https://codesandbox.io/s/mouse-depth-h9qz2r This does NOT include latest (2017-05-28) Martin's answer though – abernier Apr 29 '23 at 00:02
  • 1
    Using "postprocessing" DepthPickingPass (GPU-based), I managed to get a totally similar example working: https://codesandbox.io/embed/depth-picking-x130hg?codemirror=1 – abernier Apr 30 '23 at 11:13

1 Answers1

7

Ok, so. Finally solved it. So for everyone having trouble with similar issues, here's the solution:

The last line of the computeDepth-function was just wrong. There is a function perspectiveDepthToViewZ in packing.glsl, that is pretty easy to convert to JS:

function perspectiveDepthToViewZ(invClipZ, near, far) {
  return (near * far) / ((far - near) * invClipZ - far);
}

(i believe this is somehow part of the inverse projection-matrix)

function computeDepth() {
  let z = unpackRGBAToDepth(rgbaBuffer);
  return perspectiveDepthToViewZ(z, camera.near, camera.far);
}

Now this will return the z-axis value in view-space for the point. Left to do is converting this back to world-space coordinates:

const setPositionFromViewZ = (function() {
  const viewSpaceCoord = new THREE.Vector3();
  const projInv = new THREE.Matrix4();

  return function(position, viewZ) {
    projInv.getInverse(camera.projectionMatrix);
    position
      .set(
        mousePosition.x / windowWidth * 2 - 1,
        -(mousePosition.y / windowHeight) * 2 + 1,
        0.5
      )
      .applyMatrix4(projInv);

    position.multiplyScalar(viewZ / position.z);
    position.applyMatrix4(camera.matrixWorld);
  };
}) ();
Martin Schuhfuß
  • 6,814
  • 1
  • 36
  • 44
  • Can you please update the jsfiddle with the correct solution, for us mortals to understand it? – user5515 Jun 15 '18 at 23:27
  • Awesome, I was trying to figure out a solution for days, and I couldn't do it with just two passes -I'll study it exhaustively, many thanks! :) – user5515 Jun 17 '18 at 22:32