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