Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compute 3D point from mouse-position and depth-map

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.

like image 959
Martin Schuhfuß Avatar asked May 22 '17 20:05

Martin Schuhfuß


1 Answers

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);
  };
}) ();
like image 178
Martin Schuhfuß Avatar answered Sep 30 '22 08:09

Martin Schuhfuß