diff options
| -rw-r--r-- | 3ner.shader | 16 | ||||
| -rw-r--r-- | 3ner.shader.meta | 18 | ||||
| -rw-r--r-- | LUTS/dfg_cloth.exr | bin | 0 -> 67752 bytes | |||
| -rw-r--r-- | LUTS/dfg_cloth.exr.meta | 127 | ||||
| -rw-r--r-- | LUTS/dfg_standard.exr | bin | 0 -> 132014 bytes | |||
| -rw-r--r-- | LUTS/dfg_standard.exr.meta | 127 | ||||
| -rw-r--r-- | Scripts/make_dfg_lut.py | 271 | ||||
| -rw-r--r-- | brdf.cginc | 124 | ||||
| -rw-r--r-- | features.cginc | 4 | ||||
| -rw-r--r-- | globals.cginc | 8 | ||||
| -rw-r--r-- | lighting.cginc | 22 | ||||
| -rw-r--r-- | math.cginc | 1 | ||||
| -rw-r--r-- | pbr.cginc | 10 |
13 files changed, 695 insertions, 33 deletions
diff --git a/3ner.shader b/3ner.shader index 1058d59..a3d64f7 100644 --- a/3ner.shader +++ b/3ner.shader @@ -70,9 +70,10 @@ Shader "yum_food/3ner" [HideInInspector] m_start_Rendering_Options("Rendering Options", Float) = 0 [HideInInspector] m_start_BRDF("BRDF", Float) = 0 - _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 + _DFG_LUT("DFG LUT", 2D) = "white" {} + _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 _Clearcoat_Enabled==0 [HideInInspector] m_start_Clearcoat("Clearcoat", Float) = 0 @@ -82,6 +83,15 @@ Shader "yum_food/3ner" [HideInInspector] m_end_Clearcoat("Clearcoat", Float) = 0 //endex + //ifex _Cloth_Sheen_Enabled==0 + [HideInInspector] m_start_Cloth_Sheen("Cloth Sheen", Float) = 0 + [ThryToggle(_CLOTH_SHEEN)] _Cloth_Sheen_Enabled("Enable", Float) = 0 + _Cloth_Sheen_DFG_LUT("Cloth DFG LUT", 2D) = "white" {} + _Cloth_Sheen_Strength("Strength", Range(0, 1)) = 0 + _Cloth_Sheen_Color("Color", Color) = (1, 1, 1, 1) + [HideInInspector] m_end_Cloth_Sheen("Cloth Sheen", 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/3ner.shader.meta b/3ner.shader.meta new file mode 100644 index 0000000..9aba81c --- /dev/null +++ b/3ner.shader.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: b3b9df873cd3aae4f80bfd1c8bb464a3 +ShaderImporter: + externalObjects: {} + defaultTextures: + - _MainTex: {instanceID: 0} + - _BumpMap: {instanceID: 0} + - _MetallicGlossMap: {instanceID: 0} + - _Marble_U_Ramp: {instanceID: 0} + - _Marble_V_Ramp: {instanceID: 0} + - _Marble_W_Ramp: {instanceID: 0} + - _DFG_LUT: {fileID: 2800000, guid: d6fbf383c1bdb87439939bf17f69d539, type: 3} + - _Cloth_Sheen_DFG_LUT: {fileID: 2800000, guid: 59729cfaee66a3c4d847e732c7f99272, + type: 3} + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/LUTS/dfg_cloth.exr b/LUTS/dfg_cloth.exr Binary files differnew file mode 100644 index 0000000..b30a2a7 --- /dev/null +++ b/LUTS/dfg_cloth.exr diff --git a/LUTS/dfg_cloth.exr.meta b/LUTS/dfg_cloth.exr.meta new file mode 100644 index 0000000..2dc43e3 --- /dev/null +++ b/LUTS/dfg_cloth.exr.meta @@ -0,0 +1,127 @@ +fileFormatVersion: 2 +guid: 59729cfaee66a3c4d847e732c7f99272 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + 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: 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: 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 Binary files differnew file mode 100644 index 0000000..016c35d --- /dev/null +++ b/LUTS/dfg_standard.exr diff --git a/LUTS/dfg_standard.exr.meta b/LUTS/dfg_standard.exr.meta new file mode 100644 index 0000000..a0002af --- /dev/null +++ b/LUTS/dfg_standard.exr.meta @@ -0,0 +1,127 @@ +fileFormatVersion: 2 +guid: d6fbf383c1bdb87439939bf17f69d539 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + 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: 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: 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/Scripts/make_dfg_lut.py b/Scripts/make_dfg_lut.py new file mode 100644 index 0000000..b4faf0f --- /dev/null +++ b/Scripts/make_dfg_lut.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 + +import argparse +import math +import numpy as np +import OpenEXR +import Imath +import numba +import random +import concurrent.futures +import os +from functools import partial + + +@numba.njit(cache=True) +def rcp(a): + return 1.0 / a + + +@numba.njit(cache=True) +def lerp(a, b, t): + return a + (b - a) * t + + +@numba.njit(cache=True) +def saturate(a): + if a < 0.0: return 0.0 + if a > 1.0: return 1.0 + return a + + +# Standard BRDF components. +@numba.njit(cache=True) +def F_Schlick(LoH, f0, f90=1.0): + term = 1.0 - LoH + term2 = term * term + term5 = term2 * term2 * term + return f0 + (f90 - f0) * term5 + + +@numba.njit(cache=True) +def D_GGX(roughness, NoH): + r2 = roughness * roughness + NoH2 = NoH * NoH + NoH4 = NoH2 * NoH2 + k = rcp(NoH2) - 1.0 + r2_plus_k = r2 + k + denom = NoH4 * r2_plus_k * r2_plus_k + return r2 / (denom + 1e-6) + + +@numba.njit(cache=True) +def G_GGXSmith(roughness, NoL, NoV): + denom = 2.0 * lerp(2.0 * NoL * NoV, NoL + NoV, roughness) + return rcp(denom + 1e-6) + + +# Cloth BRDF components. +@numba.njit(cache=True) +def D_Cloth(roughness, NoH): + if roughness < 1e-4: return 0.0 + r_rcp = rcp(roughness) + sin2H = 1.0 - NoH * NoH + return (2.0 + r_rcp) * pow(sin2H, r_rcp * 0.5) / (2.0 * math.pi) + + +@numba.njit(cache=True) +def G_Cloth_L(x, a, b, c, d, e): + return a / (1.0 + b * pow(x, c)) + d * x + e + + +@numba.njit(cache=True) +def G_Cloth(roughness, LoH): + 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 + + 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) + 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_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) + + lambda_val = math.exp(2.0 * L_05 - L_LoH) + + # Apply terminator softening (equation 4) + return pow(lambda_val, 1.0 + 2.0 * pow(one_minus_LoH, 8.0)) + + +@numba.njit(cache=True) +def integrate_brdf_jitted(roughness, NoV, brdf_type, num_samples): + V_x = math.sqrt(1.0 - NoV * NoV) + V_y = 0.0 + V_z = NoV + + A, B = 0.0, 0.0 + + for i in range(num_samples): + e1, e2 = random.random(), random.random() + + # Importance sample GGX + a = roughness + a2 = a * a + + phi = 2.0 * math.pi * e1 + cos_theta = math.sqrt((1.0 - e2) / (1.0 + (a2 - 1.0) * e2)) + sin_theta = math.sqrt(1.0 - cos_theta * cos_theta) + + H_x = math.cos(phi) * sin_theta + H_y = math.sin(phi) * sin_theta + H_z = cos_theta + + VoH = H_x * V_x + H_y * V_y + H_z * V_z + if VoH <= 0: continue + + L_x = 2.0 * VoH * H_x - V_x + L_y = 2.0 * VoH * H_y - V_y + L_z = 2.0 * VoH * H_z - V_z + + NoL = saturate(L_z) + NoH = saturate(H_z) + NoV_proxy = saturate(V_z) # NoV is 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) + + # PDF = D_GGX(r, NoH) * NoH / (4 * VoH) + pdf_ggx = D_GGX(roughness, NoH) * NoH / (4.0 * VoH + 1e-6) + + # 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 + + A += scale + B += bias + + return A / num_samples, B / num_samples + + +def calculate_pixel(coords, resolution, brdf_type, num_samples): + x, y = coords + u = (x + 0.5) / resolution + v = (y + 0.5) / resolution + + roughness = saturate(u) + NoV = 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) + + return x, y, r, g, 0.0 + + +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.") + 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) } + + 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) + + processed_count = 0 + total_pixels = len(coords_to_process) + print(f"Starting pixel processing...") + + with concurrent.futures.ProcessPoolExecutor(max_workers=num_workers) as executor: + futures = {executor.submit(worker_func, coord): coord for coord in coords_to_process} + + for future in concurrent.futures.as_completed(futures): + try: + x, y, r, g, b = future.result() + pixel_data[y, x] = (r, g, b) + except Exception as exc: + coord = futures[future] + print(f'\nPixel at {coord} generated an exception: {exc}') + + processed_count += 1 + print(f" ...processed {processed_count}/{total_pixels} pixels ({processed_count/total_pixels:.1%})", end='\r') + + print(f"\nProcessing complete. Writing to {output_filename}...") + try: + # Vertically flip to match UV coordinates (0,0 at bottom-left). + pixel_data = np.flipud(pixel_data) + + exr_file = OpenEXR.OutputFile(output_filename, header) + r_data = pixel_data[:, :, 0].ravel().tobytes() + g_data = pixel_data[:, :, 1].ravel().tobytes() + b_data = pixel_data[:, :, 2].ravel().tobytes() + exr_file.writePixels({'R': r_data, 'G': g_data, 'B': b_data}) + exr_file.close() + print(f"Successfully generated {output_filename}") + except Exception as e: + 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.add_argument('-r', '--resolution', type=int, default=128, + help='Resolution of the square EXR image (default: 128)') + parser.add_argument('-s', '--samples', type=int, default=1024, + help='Number of samples per pixel for integration (default: 1024)') + parser.add_argument('-o', '--output', + help='Output filename (default: dfg_standard.exr or dfg_cloth.exr)') + parser.add_argument('-j', '--workers', type=int, default=os.cpu_count(), + help=f'Number of worker processes (default: {os.cpu_count()})') + + args = parser.parse_args() + + if args.resolution <= 0: + 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) + except Exception as e: + print(f"Error: {e}") + return 1 + + return 0 + +if __name__ == '__main__': + exit(main()) @@ -50,11 +50,70 @@ float D_GGX(float roughness, float NoH) { // denominator of the specular lobe because of some cancellations. // The original, un-optimized equation is: // 2 * NoL * NoV / lerp(2 * NoL * NoV, NoL + NoV, roughness) -float V_GGXSmith(float roughness, float NoL, float NoV) { +float G_GGXSmith(float roughness, float NoL, float NoV) { float denom = 2.0f * lerp(2.0f * NoL * NoV, NoL + NoV, roughness); return rcp(denom); } +// Estevez "Production Friendly Microfacet Sheen BRDF" +// Equation 2. +// The original equation is: +// (2 + 1/r) * sin^(1-r)(theta) / (2 pi) +// Recall that: +// cos^2(theta) + sin^2(theta) = 1 +// So: +// sin^2(theta) = 1 - cos^2(theta) +// sin(theta) = (1 - cos^2(theta)) ^ (1/2) +// sin^k(theta) = (1 - cos^2(theta)) ^ (k/2) +float D_Cloth(float roughness, float NoH) { + float r_rcp = rcp(roughness); + return (2.0f + r_rcp) * pow(1.0f - NoH * NoH, r_rcp * 0.5f) / TAU; +} + +float G_Cloth_L(float x, float a, float b, float c, float d, float e) { + return a / (1.0f + b * pow(x, c)) + d * x + e; +} + +// Estevez "Production Friendly Microfacet Sheen BRDF" +// Equations 3 and 4. +float G_Cloth(float roughness, float LoH) { + // Table 1 + float a0 = 25.3245f; + float a1 = 21.5473f; + float b0 = 3.32435f; + float b1 = 3.82987f; + float c0 = 0.16801f; + float c1 = 0.19823f; + float d0 = -1.27393f; + float d1 = -1.97760f; + float e0 = -4.85967f; + float e1 = -4.32054f; + float one_minus_r = 1.0f - roughness; + float one_minus_r_sq = one_minus_r * one_minus_r; + float one_minus_LoH = 1.0f - LoH; + + float lambda; + [branch] + if (LoH < 0.5f) { + float L0 = G_Cloth_L(LoH, a0, b0, c0, d0, e0); + float L1 = G_Cloth_L(LoH, a1, b1, c1, d1, e1); + float L = lerp(L0, L1, one_minus_r_sq); + lambda = exp(L); + } else { + float L_05_0 = G_Cloth_L(0.5f, a0, b0, c0, d0, e0); + float L_05_1 = G_Cloth_L(0.5f, a1, b1, c1, d1, e1); + float L_05 = lerp(L_05_0, L_05_1, one_minus_r_sq); + + float L_LoH_0 = G_Cloth_L(one_minus_LoH, a0, b0, c0, d0, e0); + float L_LoH_1 = G_Cloth_L(one_minus_LoH, a1, b1, c1, d1, e1); + float L_LoH = lerp(L_LoH_0, L_LoH_1, one_minus_r_sq); + + lambda = exp(2.0f * L_05 - L_LoH); + } + // Apply terminator softening (equation 4). + return pow(lambda, 1.0f + 2.0f * pow(one_minus_LoH, 8)); +} + float4 brdf(Pbr pbr, LightData data) { float3 specular = 0; float3 diffuse = 0; @@ -80,19 +139,31 @@ float4 brdf(Pbr pbr, LightData data) { #if defined(_CLEARCOAT) float cc_f0 = 0.04f; - float Fc = F_Schlick(data.direct.LoH, cc_f0, f90); - float Dc = D_GGX(pbr.cc_roughness, data.direct.NoH_cc); - float Gc = V_GGXSmith(pbr.cc_roughness, data.direct.NoL_cc, data.common.NoV_cc); - float FDGc = Fc * Dc * Gc; - float3 direct_specular_cc = FDGc * data.direct.color * data.direct.NoL_cc * pbr.cc_strength; + float Fcc = F_Schlick(data.direct.LoH, cc_f0, f90); + float Dcc = D_GGX(pbr.cc_roughness, data.direct.NoH_cc); + float Gcc = G_GGXSmith(pbr.cc_roughness, data.direct.NoL_cc, data.common.NoV_cc); + float DFGcc = Fcc * Dcc * Gcc; + float3 direct_specular_cc = DFGcc * data.direct.color * data.direct.NoL_cc * pbr.cc_strength; direct_specular_cc = max(0, direct_specular_cc); specular += direct_specular_cc; - remainder -= Fc * pbr.cc_strength; + remainder -= Fcc * pbr.cc_strength; +#endif + +#if defined(_CLOTH_SHEEN) + float cl_f0 = 0.04f; + float Fcl = 1; + float Dcl = D_Cloth(pbr.roughness, data.direct.NoH); + float Gcl = G_Cloth(pbr.roughness, data.direct.LoH); + float DFGcl = Fcl * Dcl * Gcl; + float3 direct_specular_cl = DFGcl * data.direct.color * pbr.cl_strength * pbr.cl_color * data.direct.NoL; + direct_specular_cl = max(0, direct_specular_cl); + specular += direct_specular_cl; + remainder -= direct_specular_cl; #endif float F = F_Schlick(data.direct.LoH, f0, f90); float D = D_GGX(pbr.roughness, data.direct.NoH); - float G = V_GGXSmith(pbr.roughness, data.direct.NoL, data.common.NoV); + float G = G_GGXSmith(pbr.roughness, data.direct.NoL, data.common.NoV); float FDG = F * D * G; @@ -111,21 +182,39 @@ float4 brdf(Pbr pbr, LightData data) { #if defined(FORWARD_BASE_PASS) if (true) { float remainder = 1.0f; + float2 dfg_uv = float2(data.common.NoV, pbr.roughness); + #if defined(_CLEARCOAT) float cc_f0 = 0.04f; - float Fc = F_Schlick(data.indirect.LoH, cc_f0, f90); - float3 indirect_specular_cc = Fc * data.indirect.specular_cc * pbr.cc_strength; + float Fcc = F_Schlick(data.common.NoV, cc_f0, 1.0f); + float3 indirect_specular_cc = Fcc * data.indirect.specular_cc * pbr.cc_strength; specular += indirect_specular_cc; - remainder -= Fc * pbr.cc_strength; + remainder *= (1.0f - Fcc * pbr.cc_strength); #endif - float F = F_Schlick(data.indirect.LoH, f0, f90); - float3 indirect_specular = F * data.indirect.specular; - specular += indirect_specular; - remainder -= F; +#if defined(_CLOTH_SHEEN) + float DFGcl = _Cloth_Sheen_DFG_LUT.Sample(bilinear_clamp_s, dfg_uv).r; + float3 indirect_specular_cl = DFGcl * data.indirect.specular * pbr.cl_strength * pbr.cl_color; + specular += indirect_specular_cl * remainder; + // Energy conservation for cloth is tricky with IBL. + // A simple approximation is to use the Fresnel of the sheen layer. + float Fcl = F_Schlick(data.common.NoV, 0.04, 1.0); + remainder *= (1.0f - Fcl * pbr.cl_strength); +#endif - float Fd = 1.0f; // Lambertian divide is baked into SH - float3 indirect_diffuse = Fd * remainder * pbr.albedo.xyz * data.indirect.diffuse; + // Standard PBR IBL using split-sum approximation + float2 dfg = _DFG_LUT.Sample(bilinear_clamp_s, dfg_uv).rg; + float3 f0_spec = lerp(0.04f, pbr.albedo.xyz, pbr.metallic); + float3 ibl_specular_reflectance = f0_spec * dfg.x + dfg.y; + float3 indirect_specular = data.indirect.specular * ibl_specular_reflectance; + specular += indirect_specular * remainder; + + // For energy conservation with the diffuse term, we use the view-dependent Fresnel. + float3 F = F_Schlick(data.common.NoV, f0_spec, 1.0f); + remainder *= (1.0f - F); + + // Diffuse is Lambertian, which is pre-integrated into the SH diffuse probe + float3 indirect_diffuse = pbr.albedo.xyz * data.indirect.diffuse * remainder * (1.0 - pbr.metallic); diffuse += indirect_diffuse; } #endif @@ -134,4 +223,3 @@ float4 brdf(Pbr pbr, LightData data) { } #endif // __BRDF_INC - diff --git a/features.cginc b/features.cginc index e6de1c1..705939c 100644 --- a/features.cginc +++ b/features.cginc @@ -19,4 +19,8 @@ #pragma shader_feature_local _CLEARCOAT //endex +//ifex _Cloth_Sheen_Enabled==0 +#pragma shader_feature_local _CLOTH_SHEEN +//endex + #endif // __FEATURES_INC diff --git a/globals.cginc b/globals.cginc index 12e86c8..3ccaac4 100644 --- a/globals.cginc +++ b/globals.cginc @@ -7,6 +7,7 @@ SamplerState point_repeat_s; SamplerState linear_repeat_s; SamplerState bilinear_repeat_s; SamplerState linear_clamp_s; +SamplerState bilinear_clamp_s; SamplerState trilinear_repeat_s; int _Mode; // opaque, cutout, transparent, etc. @@ -25,6 +26,7 @@ float4 _MetallicGlossMap_ST; float _Glossiness; float _Metallic; +texture2D _DFG_LUT; float _Specular_AA_Variance; float _Specular_AA_Threshold; float _BRDF_Specular_Min_Denom; @@ -49,4 +51,10 @@ float _Clearcoat_Strength; float _Clearcoat_Roughness; #endif // _CLEARCOAT +#if defined(_CLOTH_SHEEN) +float _Cloth_Sheen_Strength; +float3 _Cloth_Sheen_Color; +texture2D _Cloth_Sheen_DFG_LUT; +#endif // _CLOTH_SHEEN + #endif // __GLOBALS_INC diff --git a/lighting.cginc b/lighting.cginc index 5055798..48d95a1 100644 --- a/lighting.cginc +++ b/lighting.cginc @@ -99,13 +99,13 @@ float3 getIndirectSpecular(v2f i, float roughness, float3 view_dir, float3 refle float3 yumSH9(float4 n, float3 worldPos, inout LightIndirect light) { [branch] - if (_UdonLightVolumeEnabled) { - LightVolumeSH(worldPos, light.L00, light.L01r, light.L01g, light.L01b); - return light.L00 + float3( - dot(light.L01r, n.xyz), - dot(light.L01g, n.xyz), - dot(light.L01b, n.xyz)); - } + if (_UdonLightVolumeEnabled) { + LightVolumeSH(worldPos, light.L00, light.L01r, light.L01g, light.L01b); + return light.L00 + float3( + dot(light.L01r, n.xyz), + dot(light.L01g, n.xyz), + dot(light.L01b, n.xyz)); + } // No light volumes - use SH9 // Unity gives us the first three bands (L0-L2) of SH coefficients as follows: @@ -175,7 +175,7 @@ float4 getIndirectDiffuse(v2f i, Pbr pbr, inout LightIndirect light) { void GetLighting(v2f i, Pbr pbr, out LightData data) { data = (LightData) 0; - + float3 view_dir = normalize(i.eyeVec.xyz); data.common.V = -view_dir; @@ -184,7 +184,7 @@ void GetLighting(v2f i, Pbr pbr, out LightData data) { #if defined(_CLEARCOAT) data.common.NoV_cc = saturate(dot(i.normal, data.common.V)); #endif - + // Direct lighting data.direct.dir = getDirectLightDirection(i); data.direct.H = normalize(data.common.V + data.direct.dir); @@ -198,10 +198,10 @@ void GetLighting(v2f i, Pbr pbr, out LightData data) { float direct_LoV = dot(data.direct.dir, data.common.V); data.direct.LoV = saturate(direct_LoV); data.direct.double_LoV = saturate(2.0f * direct_LoV * direct_LoV - 1.0f); - + float4 lightColorIntensity = getDirectLightColorIntensity(); data.direct.color = lightColorIntensity.rgb * lightColorIntensity.w; - + // Indirect lighting data.indirect.dir = -reflect(data.common.V, pbr.normal); data.indirect.H = normalize(data.common.V + data.indirect.dir); @@ -3,6 +3,7 @@ #define PI 3.14159265358979f #define RCP_PI (1.0f / PI) +#define TAU (2.0f * PI) float sin_noise_3d(float3 uvw) { return sin(uvw[0]) * sin(uvw[1]) * sin(uvw[2]); @@ -17,10 +17,14 @@ struct Pbr { float cc_roughness; float cc_strength; #endif +#if defined(_CLOTH_SHEEN) + float cl_strength; + float3 cl_color; +#endif }; #define MIN_PERCEPTUAL_ROUGHNESS 5e-2f -#define MIN_ROUGHNESS 2e-3f +#define MIN_ROUGHNESS 5e-3f // TODO consider normal filtering like filamented void propagateSmoothness(inout Pbr pbr) { @@ -67,6 +71,10 @@ Pbr getPbr(v2f i) { pbr.cc_roughness = _Clearcoat_Roughness; pbr.cc_strength = _Clearcoat_Strength; #endif +#if defined(_CLOTH_SHEEN) + pbr.cl_strength = _Cloth_Sheen_Strength; + pbr.cl_color = _Cloth_Sheen_Color; +#endif propagateSmoothness(pbr); return pbr; |
