HTrace WSGI composites indirect lighting after post-processing using the GBuffer albedo/occlusion of the player's stock URP GBuffer pass. That pass knew neither the painted-height AO semantics nor the brightness/saturation grade, so desaturated paint reappeared fully saturated in shadow/indirect light and painted height modulated GI with the raw 0.5-centered value. - PlayerCamoShared.hlsl (new): single source for camo knobs, ApplyPaintedHeightSurface and ApplyCamoBodyGrade shared by both passes - PlayerCamoGBuffer.hlsl (new): CamoGBufferFragment = URP 17.3 Lit GBuffer fragment + height-AO remap on occlusion + grade applied to albedo - PlayerCamoLit.shader: GBuffer pass now uses CamoGBufferFragment - PlayerCamo.hlsl: consumes the shared helpers (forward path unchanged) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
286 lines
15 KiB
HLSL
286 lines
15 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 0, _HalfLambertBrightness 0 and _Saturation 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): GENERAL brightness (exposure) on the WHOLE shaded body - diffuse,
|
|
// ambient/GI AND specular - as a final multiply (1 + value), default
|
|
// 0 = neutral. NO LONGER tied to the half-Lambert diffuse term (the
|
|
// "HalfLambert" in the name is legacy): dial down to sink the body
|
|
// into a dark scene, up to lift it into a bright one. Applied in
|
|
// CamoForwardFragment, before fog and the X-ray glow.
|
|
// _Saturation (-1..1): colour saturation of the shaded body, default 0 = neutral. -1 =
|
|
// greyscale, <0 = muted (blends into muted scenes), >0 = punchier.
|
|
// Luma-preserving lerp(grey, colour, 1 + _Saturation) toward Rec.709
|
|
// grey, same final stage as brightness.
|
|
// _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. Brightness/saturation and the painted
|
|
// height (knobs + ApplyPaintedHeightSurface) live in PlayerCamoShared.hlsl - the UniversalGBuffer
|
|
// pass (PlayerCamoGBuffer.hlsl, HTrace WSGI's GI input) mirrors them so injected indirect light
|
|
// matches the graded forward look.
|
|
#include "PlayerCamoShared.hlsl"
|
|
half _HalfLambert;
|
|
half _SpecularDampen;
|
|
|
|
// X-Ray reveal glow (rgb = HDR color, a = fade/opacity master). Consumed here for the VISIBLE parts;
|
|
// the separate XRayOverlay pass (SRPDefaultUnlit) covers the OCCLUDED parts. See CamoForwardFragment.
|
|
half4 _XRayColor;
|
|
half _XRayPulseSpeed; // 0 = steady; otherwise breathing pulse via _Time (see CamoForwardFragment)
|
|
half _XRayPulseStrength; // modulation depth: how far the trough dims below the full-intensity peak
|
|
half _XRayPulseShape; // >1 bends the pulse: longer near-off plateau, rising to a brief full peak
|
|
|
|
// 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.
|
|
// (Brightness is NOT applied here anymore - it is a general final-color exposure in CamoForwardFragment.)
|
|
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); // in [0,1], no negative light
|
|
|
|
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);
|
|
|
|
// Painted height: reinterpret the occlusion channel as a 0.5-centred height -> remap the AO
|
|
// (only the lower half darkens) and add a relief normal from the height gradient. The uv is
|
|
// already parallax-displaced above (_PARALLAXMAP), so this stays consistent with the offset.
|
|
ApplyPaintedHeightSurface(input.uv, surfaceData);
|
|
|
|
#ifdef LOD_FADE_CROSSFADE
|
|
LODFadeCrossFade(input.positionCS);
|
|
#endif
|
|
|
|
InputData inputData;
|
|
InitializeInputData(input, surfaceData.normalTS, inputData);
|
|
|
|
// Camo: the MACRO form is de-emphasised purely inside the light loop (direct diffuse driven toward
|
|
// the dark/shadow value + specular faded), so the body's true shape is kept here and GI/reflections
|
|
// stay physically correct - no macro normal bending, so no bright/dark flip when orbiting. The
|
|
// painted height above intentionally adds LOCAL relief normals (holes/hills) on top of that.
|
|
|
|
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);
|
|
|
|
// Camo body-tone grade on the WHOLE shaded body (direct + ambient/GI + specular). Applied before
|
|
// fog (part of the body look) and before the X-ray reveal glow below, so the reveal keeps its
|
|
// authored intensity. Neutral at brightness 0 / saturation 0 => byte-identical to stock when the
|
|
// camo knobs are off. Mirrored onto the GBuffer albedo (PlayerCamoGBuffer.hlsl) so HTrace WSGI's
|
|
// injected indirect light follows the grade too - otherwise paint keeps its raw look in shadows.
|
|
ApplyCamoBodyGrade(color.rgb);
|
|
|
|
color.rgb = MixFog(color.rgb, inputData.fogCoord);
|
|
|
|
// X-Ray reveal glow on the VISIBLE parts. URP draws the SRPDefaultUnlit XRayOverlay pass BEFORE
|
|
// this UniversalForward pass (see DrawObjectsPass shader-tag order), so that pass alone can only
|
|
// survive on OCCLUDED pixels - this opaque forward write overwrites the visible ones. Adding the
|
|
// same additive term here reveals the glow on the visible parts too. Visible (here) and occluded
|
|
// (overlay, ZTest Greater) are mutually exclusive by depth => no double contribution. Added
|
|
// post-fog to match the overlay, which outputs the raw color.
|
|
// Asymmetric breathing pulse on the glow master, matching the XRayOverlay pass exactly so the visible
|
|
// and occluded halves stay in phase. A raised cosine (0..1, peak once per cycle) raised to _XRayPulseShape
|
|
// rises from a long near-off plateau to a brief peak; _XRayPulseStrength sets how deep the trough dims
|
|
// (peak stays at the authored intensity). Speed 0 => cos term constant => steady glow.
|
|
half xrayCurve = pow(0.5h - 0.5h * cos(_Time.y * _XRayPulseSpeed), _XRayPulseShape);
|
|
half xrayPulse = 1.0h - _XRayPulseStrength * (1.0h - xrayCurve);
|
|
color.rgb += _XRayColor.rgb * (_XRayColor.a * xrayPulse);
|
|
|
|
color.a = OutputAlpha(color.a, IsSurfaceTypeTransparent(_Surface));
|
|
|
|
outColor = color;
|
|
|
|
#ifdef _WRITE_RENDERING_LAYERS
|
|
outRenderingLayers = EncodeMeshRenderingLayer();
|
|
#endif
|
|
}
|
|
|
|
#endif
|