using System; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; #if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem.UI; #endif /// /// In-game paint UI. Builds its own Screen Space - Overlay canvas (and an EventSystem /// if the scene has none) entirely in code and binds the widgets directly to the live /// fields, so changes apply immediately. The panel is shown /// only while the active is (it follows /// the manager's ModeChanged event). /// /// First, simple pass: channel toggles, an HSV colour picker for albedo, metallic / /// smoothness / occlusion sliders, an HSV picker + intensity for emission, and brush /// size. Attach to an empty GameObject; everything else is generated at runtime. /// [DisallowMultipleComponent] public class PaintHud : MonoBehaviour { [Tooltip("Mode manager driving visibility. Empty = found automatically.")] public GameModeManager modeManager; [Tooltip("Painter the HUD edits. Empty = the manager's painter, else found automatically.")] public MeshPainter painter; private static readonly Color PanelColor = new Color(0.09f, 0.09f, 0.11f, 0.9f); private static readonly Color Accent = new Color(0.40f, 0.75f, 1f, 1f); private static readonly Color BoxColor = new Color(0.20f, 0.20f, 0.24f, 1f); private GameObject _panel; private Font _font; private void Awake() { if (!modeManager) modeManager = FindFirstObjectByType(); } private void OnEnable() { if (modeManager) modeManager.ModeChanged += OnModeChanged; } private void OnDisable() { if (modeManager) modeManager.ModeChanged -= OnModeChanged; } private void Start() { if (!painter) painter = modeManager ? modeManager.Painter : null; if (!painter) painter = FindFirstObjectByType(); _font = Resources.GetBuiltinResource("LegacyRuntime.ttf"); if (!_font) _font = Resources.GetBuiltinResource("Arial.ttf"); EnsureEventSystem(); BuildUi(); // Set initial visibility (the manager's first SwitchTo may have fired before this Start). OnModeChanged(modeManager ? modeManager.ActiveMode : null); } private void OnModeChanged(GameMode mode) { if (_panel) _panel.SetActive(mode is PaintMode); } // --- UI construction --- private void BuildUi() { var canvas = GetComponent(); if (!canvas) canvas = gameObject.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 100; var scaler = GetComponent(); if (!scaler) scaler = gameObject.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920f, 1080f); scaler.matchWidthOrHeight = 0.5f; if (!GetComponent()) gameObject.AddComponent(); // Panel (this is what gets shown/hidden per mode). var panel = NewRect("Panel", transform); _panel = panel.gameObject; panel.anchorMin = panel.anchorMax = new Vector2(0f, 1f); panel.pivot = new Vector2(0f, 1f); panel.anchoredPosition = new Vector2(16f, -16f); panel.sizeDelta = new Vector2(296f, 0f); // width fixed; height from the ContentSizeFitter AddImage(panel.gameObject, PanelColor); var vlg = panel.gameObject.AddComponent(); vlg.padding = new RectOffset(12, 12, 12, 12); vlg.spacing = 8f; vlg.childAlignment = TextAnchor.UpperLeft; vlg.childControlWidth = true; vlg.childControlHeight = true; vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false; var fitter = panel.gameObject.AddComponent(); fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; AddSectionLabel(panel, "PAINT", 15, FontStyle.Bold); // Channels. AddSectionLabel(panel, "Channels", 13, FontStyle.Bold); AddToggle(panel, "Albedo", painter && painter.paintAlbedo, v => { if (painter) painter.paintAlbedo = v; }); AddToggle(panel, "Metallic + Smoothness", painter && painter.paintMetallicSmoothness, v => { if (painter) painter.paintMetallicSmoothness = v; }); AddToggle(panel, "Occlusion", painter && painter.paintOcclusion, v => { if (painter) painter.paintOcclusion = v; }); AddToggle(panel, "Emission", painter && painter.paintEmission, v => { if (painter) painter.paintEmission = v; }); // Albedo colour. AddSectionLabel(panel, "Albedo Color", 13, FontStyle.Bold); var albedo = AddColorPicker(panel); albedo.SetColor(painter ? painter.brushColor : Color.red); albedo.Changed += c => { if (painter) painter.brushColor = new Color(c.r, c.g, c.b, painter.brushColor.a); }; // Metallic / smoothness / occlusion. AddSlider(panel, "Metallic", 0f, 1f, false, painter ? painter.metallic : 0f, v => { if (painter) painter.metallic = v; }); AddSlider(panel, "Smoothness", 0f, 1f, false, painter ? painter.smoothness : 0.5f, v => { if (painter) painter.smoothness = v; }); AddSlider(panel, "Occlusion", 0f, 1f, false, painter ? painter.occlusion : 0.5f, v => { if (painter) painter.occlusion = v; }); // Emission. AddSectionLabel(panel, "Emission Color", 13, FontStyle.Bold); var emission = AddColorPicker(panel); emission.SetColor(painter ? painter.emissionColor : Color.black); emission.Changed += c => { if (painter) painter.emissionColor = new Color(c.r, c.g, c.b, painter.emissionColor.a); }; AddSlider(panel, "Emission Intensity", 0f, 8f, false, painter ? painter.emissionIntensity : 1f, v => { if (painter) painter.emissionIntensity = v; }); // Brush size. AddSlider(panel, "Brush Size", 1f, 256f, true, painter ? painter.brushSize : 16f, v => { if (painter) painter.brushSize = Mathf.RoundToInt(v); }); } private void EnsureEventSystem() { if (FindFirstObjectByType()) return; var es = new GameObject("EventSystem", typeof(EventSystem)); #if ENABLE_INPUT_SYSTEM var module = es.AddComponent(); if (!module.actionsAsset) module.AssignDefaultActions(); #else es.AddComponent(); #endif } // --- Widgets --- private void AddSectionLabel(RectTransform panel, string text, int size, FontStyle style) { var rt = NewRect("Label", panel); var le = rt.gameObject.AddComponent(); le.minHeight = le.preferredHeight = 18f; var t = MakeText(rt.gameObject, text, size, style, new Color(0.85f, 0.85f, 0.9f, 1f), TextAnchor.MiddleLeft); t.raycastTarget = false; } private void AddToggle(RectTransform panel, string label, bool value, Action onChanged) { var rt = NewRect("Toggle", panel); var le = rt.gameObject.AddComponent(); le.minHeight = le.preferredHeight = 22f; var toggle = rt.gameObject.AddComponent(); var box = NewRect("Box", rt); box.anchorMin = box.anchorMax = new Vector2(0f, 0.5f); box.pivot = new Vector2(0f, 0.5f); box.anchoredPosition = Vector2.zero; box.sizeDelta = new Vector2(18f, 18f); var boxImg = AddImage(box.gameObject, BoxColor); var check = NewRect("Check", box); check.anchorMin = Vector2.zero; check.anchorMax = Vector2.one; check.offsetMin = new Vector2(3f, 3f); check.offsetMax = new Vector2(-3f, -3f); var checkImg = AddImage(check.gameObject, Accent); checkImg.raycastTarget = false; var lbl = NewRect("Label", rt); lbl.anchorMin = Vector2.zero; lbl.anchorMax = Vector2.one; lbl.offsetMin = new Vector2(24f, 0f); lbl.offsetMax = Vector2.zero; var t = MakeText(lbl.gameObject, label, 14, FontStyle.Normal, Color.white, TextAnchor.MiddleLeft); t.raycastTarget = false; toggle.targetGraphic = boxImg; toggle.graphic = checkImg; toggle.isOn = value; toggle.onValueChanged.AddListener(v => onChanged(v)); } private void AddSlider(RectTransform panel, string label, float min, float max, bool whole, float value, Action onChanged) { var rt = NewRect("Slider", panel); var le = rt.gameObject.AddComponent(); le.minHeight = le.preferredHeight = 34f; // Caption row (label left, value right), pinned to the top of the cell. var lbl = NewRect("Label", rt); lbl.anchorMin = new Vector2(0f, 1f); lbl.anchorMax = new Vector2(1f, 1f); lbl.pivot = new Vector2(0f, 1f); lbl.offsetMin = new Vector2(0f, -16f); lbl.offsetMax = new Vector2(-48f, 0f); var lt = MakeText(lbl.gameObject, label, 13, FontStyle.Normal, new Color(0.8f, 0.8f, 0.85f, 1f), TextAnchor.MiddleLeft); lt.raycastTarget = false; var valRt = NewRect("Value", rt); valRt.anchorMin = new Vector2(1f, 1f); valRt.anchorMax = new Vector2(1f, 1f); valRt.pivot = new Vector2(1f, 1f); valRt.anchoredPosition = Vector2.zero; valRt.sizeDelta = new Vector2(56f, 16f); var vt = MakeText(valRt.gameObject, "", 13, FontStyle.Normal, Color.white, TextAnchor.MiddleRight); vt.raycastTarget = false; // Track, pinned to the bottom of the cell. var track = NewRect("Track", rt); track.anchorMin = new Vector2(0f, 0f); track.anchorMax = new Vector2(1f, 0f); track.pivot = new Vector2(0.5f, 0f); track.anchoredPosition = Vector2.zero; track.sizeDelta = new Vector2(0f, 16f); var slider = track.gameObject.AddComponent(); var bg = NewRect("Background", track); bg.anchorMin = new Vector2(0f, 0.5f); bg.anchorMax = new Vector2(1f, 0.5f); bg.pivot = new Vector2(0.5f, 0.5f); bg.anchoredPosition = Vector2.zero; bg.sizeDelta = new Vector2(0f, 5f); AddImage(bg.gameObject, BoxColor); // raycast target so clicking the bar jumps the value var fillArea = NewRect("Fill Area", track); fillArea.anchorMin = new Vector2(0f, 0.5f); fillArea.anchorMax = new Vector2(1f, 0.5f); fillArea.pivot = new Vector2(0.5f, 0.5f); fillArea.anchoredPosition = Vector2.zero; fillArea.sizeDelta = new Vector2(-16f, 5f); var fill = NewRect("Fill", fillArea); fill.anchorMin = Vector2.zero; fill.anchorMax = Vector2.one; fill.offsetMin = Vector2.zero; fill.offsetMax = Vector2.zero; var fillImg = AddImage(fill.gameObject, Accent); fillImg.raycastTarget = false; var handleArea = NewRect("Handle Slide Area", track); handleArea.anchorMin = Vector2.zero; handleArea.anchorMax = Vector2.one; handleArea.offsetMin = new Vector2(8f, 0f); handleArea.offsetMax = new Vector2(-8f, 0f); var handle = NewRect("Handle", handleArea); handle.sizeDelta = new Vector2(14f, 0f); var handleImg = AddImage(handle.gameObject, Color.white); slider.fillRect = fill; slider.handleRect = handle; slider.targetGraphic = handleImg; slider.direction = Slider.Direction.LeftToRight; slider.minValue = min; slider.maxValue = max; slider.wholeNumbers = whole; slider.value = value; Action setText = v => vt.text = whole ? Mathf.RoundToInt(v).ToString() : v.ToString("0.00"); setText(slider.value); // show the actual (clamped) value, not the unclamped input slider.onValueChanged.AddListener(v => { setText(v); onChanged(v); }); } private HsvColorPicker AddColorPicker(RectTransform panel) { const float square = 120f; const float hueH = 14f; var rt = NewRect("ColorPicker", panel); var le = rt.gameObject.AddComponent(); le.minHeight = le.preferredHeight = square + 6f + hueH; var picker = rt.gameObject.AddComponent(); picker.Build(square, hueH); return picker; } private Text MakeText(GameObject go, string text, int size, FontStyle style, Color color, TextAnchor anchor) { var t = go.AddComponent(); t.font = _font; t.fontSize = size; t.fontStyle = style; t.color = color; t.alignment = anchor; t.text = text; t.horizontalOverflow = HorizontalWrapMode.Overflow; t.verticalOverflow = VerticalWrapMode.Truncate; return t; } // --- Static UI factory (shared with HsvColorPicker) --- public static RectTransform NewRect(string name, Transform parent) { var go = new GameObject(name, typeof(RectTransform)); go.transform.SetParent(parent, false); return (RectTransform)go.transform; } public static Image AddImage(GameObject go, Color color) { var img = go.AddComponent(); img.color = color; return img; } /// Adds a full-rect, non-raycasting overlay image with the given sprite. public static Image AddOverlay(RectTransform parent, Sprite sprite) { var rt = NewRect("Overlay", parent); rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; var img = rt.gameObject.AddComponent(); img.sprite = sprite; img.type = Image.Type.Simple; img.raycastTarget = false; return img; } // Cached gradient sprites for the colour picker. private static Sprite _satSprite, _valSprite, _hueSprite; /// Horizontal white-out: opaque white (left) -> transparent (right) = saturation. public static Sprite SaturationSprite() { if (_satSprite) return _satSprite; const int n = 64; var tex = NewGradientTex(n, 1); for (int x = 0; x < n; x++) { byte a = (byte)Mathf.RoundToInt(255f * (1f - x / (float)(n - 1))); tex.SetPixel(x, 0, new Color32(255, 255, 255, a)); } tex.Apply(false); _satSprite = ToSprite(tex); return _satSprite; } /// Vertical black-out: opaque black (bottom) -> transparent (top) = value. public static Sprite ValueSprite() { if (_valSprite) return _valSprite; const int n = 64; var tex = NewGradientTex(1, n); for (int y = 0; y < n; y++) { byte a = (byte)Mathf.RoundToInt(255f * (1f - y / (float)(n - 1))); tex.SetPixel(0, y, new Color32(0, 0, 0, a)); } tex.Apply(false); _valSprite = ToSprite(tex); return _valSprite; } /// Horizontal full-saturation hue ramp 0..1. public static Sprite HueSprite() { if (_hueSprite) return _hueSprite; const int n = 64; var tex = NewGradientTex(n, 1); for (int x = 0; x < n; x++) tex.SetPixel(x, 0, Color.HSVToRGB(x / (float)(n - 1), 1f, 1f)); tex.Apply(false); _hueSprite = ToSprite(tex); return _hueSprite; } private static Texture2D NewGradientTex(int w, int h) { return new Texture2D(w, h, TextureFormat.RGBA32, false, false) // sRGB sampling for correct colours { name = "PaintHud_Gradient", wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear, hideFlags = HideFlags.HideAndDontSave }; } private static Sprite ToSprite(Texture2D tex) { var s = Sprite.Create(tex, new Rect(0f, 0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f); s.hideFlags = HideFlags.HideAndDontSave; return s; } }