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);
}
}
}
}