From a0c233000c210273ca93c0444413fd45b1e6c928 Mon Sep 17 00:00:00 2001 From: yum Date: Mon, 16 Mar 2026 19:35:38 -0700 Subject: Begin work on cloth (again) --- 3ner.shader | 6 +++ LUTS/dfg_cloth.exr | Bin 67752 -> 0 bytes LUTS/dfg_cloth.exr.meta | 127 ------------------------------------------------ LUTS/dfg_standard.exr | Bin 2052797 -> 3026136 bytes Scripts/make_dfg_lut.py | 120 ++++++++++++++++++++------------------------- brdf.cginc | 67 ++++++++++++++++++++++++- features.cginc | 4 ++ globals.cginc | 4 ++ lighting.cginc | 2 +- 9 files changed, 133 insertions(+), 197 deletions(-) delete mode 100755 LUTS/dfg_cloth.exr delete mode 100755 LUTS/dfg_cloth.exr.meta diff --git a/3ner.shader b/3ner.shader index 8d9167f..569ddc5 100755 --- a/3ner.shader +++ b/3ner.shader @@ -1133,6 +1133,12 @@ Shader "yum_food/3ner" _BRDF_Specular_Min_Denom("Specular minimum denominator", Float) = 0.000001 _Specular_AA_Variance("Specular AA Variance", Float) = 0.15 _Specular_AA_Threshold("Specular AA Threshold", Float) = 0.25 + //ifex _Cloth_Enabled==0 + [HideInInspector] m_start_Cloth("Cloth", Float) = 0 + [ThryToggle(_CLOTH)] _Cloth_Enabled("Enable", Float) = 0 + _Cloth_Sheen("Sheen", Color) = (0.5, 0.5, 0.5, 1) + [HideInInspector] m_end_Cloth("Cloth", Float) = 0 + //endex [HideInInspector] m_end_BRDF("BRDF", Float) = 0 [HideInInspector] m_start_blending ("Blending--{button_help:{text:Tutorial,action:{type:URL,data:https://www.poiyomi.com/rendering/blending},hover:Documentation}}", Float) = 0 diff --git a/LUTS/dfg_cloth.exr b/LUTS/dfg_cloth.exr deleted file mode 100755 index b30a2a7..0000000 Binary files a/LUTS/dfg_cloth.exr and /dev/null differ diff --git a/LUTS/dfg_cloth.exr.meta b/LUTS/dfg_cloth.exr.meta deleted file mode 100755 index a3630b0..0000000 --- a/LUTS/dfg_cloth.exr.meta +++ /dev/null @@ -1,127 +0,0 @@ -fileFormatVersion: 2 -guid: 59729cfaee66a3c4d847e732c7f99272 -TextureImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 12 - mipmaps: - mipMapMode: 1 - enableMipMap: 1 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - flipGreenChannel: 0 - isReadable: 0 - streamingMipmaps: 1 - streamingMipmapsPriority: 0 - vTOnly: 0 - ignoreMipmapLimit: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: 1 - aniso: 1 - mipBias: 0 - wrapU: 0 - wrapV: 0 - wrapW: 0 - nPOTScale: 1 - lightmap: 0 - compressionQuality: 50 - spriteMode: 0 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 0 - spriteTessellationDetail: -1 - textureType: 0 - textureShape: 1 - singleChannelComponent: 0 - flipbookRows: 1 - flipbookColumns: 1 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - ignorePngGamma: 0 - applyGammaDecoding: 0 - swizzle: 50462976 - cookieLightType: 0 - platformSettings: - - serializedVersion: 3 - buildTarget: DefaultTexturePlatform - maxTextureSize: 128 - resizeAlgorithm: 0 - textureFormat: 19 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 3 - buildTarget: Standalone - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 3 - buildTarget: Android - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - physicsShape: [] - bones: [] - spriteID: - internalID: 0 - vertices: [] - indices: - edges: [] - weights: [] - secondaryTextures: [] - nameFileIdTable: {} - mipmapLimitGroupName: - pSDRemoveMatte: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/LUTS/dfg_standard.exr b/LUTS/dfg_standard.exr index f5209c6..812ac05 100644 Binary files a/LUTS/dfg_standard.exr and b/LUTS/dfg_standard.exr differ diff --git a/Scripts/make_dfg_lut.py b/Scripts/make_dfg_lut.py index d58f3cf..4e16c99 100755 --- a/Scripts/make_dfg_lut.py +++ b/Scripts/make_dfg_lut.py @@ -73,45 +73,54 @@ def G_Cloth_L(x, a, b, c, d, e): @numba.njit(cache=True) -def G_Cloth(roughness, LoH): +def Lambda_Cloth(roughness, cos_theta): a0, a1 = 25.3245, 21.5473 b0, b1 = 3.32435, 3.82987 c0, c1 = 0.16801, 0.19823 d0, d1 = -1.27393, -1.97760 e0, e1 = -4.85967, -4.32054 - one_minus_r = 1.0 - roughness - one_minus_r_sq = one_minus_r * one_minus_r - one_minus_LoH = 1.0 - LoH + # Matches shader: interpolator = r^2 blends toward rough (a1) column + r_sq = roughness * roughness lambda_val = 0.0 - if LoH < 0.5: - L0 = G_Cloth_L(LoH, a0, b0, c0, d0, e0) - L1 = G_Cloth_L(LoH, a1, b1, c1, d1, e1) - L = lerp(L0, L1, one_minus_r_sq) + if cos_theta < 0.5: + L0 = G_Cloth_L(cos_theta, a0, b0, c0, d0, e0) + L1 = G_Cloth_L(cos_theta, a1, b1, c1, d1, e1) + L = lerp(L0, L1, r_sq) lambda_val = math.exp(L) else: L_05_0 = G_Cloth_L(0.5, a0, b0, c0, d0, e0) L_05_1 = G_Cloth_L(0.5, a1, b1, c1, d1, e1) - L_05 = lerp(L_05_0, L_05_1, one_minus_r_sq) + L_05 = lerp(L_05_0, L_05_1, r_sq) - L_LoH_0 = G_Cloth_L(one_minus_LoH, a0, b0, c0, d0, e0) - L_LoH_1 = G_Cloth_L(one_minus_LoH, a1, b1, c1, d1, e1) - L_LoH = lerp(L_LoH_0, L_LoH_1, one_minus_r_sq) + one_minus_cos = 1.0 - cos_theta + L_c_0 = G_Cloth_L(one_minus_cos, a0, b0, c0, d0, e0) + L_c_1 = G_Cloth_L(one_minus_cos, a1, b1, c1, d1, e1) + L_c = lerp(L_c_0, L_c_1, r_sq) - lambda_val = math.exp(2.0 * L_05 - L_LoH) + lambda_val = math.exp(2.0 * L_05 - L_c) # Apply terminator softening (equation 4) - return pow(lambda_val, 1.0 + 2.0 * pow(one_minus_LoH, 8.0)) + return pow(lambda_val, 1.0 + 2.0 * pow(1.0 - cos_theta, 8.0)) @numba.njit(cache=True) -def integrate_brdf_jitted(roughness, NoV, brdf_type, num_samples): +def V_Cloth(roughness, NoL, NoV): + # Height-correlated Smith: G2 / (4 * NoL * NoV) + lambda_l = Lambda_Cloth(roughness, NoL) + lambda_v = Lambda_Cloth(roughness, NoV) + return 1.0 / ((1.0 + lambda_l + lambda_v) * 4.0 * NoL * NoV + 1e-6) + + +@numba.njit(cache=True) +def integrate_brdf_jitted(roughness, NoV, num_samples): V_x = math.sqrt(1.0 - NoV * NoV) V_y = 0.0 V_z = NoV - A, B = 0.0, 0.0 + # R: GGX scale, G: GGX bias, B: cloth DFG + std_scale, std_bias, cloth_val = 0.0, 0.0, 0.0 for i in range(num_samples): e1, e2 = random.random(), random.random() @@ -137,47 +146,33 @@ def integrate_brdf_jitted(roughness, NoV, brdf_type, num_samples): NoL = saturate(L_z) NoH = saturate(H_z) - NoV_proxy = saturate(V_z) # NoV is V_z + NoV_proxy = saturate(V_z) if NoL > 0: - scale, bias = 0.0, 0.0 # --- Standard BRDF --- - if brdf_type == 1: - # Note that the D term is present in the numerator and the denominator, so it cancels out. - #D = D_GGX(roughness, NoH) - G = G_GGXSmith(roughness, NoL, NoV_proxy) - Fc_term = pow(1.0 - VoH, 5.0) - - # PDF of GGX Importance Sampling is D * NoH / (4 * VoH). - # The full term is (D * G * NoL) / PDF, which simplifies to: - # G * NoL * (4 * VoH / NoH). - # This can be unstable when NoH is close to zero, so we clamp the denominator. - common_term = (G * NoL * 4.0 * VoH) / max(NoH, 1e-5) - - # We are baking the two components of the split-sum approximation for IBL: - # reflectance = f0 * scale + bias - scale = common_term * (1.0 - Fc_term) - bias = common_term * Fc_term - # --- Cloth BRDF --- - elif brdf_type == 2: - # We are importance sampling GGX, so must account for its PDF. - D_c = D_Cloth(roughness, NoH) - G_c = G_Cloth(roughness, VoH) + # D cancels between numerator and PDF. + G = G_GGXSmith(roughness, NoL, NoV_proxy) + Fc_term = pow(1.0 - VoH, 5.0) - # PDF = D_GGX(r, NoH) * NoH / (4 * VoH) - pdf_ggx = D_GGX(roughness, NoH) * NoH / (4.0 * VoH + 1e-6) + # PDF = D_GGX * NoH / (4 * VoH), so (D * G * NoL) / PDF simplifies to: + common_term = (G * NoL * 4.0 * VoH) / max(NoH, 1e-5) - # We must divide by the PDF and multiply by our target distribution and the cosine term. - scale = (D_c * G_c * NoL) / (pdf_ggx + 1e-6) - bias = 0.0 + std_scale += common_term * (1.0 - Fc_term) + std_bias += common_term * Fc_term - A += scale - B += bias + # --- Cloth BRDF --- + # Same GGX importance samples, reweighted for cloth D and V. + if roughness >= 1e-4: + D_c = D_Cloth(roughness, NoH) + V_c = V_Cloth(roughness, NoL, NoV_proxy) + pdf_ggx = D_GGX(roughness, NoH) * NoH / (4.0 * VoH + 1e-6) + cloth_val += (D_c * V_c * NoL) / (pdf_ggx + 1e-6) - return A / num_samples, B / num_samples + inv_n = 1.0 / num_samples + return std_scale * inv_n, std_bias * inv_n, cloth_val * inv_n -def calculate_pixel(coords, resolution, brdf_type, num_samples): +def calculate_pixel(coords, resolution, num_samples): x, y = coords u = (x + 0.5) / resolution v = (y + 0.5) / resolution @@ -186,18 +181,14 @@ def calculate_pixel(coords, resolution, brdf_type, num_samples): roughness = saturate(v) if NoV < 1e-4: return x, y, 0.0, 0.0, 0.0 - r, g = 0.0, 0.0 - if brdf_type == 1: # standard - r, g = integrate_brdf_jitted(roughness, NoV, 1, num_samples) - elif brdf_type == 2: # cloth - if roughness < 1e-4: return x, y, 0.0, 0.0, 0.0 - r, g = integrate_brdf_jitted(roughness, NoV, 2, num_samples) + std_scale, std_bias, cloth = integrate_brdf_jitted(roughness, NoV, num_samples) - return x, y, r, g, 0.0 + # R: GGX scale, G: GGX bias, B: cloth DFG + return x, y, std_scale, std_bias, cloth -def generate_exr(resolution, output_filename, brdf_type, num_samples, num_workers): - print(f"Generating {resolution}x{resolution} EXR '{output_filename}' ({num_samples} samples/pixel) using {num_workers} workers.") +def generate_exr(resolution, output_filename, num_samples, num_workers): + print(f"Generating {resolution}x{resolution} EXR '{output_filename}' (R=GGX scale, G=GGX bias, B=cloth) ({num_samples} samples/pixel) using {num_workers} workers.") header = OpenEXR.Header(resolution, resolution) pt = Imath.PixelType(Imath.PixelType.FLOAT) header['channels'] = { 'R': Imath.Channel(pt), 'G': Imath.Channel(pt), 'B': Imath.Channel(pt) } @@ -205,7 +196,7 @@ def generate_exr(resolution, output_filename, brdf_type, num_samples, num_worker pixel_data = np.zeros((resolution, resolution, 3), dtype=np.float32) coords_to_process = [(x, y) for y in range(resolution) for x in range(resolution)] - worker_func = partial(calculate_pixel, resolution=resolution, brdf_type=brdf_type, num_samples=num_samples) + worker_func = partial(calculate_pixel, resolution=resolution, num_samples=num_samples) processed_count = 0 total_pixels = len(coords_to_process) @@ -241,15 +232,13 @@ def generate_exr(resolution, output_filename, brdf_type, num_samples, num_worker raise RuntimeError(f"Failed to write EXR file '{output_filename}': {e}") def main(): - parser = argparse.ArgumentParser(description='Generate DFG LUT EXR images for PBR.') - parser.add_argument('-t', '--type', choices=['standard', 'cloth'], default='standard', - help='Type of DFG texture to generate (default: standard)') + parser = argparse.ArgumentParser(description='Generate packed DFG LUT (R=GGX scale, G=cloth, B=GGX bias).') parser.add_argument('-r', '--resolution', type=int, default=512, help='Resolution of the square EXR image (default: 512)') parser.add_argument('-s', '--samples', type=int, default=8192, help='Number of samples per pixel for integration (default: 8192)') - parser.add_argument('-o', '--output', - help='Output filename (default: dfg_standard.exr or dfg_cloth.exr)') + parser.add_argument('-o', '--output', default='dfg.exr', + help='Output filename (default: dfg.exr)') parser.add_argument('-j', '--workers', type=int, default=os.cpu_count(), help=f'Number of worker processes (default: {os.cpu_count()})') @@ -259,11 +248,8 @@ def main(): print("Error: Resolution must be a positive integer") return 1 - brdf_type = 1 if args.type == 'standard' else 2 - output_filename = args.output if args.output else f'dfg_{args.type}.exr' - try: - generate_exr(args.resolution, output_filename, brdf_type, args.samples, args.workers) + generate_exr(args.resolution, args.output, args.samples, args.workers) except Exception as e: print(f"Error: {e}") return 1 diff --git a/brdf.cginc b/brdf.cginc index 94e7858..d9443ed 100755 --- a/brdf.cginc +++ b/brdf.cginc @@ -39,8 +39,6 @@ float3 F_Schlick(float LoH, float3 f0, float f90) { // tan^2(theta) = sin^2(theta) / cos^2(theta) // = (1 - cos^2(theta)) / cos^2(theta) // = -1 + 1 / cos^2(theta) -// The `max(1e-4, _)` and `min(4096, _)` are hand picked to minimize the issue -// of bloom making specular highlights flash as the camera moves. float D_GGX(float roughness, float NoH) { float r2 = roughness * roughness; float NoH2 = NoH * NoH; @@ -54,6 +52,14 @@ float D_GGX(float roughness, float NoH) { return r2 / denom; } +float D_Estevez(float roughness, float NoH) { + float r_rcp = rcp(roughness); + float sin_theta = sqrt(1 - NoH * NoH); + float D = (2 + r_rcp) * pow(sin_theta, r_rcp) / TAU; + + return D; +} + // Hammon "PBR Diffuse Lighting for GGX+Smith Microsurfaces" // Slide 84. Note that we remove the (4 * NoL * NoV) from the // denominator of the specular lobe because of some cancellations. @@ -64,6 +70,36 @@ float G_GGXSmith(float roughness, float NoL, float NoV) { return rcp(denom); } +float L_Estevez(float r, float x) { + // Recover constants according to Table 1. + float interpolator = 1 - r * r; + float one_minus_i = 1 - interpolator; + float a = interpolator * 25.3245 + one_minus_i * 21.5473; + float b = interpolator * 3.32435 + one_minus_i * 3.82987; + float c = interpolator * 0.16801 + one_minus_i * 0.19823; + float d = interpolator * -1.27393 + one_minus_i * -1.97760; + float e = interpolator * -4.85967 + one_minus_i * -4.32054; + + return a / (1 + b*pow(x, c)) + d*x + e; +} + +float Lambda_Estevez(float cos_theta, float roughness) { + // Equation 3 + float lambda = cos_theta < 0.5 + ? exp(L_Estevez(roughness, cos_theta)) + : exp(2 * L_Estevez(roughness, 0.5) - L_Estevez(roughness, 1 - cos_theta)); + // Equation 4 + return pow(lambda, 1 + 2 * pow(1 - cos_theta, 8)); +} + +// Estevez & Kulla "Production Friendly Microfacet Sheen BRDF" +// Height-correlated Smith: G2 / (4 * NoL * NoV) +float G_Estevez(float roughness, float NoL, float NoV) { + float lambda_l = Lambda_Estevez(NoL, roughness); + float lambda_v = Lambda_Estevez(NoV, roughness); + return 1.0 / ((1.0 + lambda_l + lambda_v) * 4.0 * NoL * NoV); +} + float4 brdf(Pbr pbr, LightData data, out BrdfData bd) { bd = (BrdfData)0; float3 specular = 0; @@ -124,6 +160,23 @@ float4 brdf(Pbr pbr, LightData data, out BrdfData bd) { remainder *= saturate(1.0f - bd.direct_f_cc * pbr.cc_strength); #endif +#if defined(_CLOTH) + float3 cloth_f0 = _Cloth_Sheen.rgb; + bd.direct_f = F_Schlick(data.direct.LoH, cloth_f0, f90); + bd.direct_d = D_Estevez(pbr.roughness, data.direct.NoH); + bd.direct_g = G_Estevez(pbr.roughness, data.direct.NoL, data.common.NoV); + + float3 direct_specular = (bd.direct_d * bd.direct_g) * bd.direct_f; + direct_specular *= data.direct.color * data.direct.NoL; + direct_specular *= remainder; + specular += direct_specular; + + float Fd = Fd_Lambertian(data.direct.NoL) / PI; + float3 direct_diffuse = Fd * pbr.albedo.xyz * data.direct.color; + direct_diffuse *= remainder; + direct_diffuse = max(0, direct_diffuse); + diffuse += direct_diffuse; +#else bd.direct_f = F_Schlick(data.direct.LoH, f0_color, f90); bd.direct_d = D_GGX(pbr.roughness, data.direct.NoH); bd.direct_g = G_GGXSmith(pbr.roughness, data.direct.NoL, data.common.NoV); @@ -143,6 +196,7 @@ float4 brdf(Pbr pbr, LightData data, out BrdfData bd) { direct_diffuse *= remainder; direct_diffuse = max(0, direct_diffuse); diffuse += direct_diffuse; +#endif } // Indirect @@ -157,6 +211,14 @@ float4 brdf(Pbr pbr, LightData data, out BrdfData bd) { remainder -= cc_specular_dfg; #endif +#if defined(_CLOTH) + float3 specular_dfg = _Cloth_Sheen.rgb * bd.ibl_dfg.zzz; + float3 indirect_specular = data.indirect.specular * specular_dfg; + specular += indirect_specular * remainder; + + float3 indirect_diffuse = pbr.albedo.xyz * data.indirect.diffuse; + diffuse += indirect_diffuse * remainder; +#else float3 specular_dfg = bd.ibl_dfg.xxx * f0_color + bd.ibl_dfg.yyy; // filament 5.3.4.6 float3 indirect_specular = data.indirect.specular * specular_dfg; @@ -165,6 +227,7 @@ float4 brdf(Pbr pbr, LightData data, out BrdfData bd) { float3 indirect_diffuse = pbr.albedo.xyz * data.indirect.diffuse * (1.0 - pbr.metallic); diffuse += indirect_diffuse * remainder; +#endif } #endif diff --git a/features.cginc b/features.cginc index 9a9c8fd..79c5001 100755 --- a/features.cginc +++ b/features.cginc @@ -223,4 +223,8 @@ #pragma shader_feature_local _RIM_LIGHTING0_MASK //endex +//ifex _Cloth_Enabled==0 +#pragma shader_feature_local _CLOTH +//endex + #endif // __FEATURES_INC diff --git a/globals.cginc b/globals.cginc index ded2048..d32feeb 100755 --- a/globals.cginc +++ b/globals.cginc @@ -564,4 +564,8 @@ float4 _Vertex_Deformation_Slot_15_Vector_2; float4 _Vertex_Deformation_Slot_15_Vector_3; #endif // _VERTEX_DEFORMATION +#if defined(_CLOTH) +float4 _Cloth_Sheen; +#endif // _CLOTH + #endif // __GLOBALS_INC diff --git a/lighting.cginc b/lighting.cginc index c8c3f64..c735fdb 100755 --- a/lighting.cginc +++ b/lighting.cginc @@ -229,7 +229,7 @@ void GetLighting(v2f i, Pbr pbr, out LightData data) { data.direct.double_LoV = max(1e-4, 2.0f * direct_LoV * direct_LoV - 1.0f); float4 lightColorIntensity = getDirectLightColorIntensity(); - data.direct.color = lightColorIntensity.rgb * getShadowAttenuation(i); + data.direct.color = lightColorIntensity.rgb * (lightColorIntensity.w * getShadowAttenuation(i)); // Indirect lighting float3 reflect_dir = reflect(-data.common.V, pbr.normal); -- cgit v1.2.3