Drive parallax height and relief from the painted occlusion channel

Reuse the existing occlusion paint channel (no new map, MAP_COUNT stays 3):
0.5 is neutral, above 0.5 raises a hill, below 0.5 sinks a hole and also
fades ambient occlusion (saturate(2*occ)). Bind the MSO map to _ParallaxMap,
enable _PARALLAXMAP, derive a relief normal from the height gradient, and
reseed unpainted occlusion to 0.5 so untouched surfaces stay visually neutral.
Pin the _PARALLAXMAP runtime variant in the build keep-alive material.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-30 22:40:23 +02:00
1 parent e349446fb7
commit fdcb4f8d8a
6 files changed
+78 -13

No files matched your search

+48 -3
View File
@@ -32,6 +32,45 @@ half _HalfLambert;
half _HalfLambertBrightness;
half _SpecularDampen;
// --- Painted height (from the occlusion channel) ---
// The painted occlusion value (G of the MSO mask map, bound to _OcclusionMap AND _ParallaxMap) is
// reinterpreted as a 0.5-centred HEIGHT field:
// 0.5 -> neutral: no displacement, flat relief normal, AO = 1 (identical to the unpainted look)
// <0.5 -> recessed (hole) >0.5 -> raised (hill)
// The UV displacement is URP's stock single-step parallax (ParallaxOffset1Step already does
// height*amp - amp/2, i.e. the 0.5 centring) under the _PARALLAXMAP keyword. Here we add the
// matching per-pixel relief NORMAL from the height gradient (the dominant cue) and remap the ambient
// occlusion so only the LOWER half darkens: AO = saturate(occ*2) (occ>=0.5 -> no darkening, occ in
// [0,0.5] -> AO 1..0). Declared outside UnityPerMaterial like the camo knobs above (the player is a
// handful of instances, so the SRP-batcher break is irrelevant).
half _HeightNormalStrength;
float4 _OcclusionMap_TexelSize;
// Remaps the painted occlusion/height value into surface AO + a relief normal. Runs after URP's
// InitializeStandardLitSurfaceData (uv is already parallax-displaced) and before InitializeInputData,
// so the relief normal flows into inputData.normalWS through the tangent frame.
void ApplyPaintedHeightSurface(float2 uv, inout SurfaceData surfaceData)
{
#if defined(_OCCLUSIONMAP)
// Lower-half AO remap: occ>=0.5 stays fully lit, occ in [0,0.5] darkens toward 0.
half occ = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g;
surfaceData.occlusion = LerpWhiteTo(saturate(occ * 2.0h), _OcclusionStrength);
#endif
#if defined(_PARALLAXMAP)
// Per-pixel relief normal from the height (occlusion) gradient via central differences over one
// texel. A flat field (occ=0.5 everywhere) yields zero deltas -> (0,0,1), so neutral stays unbent.
float2 texel = _OcclusionMap_TexelSize.xy;
half hL = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv - float2(texel.x, 0.0)).g;
half hR = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv + float2(texel.x, 0.0)).g;
half hD = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv - float2(0.0, texel.y)).g;
half hU = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv + float2(0.0, texel.y)).g;
half3 reliefTS = normalize(half3((hL - hR) * _HeightNormalStrength,
(hD - hU) * _HeightNormalStrength, 1.0h));
surfaceData.normalTS = BlendNormalRNM(surfaceData.normalTS, reliefTS);
#endif
}
// 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.
@@ -208,6 +247,11 @@ void CamoForwardFragment(
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
@@ -215,9 +259,10 @@ void CamoForwardFragment(
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.
// 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));
+1
View File
@@ -43,6 +43,7 @@ Shader "Custom/PlayerCamoLit"
_Parallax("Scale", Range(0.005, 0.08)) = 0.005
_ParallaxMap("Height Map", 2D) = "black" {}
_HeightNormalStrength("Painted Height Normal Strength", Range(0.0, 50.0)) = 8.0
_OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0
_OcclusionMap("Occlusion", 2D) = "white" {}