summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2026-02-23 22:44:17 -0800
committeryum <yum.food.vr@gmail.com>2026-02-23 22:44:17 -0800
commitede8bbae3496315e5ef821d4c00cf04022a3d453 (patch)
tree288770e1ce30fb71efb3744aec289108362c91a2
parent87e0f98e6d4e2774c08763c94aa21ad68b0b13f9 (diff)
Implement bent normals
-rwxr-xr-xfilamented.cginc35
-rwxr-xr-xlighting.cginc18
-rwxr-xr-xpbr.cginc16
3 files changed, 68 insertions, 1 deletions
diff --git a/filamented.cginc b/filamented.cginc
index 62ec4c3..6d34123 100755
--- a/filamented.cginc
+++ b/filamented.cginc
@@ -301,4 +301,39 @@ inline half3 UnityGI_prefilteredRadiance(const UnityGIInput data, const float pe
return specular;
}
+#if defined(_AMBIENT_OCCLUSION) || defined(_BENT_NORMALS)
+// Oat and Sander 2007, "Ambient Aperture Lighting"
+// Jimenez et al. 2016, "Practical Realtime Strategies for Accurate Indirect Occlusion"
+float sphericalCapsIntersection(float cosCap1, float cosCap2, float cosDistance) {
+ float r1 = acos(cosCap1);
+ float r2 = acos(cosCap2);
+ float d = acos(cosDistance);
+
+ if (min(r1, r2) <= max(r1, r2) - d) {
+ return 1.0 - max(cosCap1, cosCap2);
+ } else if (r1 + r2 <= d) {
+ return 0.0;
+ }
+
+ float delta = abs(r1 - r2);
+ float x = 1.0 - saturate((d - delta) / max(r1 + r2 - delta, 1e-4));
+ float x2 = x * x;
+ float area = -2.0 * x2 * x + 3.0 * x2;
+ return area * (1.0 - max(cosCap1, cosCap2));
+}
+
+float computeSpecularAO(float NoV, float visibility, float roughness, float3 bent_normal, float3 reflect_dir) {
+#if defined(_BENT_NORMALS)
+ // Jimenez et al. 2016, "Practical Realtime Strategies for Accurate Indirect Occlusion"
+ float cosAv = sqrt(1.0 - visibility); // aperture from AO
+ float cosAs = exp2(-3.321928 * roughness * roughness); // aperture from roughness; log(10)/log(2) = 3.321928
+ float cosB = dot(bent_normal, reflect_dir); // angle between bent normal and reflection
+ return sphericalCapsIntersection(cosAv, cosAs, saturate(0.5 * cosB + 0.5)) / (1.0 - cosAs);
+#else
+ // Lagarde and de Rousiers 2014, "Moving Frostbite to Physically Based Rendering"
+ return saturate(pow(NoV + visibility, exp2(-16.0 * roughness - 1.0)) - 1.0 + visibility);
+#endif
+}
+#endif // _AMBIENT_OCCLUSION || _BENT_NORMALS
+
#endif // __FILAMENTED_INC
diff --git a/lighting.cginc b/lighting.cginc
index d5e0be8..e56bb37 100755
--- a/lighting.cginc
+++ b/lighting.cginc
@@ -174,11 +174,15 @@ float3 yumSH9(float4 n, float3 worldPos, inout LightIndirect light) {
float4 getIndirectDiffuse(v2f i, Pbr pbr, inout LightIndirect light) {
float4 diffuse = 0;
#if defined(FORWARD_BASE_PASS)
+#if defined(_BENT_NORMALS)
+ diffuse.xyz += max(0, yumSH9(float4(pbr.bent_normal, 1.0), i.worldPos, light));
+#else
diffuse.xyz += max(0, yumSH9(float4(i.normal, 1.0), i.worldPos, light));
#endif
+#endif
#if defined(_AMBIENT_OCCLUSION)
- diffuse.xyz *= saturate(lerp(1, _OcclusionMap.Sample(bilinear_repeat_s, i.uv01.xy), _OcclusionStrength));
+ diffuse.xyz *= pbr.ao;
#endif
return diffuse;
@@ -233,6 +237,18 @@ void GetLighting(v2f i, Pbr pbr, out LightData data) {
data.indirect.diffuse = getIndirectDiffuse(i, pbr, data.indirect);
data.indirect.specular = getIndirectSpecular(i, pbr.roughness_perceptual, view_dir, -data.indirect.dir);
+#if defined(_AMBIENT_OCCLUSION) || defined(_BENT_NORMALS)
+ float ao_vis = 1.0;
+#if defined(_AMBIENT_OCCLUSION)
+ ao_vis = pbr.ao;
+#endif
+#if defined(_BENT_NORMALS)
+ float3 spec_ao_bent = pbr.bent_normal;
+#else
+ float3 spec_ao_bent = pbr.normal;
+#endif
+ data.indirect.specular *= computeSpecularAO(data.common.NoV, ao_vis, pbr.roughness, spec_ao_bent, -data.indirect.dir);
+#endif
#if defined(_CLEARCOAT)
data.indirect.specular_cc = getIndirectSpecular(i, saturate(sqrt(pbr.cc_roughness)), view_dir, dir_cc);
#if defined(_CLEARCOAT_MASK)
diff --git a/pbr.cginc b/pbr.cginc
index 25d6ece..5e19089 100755
--- a/pbr.cginc
+++ b/pbr.cginc
@@ -16,6 +16,12 @@ struct Pbr {
float roughness_perceptual;
float roughness;
float metallic;
+#if defined(_AMBIENT_OCCLUSION)
+ float ao;
+#endif
+#if defined(_BENT_NORMALS)
+ float3 bent_normal;
+#endif
#if defined(_CLEARCOAT)
float cc_roughness;
float cc_strength;
@@ -208,6 +214,12 @@ Pbr getPbr(v2f i) {
pbr.normal = normalize(mul(normal_tangent, pbr.tbn));
+#if defined(_BENT_NORMALS)
+ float3 bent_ts = UnpackNormal(_Bent_Normals_Map.Sample(aniso4_trilinear_repeat_s, uv_parallax * _Bent_Normals_Map_ST.xy + _Bent_Normals_Map_ST.zw));
+ bent_ts.xy *= _Bent_Normals_Strength;
+ pbr.bent_normal = normalize(mul(bent_ts, pbr.tbn));
+#endif
+
float4 metallic_gloss = _MetallicGlossMap.Sample(aniso4_trilinear_repeat_s, uv_parallax * _MetallicGlossMap_ST.xy);
pbr.smoothness = metallic_gloss.a * _Glossiness;
pbr.metallic = metallic_gloss.r * _Metallic;
@@ -219,6 +231,10 @@ Pbr getPbr(v2f i) {
#endif
propagateSmoothness(pbr);
+#if defined(_AMBIENT_OCCLUSION)
+ pbr.ao = saturate(lerp(1.0, _OcclusionMap.Sample(bilinear_repeat_s, i.uv01.xy).r, _OcclusionStrength));
+#endif
+
return pbr;
}