using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace InteractiveVideo { /// Highlight styles implemented by InteractiveVideoHighlight.shader (_HighlightMode). public enum HighlightMode { Outline = 0, Fill = 1, OutlineAndFill = 2, DimBackgroundAndOutline = 3, PulseOutline = 4, } /// /// Connects selection to pixels: reads the selected objects' polygons at the current frame from the tracking /// manager, asks the for a mask, and feeds mask + look parameters to one /// persistent material instance on the video RawImage. /// /// The mask is rebuilt only when the selection or the resolved tracking frame changed. A selected object that /// is missing or visible=false in the current frame clears the mask but keeps the selection, so it lights up /// again when it reappears. /// [DisallowMultipleComponent] [AddComponentMenu("Interactive Video/Video Highlight Controller")] public sealed class VideoHighlightController : MonoBehaviour { [Header("Components")] [SerializeField] private VideoTrackingManager trackingManager; [SerializeField] private PolygonMaskRenderer maskRenderer; [Tooltip("The RawImage showing the video. Receives the highlight material instance.")] [SerializeField] private RawImage videoDisplay; [Tooltip("Template material using InteractiveVideo/Highlight. Instantiated once; the asset is never modified.")] [SerializeField] private Material highlightMaterial; [Header("Look")] [SerializeField] private HighlightMode highlightMode = HighlightMode.DimBackgroundAndOutline; [SerializeField] private Color highlightColor = new Color(1f, 0.85f, 0.1f, 1f); [Range(0f, 1f)] [SerializeField] private float highlightStrength = 0.45f; [Tooltip("Outline thickness in mask texels.")] [Range(0f, 16f)] [SerializeField] private float outlineWidth = 3f; [Range(0f, 1f)] [SerializeField] private float outlineStrength = 1f; [Tooltip("Brightness of everything that is not selected (Dim Background mode). 1 = untouched.")] [Range(0f, 1f)] [SerializeField] private float backgroundDimAmount = 0.35f; [Range(0f, 20f)] [SerializeField] private float pulseSpeed = 4f; [Range(0f, 1f)] [SerializeField] private float pulseAmount = 0.35f; [Header("Behaviour")] [Tooltip("Clear the mask while the selected object is marked visible=false (selection is kept).")] [SerializeField] private bool hideWhenNotVisible = true; /// Raised when the highlight starts/stops showing pixels (object appeared / vanished / deselected). public event Action HighlightVisibilityChanged; public Material MaterialInstance => _material; public bool IsHighlightVisible { get; private set; } public HighlightMode Mode => highlightMode; public Color HighlightColor => highlightColor; private Material _material; private int _lastResolvedVersion = int.MinValue; private bool _dirty = true; private readonly List _uvScratch = new List(128); private static readonly int s_MaskTex = Shader.PropertyToID("_MaskTex"); private static readonly int s_HighlightColor = Shader.PropertyToID("_HighlightColor"); private static readonly int s_HighlightStrength = Shader.PropertyToID("_HighlightStrength"); private static readonly int s_OutlineWidth = Shader.PropertyToID("_OutlineWidth"); private static readonly int s_OutlineStrength = Shader.PropertyToID("_OutlineStrength"); private static readonly int s_DimBackground = Shader.PropertyToID("_DimBackground"); private static readonly int s_PulseSpeed = Shader.PropertyToID("_PulseSpeed"); private static readonly int s_PulseAmount = Shader.PropertyToID("_PulseAmount"); private static readonly int s_HighlightMode = Shader.PropertyToID("_HighlightMode"); private static readonly int s_HasMask = Shader.PropertyToID("_HasMask"); private static readonly int s_VideoRect = Shader.PropertyToID("_VideoRect"); private static readonly int s_LetterboxColor = Shader.PropertyToID("_LetterboxColor"); // ------------------------------------------------------------------ lifecycle private void Reset() { trackingManager = GetComponent(); maskRenderer = GetComponent(); } private void Awake() { if (trackingManager == null) trackingManager = GetComponent(); if (maskRenderer == null) maskRenderer = GetComponent(); if (maskRenderer == null) { Debug.LogError("[InteractiveVideo] VideoHighlightController needs a PolygonMaskRenderer.", this); enabled = false; return; } if (!CreateMaterial()) { enabled = false; return; } maskRenderer.MaskTextureChanged += OnMaskTextureChanged; _material.SetTexture(s_MaskTex, maskRenderer.MaskTexture); ApplyLook(); SetDisplayRect(new Rect(0, 0, 1, 1), Color.black); } private void OnEnable() { if (trackingManager != null) { trackingManager.SelectionChanged += MarkDirty; trackingManager.TrackingLoaded += OnTrackingLoaded; } _dirty = true; } private void OnDisable() { if (trackingManager != null) { trackingManager.SelectionChanged -= MarkDirty; trackingManager.TrackingLoaded -= OnTrackingLoaded; } } private void OnDestroy() { if (maskRenderer != null) maskRenderer.MaskTextureChanged -= OnMaskTextureChanged; if (videoDisplay != null && videoDisplay.material == _material) videoDisplay.material = null; if (_material != null) Destroy(_material); } private void OnValidate() { if (_material != null) ApplyLook(); } private void LateUpdate() { if (trackingManager == null) return; if (!_dirty && trackingManager.ResolvedVersion == _lastResolvedVersion) return; _dirty = false; _lastResolvedVersion = trackingManager.ResolvedVersion; RebuildMask(); } // ------------------------------------------------------------------ public API public void SelectObject(string trackId) => trackingManager?.Select(trackId); public void ClearSelection() => trackingManager?.ClearSelection(); public void SetHighlightMode(HighlightMode mode) { highlightMode = mode; ApplyLook(); } public void SetHighlightMode(int mode) => SetHighlightMode((HighlightMode)Mathf.Clamp(mode, 0, 4)); public void SetHighlightColor(Color color) { highlightColor = color; ApplyLook(); } public void SetHighlightStrength(float strength) { highlightStrength = Mathf.Clamp01(strength); ApplyLook(); } public void SetOutlineWidth(float texels) { outlineWidth = Mathf.Clamp(texels, 0f, 16f); ApplyLook(); } public void SetOutlineStrength(float strength) { outlineStrength = Mathf.Clamp01(strength); ApplyLook(); } public void SetDimAmount(float backgroundBrightness) { backgroundDimAmount = Mathf.Clamp01(backgroundBrightness); ApplyLook(); } public void SetPulse(float speed, float amount) { pulseSpeed = Mathf.Max(0f, speed); pulseAmount = Mathf.Clamp01(amount); ApplyLook(); } /// Called by InteractiveVideoPlayer: where the video sits in the RawImage (letterboxing). public void SetDisplayRect(Rect normalizedRect, Color letterbox) { if (_material == null) return; _material.SetVector(s_VideoRect, new Vector4(normalizedRect.x, normalizedRect.y, normalizedRect.width, normalizedRect.height)); _material.SetColor(s_LetterboxColor, letterbox); if (normalizedRect.width > 0f && normalizedRect.height > 0f && maskRenderer != null && videoDisplay != null) { Vector2 size = videoDisplay.rectTransform.rect.size; if (size.x > 0f && size.y > 0f) maskRenderer.SetVideoAspect((size.x * normalizedRect.width) / (size.y * normalizedRect.height)); } } /// Force the mask to be rebuilt on the next LateUpdate. public void MarkDirty() => _dirty = true; // ------------------------------------------------------------------ internals private bool CreateMaterial() { Shader shader = highlightMaterial != null ? highlightMaterial.shader : Shader.Find("InteractiveVideo/Highlight"); if (shader == null) { Debug.LogError("[InteractiveVideo] VideoHighlightController: assign the InteractiveVideoHighlight material (shader InteractiveVideo/Highlight not found).", this); return false; } _material = highlightMaterial != null ? new Material(highlightMaterial) : new Material(shader); _material.name = "InteractiveVideo_Highlight (instance)"; if (videoDisplay != null) videoDisplay.material = _material; else Debug.LogWarning("[InteractiveVideo] VideoHighlightController has no RawImage; read MaterialInstance and assign it yourself.", this); return true; } private void ApplyLook() { if (_material == null) return; _material.SetColor(s_HighlightColor, highlightColor); _material.SetFloat(s_HighlightStrength, highlightStrength); _material.SetFloat(s_OutlineWidth, outlineWidth); _material.SetFloat(s_OutlineStrength, outlineStrength); _material.SetFloat(s_DimBackground, backgroundDimAmount); _material.SetFloat(s_PulseSpeed, pulseSpeed); _material.SetFloat(s_PulseAmount, pulseAmount); _material.SetFloat(s_HighlightMode, (int)highlightMode); } private void OnMaskTextureChanged(RenderTexture texture) { if (_material != null) _material.SetTexture(s_MaskTex, texture); _dirty = true; } private void OnTrackingLoaded(TrackingData data) => _dirty = true; private void RebuildMask() { var selected = trackingManager.SelectedTrackIds; bool any = false; if (selected.Count > 0 && trackingManager.CurrentTrackingFrame != null) { maskRenderer.BeginPolygons(); for (int i = 0; i < selected.Count; i++) { if (!trackingManager.TryGetCurrentObject(selected[i], out var state)) continue; // temporarily missing if (!state.HasPolygon) continue; if (hideWhenNotVisible && !state.Visible) continue; _uvScratch.Clear(); for (int p = 0; p < state.Polygon.Count; p++) _uvScratch.Add(VideoCoordinates.TrackingToUv(state.Polygon[p])); maskRenderer.AddPolygon(_uvScratch); any = true; } maskRenderer.EndPolygons(); } else { maskRenderer.Clear(); } _material.SetFloat(s_HasMask, any ? 1f : 0f); if (any != IsHighlightVisible) { IsHighlightVisible = any; HighlightVisibilityChanged?.Invoke(any); } } } }