summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--LUTS/bpe_lut.pngbin0 -> 598482 bytes
-rw-r--r--LUTS/dfg-ms.exrbin34428 -> 0 bytes
-rw-r--r--LUTS/dfg-ms.exr.meta127
-rw-r--r--LUTS/dfg-s.exrbin35458 -> 0 bytes
-rw-r--r--LUTS/dfg-s.exr.meta116
-rw-r--r--LUTS/dfg_lut.exrbin0 -> 1839260 bytes
-rw-r--r--Scripts/generate_dfg_lut.py190
7 files changed, 190 insertions, 243 deletions
diff --git a/LUTS/bpe_lut.png b/LUTS/bpe_lut.png
new file mode 100644
index 0000000..740b1e1
--- /dev/null
+++ b/LUTS/bpe_lut.png
Binary files differ
diff --git a/LUTS/dfg-ms.exr b/LUTS/dfg-ms.exr
deleted file mode 100644
index 31e8c9d..0000000
--- a/LUTS/dfg-ms.exr
+++ /dev/null
Binary files differ
diff --git a/LUTS/dfg-ms.exr.meta b/LUTS/dfg-ms.exr.meta
deleted file mode 100644
index 8fc4adb..0000000
--- a/LUTS/dfg-ms.exr.meta
+++ /dev/null
@@ -1,127 +0,0 @@
-fileFormatVersion: 2
-guid: 53122c95e82e994448dce5a9c8346223
-TextureImporter:
- internalIDToNameTable: []
- externalObjects: {}
- serializedVersion: 12
- mipmaps:
- mipMapMode: 0
- enableMipMap: 0
- sRGBTexture: 0
- 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: 0
- 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: 1
- wrapV: 1
- wrapW: 1
- 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: 0
- alphaIsTransparency: 0
- spriteTessellationDetail: -1
- textureType: 0
- textureShape: 1
- singleChannelComponent: 0
- flipbookRows: 1
- flipbookColumns: 1
- maxTextureSizeSet: 0
- compressionQualitySet: 0
- textureFormatSet: 0
- ignorePngGamma: 0
- applyGammaDecoding: 1
- swizzle: 50462976
- cookieLightType: 1
- platformSettings:
- - serializedVersion: 3
- buildTarget: DefaultTexturePlatform
- maxTextureSize: 128
- resizeAlgorithm: 1
- textureFormat: -1
- textureCompression: 0
- compressionQuality: 50
- crunchedCompression: 0
- allowsAlphaSplitting: 0
- overridden: 0
- ignorePlatformSupport: 0
- androidETC2FallbackOverride: 0
- forceMaximumCompressionQuality_BC6H_BC7: 0
- - serializedVersion: 3
- buildTarget: Standalone
- maxTextureSize: 128
- resizeAlgorithm: 1
- textureFormat: -1
- textureCompression: 0
- compressionQuality: 50
- crunchedCompression: 0
- allowsAlphaSplitting: 0
- overridden: 0
- ignorePlatformSupport: 0
- androidETC2FallbackOverride: 0
- forceMaximumCompressionQuality_BC6H_BC7: 0
- - serializedVersion: 3
- buildTarget: Android
- maxTextureSize: 128
- resizeAlgorithm: 1
- textureFormat: -1
- textureCompression: 0
- 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-s.exr b/LUTS/dfg-s.exr
deleted file mode 100644
index 1048da9..0000000
--- a/LUTS/dfg-s.exr
+++ /dev/null
Binary files differ
diff --git a/LUTS/dfg-s.exr.meta b/LUTS/dfg-s.exr.meta
deleted file mode 100644
index 5360498..0000000
--- a/LUTS/dfg-s.exr.meta
+++ /dev/null
@@ -1,116 +0,0 @@
-fileFormatVersion: 2
-guid: 302270a7d573a6f4287fc9c50a70d26a
-TextureImporter:
- internalIDToNameTable: []
- externalObjects: {}
- serializedVersion: 11
- mipmaps:
- mipMapMode: 0
- enableMipMap: 0
- sRGBTexture: 0
- 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
- isReadable: 0
- streamingMipmaps: 0
- streamingMipmapsPriority: 0
- grayScaleToAlpha: 0
- generateCubemap: 6
- cubemapConvolution: 0
- seamlessCubemap: 0
- textureFormat: 1
- maxTextureSize: 2048
- textureSettings:
- serializedVersion: 2
- filterMode: 2
- aniso: 1
- mipBias: 0
- wrapU: 1
- wrapV: 1
- wrapW: 1
- 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
- maxTextureSizeSet: 0
- compressionQualitySet: 0
- textureFormatSet: 0
- applyGammaDecoding: 1
- platformSettings:
- - serializedVersion: 3
- buildTarget: DefaultTexturePlatform
- maxTextureSize: 2048
- resizeAlgorithm: 0
- textureFormat: -1
- textureCompression: 0
- compressionQuality: 50
- crunchedCompression: 0
- allowsAlphaSplitting: 0
- overridden: 0
- androidETC2FallbackOverride: 0
- forceMaximumCompressionQuality_BC6H_BC7: 0
- - serializedVersion: 3
- buildTarget: Standalone
- maxTextureSize: 2048
- resizeAlgorithm: 0
- textureFormat: -1
- textureCompression: 0
- compressionQuality: 50
- crunchedCompression: 0
- allowsAlphaSplitting: 0
- overridden: 0
- androidETC2FallbackOverride: 0
- forceMaximumCompressionQuality_BC6H_BC7: 0
- - serializedVersion: 3
- buildTarget: Android
- maxTextureSize: 2048
- resizeAlgorithm: 0
- textureFormat: -1
- textureCompression: 0
- compressionQuality: 50
- crunchedCompression: 0
- allowsAlphaSplitting: 0
- overridden: 0
- androidETC2FallbackOverride: 0
- forceMaximumCompressionQuality_BC6H_BC7: 0
- spriteSheet:
- serializedVersion: 2
- sprites: []
- outline: []
- physicsShape: []
- bones: []
- spriteID:
- internalID: 0
- vertices: []
- indices:
- edges: []
- weights: []
- secondaryTextures: []
- spritePackingTag:
- pSDRemoveMatte: 0
- pSDShowRemoveMatteOption: 0
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/LUTS/dfg_lut.exr b/LUTS/dfg_lut.exr
new file mode 100644
index 0000000..3086d07
--- /dev/null
+++ b/LUTS/dfg_lut.exr
Binary files differ
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()