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

+3 -2
View File
@@ -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" };
}
}
+17 -3
View File
@@ -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>
+7 -4
View File
@@ -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");