generateDFGLUT.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /**
  2. * DFG LUT Generator (32x32)
  3. *
  4. * Generates a precomputed lookup table for the split-sum approximation
  5. * used in Image-Based Lighting. The 32x32 resolution provides a minimal
  6. * memory footprint for DataTexture usage.
  7. *
  8. * Reference: "Real Shading in Unreal Engine 4" by Brian Karis
  9. */
  10. import * as fs from 'fs';
  11. const LUT_SIZE = 32;
  12. const SAMPLE_COUNT = 4096;
  13. // Van der Corput sequence
  14. function radicalInverse_VdC( bits ) {
  15. bits = ( bits << 16 ) | ( bits >>> 16 );
  16. bits = ( ( bits & 0x55555555 ) << 1 ) | ( ( bits & 0xAAAAAAAA ) >>> 1 );
  17. bits = ( ( bits & 0x33333333 ) << 2 ) | ( ( bits & 0xCCCCCCCC ) >>> 2 );
  18. bits = ( ( bits & 0x0F0F0F0F ) << 4 ) | ( ( bits & 0xF0F0F0F0 ) >>> 4 );
  19. bits = ( ( bits & 0x00FF00FF ) << 8 ) | ( ( bits & 0xFF00FF00 ) >>> 8 );
  20. return ( bits >>> 0 ) * 2.3283064365386963e-10;
  21. }
  22. function hammersley( i, N ) {
  23. return [ i / N, radicalInverse_VdC( i ) ];
  24. }
  25. function importanceSampleGGX( xi, N, roughness ) {
  26. const a = roughness * roughness;
  27. const phi = 2.0 * Math.PI * xi[ 0 ];
  28. const cosTheta = Math.sqrt( ( 1.0 - xi[ 1 ] ) / ( 1.0 + ( a * a - 1.0 ) * xi[ 1 ] ) );
  29. const sinTheta = Math.sqrt( 1.0 - cosTheta * cosTheta );
  30. const H = [
  31. Math.cos( phi ) * sinTheta,
  32. Math.sin( phi ) * sinTheta,
  33. cosTheta
  34. ];
  35. const up = Math.abs( N[ 2 ] ) < 0.999 ? [ 0, 0, 1 ] : [ 1, 0, 0 ];
  36. const tangent = normalize( cross( up, N ) );
  37. const bitangent = cross( N, tangent );
  38. const sampleVec = [
  39. tangent[ 0 ] * H[ 0 ] + bitangent[ 0 ] * H[ 1 ] + N[ 0 ] * H[ 2 ],
  40. tangent[ 1 ] * H[ 0 ] + bitangent[ 1 ] * H[ 1 ] + N[ 1 ] * H[ 2 ],
  41. tangent[ 2 ] * H[ 0 ] + bitangent[ 2 ] * H[ 1 ] + N[ 2 ] * H[ 2 ]
  42. ];
  43. return normalize( sampleVec );
  44. }
  45. function V_SmithGGXCorrelated( NdotV, NdotL, roughness ) {
  46. const a2 = Math.pow( roughness, 4.0 );
  47. const GGXV = NdotL * Math.sqrt( NdotV * NdotV * ( 1.0 - a2 ) + a2 );
  48. const GGXL = NdotV * Math.sqrt( NdotL * NdotL * ( 1.0 - a2 ) + a2 );
  49. return 0.5 / ( GGXV + GGXL );
  50. }
  51. function dot( a, b ) {
  52. return a[ 0 ] * b[ 0 ] + a[ 1 ] * b[ 1 ] + a[ 2 ] * b[ 2 ];
  53. }
  54. function cross( a, b ) {
  55. return [
  56. a[ 1 ] * b[ 2 ] - a[ 2 ] * b[ 1 ],
  57. a[ 2 ] * b[ 0 ] - a[ 0 ] * b[ 2 ],
  58. a[ 0 ] * b[ 1 ] - a[ 1 ] * b[ 0 ]
  59. ];
  60. }
  61. function length( v ) {
  62. return Math.sqrt( dot( v, v ) );
  63. }
  64. function normalize( v ) {
  65. const len = length( v );
  66. return len > 0 ? [ v[ 0 ] / len, v[ 1 ] / len, v[ 2 ] / len ] : [ 0, 0, 0 ];
  67. }
  68. function add( a, b ) {
  69. return [ a[ 0 ] + b[ 0 ], a[ 1 ] + b[ 1 ], a[ 2 ] + b[ 2 ] ];
  70. }
  71. function scale( v, s ) {
  72. return [ v[ 0 ] * s, v[ 1 ] * s, v[ 2 ] * s ];
  73. }
  74. // Convert float32 to float16 (half float)
  75. function floatToHalf( float ) {
  76. const floatView = new Float32Array( 1 );
  77. const int32View = new Int32Array( floatView.buffer );
  78. floatView[ 0 ] = float;
  79. const x = int32View[ 0 ];
  80. let bits = ( x >> 16 ) & 0x8000; // sign bit
  81. let m = ( x >> 12 ) & 0x07ff; // mantissa
  82. const e = ( x >> 23 ) & 0xff; // exponent
  83. // Handle special cases
  84. if ( e < 103 ) return bits; // Zero or denormal (too small)
  85. if ( e > 142 ) {
  86. bits |= 0x7c00; // Infinity
  87. bits |= ( ( e == 255 ) ? 0 : ( x & 0x007fffff ) ) >> 13; // NaN if e == 255 and mantissa != 0
  88. return bits;
  89. }
  90. if ( e < 113 ) {
  91. m |= 0x0800; // Add implicit leading bit
  92. bits |= ( m >> ( 114 - e ) ) + ( ( m >> ( 113 - e ) ) & 1 ); // Denormal with rounding
  93. return bits;
  94. }
  95. bits |= ( ( e - 112 ) << 10 ) | ( m >> 1 );
  96. bits += m & 1; // Rounding
  97. return bits;
  98. }
  99. function integrateBRDF( NdotV, roughness ) {
  100. const V = [
  101. Math.sqrt( 1.0 - NdotV * NdotV ),
  102. 0.0,
  103. NdotV
  104. ];
  105. let A = 0.0;
  106. let B = 0.0;
  107. const N = [ 0.0, 0.0, 1.0 ];
  108. for ( let i = 0; i < SAMPLE_COUNT; i ++ ) {
  109. const xi = hammersley( i, SAMPLE_COUNT );
  110. const H = importanceSampleGGX( xi, N, roughness );
  111. const L = normalize( add( scale( H, 2.0 * dot( V, H ) ), scale( V, - 1.0 ) ) );
  112. const NdotL = Math.max( L[ 2 ], 0.0 );
  113. const NdotH = Math.max( H[ 2 ], 0.0 );
  114. const VdotH = Math.max( dot( V, H ), 0.0 );
  115. if ( NdotL > 0.0 ) {
  116. const V_pdf = V_SmithGGXCorrelated( NdotV, NdotL, roughness ) * VdotH * NdotL / NdotH;
  117. const Fc = Math.pow( 1.0 - VdotH, 5.0 );
  118. A += ( 1.0 - Fc ) * V_pdf;
  119. B += Fc * V_pdf;
  120. }
  121. }
  122. return [ 4.0 * A / SAMPLE_COUNT, 4.0 * B / SAMPLE_COUNT ];
  123. }
  124. function generateDFGLUT() {
  125. console.log( `Generating ${LUT_SIZE}x${LUT_SIZE} DFG LUT with ${SAMPLE_COUNT} samples...` );
  126. const data = [];
  127. for ( let y = 0; y < LUT_SIZE; y ++ ) {
  128. const NdotV = ( y + 0.5 ) / LUT_SIZE;
  129. for ( let x = 0; x < LUT_SIZE; x ++ ) {
  130. const roughness = ( x + 0.5 ) / LUT_SIZE;
  131. const result = integrateBRDF( NdotV, roughness );
  132. data.push( result[ 0 ], result[ 1 ] );
  133. }
  134. if ( ( y + 1 ) % 8 === 0 ) {
  135. console.log( `Progress: ${( ( y + 1 ) / LUT_SIZE * 100 ).toFixed( 1 )}%` );
  136. }
  137. }
  138. console.log( 'Generation complete!' );
  139. return data;
  140. }
  141. // Save as JavaScript module
  142. function saveAsJavaScript( data ) {
  143. // Convert float32 data to half floats (uint16)
  144. const halfFloatData = [];
  145. for ( let i = 0; i < data.length; i ++ ) {
  146. halfFloatData.push( floatToHalf( data[ i ] ) );
  147. }
  148. const rows = [];
  149. for ( let y = 0; y < LUT_SIZE; y ++ ) {
  150. const rowData = [];
  151. for ( let x = 0; x < LUT_SIZE; x ++ ) {
  152. const idx = ( y * LUT_SIZE + x ) * 2;
  153. rowData.push( `0x${halfFloatData[ idx ].toString( 16 ).padStart( 4, '0' )}`, `0x${halfFloatData[ idx + 1 ].toString( 16 ).padStart( 4, '0' )}` );
  154. }
  155. rows.push( `\t${rowData.join( ', ' )}` );
  156. }
  157. const webgl = `/**
  158. * Precomputed DFG LUT for Image-Based Lighting
  159. * Resolution: ${LUT_SIZE}x${LUT_SIZE}
  160. * Samples: ${SAMPLE_COUNT} per texel
  161. * Format: RG16F (2 half floats per texel: scale, bias)
  162. */
  163. import { DataTexture } from '../../textures/DataTexture.js';
  164. import { RGFormat, HalfFloatType, LinearFilter, ClampToEdgeWrapping } from '../../constants.js';
  165. const DATA = new Uint16Array( [
  166. ${rows.join( ',\n' )}
  167. ] );
  168. let lut = null;
  169. export function getDFGLUT() {
  170. if ( lut === null ) {
  171. lut = new DataTexture( DATA, ${LUT_SIZE}, ${LUT_SIZE}, RGFormat, HalfFloatType );
  172. lut.minFilter = LinearFilter;
  173. lut.magFilter = LinearFilter;
  174. lut.wrapS = ClampToEdgeWrapping;
  175. lut.wrapT = ClampToEdgeWrapping;
  176. lut.generateMipmaps = false;
  177. lut.needsUpdate = true;
  178. }
  179. return lut;
  180. }
  181. `;
  182. const webgpu = `import { Fn, vec2 } from '../../tsl/TSLBase.js';
  183. import { texture } from '../../accessors/TextureNode.js';
  184. import { DataTexture } from '../../../textures/DataTexture.js';
  185. import { RGFormat, HalfFloatType, LinearFilter, ClampToEdgeWrapping } from '../../../constants.js';
  186. /**
  187. * Precomputed DFG LUT for Image-Based Lighting
  188. * Resolution: ${LUT_SIZE}x${LUT_SIZE}
  189. * Samples: ${SAMPLE_COUNT} per texel
  190. * Format: RG16F (2 half floats per texel: scale, bias)
  191. */
  192. const DATA = new Uint16Array( [
  193. ${rows.join( ',\n' )}
  194. ] );
  195. let lut = null;
  196. const DFGApprox = /*@__PURE__*/ Fn( ( { roughness, dotNV } ) => {
  197. if ( lut === null ) {
  198. lut = new DataTexture( DATA, ${LUT_SIZE}, ${LUT_SIZE}, RGFormat, HalfFloatType );
  199. lut.minFilter = LinearFilter;
  200. lut.magFilter = LinearFilter;
  201. lut.wrapS = ClampToEdgeWrapping;
  202. lut.wrapT = ClampToEdgeWrapping;
  203. lut.generateMipmaps = false;
  204. lut.needsUpdate = true;
  205. }
  206. const uv = vec2( roughness, dotNV );
  207. return texture( lut, uv ).rg;
  208. } );
  209. export default DFGApprox;
  210. `;
  211. fs.writeFileSync( './src/renderers/shaders/DFGLUTData.js', webgl );
  212. console.log( 'Saved WebGL version to ./src/renderers/shaders/DFGLUTData.js' );
  213. fs.writeFileSync( './src/nodes/functions/BSDF/DFGApprox.js', webgpu );
  214. console.log( 'Saved WebGPU version to ./src/nodes/functions/BSDF/DFGApprox.js' );
  215. }
  216. // Generate and save
  217. const lutData = generateDFGLUT();
  218. saveAsJavaScript( lutData );
  219. console.log( '\nDFG LUT generation complete!' );
  220. console.log( `Size: ${LUT_SIZE}x${LUT_SIZE} = ${LUT_SIZE * LUT_SIZE} texels` );
  221. console.log( `Data size: ${( lutData.length * 2 / 1024 ).toFixed( 2 )} KB (Uint16/Half Float)` );
  222. console.log( '\nThe LUT is used as a DataTexture in the renderer.' );
粤ICP备19079148号