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:
1 parent
e349446fb7
commit
fdcb4f8d8a
6 files changed
+78
-13
No files matched your search
@@ -28,6 +28,7 @@ Material:
|
||||
- _EMISSION
|
||||
- _METALLICSPECGLOSSMAP
|
||||
- _OCCLUSIONMAP
|
||||
- _PARALLAXMAP
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 1
|
||||
m_EnableInstancingVariants: 0
|
||||
@@ -78,7 +79,7 @@ Material:
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Texture: {fileID: 2800000, guid: c24e49ee8237bac42930ef868a325c48, type: 2}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
|
||||
@@ -69,8 +69,9 @@ public partial class MeshPainter
|
||||
switch (mi)
|
||||
{
|
||||
case MAP_ALBEDO: return new[] { "_BaseMap", "_MainTex" };
|
||||
// One mask map drives both URP slots: _MetallicGlossMap (R/A) + _OcclusionMap (G).
|
||||
case MAP_MSO: return new[] { "_MetallicGlossMap", "_OcclusionMap" };
|
||||
// One mask map drives three URP slots: _MetallicGlossMap (R/A) + _OcclusionMap (G) +
|
||||
// _ParallaxMap (G = occlusion reused as a 0.5-centred height for parallax/relief).
|
||||
case MAP_MSO: return new[] { "_MetallicGlossMap", "_OcclusionMap", "_ParallaxMap" };
|
||||
default: return new[] { "_EmissionMap" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,12 +148,14 @@ public partial class MeshPainter
|
||||
_seedCanonical = new Color32[MAP_COUNT][];
|
||||
_seedCanonical[MAP_ALBEDO] = BuildBaseColors();
|
||||
|
||||
// Mask map seed from the material scalars: R = metallic, G = occlusion (255 =
|
||||
// unoccluded, so the look is unchanged until painted), B = 0, A = smoothness.
|
||||
// Mask map seed from the material scalars: R = metallic, G = occlusion/height, B = 0,
|
||||
// A = smoothness. G = 128 (= 0.5) is the NEUTRAL height: no parallax/relief and AO =
|
||||
// saturate(2*0.5) = 1 (no darkening), so the unpainted look is unchanged. Painting G
|
||||
// below 0.5 carves a hole (and darkens), above 0.5 raises a hill (see PlayerCamoLit).
|
||||
float metallic0 = _materialInstance.HasProperty(MetallicId) ? _materialInstance.GetFloat(MetallicId) : 0f;
|
||||
float smoothness0 = _materialInstance.HasProperty(SmoothnessId) ? _materialInstance.GetFloat(SmoothnessId) : 0.5f;
|
||||
_seedCanonical[MAP_MSO] = FillCanonical(new Color32(
|
||||
(byte)(Mathf.Clamp01(metallic0) * 255f), 255, 0, (byte)(Mathf.Clamp01(smoothness0) * 255f)));
|
||||
(byte)(Mathf.Clamp01(metallic0) * 255f), 128, 0, (byte)(Mathf.Clamp01(smoothness0) * 255f)));
|
||||
|
||||
// Emission: keep the existing look (black if the material's emission is off).
|
||||
Color emission0 = Color.black;
|
||||
@@ -232,6 +234,18 @@ public partial class MeshPainter
|
||||
_materialInstance.EnableKeyword("_OCCLUSIONMAP");
|
||||
if (_materialInstance.HasProperty(OcclusionStrengthId)) _materialInstance.SetFloat(OcclusionStrengthId, 1f);
|
||||
}
|
||||
|
||||
// Height: the same mask map is also bound to _ParallaxMap (see MapProps), so the painted
|
||||
// occlusion value (G) doubles as a 0.5-centred height. _PARALLAXMAP turns on URP's single-
|
||||
// step parallax (ApplyPerPixelDisplacement) AND our relief-normal/AO remap in PlayerCamoLit.
|
||||
// Lift _Parallax off its floor so the displacement is actually visible (the relief normal is
|
||||
// the dominant cue; this just adds subtle motion parallax). Live-tunable on the instance.
|
||||
if (_materialInstance.HasProperty(ParallaxMapId))
|
||||
{
|
||||
_materialInstance.EnableKeyword("_PARALLAXMAP");
|
||||
if (_materialInstance.HasProperty(ParallaxId) && _materialInstance.GetFloat(ParallaxId) < 0.02f)
|
||||
_materialInstance.SetFloat(ParallaxId, 0.02f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pushes the global emission intensity to the material's _EmissionColor (live-tunable).</summary>
|
||||
|
||||
@@ -130,9 +130,10 @@ public partial class MeshPainter : MonoBehaviour
|
||||
[Range(0f, 1f)]
|
||||
public float metallicSmoothnessOpacity = 1f;
|
||||
|
||||
[Tooltip("Ambient occlusion painted into the mask map (1 = fully lit / no darkening, " +
|
||||
"0 = fully occluded / dark). NOTE: URP only darkens INDIRECT light (ambient + " +
|
||||
"reflections), so the effect is subtle under strong direct lighting. 1 = no-op.")]
|
||||
[Tooltip("Occlusion + HEIGHT painted into the mask map (one value, 0..1, 0.5 = neutral/no-op). " +
|
||||
"Drives parallax + a relief normal: 0.5 = flat, >0.5 = raised hill, <0.5 = carved hole. " +
|
||||
"Also drives ambient occlusion, but only the LOWER half darkens: 0.5..1 = fully lit, " +
|
||||
"0.5..0 = AO fades 1..0 (so a hole is also darker). See Custom/PlayerCamoLit.")]
|
||||
[Range(0f, 1f)]
|
||||
public float occlusion = 0.5f;
|
||||
|
||||
@@ -284,7 +285,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
|
||||
// Seed colors per map in canonical (material) layout, built once at Start:
|
||||
// albedo -> RGB color
|
||||
// MSO -> R = metallic, G = occlusion (1 = unoccluded), A = smoothness (mask map)
|
||||
// MSO -> R = metallic, G = occlusion/height (0.5 = neutral: no relief, AO = 1), A = smoothness
|
||||
// emission -> RGB emission
|
||||
private Color32[][] _seedCanonical;
|
||||
|
||||
@@ -322,6 +323,8 @@ public partial class MeshPainter : MonoBehaviour
|
||||
private static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor");
|
||||
private static readonly int OcclusionMapId = Shader.PropertyToID("_OcclusionMap");
|
||||
private static readonly int OcclusionStrengthId = Shader.PropertyToID("_OcclusionStrength");
|
||||
private static readonly int ParallaxMapId = Shader.PropertyToID("_ParallaxMap");
|
||||
private static readonly int ParallaxId = Shader.PropertyToID("_Parallax");
|
||||
private static readonly int HalfLambertId = Shader.PropertyToID("_HalfLambert");
|
||||
private static readonly int HalfLambertBrightnessId = Shader.PropertyToID("_HalfLambertBrightness");
|
||||
private static readonly int SpecularDampenId = Shader.PropertyToID("_SpecularDampen");
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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" {}
|
||||
|
||||
Reference in new issue
Block a user