目前网络上常用的Unity AssetBundle解包工具主要是disunity、AssetsBundleExtractor和UnityStudio三种,但是disunity不支持较新版的包,也不能导出PNG格式,后两者功能强大、更新及时,然而(据我所知)不支持批量自动化导出。于是我直接使用Unity本身的组件写了一个GUI工具,能够将指定目录下的所有”.unity3d”文件中的所有Texture2D资源批量导出成PNG格式,并支持根据不同来源的包来定制导出方式。

第一步,我们需要一个UI,简单起见就放一个Dropdown和一个Button:

然后,我们写一个UnityExtractor类,用来处理按钮Click事件。首先实现核心的ExtractAsset方法,为了支持各种定制化要求,这个方法需要支持很多参数:

private void ExtractAsset(
    string folderPath, // PNG输出目录
    string filePath, // ".unity3d"文件路径
    int pngX, int pngY, int pngWidth, int pngHeight, // 如果PNG需要裁剪,指定目标区域的坐标和尺寸。尺寸传入0则不裁剪。pngY一般传0或-1,具体算法见实现。
    bool needScale, int scaledWidth, int scaledHeight,  // 如果PNG需要缩放,指定最终尺寸。
    Func<string, bool> assetNameChecker, // 根据name判定是否需要导出这个asset
    Func<string, string, string> pngFileNameMaker // 支持对导出的PNG文件重命名)
{
    // ...
}

ExtractAsset函数要做的第一件事就是打开AssetBundle文件并遍历所有包含的asset,判断是否需要导出:

AssetBundle assetBundle = AssetBundle.LoadFromFile(filePath);
UnityEngine.Object[] assetList = assetBundle.LoadAllAssets();
foreach (var asset in assetList)
{
    string assetName = asset.name;
    if (asset is Texture2D && (assetNameChecker == null || assetNameChecker(assetName)))
    {
        Debug.Log("Processing " + asset.GetType() + " asset: " + assetName);
        // ...
    }
}

如果需要导出,那么我们将这个Texture2D渲染到RenderTexture中[1],然后用一个新的Texture2D从RenderTexture中读出像素值并应用裁剪,接着应用缩放,最后保存为PNG格式文件:

Texture2D t2d = asset as Texture2D;
int width = pngWidth == 0 ? t2d.width : pngWidth;
int height = pngHeight == 0 ? t2d.height : pngHeight;

t2d.filterMode = FilterMode.Point;
RenderTexture rt = RenderTexture.GetTemporary(t2d.width, t2d.height);
rt.filterMode = FilterMode.Point;
RenderTexture.active = rt;
Graphics.Blit(t2d, rt);
Texture2D newT2d = new Texture2D(width, height);
newT2d.ReadPixels(new Rect(pngX, pngY == -1 ? t2d.height - height : pngY, width, height), 0, 0);
newT2d.Apply();
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);

if (needScale)
{
    newT2d = ScaleTexture(newT2d, scaledWidth, scaledHeight);
}

byte[] pngData = newT2d.EncodeToPNG();
string assetFileName = Path.GetFileName(assetName);
string outputFileName = pngFileNameMaker == null ? assetFileName : pngFileNameMaker(Path.GetFileNameWithoutExtension(filePath), assetFileName);
File.WriteAllBytes(folderPath + "/output/" + outputFileName, pngData);

ScaleTexture函数实现如下:

private Texture2D ScaleTexture(Texture2D source, int targetWidth, int targetHeight)
{
    Texture2D result = new Texture2D(targetWidth, targetHeight, source.format, true);
    Color[] rpixels = result.GetPixels(0);
    float incX = ((float)1 / source.width) * ((float)source.width / targetWidth);
    float incY = ((float)1 / source.height) * ((float)source.height / targetHeight);
    for (int px = 0; px < rpixels.Length; px++)
    {
        rpixels[px] = source.GetPixelBilinear(incX * ((float)px % targetWidth), incY * Mathf.Floor(px / targetWidth));
    }
    result.SetPixels(rpixels, 0);
    result.Apply();
    return result;
}

最后在ExtractAsset函数结尾别忘了:

assetBundle.Unload(true);

下面是Run函数的基本实现:

Dropdown dropdown = GameObject.Find("ddGameName").GetComponent<Dropdown>();
string selectedName = dropdown.GetComponentInChildren<Text>().text;

int pngX = 0;
int pngY = 0;
int pngWidth = 0;
int pngHeight = 0;
bool needScale = false;
int scaledWidth = 0;
int scaledHeight = 0;
Func<string, bool> assetNameChecker = null;
Func<string, string, string> pngFileNameMaker = (namePrefix, assetFileName) => namePrefix + ".png";

switch (selectedName)
{
    case "Name1":
        {
            pngWidth = 1334;
            pngHeight = 750;
            pngX = 0;
            pngY = -1;
            assetNameChecker = (assetName) => Regex.IsMatch(assetName, "^\\d+$");
            pngFileNameMaker = (namePrefix, assetFileName) => namePrefix + "_" + assetFileName + ".png";
            break;
        }
    case "Name2":
        {
            needScale = true;
            scaledWidth = 1024;
            scaledHeight = 576;
            break;
        }
}

string folderRoot = EditorUtility.OpenFolderPanel("Choose folder", @"D:\Temp", "");
if (folderRoot.Length != 0)
{
    string[] folderList = Directory.GetDirectories(folderRoot);
    if (folderList.Length > 0)
    {
        foreach (string folderPath in folderList)
        {
            if (!folderPath.EndsWith("output"))
            {
                Directory.CreateDirectory(folderPath + "/output");
                string[] fileList = Directory.GetFiles(folderPath);
                foreach (string filePath in fileList)
                {
                    if (filePath.EndsWith(".unity3d"))
                    {
                        ExtractAsset(folderPath, filePath, pngX, pngY, pngWidth, pngHeight, needScale, scaledWidth, scaledHeight, assetNameChecker, pngFileNameMaker);
                    }
                }
            }
        }
    }
}

Debug.Log("ExtractAsset() done!");

使用的时候,打开这个Unity工程,直接运行然后点击Start按钮选择目录即可。以上的代码为了说明原理做了简化,我实际中用的代码还有些其他细节处理,并支持TXT格式导出,原理类似就不赘述了。

需要注意的是,这个工程无法Build,因为使用了UnityEditor命名空间下的EditorUtility来打开目录,如果想要Build成EXE文件来支持命令行操作等更深自动化操作的话,需要用其他方式实现打开文件操作,因为我这个人很懒,就不继续搞了(逃

参考资料:
[1] How Can I Get Pixels From Unreadable Textures?

» 转载请注明来源及链接:未来代码研究所

Related Posts:

  • No Related Posts

2 Responses to “使用Unity开发AssetBundle批量解包工具”

Leave a Reply

World Line
Time Machine
Friendly Links
Online Tools