@openhps/core
Version:
Open Hybrid Positioning System - Core component
253 lines (237 loc) • 9.63 kB
JavaScript
import Node from '../core/Node.js';
import { float, log, log2, nodeImmutable, nodeProxy } from '../tsl/TSLBase.js';
import { cameraNear, cameraFar } from '../accessors/Camera.js';
import { positionView } from '../accessors/Position.js';
import { viewportDepthTexture } from './ViewportDepthTextureNode.js';
/**
* This node offers a collection of features in context of the depth logic in the fragment shader.
* Depending on {@link ViewportDepthNode#scope}, it can be used to define a depth value for the current
* fragment or for depth evaluation purposes.
*
* @augments Node
*/
class ViewportDepthNode extends Node {
static get type() {
return 'ViewportDepthNode';
}
/**
* Constructs a new viewport depth node.
*
* @param {('depth'|'depthBase'|'linearDepth')} scope - The node's scope.
* @param {?Node} [valueNode=null] - The value node.
*/
constructor(scope, valueNode = null) {
super('float');
/**
* The node behaves differently depending on which scope is selected.
*
* - `ViewportDepthNode.DEPTH_BASE`: Allows to define a value for the current fragment's depth.
* - `ViewportDepthNode.DEPTH`: Represents the depth value for the current fragment (`valueNode` is ignored).
* - `ViewportDepthNode.LINEAR_DEPTH`: Represents the linear (orthographic) depth value of the current fragment.
* If a `valueNode` is set, the scope can be used to convert perspective depth data to linear data.
*
* @type {('depth'|'depthBase'|'linearDepth')}
*/
this.scope = scope;
/**
* Can be used to define a custom depth value.
* The property is ignored in the `ViewportDepthNode.DEPTH` scope.
*
* @type {?Node}
* @default null
*/
this.valueNode = valueNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isViewportDepthNode = true;
}
generate(builder) {
const {
scope
} = this;
if (scope === ViewportDepthNode.DEPTH_BASE) {
return builder.getFragDepth();
}
return super.generate(builder);
}
setup({
camera
}) {
const {
scope
} = this;
const value = this.valueNode;
let node = null;
if (scope === ViewportDepthNode.DEPTH_BASE) {
if (value !== null) {
node = depthBase().assign(value);
}
} else if (scope === ViewportDepthNode.DEPTH) {
if (camera.isPerspectiveCamera) {
node = viewZToPerspectiveDepth(positionView.z, cameraNear, cameraFar);
} else {
node = viewZToOrthographicDepth(positionView.z, cameraNear, cameraFar);
}
} else if (scope === ViewportDepthNode.LINEAR_DEPTH) {
if (value !== null) {
if (camera.isPerspectiveCamera) {
const viewZ = perspectiveDepthToViewZ(value, cameraNear, cameraFar);
node = viewZToOrthographicDepth(viewZ, cameraNear, cameraFar);
} else {
node = value;
}
} else {
node = viewZToOrthographicDepth(positionView.z, cameraNear, cameraFar);
}
}
return node;
}
}
ViewportDepthNode.DEPTH_BASE = 'depthBase';
ViewportDepthNode.DEPTH = 'depth';
ViewportDepthNode.LINEAR_DEPTH = 'linearDepth';
export default ViewportDepthNode;
// NOTE: viewZ, the z-coordinate in camera space, is negative for points in front of the camera
/**
* TSL function for converting a viewZ value to an orthographic depth value.
*
* @tsl
* @function
* @param {Node<float>} viewZ - The viewZ node.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
export const viewZToOrthographicDepth = (viewZ, near, far) => viewZ.add(near).div(near.sub(far));
/**
* TSL function for converting an orthographic depth value to a viewZ value.
*
* @tsl
* @function
* @param {Node<float>} depth - The orthographic depth.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
export const orthographicDepthToViewZ = (depth, near, far) => near.sub(far).mul(depth).sub(near);
/**
* TSL function for converting a viewZ value to a perspective depth value.
*
* Note: {link https://twitter.com/gonnavis/status/1377183786949959682}.
*
* @tsl
* @function
* @param {Node<float>} viewZ - The viewZ node.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
export const viewZToPerspectiveDepth = (viewZ, near, far) => near.add(viewZ).mul(far).div(far.sub(near).mul(viewZ));
/**
* TSL function for converting a perspective depth value to a viewZ value.
*
* @tsl
* @function
* @param {Node<float>} depth - The perspective depth.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
export const perspectiveDepthToViewZ = (depth, near, far) => near.mul(far).div(far.sub(near).mul(depth).sub(far));
/**
* TSL function for converting a viewZ value to a logarithmic depth value.
*
* @tsl
* @function
* @param {Node<float>} viewZ - The viewZ node.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
export const viewZToLogarithmicDepth = (viewZ, near, far) => {
// NOTE: viewZ must be negative--see explanation at the end of this comment block.
// The final logarithmic depth formula used here is adapted from one described in an
// article by Thatcher Ulrich (see http://tulrich.com/geekstuff/log_depth_buffer.txt),
// which was an improvement upon an earlier formula one described in an
// Outerra article (https://outerra.blogspot.com/2009/08/logarithmic-z-buffer.html).
// Ulrich's formula is the following:
// z = K * log( w / cameraNear ) / log( cameraFar / cameraNear )
// where K = 2^k - 1, and k is the number of bits in the depth buffer.
// The Outerra variant ignored the camera near plane (it assumed it was 0) and instead
// opted for a "C-constant" for resolution adjustment of objects near the camera.
// Outerra states: "Notice that the 'C' variant doesn’t use a near plane distance, it has it
// set at 0" (quote from https://outerra.blogspot.com/2012/11/maximizing-depth-buffer-range-and.html).
// Ulrich's variant has the benefit of constant relative precision over the whole near-far range.
// It was debated here whether Outerra's "C-constant" or Ulrich's "near plane" variant should
// be used, and ultimately Ulrich's "near plane" version was chosen.
// Outerra eventually made another improvement to their original "C-constant" variant,
// but it still does not incorporate the camera near plane (for this version,
// see https://outerra.blogspot.com/2013/07/logarithmic-depth-buffer-optimizations.html).
// Here we make 4 changes to Ulrich's formula:
// 1. Clamp the camera near plane so we don't divide by 0.
// 2. Use log2 instead of log to avoid an extra multiply (shaders implement log using log2).
// 3. Assume K is 1 (K = maximum value in depth buffer; see Ulrich's formula above).
// 4. To maintain consistency with the functions "viewZToOrthographicDepth" and "viewZToPerspectiveDepth",
// we modify the formula here to use 'viewZ' instead of 'w'. The other functions expect a negative viewZ,
// so we do the same here, hence the 'viewZ.negate()' call.
// For visual representation of this depth curve, see https://www.desmos.com/calculator/uyqk0vex1u
near = near.max(1e-6).toVar();
const numerator = log2(viewZ.negate().div(near));
const denominator = log2(far.div(near));
return numerator.div(denominator);
};
/**
* TSL function for converting a logarithmic depth value to a viewZ value.
*
* @tsl
* @function
* @param {Node<float>} depth - The logarithmic depth.
* @param {Node<float>} near - The camera's near value.
* @param {Node<float>} far - The camera's far value.
* @returns {Node<float>}
*/
export const logarithmicDepthToViewZ = (depth, near, far) => {
// NOTE: we add a 'negate()' call to the return value here to maintain consistency with
// the functions "orthographicDepthToViewZ" and "perspectiveDepthToViewZ" (they return
// a negative viewZ).
const exponent = depth.mul(log(far.div(near)));
return float(Math.E).pow(exponent).mul(near).negate();
};
/**
* TSL function for defining a value for the current fragment's depth.
*
* @tsl
* @function
* @param {Node<float>} value - The depth value to set.
* @returns {ViewportDepthNode<float>}
*/
const depthBase = /*@__PURE__*/nodeProxy(ViewportDepthNode, ViewportDepthNode.DEPTH_BASE);
/**
* TSL object that represents the depth value for the current fragment.
*
* @tsl
* @type {ViewportDepthNode}
*/
export const depth = /*@__PURE__*/nodeImmutable(ViewportDepthNode, ViewportDepthNode.DEPTH);
/**
* TSL function for converting a perspective depth value to linear depth.
*
* @tsl
* @function
* @param {?Node<float>} [value=null] - The perspective depth. If `null` is provided, the current fragment's depth is used.
* @returns {ViewportDepthNode<float>}
*/
export const linearDepth = /*@__PURE__*/nodeProxy(ViewportDepthNode, ViewportDepthNode.LINEAR_DEPTH).setParameterLength(0, 1);
/**
* TSL object that represents the linear (orthographic) depth value of the current fragment
*
* @tsl
* @type {ViewportDepthNode}
*/
export const viewportLinearDepth = /*@__PURE__*/linearDepth(viewportDepthTexture());
depth.assign = value => depthBase(value);