Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebGPURenderer: support clipping #27691

Merged
merged 14 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@
"webgpu_backdrop_water",
"webgpu_camera_logarithmicdepthbuffer",
"webgpu_clearcoat",
"webgpu_clipping",
"webgpu_compute_audio",
"webgpu_compute_particles",
"webgpu_compute_particles_rain",
Expand Down
144 changes: 144 additions & 0 deletions examples/jsm/nodes/accessors/ClippingNode.js
Original file line number Diff line number Diff line change
@@ -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 ) );
2 changes: 2 additions & 0 deletions examples/jsm/nodes/core/NodeBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class NodeBuilder {
this.fogNode = null;
this.toneMappingNode = null;

this.clippingContext = null;

this.vertexShader = null;
this.fragmentShader = null;
this.computeShader = null;
Expand Down
30 changes: 30 additions & 0 deletions examples/jsm/nodes/materials/NodeMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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 );
Expand All @@ -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
Expand All @@ -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;
Expand Down
165 changes: 165 additions & 0 deletions examples/jsm/renderers/common/ClippingContext.js
Original file line number Diff line number Diff line change
@@ -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;
Loading