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>
229 lines
9.7 KiB
HLSL
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
|