Files
chameleon/Assets/Shaders/PlayerCamo.hlsl
T
adminandClaude Opus 4.8 05cd52aa5e Add player camouflage shader
Custom/PlayerCamoLit is a URP 17 Lit clone whose ForwardLit pass routes
through PlayerCamo.hlsl - a faithful copy of UniversalFragmentPBR that
delegates all GI/APV/shadow/cluster-light helpers unchanged and only swaps
the three light calls for a camo variant. Three sliders flatten the form
cues that give a painted player away: _FormSoftness bends the per-pixel
normal toward the camera (softens diffuse + specular + reflections),
_HalfLambert lifts the diffuse shadow side, _SpecularDampen scales down the
direct highlight. All sliders at 0 are byte-identical to URP/Lit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 16:22:57 +02:00

229 lines
9.7 KiB
HLSL

#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:
//
// _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).
//
// 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.
//
// 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 _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.
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);
half diffuseTerm = lerp(ndlSat, NdotL * 0.5h + 0.5h, _HalfLambert);
half3 atten = lightColor * lightAttenuation;
half3 diffuseRadiance = atten * diffuseTerm; // wrapped (half-Lambert)
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
spec *= (1.0h - _SpecularDampen);
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 and inserts the normal-soften step before GI + lighting,
// so GI (probe) direction, direct lighting and reflections all use the flattened normal.
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: flatten the shading normal toward the camera (no-op when _FormSoftness == 0).
inputData.normalWS = SoftenNormalCamo(inputData.normalWS, inputData.viewDirectionWS);
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