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) --- Scripts/make_dfg_lut.py | 120 +++++++++++++++++++++--------------------------- 1 file changed, 53 insertions(+), 67 deletions(-) (limited to 'Scripts') 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 -- cgit v1.2.3