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' ) {