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>
242 lines
11 KiB
HLSL
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
|