| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- /**
- * DFG LUT Generator
- *
- * Generates a precomputed lookup table for the split-sum approximation
- * used in Image-Based Lighting.
- *
- * Reference: "Real Shading in Unreal Engine 4" by Brian Karis
- */
- import * as fs from 'fs';
- const LUT_SIZE = 16;
- const SAMPLE_COUNT = 4096;
- // Van der Corput sequence
- function radicalInverse_VdC( bits ) {
- bits = ( bits << 16 ) | ( bits >>> 16 );
- bits = ( ( bits & 0x55555555 ) << 1 ) | ( ( bits & 0xAAAAAAAA ) >>> 1 );
- bits = ( ( bits & 0x33333333 ) << 2 ) | ( ( bits & 0xCCCCCCCC ) >>> 2 );
- bits = ( ( bits & 0x0F0F0F0F ) << 4 ) | ( ( bits & 0xF0F0F0F0 ) >>> 4 );
- bits = ( ( bits & 0x00FF00FF ) << 8 ) | ( ( bits & 0xFF00FF00 ) >>> 8 );
- return ( bits >>> 0 ) * 2.3283064365386963e-10;
- }
- function hammersley( i, N ) {
- return [ i / N, radicalInverse_VdC( i ) ];
- }
- function importanceSampleGGX( xi, N, roughness ) {
- const a = roughness * roughness;
- const phi = 2.0 * Math.PI * xi[ 0 ];
- const cosTheta = Math.sqrt( ( 1.0 - xi[ 1 ] ) / ( 1.0 + ( a * a - 1.0 ) * xi[ 1 ] ) );
- const sinTheta = Math.sqrt( 1.0 - cosTheta * cosTheta );
- const H = [
- Math.cos( phi ) * sinTheta,
- Math.sin( phi ) * sinTheta,
- cosTheta
- ];
- const up = Math.abs( N[ 2 ] ) < 0.999 ? [ 0, 0, 1 ] : [ 1, 0, 0 ];
- const tangent = normalize( cross( up, N ) );
- const bitangent = cross( N, tangent );
- const sampleVec = [
- tangent[ 0 ] * H[ 0 ] + bitangent[ 0 ] * H[ 1 ] + N[ 0 ] * H[ 2 ],
- tangent[ 1 ] * H[ 0 ] + bitangent[ 1 ] * H[ 1 ] + N[ 1 ] * H[ 2 ],
- tangent[ 2 ] * H[ 0 ] + bitangent[ 2 ] * H[ 1 ] + N[ 2 ] * H[ 2 ]
- ];
- return normalize( sampleVec );
- }
- function V_SmithGGXCorrelated( NdotV, NdotL, roughness ) {
- const a2 = Math.pow( roughness, 4.0 );
- const GGXV = NdotL * Math.sqrt( NdotV * NdotV * ( 1.0 - a2 ) + a2 );
- const GGXL = NdotV * Math.sqrt( NdotL * NdotL * ( 1.0 - a2 ) + a2 );
- return 0.5 / ( GGXV + GGXL );
- }
- function dot( a, b ) {
- return a[ 0 ] * b[ 0 ] + a[ 1 ] * b[ 1 ] + a[ 2 ] * b[ 2 ];
- }
- function cross( a, b ) {
- return [
- a[ 1 ] * b[ 2 ] - a[ 2 ] * b[ 1 ],
- a[ 2 ] * b[ 0 ] - a[ 0 ] * b[ 2 ],
- a[ 0 ] * b[ 1 ] - a[ 1 ] * b[ 0 ]
- ];
- }
- function length( v ) {
- return Math.sqrt( dot( v, v ) );
- }
- function normalize( v ) {
- const len = length( v );
- return len > 0 ? [ v[ 0 ] / len, v[ 1 ] / len, v[ 2 ] / len ] : [ 0, 0, 0 ];
- }
- function add( a, b ) {
- return [ a[ 0 ] + b[ 0 ], a[ 1 ] + b[ 1 ], a[ 2 ] + b[ 2 ] ];
- }
- function scale( v, s ) {
- return [ v[ 0 ] * s, v[ 1 ] * s, v[ 2 ] * s ];
- }
- // Convert float32 to float16 (half float)
- function floatToHalf( float ) {
- const floatView = new Float32Array( 1 );
- const int32View = new Int32Array( floatView.buffer );
- floatView[ 0 ] = float;
- const x = int32View[ 0 ];
- let bits = ( x >> 16 ) & 0x8000; // sign bit
- let m = ( x >> 12 ) & 0x07ff; // mantissa
- const e = ( x >> 23 ) & 0xff; // exponent
- // Handle special cases
- if ( e < 103 ) return bits; // Zero or denormal (too small)
- if ( e > 142 ) {
- bits |= 0x7c00; // Infinity
- bits |= ( ( e == 255 ) ? 0 : ( x & 0x007fffff ) ) >> 13; // NaN if e == 255 and mantissa != 0
- return bits;
- }
- if ( e < 113 ) {
- m |= 0x0800; // Add implicit leading bit
- bits |= ( m >> ( 114 - e ) ) + ( ( m >> ( 113 - e ) ) & 1 ); // Denormal with rounding
- return bits;
- }
- bits |= ( ( e - 112 ) << 10 ) | ( m >> 1 );
- bits += m & 1; // Rounding
- return bits;
- }
- function integrateBRDF( NdotV, roughness ) {
- const V = [
- Math.sqrt( 1.0 - NdotV * NdotV ),
- 0.0,
- NdotV
- ];
- let A = 0.0;
- let B = 0.0;
- const N = [ 0.0, 0.0, 1.0 ];
- for ( let i = 0; i < SAMPLE_COUNT; i ++ ) {
- const xi = hammersley( i, SAMPLE_COUNT );
- const H = importanceSampleGGX( xi, N, roughness );
- const L = normalize( add( scale( H, 2.0 * dot( V, H ) ), scale( V, - 1.0 ) ) );
- const NdotL = Math.max( L[ 2 ], 0.0 );
- const NdotH = Math.max( H[ 2 ], 0.0 );
- const VdotH = Math.max( dot( V, H ), 0.0 );
- if ( NdotL > 0.0 ) {
- const V_pdf = V_SmithGGXCorrelated( NdotV, NdotL, roughness ) * VdotH * NdotL / NdotH;
- const Fc = Math.pow( 1.0 - VdotH, 5.0 );
- A += ( 1.0 - Fc ) * V_pdf;
- B += Fc * V_pdf;
- }
- }
- return [ 4.0 * A / SAMPLE_COUNT, 4.0 * B / SAMPLE_COUNT ];
- }
- function generateDFGLUT() {
- console.log( `Generating ${LUT_SIZE}x${LUT_SIZE} DFG LUT with ${SAMPLE_COUNT} samples...` );
- const data = [];
- for ( let y = 0; y < LUT_SIZE; y ++ ) {
- const NdotV = ( y + 0.5 ) / LUT_SIZE;
- for ( let x = 0; x < LUT_SIZE; x ++ ) {
- const roughness = ( x + 0.5 ) / LUT_SIZE;
- const result = integrateBRDF( NdotV, roughness );
- data.push( result[ 0 ], result[ 1 ] );
- }
- if ( ( y + 1 ) % 8 === 0 ) {
- console.log( `Progress: ${( ( y + 1 ) / LUT_SIZE * 100 ).toFixed( 1 )}%` );
- }
- }
- console.log( 'Generation complete!' );
- return data;
- }
- // Save as JavaScript module
- function saveAsJavaScript( data ) {
- // Convert float32 data to half floats (uint16)
- const halfFloatData = [];
- for ( let i = 0; i < data.length; i ++ ) {
- halfFloatData.push( floatToHalf( data[ i ] ) );
- }
- const rows = [];
- for ( let y = 0; y < LUT_SIZE; y ++ ) {
- const rowData = [];
- for ( let x = 0; x < LUT_SIZE; x ++ ) {
- const idx = ( y * LUT_SIZE + x ) * 2;
- rowData.push( `0x${halfFloatData[ idx ].toString( 16 ).padStart( 4, '0' )}`, `0x${halfFloatData[ idx + 1 ].toString( 16 ).padStart( 4, '0' )}` );
- }
- rows.push( `\t${rowData.join( ', ' )}` );
- }
- const webgl = `/**
- * Precomputed DFG LUT for Image-Based Lighting
- * Resolution: ${LUT_SIZE}x${LUT_SIZE}
- * Samples: ${SAMPLE_COUNT} per texel
- * Format: RG16F (2 half floats per texel: scale, bias)
- */
- import { DataTexture } from '../../textures/DataTexture.js';
- import { RGFormat, HalfFloatType, LinearFilter, ClampToEdgeWrapping } from '../../constants.js';
- const DATA = new Uint16Array( [
- ${rows.join( ',\n' )}
- ] );
- let lut = null;
- export function getDFGLUT() {
- if ( lut === null ) {
- lut = new DataTexture( DATA, ${LUT_SIZE}, ${LUT_SIZE}, RGFormat, HalfFloatType );
- lut.name = 'DFG_LUT';
- lut.minFilter = LinearFilter;
- lut.magFilter = LinearFilter;
- lut.wrapS = ClampToEdgeWrapping;
- lut.wrapT = ClampToEdgeWrapping;
- lut.generateMipmaps = false;
- lut.needsUpdate = true;
- }
- return lut;
- }
- `;
- const webgpu = `import { Fn, vec2 } from '../../tsl/TSLBase.js';
- import { texture } from '../../accessors/TextureNode.js';
- import { DataTexture } from '../../../textures/DataTexture.js';
- import { RGFormat, HalfFloatType, LinearFilter, ClampToEdgeWrapping } from '../../../constants.js';
- /**
- * Precomputed DFG LUT for Image-Based Lighting
- * Resolution: ${LUT_SIZE}x${LUT_SIZE}
- * Samples: ${SAMPLE_COUNT} per texel
- * Format: RG16F (2 half floats per texel: scale, bias)
- */
- const DATA = new Uint16Array( [
- ${rows.join( ',\n' )}
- ] );
- let lut = null;
- const DFG = /*@__PURE__*/ Fn( ( { roughness, dotNV } ) => {
- if ( lut === null ) {
- lut = new DataTexture( DATA, ${LUT_SIZE}, ${LUT_SIZE}, RGFormat, HalfFloatType );
- lut.name = 'DFG_LUT';
- lut.minFilter = LinearFilter;
- lut.magFilter = LinearFilter;
- lut.wrapS = ClampToEdgeWrapping;
- lut.wrapT = ClampToEdgeWrapping;
- lut.generateMipmaps = false;
- lut.needsUpdate = true;
- }
- const uv = vec2( roughness, dotNV );
- return texture( lut, uv ).rg;
- } );
- export default DFG;
- `;
- fs.writeFileSync( './src/renderers/shaders/DFGLUTData.js', webgl );
- console.log( 'Saved WebGL version to ./src/renderers/shaders/DFGLUTData.js' );
- fs.writeFileSync( './src/nodes/functions/BSDF/DFG.js', webgpu );
- console.log( 'Saved WebGPU version to ./src/nodes/functions/BSDF/DFG.js' );
- }
- // Generate and save
- const lutData = generateDFGLUT();
- saveAsJavaScript( lutData );
- console.log( '\nDFG LUT generation complete!' );
- console.log( `Size: ${LUT_SIZE}x${LUT_SIZE} = ${LUT_SIZE * LUT_SIZE} texels` );
- console.log( `Data size: ${( lutData.length * 2 / 1024 ).toFixed( 2 )} KB (Uint16/Half Float)` );
- console.log( '\nThe LUT is used as a DataTexture in the renderer.' );
|