generateDFGLUT.js 7.7 KB

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