using System; using System.Collections.Generic; using Uigenx.Animation; using Uigenx.Binding; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.UIElements; namespace Uigenx.Effects { public enum UgxMaterialParamKind { Float = 0, Color = 1, Vector = 2, } /// One material parameter driven from a data source path. public readonly struct UgxMaterialParam { public readonly string Name; public readonly UgxMaterialParamKind Kind; public readonly string Path; public UgxMaterialParam(string name, UgxMaterialParamKind kind, string path) { Name = name; Kind = kind; Path = path; } } /// /// One row of the compiled material table. There is no material reference here on purpose: the /// stylesheet assigns it with `-unity-material`, because generated C# cannot hold an asset /// reference without a Resources lookup. Constants belong in the `.mat` asset, so a row exists /// only when at least one parameter is data driven. /// public readonly struct UgxMaterialEntry { public readonly string ElementName; public readonly UgxMaterialParam[] Params; public UgxMaterialEntry(string elementName, params UgxMaterialParam[] parameters) { ElementName = elementName; Params = parameters ?? Array.Empty(); } } /// /// Assigns a material to an element through the USS `-unity-material` property and drives its /// parameters from tokens or live data. /// /// /// Verified against Unity 6000.5: `IStyle.unityMaterial` is a `StyleMaterialDefinition` and /// `IResolvedStyle.unityMaterial` is a `MaterialDefinition`. `MaterialDefinition` is a struct, /// so the definition is kept here and pushed back into the style after every change. The /// material affects the element and all of its children, which is why per element instancing /// is opt in. UI Shader Graph materials are URP only; on any other pipeline the binder warns /// once and leaves the element unstyled. /// public sealed class UgxMaterialBinder : IDisposable { static bool _pipelineWarningShown; readonly VisualElement _element; readonly Material _instance; readonly List _bound = new List(4); MaterialDefinition _definition; INotifyBindablePropertyChanged _source; IUgxDataSource _dataSource; bool _disposed; // True between Adopt and the first geometry pass, while the material is not resolved yet. bool _adopting; /// /// Drives the material the element already got from USS `-unity-material`, instead of /// taking one as an argument. This is the path generated code uses: a C# table cannot hold /// an asset reference, but USS resolves one at import time, so the stylesheet assigns the /// material and this adopts it to drive its parameters. /// /// /// The resolved style is not available until the element is in a panel and has been laid /// out, so adoption is deferred to the first geometry pass and queued parameter bindings /// are applied then. /// public static UgxMaterialBinder Adopt(VisualElement element) => new UgxMaterialBinder(element); UgxMaterialBinder(VisualElement element) { _element = element ?? throw new ArgumentNullException(nameof(element)); _adopting = true; _element.RegisterCallback(OnFirstGeometry); } void OnFirstGeometry(GeometryChangedEvent evt) { _element.UnregisterCallback(OnFirstGeometry); if (_disposed) return; _adopting = false; var resolved = _element.resolvedStyle.unityMaterial; if (resolved.material == null) { Debug.LogWarning("[ugx] no -unity-material resolved on '" + _element.name + "', so its material parameters were not applied. Check the USS."); return; } _definition = resolved; PushBound(null); Push(); } public UgxMaterialBinder(VisualElement element, Material material, bool instancePerElement = false) { _element = element ?? throw new ArgumentNullException(nameof(element)); if (material == null) throw new ArgumentNullException(nameof(material)); if (!IsSupportedPipeline()) { if (!_pipelineWarningShown) { _pipelineWarningShown = true; Debug.LogWarning("[ugx] UI materials need the Universal Render Pipeline. '" + material.name + "' was not applied; the element renders without it."); } return; } if (instancePerElement) { _instance = new Material(material) { name = material.name + " (ugx instance)" }; _instance.hideFlags = HideFlags.HideAndDontSave; } _definition = MaterialDefinition.FromMaterial(_instance != null ? _instance : material); Push(); } public bool IsApplied => _definition.material != null; public Material Material => _definition.material; public void SetFloat(string name, float value) { if (!IsApplied) return; _definition.SetFloat(name, value); Push(); } public void SetColor(string name, Color value) { if (!IsApplied) return; _definition.SetColor(name, value); Push(); } public void SetVector(string name, Vector4 value) { if (!IsApplied) return; _definition.SetVector(name, value); Push(); } public void SetTexture(string name, Texture value) { if (!IsApplied) return; _definition.SetTexture(name, value); Push(); } /// Drives one material parameter from a data source path, updating on every change. public void Bind(string parameterName, string path, UgxMaterialParamKind kind, IUgxDataSource source) { if (string.IsNullOrEmpty(parameterName) || string.IsNullOrEmpty(path)) return; // While adopting there is no material yet, so the binding is recorded and applied when // the material resolves. if (!IsApplied && !_adopting) return; _bound.Add(new Bound(parameterName, path, kind)); _dataSource = source; PushBound(path); if (_source != null || !(source is INotifyBindablePropertyChanged notifier)) return; _source = notifier; _source.propertyChanged += OnSourceChanged; } public void Dispose() { if (_disposed) return; _disposed = true; if (_adopting) { _adopting = false; _element.UnregisterCallback(OnFirstGeometry); } if (_source != null) { _source.propertyChanged -= OnSourceChanged; _source = null; } _bound.Clear(); if (IsApplied) _element.style.unityMaterial = StyleKeyword.Null; _definition = default; if (_instance != null) { if (Application.isPlaying) UnityEngine.Object.Destroy(_instance); else UnityEngine.Object.DestroyImmediate(_instance); } } void OnSourceChanged(object sender, BindablePropertyChangedEventArgs args) { if (_disposed) return; PushBound((string)args.propertyName); } void PushBound(string path) { if (_dataSource == null) return; bool changed = false; for (int i = 0; i < _bound.Count; i++) { var bound = _bound[i]; if (!string.IsNullOrEmpty(path) && bound.Path != path) continue; if (!_dataSource.TryGet(bound.Path, out var raw)) continue; switch (bound.Kind) { case UgxMaterialParamKind.Color: if (TryColor(raw, out var color)) _definition.SetColor(bound.Name, color); break; case UgxMaterialParamKind.Vector: if (raw is Vector4 v4) _definition.SetVector(bound.Name, v4); else if (raw is Vector3 v3) _definition.SetVector(bound.Name, v3); else if (raw is Vector2 v2) _definition.SetVector(bound.Name, v2); break; default: _definition.SetFloat(bound.Name, ToFloat(raw)); break; } changed = true; } if (changed) Push(); } void Push() => _element.style.unityMaterial = new StyleMaterialDefinition(_definition); static bool TryColor(object raw, out Color color) { switch (raw) { case Color c: color = c; return true; case Color32 c32: color = c32; return true; case string s: return UgxValueParser.TryColor(s, out color); default: color = default; return false; } } static float ToFloat(object raw) { switch (raw) { case float f: return f; case double d: return (float)d; case int i: return i; case long l: return l; case bool b: return b ? 1f : 0f; case string s: return UgxValueParser.TryFloat(s, out var parsed) ? parsed : 0f; default: return 0f; } } static bool IsSupportedPipeline() { var pipeline = GraphicsSettings.currentRenderPipeline; if (pipeline == null) return false; return pipeline.GetType().FullName?.Contains("Universal") == true; } readonly struct Bound { public readonly string Name; public readonly string Path; public readonly UgxMaterialParamKind Kind; public Bound(string name, string path, UgxMaterialParamKind kind) { Name = name; Path = path; Kind = kind; } } } }