diff --git a/examples/webgpu_caustics.html b/examples/webgpu_caustics.html index 27e06f5504641b..b5355f7028d91a 100644 --- a/examples/webgpu_caustics.html +++ b/examples/webgpu_caustics.html @@ -179,6 +179,7 @@ renderer.setSize( window.innerWidth, window.innerHeight ); renderer.setAnimationLoop( animate ); renderer.shadowMap.enabled = true; + renderer.shadowMap.color = true; renderer.inspector = new Inspector(); document.body.appendChild( renderer.domElement ); diff --git a/examples/webgpu_shadowmap.html b/examples/webgpu_shadowmap.html index 305333dad34d5e..bbf86ccf20cd70 100644 --- a/examples/webgpu_shadowmap.html +++ b/examples/webgpu_shadowmap.html @@ -107,22 +107,7 @@ const discardNode = hash( vertexIndex ).greaterThan( 0.5 ); - materialCustomShadow.colorNode = Fn( () => { - - discardNode.discard(); - - return materialColor; - - } )(); - - - materialCustomShadow.castShadowNode = Fn( () => { - - discardNode.discard(); - - return materialColor; - - } )(); + materialCustomShadow.maskNode = discardNode; torusKnot = new THREE.Mesh( geometry, materialCustomShadow ); torusKnot.scale.multiplyScalar( 1 / 18 ); diff --git a/examples/webgpu_shadowmap_opacity.html b/examples/webgpu_shadowmap_opacity.html index 3b1856cc0d69cb..459c2b6f243f8a 100644 --- a/examples/webgpu_shadowmap_opacity.html +++ b/examples/webgpu_shadowmap_opacity.html @@ -59,6 +59,7 @@ renderer.toneMapping = THREE.AgXToneMapping; renderer.toneMappingExposure = 1.5; renderer.shadowMap.enabled = true; + renderer.shadowMap.color = true; renderer.inspector = new Inspector(); container.appendChild( renderer.domElement ); diff --git a/examples/webgpu_tsl_angular_slicing.html b/examples/webgpu_tsl_angular_slicing.html index bb575d6582c194..e7628ad43d4388 100644 --- a/examples/webgpu_tsl_angular_slicing.html +++ b/examples/webgpu_tsl_angular_slicing.html @@ -113,13 +113,16 @@ const sliceArc = uniform( 1.25 ); const sliceColor = uniform( color( '#b62f58' ) ); - // output + // mask - slicedMaterial.outputNode = Fn( () => { + const mask = inAngle( positionLocal.xy, sliceStart, sliceArc ).not(); + + slicedMaterial.maskNode = mask; + //slicedMaterial.maskShadowNode = mask; // optional: custom mask shadows - // discard + // output - inAngle( positionLocal.xy, sliceStart, sliceArc ).discard(); + slicedMaterial.outputNode = Fn( () => { // backface color @@ -134,18 +137,6 @@ } )(); - // shadow - - slicedMaterial.castShadowNode = Fn( () => { - - // discard - - inAngle( positionLocal.xy, sliceStart, sliceArc ).discard(); - - return vec4( 0, 0, 0, 1 ); - - } )(); - // model const dracoLoader = new DRACOLoader(); diff --git a/examples/webgpu_volume_caustics.html b/examples/webgpu_volume_caustics.html index 0399ff8a5165a1..d9dc37582b08b6 100644 --- a/examples/webgpu_volume_caustics.html +++ b/examples/webgpu_volume_caustics.html @@ -165,6 +165,7 @@ renderer = new THREE.WebGPURenderer( { antialias: true } ); renderer.shadowMap.enabled = true; + renderer.shadowMap.color = true; renderer.inspector = new Inspector(); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); diff --git a/src/Three.TSL.js b/src/Three.TSL.js index 80141c70d1a687..505bee4831a981 100644 --- a/src/Three.TSL.js +++ b/src/Three.TSL.js @@ -127,6 +127,7 @@ export const clamp = TSL.clamp; export const clearcoat = TSL.clearcoat; export const clearcoatNormalView = TSL.clearcoatNormalView; export const clearcoatRoughness = TSL.clearcoatRoughness; +export const clipSpace = TSL.clipSpace; export const code = TSL.code; export const color = TSL.color; export const colorSpaceToWorking = TSL.colorSpaceToWorking; diff --git a/src/materials/nodes/NodeMaterial.js b/src/materials/nodes/NodeMaterial.js index 9bf2e531e9efee..3dab50b4e7e820 100644 --- a/src/materials/nodes/NodeMaterial.js +++ b/src/materials/nodes/NodeMaterial.js @@ -240,6 +240,14 @@ class NodeMaterial extends Material { */ this.maskNode = null; + /** + * This node can be used to implement a shadow mask for the material. + * + * @type {?Node} + * @default null + */ + this.maskShadowNode = null; + /** * The local vertex positions are computed based on multiple factors like the * attribute data, morphing or skinning. This node property allows to overwrite @@ -531,6 +539,8 @@ class NodeMaterial extends Material { const vertexNode = this.vertexNode || mvp; + builder.context.clipSpace = vertexNode; + builder.stack.outputNode = vertexNode; this.setupHardwareClipping( builder ); @@ -795,7 +805,7 @@ class NodeMaterial extends Material { this.setupPosition( builder ); - builder.context.vertex = builder.removeStack(); + builder.context.position = builder.removeStack(); return modelViewProjection; @@ -1339,6 +1349,7 @@ class NodeMaterial extends Material { this.backdropAlphaNode = source.backdropAlphaNode; this.alphaTestNode = source.alphaTestNode; this.maskNode = source.maskNode; + this.maskShadowNode = source.maskShadowNode; this.positionNode = source.positionNode; this.geometryNode = source.geometryNode; diff --git a/src/nodes/accessors/Position.js b/src/nodes/accessors/Position.js index d22225eea962cd..d15176e91321eb 100644 --- a/src/nodes/accessors/Position.js +++ b/src/nodes/accessors/Position.js @@ -1,6 +1,28 @@ import { attribute } from '../core/AttributeNode.js'; -import { Fn, vec3 } from '../tsl/TSLCore.js'; +import { Fn, vec3, vec4 } from '../tsl/TSLCore.js'; import { modelWorldMatrix } from './ModelNode.js'; +import { cameraProjectionMatrixInverse, cameraWorldMatrix } from './Camera.js'; +import { warnOnce } from '../../utils.js'; + +/** + * TSL object that represents the clip space position of the current rendered object. + * + * @tsl + * @type {VaryingNode} + */ +export const clipSpace = /*@__PURE__*/ ( Fn( ( builder ) => { + + if ( builder.shaderStage !== 'fragment' ) { + + warnOnce( 'TSL: `clipSpace` is only available in fragment stage.' ); + + return vec4(); + + } + + return builder.context.clipSpace.toVarying( 'v_clipSpace' ); + +} ).once() )(); /** * TSL object that represents the position attribute of the current rendered object. @@ -35,6 +57,14 @@ export const positionPrevious = /*@__PURE__*/ positionGeometry.toVarying( 'posit */ export const positionWorld = /*@__PURE__*/ ( Fn( ( builder ) => { + if ( builder.shaderStage === 'fragment' && builder.material.vertexNode ) { + + // reconstruct world position from view position + + return cameraWorldMatrix.mul( positionView ).xyz.toVar( 'positionWorld' ); + + } + return modelWorldMatrix.mul( positionLocal ).xyz.toVarying( builder.getSubBuildProperty( 'v_positionWorld' ) ); }, 'vec3' ).once( [ 'POSITION' ] ) )(); @@ -61,6 +91,16 @@ export const positionWorldDirection = /*@__PURE__*/ ( Fn( () => { */ export const positionView = /*@__PURE__*/ ( Fn( ( builder ) => { + if ( builder.shaderStage === 'fragment' && builder.material.vertexNode ) { + + // reconstruct view position from clip space + + const viewPos = cameraProjectionMatrixInverse.mul( clipSpace ); + + return viewPos.xyz.div( viewPos.w ).toVar( 'positionView' ); + + } + return builder.context.setupPositionView().toVarying( 'v_positionView' ); }, 'vec3' ).once( [ 'POSITION' ] ) )(); diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js index 36e0afc5e09f08..527ddc54c0e930 100644 --- a/src/nodes/core/NodeBuilder.js +++ b/src/nodes/core/NodeBuilder.js @@ -2916,9 +2916,9 @@ class NodeBuilder { this.setBuildStage( buildStage ); - if ( this.context.vertex && this.context.vertex.isNode ) { + if ( this.context.position && this.context.position.isNode ) { - this.flowNodeFromShaderStage( 'vertex', this.context.vertex ); + this.flowNodeFromShaderStage( 'vertex', this.context.position ); } diff --git a/src/nodes/lighting/ShadowNode.js b/src/nodes/lighting/ShadowNode.js index 171eee030841d5..0d1db19cf77bc9 100644 --- a/src/nodes/lighting/ShadowNode.js +++ b/src/nodes/lighting/ShadowNode.js @@ -518,24 +518,40 @@ class ShadowNode extends ShadowBaseNode { let shadowColor; - if ( shadowMap.texture.isCubeTexture ) { + if ( renderer.shadowMap.color === true ) { - // For cube shadow maps (point lights), use cubeTexture with vec3 coordinates - shadowColor = cubeTexture( shadowMap.texture, shadowCoord.xyz ); + if ( shadowMap.texture.isCubeTexture ) { - } else { + // For cube shadow maps (point lights), use cubeTexture with vec3 coordinates + shadowColor = cubeTexture( shadowMap.texture, shadowCoord.xyz ); - shadowColor = texture( shadowMap.texture, shadowCoord ); + } else { - if ( depthTexture.isArrayTexture ) { + shadowColor = texture( shadowMap.texture, shadowCoord ); - shadowColor = shadowColor.depth( this.depthLayer ); + if ( depthTexture.isArrayTexture ) { + + shadowColor = shadowColor.depth( this.depthLayer ); + + } } } - const shadowOutput = mix( 1, shadowNode.rgb.mix( shadowColor, 1 ), shadowIntensity.mul( shadowColor.a ) ).toVar(); + // + + let shadowOutput; + + if ( shadowColor ) { + + shadowOutput = mix( 1, shadowNode.rgb.mix( shadowColor, 1 ), shadowIntensity.mul( shadowColor.a ) ).toVar(); + + } else { + + shadowOutput = mix( 1, shadowNode, shadowIntensity ).toVar(); + + } this.shadowMap = shadowMap; this.shadow.map = shadowMap; @@ -544,17 +560,23 @@ class ShadowNode extends ShadowBaseNode { const inspectName = `${ this.light.type } Shadow [ ${ this.light.name || 'ID: ' + this.light.id } ]`; - return shadowOutput.toInspector( `${ inspectName } / Color`, () => { + if ( shadowColor ) { - if ( this.shadowMap.texture.isCubeTexture ) { + shadowOutput.toInspector( `${ inspectName } / Color`, () => { - return cubeTexture( this.shadowMap.texture ); + if ( this.shadowMap.texture.isCubeTexture ) { - } + return cubeTexture( this.shadowMap.texture ); - return texture( this.shadowMap.texture ); + } + + return texture( this.shadowMap.texture ); + + } ); + + } - } ).toInspector( `${ inspectName } / Depth`, () => { + return shadowOutput.toInspector( `${ inspectName } / Depth`, () => { // TODO: Use linear depth diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index 217dfc0ddc78d8..1685be3b1c9040 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -32,7 +32,7 @@ import { Vector4 } from '../../math/Vector4.js'; import { RenderTarget } from '../../core/RenderTarget.js'; import { DoubleSide, BackSide, FrontSide, SRGBColorSpace, NoToneMapping, LinearFilter, HalfFloatType, RGBAFormat, PCFShadowMap } from '../../constants.js'; -import { float, vec3, vec4 } from '../../nodes/tsl/TSLCore.js'; +import { float, vec3, vec4, Fn } from '../../nodes/tsl/TSLCore.js'; import { reference } from '../../nodes/accessors/ReferenceNode.js'; import { highpModelNormalViewMatrix, highpModelViewMatrix } from '../../nodes/accessors/ModelNode.js'; import { context } from '../../nodes/core/ContextNode.js'; @@ -658,6 +658,7 @@ class Renderer { * Shadow map configuration * @typedef {Object} ShadowMapConfig * @property {boolean} enabled - Whether to globally enable shadows or not. + * @property {boolean} color - Whether to include shadow color or not. * @property {number} type - The shadow map type. */ @@ -668,6 +669,7 @@ class Renderer { */ this.shadowMap = { enabled: false, + color: false, type: PCFShadowMap }; @@ -3035,12 +3037,13 @@ class Renderer { const hasMap = material.map !== null; const hasColorNode = material.colorNode && material.colorNode.isNode; const hasCastShadowNode = material.castShadowNode && material.castShadowNode.isNode; + const hasMaskNode = ( material.maskShadowNode && material.maskShadowNode.isNode ) || ( material.maskNode && material.maskNode.isNode ); let positionNode = null; let colorNode = null; let depthNode = null; - if ( hasMap || hasColorNode || hasCastShadowNode ) { + if ( hasMap || hasColorNode || hasCastShadowNode || hasMaskNode ) { let shadowRGB; let shadowAlpha; @@ -3050,6 +3053,12 @@ class Renderer { shadowRGB = material.castShadowNode.rgb; shadowAlpha = material.castShadowNode.a; + if ( this.shadowMap.color !== true ) { + + warnOnce( 'Renderer: `shadowMap.color` needs to be enabled when using `material.castShadowNode`.' ); + + } + } else { shadowRGB = vec3( 0 ); @@ -3071,6 +3080,20 @@ class Renderer { colorNode = vec4( shadowRGB, shadowAlpha ); + if ( hasMaskNode ) { + + const maskNode = material.maskShadowNode || material.maskNode; + + colorNode = Fn( ( [ color ] ) => { + + maskNode.not().discard(); + + return color; + + } )( colorNode ); + + } + } if ( material.depthNode && material.depthNode.isNode ) { diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js index 828c2062ce1980..7370d60124d3f5 100644 --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js @@ -1663,7 +1663,7 @@ ${ flowData.code } if ( shaderStage === 'vertex' ) { - this.getBuiltin( 'position', 'Vertex', 'vec4', 'vertex' ); + this.getBuiltin( 'position', 'builtinClipSpace', 'vec4', 'vertex' ); } @@ -1961,7 +1961,7 @@ ${ flowData.code } if ( shaderStage === 'vertex' ) { - flow += `varyings.Vertex = ${ flowSlotData.result };`; + flow += `varyings.builtinClipSpace = ${ flowSlotData.result };`; } else if ( shaderStage === 'fragment' ) {