first commit

This commit is contained in:
2026-06-24 09:48:54 +02:00
commit 41e62ddcad
33739 changed files with 4266226 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
import LightingNode from './LightingNode.js';
class AONode extends LightingNode {
static get type() {
return 'AONode';
}
constructor( aoNode = null ) {
super();
this.aoNode = aoNode;
}
setup( builder ) {
builder.context.ambientOcclusion.mulAssign( this.aoNode );
}
}
export default AONode;
+25
View File
@@ -0,0 +1,25 @@
import AnalyticLightNode from './AnalyticLightNode.js';
class AmbientLightNode extends AnalyticLightNode {
static get type() {
return 'AmbientLightNode';
}
constructor( light = null ) {
super( light );
}
setup( { context } ) {
context.irradiance.addAssign( this.colorNode );
}
}
export default AmbientLightNode;
+126
View File
@@ -0,0 +1,126 @@
import LightingNode from './LightingNode.js';
import { NodeUpdateType } from '../core/constants.js';
import { uniform } from '../core/UniformNode.js';
import { Color } from '../../math/Color.js';
import { renderGroup } from '../core/UniformGroupNode.js';
import { hash } from '../core/NodeUtils.js';
import { shadow } from './ShadowNode.js';
import { nodeObject } from '../tsl/TSLCore.js';
class AnalyticLightNode extends LightingNode {
static get type() {
return 'AnalyticLightNode';
}
constructor( light = null ) {
super();
this.light = light;
this.color = new Color();
this.colorNode = ( light && light.colorNode ) || uniform( this.color ).setGroup( renderGroup );
this.baseColorNode = null;
this.shadowNode = null;
this.shadowColorNode = null;
this.isAnalyticLightNode = true;
this.updateType = NodeUpdateType.FRAME;
}
getCacheKey() {
return hash( super.getCacheKey(), this.light.id, this.light.castShadow ? 1 : 0 );
}
getHash() {
return this.light.uuid;
}
setupShadowNode() {
return shadow( this.light );
}
setupShadow( builder ) {
const { renderer } = builder;
if ( renderer.shadowMap.enabled === false ) return;
let shadowColorNode = this.shadowColorNode;
if ( shadowColorNode === null ) {
const customShadowNode = this.light.shadow.shadowNode;
let shadowNode;
if ( customShadowNode !== undefined ) {
shadowNode = nodeObject( customShadowNode );
} else {
shadowNode = this.setupShadowNode( builder );
}
this.shadowNode = shadowNode;
this.shadowColorNode = shadowColorNode = this.colorNode.mul( shadowNode );
this.baseColorNode = this.colorNode;
}
//
this.colorNode = shadowColorNode;
}
setup( builder ) {
this.colorNode = this.baseColorNode || this.colorNode;
if ( this.light.castShadow ) {
if ( builder.object.receiveShadow ) {
this.setupShadow( builder );
}
} else if ( this.shadowNode !== null ) {
this.shadowNode.dispose();
this.shadowNode = null;
this.shadowColorNode = null;
}
}
update( /*frame*/ ) {
const { light } = this;
this.color.copy( light.color ).multiplyScalar( light.intensity );
}
}
export default AnalyticLightNode;
+30
View File
@@ -0,0 +1,30 @@
import LightingNode from './LightingNode.js';
import { cubeMapNode } from '../utils/CubeMapNode.js';
class BasicEnvironmentNode extends LightingNode {
static get type() {
return 'BasicEnvironmentNode';
}
constructor( envNode = null ) {
super();
this.envNode = envNode;
}
setup( builder ) {
// environment property is used in the finish() method of BasicLightingModel
builder.context.environment = cubeMapNode( this.envNode );
}
}
export default BasicEnvironmentNode;
+32
View File
@@ -0,0 +1,32 @@
import LightingNode from './LightingNode.js';
import { float } from '../tsl/TSLBase.js';
class BasicLightMapNode extends LightingNode {
static get type() {
return 'BasicLightMapNode';
}
constructor( lightMapNode = null ) {
super();
this.lightMapNode = lightMapNode;
}
setup( builder ) {
// irradianceLightMap property is used in the indirectDiffuse() method of BasicLightingModel
const RECIPROCAL_PI = float( 1 / Math.PI );
builder.context.irradianceLightMap = this.lightMapNode.mul( RECIPROCAL_PI );
}
}
export default BasicLightMapNode;
+38
View File
@@ -0,0 +1,38 @@
import AnalyticLightNode from './AnalyticLightNode.js';
import { lightTargetDirection } from '../accessors/Lights.js';
class DirectionalLightNode extends AnalyticLightNode {
static get type() {
return 'DirectionalLightNode';
}
constructor( light = null ) {
super( light );
}
setup( builder ) {
super.setup( builder );
const lightingModel = builder.context.lightingModel;
const lightColor = this.colorNode;
const lightDirection = lightTargetDirection( this.light );
const reflectedLight = builder.context.reflectedLight;
lightingModel.direct( {
lightDirection,
lightColor,
reflectedLight
}, builder.stack, builder );
}
}
export default DirectionalLightNode;
+138
View File
@@ -0,0 +1,138 @@
import LightingNode from './LightingNode.js';
import { cache } from '../core/CacheNode.js';
import { roughness, clearcoatRoughness } from '../core/PropertyNode.js';
import { cameraViewMatrix } from '../accessors/Camera.js';
import { transformedClearcoatNormalView, transformedNormalView, transformedNormalWorld } from '../accessors/Normal.js';
import { positionViewDirection } from '../accessors/Position.js';
import { float } from '../tsl/TSLBase.js';
import { reference } from '../accessors/ReferenceNode.js';
import { transformedBentNormalView } from '../accessors/AccessorsUtils.js';
import { pmremTexture } from '../pmrem/PMREMNode.js';
const _envNodeCache = new WeakMap();
class EnvironmentNode extends LightingNode {
static get type() {
return 'EnvironmentNode';
}
constructor( envNode = null ) {
super();
this.envNode = envNode;
}
setup( builder ) {
const { material } = builder;
let envNode = this.envNode;
if ( envNode.isTextureNode || envNode.isMaterialReferenceNode ) {
const value = ( envNode.isTextureNode ) ? envNode.value : material[ envNode.property ];
let cacheEnvNode = _envNodeCache.get( value );
if ( cacheEnvNode === undefined ) {
cacheEnvNode = pmremTexture( value );
_envNodeCache.set( value, cacheEnvNode );
}
envNode = cacheEnvNode;
}
//
const envMap = material.envMap;
const intensity = envMap ? reference( 'envMapIntensity', 'float', builder.material ) : reference( 'environmentIntensity', 'float', builder.scene ); // @TODO: Add materialEnvIntensity in MaterialNode
const useAnisotropy = material.useAnisotropy === true || material.anisotropy > 0;
const radianceNormalView = useAnisotropy ? transformedBentNormalView : transformedNormalView;
const radiance = envNode.context( createRadianceContext( roughness, radianceNormalView ) ).mul( intensity );
const irradiance = envNode.context( createIrradianceContext( transformedNormalWorld ) ).mul( Math.PI ).mul( intensity );
const isolateRadiance = cache( radiance );
const isolateIrradiance = cache( irradiance );
//
builder.context.radiance.addAssign( isolateRadiance );
builder.context.iblIrradiance.addAssign( isolateIrradiance );
//
const clearcoatRadiance = builder.context.lightingModel.clearcoatRadiance;
if ( clearcoatRadiance ) {
const clearcoatRadianceContext = envNode.context( createRadianceContext( clearcoatRoughness, transformedClearcoatNormalView ) ).mul( intensity );
const isolateClearcoatRadiance = cache( clearcoatRadianceContext );
clearcoatRadiance.addAssign( isolateClearcoatRadiance );
}
}
}
export default EnvironmentNode;
const createRadianceContext = ( roughnessNode, normalViewNode ) => {
let reflectVec = null;
return {
getUV: () => {
if ( reflectVec === null ) {
reflectVec = positionViewDirection.negate().reflect( normalViewNode );
// Mixing the reflection with the normal is more accurate and keeps rough objects from gathering light from behind their tangent plane.
reflectVec = roughnessNode.mul( roughnessNode ).mix( reflectVec, normalViewNode ).normalize();
reflectVec = reflectVec.transformDirection( cameraViewMatrix );
}
return reflectVec;
},
getTextureLevel: () => {
return roughnessNode;
}
};
};
const createIrradianceContext = ( normalWorldNode ) => {
return {
getUV: () => {
return normalWorldNode;
},
getTextureLevel: () => {
return float( 1.0 );
}
};
};
+56
View File
@@ -0,0 +1,56 @@
import AnalyticLightNode from './AnalyticLightNode.js';
import { uniform } from '../core/UniformNode.js';
import { mix } from '../math/MathNode.js';
import { normalView } from '../accessors/Normal.js';
import { lightPosition } from '../accessors/Lights.js';
import { renderGroup } from '../core/UniformGroupNode.js';
import { Color } from '../../math/Color.js';
class HemisphereLightNode extends AnalyticLightNode {
static get type() {
return 'HemisphereLightNode';
}
constructor( light = null ) {
super( light );
this.lightPositionNode = lightPosition( light );
this.lightDirectionNode = this.lightPositionNode.normalize();
this.groundColorNode = uniform( new Color() ).setGroup( renderGroup );
}
update( frame ) {
const { light } = this;
super.update( frame );
this.lightPositionNode.object3d = light;
this.groundColorNode.value.copy( light.groundColor ).multiplyScalar( light.intensity );
}
setup( builder ) {
const { colorNode, groundColorNode, lightDirectionNode } = this;
const dotNL = normalView.dot( lightDirectionNode );
const hemiDiffuseWeight = dotNL.mul( 0.5 ).add( 0.5 );
const irradiance = mix( groundColorNode, colorNode, hemiDiffuseWeight );
builder.context.irradiance.addAssign( irradiance );
}
}
export default HemisphereLightNode;
+37
View File
@@ -0,0 +1,37 @@
import SpotLightNode from './SpotLightNode.js';
import { texture } from '../accessors/TextureNode.js';
import { vec2 } from '../tsl/TSLBase.js';
class IESSpotLightNode extends SpotLightNode {
static get type() {
return 'IESSpotLightNode';
}
getSpotAttenuation( angleCosine ) {
const iesMap = this.light.iesMap;
let spotAttenuation = null;
if ( iesMap && iesMap.isTexture === true ) {
const angle = angleCosine.acos().mul( 1.0 / Math.PI );
spotAttenuation = texture( iesMap, vec2( angle, 0 ), 0 ).r;
} else {
spotAttenuation = super.getSpotAttenuation( angleCosine );
}
return spotAttenuation;
}
}
export default IESSpotLightNode;
+27
View File
@@ -0,0 +1,27 @@
import LightingNode from './LightingNode.js';
class IrradianceNode extends LightingNode {
static get type() {
return 'IrradianceNode';
}
constructor( node ) {
super();
this.node = node;
}
setup( builder ) {
builder.context.irradiance.addAssign( this.node );
}
}
export default IrradianceNode;
+53
View File
@@ -0,0 +1,53 @@
import AnalyticLightNode from './AnalyticLightNode.js';
import { normalWorld } from '../accessors/Normal.js';
import { uniformArray } from '../accessors/UniformArrayNode.js';
import { Vector3 } from '../../math/Vector3.js';
import getShIrradianceAt from '../functions/material/getShIrradianceAt.js';
class LightProbeNode extends AnalyticLightNode {
static get type() {
return 'LightProbeNode';
}
constructor( light = null ) {
super( light );
const array = [];
for ( let i = 0; i < 9; i ++ ) array.push( new Vector3() );
this.lightProbe = uniformArray( array );
}
update( frame ) {
const { light } = this;
super.update( frame );
//
for ( let i = 0; i < 9; i ++ ) {
this.lightProbe.array[ i ].copy( light.sh.coefficients[ i ] ).multiplyScalar( light.intensity );
}
}
setup( builder ) {
const irradiance = getShIrradianceAt( normalWorld, this.lightProbe );
builder.context.irradiance.addAssign( irradiance );
}
}
export default LightProbeNode;
+17
View File
@@ -0,0 +1,17 @@
import { Fn } from '../tsl/TSLBase.js';
export const getDistanceAttenuation = /*@__PURE__*/ Fn( ( inputs ) => {
const { lightDistance, cutoffDistance, decayExponent } = inputs;
// based upon Frostbite 3 Moving to Physically-based Rendering
// page 32, equation 26: E[window1]
// https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
const distanceFalloff = lightDistance.pow( decayExponent ).max( 0.01 ).reciprocal();
return cutoffDistance.greaterThan( 0 ).select(
distanceFalloff.mul( lightDistance.div( cutoffDistance ).pow4().oneMinus().clamp().pow2() ),
distanceFalloff
);
} ); // validated
+67
View File
@@ -0,0 +1,67 @@
import ContextNode from '../core/ContextNode.js';
import { nodeProxy, float, vec3 } from '../tsl/TSLBase.js';
class LightingContextNode extends ContextNode {
static get type() {
return 'LightingContextNode';
}
constructor( node, lightingModel = null, backdropNode = null, backdropAlphaNode = null ) {
super( node );
this.lightingModel = lightingModel;
this.backdropNode = backdropNode;
this.backdropAlphaNode = backdropAlphaNode;
this._value = null;
}
getContext() {
const { backdropNode, backdropAlphaNode } = this;
const directDiffuse = vec3().toVar( 'directDiffuse' ),
directSpecular = vec3().toVar( 'directSpecular' ),
indirectDiffuse = vec3().toVar( 'indirectDiffuse' ),
indirectSpecular = vec3().toVar( 'indirectSpecular' );
const reflectedLight = {
directDiffuse,
directSpecular,
indirectDiffuse,
indirectSpecular
};
const context = {
radiance: vec3().toVar( 'radiance' ),
irradiance: vec3().toVar( 'irradiance' ),
iblIrradiance: vec3().toVar( 'iblIrradiance' ),
ambientOcclusion: float( 1 ).toVar( 'ambientOcclusion' ),
reflectedLight,
backdrop: backdropNode,
backdropAlpha: backdropAlphaNode
};
return context;
}
setup( builder ) {
this.value = this._value || ( this._value = this.getContext() );
this.value.lightingModel = this.lightingModel || builder.context.lightingModel;
return super.setup( builder );
}
}
export default LightingContextNode;
export const lightingContext = /*@__PURE__*/ nodeProxy( LightingContextNode );
+27
View File
@@ -0,0 +1,27 @@
import Node from '../core/Node.js';
class LightingNode extends Node {
static get type() {
return 'LightingNode';
}
constructor() {
super( 'vec3' );
this.isLightingNode = true;
}
generate( /*builder*/ ) {
console.warn( 'Abstract function.' );
}
}
export default LightingNode;
+262
View File
@@ -0,0 +1,262 @@
import Node from '../core/Node.js';
import { nodeObject, vec3 } from '../tsl/TSLBase.js';
const sortLights = ( lights ) => {
return lights.sort( ( a, b ) => a.id - b.id );
};
const getLightNodeById = ( id, lightNodes ) => {
for ( const lightNode of lightNodes ) {
if ( lightNode.isAnalyticLightNode && lightNode.light.id === id ) {
return lightNode;
}
}
return null;
};
const _lightsNodeRef = /*@__PURE__*/ new WeakMap();
class LightsNode extends Node {
static get type() {
return 'LightsNode';
}
constructor() {
super( 'vec3' );
this.totalDiffuseNode = vec3().toVar( 'totalDiffuse' );
this.totalSpecularNode = vec3().toVar( 'totalSpecular' );
this.outgoingLightNode = vec3().toVar( 'outgoingLight' );
this._lights = [];
this._lightNodes = null;
this._lightNodesHash = null;
this.global = true;
}
getHash( builder ) {
if ( this._lightNodesHash === null ) {
if ( this._lightNodes === null ) this.setupLightsNode( builder );
const hash = [];
for ( const lightNode of this._lightNodes ) {
hash.push( lightNode.getSelf().getHash() );
}
this._lightNodesHash = 'lights-' + hash.join( ',' );
}
return this._lightNodesHash;
}
analyze( builder ) {
const properties = builder.getDataFromNode( this );
for ( const node of properties.nodes ) {
node.build( builder );
}
}
setupLightsNode( builder ) {
const lightNodes = [];
const previousLightNodes = this._lightNodes;
const lights = sortLights( this._lights );
const nodeLibrary = builder.renderer.library;
for ( const light of lights ) {
if ( light.isNode ) {
lightNodes.push( nodeObject( light ) );
} else {
let lightNode = null;
if ( previousLightNodes !== null ) {
lightNode = getLightNodeById( light.id, previousLightNodes ); // resuse existing light node
}
if ( lightNode === null ) {
const lightNodeClass = nodeLibrary.getLightNodeClass( light.constructor );
if ( lightNodeClass === null ) {
console.warn( `LightsNode.setupNodeLights: Light node not found for ${ light.constructor.name }` );
continue;
}
let lightNode = null;
if ( ! _lightsNodeRef.has( light ) ) {
lightNode = nodeObject( new lightNodeClass( light ) );
_lightsNodeRef.set( light, lightNode );
} else {
lightNode = _lightsNodeRef.get( light );
}
lightNodes.push( lightNode );
}
}
}
this._lightNodes = lightNodes;
}
setupLights( builder, lightNodes ) {
for ( const lightNode of lightNodes ) {
lightNode.build( builder );
}
}
setup( builder ) {
if ( this._lightNodes === null ) this.setupLightsNode( builder );
const context = builder.context;
const lightingModel = context.lightingModel;
let outgoingLightNode = this.outgoingLightNode;
if ( lightingModel ) {
const { _lightNodes, totalDiffuseNode, totalSpecularNode } = this;
context.outgoingLight = outgoingLightNode;
const stack = builder.addStack();
//
const properties = builder.getDataFromNode( this );
properties.nodes = stack.nodes;
//
lightingModel.start( context, stack, builder );
// lights
this.setupLights( builder, _lightNodes );
//
lightingModel.indirect( context, stack, builder );
//
const { backdrop, backdropAlpha } = context;
const { directDiffuse, directSpecular, indirectDiffuse, indirectSpecular } = context.reflectedLight;
let totalDiffuse = directDiffuse.add( indirectDiffuse );
if ( backdrop !== null ) {
if ( backdropAlpha !== null ) {
totalDiffuse = vec3( backdropAlpha.mix( totalDiffuse, backdrop ) );
} else {
totalDiffuse = vec3( backdrop );
}
context.material.transparent = true;
}
totalDiffuseNode.assign( totalDiffuse );
totalSpecularNode.assign( directSpecular.add( indirectSpecular ) );
outgoingLightNode.assign( totalDiffuseNode.add( totalSpecularNode ) );
//
lightingModel.finish( context, stack, builder );
//
outgoingLightNode = outgoingLightNode.bypass( builder.removeStack() );
}
return outgoingLightNode;
}
setLights( lights ) {
this._lights = lights;
this._lightNodes = null;
this._lightNodesHash = null;
return this;
}
getLights() {
return this._lights;
}
get hasLights() {
return this._lights.length > 0;
}
}
export default LightsNode;
export const lights = ( lights = [] ) => nodeObject( new LightsNode() ).setLights( lights );
+86
View File
@@ -0,0 +1,86 @@
import AnalyticLightNode from './AnalyticLightNode.js';
import { getDistanceAttenuation } from './LightUtils.js';
import { uniform } from '../core/UniformNode.js';
import { lightViewPosition } from '../accessors/Lights.js';
import { positionView } from '../accessors/Position.js';
import { Fn } from '../tsl/TSLBase.js';
import { renderGroup } from '../core/UniformGroupNode.js';
import { pointShadow } from './PointShadowNode.js';
export const directPointLight = Fn( ( { color, lightViewPosition, cutoffDistance, decayExponent }, builder ) => {
const lightingModel = builder.context.lightingModel;
const lVector = lightViewPosition.sub( positionView ); // @TODO: Add it into LightNode
const lightDirection = lVector.normalize();
const lightDistance = lVector.length();
const lightAttenuation = getDistanceAttenuation( {
lightDistance,
cutoffDistance,
decayExponent
} );
const lightColor = color.mul( lightAttenuation );
const reflectedLight = builder.context.reflectedLight;
lightingModel.direct( {
lightDirection,
lightColor,
reflectedLight
}, builder.stack, builder );
} );
class PointLightNode extends AnalyticLightNode {
static get type() {
return 'PointLightNode';
}
constructor( light = null ) {
super( light );
this.cutoffDistanceNode = uniform( 0 ).setGroup( renderGroup );
this.decayExponentNode = uniform( 0 ).setGroup( renderGroup );
}
update( frame ) {
const { light } = this;
super.update( frame );
this.cutoffDistanceNode.value = light.distance;
this.decayExponentNode.value = light.decay;
}
setupShadowNode() {
return pointShadow( this.light );
}
setup( builder ) {
super.setup( builder );
directPointLight( {
color: this.colorNode,
lightViewPosition: lightViewPosition( this.light ),
cutoffDistance: this.cutoffDistanceNode,
decayExponent: this.decayExponentNode
} ).append();
}
}
export default PointLightNode;
+254
View File
@@ -0,0 +1,254 @@
import ShadowNode from './ShadowNode.js';
import { uniform } from '../core/UniformNode.js';
import { float, vec2, If, Fn, nodeObject } from '../tsl/TSLBase.js';
import { reference } from '../accessors/ReferenceNode.js';
import { texture } from '../accessors/TextureNode.js';
import { max, abs, sign } from '../math/MathNode.js';
import { sub, div } from '../math/OperatorNode.js';
import { renderGroup } from '../core/UniformGroupNode.js';
import { Vector2 } from '../../math/Vector2.js';
import { Vector4 } from '../../math/Vector4.js';
import { Color } from '../../math/Color.js';
import { BasicShadowMap } from '../../constants.js';
const _clearColor = /*@__PURE__*/ new Color();
// cubeToUV() maps a 3D direction vector suitable for cube texture mapping to a 2D
// vector suitable for 2D texture mapping. This code uses the following layout for the
// 2D texture:
//
// xzXZ
// y Y
//
// Y - Positive y direction
// y - Negative y direction
// X - Positive x direction
// x - Negative x direction
// Z - Positive z direction
// z - Negative z direction
//
// Source and test bed:
// https://gist.github.com/tschw/da10c43c467ce8afd0c4
export const cubeToUV = /*@__PURE__*/ Fn( ( [ pos, texelSizeY ] ) => {
const v = pos.toVar();
// Number of texels to avoid at the edge of each square
const absV = abs( v );
// Intersect unit cube
const scaleToCube = div( 1.0, max( absV.x, max( absV.y, absV.z ) ) );
absV.mulAssign( scaleToCube );
// Apply scale to avoid seams
// two texels less per square (one texel will do for NEAREST)
v.mulAssign( scaleToCube.mul( texelSizeY.mul( 2 ).oneMinus() ) );
// Unwrap
// space: -1 ... 1 range for each square
//
// #X## dim := ( 4 , 2 )
// # # center := ( 1 , 1 )
const planar = vec2( v.xy ).toVar();
const almostATexel = texelSizeY.mul( 1.5 );
const almostOne = almostATexel.oneMinus();
If( absV.z.greaterThanEqual( almostOne ), () => {
If( v.z.greaterThan( 0.0 ), () => {
planar.x.assign( sub( 4.0, v.x ) );
} );
} ).ElseIf( absV.x.greaterThanEqual( almostOne ), () => {
const signX = sign( v.x );
planar.x.assign( v.z.mul( signX ).add( signX.mul( 2.0 ) ) );
} ).ElseIf( absV.y.greaterThanEqual( almostOne ), () => {
const signY = sign( v.y );
planar.x.assign( v.x.add( signY.mul( 2.0 ) ).add( 2.0 ) );
planar.y.assign( v.z.mul( signY ).sub( 2.0 ) );
} );
// Transform to UV space
// scale := 0.5 / dim
// translate := ( center + 0.5 ) / dim
return vec2( 0.125, 0.25 ).mul( planar ).add( vec2( 0.375, 0.75 ) ).flipY();
} ).setLayout( {
name: 'cubeToUV',
type: 'vec2',
inputs: [
{ name: 'pos', type: 'vec3' },
{ name: 'texelSizeY', type: 'float' }
]
} );
export const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, texelSize } ) => {
return texture( depthTexture, cubeToUV( bd3D, texelSize.y ) ).compare( dp );
} );
export const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, texelSize, shadow } ) => {
const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
const offset = vec2( - 1.0, 1.0 ).mul( radius ).mul( texelSize.y );
return texture( depthTexture, cubeToUV( bd3D.add( offset.xyy ), texelSize.y ) ).compare( dp )
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yyy ), texelSize.y ) ).compare( dp ) )
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xyx ), texelSize.y ) ).compare( dp ) )
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yyx ), texelSize.y ) ).compare( dp ) )
.add( texture( depthTexture, cubeToUV( bd3D, texelSize.y ) ).compare( dp ) )
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xxy ), texelSize.y ) ).compare( dp ) )
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yxy ), texelSize.y ) ).compare( dp ) )
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xxx ), texelSize.y ) ).compare( dp ) )
.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yxx ), texelSize.y ) ).compare( dp ) )
.mul( 1.0 / 9.0 );
} );
const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCoord, shadow } ) => {
// for point lights, the uniform @vShadowCoord is re-purposed to hold
// the vector from the light to the world-space position of the fragment.
const lightToPosition = shadowCoord.xyz.toVar();
const lightToPositionLength = lightToPosition.length();
const cameraNearLocal = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near );
const cameraFarLocal = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far );
const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
const mapSize = uniform( shadow.mapSize ).setGroup( renderGroup );
const result = float( 1.0 ).toVar();
If( lightToPositionLength.sub( cameraFarLocal ).lessThanEqual( 0.0 ).and( lightToPositionLength.sub( cameraNearLocal ).greaterThanEqual( 0.0 ) ), () => {
// dp = normalized distance from light to fragment position
const dp = lightToPositionLength.sub( cameraNearLocal ).div( cameraFarLocal.sub( cameraNearLocal ) ).toVar(); // need to clamp?
dp.addAssign( bias );
// bd3D = base direction 3D
const bd3D = lightToPosition.normalize();
const texelSize = vec2( 1.0 ).div( mapSize.mul( vec2( 4.0, 2.0 ) ) );
// percentage-closer filtering
result.assign( filterFn( { depthTexture, bd3D, dp, texelSize, shadow } ) );
} );
return result;
} );
const _viewport = /*@__PURE__*/ new Vector4();
const _viewportSize = /*@__PURE__*/ new Vector2();
const _shadowMapSize = /*@__PURE__*/ new Vector2();
//
class PointShadowNode extends ShadowNode {
static get type() {
return 'PointShadowNode';
}
constructor( light, shadow = null ) {
super( light, shadow );
}
getShadowFilterFn( type ) {
return type === BasicShadowMap ? BasicPointShadowFilter : PointShadowFilter;
}
setupShadowCoord( builder, shadowPosition ) {
return shadowPosition;
}
setupShadowFilter( builder, { filterFn, shadowTexture, depthTexture, shadowCoord, shadow } ) {
return pointShadowFilter( { filterFn, shadowTexture, depthTexture, shadowCoord, shadow } );
}
renderShadow( frame ) {
const { shadow, shadowMap, light } = this;
const { renderer, scene } = frame;
const shadowFrameExtents = shadow.getFrameExtents();
_shadowMapSize.copy( shadow.mapSize );
_shadowMapSize.multiply( shadowFrameExtents );
shadowMap.setSize( _shadowMapSize.width, _shadowMapSize.height );
_viewportSize.copy( shadow.mapSize );
//
const previousAutoClear = renderer.autoClear;
const previousClearColor = renderer.getClearColor( _clearColor );
const previousClearAlpha = renderer.getClearAlpha();
renderer.autoClear = false;
renderer.setClearColor( shadow.clearColor, shadow.clearAlpha );
renderer.clear();
const viewportCount = shadow.getViewportCount();
for ( let vp = 0; vp < viewportCount; vp ++ ) {
const viewport = shadow.getViewport( vp );
const x = _viewportSize.x * viewport.x;
const y = _shadowMapSize.y - _viewportSize.y - ( _viewportSize.y * viewport.y );
_viewport.set(
x,
y,
_viewportSize.x * viewport.z,
_viewportSize.y * viewport.w
);
shadowMap.viewport.copy( _viewport );
shadow.updateMatrices( light, vp );
renderer.render( scene, shadow.camera );
}
//
renderer.autoClear = previousAutoClear;
renderer.setClearColor( previousClearColor, previousClearAlpha );
}
}
export default PointShadowNode;
export const pointShadow = ( light, shadow ) => nodeObject( new PointShadowNode( light, shadow ) );
+100
View File
@@ -0,0 +1,100 @@
import AnalyticLightNode from './AnalyticLightNode.js';
import { texture } from '../accessors/TextureNode.js';
import { uniform } from '../core/UniformNode.js';
import { lightViewPosition } from '../accessors/Lights.js';
import { renderGroup } from '../core/UniformGroupNode.js';
import { Matrix4 } from '../../math/Matrix4.js';
import { Vector3 } from '../../math/Vector3.js';
import { NodeUpdateType } from '../core/constants.js';
const _matrix41 = /*@__PURE__*/ new Matrix4();
const _matrix42 = /*@__PURE__*/ new Matrix4();
let ltcLib = null;
class RectAreaLightNode extends AnalyticLightNode {
static get type() {
return 'RectAreaLightNode';
}
constructor( light = null ) {
super( light );
this.halfHeight = uniform( new Vector3() ).setGroup( renderGroup );
this.halfWidth = uniform( new Vector3() ).setGroup( renderGroup );
this.updateType = NodeUpdateType.RENDER;
}
update( frame ) {
super.update( frame );
const { light } = this;
const viewMatrix = frame.camera.matrixWorldInverse;
_matrix42.identity();
_matrix41.copy( light.matrixWorld );
_matrix41.premultiply( viewMatrix );
_matrix42.extractRotation( _matrix41 );
this.halfWidth.value.set( light.width * 0.5, 0.0, 0.0 );
this.halfHeight.value.set( 0.0, light.height * 0.5, 0.0 );
this.halfWidth.value.applyMatrix4( _matrix42 );
this.halfHeight.value.applyMatrix4( _matrix42 );
}
setup( builder ) {
super.setup( builder );
let ltc_1, ltc_2;
if ( builder.isAvailable( 'float32Filterable' ) ) {
ltc_1 = texture( ltcLib.LTC_FLOAT_1 );
ltc_2 = texture( ltcLib.LTC_FLOAT_2 );
} else {
ltc_1 = texture( ltcLib.LTC_HALF_1 );
ltc_2 = texture( ltcLib.LTC_HALF_2 );
}
const { colorNode, light } = this;
const lightingModel = builder.context.lightingModel;
const lightPosition = lightViewPosition( light );
const reflectedLight = builder.context.reflectedLight;
lightingModel.directRectArea( {
lightColor: colorNode,
lightPosition,
halfWidth: this.halfWidth,
halfHeight: this.halfHeight,
reflectedLight,
ltc_1,
ltc_2
}, builder.stack, builder );
}
static setLTC( ltc ) {
ltcLib = ltc;
}
}
export default RectAreaLightNode;
+593
View File
@@ -0,0 +1,593 @@
import Node from '../core/Node.js';
import { NodeUpdateType } from '../core/constants.js';
import { float, vec2, vec3, vec4, If, int, Fn, nodeObject } from '../tsl/TSLBase.js';
import { reference } from '../accessors/ReferenceNode.js';
import { texture } from '../accessors/TextureNode.js';
import { positionWorld } from '../accessors/Position.js';
import { transformedNormalWorld } from '../accessors/Normal.js';
import { mix, fract, step, max, clamp, sqrt } from '../math/MathNode.js';
import { add, sub } from '../math/OperatorNode.js';
import { DepthTexture } from '../../textures/DepthTexture.js';
import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
import QuadMesh from '../../renderers/common/QuadMesh.js';
import { Loop } from '../utils/LoopNode.js';
import { screenCoordinate } from '../display/ScreenNode.js';
import { HalfFloatType, LessCompare, NoBlending, RGFormat, VSMShadowMap, WebGPUCoordinateSystem } from '../../constants.js';
import { renderGroup } from '../core/UniformGroupNode.js';
import { viewZToLogarithmicDepth } from '../display/ViewportDepthNode.js';
import { objectPosition } from '../accessors/Object3DNode.js';
import { lightShadowMatrix } from '../accessors/Lights.js';
const shadowMaterialLib = /*@__PURE__*/ new WeakMap();
const shadowWorldPosition = /*@__PURE__*/ vec3().toVar( 'shadowWorldPosition' );
const linearDistance = /*@__PURE__*/ Fn( ( [ position, cameraNear, cameraFar ] ) => {
let dist = positionWorld.sub( position ).length();
dist = dist.sub( cameraNear ).div( cameraFar.sub( cameraNear ) );
dist = dist.saturate(); // clamp to [ 0, 1 ]
return dist;
} );
const linearShadowDistance = ( light ) => {
const camera = light.shadow.camera;
const nearDistance = reference( 'near', 'float', camera ).setGroup( renderGroup );
const farDistance = reference( 'far', 'float', camera ).setGroup( renderGroup );
const referencePosition = objectPosition( light );
return linearDistance( referencePosition, nearDistance, farDistance );
};
const getShadowMaterial = ( light ) => {
let material = shadowMaterialLib.get( light );
if ( material === undefined ) {
const depthNode = light.isPointLight ? linearShadowDistance( light ) : null;
material = new NodeMaterial();
material.colorNode = vec4( 0, 0, 0, 1 );
material.depthNode = depthNode;
material.isShadowNodeMaterial = true; // Use to avoid other overrideMaterial override material.colorNode unintentionally when using material.shadowNode
material.blending = NoBlending;
material.name = 'ShadowMaterial';
shadowMaterialLib.set( light, material );
}
return material;
};
export const BasicShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord } ) => {
return texture( depthTexture, shadowCoord.xy ).compare( shadowCoord.z );
} );
export const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow } ) => {
const depthCompare = ( uv, compare ) => texture( depthTexture, uv ).compare( compare );
const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
const texelSize = vec2( 1 ).div( mapSize );
const dx0 = texelSize.x.negate().mul( radius );
const dy0 = texelSize.y.negate().mul( radius );
const dx1 = texelSize.x.mul( radius );
const dy1 = texelSize.y.mul( radius );
const dx2 = dx0.div( 2 );
const dy2 = dy0.div( 2 );
const dx3 = dx1.div( 2 );
const dy3 = dy1.div( 2 );
return add(
depthCompare( shadowCoord.xy.add( vec2( dx0, dy0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( 0, dy0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx1, dy0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx2, dy2 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( 0, dy2 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx3, dy2 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx0, 0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx2, 0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy, shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx3, 0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx1, 0 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx2, dy3 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( 0, dy3 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx3, dy3 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx0, dy1 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( 0, dy1 ) ), shadowCoord.z ),
depthCompare( shadowCoord.xy.add( vec2( dx1, dy1 ) ), shadowCoord.z )
).mul( 1 / 17 );
} );
export const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow } ) => {
const depthCompare = ( uv, compare ) => texture( depthTexture, uv ).compare( compare );
const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
const texelSize = vec2( 1 ).div( mapSize );
const dx = texelSize.x;
const dy = texelSize.y;
const uv = shadowCoord.xy;
const f = fract( uv.mul( mapSize ).add( 0.5 ) );
uv.subAssign( f.mul( texelSize ) );
return add(
depthCompare( uv, shadowCoord.z ),
depthCompare( uv.add( vec2( dx, 0 ) ), shadowCoord.z ),
depthCompare( uv.add( vec2( 0, dy ) ), shadowCoord.z ),
depthCompare( uv.add( texelSize ), shadowCoord.z ),
mix(
depthCompare( uv.add( vec2( dx.negate(), 0 ) ), shadowCoord.z ),
depthCompare( uv.add( vec2( dx.mul( 2 ), 0 ) ), shadowCoord.z ),
f.x
),
mix(
depthCompare( uv.add( vec2( dx.negate(), dy ) ), shadowCoord.z ),
depthCompare( uv.add( vec2( dx.mul( 2 ), dy ) ), shadowCoord.z ),
f.x
),
mix(
depthCompare( uv.add( vec2( 0, dy.negate() ) ), shadowCoord.z ),
depthCompare( uv.add( vec2( 0, dy.mul( 2 ) ) ), shadowCoord.z ),
f.y
),
mix(
depthCompare( uv.add( vec2( dx, dy.negate() ) ), shadowCoord.z ),
depthCompare( uv.add( vec2( dx, dy.mul( 2 ) ) ), shadowCoord.z ),
f.y
),
mix(
mix(
depthCompare( uv.add( vec2( dx.negate(), dy.negate() ) ), shadowCoord.z ),
depthCompare( uv.add( vec2( dx.mul( 2 ), dy.negate() ) ), shadowCoord.z ),
f.x
),
mix(
depthCompare( uv.add( vec2( dx.negate(), dy.mul( 2 ) ) ), shadowCoord.z ),
depthCompare( uv.add( vec2( dx.mul( 2 ), dy.mul( 2 ) ) ), shadowCoord.z ),
f.x
),
f.y
)
).mul( 1 / 9 );
} );
// VSM
export const VSMShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord } ) => {
const occlusion = float( 1 ).toVar();
const distribution = texture( depthTexture ).uv( shadowCoord.xy ).rg;
const hardShadow = step( shadowCoord.z, distribution.x );
If( hardShadow.notEqual( float( 1.0 ) ), () => {
const distance = shadowCoord.z.sub( distribution.x );
const variance = max( 0, distribution.y.mul( distribution.y ) );
let softnessProbability = variance.div( variance.add( distance.mul( distance ) ) ); // Chebeyshevs inequality
softnessProbability = clamp( sub( softnessProbability, 0.3 ).div( 0.95 - 0.3 ) );
occlusion.assign( clamp( max( hardShadow, softnessProbability ) ) );
} );
return occlusion;
} );
const VSMPassVertical = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass } ) => {
const mean = float( 0 ).toVar();
const squaredMean = float( 0 ).toVar();
const uvStride = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( 2 ).div( samples.sub( 1 ) ) );
const uvStart = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( - 1 ) );
Loop( { start: int( 0 ), end: int( samples ), type: 'int', condition: '<' }, ( { i } ) => {
const uvOffset = uvStart.add( float( i ).mul( uvStride ) );
const depth = shadowPass.uv( add( screenCoordinate.xy, vec2( 0, uvOffset ).mul( radius ) ).div( size ) ).x;
mean.addAssign( depth );
squaredMean.addAssign( depth.mul( depth ) );
} );
mean.divAssign( samples );
squaredMean.divAssign( samples );
const std_dev = sqrt( squaredMean.sub( mean.mul( mean ) ) );
return vec2( mean, std_dev );
} );
const VSMPassHorizontal = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPass } ) => {
const mean = float( 0 ).toVar();
const squaredMean = float( 0 ).toVar();
const uvStride = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( 2 ).div( samples.sub( 1 ) ) );
const uvStart = samples.lessThanEqual( float( 1 ) ).select( float( 0 ), float( - 1 ) );
Loop( { start: int( 0 ), end: int( samples ), type: 'int', condition: '<' }, ( { i } ) => {
const uvOffset = uvStart.add( float( i ).mul( uvStride ) );
const distribution = shadowPass.uv( add( screenCoordinate.xy, vec2( uvOffset, 0 ).mul( radius ) ).div( size ) );
mean.addAssign( distribution.x );
squaredMean.addAssign( add( distribution.y.mul( distribution.y ), distribution.x.mul( distribution.x ) ) );
} );
mean.divAssign( samples );
squaredMean.divAssign( samples );
const std_dev = sqrt( squaredMean.sub( mean.mul( mean ) ) );
return vec2( mean, std_dev );
} );
const _shadowFilterLib = [ BasicShadowFilter, PCFShadowFilter, PCFSoftShadowFilter, VSMShadowFilter ];
//
const _quadMesh = /*@__PURE__*/ new QuadMesh();
class ShadowNode extends Node {
static get type() {
return 'ShadowNode';
}
constructor( light, shadow = null ) {
super();
this.light = light;
this.shadow = shadow || light.shadow;
this.shadowMap = null;
this.vsmShadowMapVertical = null;
this.vsmShadowMapHorizontal = null;
this.vsmMaterialVertical = null;
this.vsmMaterialHorizontal = null;
this.updateBeforeType = NodeUpdateType.RENDER;
this._node = null;
this.isShadowNode = true;
}
setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow } ) {
const frustumTest = shadowCoord.x.greaterThanEqual( 0 )
.and( shadowCoord.x.lessThanEqual( 1 ) )
.and( shadowCoord.y.greaterThanEqual( 0 ) )
.and( shadowCoord.y.lessThanEqual( 1 ) )
.and( shadowCoord.z.lessThanEqual( 1 ) );
const shadowNode = filterFn( { depthTexture, shadowCoord, shadow } );
return frustumTest.select( shadowNode, float( 1 ) );
}
setupShadowCoord( builder, shadowPosition ) {
const { shadow } = this;
const { renderer } = builder;
const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
let shadowCoord = shadowPosition;
let coordZ;
if ( shadow.camera.isOrthographicCamera || renderer.logarithmicDepthBuffer !== true ) {
shadowCoord = shadowCoord.xyz.div( shadowCoord.w );
coordZ = shadowCoord.z;
if ( renderer.coordinateSystem === WebGPUCoordinateSystem ) {
coordZ = coordZ.mul( 2 ).sub( 1 ); // WebGPU: Conversion [ 0, 1 ] to [ - 1, 1 ]
}
} else {
const w = shadowCoord.w;
shadowCoord = shadowCoord.xy.div( w ); // <-- Only divide X/Y coords since we don't need Z
// The normally available "cameraNear" and "cameraFar" nodes cannot be used here because they do not get
// updated to use the shadow camera. So, we have to declare our own "local" ones here.
// TODO: How do we get the cameraNear/cameraFar nodes to use the shadow camera so we don't have to declare local ones here?
const cameraNearLocal = reference( 'near', 'float', shadow.camera ).setGroup( renderGroup );
const cameraFarLocal = reference( 'far', 'float', shadow.camera ).setGroup( renderGroup );
coordZ = viewZToLogarithmicDepth( w.negate(), cameraNearLocal, cameraFarLocal );
}
shadowCoord = vec3(
shadowCoord.x,
shadowCoord.y.oneMinus(), // follow webgpu standards
coordZ.add( bias )
);
return shadowCoord;
}
getShadowFilterFn( type ) {
return _shadowFilterLib[ type ];
}
setupShadow( builder ) {
const { renderer } = builder;
const { light, shadow } = this;
const shadowMapType = renderer.shadowMap.type;
const depthTexture = new DepthTexture( shadow.mapSize.width, shadow.mapSize.height );
depthTexture.compareFunction = LessCompare;
const shadowMap = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height );
shadowMap.depthTexture = depthTexture;
shadow.camera.updateProjectionMatrix();
// VSM
if ( shadowMapType === VSMShadowMap ) {
depthTexture.compareFunction = null; // VSM does not use textureSampleCompare()/texture2DCompare()
this.vsmShadowMapVertical = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height, { format: RGFormat, type: HalfFloatType } );
this.vsmShadowMapHorizontal = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height, { format: RGFormat, type: HalfFloatType } );
const shadowPassVertical = texture( depthTexture );
const shadowPassHorizontal = texture( this.vsmShadowMapVertical.texture );
const samples = reference( 'blurSamples', 'float', shadow ).setGroup( renderGroup );
const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
const size = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
let material = this.vsmMaterialVertical || ( this.vsmMaterialVertical = new NodeMaterial() );
material.fragmentNode = VSMPassVertical( { samples, radius, size, shadowPass: shadowPassVertical } ).context( builder.getSharedContext() );
material.name = 'VSMVertical';
material = this.vsmMaterialHorizontal || ( this.vsmMaterialHorizontal = new NodeMaterial() );
material.fragmentNode = VSMPassHorizontal( { samples, radius, size, shadowPass: shadowPassHorizontal } ).context( builder.getSharedContext() );
material.name = 'VSMHorizontal';
}
//
const shadowIntensity = reference( 'intensity', 'float', shadow ).setGroup( renderGroup );
const normalBias = reference( 'normalBias', 'float', shadow ).setGroup( renderGroup );
const shadowPosition = lightShadowMatrix( light ).mul( shadowWorldPosition.add( transformedNormalWorld.mul( normalBias ) ) );
const shadowCoord = this.setupShadowCoord( builder, shadowPosition );
//
const filterFn = shadow.filterNode || this.getShadowFilterFn( renderer.shadowMap.type ) || null;
if ( filterFn === null ) {
throw new Error( 'THREE.WebGPURenderer: Shadow map type not supported yet.' );
}
const shadowDepthTexture = ( shadowMapType === VSMShadowMap ) ? this.vsmShadowMapHorizontal.texture : depthTexture;
const shadowNode = this.setupShadowFilter( builder, { filterFn, shadowTexture: shadowMap.texture, depthTexture: shadowDepthTexture, shadowCoord, shadow } );
const shadowColor = texture( shadowMap.texture, shadowCoord );
const shadowOutput = mix( 1, shadowNode.rgb.mix( shadowColor, 1 ), shadowIntensity.mul( shadowColor.a ) ).toVar();
this.shadowMap = shadowMap;
this.shadow.map = shadowMap;
return shadowOutput;
}
setup( builder ) {
if ( builder.renderer.shadowMap.enabled === false ) return;
return Fn( ( { material } ) => {
shadowWorldPosition.assign( material.shadowPositionNode || positionWorld );
let node = this._node;
if ( node === null ) {
this._node = node = this.setupShadow( builder );
}
if ( builder.material.shadowNode ) { // @deprecated, r171
console.warn( 'THREE.NodeMaterial: ".shadowNode" is deprecated. Use ".castShadowNode" instead.' );
}
if ( builder.material.receivedShadowNode ) {
node = builder.material.receivedShadowNode( node );
}
return node;
} )();
}
renderShadow( frame ) {
const { shadow, shadowMap } = this;
const { renderer, scene } = frame;
shadowMap.setSize( shadow.mapSize.width, shadow.mapSize.height );
renderer.render( scene, shadow.camera );
}
updateShadow( frame ) {
const { shadowMap, light, shadow } = this;
const { renderer, scene, camera } = frame;
const shadowType = renderer.shadowMap.type;
const depthVersion = shadowMap.depthTexture.version;
this._depthVersionCached = depthVersion;
const currentOverrideMaterial = scene.overrideMaterial;
scene.overrideMaterial = getShadowMaterial( light );
shadow.camera.layers.mask = camera.layers.mask;
const currentRenderTarget = renderer.getRenderTarget();
const currentRenderObjectFunction = renderer.getRenderObjectFunction();
const currentMRT = renderer.getMRT();
renderer.setMRT( null );
renderer.setRenderObjectFunction( ( object, ...params ) => {
if ( object.castShadow === true || ( object.receiveShadow && shadowType === VSMShadowMap ) ) {
renderer.renderObject( object, ...params );
}
} );
renderer.setRenderTarget( shadowMap );
this.renderShadow( frame );
renderer.setRenderObjectFunction( currentRenderObjectFunction );
// vsm blur pass
if ( light.isPointLight !== true && shadowType === VSMShadowMap ) {
this.vsmPass( renderer );
}
renderer.setRenderTarget( currentRenderTarget );
renderer.setMRT( currentMRT );
scene.overrideMaterial = currentOverrideMaterial;
}
vsmPass( renderer ) {
const { shadow } = this;
this.vsmShadowMapVertical.setSize( shadow.mapSize.width, shadow.mapSize.height );
this.vsmShadowMapHorizontal.setSize( shadow.mapSize.width, shadow.mapSize.height );
renderer.setRenderTarget( this.vsmShadowMapVertical );
_quadMesh.material = this.vsmMaterialVertical;
_quadMesh.render( renderer );
renderer.setRenderTarget( this.vsmShadowMapHorizontal );
_quadMesh.material = this.vsmMaterialHorizontal;
_quadMesh.render( renderer );
}
dispose() {
this.shadowMap.dispose();
this.shadowMap = null;
if ( this.vsmShadowMapVertical !== null ) {
this.vsmShadowMapVertical.dispose();
this.vsmShadowMapVertical = null;
this.vsmMaterialVertical.dispose();
this.vsmMaterialVertical = null;
}
if ( this.vsmShadowMapHorizontal !== null ) {
this.vsmShadowMapHorizontal.dispose();
this.vsmShadowMapHorizontal = null;
this.vsmMaterialHorizontal.dispose();
this.vsmMaterialHorizontal = null;
}
this.updateBeforeType = NodeUpdateType.NONE;
}
updateBefore( frame ) {
const { shadow } = this;
const needsUpdate = shadow.needsUpdate || shadow.autoUpdate;
if ( needsUpdate ) {
this.updateShadow( frame );
if ( this.shadowMap.depthTexture.version === this._depthVersionCached ) {
shadow.needsUpdate = false;
}
}
}
}
export default ShadowNode;
export const shadow = ( light, shadow ) => nodeObject( new ShadowNode( light, shadow ) );
+99
View File
@@ -0,0 +1,99 @@
import AnalyticLightNode from './AnalyticLightNode.js';
import { getDistanceAttenuation } from './LightUtils.js';
import { uniform } from '../core/UniformNode.js';
import { smoothstep } from '../math/MathNode.js';
import { positionView } from '../accessors/Position.js';
import { renderGroup } from '../core/UniformGroupNode.js';
import { lightViewPosition, lightTargetDirection, lightProjectionUV } from '../accessors/Lights.js';
import { texture } from '../accessors/TextureNode.js';
class SpotLightNode extends AnalyticLightNode {
static get type() {
return 'SpotLightNode';
}
constructor( light = null ) {
super( light );
this.coneCosNode = uniform( 0 ).setGroup( renderGroup );
this.penumbraCosNode = uniform( 0 ).setGroup( renderGroup );
this.cutoffDistanceNode = uniform( 0 ).setGroup( renderGroup );
this.decayExponentNode = uniform( 0 ).setGroup( renderGroup );
}
update( frame ) {
super.update( frame );
const { light } = this;
this.coneCosNode.value = Math.cos( light.angle );
this.penumbraCosNode.value = Math.cos( light.angle * ( 1 - light.penumbra ) );
this.cutoffDistanceNode.value = light.distance;
this.decayExponentNode.value = light.decay;
}
getSpotAttenuation( angleCosine ) {
const { coneCosNode, penumbraCosNode } = this;
return smoothstep( coneCosNode, penumbraCosNode, angleCosine );
}
setup( builder ) {
super.setup( builder );
const lightingModel = builder.context.lightingModel;
const { colorNode, cutoffDistanceNode, decayExponentNode, light } = this;
const lVector = lightViewPosition( light ).sub( positionView ); // @TODO: Add it into LightNode
const lightDirection = lVector.normalize();
const angleCos = lightDirection.dot( lightTargetDirection( light ) );
const spotAttenuation = this.getSpotAttenuation( angleCos );
const lightDistance = lVector.length();
const lightAttenuation = getDistanceAttenuation( {
lightDistance,
cutoffDistance: cutoffDistanceNode,
decayExponent: decayExponentNode
} );
let lightColor = colorNode.mul( spotAttenuation ).mul( lightAttenuation );
if ( light.map ) {
const spotLightCoord = lightProjectionUV( light );
const projectedTexture = texture( light.map, spotLightCoord.xy ).onRenderUpdate( () => light.map );
const inSpotLightMap = spotLightCoord.mul( 2. ).sub( 1. ).abs().lessThan( 1. ).all();
lightColor = inSpotLightMap.select( lightColor.mul( projectedTexture ), lightColor );
}
const reflectedLight = builder.context.reflectedLight;
lightingModel.direct( {
lightDirection,
lightColor,
reflectedLight
}, builder.stack, builder );
}
}
export default SpotLightNode;