TiledLightsNode.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import { DataTexture, FloatType, RGBAFormat, Vector2, Vector3, LightsNode, NodeUpdateType } from 'three/webgpu';
  2. import {
  3. attributeArray, nodeProxy, int, float, vec2, ivec2, ivec4, uniform, Break, Loop, positionView,
  4. Fn, If, Return, textureLoad, instanceIndex, screenCoordinate, directPointLight
  5. } from 'three/tsl';
  6. export const circleIntersectsAABB = /*@__PURE__*/ Fn( ( [ circleCenter, radius, minBounds, maxBounds ] ) => {
  7. // Find the closest point on the AABB to the circle's center using method chaining
  8. const closestX = minBounds.x.max( circleCenter.x.min( maxBounds.x ) );
  9. const closestY = minBounds.y.max( circleCenter.y.min( maxBounds.y ) );
  10. // Compute the distance between the circle's center and the closest point
  11. const distX = circleCenter.x.sub( closestX );
  12. const distY = circleCenter.y.sub( closestY );
  13. // Calculate the squared distance
  14. const distSquared = distX.mul( distX ).add( distY.mul( distY ) );
  15. return distSquared.lessThanEqual( radius.mul( radius ) );
  16. } ).setLayout( {
  17. name: 'circleIntersectsAABB',
  18. type: 'bool',
  19. inputs: [
  20. { name: 'circleCenter', type: 'vec2' },
  21. { name: 'radius', type: 'float' },
  22. { name: 'minBounds', type: 'vec2' },
  23. { name: 'maxBounds', type: 'vec2' }
  24. ]
  25. } );
  26. const _vector3 = /*@__PURE__*/ new Vector3();
  27. const _size = /*@__PURE__*/ new Vector2();
  28. /**
  29. * A custom version of `LightsNode` implementing tiled lighting. This node is used in
  30. * {@link TiledLighting} to overwrite the renderer's default lighting with
  31. * a custom implementation.
  32. *
  33. * @augments LightsNode
  34. * @three_import import { tiledLights } from 'three/addons/tsl/lighting/TiledLightsNode.js';
  35. */
  36. class TiledLightsNode extends LightsNode {
  37. static get type() {
  38. return 'TiledLightsNode';
  39. }
  40. /**
  41. * Constructs a new tiled lights node.
  42. *
  43. * @param {number} [maxLights=1024] - The maximum number of lights.
  44. * @param {number} [tileSize=32] - The tile size.
  45. */
  46. constructor( maxLights = 1024, tileSize = 32 ) {
  47. super();
  48. this.materialLights = [];
  49. this.tiledLights = [];
  50. /**
  51. * The maximum number of lights.
  52. *
  53. * @type {number}
  54. * @default 1024
  55. */
  56. this.maxLights = maxLights;
  57. /**
  58. * The tile size.
  59. *
  60. * @type {number}
  61. * @default 32
  62. */
  63. this.tileSize = tileSize;
  64. this._bufferSize = null;
  65. this._lightIndexes = null;
  66. this._screenTileIndex = null;
  67. this._compute = null;
  68. this._lightsTexture = null;
  69. this._lightsCount = uniform( 0, 'int' );
  70. this._tileLightCount = 8;
  71. this._screenSize = uniform( new Vector2() );
  72. this._cameraProjectionMatrix = uniform( 'mat4' );
  73. this._cameraViewMatrix = uniform( 'mat4' );
  74. this.updateBeforeType = NodeUpdateType.RENDER;
  75. }
  76. customCacheKey() {
  77. return this._compute.getCacheKey() + super.customCacheKey();
  78. }
  79. updateLightsTexture() {
  80. const { _lightsTexture: lightsTexture, tiledLights } = this;
  81. const data = lightsTexture.image.data;
  82. const lineSize = lightsTexture.image.width * 4;
  83. this._lightsCount.value = tiledLights.length;
  84. for ( let i = 0; i < tiledLights.length; i ++ ) {
  85. const light = tiledLights[ i ];
  86. // world position
  87. _vector3.setFromMatrixPosition( light.matrixWorld );
  88. // store data
  89. const offset = i * 4;
  90. data[ offset + 0 ] = _vector3.x;
  91. data[ offset + 1 ] = _vector3.y;
  92. data[ offset + 2 ] = _vector3.z;
  93. data[ offset + 3 ] = light.distance;
  94. data[ lineSize + offset + 0 ] = light.color.r * light.intensity;
  95. data[ lineSize + offset + 1 ] = light.color.g * light.intensity;
  96. data[ lineSize + offset + 2 ] = light.color.b * light.intensity;
  97. data[ lineSize + offset + 3 ] = light.decay;
  98. }
  99. lightsTexture.needsUpdate = true;
  100. }
  101. updateBefore( frame ) {
  102. const { renderer, camera } = frame;
  103. this.updateProgram( renderer );
  104. this.updateLightsTexture( camera );
  105. this._cameraProjectionMatrix.value = camera.projectionMatrix;
  106. this._cameraViewMatrix.value = camera.matrixWorldInverse;
  107. renderer.getDrawingBufferSize( _size );
  108. this._screenSize.value.copy( _size );
  109. renderer.compute( this._compute );
  110. }
  111. setLights( lights ) {
  112. const { tiledLights, materialLights } = this;
  113. let materialindex = 0;
  114. let tiledIndex = 0;
  115. for ( const light of lights ) {
  116. if ( light.isPointLight === true ) {
  117. tiledLights[ tiledIndex ++ ] = light;
  118. } else {
  119. materialLights[ materialindex ++ ] = light;
  120. }
  121. }
  122. materialLights.length = materialindex;
  123. tiledLights.length = tiledIndex;
  124. return super.setLights( materialLights );
  125. }
  126. getBlock( block = 0 ) {
  127. return this._lightIndexes.element( this._screenTileIndex.mul( int( 2 ).add( int( block ) ) ) );
  128. }
  129. getTile( element ) {
  130. element = int( element );
  131. const stride = int( 4 );
  132. const tileOffset = element.div( stride );
  133. const tileIndex = this._screenTileIndex.mul( int( 2 ) ).add( tileOffset );
  134. return this._lightIndexes.element( tileIndex ).element( element.mod( stride ) );
  135. }
  136. getLightData( index ) {
  137. index = int( index );
  138. const dataA = textureLoad( this._lightsTexture, ivec2( index, 0 ) );
  139. const dataB = textureLoad( this._lightsTexture, ivec2( index, 1 ) );
  140. const position = dataA.xyz;
  141. const viewPosition = this._cameraViewMatrix.mul( position );
  142. const distance = dataA.w;
  143. const color = dataB.rgb;
  144. const decay = dataB.w;
  145. return {
  146. position,
  147. viewPosition,
  148. distance,
  149. color,
  150. decay
  151. };
  152. }
  153. setupLights( builder, lightNodes ) {
  154. this.updateProgram( builder.renderer );
  155. //
  156. const lightingModel = builder.context.reflectedLight;
  157. // force declaration order, before of the loop
  158. lightingModel.directDiffuse.toStack();
  159. lightingModel.directSpecular.toStack();
  160. super.setupLights( builder, lightNodes );
  161. Fn( () => {
  162. Loop( this._tileLightCount, ( { i } ) => {
  163. const lightIndex = this.getTile( i );
  164. If( lightIndex.equal( int( 0 ) ), () => {
  165. Break();
  166. } );
  167. const { color, decay, viewPosition, distance } = this.getLightData( lightIndex.sub( 1 ) );
  168. builder.lightsNode.setupDirectLight( builder, this, directPointLight( {
  169. color,
  170. lightVector: viewPosition.sub( positionView ),
  171. cutoffDistance: distance,
  172. decayExponent: decay
  173. } ) );
  174. } );
  175. }, 'void' )();
  176. }
  177. getBufferFitSize( value ) {
  178. const multiple = this.tileSize;
  179. return Math.ceil( value / multiple ) * multiple;
  180. }
  181. setSize( width, height ) {
  182. width = this.getBufferFitSize( width );
  183. height = this.getBufferFitSize( height );
  184. if ( ! this._bufferSize || this._bufferSize.width !== width || this._bufferSize.height !== height ) {
  185. this.create( width, height );
  186. }
  187. return this;
  188. }
  189. updateProgram( renderer ) {
  190. renderer.getDrawingBufferSize( _size );
  191. const width = this.getBufferFitSize( _size.width );
  192. const height = this.getBufferFitSize( _size.height );
  193. if ( this._bufferSize === null ) {
  194. this.create( width, height );
  195. } else if ( this._bufferSize.width !== width || this._bufferSize.height !== height ) {
  196. this.create( width, height );
  197. }
  198. }
  199. create( width, height ) {
  200. const { tileSize, maxLights } = this;
  201. const bufferSize = new Vector2( width, height );
  202. const lineSize = Math.floor( bufferSize.width / tileSize );
  203. const count = Math.floor( ( bufferSize.width * bufferSize.height ) / tileSize );
  204. // buffers
  205. const lightsData = new Float32Array( maxLights * 4 * 2 ); // 2048 lights, 4 elements(rgba), 2 components, 1 component per line (position, distance, color, decay)
  206. const lightsTexture = new DataTexture( lightsData, lightsData.length / 8, 2, RGBAFormat, FloatType );
  207. const lightIndexesArray = new Int32Array( count * 4 * 2 );
  208. const lightIndexes = attributeArray( lightIndexesArray, 'ivec4' ).label( 'lightIndexes' );
  209. // compute
  210. const getBlock = ( index ) => {
  211. const tileIndex = instanceIndex.mul( int( 2 ) ).add( int( index ) );
  212. return lightIndexes.element( tileIndex );
  213. };
  214. const getTile = ( elementIndex ) => {
  215. elementIndex = int( elementIndex );
  216. const stride = int( 4 );
  217. const tileOffset = elementIndex.div( stride );
  218. const tileIndex = instanceIndex.mul( int( 2 ) ).add( tileOffset );
  219. return lightIndexes.element( tileIndex ).element( elementIndex.mod( stride ) );
  220. };
  221. const compute = Fn( () => {
  222. const { _cameraProjectionMatrix: cameraProjectionMatrix, _bufferSize: bufferSize, _screenSize: screenSize } = this;
  223. const tiledBufferSize = bufferSize.clone().divideScalar( tileSize ).floor();
  224. const tileScreen = vec2(
  225. instanceIndex.mod( tiledBufferSize.width ),
  226. instanceIndex.div( tiledBufferSize.width )
  227. ).mul( tileSize ).div( screenSize );
  228. const blockSize = float( tileSize ).div( screenSize );
  229. const minBounds = tileScreen;
  230. const maxBounds = minBounds.add( blockSize );
  231. const index = int( 0 ).toVar();
  232. getBlock( 0 ).assign( ivec4( 0 ) );
  233. getBlock( 1 ).assign( ivec4( 0 ) );
  234. Loop( this.maxLights, ( { i } ) => {
  235. If( index.greaterThanEqual( this._tileLightCount ).or( int( i ).greaterThanEqual( int( this._lightsCount ) ) ), () => {
  236. Return();
  237. } );
  238. const { viewPosition, distance } = this.getLightData( i );
  239. const projectedPosition = cameraProjectionMatrix.mul( viewPosition );
  240. const ndc = projectedPosition.div( projectedPosition.w );
  241. const screenPosition = ndc.xy.mul( 0.5 ).add( 0.5 ).flipY();
  242. const distanceFromCamera = viewPosition.z;
  243. const pointRadius = distance.div( distanceFromCamera );
  244. If( circleIntersectsAABB( screenPosition, pointRadius, minBounds, maxBounds ), () => {
  245. getTile( index ).assign( i.add( int( 1 ) ) );
  246. index.addAssign( int( 1 ) );
  247. } );
  248. } );
  249. } )().compute( count );
  250. // screen coordinate lighting indexes
  251. const screenTile = screenCoordinate.div( tileSize ).floor().toVar();
  252. const screenTileIndex = screenTile.x.add( screenTile.y.mul( lineSize ) );
  253. // assigns
  254. this._bufferSize = bufferSize;
  255. this._lightIndexes = lightIndexes;
  256. this._screenTileIndex = screenTileIndex;
  257. this._compute = compute;
  258. this._lightsTexture = lightsTexture;
  259. }
  260. get hasLights() {
  261. return super.hasLights || this.tiledLights.length > 0;
  262. }
  263. }
  264. export default TiledLightsNode;
  265. export const tiledLights = /*@__PURE__*/ nodeProxy( TiledLightsNode );