three
Version:
JavaScript 3D library
353 lines (251 loc) • 10 kB
JavaScript
import { PassNode, RenderTarget, BlendMode, RendererUtils, Vector2, HalfFloatType, UnsignedByteType, RedFormat, CustomBlending, NormalBlending, OneFactor, ZeroFactor, OneMinusSrcColorFactor } from 'three/webgpu';
import { float, mix, mrt, output, positionView, texture, vec4 } from 'three/tsl';
const _size = /*@__PURE__*/ new Vector2();
let _rendererState, _sceneState;
/**
* A render pass node that renders the scene with Order-Independent Transparency
* based on the Weighted Blended OIT technique by McGuire and Bavoil.
*
* Transparent objects are rendered in a separate pass into two accumulation
* targets (a weighted color sum and the pixel's revealage) which are
* then composited over the rest of the scene. Since the result does not depend
* on the draw order, artifacts from sorting-based transparency like popping or
* incorrectly resolved intersecting geometry are avoided.
*
* Only transparent materials using `NormalBlending` and no transmission qualify
* for OIT. All other objects are rendered as usual.
*
* MSAA is only supported with the WebGPU backend.
*
* MRT configurations assigned via `setMRT()` apply to the default pass only.
* OIT-qualified objects contribute to the color output but not to custom
* MRT outputs since a pixel may accumulate multiple transparent surfaces.
*
* ```js
* const renderPipeline = new THREE.RenderPipeline( renderer );
* renderPipeline.outputNode = oitPass( scene, camera );
* ```
*
* References:
* - {@link https://jcgt.org/published/0002/02/09/}
* - {@link https://casual-effects.blogspot.com/2014/03/weighted-blended-order-independent.html}
*
* @augments PassNode
* @three_import import { oitPass } from 'three/addons/tsl/display/OITPassNode.js';
*/
class OITPassNode extends PassNode {
static get type() {
return 'OITPassNode';
}
/**
* Constructs a new OIT pass node.
*
* @param {Scene} scene - The scene to render.
* @param {Camera} camera - The camera to render the scene with.
* @param {Object} [options={}] - Options for the internal render target.
*/
constructor( scene, camera, options = {} ) {
super( PassNode.COLOR, scene, camera, options );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isOITPassNode = true;
/**
* The depth-based weight of a transparent fragment, see equations (7) to (9)
* in the paper. When `null`, equation (9) is used. Must be assigned before
* the first render.
*
* @type {?Node<float>}
* @default null
*/
this.weightNode = null;
// the accumulation target shares the depth of the default pass so transparent
// fragments are depth-tested against the opaque scene (without depth writes)
const oitRenderTarget = new RenderTarget( 1, 1, { count: 2 } );
oitRenderTarget.depthTexture = this.renderTarget.depthTexture;
const accumTexture = oitRenderTarget.textures[ 0 ]; // RGBA16
accumTexture.name = 'accum';
accumTexture.type = HalfFloatType;
const revealageTexture = oitRenderTarget.textures[ 1 ]; // R8
revealageTexture.name = 'revealage';
revealageTexture.format = RedFormat;
revealageTexture.type = UnsignedByteType;
/**
* The render target holding the OIT accumulation textures.
*
* @private
* @type {RenderTarget}
*/
this._oitRenderTarget = oitRenderTarget;
/**
* The MRT configuration for the OIT pass.
*
* @private
* @type {?MRTNode}
*/
this._oitMRTNode = null;
/**
* The renderer of the current frame.
*
* @private
* @type {?Renderer}
*/
this._renderer = null;
/**
* Renders opaque objects and transparent objects that do not qualify for OIT.
*
* @private
* @type {Function}
*/
this._defaultRenderObjectFunction = ( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId ) => {
if ( isOITCapable( material ) === false ) {
this._renderer.renderObject( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId );
}
};
/**
* Renders OIT-qualified objects into the accumulation targets.
*
* @private
* @type {Function}
*/
this._oitRenderObjectFunction = ( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId ) => {
if ( isOITCapable( material ) === true ) {
const currentDepthWrite = material.depthWrite;
material.depthWrite = false;
this._renderer.renderObject( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId );
material.depthWrite = currentDepthWrite;
}
};
}
/**
* Returns the MRT configuration for the OIT pass.
*
* @private
* @return {MRTNode} The MRT node.
*/
_getMRTNode() {
if ( this._oitMRTNode === null ) {
const alpha = output.a;
let weight = this.weightNode;
if ( weight === null ) {
// equation (9) from the paper, based on the linear eye-space depth
const z = positionView.z.negate();
weight = alpha.mul( float( 0.03 ).div( z.div( 200 ).pow( 4 ).add( 1e-5 ) ).clamp( 1e-2, 3e3 ) );
}
// since the revealage target is single-channel, the alpha must be blended
// via its red channel
const accumBlending = new BlendMode( CustomBlending );
accumBlending.blendSrc = OneFactor;
accumBlending.blendDst = OneFactor;
const revealageBlending = new BlendMode( CustomBlending );
revealageBlending.blendSrc = ZeroFactor;
revealageBlending.blendDst = OneMinusSrcColorFactor;
this._oitMRTNode = mrt( {
accum: vec4( output.rgb.mul( alpha ), alpha ).mul( weight ),
revealage: alpha
} ).setBlendMode( 'accum', accumBlending ).setBlendMode( 'revealage', revealageBlending )
.setClearColor( 'accum', 0x000000, 0 ).setClearColor( 'revealage', 0xffffff, 1 );
}
return this._oitMRTNode;
}
setSize( width, height ) {
super.setSize( width, height );
this._oitRenderTarget.setSize( this.renderTarget.width, this.renderTarget.height );
}
setup( builder ) {
const beautyNode = super.setup( builder );
// MSAA
if ( builder.renderer.backend.isWebGPUBackend === true ) {
// sample counts must match since the depth buffer is shared
this._oitRenderTarget.samples = this.renderTarget.samples;
} else {
// The WebGL backend does not support depth texture sharing with MSAA unless
// WEBGL_multisampled_render_to_texture is supported (which isn't available on most devices)
this.renderTarget.samples = 0;
}
// TSL
const accumNode = texture( this._oitRenderTarget.textures[ 0 ] );
const revealageNode = texture( this._oitRenderTarget.textures[ 1 ] ).r;
const accumColor = accumNode.rgb.div( accumNode.a.max( 1e-5 ) );
return vec4( mix( accumColor, beautyNode.rgb, revealageNode ), beautyNode.a );
}
updateBefore( frame ) {
const { renderer } = frame;
const { scene, camera } = this;
this._renderer = renderer;
renderer.getDrawingBufferSize( _size );
this.setSize( _size.width, _size.height );
_rendererState = RendererUtils.saveRendererState( renderer, _rendererState );
const currentAutoClearColor = renderer.autoClearColor;
const currentAutoClearDepth = renderer.autoClearDepth;
const currentAutoClearStencil = renderer.autoClearStencil;
const currentTransparent = renderer.transparent;
const currentOpaque = renderer.opaque;
const currentMask = camera.layers.mask;
this._cameraNear.value = camera.near;
this._cameraFar.value = camera.far;
if ( this._layers !== null ) {
camera.layers.mask = this._layers.mask;
}
renderer.autoClear = this.autoClear;
renderer.autoClearColor = this.autoClearColor;
renderer.autoClearDepth = this.autoClearDepth;
renderer.autoClearStencil = this.autoClearStencil;
// default pass: opaque objects and transparent objects that do not qualify for OIT
renderer.setMRT( this._mrt );
renderer.setRenderTarget( this.renderTarget );
renderer.setRenderObjectFunction( this._defaultRenderObjectFunction );
renderer.render( scene, camera );
// OIT pass: accumulate the weighted colors and the revealage of all OIT-qualified objects
_sceneState = RendererUtils.resetSceneState( scene, _sceneState ); // the background must not affect the accumulation targets
renderer.setRenderTarget( this._oitRenderTarget );
renderer.setMRT( this._getMRTNode() );
renderer.setRenderObjectFunction( this._oitRenderObjectFunction );
renderer.autoClearDepth = false; // the depth buffer is shared with the default pass
renderer.opaque = false;
renderer.transparent = true;
renderer.render( scene, camera );
// restore
RendererUtils.restoreSceneState( scene, _sceneState );
RendererUtils.restoreRendererState( renderer, _rendererState );
renderer.autoClearColor = currentAutoClearColor;
renderer.autoClearDepth = currentAutoClearDepth;
renderer.autoClearStencil = currentAutoClearStencil;
renderer.transparent = currentTransparent;
renderer.opaque = currentOpaque;
camera.layers.mask = currentMask;
this._renderer = null;
}
dispose() {
super.dispose();
this._oitRenderTarget.dispose();
}
}
/**
* Returns `true` if the given material qualifies for OIT.
*
* @param {Material} material - The material to check.
* @return {boolean} Whether the material qualifies for OIT or not.
*/
function isOITCapable( material ) {
return material.transparent === true && material.blending === NormalBlending &&
( material.transmission > 0 ) === false &&
! ( material.transmissionNode && material.transmissionNode.isNode ) &&
! ( material.backdropNode && material.backdropNode.isNode );
}
export default OITPassNode;
/**
* TSL function for creating an OIT pass node.
*
* @tsl
* @function
* @param {Scene} scene - The scene to render.
* @param {Camera} camera - The camera to render the scene with.
* @param {Object} [options={}] - Options for the internal render target.
* @returns {OITPassNode}
*/
export const oitPass = ( scene, camera, options ) => new OITPassNode( scene, camera, options );