diff options
| author | yum <yum.food.vr@gmail.com> | 2026-02-17 16:47:22 -0800 |
|---|---|---|
| committer | yum <yum.food.vr@gmail.com> | 2026-02-17 16:47:22 -0800 |
| commit | 6aea3d8274e140e739dfd1da1133ed19656ff6ec (patch) | |
| tree | af221367d9a17f02e448b74a693cc250b721a77c /Scripts | |
| parent | 3f7a950d47d64b364f1f4efea61311424d2198c4 (diff) | |
Diffstat (limited to 'Scripts')
| -rw-r--r-- | Scripts/generate_dfg_lut.py | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/Scripts/generate_dfg_lut.py b/Scripts/generate_dfg_lut.py new file mode 100644 index 0000000..83759d2 --- /dev/null +++ b/Scripts/generate_dfg_lut.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "numpy", +# "openexr", +# ] +# /// +""" +Generate a DFG LUT (Look-Up Table) for PBR split-sum approximation. + +This computes the pre-integrated BRDF for the GGX microfacet model, +storing scale and bias factors for the Fresnel term. + +Output: DFG LUT as an EXR file with RG channels (scale, bias). +""" + +import numpy as np + +try: + import OpenEXR + import Imath + HAS_OPENEXR = True +except ImportError: + HAS_OPENEXR = False + + +def generate_hammersley_sequence(n): + """Pre-compute Hammersley 2D sequence for n samples.""" + i = np.arange(n, dtype=np.uint32) + + # Reverse bits for radical inverse + v = i.copy() + v = ((v >> 1) & 0x55555555) | ((v & 0x55555555) << 1) + v = ((v >> 2) & 0x33333333) | ((v & 0x33333333) << 2) + v = ((v >> 4) & 0x0F0F0F0F) | ((v & 0x0F0F0F0F) << 4) + v = ((v >> 8) & 0x00FF00FF) | ((v & 0x00FF00FF) << 8) + v = (v >> 16) | (v << 16) + + e1 = i.astype(np.float32) / n + e2 = v.astype(np.float64) / 0x100000000 + + return e1, e2.astype(np.float32) + + +def generate_dfg_lut(width=64, height=32, num_samples=512): + """ + Generate the full DFG LUT (vectorized). + + Compatible with HLSL: float2 dfg_uv = float2(NoV, roughness); + X axis (U): NdotV (0 to 1) + Y axis (V): roughness (0 to 1) + """ + # Pre-compute Hammersley sequence + e1, e2 = generate_hammersley_sequence(num_samples) + phi = 2.0 * np.pi * e1 + cos_phi = np.cos(phi) + sin_phi = np.sin(phi) + + # Create coordinate grids matching HLSL UV layout + x = np.arange(width, dtype=np.float32) + y = np.arange(height, dtype=np.float32) + ndotv_arr = (x + 0.5) / width # shape: (width,) - U axis + roughness = (y + 0.5) / height # shape: (height,) - V axis + + # Pre-compute roughness terms + m = roughness * roughness + m2 = m * m # shape: (height,) + + lut = np.zeros((height, width, 2), dtype=np.float32) + + for yi, (rough, rough_m2) in enumerate(zip(roughness, m2)): + # GGX importance sampling - vectorized over samples and NdotV + # cos_theta shape: (width, num_samples) + denom = 1.0 + (rough_m2 - 1.0) * e2[np.newaxis, :] + cos_theta = np.sqrt((1.0 - e2[np.newaxis, :]) / denom) + sin_theta = np.sqrt(1.0 - cos_theta * cos_theta) + + # Half vector in tangent space + hx = sin_theta * cos_phi[np.newaxis, :] + hy = sin_theta * sin_phi[np.newaxis, :] + hz = cos_theta + + # View vector in tangent space (varies per column) + ndotv = ndotv_arr[:, np.newaxis] # shape: (width, 1) + vx = np.sqrt(1.0 - ndotv * ndotv) + vz = ndotv + + # V dot H + vdh = vx * hx + vz * hz + + # Light vector (reflect view around half) + lx = 2.0 * vdh * hx - vx + lz = 2.0 * vdh * hz - vz + + ndotl = np.maximum(lz, 0.0) + ndoth = np.maximum(hz, 0.0) + vdoth = np.maximum(vdh, 0.0) + + # Visibility function (Smith GGX correlated) + vis_v = ndotl * np.sqrt(ndotv * (ndotv - ndotv * rough_m2) + rough_m2) + vis_l = ndotv * np.sqrt(ndotl * (ndotl - ndotl * rough_m2) + rough_m2) + vis = 0.5 / (vis_v + vis_l + 1e-8) + + # Compute contribution + ndotl_vis_pdf = ndotl * vis * (4.0 * vdoth / (ndoth + 1e-8)) + fresnel = (1.0 - vdoth) ** 5 + + # Mask invalid samples + mask = ndotl > 0.0 + scale_contrib = np.where(mask, ndotl_vis_pdf * (1.0 - fresnel), 0.0) + bias_contrib = np.where(mask, ndotl_vis_pdf * fresnel, 0.0) + + # Sum over samples + scale = np.sum(scale_contrib, axis=1) / num_samples + bias = np.sum(bias_contrib, axis=1) / num_samples + + # Filament-compatible layout: + # R = bias (F0-independent term) + # G = scale + bias (reflectance when F0 = 1) + # Used as: lerp(dfg.x, dfg.y, f0) = bias + f0 * scale + lut[yi, :, 0] = bias + lut[yi, :, 1] = scale + bias + + print(f"\rGenerating DFG LUT: {(yi + 1) / height * 100:.1f}%", end="", flush=True) + + print() + # Flip vertically so V=0 (top) is high roughness, V=1 (bottom) is low roughness + return np.flipud(lut) + + +def save_exr(filename, lut): + """Save the DFG LUT as an EXR file.""" + if not HAS_OPENEXR: + raise ImportError("OpenEXR module not available. Install with: pip install OpenEXR") + + height, width = lut.shape[:2] + + header = OpenEXR.Header(width, height) + header['channels'] = { + 'R': Imath.Channel(Imath.PixelType(Imath.PixelType.FLOAT)), + 'G': Imath.Channel(Imath.PixelType(Imath.PixelType.FLOAT)), + } + + r_channel = lut[:, :, 0].astype(np.float32).tobytes() + g_channel = lut[:, :, 1].astype(np.float32).tobytes() + + exr = OpenEXR.OutputFile(filename, header) + exr.writePixels({'R': r_channel, 'G': g_channel}) + exr.close() + + +def save_npy(filename, lut): + """Save the DFG LUT as a numpy file (fallback).""" + np.save(filename, lut) + + +def main(): + import argparse + + parser = argparse.ArgumentParser(description="Generate DFG LUT for PBR rendering") + parser.add_argument("-o", "--output", default="dfg_lut.exr", help="Output filename (default: dfg_lut.exr)") + parser.add_argument("-W", "--width", type=int, default=64, help="LUT width (NdotV axis, default: 64)") + parser.add_argument("-H", "--height", type=int, default=32, help="LUT height (roughness axis, default: 32)") + parser.add_argument("-s", "--samples", type=int, default=512, help="Number of samples per texel (default: 512)") + args = parser.parse_args() + + print(f"Generating {args.width}x{args.height} DFG LUT with {args.samples} samples per texel...") + lut = generate_dfg_lut(args.width, args.height, args.samples) + + output = args.output + if output.endswith(".exr"): + if HAS_OPENEXR: + save_exr(output, lut) + print(f"Saved EXR: {output}") + else: + output = output.replace(".exr", ".npy") + print("Warning: OpenEXR not available. Install with: pip install OpenEXR") + save_npy(output, lut) + print(f"Saved NumPy array instead: {output}") + elif output.endswith(".npy"): + save_npy(output, lut) + print(f"Saved NumPy array: {output}") + else: + save_exr(output, lut) + print(f"Saved: {output}") + + +if __name__ == "__main__": + main() |
