Files
5.24BuYu/Assets/MyGame/Framework/Editor/AnimationClipFrameChecker.cs
T
2026-10-20 18:30:03 +08:00

926 lines
33 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}