I’ve been attempting to port the OutlinePass to the WebGPU Postprocessing system, but have been encountering a lot of issues with the PassNode API. I’m not quite sure whether these are issues with my implementation, or just with the node system itself, but I’ll list them out to see if I can come to a solution.
1… Assigning Node Materials in setup exceeds the calls stack
I currently have a successful postprocess pass that takes two depth passes ( one where all selected objects are displayed and one where all nonselected objects are displayed). The output successfully displays the outline mask with this TSL function
const prepareMask = tslFn( () => {
const nonSelectedViewZ = perspectiveDepthToViewZ( nsDepthNode, this._cameraNear, this._cameraFar );
const selectedViewZ = perspectiveDepthToViewZ( sDepthNode, this._cameraNear, this._cameraFar );
const depthTest = selectedViewZ.greaterThan( nonSelectedViewZ ).cond( 1.0, 0.0 );
return vec4( 0.0, depthTest, 0.0, 1.0 );
} );
const outputNode = prepareMask();
return outputNode;
While the shader displays successfully, this pass obviously needs to be the first in a chain of passes that takes the mask and either utilizes or modifies it, starting with the down sample pass.
Conseqeuntly, I need to update the updateBefore() method and setup method to facilitate rendering the prepareMask shader out to a quadMesh. I update the code following GausianBlurNode and BloomNode as examples.
const _prepareMaskQuad = new QuadMesh();
class OutlineNode extends TempNode {
constructor( nsColor, nsDepth, sColor, sDepth, uniformObject ) {
super();
// Texture Nodes
this.nsColorNode = nsColor;
this.sColorNode = sColor;
this.nsDepthNode = nsDepth;
this.sDepthNode = sDepth;
// External uniforms
// Internal uniforms ( Global )
// Internal uniforms ( per Output Pass )
// Interal uniforms ( per Output Pass Step )
// Render Targets
this._prepareMaskRT = this.createOutlineNodeTarget( 'prepareMask' );
this._maskDownSampleRT = this.createOutlineNodeTarget( 'maskDownSamplePass' );
this._passPrepareMaskTextureNode = passTexture( this, this._prepareMaskRT.texture );
this._passDownsampleTextureNode = passTexture( this, this._maskDownSampleRT.texture );
this.updateBeforeType = NodeUpdateType.RENDER;
}
createOutlineNodeTarget( name ) {
const rt = new RenderTarget();
rt.texture.name = `${OUTLINE_NODE_ID}.${name}`;
return rt;
}
updateBefore( frame ) {
const { renderer } = frame;
const sTextureNode = this.sColorNode;
const map = sTextureNode.value;
const currentRenderTarget = renderer.getRenderTarget();
const currentMRT = renderer.getMRT();
const currentTexture = sTextureNode.value;
_prepareMaskQuad.material = this._prepareMaskMaterial;
this.setSize( map.image.width, map.image.height );
const textureType = map.type;
this._prepareMaskRT.texture.type = textureType;
renderer.setMRT( null );
renderer.setRenderTarget( this._prepareMaskRT );
_prepareMaskQuad.render( renderer );
renderer.setRenderTarget( currentRenderTarget );
renderer.setMRT( currentMRT );
} */
setSize( width, height ) {
this._prepareMaskRT.setSize( width, height );
let resx = Math.round( width / this.downSampleRatio );
let resy = Math.round( height / this.downSampleRatio );
this._maskDownSampleRT.setSize( resx, resy );
}
dispose() {
this._prepareMaskRT.dispose();
this._maskDownSampleRT.dispose();
this._blur1RT.dispose();
this._blur2RT.dispose();
this._edge1RT.dispose();
this._edge2RT.dispose();
}
setup( builder ) {
const nsColorNode = this.nsColorNode;
const sColorNode = this.sColorNode;
const nsDepthNode = this.nsDepthNode;
const sDepthNode = this.sDepthNode;
const prepareMask = tslFn( () => {
const nonSelectedViewZ = perspectiveDepthToViewZ( nsDepthNode, this._cameraNear, this._cameraFar );
const selectedViewZ = perspectiveDepthToViewZ( sDepthNode, this._cameraNear, this._cameraFar );
const depthTest = selectedViewZ.greaterThan( nonSelectedViewZ ).cond( 1.0, 0.0 );
return vec4( 0.0, depthTest, 0.0, 1.0 );
} );
const material = this._prepareMaskMaterial || ( this._prepareMaskMaterial = builder.createNodeMaterial() );
material.fragmentNode = prepareMask().context( builder.getSharedContext() );
material.needsUpdate = true;
return this._passPrepareMaskTextureNode;
}
}
To start, before working on the downsample pass, I try to output the value of the prepare mask pass after I’ve initialized the material and rendered to the prepareMask quad in update before. Based on my understanding of the other post process nodes, this should work, but instead I get a call stack error.
Any of the workarounds I’ve tried to this approach yield similar errors. I presume I’m missing some nuance of the node/PassTextureNode system, so I was just wondering if anyone knew why this behavior might be occurring.

