diff --git a/Assets/Scripts/HsvColorPicker.cs b/Assets/Scripts/HsvColorPicker.cs
index 9d67ef5..8bd68ff 100644
--- a/Assets/Scripts/HsvColorPicker.cs
+++ b/Assets/Scripts/HsvColorPicker.cs
@@ -3,35 +3,33 @@ using UnityEngine;
using UnityEngine.UI;
///
-/// Compact runtime HSV colour picker: a saturation/value square, a hue bar, an alpha bar and a
-/// live swatch. The square is a pure-hue base with two static gradient overlays on top (white-out
-/// -> saturation, black-out -> value), so only the base tint changes per hue - no per-frame
-/// texture rebuilds. Hue is never lost while dragging the square (the square only edits S/V,
-/// the bar only edits H, the alpha bar only edits A).
+/// Compact runtime HSV colour picker: a saturation/value square, a hue bar and a live swatch. The
+/// square is a pure-hue base with two static gradient overlays on top (white-out -> saturation,
+/// black-out -> value), so only the base tint changes per hue - no per-frame texture rebuilds. Hue is
+/// never lost while dragging the square (the square only edits S/V, the bar only edits H). Alpha is
+/// edited separately by a standalone slider in the HUD, not by this picker;
+/// still stores the incoming alpha so round-trips it.
///
/// creates the child structure (stable names, no sprites or hooks); it can
/// be baked into a prefab. then (re)assigns the runtime gradient sprites
/// (which a prefab cannot store) and (re)hooks the drag relays - it runs for both the code-built
-/// picker and one instantiated from a prefab, so the two are behaviourally identical. For a prefab
-/// baked before the alpha bar existed it also builds that bar on the fly (and grows the picker's
-/// reserved layout height), so no re-bake is required.
+/// picker and one instantiated from a prefab, so the two are behaviourally identical. A leftover
+/// alpha bar from a pre-split prefab is hidden by , so no re-bake is required.
///
public class HsvColorPicker : MonoBehaviour
{
- /// Raised with the new colour when the user moves the square, hue bar or alpha bar.
+ /// Raised with the new colour (RGB; alpha is preserved, not edited here) when the user moves
+ /// the square or hue bar.
public event Action Changed;
private float _h, _s, _v, _a = 1f;
- // Gap between the stacked square / hue / alpha rows (matches the layout in Build).
+ // Gap between the stacked square / hue rows (matches the layout in Build).
private const float RowGap = 6f;
- private static readonly Color AlphaBackdrop = new Color(0.5f, 0.5f, 0.5f, 1f);
private Image _hueBase; // pure-hue background of the SV square
private RectTransform _svHandle;
private RectTransform _hueHandle;
- private Image _alphaGrad; // colour-tinted transparency ramp of the alpha bar
- private RectTransform _alphaHandle;
private Image _swatch;
/// Current colour (HSV -> RGB, with the preserved alpha).
@@ -54,7 +52,7 @@ public class HsvColorPicker : MonoBehaviour
}
///
- /// Builds the picker structure (square + hue bar + alpha bar + swatch) under this object,
+ /// Builds the picker structure (square + hue bar + swatch) under this object,
/// top-left aligned. Layout only - no gradient sprites and no event hooks, so the result is
/// safe to bake into a prefab. Call afterwards (at runtime) to make it
/// functional.
@@ -87,41 +85,12 @@ public class HsvColorPicker : MonoBehaviour
hueHandle.sizeDelta = new Vector2(4f, hueHeight + 4f);
PaintHud.AddImage(hueHandle.gameObject, Color.white).raycastTarget = false;
- // Alpha bar below the hue bar.
- BuildAlphaBar(sv, hue);
-
// Swatch, to the right of the square.
var sw = PaintHud.NewRect("Swatch", transform);
TopLeft(sw, new Vector2(squareSize + 10f, 0f), new Vector2(34f, 34f));
PaintHud.AddImage(sw.gameObject, Color.white).raycastTarget = false;
}
- ///
- /// Builds the alpha bar (grey backdrop + colour-tinted transparency ramp + drag relay + handle)
- /// directly below the hue bar, sized/positioned from the existing SV square and hue bar so it
- /// works for both a fresh build and an older baked prefab. Sprites/hooks are applied by
- /// .
- ///
- private RectTransform BuildAlphaBar(RectTransform sv, RectTransform hue)
- {
- float width = sv.sizeDelta.x;
- float barHeight = hue.sizeDelta.y;
- float y = hue.anchoredPosition.y - (barHeight + RowGap);
-
- var alpha = PaintHud.NewRect("Alpha", transform);
- TopLeft(alpha, new Vector2(0f, y), new Vector2(width, barHeight));
- PaintHud.AddImage(alpha.gameObject, AlphaBackdrop); // raycast target for the drag relay
-
- PaintHud.AddOverlay(alpha, "AlphaGrad"); // tinted alpha ramp, sprite assigned in Rebind
-
- alpha.gameObject.AddComponent();
-
- var handle = PaintHud.NewRect("Handle", alpha);
- handle.sizeDelta = new Vector2(4f, barHeight + 4f);
- PaintHud.AddImage(handle.gameObject, Color.white).raycastTarget = false;
- return alpha;
- }
-
///
/// Re-finds the child widgets, (re)assigns the runtime gradient sprites and (re)hooks the drag
/// relays. Idempotent (unsubscribe-then-subscribe), so it is safe whether the structure was just
@@ -161,32 +130,10 @@ public class HsvColorPicker : MonoBehaviour
}
}
- // Alpha bar: build it if the (older) prefab lacks it, then (re)assign sprite + relay.
- var alpha = transform.Find("Alpha") as RectTransform;
- bool builtAlpha = false;
- if (!alpha && sv is RectTransform svRect && hue)
- {
- alpha = BuildAlphaBar(svRect, hue);
- builtAlpha = true;
- }
- if (alpha)
- {
- var grad = alpha.Find("AlphaGrad");
- if (grad && grad.TryGetComponent(out Image gradImg))
- {
- gradImg.sprite = PaintHud.AlphaSprite();
- _alphaGrad = gradImg;
- }
- _alphaHandle = alpha.Find("Handle") as RectTransform;
-
- if (alpha.TryGetComponent(out UiPointerSurface alphaPointer))
- {
- alphaPointer.Picked -= OnAlphaPicked;
- alphaPointer.Picked += OnAlphaPicked;
- }
-
- if (builtAlpha) GrowReservedHeight(alpha.sizeDelta.y + RowGap);
- }
+ // Alpha now lives on a standalone slider (PaintHud), not in the picker. Hide any alpha bar a
+ // pre-split prefab still carries so it does not show as a dead, non-functional strip.
+ var leftoverAlpha = transform.Find("Alpha");
+ if (leftoverAlpha) leftoverAlpha.gameObject.SetActive(false);
var swatch = transform.Find("Swatch");
if (swatch) _swatch = swatch.GetComponent();
@@ -194,15 +141,6 @@ public class HsvColorPicker : MonoBehaviour
Refresh();
}
- /// Grows this picker's LayoutElement so a runtime-added alpha bar does not overlap the
- /// widget below it; the panel's ContentSizeFitter then expands to match. No-op without a LayoutElement.
- private void GrowReservedHeight(float extra)
- {
- if (!TryGetComponent(out LayoutElement le)) return;
- if (le.minHeight > 0f) le.minHeight += extra;
- if (le.preferredHeight > 0f) le.preferredHeight += extra;
- }
-
private void OnSvPicked(Vector2 p)
{
_s = p.x;
@@ -218,13 +156,6 @@ public class HsvColorPicker : MonoBehaviour
Changed?.Invoke(Value);
}
- private void OnAlphaPicked(Vector2 p)
- {
- _a = p.x;
- Refresh();
- Changed?.Invoke(Value);
- }
-
private void Refresh()
{
if (_hueBase) _hueBase.color = Color.HSVToRGB(_h, 1f, 1f);
@@ -238,12 +169,6 @@ public class HsvColorPicker : MonoBehaviour
_hueHandle.anchorMin = _hueHandle.anchorMax = new Vector2(_h, 0.5f);
_hueHandle.anchoredPosition = Vector2.zero;
}
- if (_alphaGrad) _alphaGrad.color = Color.HSVToRGB(_h, _s, _v); // tint the transparency ramp to the current colour
- if (_alphaHandle)
- {
- _alphaHandle.anchorMin = _alphaHandle.anchorMax = new Vector2(_a, 0.5f);
- _alphaHandle.anchoredPosition = Vector2.zero;
- }
if (_swatch) _swatch.color = Color.HSVToRGB(_h, _s, _v);
}
diff --git a/Assets/Scripts/MeshPainter.Persistence.cs b/Assets/Scripts/MeshPainter.Persistence.cs
index 10dde16..0701d45 100644
--- a/Assets/Scripts/MeshPainter.Persistence.cs
+++ b/Assets/Scripts/MeshPainter.Persistence.cs
@@ -16,9 +16,21 @@ 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 = 2; // v2: added the display-name header
+ private const int PaintFileVersion = 3; // v3: added the per-player material scalars (emission + camo)
private const string PaintFileExt = ".paint";
+ ///
+ /// Per-player material scalars stored alongside the maps from v3 on. These are NOT baked into the
+ /// painted pixels (a global emission multiplier + the PlayerCamoLit knobs), so they ride the slot
+ /// header and are re-applied onto the painter when the slot loads.
+ ///
+ private struct PaintScalars
+ {
+ public bool has; // false = legacy v2 slot with no scalars -> leave the painter's values untouched
+ public float emissionIntensity;
+ public float camoHalfLambert, camoHalfLambertBrightness, camoSpecularDampen;
+ }
+
private static string PaintLibraryDir =>
Path.Combine(Application.persistentDataPath, "MeshPaint", "Saved");
@@ -83,6 +95,14 @@ public partial class MeshPainter
w.Write(textureSize);
w.Write(MAP_COUNT);
+ // v3: per-player material scalars that are NOT baked into the maps (a global emission
+ // multiplier + the PlayerCamoLit knobs). The painter is ready here (_setupValid), so the
+ // material-backed camo getters return the live values rather than the not-ready 0.
+ w.Write(emissionIntensity);
+ w.Write(CamoHalfLambert);
+ w.Write(CamoHalfLambertBrightness);
+ w.Write(CamoSpecularDampen);
+
var bytes = new byte[n * 4];
for (int mi = 0; mi < MAP_COUNT; mi++)
{
@@ -116,7 +136,7 @@ public partial class MeshPainter
{
if (!_setupValid) return false;
- Color32[][] canonical = ReadMaps(PathForName(name));
+ Color32[][] canonical = ReadMaps(PathForName(name), out PaintScalars scalars);
if (canonical == null) return false;
// Re-seed exactly like SwitchBackend / ApplyPaintSnapshot, so the island/dilation seeding path
@@ -126,6 +146,19 @@ public partial class MeshPainter
// A load replaces the whole painting and is NOT undoable: the loaded maps become the new history
// baseline, clearing any prior undo/redo. Peers reset to the same baseline via the load broadcast.
ResetHistoryToCurrent(canonical);
+
+ // v3 slots also carry the per-player material scalars; apply them so the loaded look is complete.
+ // Legacy v2 slots have none -> leave the current emission/camo untouched. The painter is ready here
+ // (_setupValid), so the material-backed camo setters take effect; PlayerPaintSync replicates the
+ // changed values to peers via their owner-authoritative NetworkVariables on its next LateUpdate.
+ if (scalars.has)
+ {
+ emissionIntensity = scalars.emissionIntensity;
+ RefreshEmissionColor();
+ CamoHalfLambert = scalars.camoHalfLambert;
+ CamoHalfLambertBrightness = scalars.camoHalfLambertBrightness;
+ CamoSpecularDampen = scalars.camoSpecularDampen;
+ }
return true;
}
@@ -155,7 +188,8 @@ public partial class MeshPainter
using (var r = new BinaryReader(fs, Encoding.UTF8))
{
if (r.ReadUInt32() != PaintFileMagic) return null;
- if (r.ReadInt32() != PaintFileVersion) return null;
+ int ver = r.ReadInt32();
+ if (ver != 2 && ver != PaintFileVersion) return null; // list legacy v2 slots + current
return r.ReadString();
}
}
@@ -169,8 +203,9 @@ public partial class MeshPainter
/// Reads all maps from a slot file in canonical layout, or null if missing / incompatible. Per-map
/// null entries are tolerated by both backends (they fall back to the material seed).
///
- private Color32[][] ReadMaps(string path)
+ private Color32[][] ReadMaps(string path, out PaintScalars scalars)
{
+ scalars = default;
if (!File.Exists(path)) return null;
try
@@ -180,12 +215,22 @@ public partial class MeshPainter
{
if (r.ReadUInt32() != PaintFileMagic) return null;
int ver = r.ReadInt32();
- if (ver != PaintFileVersion) return null;
+ if (ver != 2 && ver != PaintFileVersion) return null; // read legacy v2 + current
r.ReadString(); // display name (header only; not needed for the pixel load)
int w = r.ReadInt32();
int h = r.ReadInt32();
int maps = r.ReadInt32();
+ // v3+: the per-player material scalars sit between the header and the pixel maps.
+ if (ver >= 3)
+ {
+ scalars.has = true;
+ scalars.emissionIntensity = r.ReadSingle();
+ scalars.camoHalfLambert = r.ReadSingle();
+ scalars.camoHalfLambertBrightness = r.ReadSingle();
+ scalars.camoSpecularDampen = r.ReadSingle();
+ }
+
// Resolution change -> ignore rather than smear mismatched data; the brush re-seeds
// from the material instead.
if (w != textureSize || h != textureSize)
diff --git a/Assets/Scripts/PaintHud.Library.cs b/Assets/Scripts/PaintHud.Library.cs
index a375507..fb52090 100644
--- a/Assets/Scripts/PaintHud.Library.cs
+++ b/Assets/Scripts/PaintHud.Library.cs
@@ -21,6 +21,7 @@ public partial class PaintHud
private const string LibSaveButton = "Lib_Save";
private const string LibLoadButton = "Lib_Load";
private const string LibDeleteButton = "Lib_Delete";
+ private const string LibResetButton = "Lib_ResetHud";
private const string LibSaveDialog = "Lib_SaveDialog";
private const string LibNameInput = "Lib_NameInput";
private const string LibSaveInfo = "Lib_SaveInfo";
@@ -34,7 +35,7 @@ public partial class PaintHud
private bool _libraryBound;
private RectTransform _listContent;
private GameObject _entryTemplate;
- private Button _saveButton, _loadButton, _deleteButton;
+ private Button _saveButton, _loadButton, _deleteButton, _resetButton;
private string _selectedPainting;
// Save dialog
@@ -76,9 +77,11 @@ public partial class PaintHud
_saveButton = Find