yum-archive/2ner
A toon shader for Unity's BIRP.
git clone https://git.yummers.dev/yum-archive/2ner
6aea3d8
master
1#!/usr/bin/env python3 2# /// script 3# requires-python = ">=3.9" 4# dependencies = [ 5# "numpy", 6# "openexr", 7# ] 8# /// 9""" 10Generate a DFG LUT (Look-Up Table) for PBR split-sum approximation. 11 12This computes the pre-integrated BRDF for the GGX microfacet model, 13storing scale and bias factors for the Fresnel term. 14 15Output: DFG LUT as an EXR file with RG channels (scale, bias). 16""" 17 18import numpy as np 19 20try : 21import OpenEXR 22import Imath 23HAS_OPENEXR = True 24except ImportError : 25HAS_OPENEXR = False 26 27 28def generate_hammersley_sequence (n ): 29"""Pre-compute Hammersley 2D sequence for n samples.""" 30i = np .arange (n ,dtype = np .uint32 ) 31 32# Reverse bits for radical inverse 33v = i .copy () 34v = ((v >> 1 )& 0x55555555 )| ((v & 0x55555555 )<< 1 ) 35v = ((v >> 2 )& 0x33333333 )| ((v & 0x33333333 )<< 2 ) 36v = ((v >> 4 )& 0x0F0F0F0F )| ((v & 0x0F0F0F0F )<< 4 ) 37v = ((v >> 8 )& 0x00FF00FF )| ((v & 0x00FF00FF )<< 8 ) 38v = (v >> 16 )| (v << 16 ) 39 40e1 = i .astype (np .float32 )/ n 41e2 = v .astype (np .float64 )/ 0x100000000 42 43return e1 ,e2 .astype (np .float32 ) 44 45 46def generate_dfg_lut (width = 64 ,height = 32 ,num_samples = 512 ): 47""" 48Generate the full DFG LUT (vectorized). 49 50Compatible with HLSL: float2 dfg_uv = float2(NoV, roughness); 51X axis (U): NdotV (0 to 1) 52Y axis (V): roughness (0 to 1) 53""" 54# Pre-compute Hammersley sequence 55e1 ,e2 = generate_hammersley_sequence (num_samples ) 56phi = 2.0 * np .pi * e1 57cos_phi = np .cos (phi ) 58sin_phi = np .sin (phi ) 59 60# Create coordinate grids matching HLSL UV layout 61x = np .arange (width ,dtype = np .float32 ) 62y = np .arange (height ,dtype = np .float32 ) 63ndotv_arr = (x + 0.5 )/ width # shape: (width,) - U axis 64roughness = (y + 0.5 )/ height # shape: (height,) - V axis 65 66# Pre-compute roughness terms 67m = roughness * roughness 68m2 = m * m # shape: (height,) 69 70lut = np .zeros ((height ,width ,2 ),dtype = np .float32 ) 71 72for yi , (rough ,rough_m2 )in enumerate (zip (roughness ,m2 )): 73# GGX importance sampling - vectorized over samples and NdotV 74# cos_theta shape: (width, num_samples) 75denom = 1.0 + (rough_m2 - 1.0 )* e2 [np .newaxis , :] 76cos_theta = np .sqrt ((1.0 - e2 [np .newaxis , :])/ denom ) 77sin_theta = np .sqrt (1.0 - cos_theta * cos_theta ) 78 79# Half vector in tangent space 80hx = sin_theta * cos_phi [np .newaxis , :] 81hy = sin_theta * sin_phi [np .newaxis , :] 82hz = cos_theta 83 84# View vector in tangent space (varies per column) 85ndotv = ndotv_arr [:,np .newaxis ]# shape: (width, 1) 86vx = np .sqrt (1.0 - ndotv * ndotv ) 87vz = ndotv 88 89# V dot H 90vdh = vx * hx + vz * hz 91 92# Light vector (reflect view around half) 93lx = 2.0 * vdh * hx - vx 94lz = 2.0 * vdh * hz - vz 95 96ndotl = np .maximum (lz ,0.0 ) 97ndoth = np .maximum (hz ,0.0 ) 98vdoth = np .maximum (vdh ,0.0 ) 99 100# Visibility function (Smith GGX correlated) 101vis_v = ndotl * np .sqrt (ndotv * (ndotv - ndotv * rough_m2 )+ rough_m2 ) 102vis_l = ndotv * np .sqrt (ndotl * (ndotl - ndotl * rough_m2 )+ rough_m2 ) 103vis = 0.5 / (vis_v + vis_l + 1e-8 ) 104 105# Compute contribution 106ndotl_vis_pdf = ndotl * vis * (4.0 * vdoth / (ndoth + 1e-8 )) 107fresnel = (1.0 - vdoth )** 5 108 109# Mask invalid samples 110mask = ndotl > 0.0 111scale_contrib = np .where (mask ,ndotl_vis_pdf * (1.0 - fresnel ),0.0 ) 112bias_contrib = np .where (mask ,ndotl_vis_pdf * fresnel ,0.0 ) 113 114# Sum over samples 115scale = np .sum (scale_contrib ,axis = 1 )/ num_samples 116bias = np .sum (bias_contrib ,axis = 1 )/ num_samples 117 118# Filament-compatible layout: 119# R = bias (F0-independent term) 120# G = scale + bias (reflectance when F0 = 1) 121# Used as: lerp(dfg.x, dfg.y, f0) = bias + f0 * scale 122lut [yi , :,0 ]= bias 123lut [yi , :,1 ]= scale + bias 124 125f"\rGenerating DFG LUT: { ( yi + 1 ) / height * 100 :.1f } %" ,end = "" ,flush = True ) 126 127# Flip vertically so V=0 (top) is high roughness, V=1 (bottom) is low roughness 129return np .flipud (lut ) 130 131 132def save_exr (filename ,lut ): 133"""Save the DFG LUT as an EXR file.""" 134if not HAS_OPENEXR : 135raise ImportError ("OpenEXR module not available. Install with: pip install OpenEXR" ) 136 137height ,width = lut .shape [:2 ] 138 139header = OpenEXR .Header (width ,height ) 140header ['channels' ]= { 141'R' :Imath .Channel (Imath .PixelType (Imath .PixelType .FLOAT )), 142'G' :Imath .Channel (Imath .PixelType (Imath .PixelType .FLOAT )), 143 } 144 145r_channel = lut [:, :,0 ].astype (np .float32 ).tobytes () 146g_channel = lut [:, :,1 ].astype (np .float32 ).tobytes () 147 148exr = OpenEXR .OutputFile (filename ,header ) 149exr .writePixels ({'R' :r_channel ,'G' :g_channel }) 150exr .close () 151 152 153def save_npy (filename ,lut ): 154"""Save the DFG LUT as a numpy file (fallback).""" 155np .save (filename ,lut ) 156 157 158def main (): 159import argparse 160 161parser = argparse .ArgumentParser (description = "Generate DFG LUT for PBR rendering" ) 162parser .add_argument ("-o" ,"--output" ,default = "dfg_lut.exr" ,help = "Output filename (default: dfg_lut.exr)" ) 163parser .add_argument ("-W" ,"--width" ,type = int ,default = 64 ,help = "LUT width (NdotV axis, default: 64)" ) 164parser .add_argument ("-H" ,"--height" ,type = int ,default = 32 ,help = "LUT height (roughness axis, default: 32)" ) 165parser .add_argument ("-s" ,"--samples" ,type = int ,default = 512 ,help = "Number of samples per texel (default: 512)" ) 166args = parser .parse_args () 167 168f"Generating { args . width } x { args . height } DFG LUT with { args . samples } samples per texel..." ) 169lut = generate_dfg_lut (args .width ,args .height ,args .samples ) 170 171output = args .output 172if output .endswith (".exr" ): 173if HAS_OPENEXR : 174save_exr (output ,lut ) 175f"Saved EXR: { output } " ) 176else : 177output = output .replace (".exr" ,".npy" ) 178"Warning: OpenEXR not available. Install with: pip install OpenEXR" ) 179save_npy (output ,lut ) 180f"Saved NumPy array instead: { output } " ) 181elif output .endswith (".npy" ): 182save_npy (output ,lut ) 183f"Saved NumPy array: { output } " ) 184else : 185save_exr (output ,lut ) 186f"Saved: { output } " ) 187 188 189if __name__ == "__main__" : 190main ()