Add painted-height HUD sliders for AO shift and relief intensity
Wire three material-backed look sliders (occlusion AO shift, relief normal intensity, parallax intensity) through the paint HUD, its layout/state persistence, the .paint slot format (bumped to v4), owner-authoritative network sync and the runtime reset, mirroring the existing camo-knob path so they save, restore, replicate and reset alongside the brush values. Change the occlusion AO mapping from a division threshold to a linear shift: AO fades from 1 down to (1 - shift) at the deepest painted hole, so a 0 shift no longer leaves black spots. Enable _NORMALMAP alongside _PARALLAXMAP so URP transforms the painted relief normal into world space (otherwise it discards normalTS and the relief is invisible), and pin the added keyword in the build keep-alive material. Stop the pipette from picking the occlusion/height value and turn its toggle off after a pick. Default the knobs to 0.2 shift / 1.4 normal / 1.0 parallax across the shader, painter, network and HUD fallbacks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
fdcb4f8d8a
commit
59939b3496
11 files changed
+216
-49
No files matched your search
@@ -27,6 +27,7 @@ Material:
|
||||
m_ValidKeywords:
|
||||
- _EMISSION
|
||||
- _METALLICSPECGLOSSMAP
|
||||
- _NORMALMAP
|
||||
- _OCCLUSIONMAP
|
||||
- _PARALLAXMAP
|
||||
m_InvalidKeywords: []
|
||||
@@ -47,7 +48,7 @@ Material:
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Texture: {fileID: 2800000, guid: c24e49ee8237bac42930ef868a325c48, type: 2}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
|
||||
@@ -16,7 +16,7 @@ public partial class MeshPainter
|
||||
// map's canonical Color32 bytes. Raw (not PNG) avoids any sRGB/alpha-premultiply ambiguity and
|
||||
// round-trips the exact canonical bytes ReadCanonical produces.
|
||||
private const uint PaintFileMagic = 0x504D4843; // 'CHMP' little-endian
|
||||
private const int PaintFileVersion = 3; // v3: added the per-player material scalars (emission + camo)
|
||||
private const int PaintFileVersion = 4; // v3: emission + camo scalars; v4: + painted-height knobs
|
||||
private const string PaintFileExt = ".paint";
|
||||
|
||||
/// <summary>
|
||||
@@ -26,9 +26,11 @@ public partial class MeshPainter
|
||||
/// </summary>
|
||||
private struct PaintScalars
|
||||
{
|
||||
public bool has; // false = legacy v2 slot with no scalars -> leave the painter's values untouched
|
||||
public bool has; // false = legacy v2 slot with no scalars -> leave the painter's values untouched
|
||||
public bool hasHeight; // v4+ painted-height knobs present (occlusion threshold + normal/parallax intensity)
|
||||
public float emissionIntensity;
|
||||
public float camoHalfLambert, camoHalfLambertBrightness, camoSpecularDampen;
|
||||
public float occlusionThreshold, heightNormalIntensity, parallaxIntensity;
|
||||
}
|
||||
|
||||
private static string PaintLibraryDir =>
|
||||
@@ -104,6 +106,11 @@ public partial class MeshPainter
|
||||
w.Write(CamoHalfLambertBrightness);
|
||||
w.Write(CamoSpecularDampen);
|
||||
|
||||
// v4: the painted-height knobs (same material-instance source of truth as the camo knobs).
|
||||
w.Write(OcclusionThreshold);
|
||||
w.Write(HeightNormalIntensity);
|
||||
w.Write(ParallaxIntensity);
|
||||
|
||||
var bytes = new byte[n * 4];
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
@@ -160,6 +167,13 @@ public partial class MeshPainter
|
||||
CamoHalfLambertBrightness = scalars.camoHalfLambertBrightness;
|
||||
CamoSpecularDampen = scalars.camoSpecularDampen;
|
||||
}
|
||||
// v4 slots also carry the painted-height knobs; older slots (v2/v3) leave them at the current value.
|
||||
if (scalars.hasHeight)
|
||||
{
|
||||
OcclusionThreshold = scalars.occlusionThreshold;
|
||||
HeightNormalIntensity = scalars.heightNormalIntensity;
|
||||
ParallaxIntensity = scalars.parallaxIntensity;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -190,7 +204,7 @@ public partial class MeshPainter
|
||||
{
|
||||
if (r.ReadUInt32() != PaintFileMagic) return null;
|
||||
int ver = r.ReadInt32();
|
||||
if (ver != 2 && ver != PaintFileVersion) return null; // list legacy v2 slots + current
|
||||
if (ver < 2 || ver > PaintFileVersion) return null; // list legacy v2..v3 slots + current
|
||||
return r.ReadString();
|
||||
}
|
||||
}
|
||||
@@ -216,7 +230,7 @@ public partial class MeshPainter
|
||||
{
|
||||
if (r.ReadUInt32() != PaintFileMagic) return null;
|
||||
int ver = r.ReadInt32();
|
||||
if (ver != 2 && ver != PaintFileVersion) return null; // read legacy v2 + current
|
||||
if (ver < 2 || ver > PaintFileVersion) return null; // read legacy v2..v3 + current
|
||||
r.ReadString(); // display name (header only; not needed for the pixel load)
|
||||
int w = r.ReadInt32();
|
||||
int h = r.ReadInt32();
|
||||
@@ -232,6 +246,15 @@ public partial class MeshPainter
|
||||
scalars.camoSpecularDampen = r.ReadSingle();
|
||||
}
|
||||
|
||||
// v4+: the painted-height knobs follow the camo scalars.
|
||||
if (ver >= 4)
|
||||
{
|
||||
scalars.hasHeight = true;
|
||||
scalars.occlusionThreshold = r.ReadSingle();
|
||||
scalars.heightNormalIntensity = r.ReadSingle();
|
||||
scalars.parallaxIntensity = r.ReadSingle();
|
||||
}
|
||||
|
||||
// Resolution change -> ignore rather than smear mismatched data; the brush re-seeds
|
||||
// from the material instead.
|
||||
if (w != textureSize || h != textureSize)
|
||||
|
||||
@@ -6,11 +6,12 @@ public partial class MeshPainter
|
||||
// --- Pipette (eyedropper) ---
|
||||
//
|
||||
// A sampling MODE rather than a paint channel: while active (Shift or Ctrl held, or the HUD toggle), a
|
||||
// left-click raycasts onto ANY surface (not just the paint mesh) and reads every channel value at
|
||||
// the hit point (albedo, metallic, smoothness, occlusion, emission) into the brush properties -
|
||||
// with every opacity/alpha forced to 1 so re-painting reproduces the picked look at full strength.
|
||||
// left-click raycasts onto ANY surface (not just the paint mesh) and reads the channel values at
|
||||
// the hit point (albedo, metallic, smoothness, emission) into the brush properties - with every
|
||||
// opacity/alpha forced to 1 so re-painting reproduces the picked look at full strength. Occlusion is
|
||||
// excluded: it drives painted height now, so the pipette neither samples it nor enables its toggle.
|
||||
//
|
||||
// On a FOREIGN object the data channels (metallic/smoothness/occlusion/emission) come texel-exact
|
||||
// On a FOREIGN object the data channels (metallic/smoothness/emission) come texel-exact
|
||||
// from the material (raw, UNLIT). Albedo follows the 'pipetteAlbedoFromScreen' toggle: OFF (default)
|
||||
// reads it texel-exact from the material too; ON takes it from the rendered, fully-lit on-screen
|
||||
// pixel ("what you see" - honours baked lightmaps/ambient/SSAO), combining material PBR fidelity
|
||||
@@ -19,16 +20,16 @@ public partial class MeshPainter
|
||||
// terrain are read texel-exact.
|
||||
|
||||
[Header("Brush - pipette (eyedropper)")]
|
||||
[Tooltip("Pipette mode: a click samples ALL channel values at the hit point on ANY surface " +
|
||||
"(albedo, metallic, smoothness, occlusion, emission) and adopts them into the brush " +
|
||||
"(all alphas set to 1) instead of painting. On a foreign object the data channels are read " +
|
||||
"texel-exact from the material; albedo follows 'Pipette Albedo From Screen'. Held button picks once.")]
|
||||
[Tooltip("Pipette mode: a click samples the channel values at the hit point on ANY surface " +
|
||||
"(albedo, metallic, smoothness, emission - NOT occlusion/height) and adopts them into the " +
|
||||
"brush (all alphas set to 1) instead of painting. On a foreign object the data channels are " +
|
||||
"read texel-exact from the material; albedo follows 'Pipette Albedo From Screen'. Held button picks once.")]
|
||||
public bool pipette = false;
|
||||
|
||||
[Tooltip("On a foreign-object pick, where albedo comes from. OFF (default): albedo is read " +
|
||||
"texel-exact from the material like the other channels (raw, unlit). ON: albedo is taken " +
|
||||
"from the lit on-screen pixel ('what you see' - honours baked lightmaps/ambient/SSAO). " +
|
||||
"The data channels (metallic/smoothness/occlusion/emission) always come from the material.")]
|
||||
"The data channels (metallic/smoothness/emission) always come from the material.")]
|
||||
[SerializeField] private bool pipetteAlbedoFromScreen = false;
|
||||
|
||||
/// <summary>
|
||||
@@ -44,9 +45,10 @@ public partial class MeshPainter
|
||||
private const float TerrainFoliageHeightEpsilon = 0.3f;
|
||||
|
||||
/// <summary>
|
||||
/// Raycasts at a screen position against ANY collider, samples every channel value at the hit
|
||||
/// (texel-exact where possible) and copies it into the brush, forcing all opacities/alphas to
|
||||
/// 1 and enabling every paint channel. Reads regardless of which channels are currently toggled.
|
||||
/// Raycasts at a screen position against ANY collider, samples the channel values at the hit
|
||||
/// (texel-exact where possible) and copies them into the brush, forcing the sampled opacities/alphas
|
||||
/// to 1 and enabling those paint channels. Occlusion/height is excluded (see FinalizePick). Reads
|
||||
/// regardless of which channels are currently toggled.
|
||||
/// Pick paths, by what was hit:
|
||||
/// - our paint mesh -> texel-exact from the painted maps (full PBR);
|
||||
/// - terrain ground -> blend of the terrain layers by their splat weights (full PBR);
|
||||
@@ -83,18 +85,19 @@ public partial class MeshPainter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Common tail of every successful pick: forces all opacities/alphas to 1, enables every paint
|
||||
/// channel (a whole-material pick implies repainting all of it), leaves pipette mode (one-shot)
|
||||
/// and notifies listeners (the in-game HUD) to re-sync their widgets.
|
||||
/// Common tail of every successful pick: forces the sampled opacities/alphas to 1, enables the
|
||||
/// sampled paint channels, leaves pipette mode (one-shot) and notifies listeners (the in-game HUD) to
|
||||
/// re-sync their widgets. Occlusion is deliberately EXCLUDED: that channel now drives painted height,
|
||||
/// not a surface property to sample, so the pipette neither reads it (see the pick paths) nor enables
|
||||
/// its toggle - the deliberate height brush value is left untouched.
|
||||
/// </summary>
|
||||
private void FinalizePick()
|
||||
{
|
||||
metallicSmoothnessOpacity = 1f;
|
||||
occlusionOpacity = 1f;
|
||||
|
||||
paintAlbedo = true;
|
||||
paintMetallicSmoothness = true;
|
||||
paintOcclusion = true;
|
||||
paintOcclusion = false;
|
||||
paintEmission = true;
|
||||
|
||||
pipette = false;
|
||||
@@ -136,16 +139,16 @@ public partial class MeshPainter
|
||||
Color32 e = em[idx];
|
||||
|
||||
// sRGB bytes map straight back into the gamma-space brush colours (round-trips exactly).
|
||||
// Occlusion (m.g) is NOT picked: it now drives painted height, kept as the deliberate brush value.
|
||||
brushColor = new Color(a.r / 255f, a.g / 255f, a.b / 255f, 1f);
|
||||
metallic = m.r / 255f;
|
||||
occlusion = m.g / 255f;
|
||||
smoothness = m.a / 255f;
|
||||
emissionColor = new Color(e.r / 255f, e.g / 255f, e.b / 255f, 1f);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Foreign-object pick: the data channels (metallic/smoothness/occlusion/emission) always come
|
||||
/// Foreign-object pick: the data channels (metallic/smoothness/emission) always come
|
||||
/// texel-exact from the material via <see cref="PickFromForeignMaterial"/> (raw, unlit). Albedo
|
||||
/// follows <see cref="pipetteAlbedoFromScreen"/>: when set, it is overwritten with the rendered,
|
||||
/// fully-lit on-screen pixel ("what you see" - honours baked lightmaps/ambient/SSAO); otherwise
|
||||
@@ -237,13 +240,7 @@ public partial class MeshPainter
|
||||
metallic = Mathf.Clamp01(metallicVal);
|
||||
smoothness = Mathf.Clamp01(smoothVal);
|
||||
|
||||
// --- Occlusion: OcclusionMap.g (linear), lerp(1, g, strength) ---
|
||||
float occStrength = GetFloatProp(mat, "_OcclusionStrength", null, 1f);
|
||||
float occ = 1f;
|
||||
Texture occTex = GetTexProp(mat, "_OcclusionMap", null);
|
||||
if (hasUv && occTex)
|
||||
occ = Mathf.Lerp(1f, SampleTexel(occTex, ApplyST(mat, "_OcclusionMap", null, uv), false).g, occStrength);
|
||||
occlusion = Mathf.Clamp01(occ);
|
||||
// Occlusion is deliberately NOT sampled (that channel drives painted height now, see FinalizePick).
|
||||
|
||||
// --- Emission: EmissionMap (sRGB) * EmissionColor ---
|
||||
Color emColor = GetColorProp(mat, "_EmissionColor", null, Color.black);
|
||||
@@ -292,7 +289,7 @@ public partial class MeshPainter
|
||||
/// <summary>
|
||||
/// Blends the terrain's layers at a ground point by their splat (alphamap) weights: the weighted
|
||||
/// diffuse colour becomes albedo and the weighted per-layer metallic/smoothness scalars feed
|
||||
/// those channels; occlusion is 1 and emission black. Terrain has no Renderer/material, so this
|
||||
/// those channels; emission is black (occlusion is left as the brush's height value). Terrain has no Renderer/material, so this
|
||||
/// is the only way to read its surface.
|
||||
/// </summary>
|
||||
private bool PickFromTerrainLayers(TerrainData td, Vector3 local, float nx, float nz)
|
||||
@@ -333,8 +330,7 @@ public partial class MeshPainter
|
||||
brushColor = new Color(Mathf.Clamp01(albedo.r * inv), Mathf.Clamp01(albedo.g * inv), Mathf.Clamp01(albedo.b * inv), 1f);
|
||||
metallic = Mathf.Clamp01(metallicVal * inv);
|
||||
smoothness = Mathf.Clamp01(smoothVal * inv);
|
||||
occlusion = 1f;
|
||||
emissionColor = new Color(0f, 0f, 0f, 1f);
|
||||
emissionColor = new Color(0f, 0f, 0f, 1f); // occlusion left untouched (now painted height)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -371,8 +367,7 @@ public partial class MeshPainter
|
||||
brushColor = new Color(Mathf.Clamp01(c.r), Mathf.Clamp01(c.g), Mathf.Clamp01(c.b), 1f);
|
||||
metallic = 0f;
|
||||
smoothness = 0f;
|
||||
occlusion = 1f;
|
||||
emissionColor = new Color(0f, 0f, 0f, 1f);
|
||||
emissionColor = new Color(0f, 0f, 0f, 1f); // occlusion left untouched (now painted height)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -240,9 +240,15 @@ public partial class MeshPainter
|
||||
// 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.
|
||||
// _NORMALMAP is REQUIRED for the relief normal to show: URP's InitializeInputData only
|
||||
// transforms surfaceData.normalTS into world space when _NORMALMAP (or _DETAIL) is set -
|
||||
// otherwise it discards normalTS and uses the geometric normal, so the relief is invisible.
|
||||
// _BumpMap stays unbound (= flat default normal), so it only enables the tangent->world path
|
||||
// and our relief blends on top of a flat base.
|
||||
if (_materialInstance.HasProperty(ParallaxMapId))
|
||||
{
|
||||
_materialInstance.EnableKeyword("_PARALLAXMAP");
|
||||
_materialInstance.EnableKeyword("_NORMALMAP");
|
||||
if (_materialInstance.HasProperty(ParallaxId) && _materialInstance.GetFloat(ParallaxId) < 0.02f)
|
||||
_materialInstance.SetFloat(ParallaxId, 0.02f);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,35 @@ public partial class MeshPainter : MonoBehaviour
|
||||
set { if (_materialInstance && _materialInstance.HasProperty(SpecularDampenId)) _materialInstance.SetFloat(SpecularDampenId, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Painted-height look knobs on the same <c>Custom/PlayerCamoLit</c> material instance, surfaced for
|
||||
/// the HUD like the camo knobs above (material-backed source of truth, no-op before the instance
|
||||
/// exists). All three persist with the painting (PaintHud.Persistence + the .paint slot) and replicate
|
||||
/// to peers via <see cref="PlayerPaintSync"/>. Defaults match the shader so the unpainted look is unchanged.
|
||||
/// </summary>
|
||||
/// <summary>Painted-height AO darkening depth (shift, 0..1): AO fades from 1 down to (1 - this) at
|
||||
/// the deepest painted hole. 0 = no darkening (no black spots), 1 = full black at occ=0.</summary>
|
||||
public float OcclusionThreshold
|
||||
{
|
||||
get => _materialInstance && _materialInstance.HasProperty(OcclusionThresholdId) ? _materialInstance.GetFloat(OcclusionThresholdId) : 0.2f;
|
||||
set { if (_materialInstance && _materialInstance.HasProperty(OcclusionThresholdId)) _materialInstance.SetFloat(OcclusionThresholdId, value); }
|
||||
}
|
||||
|
||||
/// <summary>Multiplier on the painted-height relief NORMAL strength (1 = base, 0 = flat).</summary>
|
||||
public float HeightNormalIntensity
|
||||
{
|
||||
get => _materialInstance && _materialInstance.HasProperty(HeightNormalIntensityId) ? _materialInstance.GetFloat(HeightNormalIntensityId) : 1.4f;
|
||||
set { if (_materialInstance && _materialInstance.HasProperty(HeightNormalIntensityId)) _materialInstance.SetFloat(HeightNormalIntensityId, value); }
|
||||
}
|
||||
|
||||
/// <summary>Multiplier on the painted-height UV PARALLAX amplitude (1 = base <see cref="ParallaxBase"/>,
|
||||
/// 0 = no displacement). Backed by the material's _Parallax, so it round-trips like the others.</summary>
|
||||
public float ParallaxIntensity
|
||||
{
|
||||
get => _materialInstance && _materialInstance.HasProperty(ParallaxId) ? _materialInstance.GetFloat(ParallaxId) / ParallaxBase : 1f;
|
||||
set { if (_materialInstance && _materialInstance.HasProperty(ParallaxId)) _materialInstance.SetFloat(ParallaxId, Mathf.Max(0f, value) * ParallaxBase); }
|
||||
}
|
||||
|
||||
[Header("Backend")]
|
||||
[Tooltip("ON = GPU painting (fast), OFF = CPU painting. Can be toggled live in Play mode.")]
|
||||
public bool useGpu = true;
|
||||
@@ -325,6 +354,10 @@ public partial class MeshPainter : MonoBehaviour
|
||||
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 HeightNormalIntensityId = Shader.PropertyToID("_HeightNormalIntensity");
|
||||
private static readonly int OcclusionThresholdId = Shader.PropertyToID("_OcclusionThreshold");
|
||||
// _Parallax amplitude that ParallaxIntensity = 1 maps to (the floor ConfigureMaterialPbr lifts to).
|
||||
private const float ParallaxBase = 0.02f;
|
||||
private static readonly int HalfLambertId = Shader.PropertyToID("_HalfLambert");
|
||||
private static readonly int HalfLambertBrightnessId = Shader.PropertyToID("_HalfLambertBrightness");
|
||||
private static readonly int SpecularDampenId = Shader.PropertyToID("_SpecularDampen");
|
||||
|
||||
@@ -251,8 +251,9 @@ public partial class PaintHud
|
||||
{
|
||||
if (!painter || !painter.LoadPaintByName(name)) return;
|
||||
|
||||
// The load also restored the slot's emission intensity + camo knobs onto the painter (v3 slots) ->
|
||||
// push them into the sliders so the HUD reflects the loaded look, exactly like a pipette pick does.
|
||||
// The load also restored the slot's material scalars onto the painter (emission + camo from v3,
|
||||
// the painted-height knobs from v4) -> push them into the sliders so the HUD reflects the loaded
|
||||
// look, exactly like a pipette pick does. SyncCamoSliders covers the camo + height sliders.
|
||||
SyncFromPainter();
|
||||
SyncCamoSliders();
|
||||
|
||||
|
||||
@@ -60,6 +60,12 @@ public partial class PaintHud
|
||||
private bool _hasPendingCamo;
|
||||
private bool _camoEverReady;
|
||||
|
||||
// Painted-height knobs (occlusion threshold, normal intensity, parallax intensity) ride the exact same
|
||||
// material-ready deferral as the camo knobs above: they become writable only after the painter's Start,
|
||||
// so a restored/carried value waits here and is applied in the LateUpdate ready-gate (shared timing).
|
||||
private Vector3 _pendingHeight;
|
||||
private bool _hasPendingHeight;
|
||||
|
||||
// Authored defaults for the runtime reset (= "as if the saved layout file had been deleted"): the
|
||||
// prefab's panel positions, captured before any saved layout is restored over them, and the painter's
|
||||
// prefab/material default brush values, captured before carry/restore mutate the live painter. Camo is
|
||||
@@ -95,6 +101,7 @@ public partial class PaintHud
|
||||
public int version = HudFileVersion;
|
||||
public bool hasValues; // false = no painter was bound when saved -> restore positions only
|
||||
public bool hasCamoValues; // false = pre-camo save file -> keep the material's authored camo (no clobber)
|
||||
public bool hasHeightValues; // false = pre-height save file -> keep the material's authored height knobs (no clobber)
|
||||
public List<PanelPos> panels = new List<PanelPos>();
|
||||
public HudValues values = new HudValues();
|
||||
}
|
||||
@@ -120,6 +127,7 @@ public partial class PaintHud
|
||||
public int sprayDots;
|
||||
public float sprayDotSizeMin, sprayDotSizeMax, sprayDotAlphaMin, sprayDotAlphaMax;
|
||||
public float camoHalfLambert, camoHalfLambertBrightness, camoSpecularDampen; // PlayerCamoLit material knobs
|
||||
public float occlusionThreshold, heightNormalIntensity, parallaxIntensity; // painted-height knobs (material-backed)
|
||||
}
|
||||
|
||||
/// <summary>A draggable panel was dropped -> persist the layout immediately.</summary>
|
||||
@@ -146,6 +154,7 @@ public partial class PaintHud
|
||||
CopyValues(painter, state.values);
|
||||
state.hasValues = true;
|
||||
state.hasCamoValues = painter.IsReady; // only claim camo when it was actually readable
|
||||
state.hasHeightValues = painter.IsReady; // height knobs share the camo readiness
|
||||
}
|
||||
else if (_hasCarry)
|
||||
{
|
||||
@@ -153,6 +162,7 @@ public partial class PaintHud
|
||||
state.values = _carry;
|
||||
state.hasValues = true;
|
||||
state.hasCamoValues = _camoEverReady; // _carry holds valid camo only after a ready painter
|
||||
state.hasHeightValues = _camoEverReady;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(HudSaveDir);
|
||||
@@ -198,6 +208,16 @@ public partial class PaintHud
|
||||
_hasPendingCamo = true;
|
||||
}
|
||||
|
||||
// Same deferral for the painted-height knobs; hasHeightValues guards pre-height save files so they
|
||||
// keep the material's authored defaults instead of being slammed to a 0 threshold/flat relief.
|
||||
if (state.hasHeightValues && state.values != null)
|
||||
{
|
||||
_pendingHeight = new Vector3(state.values.occlusionThreshold,
|
||||
state.values.heightNormalIntensity,
|
||||
state.values.parallaxIntensity);
|
||||
_hasPendingHeight = true;
|
||||
}
|
||||
|
||||
// Point-anchored panels: anchoredPosition is resolution-independent under the canvas'
|
||||
// ScaleWithScreenSize scaler, so the raw saved value places them identically next session.
|
||||
var root = _panel.transform;
|
||||
@@ -229,6 +249,9 @@ public partial class PaintHud
|
||||
_defaults.camoHalfLambert = painter.CamoHalfLambert;
|
||||
_defaults.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness;
|
||||
_defaults.camoSpecularDampen = painter.CamoSpecularDampen;
|
||||
_defaults.occlusionThreshold = painter.OcclusionThreshold;
|
||||
_defaults.heightNormalIntensity = painter.HeightNormalIntensity;
|
||||
_defaults.parallaxIntensity = painter.ParallaxIntensity;
|
||||
_hasDefaultCamo = true;
|
||||
}
|
||||
|
||||
@@ -239,11 +262,21 @@ public partial class PaintHud
|
||||
painter.CamoSpecularDampen = _pendingCamo.z;
|
||||
_hasPendingCamo = false;
|
||||
}
|
||||
if (_hasPendingHeight)
|
||||
{
|
||||
painter.OcclusionThreshold = _pendingHeight.x;
|
||||
painter.HeightNormalIntensity = _pendingHeight.y;
|
||||
painter.ParallaxIntensity = _pendingHeight.z;
|
||||
_hasPendingHeight = false;
|
||||
}
|
||||
SyncCamoSliders();
|
||||
|
||||
_savedValues.camoHalfLambert = _lastSeenValues.camoHalfLambert = painter.CamoHalfLambert;
|
||||
_savedValues.camoHalfLambertBrightness = _lastSeenValues.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness;
|
||||
_savedValues.camoSpecularDampen = _lastSeenValues.camoSpecularDampen = painter.CamoSpecularDampen;
|
||||
_savedValues.occlusionThreshold = _lastSeenValues.occlusionThreshold = painter.OcclusionThreshold;
|
||||
_savedValues.heightNormalIntensity = _lastSeenValues.heightNormalIntensity = painter.HeightNormalIntensity;
|
||||
_savedValues.parallaxIntensity = _lastSeenValues.parallaxIntensity = painter.ParallaxIntensity;
|
||||
|
||||
_camoEverReady = true;
|
||||
_camoSynced = true;
|
||||
@@ -312,7 +345,10 @@ public partial class PaintHud
|
||||
&& p.sprayDotAlphaMax == v.sprayDotAlphaMax
|
||||
&& p.CamoHalfLambert == v.camoHalfLambert
|
||||
&& p.CamoHalfLambertBrightness == v.camoHalfLambertBrightness
|
||||
&& p.CamoSpecularDampen == v.camoSpecularDampen;
|
||||
&& p.CamoSpecularDampen == v.camoSpecularDampen
|
||||
&& p.OcclusionThreshold == v.occlusionThreshold
|
||||
&& p.HeightNormalIntensity == v.heightNormalIntensity
|
||||
&& p.ParallaxIntensity == v.parallaxIntensity;
|
||||
}
|
||||
|
||||
/// <summary>Snapshots the bound painter's brush values into the in-memory mirror.</summary>
|
||||
@@ -338,6 +374,10 @@ public partial class PaintHud
|
||||
_carry.camoHalfLambertBrightness,
|
||||
_carry.camoSpecularDampen);
|
||||
_hasPendingCamo = true;
|
||||
_pendingHeight = new Vector3(_carry.occlusionThreshold,
|
||||
_carry.heightNormalIntensity,
|
||||
_carry.parallaxIntensity);
|
||||
_hasPendingHeight = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,6 +412,9 @@ public partial class PaintHud
|
||||
v.camoHalfLambert = from.CamoHalfLambert;
|
||||
v.camoHalfLambertBrightness = from.CamoHalfLambertBrightness;
|
||||
v.camoSpecularDampen = from.CamoSpecularDampen;
|
||||
v.occlusionThreshold = from.OcclusionThreshold;
|
||||
v.heightNormalIntensity = from.HeightNormalIntensity;
|
||||
v.parallaxIntensity = from.ParallaxIntensity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,11 +441,14 @@ public partial class PaintHud
|
||||
to.sprayDotAlphaMin = v.sprayDotAlphaMin;
|
||||
to.sprayDotAlphaMax = v.sprayDotAlphaMax;
|
||||
|
||||
// Camo setters are a no-op until the material exists; the guaranteed deferred apply runs from the
|
||||
// LateUpdate ready-gate via _pendingCamo. Writing here too keeps Copy/Apply/Equal symmetric.
|
||||
// Camo + height setters are a no-op until the material exists; the guaranteed deferred apply runs
|
||||
// from the LateUpdate ready-gate via _pendingCamo/_pendingHeight. Writing here too keeps Copy/Apply/Equal symmetric.
|
||||
to.CamoHalfLambert = v.camoHalfLambert;
|
||||
to.CamoHalfLambertBrightness = v.camoHalfLambertBrightness;
|
||||
to.CamoSpecularDampen = v.camoSpecularDampen;
|
||||
to.OcclusionThreshold = v.occlusionThreshold;
|
||||
to.HeightNormalIntensity = v.heightNormalIntensity;
|
||||
to.ParallaxIntensity = v.parallaxIntensity;
|
||||
}
|
||||
|
||||
private HudState LoadHudState()
|
||||
@@ -482,15 +528,19 @@ public partial class PaintHud
|
||||
_defaults.camoHalfLambert = painter.CamoHalfLambert;
|
||||
_defaults.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness;
|
||||
_defaults.camoSpecularDampen = painter.CamoSpecularDampen;
|
||||
_defaults.occlusionThreshold = painter.OcclusionThreshold;
|
||||
_defaults.heightNormalIntensity = painter.HeightNormalIntensity;
|
||||
_defaults.parallaxIntensity = painter.ParallaxIntensity;
|
||||
}
|
||||
|
||||
ApplyValues(painter, _defaults);
|
||||
SyncFromPainter();
|
||||
SyncCamoSliders();
|
||||
|
||||
// ApplyValues wrote the camo straight to the material (painter is ready at a runtime reset), so
|
||||
// drop any still-pending restored camo the ready-gate would otherwise re-apply over the default.
|
||||
// ApplyValues wrote the camo + height knobs straight to the material (painter is ready at a
|
||||
// runtime reset), so drop any still-pending restored values the ready-gate would re-apply over the default.
|
||||
_hasPendingCamo = false;
|
||||
_hasPendingHeight = false;
|
||||
|
||||
CaptureCarry(); // a later respawn now carries the defaults, not the pre-reset values
|
||||
SnapshotSavedBaseline(); // values match the (now deleted) save -> the reset itself triggers no auto-save
|
||||
|
||||
@@ -70,6 +70,9 @@ public partial class PaintHud : MonoBehaviour
|
||||
private const string SliderHalfLambert = "Slider_HalfLambert";
|
||||
private const string SliderHalfLambertBrightness = "Slider_HalfLambertBrightness";
|
||||
private const string SliderSpecularDamp = "Slider_SpecularDamp";
|
||||
private const string SliderOcclusionThreshold = "Slider_OcclusionThreshold";
|
||||
private const string SliderNormalIntensity = "Slider_NormalIntensity";
|
||||
private const string SliderParallaxIntensity = "Slider_ParallaxIntensity";
|
||||
private const string MaskSelector = "MaskSelector";
|
||||
private const string MaskSwatchPrefix = "MaskSwatch_";
|
||||
|
||||
@@ -310,6 +313,12 @@ public partial class PaintHud : MonoBehaviour
|
||||
WireSlider(panel, SliderHalfLambertBrightness, painter ? painter.CamoHalfLambertBrightness : 0f, v => { if (painter) painter.CamoHalfLambertBrightness = v; });
|
||||
WireSlider(panel, SliderSpecularDamp, painter ? painter.CamoSpecularDampen : 0f, v => { if (painter) painter.CamoSpecularDampen = v; });
|
||||
|
||||
// Painted-height knobs: material-backed like the camo sliders above, so they are seeded once the
|
||||
// painter is ready (SyncCamoSliders) and a set before then is a safe no-op.
|
||||
WireSlider(panel, SliderOcclusionThreshold, painter ? painter.OcclusionThreshold : 0.2f, v => { if (painter) painter.OcclusionThreshold = v; });
|
||||
WireSlider(panel, SliderNormalIntensity, painter ? painter.HeightNormalIntensity : 1.4f, v => { if (painter) painter.HeightNormalIntensity = v; });
|
||||
WireSlider(panel, SliderParallaxIntensity, painter ? painter.ParallaxIntensity : 1f, v => { if (painter) painter.ParallaxIntensity = v; });
|
||||
|
||||
foreach (var name in DraggablePanels)
|
||||
MakeDraggable(panel, name);
|
||||
}
|
||||
@@ -379,6 +388,10 @@ public partial class PaintHud : MonoBehaviour
|
||||
SetSlider(panel, SliderHalfLambert, painter.CamoHalfLambert);
|
||||
SetSlider(panel, SliderHalfLambertBrightness, painter.CamoHalfLambertBrightness);
|
||||
SetSlider(panel, SliderSpecularDamp, painter.CamoSpecularDampen);
|
||||
// Painted-height knobs share the same material-ready timing as the camo sliders.
|
||||
SetSlider(panel, SliderOcclusionThreshold, painter.OcclusionThreshold);
|
||||
SetSlider(panel, SliderNormalIntensity, painter.HeightNormalIntensity);
|
||||
SetSlider(panel, SliderParallaxIntensity, painter.ParallaxIntensity);
|
||||
}
|
||||
|
||||
private static void SetToggle(Transform panel, string name, bool value)
|
||||
|
||||
@@ -94,6 +94,14 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
new(new Vector3(0.5f, 0f, 0.1f), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
|
||||
private bool _camoApplyPending; // remote: apply _camo.Value to the material once IsReady / on change
|
||||
|
||||
// Painted-height knobs (x = occlusion AO threshold, y = relief normal intensity, z = parallax
|
||||
// intensity). Same per-player material values as the camo knobs - not baked into the maps - so they
|
||||
// ride their own owner-authoritative NetworkVariable and apply once the remote painter is ready. The
|
||||
// default mirrors the PlayerCamoLit shader defaults (0.2 / 1.4 / 1).
|
||||
private readonly NetworkVariable<Vector3> _height =
|
||||
new(new Vector3(0.2f, 1.4f, 1f), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
|
||||
private bool _heightApplyPending; // remote: apply _height.Value to the material once IsReady / on change
|
||||
|
||||
// Monotonic message counter on the owner (one per stamp batch / checkpoint / restore); on every peer,
|
||||
// the highest sequence accounted for (applied to a remote replica / sent by the owner). Used as the
|
||||
// snapshot's cutoff marker so the join window neither loses nor double-applies events.
|
||||
@@ -164,6 +172,10 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
_camo.OnValueChanged += OnCamoChanged;
|
||||
_camoApplyPending = true;
|
||||
|
||||
// Same deferred apply for the painted-height knobs.
|
||||
_height.OnValueChanged += OnHeightChanged;
|
||||
_heightApplyPending = true;
|
||||
|
||||
// The server (host) has been present since the start, so its replica already holds the full
|
||||
// paint via the live stream; only a genuine late joiner needs the baseline pulled over.
|
||||
if (!IsServer)
|
||||
@@ -191,6 +203,7 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
}
|
||||
_emissionIntensity.OnValueChanged -= OnEmissionChanged;
|
||||
_camo.OnValueChanged -= OnCamoChanged;
|
||||
_height.OnValueChanged -= OnHeightChanged;
|
||||
}
|
||||
|
||||
// Owner history events -> appended to the ordered outgoing FIFO, flushed in LateUpdate.
|
||||
@@ -221,6 +234,7 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
// Apply on the next LateUpdate once the material is ready (the camo setters are a no-op before that,
|
||||
// and on a fresh spawn the OnValueChanged may arrive before the painter's Start has built the instance).
|
||||
private void OnCamoChanged(Vector3 previous, Vector3 current) => _camoApplyPending = true;
|
||||
private void OnHeightChanged(Vector3 previous, Vector3 current) => _heightApplyPending = true;
|
||||
|
||||
// ---- Live stamp stream (owner -> server -> remotes) ----
|
||||
|
||||
@@ -252,6 +266,16 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
_camoApplyPending = false;
|
||||
}
|
||||
|
||||
// Remote replica: apply the owner's painted-height knobs once the material exists / on change.
|
||||
if (!IsOwner && _heightApplyPending && _painter && _painter.IsReady)
|
||||
{
|
||||
Vector3 h = _height.Value;
|
||||
_painter.OcclusionThreshold = h.x;
|
||||
_painter.HeightNormalIntensity = h.y;
|
||||
_painter.ParallaxIntensity = h.z;
|
||||
_heightApplyPending = false;
|
||||
}
|
||||
|
||||
if (!IsOwner)
|
||||
return;
|
||||
|
||||
@@ -282,6 +306,11 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
var camo = new Vector3(_painter.CamoHalfLambert, _painter.CamoHalfLambertBrightness, _painter.CamoSpecularDampen);
|
||||
if (_camo.Value != camo)
|
||||
_camo.Value = camo;
|
||||
|
||||
// Publish the painted-height knobs the same way (delta-only, ready-gated).
|
||||
var height = new Vector3(_painter.OcclusionThreshold, _painter.HeightNormalIntensity, _painter.ParallaxIntensity);
|
||||
if (_height.Value != height)
|
||||
_height.Value = height;
|
||||
}
|
||||
|
||||
FlushOutgoing(unlimited: false);
|
||||
|
||||
@@ -40,10 +40,15 @@ half _SpecularDampen;
|
||||
// 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
|
||||
// occlusion so only the LOWER half darkens, with the darkening DEPTH driven by _OcclusionThreshold
|
||||
// (a shift, see below). Declared outside UnityPerMaterial like the camo knobs above (the player is a
|
||||
// handful of instances, so the SRP-batcher break is irrelevant).
|
||||
half _HeightNormalStrength;
|
||||
// Runtime intensities tuned from the paint HUD. _HeightNormalIntensity scales the relief normal
|
||||
// (default 1). _OcclusionThreshold is a SHIFT (0..1, default 0.5): the AO darkening depth at the
|
||||
// deepest painted hole - 0 = no darkening at all, 1 = AO reaches full black at occ=0.
|
||||
half _HeightNormalIntensity;
|
||||
half _OcclusionThreshold;
|
||||
float4 _OcclusionMap_TexelSize;
|
||||
|
||||
// Remaps the painted occlusion/height value into surface AO + a relief normal. Runs after URP's
|
||||
@@ -52,9 +57,14 @@ float4 _OcclusionMap_TexelSize;
|
||||
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.
|
||||
// Only the lower (recessed) half of the painted height darkens. _OcclusionThreshold is a SHIFT =
|
||||
// how DEEP the darkening reaches at the deepest hole: AO fades from 1 (occ>=0.5, neutral/hill)
|
||||
// down to (1 - shift) at occ=0. shift=0 -> AO stays 1 everywhere (no darkening, no black spots),
|
||||
// shift=0.5 -> AO in [0.5,1], shift=1 -> AO in [0,1] (full darkening). Linear over occ in [0,0.5].
|
||||
half occ = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g;
|
||||
surfaceData.occlusion = LerpWhiteTo(saturate(occ * 2.0h), _OcclusionStrength);
|
||||
half depth = saturate(1.0h - occ * 2.0h); // 0 at occ>=0.5 (neutral/hill), 1 at occ=0 (deepest)
|
||||
half ao = 1.0h - depth * _OcclusionThreshold; // darken toward (1 - shift) as the hole deepens
|
||||
surfaceData.occlusion = LerpWhiteTo(ao, _OcclusionStrength);
|
||||
#endif
|
||||
|
||||
#if defined(_PARALLAXMAP)
|
||||
@@ -65,8 +75,9 @@ void ApplyPaintedHeightSurface(float2 uv, inout SurfaceData surfaceData)
|
||||
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));
|
||||
half strength = _HeightNormalStrength * _HeightNormalIntensity;
|
||||
half3 reliefTS = normalize(half3((hL - hR) * strength,
|
||||
(hD - hU) * strength, 1.0h));
|
||||
surfaceData.normalTS = BlendNormalRNM(surfaceData.normalTS, reliefTS);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@ 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
|
||||
// Runtime knobs for the painted-height look (1 = base, tuned live in the paint HUD):
|
||||
_HeightNormalIntensity("Painted Height Normal Intensity", Range(0.0, 2.0)) = 1.4
|
||||
// AO darkening DEPTH (shift): AO fades from 1 down to (1 - this) at the deepest painted hole.
|
||||
// 0 = no darkening (no black spots), 1 = full black at occ=0.
|
||||
_OcclusionThreshold("Painted Occlusion AO Shift", Range(0.0, 1.0)) = 0.2
|
||||
|
||||
_OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0
|
||||
_OcclusionMap("Occlusion", 2D) = "white" {}
|
||||
|
||||
Reference in new issue
Block a user