using TMPro; using UnityEditor; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using UnityEngine.Video; using InteractiveVideo.Examples; namespace InteractiveVideo.Editor { /// /// GameObject > Interactive Video > Create Example Setup /// Builds the whole example hierarchy in the open scene and wires every reference, so the only manual steps /// are dropping a VideoClip on the VideoPlayer and (optionally) swapping the sample tracking JSON. /// public static class InteractiveVideoSetupMenu { private const string Root = "Assets/InteractiveVideo/"; private const string HighlightMaterialPath = Root + "Materials/InteractiveVideoHighlight.mat"; private const string MaskMaterialPath = Root + "Materials/InteractiveVideoMask.mat"; private const string SampleJsonPath = Root + "Examples/tracking_sample.json"; // The Speech "throw" clip (child throws a red ball) and its tracking data: used when both are present. private const string ThrowClipPath = Root + "Videos/spaixjbHYK.mp4"; private const string ThrowJsonPath = Root + "Videos/spaixjbHYK.json"; /// The throw clip when it is in the package, else no clip (the sample JSON is used for plumbing tests). [MenuItem("GameObject/Interactive Video/Create Example Setup", false, 10)] public static void CreateExampleSetup() { var clip = AssetDatabase.LoadAssetAtPath(ThrowClipPath); CreateSetup(clip, clip != null ? AssetDatabase.LoadAssetAtPath(ThrowJsonPath) : null); } /// Pick any clip under Assets; its tracking JSON is the file with the same name next to it. [MenuItem("GameObject/Interactive Video/Create Setup From Clip...", false, 11)] public static void CreateSetupFromClipDialog() { var start = System.IO.Directory.Exists(Application.dataPath + "/InteractiveVideo/Videos") ? Application.dataPath + "/InteractiveVideo/Videos" : Application.dataPath; var picked = EditorUtility.OpenFilePanelWithFilters("Pick a video clip inside Assets", start, new[] { "Video", "mp4,webm,mov,m4v", "All files", "*" }); if (string.IsNullOrEmpty(picked)) return; var full = System.IO.Path.GetFullPath(picked).Replace('\\', '/'); var assets = System.IO.Path.GetFullPath(Application.dataPath).Replace('\\', '/'); if (!full.StartsWith(assets, System.StringComparison.OrdinalIgnoreCase)) { EditorUtility.DisplayDialog("Interactive Video", "The clip must be inside this project's Assets folder (copy it to Assets/InteractiveVideo/Videos first).", "OK"); return; } var assetPath = "Assets" + full.Substring(assets.Length); var clip = AssetDatabase.LoadAssetAtPath(assetPath); if (clip == null) { EditorUtility.DisplayDialog("Interactive Video", $"{assetPath} has not been imported as a VideoClip yet. Let Unity finish importing, then try again.", "OK"); return; } CreateSetup(clip, TrackingJsonFor(assetPath)); } /// Right-click a VideoClip in the Project window. [MenuItem("Assets/Interactive Video/Create Setup From Selected Clip", false, 20)] public static void CreateSetupFromSelectedClip() { var clip = Selection.activeObject as VideoClip; if (clip == null) return; CreateSetup(clip, TrackingJsonFor(AssetDatabase.GetAssetPath(clip))); } [MenuItem("Assets/Interactive Video/Create Setup From Selected Clip", true)] private static bool CreateSetupFromSelectedClipValidate() => Selection.activeObject is VideoClip; /// Assets/.../name.mp4 -> Assets/.../name.json, or null with a warning when there is none. public static TextAsset TrackingJsonFor(string clipAssetPath) { var jsonPath = System.IO.Path.ChangeExtension(clipAssetPath, ".json"); var json = AssetDatabase.LoadAssetAtPath(jsonPath); if (json == null) Debug.LogWarning($"[InteractiveVideo] No tracking file at {jsonPath}. The scene is built with the sample tracking; put the clip's JSON next to it with the same name and assign it on VideoTrackingManager."); return json; } /// Builds the whole hierarchy for a clip and its tracking (either may be null: no clip = assign one by hand; no tracking = the sample JSON). public static void CreateSetup(VideoClip clip, TextAsset trackingJson) { var highlightMat = AssetDatabase.LoadAssetAtPath(HighlightMaterialPath); var maskMat = AssetDatabase.LoadAssetAtPath(MaskMaterialPath); if (trackingJson == null) trackingJson = AssetDatabase.LoadAssetAtPath(SampleJsonPath); var firstObject = FirstObjectOf(trackingJson); if (highlightMat == null || maskMat == null) Debug.LogWarning($"[InteractiveVideo] Materials not found under {Root}Materials. Assign them by hand on the Highlight Controller / Mask Renderer."); // ---- EventSystem ---- if (Object.FindAnyObjectByType() == null) { var es = new GameObject("EventSystem", typeof(EventSystem)); #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER var moduleType = System.Type.GetType("UnityEngine.InputSystem.UI.InputSystemUIInputModule, Unity.InputSystem"); if (moduleType != null) es.AddComponent(moduleType); else es.AddComponent(); #else es.AddComponent(); #endif Undo.RegisterCreatedObjectUndo(es, "Create EventSystem"); } // ---- Canvas ---- var canvasGo = new GameObject("InteractiveVideoCanvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); Undo.RegisterCreatedObjectUndo(canvasGo, "Create Interactive Video Setup"); var canvas = canvasGo.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; var scaler = canvasGo.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.matchWidthOrHeight = 0.5f; // ---- System object: VideoPlayer + our components ---- var systemGo = new GameObject("InteractiveVideo", typeof(VideoPlayer), typeof(InteractiveVideoPlayer), typeof(VideoTrackingManager), typeof(PolygonMaskRenderer), typeof(VideoHighlightController)); Undo.RegisterCreatedObjectUndo(systemGo, "Create Interactive Video Setup"); var videoPlayer = systemGo.GetComponent(); videoPlayer.playOnAwake = false; videoPlayer.renderMode = VideoRenderMode.RenderTexture; videoPlayer.isLooping = true; videoPlayer.audioOutputMode = VideoAudioOutputMode.Direct; if (clip != null) { videoPlayer.source = VideoSource.VideoClip; videoPlayer.clip = clip; } var ivPlayer = systemGo.GetComponent(); var tracking = systemGo.GetComponent(); var mask = systemGo.GetComponent(); var highlight = systemGo.GetComponent(); // ---- Video RawImage (fills the canvas, letterboxing done by the shader) ---- var rawGo = new GameObject("VideoRawImage", typeof(RectTransform), typeof(CanvasRenderer), typeof(RawImage), typeof(VideoClickHandler)); rawGo.transform.SetParent(canvasGo.transform, false); Stretch(rawGo.GetComponent()); var rawImage = rawGo.GetComponent(); rawImage.color = Color.white; rawImage.raycastTarget = true; var click = rawGo.GetComponent(); // ---- Debug overlay (child of the RawImage) ---- var debugGo = new GameObject("TrackingDebugOverlay", typeof(RectTransform), typeof(CanvasRenderer), typeof(TrackingDebugOverlay)); debugGo.transform.SetParent(rawGo.transform, false); Stretch(debugGo.GetComponent()); var overlay = debugGo.GetComponent(); // ---- Info panel ---- var panelGo = new GameObject("InfoPanel", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter), typeof(VideoObjectInfoPanel)); panelGo.transform.SetParent(canvasGo.transform, false); var panelRt = panelGo.GetComponent(); panelRt.anchorMin = panelRt.anchorMax = new Vector2(0f, 1f); panelRt.pivot = new Vector2(0f, 1f); panelRt.anchoredPosition = new Vector2(24f, -24f); panelRt.sizeDelta = new Vector2(360f, 0f); panelGo.GetComponent().color = new Color(0f, 0f, 0f, 0.6f); panelGo.GetComponent().raycastTarget = false; var layout = panelGo.GetComponent(); layout.padding = new RectOffset(16, 16, 12, 12); layout.spacing = 4f; layout.childForceExpandHeight = false; layout.childControlHeight = true; layout.childControlWidth = true; panelGo.GetComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; var nameText = MakeText(panelGo.transform, "NameText", firstObject.name, 36, FontStyles.Bold); var actionText = MakeText(panelGo.transform, "ActionText", firstObject.action, 26, FontStyles.Normal); var idText = MakeText(panelGo.transform, "TrackIdText", firstObject.id, 18, FontStyles.Italic); idText.color = new Color(1f, 1f, 1f, 0.6f); var confText = MakeText(panelGo.transform, "ConfidenceText", "97 %", 18, FontStyles.Normal); confText.color = new Color(1f, 1f, 1f, 0.6f); var infoPanel = panelGo.GetComponent(); // ---- Demo controls (bottom bar) ---- var barGo = new GameObject("DemoControls", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(HorizontalLayoutGroup), typeof(InteractiveVideoDemoControls)); barGo.transform.SetParent(canvasGo.transform, false); var barRt = barGo.GetComponent(); barRt.anchorMin = new Vector2(0f, 0f); barRt.anchorMax = new Vector2(1f, 0f); barRt.pivot = new Vector2(0.5f, 0f); barRt.anchoredPosition = Vector2.zero; barRt.sizeDelta = new Vector2(0f, 64f); barGo.GetComponent().color = new Color(0f, 0f, 0f, 0.5f); var hl = barGo.GetComponent(); hl.padding = new RectOffset(16, 16, 12, 12); hl.spacing = 12f; hl.childForceExpandWidth = false; hl.childForceExpandHeight = true; hl.childControlWidth = true; hl.childControlHeight = true; hl.childAlignment = TextAnchor.MiddleLeft; var playBtn = MakeButton(barGo.transform, "PlayPauseButton", "Play", 110f, out var playLabel); var slider = MakeSlider(barGo.transform, "Scrubber"); var frameLabel = MakeText(barGo.transform, "FrameLabel", "0 / 0", 20, FontStyles.Normal); frameLabel.gameObject.AddComponent().preferredWidth = 140f; frameLabel.alignment = TextAlignmentOptions.Center; var modeBtn = MakeButton(barGo.transform, "ModeButton", "DimBackgroundAndOutline", 280f, out var modeLabel); var toggle = MakeToggle(barGo.transform, "DebugToggle", "Debug"); var demo = barGo.GetComponent(); // ---- wire everything via SerializedObject (fields are private) ---- Set(ivPlayer, ("videoPlayer", videoPlayer), ("videoDisplay", rawImage), ("trackingManager", tracking), ("highlightController", highlight)); Set(tracking, ("trackingJson", trackingJson)); SetBool(tracking, "showTrackingDebug", true); Set(mask, ("maskMaterial", maskMat)); Set(highlight, ("trackingManager", tracking), ("maskRenderer", mask), ("videoDisplay", rawImage), ("highlightMaterial", highlightMat)); Set(click, ("player", ivPlayer), ("trackingManager", tracking)); Set(overlay, ("player", ivPlayer), ("trackingManager", tracking)); Set(infoPanel, ("trackingManager", tracking), ("nameText", nameText), ("actionText", actionText), ("trackIdText", idText), ("confidenceText", confText)); SetBool(infoPanel, "showConfidence", true); Set(demo, ("player", ivPlayer), ("trackingManager", tracking), ("highlightController", highlight), ("playPauseButton", playBtn), ("playPauseLabel", playLabel), ("scrubber", slider), ("frameLabel", frameLabel), ("modeButton", modeBtn), ("modeLabel", modeLabel), ("debugToggle", toggle)); Selection.activeGameObject = systemGo; Debug.Log(clip != null ? $"[InteractiveVideo] Setup created with {AssetDatabase.GetAssetPath(clip)} and tracking '{(trackingJson != null ? trackingJson.name : "none")}'. Press Play and click the tracked things." : "[InteractiveVideo] Example setup created. Assign a VideoClip (or URL) on the VideoPlayer of 'InteractiveVideo' and press Play."); } // ------------------------------------------------------------------ helpers /// Name, id and first action of the first object in a tracking file, for the info panel's placeholder text. private static (string name, string id, string action) FirstObjectOf(TextAsset trackingJson) { var data = trackingJson != null ? VideoTrackingLoader.Load(trackingJson) : null; if (data == null || data.Objects.Count == 0) return ("Name", "Action", "track id"); var first = data.Objects[0]; string action = null; foreach (var frame in data.Frames) { foreach (var o in frame.Objects) if (o.TrackId == first.Id && !string.IsNullOrEmpty(o.Action)) { action = o.Action; break; } if (action != null) break; } var name = string.IsNullOrEmpty(first.Name) ? first.Id : first.Name; action = string.IsNullOrEmpty(action) ? "Action" : char.ToUpperInvariant(action[0]) + action.Substring(1); return (name, first.Id, action); } private static void Stretch(RectTransform rt) { rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; } private static TMP_Text MakeText(Transform parent, string name, string text, float size, FontStyles style) { var go = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI)); go.transform.SetParent(parent, false); var t = go.GetComponent(); t.text = text; t.fontSize = size; t.fontStyle = style; t.color = Color.white; t.raycastTarget = false; t.textWrappingMode = TextWrappingModes.NoWrap; t.overflowMode = TextOverflowModes.Ellipsis; return t; } private static Button MakeButton(Transform parent, string name, string label, float width, out TMP_Text labelText) { var go = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button), typeof(LayoutElement)); go.transform.SetParent(parent, false); go.GetComponent().color = new Color(1f, 1f, 1f, 0.15f); go.GetComponent().preferredWidth = width; labelText = MakeText(go.transform, "Label", label, 20, FontStyles.Normal); Stretch(labelText.rectTransform); labelText.alignment = TextAlignmentOptions.Center; return go.GetComponent