|
|
@@ -0,0 +1,161 @@
|
|
|
+import { Color, Node, Vector3, Vector4 } from 'three/webgpu';
|
|
|
+import { Loop, NodeUpdateType, getDistanceAttenuation, positionView, renderGroup, smoothstep, uniform, uniformArray, vec3 } from 'three/tsl';
|
|
|
+
|
|
|
+const _lightPosition = /*@__PURE__*/ new Vector3();
|
|
|
+const _targetPosition = /*@__PURE__*/ new Vector3();
|
|
|
+
|
|
|
+const warn = ( message ) => {
|
|
|
+
|
|
|
+ console.warn( `THREE.SpotLightDataNode: ${ message }` );
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * Batched data node for simple spot lights in dynamic lighting mode.
|
|
|
+ *
|
|
|
+ * Projected spot lights keep the default per-light path.
|
|
|
+ *
|
|
|
+ * @augments Node
|
|
|
+ */
|
|
|
+class SpotLightDataNode extends Node {
|
|
|
+
|
|
|
+ static get type() {
|
|
|
+
|
|
|
+ return 'SpotLightDataNode';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ constructor( maxCount = 16 ) {
|
|
|
+
|
|
|
+ super();
|
|
|
+
|
|
|
+ this.maxCount = maxCount;
|
|
|
+ this._lights = [];
|
|
|
+ this._colors = [];
|
|
|
+ this._positionsAndCutoff = [];
|
|
|
+ this._directionsAndDecay = [];
|
|
|
+ this._cones = [];
|
|
|
+
|
|
|
+ for ( let i = 0; i < maxCount; i ++ ) {
|
|
|
+
|
|
|
+ this._colors.push( new Color() );
|
|
|
+ this._positionsAndCutoff.push( new Vector4() );
|
|
|
+ this._directionsAndDecay.push( new Vector4() );
|
|
|
+ this._cones.push( new Vector4() );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ this.colorsNode = uniformArray( this._colors, 'color' ).setGroup( renderGroup );
|
|
|
+ this.positionsAndCutoffNode = uniformArray( this._positionsAndCutoff, 'vec4' ).setGroup( renderGroup );
|
|
|
+ this.directionsAndDecayNode = uniformArray( this._directionsAndDecay, 'vec4' ).setGroup( renderGroup );
|
|
|
+ this.conesNode = uniformArray( this._cones, 'vec4' ).setGroup( renderGroup );
|
|
|
+ this.countNode = uniform( 0, 'int' ).setGroup( renderGroup );
|
|
|
+ this.updateType = NodeUpdateType.RENDER;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ setLights( lights ) {
|
|
|
+
|
|
|
+ if ( lights.length > this.maxCount ) {
|
|
|
+
|
|
|
+ warn( `${ lights.length } lights exceed the configured max of ${ this.maxCount }. Excess lights are ignored.` );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ this._lights = lights;
|
|
|
+
|
|
|
+ return this;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ update( { camera } ) {
|
|
|
+
|
|
|
+ const count = Math.min( this._lights.length, this.maxCount );
|
|
|
+
|
|
|
+ this.countNode.value = count;
|
|
|
+
|
|
|
+ for ( let i = 0; i < count; i ++ ) {
|
|
|
+
|
|
|
+ const light = this._lights[ i ];
|
|
|
+
|
|
|
+ this._colors[ i ].copy( light.color ).multiplyScalar( light.intensity );
|
|
|
+
|
|
|
+ _lightPosition.setFromMatrixPosition( light.matrixWorld );
|
|
|
+ _lightPosition.applyMatrix4( camera.matrixWorldInverse );
|
|
|
+
|
|
|
+ const positionAndCutoff = this._positionsAndCutoff[ i ];
|
|
|
+ positionAndCutoff.x = _lightPosition.x;
|
|
|
+ positionAndCutoff.y = _lightPosition.y;
|
|
|
+ positionAndCutoff.z = _lightPosition.z;
|
|
|
+ positionAndCutoff.w = light.distance;
|
|
|
+
|
|
|
+ _lightPosition.setFromMatrixPosition( light.matrixWorld );
|
|
|
+ _targetPosition.setFromMatrixPosition( light.target.matrixWorld );
|
|
|
+ _lightPosition.sub( _targetPosition ).transformDirection( camera.matrixWorldInverse );
|
|
|
+
|
|
|
+ const directionAndDecay = this._directionsAndDecay[ i ];
|
|
|
+ directionAndDecay.x = _lightPosition.x;
|
|
|
+ directionAndDecay.y = _lightPosition.y;
|
|
|
+ directionAndDecay.z = _lightPosition.z;
|
|
|
+ directionAndDecay.w = light.decay;
|
|
|
+
|
|
|
+ const cone = this._cones[ i ];
|
|
|
+ cone.x = Math.cos( light.angle );
|
|
|
+ cone.y = Math.cos( light.angle * ( 1 - light.penumbra ) );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ setup( builder ) {
|
|
|
+
|
|
|
+ const surfacePosition = builder.context.positionView || positionView;
|
|
|
+ const { lightingModel, reflectedLight } = builder.context;
|
|
|
+ const dynDiffuse = vec3( 0 ).toVar( 'dynSpotDiffuse' );
|
|
|
+ const dynSpecular = vec3( 0 ).toVar( 'dynSpotSpecular' );
|
|
|
+
|
|
|
+ Loop( this.countNode, ( { i } ) => {
|
|
|
+
|
|
|
+ const positionAndCutoff = this.positionsAndCutoffNode.element( i );
|
|
|
+ const lightViewPosition = positionAndCutoff.xyz;
|
|
|
+ const cutoffDistance = positionAndCutoff.w;
|
|
|
+
|
|
|
+ const directionAndDecay = this.directionsAndDecayNode.element( i );
|
|
|
+ const spotDirection = directionAndDecay.xyz;
|
|
|
+ const decayExponent = directionAndDecay.w;
|
|
|
+
|
|
|
+ const cone = this.conesNode.element( i );
|
|
|
+ const coneCos = cone.x;
|
|
|
+ const penumbraCos = cone.y;
|
|
|
+
|
|
|
+ const lightVector = lightViewPosition.sub( surfacePosition ).toVar();
|
|
|
+ const lightDirection = lightVector.normalize().toVar();
|
|
|
+ const lightDistance = lightVector.length();
|
|
|
+
|
|
|
+ const angleCos = lightDirection.dot( spotDirection );
|
|
|
+ const spotAttenuation = smoothstep( coneCos, penumbraCos, angleCos );
|
|
|
+ const distanceAttenuation = getDistanceAttenuation( {
|
|
|
+ lightDistance,
|
|
|
+ cutoffDistance,
|
|
|
+ decayExponent
|
|
|
+ } );
|
|
|
+
|
|
|
+ const lightColor = this.colorsNode.element( i ).mul( spotAttenuation ).mul( distanceAttenuation ).toVar();
|
|
|
+
|
|
|
+ lightingModel.direct( {
|
|
|
+ lightDirection,
|
|
|
+ lightColor,
|
|
|
+ lightNode: { light: {}, shadowNode: null },
|
|
|
+ reflectedLight: { directDiffuse: dynDiffuse, directSpecular: dynSpecular }
|
|
|
+ }, builder );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ reflectedLight.directDiffuse.addAssign( dynDiffuse );
|
|
|
+ reflectedLight.directSpecular.addAssign( dynSpecular );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+export default SpotLightDataNode;
|