414 lines
15 KiB
C#
414 lines
15 KiB
C#
/*----------------------------------------------------------------
|
||
// 基于 Leviathan 的 RawImage 视频解码扩展:ChromaKey 参数可序列化、编辑器首帧预览
|
||
//----------------------------------------------------------------*/
|
||
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
#if UNITY_EDITOR
|
||
using UnityEditor;
|
||
#endif
|
||
|
||
/// <summary>
|
||
/// UI RawImage 视频解码(ZZVideo):持久化 ChromaKey、编辑器预览首帧
|
||
/// </summary>
|
||
/// <remarks>编辑器下使用 <see cref="ExecuteAlways"/>(仅 UNITY_EDITOR 编译),以便 <see cref="OnEnable"/> 触发自动预览;运行时构建无此特性,与默认 MonoBehaviour 一致。</remarks>
|
||
#if UNITY_EDITOR
|
||
[ExecuteAlways]
|
||
#endif
|
||
[RequireComponent(typeof(RawImage))]
|
||
public class ZZVideoRawImageDecoder : LeviathanVideoDecoder
|
||
{
|
||
[SerializeField]
|
||
[Tooltip("仅在 AlphaType 为 ChromaKey 时使用")]
|
||
private ZZVideoChromaKeyData _chromaKeyData = new ZZVideoChromaKeyData();
|
||
|
||
/// <summary>播放速度(1 为正常)。继承自基类序列化字段,可在 Inspector 与代码中修改。</summary>
|
||
public float PlaybackSpeed
|
||
{
|
||
get => base.PlaySpeed;
|
||
set => SetPlaySpeed(value);
|
||
}
|
||
|
||
/// <summary>代码侧设置播放速度(与 <see cref="PlaybackSpeed"/> / 基类 <see cref="LeviathanVideoDecoderBase.SetPlaySpeed"/> 等价)。</summary>
|
||
public void SetPlaybackSpeed(float speed) => SetPlaySpeed(speed);
|
||
|
||
/// <summary>获取当前播放速度倍率。</summary>
|
||
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<RawImage>();
|
||
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);
|
||
|
||
/// <summary>
|
||
/// 使用当前 bytesFile / AlphaType 开始播放(Leviathan 基类 <c>Play()</c> 为 internal,跨程序集需由此入口调用)
|
||
/// </summary>
|
||
public void PlayVideoFromSerialized()
|
||
{
|
||
if (!Application.isPlaying) return;
|
||
if (bytesFile == null) return;
|
||
PlayVideo(bytesFile, _alphaType, true);
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
private bool _editorAutoPreviewFlushRegistered;
|
||
private bool _editorValidateFlushRegistered;
|
||
|
||
/// <summary>编辑模式下不跑基类 Update/LateUpdate,避免 ExecuteAlways 下每帧进解码逻辑;运行时照常。</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 合并到下一编辑器帧执行一次首帧预览,避免 OnEnable/OnValidate/Inspector 重复解码。
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// 编辑模式下解码并显示首帧(单线程)。自动调度走 <paramref name="force"/> false 时会按资源与时间去重,避免闪烁。
|
||
/// </summary>
|
||
/// <param name="force">为 true 时跳过去重(Inspector「强制刷新首帧预览」)。</param>
|
||
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
|