using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; namespace InteractiveVideo { /// /// Rasterises polygons into a small single-channel RenderTexture on the GPU: white inside, black outside. /// The polygons are triangulated on the CPU (ear clipping, a few dozen points) and drawn as one mesh with a /// command buffer, so no per-pixel CPU work and no per-frame allocations after warm-up. /// /// Input polygons are in VIDEO UV space (0..1, Y up). Callers convert from tracking space with /// . The mask is sampled with the same UVs as the video texture, so a fixed /// (video-independent) resolution is fine: it only drives the highlight, not the image. /// [DisallowMultipleComponent] [AddComponentMenu("Interactive Video/Polygon Mask Renderer")] public sealed class PolygonMaskRenderer : MonoBehaviour { [Tooltip("Mask width in pixels. 256 / 512 / 1024. Height follows the video aspect when Match Video Aspect is on, otherwise square.")] [SerializeField] private int maskResolution = 512; [Tooltip("Size the mask height to the video aspect (e.g. 512x288 for 16:9) so the outline is equally thick in both directions.")] [SerializeField] private bool matchVideoAspect; [Tooltip("MSAA samples for the mask (1 = off). Softens the polygon edge.")] [SerializeField] private int antiAliasing = 4; [Tooltip("Material using the InteractiveVideo/PolygonMask shader. Falls back to Shader.Find when empty.")] [SerializeField] private Material maskMaterial; /// Raised when the RenderTexture is (re)created; the highlight controller re-binds it. public event Action MaskTextureChanged; /// The mask. Created lazily; do not cache across resolution changes (listen to MaskTextureChanged). public RenderTexture MaskTexture { get { EnsureTexture(); return _texture; } } /// True when the last render drew at least one polygon. public bool HasContent { get; private set; } public int MaskResolution { get => maskResolution; set { maskResolution = ClampResolution(value); EnsureTexture(); } } private RenderTexture _texture; private Mesh _mesh; private Material _material; private bool _ownsMaterial; private CommandBuffer _cmd; private float _videoAspect = 1f; private readonly List _vertices = new List(256); private readonly List _indices = new List(768); private bool _building; private bool _clearedOnce; private static readonly Matrix4x4 s_View = Matrix4x4.identity; private static readonly Matrix4x4 s_Ortho = Matrix4x4.Ortho(0f, 1f, 0f, 1f, -1f, 1f); // ------------------------------------------------------------------ lifecycle private void OnValidate() { maskResolution = ClampResolution(maskResolution); antiAliasing = Mathf.Clamp(antiAliasing, 1, 8); if (antiAliasing == 3 || (antiAliasing > 4 && antiAliasing < 8)) antiAliasing = antiAliasing < 4 ? 2 : 4; } private void OnDestroy() { if (_texture != null) { _texture.Release(); Destroy(_texture); _texture = null; } if (_mesh != null) { Destroy(_mesh); _mesh = null; } if (_ownsMaterial && _material != null) { Destroy(_material); _material = null; } _cmd?.Release(); _cmd = null; } // ------------------------------------------------------------------ public API /// Tell the renderer the video aspect (width/height) so Match Video Aspect can size the mask. public void SetVideoAspect(float aspect) { if (aspect <= 0f || Mathf.Approximately(aspect, _videoAspect)) return; _videoAspect = aspect; if (matchVideoAspect) EnsureTexture(); } /// Clears the mask to black (nothing selected). public void Clear() { EnsureTexture(); if (!HasContent && _clearedOnce) return; _vertices.Clear(); _indices.Clear(); Execute(drawMesh: false); HasContent = false; _clearedOnce = true; } /// Renders a single polygon (video UV space). public void Render(IReadOnlyList uvPolygon) { BeginPolygons(); AddPolygon(uvPolygon); EndPolygons(); } /// Start collecting polygons for one mask render (multi-selection). public void BeginPolygons() { _vertices.Clear(); _indices.Clear(); _building = true; } /// Adds a polygon (video UV space, 3+ points; fewer are ignored). Copies the points, so buffers may be reused. public void AddPolygon(IReadOnlyList uvPolygon) { if (!_building) BeginPolygons(); if (uvPolygon == null || uvPolygon.Count < 3) return; int offset = _vertices.Count; for (int i = 0; i < uvPolygon.Count; i++) _vertices.Add(new Vector3(uvPolygon[i].x, uvPolygon[i].y, 0f)); PolygonUtility.Triangulate(uvPolygon, _indices, offset); } /// Draws everything added since BeginPolygons. public void EndPolygons() { _building = false; EnsureTexture(); if (_indices.Count == 0) { Clear(); return; } Execute(drawMesh: true); HasContent = true; } // ------------------------------------------------------------------ internals private void Execute(bool drawMesh) { if (_cmd == null) _cmd = new CommandBuffer { name = "InteractiveVideo Polygon Mask" }; _cmd.Clear(); _cmd.SetRenderTarget(_texture); _cmd.ClearRenderTarget(false, true, Color.black); if (drawMesh && EnsureMaterial()) { if (_mesh == null) { _mesh = new Mesh { name = "InteractiveVideo_PolygonMask", indexFormat = IndexFormat.UInt32 }; _mesh.MarkDynamic(); } _mesh.Clear(false); _mesh.SetVertices(_vertices); _mesh.SetIndices(_indices, MeshTopology.Triangles, 0, false); _mesh.bounds = new Bounds(new Vector3(0.5f, 0.5f, 0f), new Vector3(4f, 4f, 4f)); // renderIntoTexture=true lets Unity apply the platform Y flip, so the mask samples with the same // UV convention as any other texture regardless of graphics API. _cmd.SetViewProjectionMatrices(s_View, GL.GetGPUProjectionMatrix(s_Ortho, true)); _cmd.DrawMesh(_mesh, Matrix4x4.identity, _material, 0, 0); } Graphics.ExecuteCommandBuffer(_cmd); } private bool EnsureMaterial() { if (_material != null) return true; if (maskMaterial != null) { _material = maskMaterial; return true; } var shader = Shader.Find("Hidden/InteractiveVideo/PolygonMask"); if (shader == null) { Debug.LogError("[InteractiveVideo] PolygonMaskRenderer: assign the InteractiveVideoMask material (shader Hidden/InteractiveVideo/PolygonMask not found).", this); return false; } _material = new Material(shader) { name = "InteractiveVideo_PolygonMask (runtime)" }; _ownsMaterial = true; return true; } private void EnsureTexture() { int w = ClampResolution(maskResolution); int h = matchVideoAspect ? Mathf.Max(8, Mathf.RoundToInt(w / _videoAspect)) : w; int aa = Mathf.Max(1, antiAliasing); if (_texture != null && _texture.width == w && _texture.height == h && _texture.antiAliasing == aa) return; if (_texture != null) { _texture.Release(); Destroy(_texture); } var format = SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.R8) ? RenderTextureFormat.R8 : RenderTextureFormat.ARGB32; _texture = new RenderTexture(w, h, 0, format, RenderTextureReadWrite.Linear) { name = "InteractiveVideo_Mask", antiAliasing = aa, useMipMap = false, autoGenerateMips = false, filterMode = FilterMode.Bilinear, wrapMode = TextureWrapMode.Clamp, }; _texture.Create(); _clearedOnce = false; HasContent = false; Execute(drawMesh: false); _clearedOnce = true; MaskTextureChanged?.Invoke(_texture); } private static int ClampResolution(int value) { if (value <= 256) return 256; if (value <= 512) return 512; return 1024; } } }