Files
chameleon/Assets/Shaders/PlayerCamo.hlsl
T
adminandClaude Opus 4.8 b5bd1fdb8b 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 <noreply@anthropic.com>
2026-06-29 20:12:31 +02:00

242 lines
11 KiB
HLSL

#ifndef PLAYER_CAMO_INCLUDED
#define PLAYER_CAMO_INCLUDED
// 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:
//
// _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 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 _HalfLambert;
half _HalfLambertBrightness;
half _SpecularDampen;
// 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.
half3 LightingPhysicallyBasedCamo(BRDFData brdfData, BRDFData brdfDataClearCoat,
half3 lightColor, half3 lightDirectionWS, float lightAttenuation,
half3 normalWS, half3 viewDirectionWS,
half clearCoatMask, bool specularHighlightsOff)
{
half NdotL = dot(normalWS, lightDirectionWS);
half ndlSat = saturate(NdotL);
// 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; // flattened + brightness-shifted
half3 specularRadiance = atten * ndlSat; // clamped (no back-face specular)
half3 color = brdfData.diffuse * diffuseRadiance;
#ifndef _SPECULARHIGHLIGHTS_OFF
[branch] if (!specularHighlightsOff)
{
half3 spec = brdfData.specular * DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS);
#if defined(_CLEARCOAT) || defined(_CLEARCOATMAP)
// Inert for URP/Lit (no clear-coat keywords on the forward pass); kept for fidelity.
half brdfCoat = kDielectricSpec.r * DirectBRDFSpecular(brdfDataClearCoat, normalWS, lightDirectionWS, viewDirectionWS);
half NoV = saturate(dot(normalWS, viewDirectionWS));
half coatFresnel = kDielectricSpec.x + kDielectricSpec.a * Pow4(1.0 - NoV);
spec = spec * (1.0 - clearCoatMask * coatFresnel) + (brdfCoat * clearCoatMask).xxx;
#endif
// 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
return color;
}
half3 LightingPhysicallyBasedCamo(BRDFData brdfData, BRDFData brdfDataClearCoat, Light light,
half3 normalWS, half3 viewDirectionWS, half clearCoatMask, bool specularHighlightsOff)
{
return LightingPhysicallyBasedCamo(brdfData, brdfDataClearCoat, light.color, light.direction,
light.distanceAttenuation * light.shadowAttenuation, normalWS, viewDirectionWS,
clearCoatMask, specularHighlightsOff);
}
// Faithful copy of URP 17.3 UniversalFragmentPBR. ONLY change vs. stock: the three per-light calls
// go to LightingPhysicallyBasedCamo (above). Everything else delegates to URP unchanged.
half4 UniversalFragmentCamo(InputData inputData, SurfaceData surfaceData)
{
#if defined(_SPECULARHIGHLIGHTS_OFF)
bool specularHighlightsOff = true;
#else
bool specularHighlightsOff = false;
#endif
BRDFData brdfData;
// NOTE: can modify "surfaceData"...
InitializeBRDFData(surfaceData, brdfData);
#if defined(DEBUG_DISPLAY)
half4 debugColor;
if (CanDebugOverrideOutputColor(inputData, surfaceData, brdfData, debugColor))
{
return debugColor;
}
#endif
BRDFData brdfDataClearCoat = CreateClearCoatBRDFData(surfaceData, brdfData);
half4 shadowMask = CalculateShadowMask(inputData);
AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData);
uint meshRenderingLayers = GetMeshRenderingLayer();
Light mainLight = GetMainLight(inputData, shadowMask, aoFactor);
MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI);
LightingData lightingData = CreateLightingData(inputData, surfaceData);
lightingData.giColor = GlobalIllumination(brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,
inputData.bakedGI, aoFactor.indirectAmbientOcclusion, inputData.positionWS,
inputData.normalWS, inputData.viewDirectionWS, inputData.normalizedScreenSpaceUV);
#ifdef _LIGHT_LAYERS
if (IsMatchingLightLayer(mainLight.layerMask, meshRenderingLayers))
#endif
{
lightingData.mainLightColor = LightingPhysicallyBasedCamo(brdfData, brdfDataClearCoat,
mainLight,
inputData.normalWS, inputData.viewDirectionWS,
surfaceData.clearCoatMask, specularHighlightsOff);
}
#if defined(_ADDITIONAL_LIGHTS)
uint pixelLightCount = GetAdditionalLightsCount();
#if USE_CLUSTER_LIGHT_LOOP
[loop] for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)
{
CLUSTER_LIGHT_LOOP_SUBTRACTIVE_LIGHT_CHECK
Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);
#ifdef _LIGHT_LAYERS
if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
#endif
{
lightingData.additionalLightsColor += LightingPhysicallyBasedCamo(brdfData, brdfDataClearCoat, light,
inputData.normalWS, inputData.viewDirectionWS,
surfaceData.clearCoatMask, specularHighlightsOff);
}
}
#endif
LIGHT_LOOP_BEGIN(pixelLightCount)
Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);
#ifdef _LIGHT_LAYERS
if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
#endif
{
lightingData.additionalLightsColor += LightingPhysicallyBasedCamo(brdfData, brdfDataClearCoat, light,
inputData.normalWS, inputData.viewDirectionWS,
surfaceData.clearCoatMask, specularHighlightsOff);
}
LIGHT_LOOP_END
#endif
#if defined(_ADDITIONAL_LIGHTS_VERTEX)
lightingData.vertexLightingColor += inputData.vertexLighting * brdfData.diffuse;
#endif
#if REAL_IS_HALF
return min(CalculateFinalColor(lightingData, surfaceData.alpha), HALF_MAX);
#else
return CalculateFinalColor(lightingData, surfaceData.alpha);
#endif
}
// 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
#ifdef _WRITE_RENDERING_LAYERS
, out uint outRenderingLayers : SV_Target1
#endif
)
{
UNITY_SETUP_INSTANCE_ID(input);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
#if defined(_PARALLAXMAP)
#if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
half3 viewDirTS = input.viewDirTS;
#else
half3 viewDirWS = GetWorldSpaceNormalizeViewDir(input.positionWS);
half3 viewDirTS = GetViewDirectionTangentSpace(input.tangentWS, input.normalWS, viewDirWS);
#endif
ApplyPerPixelDisplacement(viewDirTS, input.uv);
#endif
SurfaceData surfaceData;
InitializeStandardLitSurfaceData(input.uv, surfaceData);
#ifdef LOD_FADE_CROSSFADE
LODFadeCrossFade(input.positionCS);
#endif
InputData inputData;
InitializeInputData(input, surfaceData.normalTS, inputData);
// 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));
#if defined(_DBUFFER)
ApplyDecalToSurfaceData(input.positionCS, surfaceData, inputData);
#endif
InitializeBakedGIData(input, inputData);
half4 color = UniversalFragmentCamo(inputData, surfaceData);
color.rgb = MixFog(color.rgb, inputData.fogCoord);
color.a = OutputAlpha(color.a, IsSurfaceTypeTransparent(_Surface));
outColor = color;
#ifdef _WRITE_RENDERING_LAYERS
outRenderingLayers = EncodeMeshRenderingLayer();
#endif
}
#endif