Add spray brush, paint interval, and per-channel paint opacity

Spray brush mode: bakes a procedural scattered-dot mask per stamp (random
position, size, alpha within the brush disc) and paints it through the
existing single-stamp pipeline; dot centers clamped to (0.5 - dotR) so the
spray footprint matches the solid brush radius.

Paint interval: float on MeshPainter; 0 keeps the continuous interpolated
stroke, >0 emits a single timed dab every x.xx seconds.

Color picker alpha: horizontal alpha bar on both HSV pickers, runtime-built
in Rebind (no prefab re-bake), passed through to brushColor.a / emissionColor.a.

Metallic/smoothness + occlusion paint opacity: new opacity fields applied in
both CPU and GPU backends (GPU MSO draw split into MS and occlusion sub-stamps
for independent opacity), with cloned alpha sliders wired into the HUD.

Also includes the prefab layout tweaks and scene adjustments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-24 19:07:28 +02:00
1 parent af3aa34d66
commit b07d9a22d2
9 files changed
+1227 -86

No files matched your search

+98 -11
View File
@@ -3,27 +3,35 @@ using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 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
/// 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 bar only edits H, the alpha bar only edits A).
///
/// <see cref="Build"/> creates the child structure (stable names, no sprites or hooks); it can
/// be baked into a prefab. <see cref="Rebind"/> 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.
/// 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.
/// </summary>
public class HsvColorPicker : MonoBehaviour
{
/// <summary>Raised with the new colour when the user moves the square or hue bar.</summary>
/// <summary>Raised with the new colour when the user moves the square, hue bar or alpha bar.</summary>
public event Action<Color> Changed;
private float _h, _s, _v, _a = 1f;
// Gap between the stacked square / hue / alpha 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;
/// <summary>Current colour (HSV -> RGB, with the preserved alpha).</summary>
@@ -46,9 +54,10 @@ public class HsvColorPicker : MonoBehaviour
}
/// <summary>
/// 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 <see cref="Rebind"/> afterwards (at runtime) to make it functional.
/// Builds the picker structure (square + hue bar + alpha 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 <see cref="Rebind"/> afterwards (at runtime) to make it
/// functional.
/// </summary>
public void Build(float squareSize, float hueHeight)
{
@@ -68,7 +77,7 @@ public class HsvColorPicker : MonoBehaviour
// Hue bar below the square.
var hue = PaintHud.NewRect("Hue", transform);
TopLeft(hue, new Vector2(0f, -(squareSize + 6f)), new Vector2(squareSize, hueHeight));
TopLeft(hue, new Vector2(0f, -(squareSize + RowGap)), new Vector2(squareSize, hueHeight));
var hueImg = hue.gameObject.AddComponent<Image>();
hueImg.type = Image.Type.Simple;
@@ -78,12 +87,41 @@ 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;
}
/// <summary>
/// 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
/// <see cref="Rebind"/>.
/// </summary>
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<UiPointerSurface>();
var handle = PaintHud.NewRect("Handle", alpha);
handle.sizeDelta = new Vector2(4f, barHeight + 4f);
PaintHud.AddImage(handle.gameObject, Color.white).raycastTarget = false;
return alpha;
}
/// <summary>
/// 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
@@ -110,7 +148,7 @@ public class HsvColorPicker : MonoBehaviour
}
}
var hue = transform.Find("Hue");
var hue = transform.Find("Hue") as RectTransform;
if (hue)
{
if (hue.TryGetComponent(out Image hueImg)) hueImg.sprite = PaintHud.HueSprite();
@@ -123,12 +161,48 @@ 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);
}
var swatch = transform.Find("Swatch");
if (swatch) _swatch = swatch.GetComponent<Image>();
Refresh();
}
/// <summary>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.</summary>
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;
@@ -144,6 +218,13 @@ 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);
@@ -157,6 +238,12 @@ 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);
}