123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751 |
- import { DepthTexture, FloatType, RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, NodeUpdateType } from 'three/webgpu';
- import { Loop, int, exp, min, float, mul, uv, vec2, vec3, Fn, textureSize, orthographicDepthToViewZ, screenUV, nodeObject, uniform, vec4, passTexture, texture, perspectiveDepthToViewZ, positionView, reference } from 'three/tsl';
- const _quadMesh = /*@__PURE__*/ new QuadMesh();
- const _size = /*@__PURE__*/ new Vector2();
- const _BLUR_DIRECTION_X = /*@__PURE__*/ new Vector2( 1.0, 0.0 );
- const _BLUR_DIRECTION_Y = /*@__PURE__*/ new Vector2( 0.0, 1.0 );
- let _rendererState;
- /**
- * Post processing node for rendering outlines around selected objects. The node
- * gives you great flexibility in composing the final outline look depending on
- * your requirements.
- * ```js
- * const postProcessing = new THREE.PostProcessing( renderer );
- *
- * const scenePass = pass( scene, camera );
- *
- * // outline parameter
- *
- * const edgeStrength = uniform( 3.0 );
- * const edgeGlow = uniform( 0.0 );
- * const edgeThickness = uniform( 1.0 );
- * const visibleEdgeColor = uniform( new THREE.Color( 0xffffff ) );
- * const hiddenEdgeColor = uniform( new THREE.Color( 0x4e3636 ) );
- *
- * outlinePass = outline( scene, camera, {
- * selectedObjects,
- * edgeGlow,
- * edgeThickness
- * } );
- *
- * // compose custom outline
- *
- * const { visibleEdge, hiddenEdge } = outlinePass;
- * const outlineColor = visibleEdge.mul( visibleEdgeColor ).add( hiddenEdge.mul( hiddenEdgeColor ) ).mul( edgeStrength );
- *
- * postProcessing.outputNode = outlineColor.add( scenePass );
- * ```
- *
- * @augments TempNode
- * @three_import import { outline } from 'three/addons/tsl/display/OutlineNode.js';
- */
- class OutlineNode extends TempNode {
- static get type() {
- return 'OutlineNode';
- }
- /**
- * Constructs a new outline node.
- *
- * @param {Scene} scene - A reference to the scene.
- * @param {Camera} camera - The camera the scene is rendered with.
- * @param {Object} params - The configuration parameters.
- * @param {Array<Object3D>} params.selectedObjects - An array of selected objects.
- * @param {Node<float>} [params.edgeThickness=float(1)] - The thickness of the edges.
- * @param {Node<float>} [params.edgeGlow=float(0)] - Can be used for an animated glow/pulse effects.
- * @param {number} [params.downSampleRatio=2] - The downsample ratio.
- */
- constructor( scene, camera, params = {} ) {
- super( 'vec4' );
- const {
- selectedObjects = [],
- edgeThickness = float( 1 ),
- edgeGlow = float( 0 ),
- downSampleRatio = 2
- } = params;
- /**
- * A reference to the scene.
- *
- * @type {Scene}
- */
- this.scene = scene;
- /**
- * The camera the scene is rendered with.
- *
- * @type {Camera}
- */
- this.camera = camera;
- /**
- * An array of selected objects.
- *
- * @type {Array<Object3D>}
- */
- this.selectedObjects = selectedObjects;
- /**
- * The thickness of the edges.
- *
- * @type {Node<float>}
- */
- this.edgeThicknessNode = nodeObject( edgeThickness );
- /**
- * Can be used for an animated glow/pulse effect.
- *
- * @type {Node<float>}
- */
- this.edgeGlowNode = nodeObject( edgeGlow );
- /**
- * The downsample ratio.
- *
- * @type {number}
- * @default 2
- */
- this.downSampleRatio = downSampleRatio;
- /**
- * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
- * its effect once per frame in `updateBefore()`.
- *
- * @type {string}
- * @default 'frame'
- */
- this.updateBeforeType = NodeUpdateType.FRAME;
- // render targets
- /**
- * The render target for the depth pre-pass.
- *
- * @private
- * @type {RenderTarget}
- */
- this._renderTargetDepthBuffer = new RenderTarget();
- this._renderTargetDepthBuffer.depthTexture = new DepthTexture();
- this._renderTargetDepthBuffer.depthTexture.type = FloatType;
- /**
- * The render target for the mask pass.
- *
- * @private
- * @type {RenderTarget}
- */
- this._renderTargetMaskBuffer = new RenderTarget();
- /**
- * The render target for the mask downsample.
- *
- * @private
- * @type {RenderTarget}
- */
- this._renderTargetMaskDownSampleBuffer = new RenderTarget( 1, 1, { depthBuffer: false } );
- /**
- * The first render target for the edge detection.
- *
- * @private
- * @type {RenderTarget}
- */
- this._renderTargetEdgeBuffer1 = new RenderTarget( 1, 1, { depthBuffer: false } );
- /**
- * The second render target for the edge detection.
- *
- * @private
- * @type {RenderTarget}
- */
- this._renderTargetEdgeBuffer2 = new RenderTarget( 1, 1, { depthBuffer: false } );
- /**
- * The first render target for the blur pass.
- *
- * @private
- * @type {RenderTarget}
- */
- this._renderTargetBlurBuffer1 = new RenderTarget( 1, 1, { depthBuffer: false } );
- /**
- * The second render target for the blur pass.
- *
- * @private
- * @type {RenderTarget}
- */
- this._renderTargetBlurBuffer2 = new RenderTarget( 1, 1, { depthBuffer: false } );
- /**
- * The render target for the final composite.
- *
- * @private
- * @type {RenderTarget}
- */
- this._renderTargetComposite = new RenderTarget( 1, 1, { depthBuffer: false } );
- // uniforms
- /**
- * Represents the near value of the scene's camera.
- *
- * @private
- * @type {ReferenceNode<float>}
- */
- this._cameraNear = reference( 'near', 'float', camera );
- /**
- * Represents the far value of the scene's camera.
- *
- * @private
- * @type {ReferenceNode<float>}
- */
- this._cameraFar = reference( 'far', 'float', camera );
- /**
- * Uniform that represents the blur direction of the pass.
- *
- * @private
- * @type {UniformNode<vec2>}
- */
- this._blurDirection = uniform( new Vector2() );
- /**
- * Texture node that holds the data from the depth pre-pass.
- *
- * @private
- * @type {TextureNode}
- */
- this._depthTextureUniform = texture( this._renderTargetDepthBuffer.depthTexture );
- /**
- * Texture node that holds the data from the mask pass.
- *
- * @private
- * @type {TextureNode}
- */
- this._maskTextureUniform = texture( this._renderTargetMaskBuffer.texture );
- /**
- * Texture node that holds the data from the mask downsample pass.
- *
- * @private
- * @type {TextureNode}
- */
- this._maskTextureDownsSampleUniform = texture( this._renderTargetMaskDownSampleBuffer.texture );
- /**
- * Texture node that holds the data from the first edge detection pass.
- *
- * @private
- * @type {TextureNode}
- */
- this._edge1TextureUniform = texture( this._renderTargetEdgeBuffer1.texture );
- /**
- * Texture node that holds the data from the second edge detection pass.
- *
- * @private
- * @type {TextureNode}
- */
- this._edge2TextureUniform = texture( this._renderTargetEdgeBuffer2.texture );
- /**
- * Texture node that holds the current blurred color data.
- *
- * @private
- * @type {TextureNode}
- */
- this._blurColorTextureUniform = texture( this._renderTargetEdgeBuffer1.texture );
- // constants
- /**
- * Visible edge color.
- *
- * @private
- * @type {Node<vec3>}
- */
- this._visibleEdgeColor = vec3( 1, 0, 0 );
- /**
- * Hidden edge color.
- *
- * @private
- * @type {Node<vec3>}
- */
- this._hiddenEdgeColor = vec3( 0, 1, 0 );
- // materials
- /**
- * The material for the depth pre-pass.
- *
- * @private
- * @type {NodeMaterial}
- */
- this._depthMaterial = new NodeMaterial();
- this._depthMaterial.fragmentNode = vec4( 0, 0, 0, 1 );
- this._depthMaterial.name = 'OutlineNode.depth';
- /**
- * The material for preparing the mask.
- *
- * @private
- * @type {NodeMaterial}
- */
- this._prepareMaskMaterial = new NodeMaterial();
- this._prepareMaskMaterial.name = 'OutlineNode.prepareMask';
- /**
- * The copy material
- *
- * @private
- * @type {NodeMaterial}
- */
- this._materialCopy = new NodeMaterial();
- this._materialCopy.name = 'OutlineNode.copy';
- /**
- * The edge detection material.
- *
- * @private
- * @type {NodeMaterial}
- */
- this._edgeDetectionMaterial = new NodeMaterial();
- this._edgeDetectionMaterial.name = 'OutlineNode.edgeDetection';
- /**
- * The material that is used to render in the blur pass.
- *
- * @private
- * @type {NodeMaterial}
- */
- this._separableBlurMaterial = new NodeMaterial();
- this._separableBlurMaterial.name = 'OutlineNode.separableBlur';
- /**
- * The material that is used to render in the blur pass.
- *
- * @private
- * @type {NodeMaterial}
- */
- this._separableBlurMaterial2 = new NodeMaterial();
- this._separableBlurMaterial2.name = 'OutlineNode.separableBlur2';
- /**
- * The final composite material.
- *
- * @private
- * @type {NodeMaterial}
- */
- this._compositeMaterial = new NodeMaterial();
- this._compositeMaterial.name = 'OutlineNode.composite';
- /**
- * A set to cache selected objects in the scene.
- *
- * @private
- * @type {Set<Object3D>}
- */
- this._selectionCache = new Set();
- /**
- * The result of the effect is represented as a separate texture node.
- *
- * @private
- * @type {PassTextureNode}
- */
- this._textureNode = passTexture( this, this._renderTargetComposite.texture );
- }
- /**
- * A mask value that represents the visible edge.
- *
- * @return {Node<float>} The visible edge.
- */
- get visibleEdge() {
- return this.r;
- }
- /**
- * A mask value that represents the hidden edge.
- *
- * @return {Node<float>} The hidden edge.
- */
- get hiddenEdge() {
- return this.g;
- }
- /**
- * Returns the result of the effect as a texture node.
- *
- * @return {PassTextureNode} A texture node that represents the result of the effect.
- */
- getTextureNode() {
- return this._textureNode;
- }
- /**
- * Sets the size of the effect.
- *
- * @param {number} width - The width of the effect.
- * @param {number} height - The height of the effect.
- */
- setSize( width, height ) {
- this._renderTargetDepthBuffer.setSize( width, height );
- this._renderTargetMaskBuffer.setSize( width, height );
- this._renderTargetComposite.setSize( width, height );
- // downsample 1
- let resx = Math.round( width / this.downSampleRatio );
- let resy = Math.round( height / this.downSampleRatio );
- this._renderTargetMaskDownSampleBuffer.setSize( resx, resy );
- this._renderTargetEdgeBuffer1.setSize( resx, resy );
- this._renderTargetBlurBuffer1.setSize( resx, resy );
- // downsample 2
- resx = Math.round( resx / 2 );
- resy = Math.round( resy / 2 );
- this._renderTargetEdgeBuffer2.setSize( resx, resy );
- this._renderTargetBlurBuffer2.setSize( resx, resy );
- }
- /**
- * This method is used to render the effect once per frame.
- *
- * @param {NodeFrame} frame - The current node frame.
- */
- updateBefore( frame ) {
- const { renderer } = frame;
- const { camera, scene } = this;
- _rendererState = RendererUtils.resetRendererAndSceneState( renderer, scene, _rendererState );
- //
- const size = renderer.getDrawingBufferSize( _size );
- this.setSize( size.width, size.height );
- //
- renderer.setClearColor( 0xffffff, 1 );
- this._updateSelectionCache();
- // 1. Draw non-selected objects in the depth buffer
- scene.overrideMaterial = this._depthMaterial;
- renderer.setRenderTarget( this._renderTargetDepthBuffer );
- renderer.setRenderObjectFunction( ( object, ...params ) => {
- if ( this._selectionCache.has( object ) === false ) {
- renderer.renderObject( object, ...params );
- }
- } );
- renderer.render( scene, camera );
- // 2. Draw only the selected objects by comparing the depth buffer of non-selected objects
- scene.overrideMaterial = this._prepareMaskMaterial;
- renderer.setRenderTarget( this._renderTargetMaskBuffer );
- renderer.setRenderObjectFunction( ( object, ...params ) => {
- if ( this._selectionCache.has( object ) === true ) {
- renderer.renderObject( object, ...params );
- }
- } );
- renderer.render( scene, camera );
- //
- renderer.setRenderObjectFunction( _rendererState.renderObjectFunction );
- this._selectionCache.clear();
- // 3. Downsample to (at least) half resolution
- _quadMesh.material = this._materialCopy;
- renderer.setRenderTarget( this._renderTargetMaskDownSampleBuffer );
- _quadMesh.render( renderer );
- // 4. Perform edge detection (half resolution)
- _quadMesh.material = this._edgeDetectionMaterial;
- renderer.setRenderTarget( this._renderTargetEdgeBuffer1 );
- _quadMesh.render( renderer );
- // 5. Apply blur (half resolution)
- this._blurColorTextureUniform.value = this._renderTargetEdgeBuffer1.texture;
- this._blurDirection.value.copy( _BLUR_DIRECTION_X );
- _quadMesh.material = this._separableBlurMaterial;
- renderer.setRenderTarget( this._renderTargetBlurBuffer1 );
- _quadMesh.render( renderer );
- this._blurColorTextureUniform.value = this._renderTargetBlurBuffer1.texture;
- this._blurDirection.value.copy( _BLUR_DIRECTION_Y );
- renderer.setRenderTarget( this._renderTargetEdgeBuffer1 );
- _quadMesh.render( renderer );
- // 6. Apply blur (quarter resolution)
- this._blurColorTextureUniform.value = this._renderTargetEdgeBuffer1.texture;
- this._blurDirection.value.copy( _BLUR_DIRECTION_X );
- _quadMesh.material = this._separableBlurMaterial2;
- renderer.setRenderTarget( this._renderTargetBlurBuffer2 );
- _quadMesh.render( renderer );
- this._blurColorTextureUniform.value = this._renderTargetBlurBuffer2.texture;
- this._blurDirection.value.copy( _BLUR_DIRECTION_Y );
- renderer.setRenderTarget( this._renderTargetEdgeBuffer2 );
- _quadMesh.render( renderer );
- // 7. Composite
- _quadMesh.material = this._compositeMaterial;
- renderer.setRenderTarget( this._renderTargetComposite );
- _quadMesh.render( renderer );
- // restore
- RendererUtils.restoreRendererAndSceneState( renderer, scene, _rendererState );
- }
- /**
- * This method is used to setup the effect's TSL code.
- *
- * @param {NodeBuilder} builder - The current node builder.
- * @return {PassTextureNode}
- */
- setup() {
- // prepare mask material
- const prepareMask = () => {
- const depth = this._depthTextureUniform.sample( screenUV );
- let viewZNode;
- if ( this.camera.isPerspectiveCamera ) {
- viewZNode = perspectiveDepthToViewZ( depth, this._cameraNear, this._cameraFar );
- } else {
- viewZNode = orthographicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
- }
- const depthTest = positionView.z.lessThanEqual( viewZNode ).select( 1, 0 );
- return vec4( 0.0, depthTest, 1.0, 1.0 );
- };
- this._prepareMaskMaterial.fragmentNode = prepareMask();
- this._prepareMaskMaterial.needsUpdate = true;
- // copy material
- this._materialCopy.fragmentNode = this._maskTextureUniform;
- this._materialCopy.needsUpdate = true;
- // edge detection material
- const edgeDetection = Fn( () => {
- const resolution = textureSize( this._maskTextureDownsSampleUniform );
- const invSize = vec2( 1 ).div( resolution ).toVar();
- const uvOffset = vec4( 1.0, 0.0, 0.0, 1.0 ).mul( vec4( invSize, invSize ) );
- const uvNode = uv();
- const c1 = this._maskTextureDownsSampleUniform.sample( uvNode.add( uvOffset.xy ) ).toVar();
- const c2 = this._maskTextureDownsSampleUniform.sample( uvNode.sub( uvOffset.xy ) ).toVar();
- const c3 = this._maskTextureDownsSampleUniform.sample( uvNode.add( uvOffset.yw ) ).toVar();
- const c4 = this._maskTextureDownsSampleUniform.sample( uvNode.sub( uvOffset.yw ) ).toVar();
- const diff1 = mul( c1.r.sub( c2.r ), 0.5 );
- const diff2 = mul( c3.r.sub( c4.r ), 0.5 );
- const d = vec2( diff1, diff2 ).length();
- const a1 = min( c1.g, c2.g );
- const a2 = min( c3.g, c4.g );
- const visibilityFactor = min( a1, a2 );
- const edgeColor = visibilityFactor.oneMinus().greaterThan( 0.001 ).select( this._visibleEdgeColor, this._hiddenEdgeColor );
- return vec4( edgeColor, 1 ).mul( d );
- } );
- this._edgeDetectionMaterial.fragmentNode = edgeDetection();
- this._edgeDetectionMaterial.needsUpdate = true;
- // separable blur material
- const MAX_RADIUS = 4;
- const gaussianPdf = Fn( ( [ x, sigma ] ) => {
- return float( 0.39894 ).mul( exp( float( - 0.5 ).mul( x ).mul( x ).div( sigma.mul( sigma ) ) ).div( sigma ) );
- } );
- const separableBlur = Fn( ( [ kernelRadius ] ) => {
- const resolution = textureSize( this._maskTextureDownsSampleUniform );
- const invSize = vec2( 1 ).div( resolution ).toVar();
- const uvNode = uv();
- const sigma = kernelRadius.div( 2 ).toVar();
- const weightSum = gaussianPdf( 0, sigma ).toVar();
- const diffuseSum = this._blurColorTextureUniform.sample( uvNode ).mul( weightSum ).toVar();
- const delta = this._blurDirection.mul( invSize ).mul( kernelRadius ).div( MAX_RADIUS ).toVar();
- const uvOffset = delta.toVar();
- Loop( { start: int( 1 ), end: int( MAX_RADIUS ), type: 'int', condition: '<=' }, ( { i } ) => {
- const x = kernelRadius.mul( float( i ) ).div( MAX_RADIUS );
- const w = gaussianPdf( x, sigma );
- const sample1 = this._blurColorTextureUniform.sample( uvNode.add( uvOffset ) );
- const sample2 = this._blurColorTextureUniform.sample( uvNode.sub( uvOffset ) );
- diffuseSum.addAssign( sample1.add( sample2 ).mul( w ) );
- weightSum.addAssign( w.mul( 2 ) );
- uvOffset.addAssign( delta );
- } );
- return diffuseSum.div( weightSum );
- } );
- this._separableBlurMaterial.fragmentNode = separableBlur( this.edgeThicknessNode );
- this._separableBlurMaterial.needsUpdate = true;
- this._separableBlurMaterial2.fragmentNode = separableBlur( MAX_RADIUS );
- this._separableBlurMaterial2.needsUpdate = true;
- // composite material
- const composite = Fn( () => {
- const edgeValue1 = this._edge1TextureUniform;
- const edgeValue2 = this._edge2TextureUniform;
- const maskColor = this._maskTextureUniform;
- const edgeValue = edgeValue1.add( edgeValue2.mul( this.edgeGlowNode ) );
- return maskColor.r.mul( edgeValue );
- } );
- this._compositeMaterial.fragmentNode = composite();
- this._compositeMaterial.needsUpdate = true;
- return this._textureNode;
- }
- /**
- * Frees internal resources. This method should be called
- * when the effect is no longer required.
- */
- dispose() {
- this.selectedObjects.length = 0;
- this._renderTargetDepthBuffer.dispose();
- this._renderTargetMaskBuffer.dispose();
- this._renderTargetMaskDownSampleBuffer.dispose();
- this._renderTargetEdgeBuffer1.dispose();
- this._renderTargetEdgeBuffer2.dispose();
- this._renderTargetBlurBuffer1.dispose();
- this._renderTargetBlurBuffer2.dispose();
- this._renderTargetComposite.dispose();
- this._depthMaterial.dispose();
- this._prepareMaskMaterial.dispose();
- this._materialCopy.dispose();
- this._edgeDetectionMaterial.dispose();
- this._separableBlurMaterial.dispose();
- this._separableBlurMaterial2.dispose();
- this._compositeMaterial.dispose();
- }
- /**
- * Updates the selection cache based on the selected objects.
- *
- * @private
- */
- _updateSelectionCache() {
- for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
- const selectedObject = this.selectedObjects[ i ];
- selectedObject.traverse( ( object ) => {
- if ( object.isMesh ) this._selectionCache.add( object );
- } );
- }
- }
- }
- export default OutlineNode;
- /**
- * TSL function for creating an outline effect around selected objects.
- *
- * @tsl
- * @function
- * @param {Scene} scene - A reference to the scene.
- * @param {Camera} camera - The camera the scene is rendered with.
- * @param {Object} params - The configuration parameters.
- * @param {Array<Object3D>} params.selectedObjects - An array of selected objects.
- * @param {Node<float>} [params.edgeThickness=float(1)] - The thickness of the edges.
- * @param {Node<float>} [params.edgeGlow=float(0)] - Can be used for animated glow/pulse effects.
- * @param {number} [params.downSampleRatio=2] - The downsample ratio.
- * @returns {OutlineNode}
- */
- export const outline = ( scene, camera, params ) => nodeObject( new OutlineNode( scene, camera, params ) );
|