summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--3ner.shader16
-rw-r--r--3ner.shader.meta18
-rw-r--r--LUTS/dfg_cloth.exrbin0 -> 67752 bytes
-rw-r--r--LUTS/dfg_cloth.exr.meta127
-rw-r--r--LUTS/dfg_standard.exrbin0 -> 132014 bytes
-rw-r--r--LUTS/dfg_standard.exr.meta127
-rw-r--r--Scripts/make_dfg_lut.py271
-rw-r--r--brdf.cginc124
-rw-r--r--features.cginc4
-rw-r--r--globals.cginc8
-rw-r--r--lighting.cginc22
-rw-r--r--math.cginc1
-rw-r--r--pbr.cginc10
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
new file mode 100644
index 0000000..b30a2a7
--- /dev/null
+++ b/LUTS/dfg_cloth.exr
Binary files differ
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
new file mode 100644
index 0000000..016c35d
--- /dev/null
+++ b/LUTS/dfg_standard.exr
Binary files differ
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())
diff --git a/brdf.cginc b/brdf.cginc
index c2ebc22..12f2770 100644
--- a/brdf.cginc
+++ b/brdf.cginc
@@ -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);
diff --git a/math.cginc b/math.cginc
index 0d59121..af71fc6 100644
--- a/math.cginc
+++ b/math.cginc
@@ -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]);
diff --git a/pbr.cginc b/pbr.cginc
index 746de9c..806a52f 100644
--- a/pbr.cginc
+++ b/pbr.cginc
@@ -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;