TiledLightsNode.js 11 KB

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