diff --git a/examples/screenshots/webgl_loader_gltf_sheen.jpg b/examples/screenshots/webgl_loader_gltf_sheen.jpg index 97bd0d7600040e..cca3ae3398bec5 100644 Binary files a/examples/screenshots/webgl_loader_gltf_sheen.jpg and b/examples/screenshots/webgl_loader_gltf_sheen.jpg differ diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index 95dac4f95731df..83de85a32597c6 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -55,6 +55,7 @@ import { WebGLUniformsGroups } from './webgl/WebGLUniformsGroups.js'; import { createCanvasElement, probeAsync, warnOnce, error, warn, log } from '../utils.js'; import { ColorManagement } from '../math/ColorManagement.js'; import { getDFGLUT } from './shaders/DFGLUTData.js'; +import { getSheenLUT } from './shaders/SheenLUTData.js'; /** * This renderer uses WebGL 2 to display scenes. @@ -2520,6 +2521,13 @@ class WebGLRenderer { } + // Set Sheen LUT for sheen materials + if ( material.sheen > 0 ) { + + m_uniforms.sheenLUT.value = getSheenLUT(); + + } + if ( refreshMaterial ) { p_uniforms.setValue( _gl, 'toneMappingExposure', _this.toneMappingExposure ); diff --git a/src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js index 330e502c8fe783..a3ea3c85c0d22a 100644 --- a/src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js +++ b/src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js @@ -1,6 +1,7 @@ export default /* glsl */` uniform sampler2D dfgLUT; +uniform sampler2D sheenLUT; struct PhysicalMaterial { @@ -530,6 +531,21 @@ void RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geome sheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness ); + float dotNV = saturate( dot( geometryNormal, geometryViewDir ) ); + float maxSheenColor = max( max( material.sheenColor.r, material.sheenColor.g ), material.sheenColor.b ); + + // Lookup directional albedo for both view and light angles + float E_sheen_V = texture2D( sheenLUT, vec2( dotNV, material.sheenRoughness ) ).r; + float E_sheen_L = texture2D( sheenLUT, vec2( dotNL, material.sheenRoughness ) ).r; + + // Take minimum for proper energy conservation + float sheenScaling = min( + 1.0 - maxSheenColor * E_sheen_V, + 1.0 - maxSheenColor * E_sheen_L + ); + + irradiance *= sheenScaling; + #endif reflectedLight.directSpecular += irradiance * BRDF_GGX_Multiscatter( directLight.direction, geometryViewDir, geometryNormal, material ); @@ -555,6 +571,15 @@ void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradia sheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ); + // Energy conservation for indirect lighting + float dotNV = saturate( dot( geometryNormal, geometryViewDir ) ); + float maxSheenColor = max( max( material.sheenColor.r, material.sheenColor.g ), material.sheenColor.b ); + + // Lookup directional albedo from LUT + float E_sheen = texture2D( sheenLUT, vec2( dotNV, material.sheenRoughness ) ).r; + + float sheenScaling = 1.0 - maxSheenColor * E_sheen; + #endif // Both indirect specular and indirect diffuse light accumulate here @@ -576,10 +601,16 @@ void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradia vec3 totalScattering = singleScattering + multiScattering; vec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) ); - reflectedLight.indirectSpecular += radiance * singleScattering; - reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance; - - reflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance; + #ifdef USE_SHEEN + // Apply energy conservation scaling to base layers + reflectedLight.indirectSpecular += radiance * singleScattering * sheenScaling; + reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance * sheenScaling; + reflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance * sheenScaling; + #else + reflectedLight.indirectSpecular += radiance * singleScattering; + reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance; + reflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance; + #endif } diff --git a/src/renderers/shaders/SheenLUTData.js b/src/renderers/shaders/SheenLUTData.js new file mode 100644 index 00000000000000..08f6e10b03a0b7 --- /dev/null +++ b/src/renderers/shaders/SheenLUTData.js @@ -0,0 +1,65 @@ +/** + * Precomputed Sheen LUT for Charlie Sheen BRDF + * Resolution: 32x32 + * Samples: 4096 per texel + * Format: R16F (directional albedo for energy conservation) + * Based on Charlie sheen BRDF model for cloth-like materials + */ + +import { DataTexture } from '../../textures/DataTexture.js'; +import { RedFormat, HalfFloatType, LinearFilter, ClampToEdgeWrapping } from '../../constants.js'; + +const DATA = new Uint16Array( [ + 0x40ca, 0x3e9c, 0x3cb8, 0x3ad1, 0x38ef, 0x371f, 0x351d, 0x3348, 0x3123, 0x2f2b, 0x2cef, 0x2ab2, 0x2878, 0x25dd, 0x2389, 0x20bd, 0x1dd0, 0x1af0, 0x1804, 0x147e, 0x10d5, 0x0cf8, 0x08d9, 0x0474, 0x01e6, 0x00be, 0x0042, 0x0014, 0x0005, 0x0000, 0x0000, 0x0000, + 0x3e4f, 0x3d01, 0x3c14, 0x3ab9, 0x3993, 0x38a2, 0x37b5, 0x3667, 0x3550, 0x3464, 0x333d, 0x31f0, 0x30d9, 0x2fde, 0x2e57, 0x2d13, 0x2c06, 0x2a53, 0x28eb, 0x278d, 0x25b8, 0x2443, 0x223b, 0x2075, 0x1e37, 0x1c32, 0x1971, 0x16b5, 0x13b4, 0x0ffc, 0x0afe, 0x0440, + 0x3d2c, 0x3c4a, 0x3b49, 0x3a42, 0x3967, 0x38af, 0x3810, 0x370e, 0x361f, 0x354e, 0x3496, 0x33eb, 0x32d0, 0x31d7, 0x30fd, 0x303d, 0x2f2b, 0x2e06, 0x2d08, 0x2c2a, 0x2ad7, 0x298f, 0x2877, 0x2713, 0x2583, 0x2435, 0x2244, 0x2085, 0x1e40, 0x1c10, 0x18cd, 0x14bb, + 0x3c8c, 0x3bba, 0x3ab6, 0x39e4, 0x3933, 0x389a, 0x3815, 0x3740, 0x3670, 0x35b7, 0x3511, 0x347c, 0x33ec, 0x32fc, 0x3223, 0x3160, 0x30b0, 0x3012, 0x2f0a, 0x2e0c, 0x2d2a, 0x2c60, 0x2b59, 0x2a1b, 0x2903, 0x280e, 0x2671, 0x2500, 0x2388, 0x2170, 0x1f60, 0x1c7f, + 0x3c24, 0x3b26, 0x3a4c, 0x399b, 0x3904, 0x3881, 0x380d, 0x374c, 0x3693, 0x35ed, 0x3555, 0x34cc, 0x344f, 0x33b9, 0x32e9, 0x322a, 0x317b, 0x30dc, 0x304a, 0x2f8a, 0x2e97, 0x2db9, 0x2cf0, 0x2c3a, 0x2b29, 0x29ff, 0x28f3, 0x2804, 0x265d, 0x24e3, 0x232f, 0x20ed, + 0x3bb4, 0x3ab9, 0x39fc, 0x3960, 0x38dc, 0x3868, 0x3801, 0x3749, 0x36a2, 0x360a, 0x357f, 0x34ff, 0x348a, 0x341e, 0x3375, 0x32bd, 0x3213, 0x3175, 0x30e4, 0x305d, 0x2fc0, 0x2eda, 0x2e05, 0x2d41, 0x2c8c, 0x2bcc, 0x2a9c, 0x2985, 0x2887, 0x2740, 0x259e, 0x2425, + 0x3b45, 0x3a65, 0x39bc, 0x3931, 0x38ba, 0x3851, 0x37e7, 0x373f, 0x36a6, 0x3619, 0x3599, 0x3522, 0x34b4, 0x344d, 0x33dd, 0x332c, 0x3287, 0x31ee, 0x315e, 0x30d8, 0x305b, 0x2fcc, 0x2ef2, 0x2e26, 0x2d68, 0x2cb7, 0x2c13, 0x2af5, 0x29d9, 0x28d3, 0x27c3, 0x2605, + 0x3aec, 0x3a22, 0x3989, 0x390a, 0x389d, 0x383d, 0x37ce, 0x3732, 0x36a4, 0x3622, 0x35a9, 0x353a, 0x34d2, 0x3471, 0x3416, 0x3383, 0x32e3, 0x324e, 0x31c2, 0x313e, 0x30c1, 0x304c, 0x2fbd, 0x2eee, 0x2e2b, 0x2d74, 0x2cc7, 0x2c26, 0x2b1c, 0x2a00, 0x28f6, 0x27fa, + 0x3aa5, 0x39eb, 0x395e, 0x38e9, 0x3884, 0x382b, 0x37b6, 0x3725, 0x36a0, 0x3626, 0x35b4, 0x354b, 0x34e8, 0x348c, 0x3435, 0x33c7, 0x332e, 0x329d, 0x3214, 0x3193, 0x3118, 0x30a4, 0x3036, 0x2f9d, 0x2ed8, 0x2e1d, 0x2d6c, 0x2cc4, 0x2c26, 0x2b20, 0x2a04, 0x28f8, + 0x3a6a, 0x39bd, 0x3939, 0x38cc, 0x386e, 0x381b, 0x37a0, 0x3717, 0x369a, 0x3627, 0x35bb, 0x3557, 0x34f9, 0x34a1, 0x344e, 0x33ff, 0x336b, 0x32df, 0x3259, 0x31db, 0x3163, 0x30f0, 0x3083, 0x301b, 0x2f71, 0x2eb5, 0x2e02, 0x2d57, 0x2cb4, 0x2c18, 0x2b09, 0x29ef, + 0x3a38, 0x3995, 0x391a, 0x38b3, 0x385b, 0x380c, 0x378b, 0x370a, 0x3694, 0x3626, 0x35c0, 0x3560, 0x3507, 0x34b2, 0x3462, 0x3417, 0x339e, 0x3316, 0x3294, 0x3219, 0x31a3, 0x3132, 0x30c7, 0x3060, 0x2ffb, 0x2f3e, 0x2e8a, 0x2ddd, 0x2d37, 0x2c99, 0x2c00, 0x2add, + 0x3a0d, 0x3973, 0x38ff, 0x389e, 0x384a, 0x37fe, 0x3778, 0x36fe, 0x368d, 0x3624, 0x35c3, 0x3567, 0x3511, 0x34c0, 0x3473, 0x342a, 0x33ca, 0x3345, 0x32c7, 0x324f, 0x31db, 0x316d, 0x3103, 0x309d, 0x303b, 0x2fbb, 0x2f06, 0x2e59, 0x2db2, 0x2d11, 0x2c76, 0x2bc2, + 0x39e8, 0x3956, 0x38e7, 0x388a, 0x383a, 0x37e7, 0x3767, 0x36f2, 0x3686, 0x3622, 0x35c4, 0x356d, 0x351a, 0x34cc, 0x3481, 0x343b, 0x33ef, 0x336e, 0x32f4, 0x327e, 0x320d, 0x31a1, 0x3138, 0x30d4, 0x3073, 0x3016, 0x2f79, 0x2ecb, 0x2e24, 0x2d82, 0x2ce5, 0x2c4e, + 0x39c8, 0x393c, 0x38d2, 0x3879, 0x382d, 0x37d2, 0x3757, 0x36e7, 0x367f, 0x361f, 0x35c5, 0x3571, 0x3521, 0x34d5, 0x348d, 0x3449, 0x3408, 0x3392, 0x331b, 0x32a8, 0x3239, 0x31cf, 0x3168, 0x3105, 0x30a6, 0x304a, 0x2fe1, 0x2f35, 0x2e8d, 0x2deb, 0x2d4e, 0x2cb6, + 0x39ab, 0x3925, 0x38bf, 0x386a, 0x3820, 0x37be, 0x3748, 0x36dc, 0x3679, 0x361c, 0x35c5, 0x3574, 0x3526, 0x34dd, 0x3498, 0x3455, 0x3416, 0x33b2, 0x333d, 0x32cd, 0x3261, 0x31f8, 0x3194, 0x3132, 0x30d4, 0x3079, 0x3021, 0x2f96, 0x2ef0, 0x2e4e, 0x2db1, 0x2d18, + 0x3991, 0x3910, 0x38ae, 0x385c, 0x3815, 0x37ad, 0x373b, 0x36d3, 0x3672, 0x3619, 0x35c5, 0x3576, 0x352b, 0x34e4, 0x34a0, 0x3460, 0x3422, 0x33ce, 0x335c, 0x32ee, 0x3284, 0x321e, 0x31bb, 0x315b, 0x30ff, 0x30a5, 0x304d, 0x2ff1, 0x2f4b, 0x2eaa, 0x2e0d, 0x2d74, + 0x397a, 0x38fd, 0x389e, 0x384f, 0x380b, 0x379c, 0x372e, 0x36ca, 0x366c, 0x3616, 0x35c4, 0x3577, 0x352f, 0x34ea, 0x34a8, 0x3469, 0x342d, 0x33e7, 0x3377, 0x330c, 0x32a4, 0x3240, 0x31df, 0x3181, 0x3126, 0x30cd, 0x3077, 0x3022, 0x2fa1, 0x2f01, 0x2e65, 0x2dcc, + 0x3966, 0x38ec, 0x3890, 0x3844, 0x3802, 0x378d, 0x3723, 0x36c1, 0x3667, 0x3612, 0x35c3, 0x3579, 0x3532, 0x34ef, 0x34af, 0x3472, 0x3437, 0x33fd, 0x3390, 0x3327, 0x32c2, 0x325f, 0x3200, 0x31a4, 0x314a, 0x30f2, 0x309d, 0x304a, 0x2ff1, 0x2f52, 0x2eb7, 0x2e1f, + 0x3953, 0x38dd, 0x3884, 0x3839, 0x37f2, 0x377f, 0x3718, 0x36b9, 0x3661, 0x360f, 0x35c2, 0x357a, 0x3535, 0x34f3, 0x34b5, 0x3479, 0x3440, 0x3409, 0x33a7, 0x3340, 0x32dc, 0x327c, 0x321e, 0x31c3, 0x316b, 0x3115, 0x30c0, 0x306e, 0x301e, 0x2f9e, 0x2f05, 0x2e6e, + 0x3942, 0x38cf, 0x3878, 0x3830, 0x37e2, 0x3773, 0x370e, 0x36b1, 0x365c, 0x360c, 0x35c1, 0x357a, 0x3537, 0x34f7, 0x34ba, 0x3480, 0x3448, 0x3412, 0x33bb, 0x3356, 0x32f5, 0x3296, 0x323a, 0x31e1, 0x318a, 0x3135, 0x30e2, 0x3090, 0x3041, 0x2fe6, 0x2f4e, 0x2eb8, + 0x3932, 0x38c2, 0x386d, 0x3827, 0x37d3, 0x3767, 0x3704, 0x36aa, 0x3657, 0x3609, 0x35c0, 0x357b, 0x3539, 0x34fb, 0x34bf, 0x3486, 0x344f, 0x341a, 0x33ce, 0x336b, 0x330b, 0x32ae, 0x3254, 0x31fc, 0x31a6, 0x3152, 0x3101, 0x30b0, 0x3062, 0x3015, 0x2f93, 0x2efe, + 0x3924, 0x38b6, 0x3863, 0x381e, 0x37c6, 0x375b, 0x36fb, 0x36a3, 0x3652, 0x3606, 0x35be, 0x357b, 0x353b, 0x34fe, 0x34c3, 0x348b, 0x3456, 0x3422, 0x33df, 0x337e, 0x3320, 0x32c5, 0x326c, 0x3215, 0x31c1, 0x316e, 0x311e, 0x30cf, 0x3081, 0x3035, 0x2fd4, 0x2f41, + 0x3916, 0x38ab, 0x385a, 0x3817, 0x37b9, 0x3751, 0x36f3, 0x369d, 0x364d, 0x3603, 0x35bd, 0x357b, 0x353c, 0x3500, 0x34c7, 0x3490, 0x345c, 0x3429, 0x33ef, 0x3390, 0x3334, 0x32da, 0x3282, 0x322d, 0x31da, 0x3189, 0x3139, 0x30eb, 0x309e, 0x3053, 0x3009, 0x2f80, + 0x390a, 0x38a1, 0x3852, 0x3810, 0x37ad, 0x3747, 0x36eb, 0x3697, 0x3649, 0x3600, 0x35bc, 0x357b, 0x353d, 0x3503, 0x34cb, 0x3495, 0x3461, 0x342f, 0x33fe, 0x33a0, 0x3345, 0x32ed, 0x3297, 0x3243, 0x31f1, 0x31a1, 0x3153, 0x3105, 0x30ba, 0x306f, 0x3026, 0x2fbc, + 0x38ff, 0x3898, 0x384a, 0x3809, 0x37a2, 0x373e, 0x36e4, 0x3691, 0x3645, 0x35fd, 0x35ba, 0x357b, 0x353e, 0x3505, 0x34ce, 0x3499, 0x3466, 0x3435, 0x3406, 0x33b0, 0x3356, 0x32ff, 0x32ab, 0x3258, 0x3207, 0x31b8, 0x316b, 0x311f, 0x30d4, 0x308a, 0x3042, 0x2ff5, + 0x38f4, 0x388f, 0x3842, 0x3803, 0x3797, 0x3736, 0x36dd, 0x368c, 0x3641, 0x35fb, 0x35b9, 0x357a, 0x353f, 0x3507, 0x34d1, 0x349d, 0x346b, 0x343b, 0x340c, 0x33be, 0x3366, 0x3310, 0x32bd, 0x326b, 0x321c, 0x31ce, 0x3181, 0x3136, 0x30ed, 0x30a4, 0x305c, 0x3016, + 0x38ea, 0x3887, 0x383b, 0x37fa, 0x378e, 0x372e, 0x36d7, 0x3687, 0x363d, 0x35f8, 0x35b7, 0x357a, 0x3540, 0x3509, 0x34d3, 0x34a0, 0x346f, 0x3440, 0x3412, 0x33cb, 0x3374, 0x3320, 0x32ce, 0x327e, 0x322f, 0x31e2, 0x3197, 0x314d, 0x3104, 0x30bc, 0x3075, 0x3030, + 0x38e1, 0x387f, 0x3835, 0x37ef, 0x3784, 0x3726, 0x36d1, 0x3682, 0x363a, 0x35f6, 0x35b6, 0x357a, 0x3541, 0x350a, 0x34d6, 0x34a4, 0x3473, 0x3445, 0x3418, 0x33d7, 0x3382, 0x332f, 0x32de, 0x328f, 0x3242, 0x31f6, 0x31ab, 0x3162, 0x311a, 0x30d3, 0x308d, 0x3048, + 0x38d8, 0x3878, 0x382f, 0x37e4, 0x377c, 0x371f, 0x36cb, 0x367e, 0x3636, 0x35f4, 0x35b5, 0x357a, 0x3541, 0x350c, 0x34d8, 0x34a7, 0x3477, 0x3449, 0x341d, 0x33e3, 0x338f, 0x333d, 0x32ed, 0x329f, 0x3253, 0x3208, 0x31bf, 0x3176, 0x312f, 0x30e9, 0x30a4, 0x3060, + 0x38d0, 0x3871, 0x3829, 0x37db, 0x3774, 0x3718, 0x36c5, 0x3679, 0x3633, 0x35f1, 0x35b4, 0x3579, 0x3542, 0x350d, 0x34da, 0x34aa, 0x347b, 0x344d, 0x3422, 0x33ee, 0x339b, 0x334a, 0x32fc, 0x32af, 0x3263, 0x321a, 0x31d1, 0x318a, 0x3143, 0x30fe, 0x30ba, 0x3076, + 0x38c8, 0x386a, 0x3823, 0x37d1, 0x376c, 0x3712, 0x36c0, 0x3675, 0x3630, 0x35ef, 0x35b2, 0x3579, 0x3542, 0x350e, 0x34dc, 0x34ac, 0x347e, 0x3451, 0x3426, 0x33f8, 0x33a7, 0x3357, 0x3309, 0x32bd, 0x3273, 0x322a, 0x31e2, 0x319c, 0x3156, 0x3112, 0x30ce, 0x308b, + 0x38c1, 0x3864, 0x381e, 0x37c9, 0x3764, 0x370b, 0x36bb, 0x3671, 0x362d, 0x35ed, 0x35b1, 0x3578, 0x3543, 0x350f, 0x34de, 0x34af, 0x3481, 0x3455, 0x342a, 0x3401, 0x33b1, 0x3363, 0x3316, 0x32cb, 0x3282, 0x323a, 0x31f3, 0x31ad, 0x3169, 0x3125, 0x30e2, 0x30a0 +] ); + +let lut = null; + +export function getSheenLUT() { + + if ( lut === null ) { + + lut = new DataTexture( DATA, 32, 32, RedFormat, HalfFloatType ); + lut.minFilter = LinearFilter; + lut.magFilter = LinearFilter; + lut.wrapS = ClampToEdgeWrapping; + lut.wrapT = ClampToEdgeWrapping; + lut.generateMipmaps = false; + lut.needsUpdate = true; + + } + + return lut; + +} diff --git a/src/renderers/shaders/UniformsLib.js b/src/renderers/shaders/UniformsLib.js index 55e046b5e79b54..01fd5cdaafff4c 100644 --- a/src/renderers/shaders/UniformsLib.js +++ b/src/renderers/shaders/UniformsLib.js @@ -16,7 +16,9 @@ const UniformsLib = { alphaMap: { value: null }, alphaMapTransform: { value: /*@__PURE__*/ new Matrix3() }, - alphaTest: { value: 0 } + alphaTest: { value: 0 }, + + sheenLUT: { value: null } }, diff --git a/utils/generateSheenLUT.js b/utils/generateSheenLUT.js new file mode 100644 index 00000000000000..bfe485017fde66 --- /dev/null +++ b/utils/generateSheenLUT.js @@ -0,0 +1,280 @@ +/** + * Sheen LUT Generator for Three.js + * Generates precomputed lookup table for Charlie Sheen BRDF + * + * Usage: node generateSheenLUT.js [resolution] [samples] + * Example: node generateSheenLUT.js 32 4096 + */ + +import * as fs from 'fs'; + +// Configuration +const RESOLUTION = parseInt(process.argv[2]) || 32; +const SAMPLES_PER_TEXEL = parseInt(process.argv[3]) || 4096; + +console.log(`Generating Sheen LUT: ${RESOLUTION}x${RESOLUTION}, ${SAMPLES_PER_TEXEL} samples per texel`); + +// ============================================================================ +// Math utilities +// ============================================================================ + + +function normalize(v) { + const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + return [v[0] / len, v[1] / len, v[2] / len]; +} + +function add(a, b) { + return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; +} + + + +function clamp(x, min, max) { + return Math.max(min, Math.min(max, x)); +} + +// ============================================================================ +// Random number generation +// ============================================================================ + +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; // / 0x100000000 +} + +function hammersley(i, N) { + return [i / N, radicalInverse_VdC(i)]; +} + +// ============================================================================ +// Sampling functions +// ============================================================================ + +// Cosine-weighted hemisphere sampling +function importanceSampleCosine(u, v) { + const phi = 2.0 * Math.PI * u; + const cosTheta = Math.sqrt(1.0 - v); + const sinTheta = Math.sqrt(v); + + return [ + Math.cos(phi) * sinTheta, + Math.sin(phi) * sinTheta, + cosTheta + ]; +} + +// Transform sample from tangent space to world space + +// ============================================================================ +// Charlie Sheen BRDF +// ============================================================================ + +function D_Charlie(roughness, NoH) { + // Charlie sheen distribution + const invAlpha = 1.0 / roughness; + const cos2h = NoH * NoH; + const sin2h = Math.max(1.0 - cos2h, 0.0078125); // Avoid division by zero + return (2.0 + invAlpha) * Math.pow(sin2h, invAlpha * 0.5) / (2.0 * Math.PI); +} + +function V_Ashikhmin(NoL, NoV) { + // Ashikhmin visibility term for sheen + return 1.0 / (4.0 * (NoL + NoV - NoL * NoV)); +} + +function charlieSheen(roughness, NoH, NoL, NoV) { + const D = D_Charlie(roughness, NoH); + const V = V_Ashikhmin(NoL, NoV); + return D * V; +} + +// ============================================================================ +// LUT Generation +// ============================================================================ + +function integrateBRDF(roughness, NoV, samples) { + const V = [Math.sqrt(1.0 - NoV * NoV), 0.0, NoV]; + const N = [0.0, 0.0, 1.0]; + + let directionalAlbedo = 0.0; + + for (let i = 0; i < samples; i++) { + const [u, v] = hammersley(i, samples); + const L = importanceSampleCosine(u, v); + const H = normalize(add(V, L)); + + const NoL = Math.max(L[2], 0.0); + const NoH = Math.max(H[2], 0.0); + + if (NoL > 0.0) { + const brdf = charlieSheen(roughness, NoH, NoL, NoV); + + // Integrate directional albedo: E(μ) = ∫ f(μ, μ_o) μ_o dω_o + // This gives us the total energy reflected, used for energy conservation + const pdf = NoL / Math.PI; // Cosine-weighted hemisphere PDF + directionalAlbedo += (brdf * NoL) / pdf; + } + } + + directionalAlbedo /= samples; + + return directionalAlbedo; +} + +function generateLUT() { + const data = new Float32Array(RESOLUTION * RESOLUTION); + + for (let y = 0; y < RESOLUTION; y++) { + const roughness = (y + 0.5) / RESOLUTION; + + for (let x = 0; x < RESOLUTION; x++) { + const NoV = (x + 0.5) / RESOLUTION; + + const directionalAlbedo = integrateBRDF(roughness, NoV, SAMPLES_PER_TEXEL); + + const idx = y * RESOLUTION + x; + data[idx] = directionalAlbedo; + } + + // Progress indicator + if ((y + 1) % 4 === 0 || y === RESOLUTION - 1) { + const progress = ((y + 1) / RESOLUTION * 100).toFixed(1); + process.stdout.write(`\rProgress: ${progress}%`); + } + } + + console.log('\nLUT generation complete!'); + return data; +} + +// ============================================================================ +// Half-float conversion (IEEE 754 16-bit) +// ============================================================================ + +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; + } + + if (e > 142) { + bits |= 0x7c00; + bits |= ((e == 255) ? 0 : 1) && (x & 0x007fffff); + return bits; + } + + if (e < 113) { + m |= 0x0800; + bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1); + return bits; + } + + bits |= ((e - 112) << 10) | (m >> 1); + bits += m & 1; + return bits; +} + +function convertToHalfFloat(data) { + const halfData = new Uint16Array(data.length); + for (let i = 0; i < data.length; i++) { + halfData[i] = floatToHalf(data[i]); + } + return halfData; +} + +// ============================================================================ +// Output generation +// ============================================================================ + +function formatHexArray(data, itemsPerLine = 32) { + const lines = []; + for (let i = 0; i < data.length; i += itemsPerLine) { + const chunk = Array.from(data.slice(i, i + itemsPerLine)) + .map(x => '0x' + x.toString(16).padStart(4, '0')) + .join(', '); + lines.push('\t' + chunk + (i + itemsPerLine < data.length ? ',' : '')); + } + return lines.join('\n'); +} + +function generateJavaScriptFile(halfData) { + const header = `/** + * Precomputed Sheen LUT for Charlie Sheen BRDF + * Resolution: ${RESOLUTION}x${RESOLUTION} + * Samples: ${SAMPLES_PER_TEXEL} per texel + * Format: R16F (directional albedo for energy conservation) + * Based on Charlie sheen BRDF model for cloth-like materials + */ + +import { DataTexture } from '../../textures/DataTexture.js'; +import { RedFormat, HalfFloatType, LinearFilter, ClampToEdgeWrapping } from '../../constants.js'; + +const DATA = new Uint16Array( [ +`; + + const footer = ` +] ); + +let lut = null; + +export function getSheenLUT() { + +\tif ( lut === null ) { + +\t\tlut = new DataTexture( DATA, ${RESOLUTION}, ${RESOLUTION}, RedFormat, HalfFloatType ); +\t\tlut.minFilter = LinearFilter; +\t\tlut.magFilter = LinearFilter; +\t\tlut.wrapS = ClampToEdgeWrapping; +\t\tlut.wrapT = ClampToEdgeWrapping; +\t\tlut.generateMipmaps = false; +\t\tlut.needsUpdate = true; + +\t} + +\treturn lut; + +} +`; + + const dataStr = formatHexArray(halfData); + return header + dataStr + footer; +} + +// ============================================================================ +// Main +// ============================================================================ + +console.log('Starting LUT generation...\n'); +const startTime = Date.now(); + +const floatData = generateLUT(); +const halfData = convertToHalfFloat(floatData); + +const jsContent = generateJavaScriptFile(halfData); +const outputPath = './SheenLUTData.js'; + +fs.writeFileSync(outputPath, jsContent); + +const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); +console.log(`\nFile written to: ${outputPath}`); +console.log(`Total time: ${elapsed}s`); +console.log(`\nStats:`); +console.log(` Resolution: ${RESOLUTION}x${RESOLUTION}`); +console.log(` Samples per texel: ${SAMPLES_PER_TEXEL}`); +console.log(` Total samples: ${RESOLUTION * RESOLUTION * SAMPLES_PER_TEXEL}`); +console.log(` Data size: ${halfData.length * 2} bytes`); \ No newline at end of file