@openhps/core
Version:
Open Hybrid Positioning System - Core component
193 lines (183 loc) • 6.61 kB
JavaScript
import Node from '../core/Node.js';
import { nodeObject, Fn, bool, float } from '../tsl/TSLBase.js';
import { positionView } from './Position.js';
import { diffuseColor } from '../core/PropertyNode.js';
import { Loop } from '../utils/LoopNode.js';
import { smoothstep } from '../math/MathNode.js';
import { uniformArray } from './UniformArrayNode.js';
import { builtin } from './BuiltinNode.js';
/**
* This node is used in {@link NodeMaterial} to setup the clipping
* which can happen hardware-accelerated (if supported) and optionally
* use alpha-to-coverage for anti-aliasing clipped edges.
*
* @augments Node
*/
class ClippingNode extends Node {
static get type() {
return 'ClippingNode';
}
/**
* Constructs a new clipping node.
*
* @param {('default'|'hardware'|'alphaToCoverage')} [scope='default'] - The node's scope. Similar to other nodes,
* the selected scope influences the behavior of the node and what type of code is generated.
*/
constructor(scope = ClippingNode.DEFAULT) {
super();
/**
* The node's scope. Similar to other nodes, the selected scope influences
* the behavior of the node and what type of code is generated.
*
* @type {('default'|'hardware'|'alphaToCoverage')}
*/
this.scope = scope;
}
/**
* Setups the node depending on the selected scope.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The result node.
*/
setup(builder) {
super.setup(builder);
const clippingContext = builder.clippingContext;
const {
intersectionPlanes,
unionPlanes
} = clippingContext;
this.hardwareClipping = builder.material.hardwareClipping;
if (this.scope === ClippingNode.ALPHA_TO_COVERAGE) {
return this.setupAlphaToCoverage(intersectionPlanes, unionPlanes);
} else if (this.scope === ClippingNode.HARDWARE) {
return this.setupHardwareClipping(unionPlanes, builder);
} else {
return this.setupDefault(intersectionPlanes, unionPlanes);
}
}
/**
* Setups alpha to coverage.
*
* @param {Array<Vector4>} intersectionPlanes - The intersection planes.
* @param {Array<Vector4>} unionPlanes - The union planes.
* @return {Node} The result node.
*/
setupAlphaToCoverage(intersectionPlanes, unionPlanes) {
return Fn(() => {
const distanceToPlane = float().toVar('distanceToPlane');
const distanceGradient = float().toVar('distanceToGradient');
const clipOpacity = float(1).toVar('clipOpacity');
const numUnionPlanes = unionPlanes.length;
if (this.hardwareClipping === false && numUnionPlanes > 0) {
const clippingPlanes = uniformArray(unionPlanes);
Loop(numUnionPlanes, ({
i
}) => {
const plane = clippingPlanes.element(i);
distanceToPlane.assign(positionView.dot(plane.xyz).negate().add(plane.w));
distanceGradient.assign(distanceToPlane.fwidth().div(2.0));
clipOpacity.mulAssign(smoothstep(distanceGradient.negate(), distanceGradient, distanceToPlane));
});
}
const numIntersectionPlanes = intersectionPlanes.length;
if (numIntersectionPlanes > 0) {
const clippingPlanes = uniformArray(intersectionPlanes);
const intersectionClipOpacity = float(1).toVar('intersectionClipOpacity');
Loop(numIntersectionPlanes, ({
i
}) => {
const plane = clippingPlanes.element(i);
distanceToPlane.assign(positionView.dot(plane.xyz).negate().add(plane.w));
distanceGradient.assign(distanceToPlane.fwidth().div(2.0));
intersectionClipOpacity.mulAssign(smoothstep(distanceGradient.negate(), distanceGradient, distanceToPlane).oneMinus());
});
clipOpacity.mulAssign(intersectionClipOpacity.oneMinus());
}
diffuseColor.a.mulAssign(clipOpacity);
diffuseColor.a.equal(0.0).discard();
})();
}
/**
* Setups the default clipping.
*
* @param {Array<Vector4>} intersectionPlanes - The intersection planes.
* @param {Array<Vector4>} unionPlanes - The union planes.
* @return {Node} The result node.
*/
setupDefault(intersectionPlanes, unionPlanes) {
return Fn(() => {
const numUnionPlanes = unionPlanes.length;
if (this.hardwareClipping === false && numUnionPlanes > 0) {
const clippingPlanes = uniformArray(unionPlanes);
Loop(numUnionPlanes, ({
i
}) => {
const plane = clippingPlanes.element(i);
positionView.dot(plane.xyz).greaterThan(plane.w).discard();
});
}
const numIntersectionPlanes = intersectionPlanes.length;
if (numIntersectionPlanes > 0) {
const clippingPlanes = uniformArray(intersectionPlanes);
const clipped = bool(true).toVar('clipped');
Loop(numIntersectionPlanes, ({
i
}) => {
const plane = clippingPlanes.element(i);
clipped.assign(positionView.dot(plane.xyz).greaterThan(plane.w).and(clipped));
});
clipped.discard();
}
})();
}
/**
* Setups hardware clipping.
*
* @param {Array<Vector4>} unionPlanes - The union planes.
* @param {NodeBuilder} builder - The current node builder.
* @return {Node} The result node.
*/
setupHardwareClipping(unionPlanes, builder) {
const numUnionPlanes = unionPlanes.length;
builder.enableHardwareClipping(numUnionPlanes);
return Fn(() => {
const clippingPlanes = uniformArray(unionPlanes);
const hw_clip_distances = builtin(builder.getClipDistance());
Loop(numUnionPlanes, ({
i
}) => {
const plane = clippingPlanes.element(i);
const distance = positionView.dot(plane.xyz).sub(plane.w).negate();
hw_clip_distances.element(i).assign(distance);
});
})();
}
}
ClippingNode.ALPHA_TO_COVERAGE = 'alphaToCoverage';
ClippingNode.DEFAULT = 'default';
ClippingNode.HARDWARE = 'hardware';
export default ClippingNode;
/**
* TSL function for setting up the default clipping logic.
*
* @tsl
* @function
* @returns {ClippingNode}
*/
export const clipping = () => nodeObject(new ClippingNode());
/**
* TSL function for setting up alpha to coverage.
*
* @tsl
* @function
* @returns {ClippingNode}
*/
export const clippingAlpha = () => nodeObject(new ClippingNode(ClippingNode.ALPHA_TO_COVERAGE));
/**
* TSL function for setting up hardware-based clipping.
*
* @tsl
* @function
* @returns {ClippingNode}
*/
export const hardwareClipping = () => nodeObject(new ClippingNode(ClippingNode.HARDWARE));