From 158d8a10d44b076b15fb7c579b55e2477340ac79 Mon Sep 17 00:00:00 2001 From: aardgoose Date: Fri, 9 Feb 2024 04:00:16 +0000 Subject: [PATCH] WebGPURenderer: Support clipping (#27691) * clipping * add ; * remove testing code * cleanup and adjust default settiing in example * remove unused import * cleanup ClippingNode * combine methods * rework to increase efficiency * update screenshot * fix asyncCompile * fix against upstream * cleanup * simplify * exclude tests --------- Co-authored-by: aardgoose --- examples/files.json | 1 + examples/jsm/nodes/accessors/ClippingNode.js | 144 +++++++++ examples/jsm/nodes/core/NodeBuilder.js | 2 + examples/jsm/nodes/materials/NodeMaterial.js | 30 ++ .../jsm/renderers/common/ClippingContext.js | 165 ++++++++++ examples/jsm/renderers/common/RenderObject.js | 43 +++ .../jsm/renderers/common/RenderObjects.js | 4 +- examples/jsm/renderers/common/Renderer.js | 47 ++- examples/jsm/renderers/common/nodes/Nodes.js | 1 + .../jsm/renderers/webgpu/WebGPUBackend.js | 7 +- examples/screenshots/webgpu_clipping.jpg | Bin 0 -> 22464 bytes examples/webgpu_clipping.html | 291 ++++++++++++++++++ test/e2e/puppeteer.js | 1 + 13 files changed, 730 insertions(+), 6 deletions(-) create mode 100644 examples/jsm/nodes/accessors/ClippingNode.js create mode 100644 examples/jsm/renderers/common/ClippingContext.js create mode 100644 examples/screenshots/webgpu_clipping.jpg create mode 100644 examples/webgpu_clipping.html diff --git a/examples/files.json b/examples/files.json index a912ac96fe03e4..12152d8fd7b153 100644 --- a/examples/files.json +++ b/examples/files.json @@ -321,6 +321,7 @@ "webgpu_backdrop_water", "webgpu_camera_logarithmicdepthbuffer", "webgpu_clearcoat", + "webgpu_clipping", "webgpu_compute_audio", "webgpu_compute_particles", "webgpu_compute_particles_rain", diff --git a/examples/jsm/nodes/accessors/ClippingNode.js b/examples/jsm/nodes/accessors/ClippingNode.js new file mode 100644 index 00000000000000..b3f67c84c8b0a6 --- /dev/null +++ b/examples/jsm/nodes/accessors/ClippingNode.js @@ -0,0 +1,144 @@ + +import Node from '../core/Node.js'; +import { nodeObject } from '../shadernode/ShaderNode.js'; +import { positionView } from './PositionNode.js'; +import { diffuseColor, property } from '../core/PropertyNode.js'; +import { tslFn } from '../shadernode/ShaderNode.js'; +import { loop } from '../utils/LoopNode.js'; +import { smoothstep } from '../math/MathNode.js'; +import { uniforms } from './UniformsNode.js'; + +class ClippingNode extends Node { + + constructor( scope = ClippingNode.DEFAULT ) { + + super(); + + this.scope = scope; + + } + + setup( builder ) { + + super.setup( builder ); + + const clippingContext = builder.clippingContext; + const { localClipIntersection, localClippingCount, globalClippingCount } = clippingContext; + + const numClippingPlanes = globalClippingCount + localClippingCount; + const numUnionClippingPlanes = localClipIntersection ? numClippingPlanes - localClippingCount : numClippingPlanes; + + if ( this.scope === ClippingNode.ALPHA_TO_COVERAGE ) { + + return this.setupAlphaToCoverage( clippingContext.planes, numClippingPlanes, numUnionClippingPlanes ); + + } else { + + return this.setupDefault( clippingContext.planes, numClippingPlanes, numUnionClippingPlanes ); + + } + + } + + setupAlphaToCoverage( planes, numClippingPlanes, numUnionClippingPlanes ) { + + return tslFn( () => { + + const clippingPlanes = uniforms( planes ); + + const distanceToPlane = property( 'float', 'distanceToPlane' ); + const distanceGradient = property( 'float', 'distanceToGradient' ); + + const clipOpacity = property( 'float', 'clipOpacity' ); + + clipOpacity.assign( 1 ); + + let plane; + + loop( numUnionClippingPlanes, ( { i } ) => { + + 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 ) ); + + clipOpacity.equal( 0.0 ).discard(); + + } ); + + if ( numUnionClippingPlanes < numClippingPlanes ) { + + const unionClipOpacity = property( 'float', 'unionclipOpacity' ); + + unionClipOpacity.assign( 1 ); + + loop( { start: numUnionClippingPlanes, end: numClippingPlanes }, ( { i } ) => { + + plane = clippingPlanes.element( i ); + + distanceToPlane.assign( positionView.dot( plane.xyz ).negate().add( plane.w ) ); + distanceGradient.assign( distanceToPlane.fwidth().div( 2.0 ) ); + + unionClipOpacity.mulAssign( smoothstep( distanceGradient.negate(), distanceGradient, distanceToPlane ).oneMinus() ); + + } ); + + clipOpacity.mulAssign( unionClipOpacity.oneMinus() ); + + } + + diffuseColor.a.mulAssign( clipOpacity ); + + diffuseColor.a.equal( 0.0 ).discard(); + + } )(); + + } + + setupDefault( planes, numClippingPlanes, numUnionClippingPlanes ) { + + return tslFn( () => { + + const clippingPlanes = uniforms( planes ); + + let plane; + + loop( numUnionClippingPlanes, ( { i } ) => { + + plane = clippingPlanes.element( i ); + positionView.dot( plane.xyz ).greaterThan( plane.w ).discard(); + + } ); + + if ( numUnionClippingPlanes < numClippingPlanes ) { + + const clipped = property( 'bool', 'clipped' ); + + clipped.assign( true ); + + loop( { start: numUnionClippingPlanes, end: numClippingPlanes }, ( { i } ) => { + + plane = clippingPlanes.element( i ); + clipped.assign( positionView.dot( plane.xyz ).greaterThan( plane.w ).and( clipped ) ); + + } ); + + clipped.discard(); + } + + } )(); + + } + +} + +ClippingNode.ALPHA_TO_COVERAGE = 'alphaToCoverage'; +ClippingNode.DEFAULT = 'default'; + +export default ClippingNode; + +export const clipping = () => nodeObject( new ClippingNode() ); + +export const clippingAlpha = () => nodeObject( new ClippingNode( ClippingNode.ALPHA_TO_COVERAGE ) ); diff --git a/examples/jsm/nodes/core/NodeBuilder.js b/examples/jsm/nodes/core/NodeBuilder.js index 4d0e90e37dfae4..6dd15892b96afc 100644 --- a/examples/jsm/nodes/core/NodeBuilder.js +++ b/examples/jsm/nodes/core/NodeBuilder.js @@ -72,6 +72,8 @@ class NodeBuilder { this.fogNode = null; this.toneMappingNode = null; + this.clippingContext = null; + this.vertexShader = null; this.fragmentShader = null; this.computeShader = null; diff --git a/examples/jsm/nodes/materials/NodeMaterial.js b/examples/jsm/nodes/materials/NodeMaterial.js index 325a08285e93b9..dbe5d6c596ccaa 100644 --- a/examples/jsm/nodes/materials/NodeMaterial.js +++ b/examples/jsm/nodes/materials/NodeMaterial.js @@ -19,6 +19,7 @@ import { lightingContext } from '../lighting/LightingContextNode.js'; import EnvironmentNode from '../lighting/EnvironmentNode.js'; import { depthPixel } from '../display/ViewportDepthNode.js'; import { cameraLogDepth } from '../accessors/CameraNode.js'; +import { clipping, clippingAlpha } from '../accessors/ClippingNode.js'; const NodeMaterials = new Map(); @@ -90,6 +91,8 @@ class NodeMaterial extends ShaderMaterial { let resultNode; + const clippingNode = this.setupClipping( builder ); + if ( this.fragmentNode === null ) { if ( this.depthWrite === true ) this.setupDepth( builder ); @@ -101,6 +104,8 @@ class NodeMaterial extends ShaderMaterial { const outgoingLightNode = this.setupLighting( builder ); + if ( clippingNode !== null ) builder.stack.add( clippingNode ); + resultNode = this.setupOutput( builder, vec4( outgoingLightNode, diffuseColor.a ) ); // OUTPUT NODE @@ -123,6 +128,31 @@ class NodeMaterial extends ShaderMaterial { } + setupClipping( builder ) { + + const { globalClippingCount, localClippingCount } = builder.clippingContext; + + let result = null; + + if ( globalClippingCount || localClippingCount ) { + + if ( this.alphaToCoverage ) { + + // to be added to flow when the color/alpha value has been determined + result = clippingAlpha(); + + } else { + + builder.stack.add( clipping() ); + + } + + } + + return result; + + } + setupDepth( builder ) { const { renderer } = builder; diff --git a/examples/jsm/renderers/common/ClippingContext.js b/examples/jsm/renderers/common/ClippingContext.js new file mode 100644 index 00000000000000..085e5d591abc26 --- /dev/null +++ b/examples/jsm/renderers/common/ClippingContext.js @@ -0,0 +1,165 @@ +import { Matrix3, Plane, Vector4 } from 'three'; + +const _plane = new Plane(); +const _viewNormalMatrix = new Matrix3(); + +let _clippingContextVersion = 0; + +class ClippingContext { + + constructor() { + + this.version = ++ _clippingContextVersion; + + this.globalClippingCount = 0; + + this.localClippingCount = 0; + this.localClippingEnabled = false; + this.localClipIntersection = false; + + this.planes = []; + + this.parentVersion = 0; + + } + + projectPlanes( source, offset ) { + + const l = source.length; + const planes = this.planes; + + for ( let i = 0; i < l; i ++ ) { + + _plane.copy( source[ i ] ).applyMatrix4( this.viewMatrix, _viewNormalMatrix ); + + const v = planes[ offset + i ]; + const normal = _plane.normal; + + v.x = - normal.x; + v.y = - normal.y; + v.z = - normal.z; + v.w = _plane.constant; + + } + + } + + updateGlobal( renderer, camera ) { + + const rendererClippingPlanes = renderer.clippingPlanes; + this.viewMatrix = camera.matrixWorldInverse; + + _viewNormalMatrix.getNormalMatrix( this.viewMatrix ); + + let update = false; + + if ( Array.isArray( rendererClippingPlanes ) && rendererClippingPlanes.length !== 0 ) { + + const l = rendererClippingPlanes.length; + + if ( l !== this.globalClippingCount ) { + + const planes = []; + + for ( let i = 0; i < l; i ++ ) { + + planes.push( new Vector4() ); + + } + + this.globalClippingCount = l; + this.planes = planes; + + update = true; + + } + + this.projectPlanes( rendererClippingPlanes, 0 ); + + } else if ( this.globalClippingCount !== 0 ) { + + this.globalClippingCount = 0; + this.planes = []; + update = true; + + } + + if ( renderer.localClippingEnabled !== this.localClippingEnabled ) { + + this.localClippingEnabled = renderer.localClippingEnabled; + update = true; + + } + + if ( update ) this.version = _clippingContextVersion ++; + + } + + update( parent, material ) { + + let update = false; + + if ( this !== parent && parent.version !== this.parentVersion ) { + + this.globalClippingCount = material.isShadowNodeMaterial ? 0 : parent.globalClippingCount; + this.localClippingEnabled = parent.localClippingEnabled; + this.planes = Array.from( parent.planes ); + this.parentVersion = parent.version; + this.viewMatrix = parent.viewMatrix; + + + update = true; + + } + + if ( this.localClippingEnabled ) { + + const localClippingPlanes = material.clippingPlanes; + + if ( ( Array.isArray( localClippingPlanes ) && localClippingPlanes.length !== 0 ) ) { + + const l = localClippingPlanes.length; + const planes = this.planes; + const offset = this.globalClippingCount; + + if ( update || l !== this.localClippingCount ) { + + planes.length = offset + l; + + for ( let i = 0; i < l; i ++ ) { + + planes[ offset + i ] = new Vector4(); + + } + + this.localClippingCount = l; + update = true; + + } + + this.projectPlanes( localClippingPlanes, offset ); + + + } else if ( this.localClippingCount !== 0 ) { + + this.localClippingCount = 0; + update = true; + + } + + if ( this.localClipIntersection !== material.clipIntersection ) { + + this.localClipIntersection = material.clipIntersection; + update = true; + + } + + } + + if ( update ) this.version = _clippingContextVersion ++; + + } + +} + +export default ClippingContext; diff --git a/examples/jsm/renderers/common/RenderObject.js b/examples/jsm/renderers/common/RenderObject.js index 5372250b8bcadd..2f0c0872444d41 100644 --- a/examples/jsm/renderers/common/RenderObject.js +++ b/examples/jsm/renderers/common/RenderObject.js @@ -1,3 +1,5 @@ +import ClippingContext from "./ClippingContext.js"; + let id = 0; export default class RenderObject { @@ -24,6 +26,10 @@ export default class RenderObject { this.pipeline = null; this.vertexBuffers = null; + this.updateClipping( renderContext.clippingContext ); + + this.clippingContextVersion = this.clippingContext.version; + this.initialNodesCacheKey = this.getNodesCacheKey(); this.initialCacheKey = this.getCacheKey(); @@ -44,6 +50,41 @@ export default class RenderObject { } + updateClipping( parent ) { + + const material = this.material; + + let clippingContext = this.clippingContext; + + if ( Array.isArray( material.clippingPlanes ) ) { + + if ( clippingContext === parent || ! clippingContext ) { + + clippingContext = new ClippingContext(); + this.clippingContext = clippingContext; + + } + + clippingContext.update( parent, material ); + + } else if ( this.clippingContext !== parent ) { + + this.clippingContext = parent; + + } + + } + + clippingNeedsUpdate () { + + if ( this.clippingContext.version === this.clippingContextVersion ) return false; + + this.clippingContextVersion = this.clippingContext.version; + + return true; + + } + getNodeBuilderState() { return this._nodeBuilderState || ( this._nodeBuilderState = this._nodes.getForRender( this ) ); @@ -131,6 +172,8 @@ export default class RenderObject { } + cacheKey += this.clippingContextVersion + ','; + if ( object.skeleton ) { cacheKey += object.skeleton.uuid + ','; diff --git a/examples/jsm/renderers/common/RenderObjects.js b/examples/jsm/renderers/common/RenderObjects.js index f223c38a917af2..e4bd3222cde20f 100644 --- a/examples/jsm/renderers/common/RenderObjects.js +++ b/examples/jsm/renderers/common/RenderObjects.js @@ -31,7 +31,9 @@ class RenderObjects { } else { - if ( renderObject.version !== material.version || renderObject.needsUpdate ) { + renderObject.updateClipping( renderContext.clippingContext ); + + if ( renderObject.version !== material.version || renderObject.needsUpdate || renderObject.clippingNeedsUpdate() ) { if ( renderObject.initialCacheKey !== renderObject.getCacheKey() ) { diff --git a/examples/jsm/renderers/common/Renderer.js b/examples/jsm/renderers/common/Renderer.js index 41a05a361664c6..9a5f3ee11cb8dd 100644 --- a/examples/jsm/renderers/common/Renderer.js +++ b/examples/jsm/renderers/common/Renderer.js @@ -11,6 +11,7 @@ import Textures from './Textures.js'; import Background from './Background.js'; import Nodes from './nodes/Nodes.js'; import Color4 from './Color4.js'; +import ClippingContext from './ClippingContext.js'; import { Scene, Frustum, Matrix4, Vector2, Vector3, Vector4, DoubleSide, BackSide, FrontSide, SRGBColorSpace, NoToneMapping } from 'three'; const _scene = new Scene(); @@ -58,6 +59,8 @@ class Renderer { this.depth = true; this.stencil = true; + this.clippingPlanes = []; + this.info = new Info(); // internals @@ -223,6 +226,9 @@ class Renderer { renderContext.depth = this.depth; renderContext.stencil = this.stencil; + if ( ! renderContext.clippingContext ) renderContext.clippingContext = new ClippingContext(); + renderContext.clippingContext.updateGlobal( this, camera ); + // sceneRef.onBeforeRender( this, scene, camera, renderTarget ); @@ -386,6 +392,9 @@ class Renderer { renderContext.scissorValue.width >>= activeMipmapLevel; renderContext.scissorValue.height >>= activeMipmapLevel; + if ( ! renderContext.clippingContext ) renderContext.clippingContext = new ClippingContext(); + renderContext.clippingContext.updateGlobal( this, camera ); + // sceneRef.onBeforeRender( this, scene, camera, renderTarget ); @@ -1107,10 +1116,42 @@ class Renderer { } - if ( overrideMaterial.isShadowNodeMaterial && ( material.shadowNode && material.shadowNode.isNode ) ) { + if ( overrideMaterial.isShadowNodeMaterial ) { + + overrideMaterial.side = material.shadowSide === null ? material.side : material.shadowSide; + + if ( material.shadowNode && material.shadowNode.isNode ) { + + overrideFragmentNode = overrideMaterial.fragmentNode; + overrideMaterial.fragmentNode = material.shadowNode; + + } + + if ( this.localClippingEnabled ) { + + if ( material.clipShadows ) { + + if ( overrideMaterial.clippingPlanes !== material.clippingPlanes ) { + + overrideMaterial.clippingPlanes = material.clippingPlanes; + overrideMaterial.needsUpdate = true; - overrideFragmentNode = overrideMaterial.fragmentNode; - overrideMaterial.fragmentNode = material.shadowNode; + } + + if ( overrideMaterial.clipIntersection !== material.clipIntersection ) { + + overrideMaterial.clipIntersection = material.clipIntersection; + + } + + } else if ( Array.isArray( overrideMaterial.clippingPlanes ) ) { + + overrideMaterial.clippingPlanes = null; + overrideMaterial.needsUpdate = true; + + } + + } } diff --git a/examples/jsm/renderers/common/nodes/Nodes.js b/examples/jsm/renderers/common/nodes/Nodes.js index 2364ee5d9ceb59..5e217a3c705bdb 100644 --- a/examples/jsm/renderers/common/nodes/Nodes.js +++ b/examples/jsm/renderers/common/nodes/Nodes.js @@ -114,6 +114,7 @@ class Nodes extends DataMap { nodeBuilder.environmentNode = this.getEnvironmentNode( renderObject.scene ); nodeBuilder.fogNode = this.getFogNode( renderObject.scene ); nodeBuilder.toneMappingNode = this.getToneMappingNode(); + nodeBuilder.clippingContext = renderObject.clippingContext; nodeBuilder.build(); nodeBuilderState = this._createNodeBuilderState( nodeBuilder ); diff --git a/examples/jsm/renderers/webgpu/WebGPUBackend.js b/examples/jsm/renderers/webgpu/WebGPUBackend.js index 8a4686c2b9d535..56c8548a6a6632 100644 --- a/examples/jsm/renderers/webgpu/WebGPUBackend.js +++ b/examples/jsm/renderers/webgpu/WebGPUBackend.js @@ -929,7 +929,8 @@ class WebGPUBackend extends Backend { data.side !== material.side || data.alphaToCoverage !== material.alphaToCoverage || data.sampleCount !== sampleCount || data.colorSpace !== colorSpace || data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat || - data.primitiveTopology !== primitiveTopology + data.primitiveTopology !== primitiveTopology || + data.clippingContextVersion !== renderObject.clippingContextVersion ) { data.material = material; data.materialVersion = material.version; @@ -947,6 +948,7 @@ class WebGPUBackend extends Backend { data.colorFormat = colorFormat; data.depthStencilFormat = depthStencilFormat; data.primitiveTopology = primitiveTopology; + data.clippingContextVersion = renderObject.clippingContextVersion; needsUpdate = true; @@ -975,7 +977,8 @@ class WebGPUBackend extends Backend { material.side, utils.getSampleCount( renderContext ), utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ), - utils.getPrimitiveTopology( object, material ) + utils.getPrimitiveTopology( object, material ), + renderObject.clippingContextVersion ].join(); } diff --git a/examples/screenshots/webgpu_clipping.jpg b/examples/screenshots/webgpu_clipping.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e93b3c0f398d17658c8993bd8a41852e7a1fd175 GIT binary patch literal 22464 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 + + + three.js webgpu - clipping planes + + + + + +
+ three.js webgpu - clipping +
+ + + + + + diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index 59134be10c8ba0..fe01ea5ccd622a 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -126,6 +126,7 @@ const exceptionList = [ 'webgpu_postprocessing_afterimage', 'webgpu_backdrop_water', 'webgpu_camera_logarithmicdepthbuffer', + 'webgpu_clipping', 'webgpu_loader_materialx', 'webgpu_materials_video', 'webgpu_materialx_noise',