LightProbeGridNode.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import { AnalyticLightNode, Vector3 } from 'three/webgpu';
  2. import { array, getShIrradianceAt, normalWorld, positionWorld, texture3D, uniform, vec3 } from 'three/tsl';
  3. // Padding texels at each boundary of every atlas sub-volume.
  4. export const ATLAS_PADDING = 1;
  5. /**
  6. * Samples the packed SH atlas and evaluates L2 irradiance for the given normal.
  7. *
  8. * The atlas stores the seven RGBA sub-volumes stacked along Z, each occupying
  9. * `( nz + 2 )` slices: one padding slice (a copy of the nearest edge slice) at
  10. * each end to prevent color bleeding when the hardware trilinear filter reads
  11. * across a sub-volume boundary.
  12. *
  13. * @private
  14. * @param {Texture3DNode} atlas - The atlas texture node.
  15. * @param {Node<vec3>} uvw - The probe-grid sample coordinate (texel centers).
  16. * @param {Node<vec3>} res - The probe resolution.
  17. * @param {Node<vec3>} normal - The world-space normal.
  18. * @return {Node<vec3>} The non-negative irradiance.
  19. */
  20. function evaluateGridIrradiance( atlas, uvw, res, normal ) {
  21. const nz = res.z;
  22. const paddedSlices = nz.add( 2.0 * ATLAS_PADDING );
  23. const atlasDepth = paddedSlices.mul( 7.0 );
  24. const uvZBase = uvw.z.mul( nz ).add( ATLAS_PADDING );
  25. const slice = ( t ) => atlas.sample( vec3( uvw.xy, uvZBase.add( paddedSlices.mul( t ) ).div( atlasDepth ) ) );
  26. const s0 = slice( 0 ), s1 = slice( 1 ), s2 = slice( 2 ), s3 = slice( 3 );
  27. const s4 = slice( 4 ), s5 = slice( 5 ), s6 = slice( 6 );
  28. // Unpack 9 vec3 L2 SH coefficients and evaluate irradiance.
  29. const sh = array( [
  30. s0.xyz,
  31. vec3( s0.w, s1.xy ),
  32. vec3( s1.zw, s2.x ),
  33. s2.yzw,
  34. s3.xyz,
  35. vec3( s3.w, s4.xy ),
  36. vec3( s4.zw, s5.x ),
  37. s5.yzw,
  38. s6.xyz
  39. ] );
  40. return getShIrradianceAt( normal, sh ).max( vec3( 0.0 ) );
  41. }
  42. /**
  43. * The light node that applies a {@link LightProbeGrid} to the scene. It samples
  44. * the baked L2 spherical-harmonic atlas at the surface position and adds the
  45. * resulting irradiance to the lighting context, so every standard node material
  46. * picks up the grid automatically (same role as the WebGL `lights_fragment_begin`
  47. * integration).
  48. *
  49. * @private
  50. * @augments AnalyticLightNode
  51. */
  52. class LightProbeGridNode extends AnalyticLightNode {
  53. static get type() {
  54. return 'LightProbeGridNode';
  55. }
  56. constructor( light = null ) {
  57. super( light );
  58. this._min = uniform( new Vector3() );
  59. this._max = uniform( new Vector3() );
  60. this._resolution = uniform( new Vector3() );
  61. this._intensity = uniform( 1 );
  62. this._falloff = uniform( 0 );
  63. }
  64. update( /* frame */ ) {
  65. const light = this.light;
  66. this._min.value.copy( light.boundingBox.min );
  67. this._max.value.copy( light.boundingBox.max );
  68. this._resolution.value.copy( light.resolution );
  69. this._intensity.value = light.intensity;
  70. this._falloff.value = light.falloff;
  71. }
  72. setup( builder ) {
  73. const light = this.light;
  74. // No baked data yet: contribute nothing.
  75. if ( light.texture === null ) return;
  76. const min = this._min;
  77. const max = this._max;
  78. const res = this._resolution;
  79. const range = max.sub( min );
  80. const resMinusOne = res.sub( 1.0 );
  81. const spacing = range.div( resMinusOne );
  82. // Offset along the normal by half a probe spacing, then remap to texel centers.
  83. const samplePos = positionWorld.add( normalWorld.mul( spacing ).mul( 0.5 ) );
  84. const uvw = samplePos.sub( min ).div( range ).clamp( 0.0, 1.0 ).mul( resMinusOne ).div( res ).add( vec3( 0.5 ).div( res ) );
  85. const result = evaluateGridIrradiance( texture3D( light.texture ), uvw, res, normalWorld );
  86. let irradiance = result.mul( this._intensity );
  87. // Optional smooth boundary for blending grids; falloff 0 applies everywhere.
  88. if ( light.falloff > 0 ) {
  89. const outside = min.sub( positionWorld ).max( 0.0 ).add( positionWorld.sub( max ).max( 0.0 ) );
  90. const weight = outside.length().smoothstep( 0.0, this._falloff ).oneMinus();
  91. irradiance = irradiance.mul( weight );
  92. }
  93. builder.context.irradiance.addAssign( irradiance );
  94. }
  95. }
  96. export { LightProbeGridNode };
粤ICP备19079148号