From b5bd1fdb8b73254bd81c581fa296295d0ae0524b Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Mon, 29 Jun 2026 20:12:31 +0200 Subject: [PATCH] Rework the camo shader to flatten toward darkness, drop FormSoftness Rebuild the camo controls around hiding a painted player in a dark scene. _HalfLambert is now a two-segment curve (0 = no effect / URP Lit, 0.5 = classic half-lambert soft terminator, 1 = fully flat) and a companion _HalfLambertBrightness (-1..1) additively shifts the flattened diffuse so the body tone matches the scene. _SpecularDampen still scales down the direct highlight. The view-coupled _FormSoftness normal-bend is removed entirely (it flipped light/dark with the camera); all knobs are now term-based and view-independent, and stay byte-identical to URP/Lit at their no-op values. Co-Authored-By: Claude Opus 4.8 --- Assets/Shaders/PlayerCamo.hlsl | 71 +++++++++++++++++------------ Assets/Shaders/PlayerCamoLit.shader | 8 ++-- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/Assets/Shaders/PlayerCamo.hlsl b/Assets/Shaders/PlayerCamo.hlsl index 2bf6bc9..d076de5 100644 --- a/Assets/Shaders/PlayerCamo.hlsl +++ b/Assets/Shaders/PlayerCamo.hlsl @@ -1,37 +1,37 @@ #ifndef PLAYER_CAMO_INCLUDED #define PLAYER_CAMO_INCLUDED -// Camouflage lighting for the player avatar. Goal: keep the player lit by the scene (so its -// brightness matches the surroundings) but de-emphasise the 3D FORM, so a correctly painted -// avatar is harder to spot. Three independent, tunable knobs - all 0 => byte-identical URP/Lit: +// Camouflage lighting for the player avatar. Goal: keep the player painted to match its +// surroundings while de-emphasising the 3D FORM, so a correctly painted avatar is harder to spot. +// Knobs - _HalfLambert at 0 and _HalfLambertBrightness at 0 => byte-identical URP/Lit: // -// _FormSoftness (0..1) : bends the per-pixel normal toward the view direction before shading. -// Flattens diffuse + specular + reflections together -> the silhouette -// shading that reveals curvature softens from every light angle. -// _HalfLambert (0..1) : wraps the DIRECT diffuse term, saturate(N.L) -> N.L*0.5+0.5, lifting -// the shadow side (classic half-Lambert). Specular stays clamped. -// _SpecularDampen (0..1) : scales down the DIRECT specular highlight (a strong shape cue). +// _HalfLambert (0..1): form-flattening diffuse over two segments - +// 0 -> saturate(N.L) plain Lambert (no-op) +// 0.5 -> N.L * 0.5 + 0.5 classic half-Lambert (soft terminator, +// no hard, form-revealing shadow line) +// 1 -> 1 fully FLAT: the direct diffuse no longer +// reveals the 3D form. View-INDEPENDENT +// (operates on the shading term, not a view-/light-bent normal, so +// there is no bright/dark flip when orbiting). The DIRECT specular +// highlight (a strong shape cue) is faded out with it, gone at 1. +// _HalfLambertBrightness (-1..1): additive shift of the (flattened) diffuse term, default 0. Tunes +// the body brighter/darker to match the scene tone (e.g. flatten to +// 1, then dial this down to sink into a dark scene). Clamped at 0. +// _SpecularDampen (0..1): scales down the DIRECT specular highlight (a strong shape cue) on +// top of the _HalfLambert fade. // // Implementation note: URP's LightingPhysicallyBased() hard-codes saturate(N.L) inline with no -// override hook, so a faithful half-Lambert needs the forward light loop reimplemented. This file -// is a 1:1 copy of URP 17.3 UniversalFragmentPBR that delegates ALL the fragile parts (GI / APV / -// shadow mask / AO / clustered light loop / final color) to URP's own helpers and only swaps the -// per-light shading call for the camo variant below. +// override hook, so the dark-converging half-Lambert needs the forward light loop reimplemented. +// This file is a 1:1 copy of URP 17.3 UniversalFragmentPBR that delegates ALL the fragile parts +// (GI / APV / shadow mask / AO / clustered light loop / final color) to URP's own helpers and only +// swaps the per-light shading call for the camo variant below. // // Declared OUTSIDE UnityPerMaterial (LitInput.hlsl owns that CBUFFER). The player is a handful of // instances, so the resulting SRP-batcher break is irrelevant. -half _FormSoftness; half _HalfLambert; +half _HalfLambertBrightness; half _SpecularDampen; -// Bend the shading normal toward the camera. n and viewDirWS are unit; for a visible (front-facing) -// fragment N.V > 0, so the lerp never collapses to zero - SafeNormalize guards the rare grazing case. -half3 SoftenNormalCamo(half3 normalWS, half3 viewDirWS) -{ - half3 n = lerp(normalWS, viewDirWS, _FormSoftness); - return half3(SafeNormalize(n)); -} - // Per-light shading: copy of URP's LightingPhysicallyBased with the diffuse term wrapped // (half-Lambert) and the specular term dampened. Diffuse and specular use SEPARATE radiance so the // wrap lifts only the diffuse shadow side and never smears specular onto back faces. @@ -42,10 +42,20 @@ half3 LightingPhysicallyBasedCamo(BRDFData brdfData, BRDFData brdfDataClearCoat, { half NdotL = dot(normalWS, lightDirectionWS); half ndlSat = saturate(NdotL); - half diffuseTerm = lerp(ndlSat, NdotL * 0.5h + 0.5h, _HalfLambert); + // Form-flattening diffuse driven by _HalfLambert over two segments: + // 0 -> saturate(N.L) plain Lambert (no-op, byte-identical URP/Lit) + // 0.5 -> N.L * 0.5 + 0.5 classic half-Lambert (soft terminator, no hard shadow line) + // 1 -> 1 fully flat: the direct diffuse no longer reveals the 3D form. + // _HalfLambertBrightness then shifts the whole (flattened) term up/down to tune the body tone; + // clamped at 0 so it never produces negative light. + half classicHL = NdotL * 0.5h + 0.5h; // already in [0,1] for N.L in [-1,1] + half t1 = saturate(_HalfLambert * 2.0h); // 0..1 across _HalfLambert in [0, 0.5] + half t2 = saturate(_HalfLambert * 2.0h - 1.0h); // 0..1 across _HalfLambert in [0.5, 1] + half diffuseTerm = lerp(lerp(ndlSat, classicHL, t1), 1.0h, t2); + diffuseTerm = max(0.0h, diffuseTerm + _HalfLambertBrightness); half3 atten = lightColor * lightAttenuation; - half3 diffuseRadiance = atten * diffuseTerm; // wrapped (half-Lambert) + half3 diffuseRadiance = atten * diffuseTerm; // flattened + brightness-shifted half3 specularRadiance = atten * ndlSat; // clamped (no back-face specular) half3 color = brdfData.diffuse * diffuseRadiance; @@ -63,7 +73,9 @@ half3 LightingPhysicallyBasedCamo(BRDFData brdfData, BRDFData brdfDataClearCoat, spec = spec * (1.0 - clearCoatMask * coatFresnel) + (brdfCoat * clearCoatMask).xxx; #endif - spec *= (1.0h - _SpecularDampen); + // Fade the bright, form-revealing highlight out with _HalfLambert (gone at 1, so no highlight + // survives at full camo), on top of the explicit _SpecularDampen. + spec *= (1.0h - _SpecularDampen) * (1.0h - _HalfLambert); color += spec * specularRadiance; } #endif @@ -170,8 +182,8 @@ half4 UniversalFragmentCamo(InputData inputData, SurfaceData surfaceData) #endif } -// Replicates URP's LitPassFragment body and inserts the normal-soften step before GI + lighting, -// so GI (probe) direction, direct lighting and reflections all use the flattened normal. +// Replicates URP's LitPassFragment body. The camo flattening lives entirely in the light loop +// (UniversalFragmentCamo), so the true normal is kept and GI/reflections stay physically correct. void CamoForwardFragment( Varyings input , out half4 outColor : SV_Target0 @@ -203,8 +215,9 @@ void CamoForwardFragment( InputData inputData; InitializeInputData(input, surfaceData.normalTS, inputData); - // Camo: flatten the shading normal toward the camera (no-op when _FormSoftness == 0). - inputData.normalWS = SoftenNormalCamo(inputData.normalWS, inputData.viewDirectionWS); + // Camo: the form is de-emphasised purely inside the light loop (direct diffuse driven toward the + // dark/shadow value + specular faded), so the true normal is kept here and GI/reflections stay + // physically correct. No normal bending -> view-independent, no bright/dark flip when orbiting. SETUP_DEBUG_TEXTURE_DATA(inputData, UNDO_TRANSFORM_TEX(input.uv, _BaseMap)); diff --git a/Assets/Shaders/PlayerCamoLit.shader b/Assets/Shaders/PlayerCamoLit.shader index 533fc90..caf4dc1 100644 --- a/Assets/Shaders/PlayerCamoLit.shader +++ b/Assets/Shaders/PlayerCamoLit.shader @@ -6,16 +6,16 @@ // transfer 1:1, the Lit inspector keeps working, and the runtime MeshPainter (which only sets // _BaseColor / PBR keywords on the material instance) is unaffected. The ONLY change is the // ForwardLit pass: its fragment is CamoForwardFragment, which reimplements URP's forward shading -// while delegating GI / shadows / clustered lights to URP and applying the three camo knobs. All +// while delegating GI / shadows / clustered lights to URP and applying the camo knobs. All // other passes (ShadowCaster, GBuffer, DepthOnly, DepthNormals, Meta, ...) are stock URP/Lit. Shader "Custom/PlayerCamoLit" { Properties { - // --- Camouflage knobs (all 0 => identical to URP/Lit) --- + // --- Camouflage knobs (both 0 => identical to URP/Lit) --- [Header(Camouflage)] - _FormSoftness("Form Softness (normal -> view)", Range(0.0, 1.0)) = 0.4 - _HalfLambert("Half Lambert (lift shadow side)", Range(0.0, 1.0)) = 0.5 + _HalfLambert("Half Lambert (0 none, 0.5 classic, 1 flat)", Range(0.0, 1.0)) = 0.5 + _HalfLambertBrightness("Half Lambert Brightness Shift", Range(-1.0, 1.0)) = 0.0 _SpecularDampen("Specular Dampen", Range(0.0, 1.0)) = 0.5 // Specular vs Metallic workflow