926 lines
33 KiB
C#
926 lines
33 KiB
C#
using System.Collections.Generic;
|
||
using System.IO;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
|
||
public class AnimationClipFrameChecker : EditorWindow
|
||
{
|
||
private List<AnimationClip> clips = new List<AnimationClip>();
|
||
private Vector2 scrollPos;
|
||
private Vector2 clipListScrollPos;
|
||
private Vector2 resultListScrollPos;
|
||
private bool hasRunCheck = false;
|
||
|
||
// 检测结果缓存
|
||
private struct ClipResult
|
||
{
|
||
public string name;
|
||
public string assetPath;
|
||
public int frameCount;
|
||
public float length;
|
||
public float frameRate;
|
||
}
|
||
private List<ClipResult> results = new List<ClipResult>();
|
||
|
||
// 大背景图片
|
||
private Texture2D bgImageGlobal;
|
||
private const string BgImagePrefKey = "ClipFrameChecker_BgImagePath";
|
||
|
||
// GIF动画背景
|
||
private Texture2D[] gifFrames;
|
||
private int[] gifDelays;
|
||
private int gifTotalDuration;
|
||
private bool isGif;
|
||
private string gifSourcePath = "";
|
||
private double gifStartTime;
|
||
private double lastGifRepaintTime;
|
||
|
||
[MenuItem("Tools/动画帧数检测工具")]
|
||
public static void ShowWindow()
|
||
{
|
||
var win = GetWindow<AnimationClipFrameChecker>("动画帧数检测工具");
|
||
win.minSize = new Vector2(500, 400);
|
||
win.LoadBgImage();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
EditorApplication.update -= OnEditorUpdate;
|
||
EditorApplication.update += OnEditorUpdate;
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
EditorApplication.update -= OnEditorUpdate;
|
||
ClearGif();
|
||
}
|
||
|
||
private void OnEditorUpdate()
|
||
{
|
||
if (isGif && gifFrames != null && gifFrames.Length > 1)
|
||
{
|
||
if (EditorApplication.timeSinceStartup - lastGifRepaintTime > 0.033)
|
||
{
|
||
Repaint();
|
||
lastGifRepaintTime = EditorApplication.timeSinceStartup;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OnGUI()
|
||
{
|
||
// 配色方案
|
||
Color headerBg = new Color(0.3f, 0.5f, 0.8f, 1f);
|
||
Color section1Bg = new Color(0.3f, 0.65f, 0.35f, 0.6f); // 绿 - 动画列表
|
||
Color section2Bg = new Color(0.65f, 0.35f, 0.7f, 0.6f); // 紫 - 检测按钮
|
||
Color section3Bg = new Color(0.2f, 0.6f, 0.55f, 0.6f); // 青 - 检测结果
|
||
Color section4Bg = new Color(0.7f, 0.45f, 0.2f, 0.6f); // 橙 - 背景设置
|
||
|
||
// 绘制大背景
|
||
Texture2D bgToDraw = null;
|
||
if (isGif && gifFrames != null && gifFrames.Length > 0)
|
||
{
|
||
int elapsed = (int)((EditorApplication.timeSinceStartup - gifStartTime) * 1000) % gifTotalDuration;
|
||
int accum = 0;
|
||
for (int i = 0; i < gifFrames.Length; i++)
|
||
{
|
||
accum += gifDelays[i];
|
||
if (elapsed < accum) { bgToDraw = gifFrames[i]; break; }
|
||
}
|
||
if (bgToDraw == null) bgToDraw = gifFrames[0];
|
||
}
|
||
else if (bgImageGlobal != null)
|
||
{
|
||
bgToDraw = bgImageGlobal;
|
||
}
|
||
if (bgToDraw != null)
|
||
{
|
||
Rect fullRect = new Rect(0, 0, position.width, position.height);
|
||
GUI.DrawTexture(fullRect, bgToDraw, ScaleMode.StretchToFill);
|
||
}
|
||
|
||
// 整体滚动
|
||
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
||
|
||
// 大标题
|
||
EditorGUILayout.Space(5);
|
||
EditorGUILayout.LabelField("动画帧数检测工具", new GUIStyle(EditorStyles.boldLabel)
|
||
{
|
||
fontSize = 18,
|
||
alignment = TextAnchor.MiddleCenter,
|
||
fontStyle = FontStyle.Bold
|
||
});
|
||
GUILayout.Space(8);
|
||
|
||
// 1. 动画列表
|
||
DrawSectionHeader("1. 拖入 Animation Clip", headerBg);
|
||
DrawSectionContent(DrawClipListInner, section1Bg);
|
||
GUILayout.Space(6);
|
||
|
||
// 2. 检测按钮
|
||
DrawSectionHeader("2. 检测动画", headerBg);
|
||
DrawSectionContent(DrawCheckSection, section2Bg);
|
||
GUILayout.Space(6);
|
||
|
||
// 3. 检测结果
|
||
if (hasRunCheck)
|
||
{
|
||
DrawSectionHeader("3. 检测结果", headerBg);
|
||
DrawSectionContent(DrawResultListInner, section3Bg);
|
||
}
|
||
GUILayout.Space(6);
|
||
|
||
// 4. 背景设置
|
||
showBgSettings = EditorGUILayout.Foldout(showBgSettings, "🎨 界面背景设置", true);
|
||
if (showBgSettings)
|
||
{
|
||
DrawSectionContent(DrawBgSettingsInner, section4Bg);
|
||
}
|
||
|
||
GUILayout.Space(8);
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private bool showBgSettings = false;
|
||
|
||
// ===================== 动画列表 =====================
|
||
private void DrawClipListInner()
|
||
{
|
||
EditorGUILayout.HelpBox("将 Animation Clip 拖入下方列表,或点击按钮选择文件。支持批量操作。", MessageType.Info);
|
||
GUILayout.Space(4);
|
||
|
||
// 拖拽区域
|
||
Rect dropArea = GUILayoutUtility.GetRect(0, 40, GUILayout.ExpandWidth(true));
|
||
GUIStyle dropStyle = new GUIStyle("box")
|
||
{
|
||
alignment = TextAnchor.MiddleCenter,
|
||
fontSize = 12,
|
||
fontStyle = FontStyle.Italic
|
||
};
|
||
GUI.Box(dropArea, "📂 拖拽 Animation Clip 到此处", dropStyle);
|
||
|
||
// 处理拖拽
|
||
if (Event.current.type == EventType.DragUpdated || Event.current.type == EventType.DragPerform)
|
||
{
|
||
if (dropArea.Contains(Event.current.mousePosition))
|
||
{
|
||
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||
if (Event.current.type == EventType.DragPerform)
|
||
{
|
||
DragAndDrop.AcceptDrag();
|
||
foreach (var obj in DragAndDrop.objectReferences)
|
||
{
|
||
if (obj is AnimationClip clip && !clips.Contains(clip))
|
||
clips.Add(clip);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
GUILayout.Space(6);
|
||
|
||
// 按钮行
|
||
EditorGUILayout.BeginHorizontal();
|
||
if (GUILayout.Button("➕ 添加动画", GUILayout.Height(28)))
|
||
{
|
||
string path = EditorUtility.OpenFilePanelWithFilters("选择 Animation Clip", "", new string[] { "Animation Files", "anim", "All Files", "*" });
|
||
if (!string.IsNullOrEmpty(path))
|
||
{
|
||
string assetPath = GetAssetPath(path);
|
||
if (string.IsNullOrEmpty(assetPath))
|
||
assetPath = path;
|
||
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(assetPath);
|
||
if (clip != null && !clips.Contains(clip))
|
||
clips.Add(clip);
|
||
}
|
||
}
|
||
if (GUILayout.Button("📂 从文件夹批量导入", GUILayout.Height(28)))
|
||
{
|
||
string folder = EditorUtility.OpenFolderPanel("选择包含动画的文件夹", "", "");
|
||
if (!string.IsNullOrEmpty(folder))
|
||
{
|
||
string assetFolder = GetAssetPath(folder);
|
||
if (string.IsNullOrEmpty(assetFolder))
|
||
{
|
||
// 用户选了项目外目录,尝试直接加载
|
||
string[] files = System.IO.Directory.GetFiles(folder, "*.anim", System.IO.SearchOption.AllDirectories);
|
||
foreach (string f in files)
|
||
{
|
||
string ap = GetAssetPath(f);
|
||
if (!string.IsNullOrEmpty(ap))
|
||
{
|
||
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(ap);
|
||
if (clip != null && !clips.Contains(clip)) clips.Add(clip);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
string[] guids = AssetDatabase.FindAssets("t:AnimationClip", new string[] { assetFolder });
|
||
foreach (string guid in guids)
|
||
{
|
||
string ap = AssetDatabase.GUIDToAssetPath(guid);
|
||
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(ap);
|
||
if (clip != null && !clips.Contains(clip)) clips.Add(clip);
|
||
}
|
||
}
|
||
Repaint();
|
||
}
|
||
}
|
||
if (GUILayout.Button("🗑️ 清空列表", GUILayout.Height(28)))
|
||
{
|
||
clips.Clear();
|
||
results.Clear();
|
||
hasRunCheck = false;
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
GUILayout.Space(6);
|
||
|
||
// 显示当前列表
|
||
if (clips.Count > 0)
|
||
{
|
||
EditorGUILayout.LabelField($"已添加 {clips.Count} 个动画", EditorStyles.boldLabel);
|
||
GUILayout.Space(4);
|
||
clipListScrollPos = EditorGUILayout.BeginScrollView(clipListScrollPos, GUILayout.MaxHeight(150));
|
||
for (int i = 0; i < clips.Count; i++)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
GUIContent clipIcon = EditorGUIUtility.IconContent("d_AnimationClip Icon");
|
||
GUILayout.Label(clipIcon, GUILayout.Width(20), GUILayout.Height(20));
|
||
GUILayout.Label(clips[i].name, GUILayout.Width(200));
|
||
GUILayout.FlexibleSpace();
|
||
GUILayout.Label(AssetDatabase.GetAssetPath(clips[i]), EditorStyles.miniLabel);
|
||
if (GUILayout.Button("✕", GUILayout.Width(30)))
|
||
{
|
||
clips.RemoveAt(i);
|
||
i--;
|
||
if (hasRunCheck) { hasRunCheck = false; results.Clear(); }
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
}
|
||
|
||
// ===================== 检测按钮 =====================
|
||
private void DrawCheckSection()
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
GUIStyle btnStyle = new GUIStyle(GUI.skin.button)
|
||
{
|
||
fontSize = 14,
|
||
fontStyle = FontStyle.Bold,
|
||
fixedHeight = 36
|
||
};
|
||
if (GUILayout.Button("🔍 开始检测动画帧数", btnStyle, GUILayout.ExpandWidth(true)))
|
||
{
|
||
RunCheck();
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
if (hasRunCheck && results.Count > 0)
|
||
{
|
||
EditorGUILayout.Space(4);
|
||
int totalFrames = 0;
|
||
foreach (var r in results) totalFrames += r.frameCount;
|
||
EditorGUILayout.HelpBox($"检测完成:共 {results.Count} 个动画,总计 {totalFrames} 帧", MessageType.Info);
|
||
}
|
||
}
|
||
|
||
// ===================== 检测结果 =====================
|
||
private void DrawResultListInner()
|
||
{
|
||
if (results.Count == 0)
|
||
{
|
||
EditorGUILayout.HelpBox("没有检测结果。", MessageType.Warning);
|
||
return;
|
||
}
|
||
|
||
GUILayout.Space(4);
|
||
|
||
// 表头
|
||
EditorGUILayout.BeginHorizontal();
|
||
GUILayout.Space(28);
|
||
GUIStyle colStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 11 };
|
||
GUILayout.Label("动画名称", colStyle, GUILayout.Width(200));
|
||
GUILayout.Label("帧数", colStyle, GUILayout.Width(70));
|
||
GUILayout.Label("时长(秒)", colStyle, GUILayout.Width(70));
|
||
GUILayout.Label("帧率(FPS)", colStyle, GUILayout.Width(70));
|
||
GUILayout.Label("工程路径", colStyle);
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
GUI.Box(GUILayoutUtility.GetLastRect(), "");
|
||
|
||
GUILayout.Space(2);
|
||
|
||
// 使用单独的滚动区域
|
||
resultListScrollPos = EditorGUILayout.BeginScrollView(resultListScrollPos, GUILayout.MaxHeight(400));
|
||
for (int i = 0; i < results.Count; i++)
|
||
{
|
||
var r = results[i];
|
||
|
||
// 交替行背景
|
||
Color rowColor = (i % 2 == 0) ? new Color(0, 0, 0, 0.1f) : new Color(0, 0, 0, 0.05f);
|
||
GUIStyle rowStyle = new GUIStyle(GUI.skin.box);
|
||
Texture2D rowTex = new Texture2D(1, 1);
|
||
rowTex.SetPixel(0, 0, rowColor);
|
||
rowTex.Apply();
|
||
rowStyle.normal.background = rowTex;
|
||
|
||
EditorGUILayout.BeginVertical(rowStyle);
|
||
EditorGUILayout.BeginHorizontal();
|
||
GUILayout.Space(5);
|
||
GUILayout.Label((i + 1).ToString(), GUILayout.Width(22));
|
||
|
||
// 动画名称
|
||
GUIContent clipIcon = EditorGUIUtility.IconContent("d_AnimationClip Icon");
|
||
GUILayout.Label(clipIcon, GUILayout.Width(20), GUILayout.Height(20));
|
||
GUILayout.Label(r.name, GUILayout.Width(200));
|
||
|
||
// 帧数
|
||
GUIStyle frameStyle = new GUIStyle(GUI.skin.label)
|
||
{
|
||
fontSize = 13,
|
||
fontStyle = FontStyle.Bold,
|
||
alignment = TextAnchor.MiddleCenter
|
||
};
|
||
GUILayout.Label(r.frameCount.ToString(), frameStyle, GUILayout.Width(70));
|
||
|
||
// 时长
|
||
GUILayout.Label(r.length.ToString("F3"), GUILayout.Width(70));
|
||
|
||
// 帧率
|
||
GUILayout.Label(r.frameRate.ToString("F1"), GUILayout.Width(70));
|
||
|
||
// 路径(可点击定位)
|
||
GUILayout.FlexibleSpace();
|
||
string displayPath = r.assetPath;
|
||
if (GUILayout.Button("📍 定位", GUILayout.Width(50), GUILayout.Height(20)))
|
||
{
|
||
// 在Project窗口中高亮选中该资源
|
||
var obj = AssetDatabase.LoadAssetAtPath<AnimationClip>(r.assetPath);
|
||
if (obj != null)
|
||
{
|
||
EditorGUIUtility.PingObject(obj);
|
||
Selection.activeObject = obj;
|
||
}
|
||
}
|
||
GUIStyle pathStyle = new GUIStyle(EditorStyles.miniLabel)
|
||
{
|
||
fontSize = 10
|
||
};
|
||
GUILayout.Label(displayPath, pathStyle);
|
||
EditorGUILayout.EndHorizontal();
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
// ===================== 背景设置 =====================
|
||
private void DrawBgSettingsInner()
|
||
{
|
||
EditorGUILayout.HelpBox("拖入背景图片(Texture2D)或选择GIF文件,拉伸填充整个工具窗口。", MessageType.Info);
|
||
GUILayout.Space(4);
|
||
|
||
Texture2D newBg = (Texture2D)EditorGUILayout.ObjectField("背景图片", bgImageGlobal, typeof(Texture2D), false);
|
||
if (newBg != bgImageGlobal)
|
||
{
|
||
bgImageGlobal = newBg;
|
||
ClearGif();
|
||
SaveBgImage();
|
||
}
|
||
|
||
string gifPath = EditorGUILayout.TextField("GIF文件路径", isGif ? gifSourcePath : "");
|
||
if (GUILayout.Button("选择GIF文件"))
|
||
{
|
||
string selected = EditorUtility.OpenFilePanel("选择GIF图片", "", "gif");
|
||
if (!string.IsNullOrEmpty(selected))
|
||
{
|
||
LoadGifFromFile(selected);
|
||
gifPath = selected;
|
||
}
|
||
}
|
||
if (GUILayout.Button("清除GIF"))
|
||
{
|
||
ClearGif();
|
||
gifPath = "";
|
||
}
|
||
GUILayout.Space(4);
|
||
}
|
||
|
||
// ===================== 核心检测逻辑 =====================
|
||
|
||
/// <summary>
|
||
/// 将绝对路径转换为 Unity AssetDatabase 可用的 Assets 相对路径
|
||
/// </summary>
|
||
private static string GetAssetPath(string absolutePath)
|
||
{
|
||
if (string.IsNullOrEmpty(absolutePath)) return "";
|
||
try
|
||
{
|
||
string dataPath = System.IO.Path.GetFullPath(Application.dataPath).Replace('\\', '/').TrimEnd('/');
|
||
string fullPath = System.IO.Path.GetFullPath(absolutePath).Replace('\\', '/').TrimEnd('/');
|
||
if (fullPath.StartsWith(dataPath, System.StringComparison.OrdinalIgnoreCase))
|
||
return "Assets" + fullPath.Substring(dataPath.Length);
|
||
}
|
||
catch { }
|
||
return "";
|
||
}
|
||
|
||
private void RunCheck()
|
||
{
|
||
if (clips.Count == 0)
|
||
{
|
||
EditorUtility.DisplayDialog("提示", "请先添加 Animation Clip!", "确定");
|
||
return;
|
||
}
|
||
|
||
results.Clear();
|
||
|
||
foreach (var clip in clips)
|
||
{
|
||
string assetPath = AssetDatabase.GetAssetPath(clip);
|
||
float length = clip.length;
|
||
float frameRate = 0f;
|
||
int frameCount = 0;
|
||
|
||
// 获取帧率:优先读取AnimationClip的帧率设置
|
||
// Unity中AnimationClip没有直接的frameRate属性,
|
||
// 通过遍历所有curve的最后一个keyframe的时间推算帧率
|
||
frameRate = GetClipFrameRate(clip);
|
||
|
||
if (frameRate > 0f)
|
||
frameCount = Mathf.RoundToInt(length * frameRate);
|
||
else
|
||
frameCount = GetClipFrameCountFromCurves(clip);
|
||
|
||
results.Add(new ClipResult
|
||
{
|
||
name = clip.name,
|
||
assetPath = assetPath,
|
||
frameCount = frameCount,
|
||
length = length,
|
||
frameRate = frameRate > 0f ? frameRate : (frameCount > 0 ? frameCount / length : 0f)
|
||
});
|
||
}
|
||
|
||
// 按时长(秒)从小到大排序
|
||
results.Sort((a, b) => a.length.CompareTo(b.length));
|
||
|
||
hasRunCheck = true;
|
||
Repaint();
|
||
|
||
Debug.Log($"[帧数检测] 完成,共检测 {results.Count} 个动画");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通过分析动画曲线推算帧率。
|
||
/// 取所有绑定中最大的 keyframe 时间差,近似帧间隔,帧率 = 1/间隔。
|
||
/// </summary>
|
||
private float GetClipFrameRate(AnimationClip clip)
|
||
{
|
||
if (clip == null) return 0f;
|
||
|
||
// 优先使用 EditorCurveBinding 读取曲线
|
||
var bindings = AnimationUtility.GetCurveBindings(clip);
|
||
if (bindings.Length == 0) return 0f;
|
||
|
||
float minInterval = float.MaxValue;
|
||
|
||
foreach (var binding in bindings)
|
||
{
|
||
var curve = AnimationUtility.GetEditorCurve(clip, binding);
|
||
if (curve == null || curve.length == 0) continue;
|
||
|
||
float prevTime = curve.keys[0].time;
|
||
for (int i = 1; i < curve.length; i++)
|
||
{
|
||
float diff = curve.keys[i].time - prevTime;
|
||
if (diff > 0.001f && diff < minInterval)
|
||
minInterval = diff;
|
||
prevTime = curve.keys[i].time;
|
||
}
|
||
}
|
||
|
||
if (minInterval < float.MaxValue && minInterval > 0f)
|
||
return 1f / minInterval;
|
||
|
||
return 0f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 备用方案:直接计算所有 curve 中不同时间点的数量作为帧数。
|
||
/// </summary>
|
||
private int GetClipFrameCountFromCurves(AnimationClip clip)
|
||
{
|
||
var bindings = AnimationUtility.GetCurveBindings(clip);
|
||
var frameTimes = new HashSet<float>();
|
||
|
||
foreach (var binding in bindings)
|
||
{
|
||
var curve = AnimationUtility.GetEditorCurve(clip, binding);
|
||
if (curve == null) continue;
|
||
foreach (var key in curve.keys)
|
||
frameTimes.Add((float)System.Math.Round(key.time, 4));
|
||
}
|
||
|
||
return frameTimes.Count;
|
||
}
|
||
|
||
// ===================== 通用 UI 辅助 =====================
|
||
private delegate void GUIAction();
|
||
|
||
private void DrawSectionHeader(string title, Color bgColor)
|
||
{
|
||
EditorGUILayout.Space(3);
|
||
var prevBg = GUI.backgroundColor;
|
||
GUI.backgroundColor = bgColor;
|
||
EditorGUILayout.BeginHorizontal("box");
|
||
GUI.backgroundColor = prevBg;
|
||
GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel)
|
||
{
|
||
fontSize = 13,
|
||
fontStyle = FontStyle.Bold,
|
||
alignment = TextAnchor.MiddleLeft,
|
||
padding = new RectOffset(4, 4, 2, 2)
|
||
};
|
||
GUIContent iconContent = EditorGUIUtility.IconContent("d_SettingsIcon");
|
||
GUILayout.Label(iconContent, GUILayout.Width(20), GUILayout.Height(20));
|
||
GUILayout.Label(title, headerStyle);
|
||
GUILayout.FlexibleSpace();
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
private static readonly Dictionary<string, Texture2D> bgTextures = new Dictionary<string, Texture2D>();
|
||
|
||
private Texture2D GetOrCreateBgTexture(string key, Color color)
|
||
{
|
||
if (!bgTextures.TryGetValue(key, out var tex))
|
||
{
|
||
tex = new Texture2D(1, 1);
|
||
tex.SetPixel(0, 0, color);
|
||
tex.Apply();
|
||
bgTextures[key] = tex;
|
||
}
|
||
return tex;
|
||
}
|
||
|
||
private void DrawSectionContent(GUIAction drawAction, Color bgColor)
|
||
{
|
||
GUIStyle bgStyle = new GUIStyle(GUI.skin.box);
|
||
bgStyle.normal.background = GetOrCreateBgTexture(bgColor.GetHashCode().ToString(), bgColor);
|
||
EditorGUILayout.BeginVertical(bgStyle);
|
||
drawAction();
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
// ===================== 背景图片/GIF =====================
|
||
private void SaveBgImage()
|
||
{
|
||
if (isGif && !string.IsNullOrEmpty(gifSourcePath))
|
||
{
|
||
EditorPrefs.SetString(BgImagePrefKey, "GIF:" + gifSourcePath);
|
||
}
|
||
else if (bgImageGlobal != null)
|
||
{
|
||
EditorPrefs.SetString(BgImagePrefKey, AssetDatabase.GetAssetPath(bgImageGlobal));
|
||
}
|
||
else
|
||
{
|
||
EditorPrefs.DeleteKey(BgImagePrefKey);
|
||
}
|
||
}
|
||
|
||
private void LoadBgImage()
|
||
{
|
||
string raw = EditorPrefs.GetString(BgImagePrefKey, "");
|
||
if (string.IsNullOrEmpty(raw)) return;
|
||
|
||
if (raw.StartsWith("GIF:"))
|
||
{
|
||
string gifPath = raw.Substring(4);
|
||
if (System.IO.File.Exists(gifPath))
|
||
LoadGifFromFile(gifPath);
|
||
else
|
||
EditorPrefs.DeleteKey(BgImagePrefKey);
|
||
}
|
||
else
|
||
{
|
||
bgImageGlobal = AssetDatabase.LoadAssetAtPath<Texture2D>(raw);
|
||
if (bgImageGlobal == null)
|
||
EditorPrefs.DeleteKey(BgImagePrefKey);
|
||
}
|
||
}
|
||
|
||
private void ClearGif()
|
||
{
|
||
if (gifFrames != null)
|
||
{
|
||
for (int i = 0; i < gifFrames.Length; i++)
|
||
{
|
||
if (gifFrames[i] != null) DestroyImmediate(gifFrames[i]);
|
||
}
|
||
gifFrames = null;
|
||
gifDelays = null;
|
||
}
|
||
isGif = false;
|
||
gifSourcePath = "";
|
||
gifTotalDuration = 0;
|
||
}
|
||
|
||
private void LoadGifFromFile(string filePath)
|
||
{
|
||
ClearGif();
|
||
if (!System.IO.File.Exists(filePath)) return;
|
||
|
||
try
|
||
{
|
||
byte[] data = System.IO.File.ReadAllBytes(filePath);
|
||
if (data.Length < 13 || data[0] != 'G' || data[1] != 'I' || data[2] != 'F')
|
||
{
|
||
Debug.LogWarning("不是GIF格式文件");
|
||
return;
|
||
}
|
||
|
||
int pos = 6;
|
||
int screenWidth = data[pos] | (data[pos + 1] << 8);
|
||
int screenHeight = data[pos + 2] | (data[pos + 3] << 8);
|
||
pos += 4;
|
||
byte packed = data[pos++];
|
||
pos++; // bg color index
|
||
pos++; // pixel aspect ratio
|
||
|
||
Color32[] gct = null;
|
||
if ((packed & 0x80) != 0)
|
||
{
|
||
int gctCount = 1 << ((packed & 0x07) + 1);
|
||
gct = ReadColorTable(data, ref pos, gctCount);
|
||
}
|
||
|
||
var frames = new List<Texture2D>();
|
||
var delays = new List<int>();
|
||
|
||
Color32[] canvas = new Color32[screenWidth * screenHeight];
|
||
Color32 bgColor = (gct != null) ? gct[0] : new Color32(0, 0, 0, 255);
|
||
for (int i = 0; i < canvas.Length; i++) canvas[i] = bgColor;
|
||
|
||
int gceDisposal = 0, gceTransIdx = -1, gceDelay = 100;
|
||
Color32[] savedCanvas = null;
|
||
|
||
while (pos < data.Length)
|
||
{
|
||
byte blockType = data[pos++];
|
||
if (blockType == 0x3B) break; // trailer
|
||
|
||
if (blockType == 0x21) // extension
|
||
{
|
||
byte label = data[pos++];
|
||
if (label == 0xF9) // GCE
|
||
{
|
||
pos++; // block size
|
||
byte gcPacked = data[pos++];
|
||
gceDisposal = (gcPacked >> 2) & 0x07;
|
||
gceTransIdx = ((gcPacked & 0x01) != 0) ? data[pos++] : -1;
|
||
if (gceTransIdx == -1) pos++;
|
||
gceDelay = (data[pos] | (data[pos + 1] << 8)) * 10;
|
||
if (gceDelay == 0) gceDelay = 100;
|
||
pos += 2;
|
||
pos++; // terminator
|
||
}
|
||
else
|
||
{
|
||
// skip other extensions
|
||
while (pos < data.Length)
|
||
{
|
||
int sz = data[pos++];
|
||
if (sz == 0) break;
|
||
pos += sz;
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (blockType == 0x2C) // image descriptor
|
||
{
|
||
int left = data[pos] | (data[pos + 1] << 8);
|
||
int top = data[pos + 2] | (data[pos + 3] << 8);
|
||
int width = data[pos + 4] | (data[pos + 5] << 8);
|
||
int height = data[pos + 6] | (data[pos + 7] << 8);
|
||
pos += 8;
|
||
byte imgPacked = data[pos++];
|
||
|
||
Color32[] lct = null;
|
||
if ((imgPacked & 0x80) != 0)
|
||
{
|
||
int lctCount = 1 << ((imgPacked & 0x07) + 1);
|
||
lct = ReadColorTable(data, ref pos, lctCount);
|
||
}
|
||
Color32[] colorTable = lct ?? gct ?? new Color32[256];
|
||
|
||
int minCodeSize = data[pos++];
|
||
int lzwPos = pos;
|
||
int bytesRemaining = 0;
|
||
int bytePos = 0;
|
||
byte[] subBlock = new byte[256];
|
||
|
||
int clearCode = 1 << minCodeSize;
|
||
int eoiCode = clearCode + 1;
|
||
int codeSize = minCodeSize + 1;
|
||
int codeMask = (1 << codeSize) - 1;
|
||
int oldCode = -1;
|
||
|
||
var codeTable = new Dictionary<int, List<byte>>();
|
||
InitializeLzwTable(codeTable, clearCode);
|
||
|
||
int x = 0, y = 0;
|
||
bool interlaced = (imgPacked & 0x40) != 0;
|
||
int pass = 0;
|
||
int[] interlaceOffsets = { 0, 4, 2, 1 };
|
||
int[] interlaceSteps = { 8, 8, 4, 2 };
|
||
int pixelIdx = 0;
|
||
var pixelBuffer = new List<byte>();
|
||
|
||
while (pixelIdx < width * height)
|
||
{
|
||
int code = ReadNextLzwCode(data, ref lzwPos, ref bytesRemaining, ref bytePos, ref subBlock, minCodeSize, codeSize, ref codeMask, ref codeTable, ref oldCode, clearCode, eoiCode, pixelBuffer);
|
||
if (code == -1) break;
|
||
|
||
foreach (byte b in pixelBuffer)
|
||
{
|
||
if (pixelIdx >= width * height) break;
|
||
int px = x;
|
||
int py = interlaced ? GetInterlacedY(y, ref pass, height) : y;
|
||
|
||
if (px < width && py < height)
|
||
{
|
||
int idx = (top + py) * screenWidth + (left + px);
|
||
if (idx >= 0 && idx < canvas.Length)
|
||
{
|
||
int cidx = b;
|
||
if (gceTransIdx >= 0 && cidx == gceTransIdx)
|
||
{ }
|
||
else if (cidx < colorTable.Length)
|
||
canvas[idx] = colorTable[cidx];
|
||
}
|
||
}
|
||
|
||
x++;
|
||
if (x >= width) { x = 0; y++; }
|
||
pixelIdx++;
|
||
}
|
||
}
|
||
|
||
// 保存disposal=3需要的画布
|
||
if (gceDisposal == 3)
|
||
{
|
||
savedCanvas = new Color32[canvas.Length];
|
||
for (int ci = 0; ci < canvas.Length; ci++) savedCanvas[ci] = canvas[ci];
|
||
}
|
||
|
||
// 生成帧纹理
|
||
var frameTex = new Texture2D(screenWidth, screenHeight, TextureFormat.RGBA32, false);
|
||
frameTex.SetPixels32(canvas);
|
||
frameTex.Apply(false, false);
|
||
frames.Add(frameTex);
|
||
delays.Add(gceDelay);
|
||
|
||
// disposal处理
|
||
if (gceDisposal == 2) // restore to bg
|
||
{
|
||
Color32 bgCol = (gct != null && data[11] < gct.Length) ? gct[data[11]] : new Color32(0, 0, 0, 255);
|
||
for (int fy = top; fy < top + height && fy < screenHeight; fy++)
|
||
for (int fx = left; fx < left + width && fx < screenWidth; fx++)
|
||
canvas[fy * screenWidth + fx] = bgCol;
|
||
}
|
||
else if (gceDisposal == 3 && savedCanvas != null)
|
||
{
|
||
for (int ci = 0; ci < canvas.Length; ci++) canvas[ci] = savedCanvas[ci];
|
||
}
|
||
|
||
// 跳到下一个块
|
||
while (pos < data.Length)
|
||
{
|
||
int sz = data[pos++];
|
||
if (sz == 0) break;
|
||
pos += sz;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (frames.Count > 0)
|
||
{
|
||
gifFrames = frames.ToArray();
|
||
gifDelays = delays.ToArray();
|
||
gifTotalDuration = 0;
|
||
foreach (int d in gifDelays) gifTotalDuration += d;
|
||
isGif = true;
|
||
gifSourcePath = filePath;
|
||
gifStartTime = EditorApplication.timeSinceStartup;
|
||
SaveBgImage();
|
||
Debug.Log($"[GIF背景] 加载完成:{gifFrames.Length} 帧,总时长 {gifTotalDuration}ms");
|
||
}
|
||
}
|
||
catch (System.Exception e)
|
||
{
|
||
Debug.LogError("GIF解析失败: " + e.Message);
|
||
ClearGif();
|
||
}
|
||
}
|
||
|
||
private Color32[] ReadColorTable(byte[] data, ref int pos, int count)
|
||
{
|
||
Color32[] table = new Color32[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
table[i] = new Color32(data[pos], data[pos + 1], data[pos + 2], 255);
|
||
pos += 3;
|
||
}
|
||
return table;
|
||
}
|
||
|
||
private static void InitializeLzwTable(Dictionary<int, List<byte>> table, int clearCode)
|
||
{
|
||
table.Clear();
|
||
for (int i = 0; i < clearCode; i++)
|
||
table[i] = new List<byte> { (byte)i };
|
||
table[clearCode] = new List<byte>(); // clear
|
||
table[clearCode + 1] = new List<byte>(); // eoi
|
||
}
|
||
|
||
private static int ReadNextLzwCode(byte[] data, ref int lzwPos, ref int bytesRemaining, ref int bytePos, ref byte[] subBlock, int minCodeSize, int codeSize, ref int codeMask, ref Dictionary<int, List<byte>> codeTable, ref int oldCode, int clearCode, int eoiCode, List<byte> output)
|
||
{
|
||
output.Clear();
|
||
int code = 0;
|
||
int bitsNeeded = codeSize;
|
||
int bitsRead = 0;
|
||
|
||
while (bitsRead < bitsNeeded)
|
||
{
|
||
if (bytesRemaining == 0)
|
||
{
|
||
if (lzwPos >= data.Length) return -1;
|
||
int sz = data[lzwPos++];
|
||
if (sz == 0) return -1;
|
||
bytesRemaining = sz;
|
||
for (int i = 0; i < sz; i++)
|
||
subBlock[i] = data[lzwPos++];
|
||
bytePos = 0;
|
||
}
|
||
|
||
int bitsAvail = 8 * bytesRemaining - bytePos;
|
||
int bitsToRead = System.Math.Min(bitsAvail, bitsNeeded - bitsRead);
|
||
int mask = (1 << bitsToRead) - 1;
|
||
|
||
int val = (subBlock[bytePos >> 3] >> (bytePos & 7)) & mask;
|
||
bytePos += bitsToRead;
|
||
code |= val << bitsRead;
|
||
bitsRead += bitsToRead;
|
||
}
|
||
|
||
if (code == clearCode)
|
||
{
|
||
codeSize = minCodeSize + 1;
|
||
codeMask = (1 << codeSize) - 1;
|
||
oldCode = -1;
|
||
InitializeLzwTable(codeTable, clearCode);
|
||
return -1;
|
||
}
|
||
|
||
if (code == eoiCode) return -1;
|
||
|
||
if (codeTable.TryGetValue(code, out var entry))
|
||
{
|
||
output.AddRange(entry);
|
||
if (oldCode >= 0 && codeTable.ContainsKey(oldCode))
|
||
{
|
||
var newEntry = new List<byte>(codeTable[oldCode]);
|
||
newEntry.Add(entry[0]);
|
||
codeTable[codeTable.Count] = newEntry;
|
||
|
||
if (codeTable.Count > codeMask + 1 && codeSize < 12)
|
||
{
|
||
codeSize++;
|
||
codeMask = (1 << codeSize) - 1;
|
||
}
|
||
}
|
||
oldCode = code;
|
||
}
|
||
return code;
|
||
}
|
||
|
||
private static int GetInterlacedY(int y, ref int pass, int height)
|
||
{
|
||
int[] offsets = { 0, 4, 2, 1 };
|
||
int[] steps = { 8, 8, 4, 2 };
|
||
int py = offsets[pass] + y * steps[pass];
|
||
while (py >= height)
|
||
{
|
||
pass++;
|
||
if (pass > 3) { pass = 0; return y; }
|
||
py = offsets[pass] + y * steps[pass];
|
||
}
|
||
return py;
|
||
}
|
||
}
|