/*---------------------------------------------------------------- // 基于 Leviathan 的 RawImage 视频解码扩展:ChromaKey 参数可序列化、编辑器首帧预览 //----------------------------------------------------------------*/ using UnityEngine; using UnityEngine.UI; #if UNITY_EDITOR using UnityEditor; #endif /// /// UI RawImage 视频解码(ZZVideo):持久化 ChromaKey、编辑器预览首帧 /// /// 编辑器下使用 (仅 UNITY_EDITOR 编译),以便 触发自动预览;运行时构建无此特性,与默认 MonoBehaviour 一致。 #if UNITY_EDITOR [ExecuteAlways] #endif [RequireComponent(typeof(RawImage))] public class ZZVideoRawImageDecoder : LeviathanVideoDecoder { [SerializeField] [Tooltip("仅在 AlphaType 为 ChromaKey 时使用")] private ZZVideoChromaKeyData _chromaKeyData = new ZZVideoChromaKeyData(); /// 播放速度(1 为正常)。继承自基类序列化字段,可在 Inspector 与代码中修改。 public float PlaybackSpeed { get => base.PlaySpeed; set => SetPlaySpeed(value); } /// 代码侧设置播放速度(与 / 基类 等价)。 public void SetPlaybackSpeed(float speed) => SetPlaySpeed(speed); /// 获取当前播放速度倍率。 public float GetPlaybackSpeed() => base.PlaySpeed; private static readonly int ChromaKeyColorId = Shader.PropertyToID("_KeyColor"); private static readonly int ChromaKeyCutoffId = Shader.PropertyToID("_ColorCutoff"); private static readonly int ChromaKeyColorFeatheringId = Shader.PropertyToID("_ColorFeathering"); private static readonly int ChromaKeyMaskFeatheringId = Shader.PropertyToID("_MaskFeathering"); private static readonly int ChromaKeySharpeningId = Shader.PropertyToID("_Sharpening"); private Material ChromaKeyMaterial { get { if (_alphaType != PlayAlphaType.ChromaKey) return null; var rawImage = GetComponent(); if (rawImage == null || rawImage.material == null) return null; return rawImage.material.HasProperty(ChromaKeyColorId) ? rawImage.material : null; } } public new void SetChromaKeyColor(Color color) { _chromaKeyData.keyColor = color; PushChromaKeyToMaterial(); } public new Color GetChromaKeyColor() => _chromaKeyData.keyColor; public new void SetChromaKeyCutoff(float value) { _chromaKeyData.cutoff = Mathf.Clamp01(value); PushChromaKeyToMaterial(); } public new float GetChromaKeyCutoff() => _chromaKeyData.cutoff; public new void SetChromaKeyColorFeathering(float value) { _chromaKeyData.colorFeathering = Mathf.Clamp01(value); PushChromaKeyToMaterial(); } public new float GetChromaKeyColorFeathering() => _chromaKeyData.colorFeathering; public new void SetChromaKeyMaskFeathering(float value) { _chromaKeyData.maskFeathering = Mathf.Clamp01(value); PushChromaKeyToMaterial(); } public new float GetChromaKeyMaskFeathering() => _chromaKeyData.maskFeathering; public new void SetChromaKeySharpening(float value) { _chromaKeyData.sharpening = Mathf.Clamp01(value); PushChromaKeyToMaterial(); } public new float GetChromaKeySharpening() => _chromaKeyData.sharpening; public new void SetChromaKeyParams(Color keyColor, float cutoff, float colorFeathering, float maskFeathering, float sharpening) { _chromaKeyData.keyColor = keyColor; _chromaKeyData.cutoff = Mathf.Clamp01(cutoff); _chromaKeyData.colorFeathering = Mathf.Clamp01(colorFeathering); _chromaKeyData.maskFeathering = Mathf.Clamp01(maskFeathering); _chromaKeyData.sharpening = Mathf.Clamp01(sharpening); PushChromaKeyToMaterial(); } public void PushChromaKeyToMaterial() { var mat = ChromaKeyMaterial; if (mat == null) return; mat.SetColor(ChromaKeyColorId, _chromaKeyData.keyColor); mat.SetFloat(ChromaKeyCutoffId, Mathf.Clamp01(_chromaKeyData.cutoff)); mat.SetFloat(ChromaKeyColorFeatheringId, Mathf.Clamp01(_chromaKeyData.colorFeathering)); mat.SetFloat(ChromaKeyMaskFeatheringId, Mathf.Clamp01(_chromaKeyData.maskFeathering)); mat.SetFloat(ChromaKeySharpeningId, Mathf.Clamp01(_chromaKeyData.sharpening)); } void ApplyChromaKeyToNewMaterial(Material mat) { if (mat == null || !mat.HasProperty(ChromaKeyColorId)) return; mat.SetColor(ChromaKeyColorId, _chromaKeyData.keyColor); mat.SetFloat(ChromaKeyCutoffId, Mathf.Clamp01(_chromaKeyData.cutoff)); mat.SetFloat(ChromaKeyColorFeatheringId, Mathf.Clamp01(_chromaKeyData.colorFeathering)); mat.SetFloat(ChromaKeyMaskFeatheringId, Mathf.Clamp01(_chromaKeyData.maskFeathering)); mat.SetFloat(ChromaKeySharpeningId, Mathf.Clamp01(_chromaKeyData.sharpening)); } protected override void OnRawImageMaterialInstanceReady(Material mat) { ApplyChromaKeyToNewMaterial(mat); } private new static void Log(string message) => LeviathanLogConfig.Log("ZZVideoRawImageDecoder", message); private new static void LogWarning(string message) => LeviathanLogConfig.LogWarning("ZZVideoRawImageDecoder", message); private new static void LogError(string message) => LeviathanLogConfig.LogError("ZZVideoRawImageDecoder", message); /// /// 使用当前 bytesFile / AlphaType 开始播放(Leviathan 基类 Play() 为 internal,跨程序集需由此入口调用) /// public void PlayVideoFromSerialized() { if (!Application.isPlaying) return; if (bytesFile == null) return; PlayVideo(bytesFile, _alphaType, true); } #if UNITY_EDITOR private bool _editorAutoPreviewFlushRegistered; private bool _editorValidateFlushRegistered; /// 编辑模式下不跑基类 Update/LateUpdate,避免 ExecuteAlways 下每帧进解码逻辑;运行时照常。 protected override void Update() { if (!Application.isPlaying) return; base.Update(); } protected override void LateUpdate() { if (!Application.isPlaying) return; base.LateUpdate(); } protected override void OnEnable() { base.OnEnable(); if (Application.isPlaying) return; RequestEditorAutoPreviewDeferred(); } protected override void OnDisable() { if (Application.isPlaying) return; if (_editorValidateFlushRegistered) { EditorApplication.delayCall -= EditorValidateFlush; _editorValidateFlushRegistered = false; } if (_editorAutoPreviewFlushRegistered) { EditorApplication.delayCall -= EditorAutoPreviewFlush; _editorAutoPreviewFlushRegistered = false; } if (_isPlaying) InternalStop(); } /// /// 合并到下一编辑器帧执行一次首帧预览,避免 OnEnable/OnValidate/Inspector 重复解码。 /// private void RequestEditorAutoPreviewDeferred() { if (Application.isPlaying) return; if (!enabled || !gameObject.activeInHierarchy) return; if (bytesFile == null) return; if (_editorAutoPreviewFlushRegistered) return; _editorAutoPreviewFlushRegistered = true; EditorApplication.delayCall += EditorAutoPreviewFlush; } private void EditorAutoPreviewFlush() { _editorAutoPreviewFlushRegistered = false; if (!this) return; if (Application.isPlaying) return; if (!enabled || !gameObject.activeInHierarchy) return; if (bytesFile == null) return; EditorPreviewFirstFrame(force: false); } private void OnValidate() { if (Application.isPlaying) return; if (_editorValidateFlushRegistered) return; _editorValidateFlushRegistered = true; EditorApplication.delayCall += EditorValidateFlush; } private void EditorValidateFlush() { _editorValidateFlushRegistered = false; if (!this) return; if (Application.isPlaying) return; var currentBytesFile = bytesFile; var lastBytesFile = _lastBytesFile; SetAlphaType(_alphaType); if (_alphaType == PlayAlphaType.ChromaKey) PushChromaKeyToMaterial(); if (currentBytesFile != lastBytesFile) { _lastBytesFile = currentBytesFile; if (IsPlaying && currentBytesFile != null) { Log("编辑器中检测到 bytesFile 变化,自动播放新视频"); if (Application.isPlaying) { Stop(); PlayVideoFromSerialized(); } else { InternalStop(); RequestEditorAutoPreviewDeferred(); } } else if (currentBytesFile != null && enabled && gameObject.activeInHierarchy) { RequestEditorAutoPreviewDeferred(); } } } [System.NonSerialized] private int _editorLastPreviewBytesInstanceId; [System.NonSerialized] private int _editorLastPreviewAlphaTypeInt; [System.NonSerialized] private double _editorLastPreviewEditorTime; private const double EditorAutoPreviewMinIntervalSeconds = 0.35; /// /// 编辑模式下解码并显示首帧(单线程)。自动调度走 false 时会按资源与时间去重,避免闪烁。 /// /// 为 true 时跳过去重(Inspector「强制刷新首帧预览」)。 public void EditorPreviewFirstFrame(bool force = false) { if (bytesFile == null) { LogWarning("EditorPreviewFirstFrame: 未指定 bytesFile"); return; } if (!force) { double now = EditorApplication.timeSinceStartup; int bid = bytesFile.GetInstanceID(); int at = (int)_alphaType; if (bid == _editorLastPreviewBytesInstanceId && at == _editorLastPreviewAlphaTypeInt && now - _editorLastPreviewEditorTime < EditorAutoPreviewMinIntervalSeconds) return; } if (_isPlaying) InternalStop(); int ret = PlayVideo(bytesFile, _alphaType, multithreadedDecode: false, beginFrameIndex: 1, syncDecodeFirstFrame: true, fastDecode: true); if (ret != 0) { LogError($"EditorPreviewFirstFrame 解码失败,错误码: {ret}"); return; } EditorPauseDecoderIgnoringPlayMode(); PushChromaKeyToMaterial(); _editorLastPreviewBytesInstanceId = bytesFile.GetInstanceID(); _editorLastPreviewAlphaTypeInt = (int)_alphaType; _editorLastPreviewEditorTime = EditorApplication.timeSinceStartup; } #endif } #if UNITY_EDITOR [CustomEditor(typeof(ZZVideoRawImageDecoder))] public class ZZVideoRawImageDecoderEditor : Editor { private static string FormatTimeSeconds(double totalSeconds) { int hours = (int)(totalSeconds / 3600); int minutes = (int)((totalSeconds % 3600) / 60); int seconds = (int)(totalSeconds % 60); int milliseconds = (int)((totalSeconds % 1) * 1000); return $"{hours:D2}:{minutes:D2}:{seconds:D2}.{milliseconds:D3}"; } private static string FormatTimeMicroseconds(long microseconds) => FormatTimeSeconds(microseconds / 1000000.0); public override void OnInspectorGUI() { serializedObject.Update(); DrawPropertiesExcluding(serializedObject, "m_Script", "_chromaKeyData", "_playSpeed"); var mono = (ZZVideoRawImageDecoder)target; EditorGUILayout.PropertyField(serializedObject.FindProperty("_chromaKeyData"), new GUIContent("ChromaKey 参数"), true); EditorGUILayout.Space(); { var speedProp = serializedObject.FindProperty("_playSpeed"); if (speedProp != null) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.PrefixLabel(new GUIContent("播放速度", "1 为正常;与 Time.deltaTime 相乘控制进帧快慢。范围 0.01~16。")); EditorGUI.BeginChangeCheck(); float v = speedProp.floatValue; v = GUILayout.HorizontalSlider(v, 0.25f, 4f); v = EditorGUILayout.DelayedFloatField(v, GUILayout.Width(52)); EditorGUILayout.EndHorizontal(); if (EditorGUI.EndChangeCheck()) speedProp.floatValue = Mathf.Clamp(v, 0.01f, 16f); } } EditorGUILayout.Space(); using (new EditorGUI.DisabledScope(mono.bytesFile == null)) { if (GUILayout.Button("强制刷新首帧预览")) { Undo.RecordObject(mono, "ZZVideo 刷新首帧预览"); mono.EditorPreviewFirstFrame(force: true); EditorUtility.SetDirty(mono); } } EditorGUI.BeginDisabledGroup(true); EditorGUILayout.TextField("帧数", $"{mono.PlayFrameIndex} / {mono.FrameCount}"); EditorGUILayout.TextField("播放时间", FormatTimeSeconds(mono.PlayFrameIndex * mono.FrameInterval)); EditorGUILayout.TextField("总时间", FormatTimeMicroseconds(mono.Duration)); EditorGUILayout.TextField("FPS", mono.FrameInterval > 0 ? $"{1.0 / mono.FrameInterval:F2}" : "0"); EditorGUILayout.TextField("纹理尺寸", $"{mono.VideoWidth} × {mono.VideoHeight}"); EditorGUILayout.TextField("视频有效尺寸", $"{mono.VideoValidWidth} × {mono.VideoHeight}"); EditorGUI.EndDisabledGroup(); { bool oldValue = mono.IsPlaying; bool newValue = EditorGUILayout.Toggle("isPlaying", oldValue); if (oldValue != newValue) { if (newValue) mono.PlayVideoFromSerialized(); else mono.Stop(); EditorUtility.SetDirty(target); } } { bool oldValue = mono.IsPause; bool newValue = EditorGUILayout.Toggle("isPause", oldValue); if (oldValue != newValue) { if (newValue) mono.Pause(); else mono.Resume(); EditorUtility.SetDirty(target); } } if (serializedObject.ApplyModifiedProperties()) { mono.PushChromaKeyToMaterial(); EditorUtility.SetDirty(mono); } Repaint(); } } #endif