Spaces:
Running
Running
import { DataTexture, FloatType, RGBAFormat, Vector2, Vector3, LightsNode, NodeUpdateType } from 'three/webgpu'; | |
import { | |
attributeArray, nodeProxy, int, float, vec2, ivec2, ivec4, uniform, Break, Loop, | |
Fn, If, Return, textureLoad, instanceIndex, screenCoordinate, directPointLight | |
} from 'three/tsl'; | |
export const circleIntersectsAABB = /*@__PURE__*/ Fn( ( [ circleCenter, radius, minBounds, maxBounds ] ) => { | |
// Find the closest point on the AABB to the circle's center using method chaining | |
const closestX = minBounds.x.max( circleCenter.x.min( maxBounds.x ) ); | |
const closestY = minBounds.y.max( circleCenter.y.min( maxBounds.y ) ); | |
// Compute the distance between the circle's center and the closest point | |
const distX = circleCenter.x.sub( closestX ); | |
const distY = circleCenter.y.sub( closestY ); | |
// Calculate the squared distance | |
const distSquared = distX.mul( distX ).add( distY.mul( distY ) ); | |
return distSquared.lessThanEqual( radius.mul( radius ) ); | |
} ).setLayout( { | |
name: 'circleIntersectsAABB', | |
type: 'bool', | |
inputs: [ | |
{ name: 'circleCenter', type: 'vec2' }, | |
{ name: 'radius', type: 'float' }, | |
{ name: 'minBounds', type: 'vec2' }, | |
{ name: 'maxBounds', type: 'vec2' } | |
] | |
} ); | |
const _vector3 = /*@__PURE__*/ new Vector3(); | |
const _size = /*@__PURE__*/ new Vector2(); | |
class TiledLightsNode extends LightsNode { | |
static get type() { | |
return 'TiledLightsNode'; | |
} | |
constructor( maxLights = 1024, tileSize = 32 ) { | |
super(); | |
this.materialLights = []; | |
this.tiledLights = []; | |
this.maxLights = maxLights; | |
this.tileSize = tileSize; | |
this._bufferSize = null; | |
this._lightIndexes = null; | |
this._screenTileIndex = null; | |
this._compute = null; | |
this._lightsTexture = null; | |
this._lightsCount = uniform( 0, 'int' ); | |
this._tileLightCount = 8; | |
this._screenSize = uniform( new Vector2() ); | |
this._cameraProjectionMatrix = uniform( 'mat4' ); | |
this._cameraViewMatrix = uniform( 'mat4' ); | |
this.updateBeforeType = NodeUpdateType.RENDER; | |
} | |
customCacheKey() { | |
return this._compute.getCacheKey() + super.customCacheKey(); | |
} | |
updateLightsTexture() { | |
const { _lightsTexture: lightsTexture, tiledLights } = this; | |
const data = lightsTexture.image.data; | |
const lineSize = lightsTexture.image.width * 4; | |
this._lightsCount.value = tiledLights.length; | |
for ( let i = 0; i < tiledLights.length; i ++ ) { | |
const light = tiledLights[ i ]; | |
// world position | |
_vector3.setFromMatrixPosition( light.matrixWorld ); | |
// store data | |
const offset = i * 4; | |
data[ offset + 0 ] = _vector3.x; | |
data[ offset + 1 ] = _vector3.y; | |
data[ offset + 2 ] = _vector3.z; | |
data[ offset + 3 ] = light.distance; | |
data[ lineSize + offset + 0 ] = light.color.r * light.intensity; | |
data[ lineSize + offset + 1 ] = light.color.g * light.intensity; | |
data[ lineSize + offset + 2 ] = light.color.b * light.intensity; | |
data[ lineSize + offset + 3 ] = light.decay; | |
} | |
lightsTexture.needsUpdate = true; | |
} | |
updateBefore( frame ) { | |
const { renderer, camera } = frame; | |
this.updateProgram( renderer ); | |
this.updateLightsTexture( camera ); | |
this._cameraProjectionMatrix.value = camera.projectionMatrix; | |
this._cameraViewMatrix.value = camera.matrixWorldInverse; | |
renderer.getDrawingBufferSize( _size ); | |
this._screenSize.value.copy( _size ); | |
renderer.compute( this._compute ); | |
} | |
setLights( lights ) { | |
const { tiledLights, materialLights } = this; | |
let materialindex = 0; | |
let tiledIndex = 0; | |
for ( const light of lights ) { | |
if ( light.isPointLight === true ) { | |
tiledLights[ tiledIndex ++ ] = light; | |
} else { | |
materialLights[ materialindex ++ ] = light; | |
} | |
} | |
materialLights.length = materialindex; | |
tiledLights.length = tiledIndex; | |
return super.setLights( materialLights ); | |
} | |
getBlock( block = 0 ) { | |
return this._lightIndexes.element( this._screenTileIndex.mul( int( 2 ).add( int( block ) ) ) ); | |
} | |
getTile( element ) { | |
element = int( element ); | |
const stride = int( 4 ); | |
const tileOffset = element.div( stride ); | |
const tileIndex = this._screenTileIndex.mul( int( 2 ) ).add( tileOffset ); | |
return this._lightIndexes.element( tileIndex ).element( element.modInt( stride ) ); | |
} | |
getLightData( index ) { | |
index = int( index ); | |
const dataA = textureLoad( this._lightsTexture, ivec2( index, 0 ) ); | |
const dataB = textureLoad( this._lightsTexture, ivec2( index, 1 ) ); | |
const position = dataA.xyz; | |
const viewPosition = this._cameraViewMatrix.mul( position ); | |
const distance = dataA.w; | |
const color = dataB.rgb; | |
const decay = dataB.w; | |
return { | |
position, | |
viewPosition, | |
distance, | |
color, | |
decay | |
}; | |
} | |
setupLights( builder, lightNodes ) { | |
this.updateProgram( builder.renderer ); | |
// | |
const lightingModel = builder.context.reflectedLight; | |
// force declaration order, before of the loop | |
lightingModel.directDiffuse.append(); | |
lightingModel.directSpecular.append(); | |
super.setupLights( builder, lightNodes ); | |
Fn( () => { | |
Loop( this._tileLightCount, ( { i } ) => { | |
const lightIndex = this.getTile( i ); | |
If( lightIndex.equal( int( 0 ) ), () => { | |
Break(); | |
} ); | |
const { color, decay, viewPosition, distance } = this.getLightData( lightIndex.sub( 1 ) ); | |
directPointLight( { | |
color, | |
lightViewPosition: viewPosition, | |
cutoffDistance: distance, | |
decayExponent: decay | |
} ).append(); | |
} ); | |
} )().append(); | |
} | |
getBufferFitSize( value ) { | |
const multiple = this.tileSize; | |
return Math.ceil( value / multiple ) * multiple; | |
} | |
setSize( width, height ) { | |
width = this.getBufferFitSize( width ); | |
height = this.getBufferFitSize( height ); | |
if ( ! this._bufferSize || this._bufferSize.width !== width || this._bufferSize.height !== height ) { | |
this.create( width, height ); | |
} | |
return this; | |
} | |
updateProgram( renderer ) { | |
renderer.getDrawingBufferSize( _size ); | |
const width = this.getBufferFitSize( _size.width ); | |
const height = this.getBufferFitSize( _size.height ); | |
if ( this._bufferSize === null ) { | |
this.create( width, height ); | |
} else if ( this._bufferSize.width !== width || this._bufferSize.height !== height ) { | |
this.create( width, height ); | |
} | |
} | |
create( width, height ) { | |
const { tileSize, maxLights } = this; | |
const bufferSize = new Vector2( width, height ); | |
const lineSize = Math.floor( bufferSize.width / tileSize ); | |
const count = Math.floor( ( bufferSize.width * bufferSize.height ) / tileSize ); | |
// buffers | |
const lightsData = new Float32Array( maxLights * 4 * 2 ); // 2048 lights, 4 elements(rgba), 2 components, 1 component per line (position, distance, color, decay) | |
const lightsTexture = new DataTexture( lightsData, lightsData.length / 8, 2, RGBAFormat, FloatType ); | |
const lightIndexesArray = new Int32Array( count * 4 * 2 ); | |
const lightIndexes = attributeArray( lightIndexesArray, 'ivec4' ).label( 'lightIndexes' ); | |
// compute | |
const getBlock = ( index ) => { | |
const tileIndex = instanceIndex.mul( int( 2 ) ).add( int( index ) ); | |
return lightIndexes.element( tileIndex ); | |
}; | |
const getTile = ( elementIndex ) => { | |
elementIndex = int( elementIndex ); | |
const stride = int( 4 ); | |
const tileOffset = elementIndex.div( stride ); | |
const tileIndex = instanceIndex.mul( int( 2 ) ).add( tileOffset ); | |
return lightIndexes.element( tileIndex ).element( elementIndex.modInt( stride ) ); | |
}; | |
const compute = Fn( () => { | |
const { _cameraProjectionMatrix: cameraProjectionMatrix, _bufferSize: bufferSize, _screenSize: screenSize } = this; | |
const tiledBufferSize = bufferSize.clone().divideScalar( tileSize ).floor(); | |
const tileScreen = vec2( | |
instanceIndex.modInt( tiledBufferSize.width ), | |
instanceIndex.div( tiledBufferSize.width ) | |
).mul( tileSize ).div( screenSize ); | |
const blockSize = float( tileSize ).div( screenSize ); | |
const minBounds = tileScreen; | |
const maxBounds = minBounds.add( blockSize ); | |
const index = int( 0 ).toVar(); | |
getBlock( 0 ).assign( ivec4( 0 ) ); | |
getBlock( 1 ).assign( ivec4( 0 ) ); | |
Loop( this.maxLights, ( { i } ) => { | |
If( index.greaterThanEqual( this._tileLightCount ).or( int( i ).greaterThanEqual( int( this._lightsCount ) ) ), () => { | |
Return(); | |
} ); | |
const { viewPosition, distance } = this.getLightData( i ); | |
const projectedPosition = cameraProjectionMatrix.mul( viewPosition ); | |
const ndc = projectedPosition.div( projectedPosition.w ); | |
const screenPosition = ndc.xy.mul( 0.5 ).add( 0.5 ).flipY(); | |
const distanceFromCamera = viewPosition.z; | |
const pointRadius = distance.div( distanceFromCamera ); | |
If( circleIntersectsAABB( screenPosition, pointRadius, minBounds, maxBounds ), () => { | |
getTile( index ).assign( i.add( int( 1 ) ) ); | |
index.addAssign( int( 1 ) ); | |
} ); | |
} ); | |
} )().compute( count ); | |
// screen coordinate lighting indexes | |
const screenTile = screenCoordinate.div( tileSize ).floor().toVar(); | |
const screenTileIndex = screenTile.x.add( screenTile.y.mul( lineSize ) ); | |
// assigns | |
this._bufferSize = bufferSize; | |
this._lightIndexes = lightIndexes; | |
this._screenTileIndex = screenTileIndex; | |
this._compute = compute; | |
this._lightsTexture = lightsTexture; | |
} | |
get hasLights() { | |
return super.hasLights || this.tiledLights.length > 0; | |
} | |
} | |
export default TiledLightsNode; | |
export const tiledLights = /*@__PURE__*/ nodeProxy( TiledLightsNode ); | |