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