From 5d04801559060f124c5adf6df54b8dbb18ecc556 Mon Sep 17 00:00:00 2001 From: huimanQAQ Date: Mon, 2 Nov 2026 22:57:47 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(jacket):=20=E6=89=93=E5=8C=85=E8=87=AA?= =?UTF-8?q?=E5=88=B6=E5=B0=81=E9=9D=A2=E6=97=B6=E9=99=90=E5=88=B6=E5=B0=BA?= =?UTF-8?q?=E5=AF=B8=EF=BC=8C=E9=81=BF=E5=85=8D=E8=B6=85=E5=A4=A7=E7=BA=B9?= =?UTF-8?q?=E7=90=86=E5=AF=BC=E8=87=B4=E9=80=89=E6=AD=8C=E5=8D=A1=E9=A1=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自制封面会把原图尺寸直接写进 AB,且固定为未压缩的 RGBA32。实测游戏数据里 存在 3000x3000 的封面(34 MB 显存 / 18.6 MB 文件),而官方封面是 400x400 BC7(0.16 MB / 130 KB),游戏同步加载时会造成明显掉帧。 - AssetBundleCreator 新增 MaxJacketDimension = 400,大封面按长边等比缩放 (只缩不放,保持原宽高比,Lanczos3) - 导入封面、改 ID 重打包、图片转 AB 工具、mcm makeab 统一应用该上限 - jacket_s 仍然是 200x200,不受影响 --- MaiChartManager.CLI/Commands/MakeAbCommand.cs | 3 +- .../Music/MusicTransferController.cs | 3 +- .../Tools/ImageToAbToolController.cs | 3 +- MaiChartManager/Utils/AssetBundleCreator.cs | 33 +++++++++++++++---- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/MaiChartManager.CLI/Commands/MakeAbCommand.cs b/MaiChartManager.CLI/Commands/MakeAbCommand.cs index 1a156475..e44414d3 100644 --- a/MaiChartManager.CLI/Commands/MakeAbCommand.cs +++ b/MaiChartManager.CLI/Commands/MakeAbCommand.cs @@ -92,7 +92,8 @@ await Task.Run(() => Path.Combine(jacketDir, $"ui_jacket_{id}.ab"), $"UI_Jacket_{id}", $"assets/assetbundle/jacket/ui_jacket_{id}.png", - $"jacket/ui_jacket_{id}.ab"); + $"jacket/ui_jacket_{id}.ab", + maxDimension: AssetBundleCreator.MaxJacketDimension); AssetBundleCreator.CreateTextureAssetBundle( path!, diff --git a/MaiChartManager/Controllers/Music/MusicTransferController.cs b/MaiChartManager/Controllers/Music/MusicTransferController.cs index bd57fab4..b755da7c 100644 --- a/MaiChartManager/Controllers/Music/MusicTransferController.cs +++ b/MaiChartManager/Controllers/Music/MusicTransferController.cs @@ -526,7 +526,8 @@ private void MoveJacketSoundVideo(MusicXmlWithABJacket music, int newId, string abJacketTarget, $"UI_Jacket_{idPad}", $"assets/assetbundle/jacket/ui_jacket_{idPad}.png", - $"jacket/ui_jacket_{idPad}.ab"); + $"jacket/ui_jacket_{idPad}.ab", + maxDimension: AssetBundleCreator.MaxJacketDimension); // 对小jacket:如果存在,重打包;如果不存在,则从png重新缩放,重新CreateTextureAssetBundle if (oldSmallAb is not null && System.IO.File.Exists(oldSmallAb)) diff --git a/MaiChartManager/Controllers/Tools/ImageToAbToolController.cs b/MaiChartManager/Controllers/Tools/ImageToAbToolController.cs index 0fa5eade..2d1fa90b 100644 --- a/MaiChartManager/Controllers/Tools/ImageToAbToolController.cs +++ b/MaiChartManager/Controllers/Tools/ImageToAbToolController.cs @@ -108,7 +108,8 @@ await WriteEvent( fullAbPath, $"UI_Jacket_{id}", $"assets/assetbundle/jacket/ui_jacket_{id}.png", - $"jacket/ui_jacket_{id}.ab"); + $"jacket/ui_jacket_{id}.ab", + maxDimension: AssetBundleCreator.MaxJacketDimension); var smallAbPath = Path.Combine(jacketSmallDir, $"ui_jacket_{id}_s.ab"); AssetBundleCreator.CreateTextureAssetBundle( diff --git a/MaiChartManager/Utils/AssetBundleCreator.cs b/MaiChartManager/Utils/AssetBundleCreator.cs index b36bbd8d..e1d662e9 100644 --- a/MaiChartManager/Utils/AssetBundleCreator.cs +++ b/MaiChartManager/Utils/AssetBundleCreator.cs @@ -12,6 +12,14 @@ public static class AssetBundleCreator { private const int TextureFormatRgba32 = 4; + /// + /// 大封面(jacket)允许的最大边长。游戏内官方封面是 400×400 的 BC7, + /// 显示区域也用不到更大尺寸;而自制封面如果按原图尺寸写进 AB, + /// 3000×3000 的 RGBA32 一张就是 34 MB 显存、18 MB 文件, + /// 选歌/结算时同步加载会造成明显掉帧,所以在打包时钳制到这个尺寸。 + /// + public const int MaxJacketDimension = 400; + private static string TemplateAbPath => Path.Combine(StaticSettings.StreamingAssets, "A000", "AssetBundleImages", "jacket", "ui_jacket_000000.ab"); @@ -22,11 +30,13 @@ public static void CreateTextureAssetBundle( string containerPath, string abName, int? resizeWidth = null, - int? resizeHeight = null) + int? resizeHeight = null, + int? maxDimension = null) { if (!File.Exists(imagePath)) throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath); - CreateTextureAssetBundle(File.ReadAllBytes(imagePath), outputAbPath, assetName, containerPath, abName, resizeWidth, resizeHeight); + CreateTextureAssetBundle(File.ReadAllBytes(imagePath), outputAbPath, assetName, containerPath, abName, resizeWidth, resizeHeight, + maxDimension); } public static void CreateTextureAssetBundle( @@ -36,11 +46,20 @@ public static void CreateTextureAssetBundle( string containerPath, string abName, int? resizeWidth = null, - int? resizeHeight = null) + int? resizeHeight = null, + int? maxDimension = null) { using var image = SixLabors.ImageSharp.Image.Load(pngImageData); if (resizeWidth.HasValue && resizeHeight.HasValue) image.Mutate(x => x.Resize(resizeWidth.Value, resizeHeight.Value)); + else if (maxDimension is > 0 && (image.Width > maxDimension.Value || image.Height > maxDimension.Value)) + // 等比缩到长边不超过上限,只缩不放,避免改变原本的宽高比 + image.Mutate(x => x.Resize(new ResizeOptions + { + Size = new SixLabors.ImageSharp.Size(maxDimension.Value, maxDimension.Value), + Mode = ResizeMode.Max, + Sampler = KnownResamplers.Lanczos3, + })); image.Mutate(x => x.Flip(FlipMode.Vertical)); @@ -170,7 +189,8 @@ public static string CreateMusicJacketAssetBundles(ReadOnlySpan pngImageDa Path.Combine(abiDir, $"{key.ToLowerInvariant()}.ab"), key, $"assets/assetbundle/jacket/{key.ToLowerInvariant()}.png", - $"jacket/{key.ToLowerInvariant()}.ab"); + $"jacket/{key.ToLowerInvariant()}.ab", + maxDimension: MaxJacketDimension); CreateTextureAssetBundle( pngImageData, @@ -193,7 +213,8 @@ public static void RepackTextureAssetBundle( string outputAbPath, string assetName, string containerPath, - string abName) + string abName, + int? maxDimension = null) { if (!File.Exists(inputAbPath)) throw new FileNotFoundException($"Input AB not found: {inputAbPath}", inputAbPath); @@ -201,7 +222,7 @@ public static void RepackTextureAssetBundle( var pngBytes = ImageConvert.GetTextureAsPngData(inputAbPath); if (pngBytes is null || pngBytes.Length == 0) throw new InvalidOperationException($"Could not decode jacket texture to PNG from: {inputAbPath}"); - CreateTextureAssetBundle(pngBytes, outputAbPath, assetName, containerPath, abName); + CreateTextureAssetBundle(pngBytes, outputAbPath, assetName, containerPath, abName, maxDimension: maxDimension); } private static void WriteUncompressedBundle(byte[] assetsData, string cabName, Stream output) From a34981e849a47ac3b97ba878daa05c1eb22dc3ab Mon Sep 17 00:00:00 2001 From: Clansty Date: Sat, 7 Nov 2026 01:07:29 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor(jacket):=20=E7=BC=A9=E6=94=BE?= =?UTF-8?q?=E8=A7=84=E5=88=99=E7=BB=9F=E4=B8=80=E5=88=B0=20GetResizeOption?= =?UTF-8?q?s=20=E5=B9=B6=E8=A1=A5=E5=85=85=E5=8F=82=E6=95=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 显式尺寸与 maxDimension 的优先级改用 GetResizeOptions 显式表达,不再靠 else if 静默吞掉 maxDimension - 显式尺寸分支必须指定 Mode = Stretch:ResizeOptions 默认值是 Crop,直接替换 Resize(w, h) 会变成居中裁剪 - 给 CreateTextureAssetBundle 补 XML 文档,说明 resizeWidth/resizeHeight/maxDimension 的优先级与是否保持宽高比 --- MaiChartManager/Utils/AssetBundleCreator.cs | 49 ++++++++++++++++----- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/MaiChartManager/Utils/AssetBundleCreator.cs b/MaiChartManager/Utils/AssetBundleCreator.cs index e1d662e9..5df00f04 100644 --- a/MaiChartManager/Utils/AssetBundleCreator.cs +++ b/MaiChartManager/Utils/AssetBundleCreator.cs @@ -39,6 +39,15 @@ public static void CreateTextureAssetBundle( maxDimension); } + /// + /// 把图片写成 Texture2D 的 AssetBundle。 + /// + /// 显式目标宽度,需与 成对给出;给出时会强制拉伸到该尺寸(可能改变宽高比) + /// 显式目标高度 + /// + /// 长边上限,超出时等比缩小(只缩不放、不裁剪)。 + /// 仅在没给显式尺寸时生效:显式尺寸优先,两者同时给出时本参数被忽略。 + /// public static void CreateTextureAssetBundle( ReadOnlySpan pngImageData, string outputAbPath, @@ -50,16 +59,9 @@ public static void CreateTextureAssetBundle( int? maxDimension = null) { using var image = SixLabors.ImageSharp.Image.Load(pngImageData); - if (resizeWidth.HasValue && resizeHeight.HasValue) - image.Mutate(x => x.Resize(resizeWidth.Value, resizeHeight.Value)); - else if (maxDimension is > 0 && (image.Width > maxDimension.Value || image.Height > maxDimension.Value)) - // 等比缩到长边不超过上限,只缩不放,避免改变原本的宽高比 - image.Mutate(x => x.Resize(new ResizeOptions - { - Size = new SixLabors.ImageSharp.Size(maxDimension.Value, maxDimension.Value), - Mode = ResizeMode.Max, - Sampler = KnownResamplers.Lanczos3, - })); + var resize = GetResizeOptions(image.Width, image.Height, resizeWidth, resizeHeight, maxDimension); + if (resize is not null) + image.Mutate(x => x.Resize(resize)); image.Mutate(x => x.Flip(FlipMode.Vertical)); @@ -71,6 +73,33 @@ public static void CreateTextureAssetBundle( CreateTextureAssetBundle(rgbaData, width, height, outputAbPath, assetName, containerPath, abName); } + /// + /// 算出这张图要缩放到多大,返回 null 表示不需要缩放。 + /// 显式尺寸优先于长边上限,避免两套缩放规则互相覆盖时行为变得不可预测。 + /// + private static ResizeOptions? GetResizeOptions(int width, int height, int? resizeWidth, int? resizeHeight, int? maxDimension) + { + if (resizeWidth is > 0 && resizeHeight is > 0) + // Stretch 才是原来 x.Resize(w, h) 的行为;ResizeOptions 的默认值是 Crop,不显式指定会变成裁剪 + return new ResizeOptions + { + Size = new Size(resizeWidth.Value, resizeHeight.Value), + Mode = ResizeMode.Stretch, + Sampler = KnownResamplers.Bicubic, + }; + + if (maxDimension is > 0 && (width > maxDimension.Value || height > maxDimension.Value)) + // 等比缩到长边不超过上限,只缩不放,避免改变原本的宽高比 + return new ResizeOptions + { + Size = new Size(maxDimension.Value, maxDimension.Value), + Mode = ResizeMode.Max, + Sampler = KnownResamplers.Lanczos3, + }; + + return null; + } + private static void CreateTextureAssetBundle( byte[] rgbaData, int width, From e3813307d7b0a7617ae61b05fa40226a9c3be23b Mon Sep 17 00:00:00 2001 From: Clansty Date: Sat, 7 Nov 2026 01:10:37 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(jacket):=20=E9=95=BF=E8=BE=B9=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E6=94=B9=E6=88=90=E7=8B=AC=E7=AB=8B=E5=88=A4=E6=96=AD?= =?UTF-8?q?=EF=BC=8C=E6=98=BE=E5=BC=8F=E5=B0=BA=E5=AF=B8=E4=B9=8B=E5=90=8E?= =?UTF-8?q?=E4=BB=8D=E7=84=B6=E9=92=B3=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原来写成 else if,同时给 resizeWidth/resizeHeight 和 maxDimension 时上限会被整体跳过,超大纹理照样写进 AB - 现在显式尺寸先应用,随后独立判断长边是否超上限,任何调用组合都不会产出超限纹理 - 去掉仅为表达优先级而引入的 GetResizeOptions,逻辑回到一处,显式尺寸分支显式写 Stretch 避免 ResizeOptions 默认 Crop 造成裁剪 --- MaiChartManager/Utils/AssetBundleCreator.cs | 43 ++++++++------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/MaiChartManager/Utils/AssetBundleCreator.cs b/MaiChartManager/Utils/AssetBundleCreator.cs index 5df00f04..57c46366 100644 --- a/MaiChartManager/Utils/AssetBundleCreator.cs +++ b/MaiChartManager/Utils/AssetBundleCreator.cs @@ -46,7 +46,7 @@ public static void CreateTextureAssetBundle( /// 显式目标高度 /// /// 长边上限,超出时等比缩小(只缩不放、不裁剪)。 - /// 仅在没给显式尺寸时生效:显式尺寸优先,两者同时给出时本参数被忽略。 + /// 无论是否给了显式尺寸都会生效:显式尺寸先应用,之后仍按上限钳制。 /// public static void CreateTextureAssetBundle( ReadOnlySpan pngImageData, @@ -59,45 +59,34 @@ public static void CreateTextureAssetBundle( int? maxDimension = null) { using var image = SixLabors.ImageSharp.Image.Load(pngImageData); - var resize = GetResizeOptions(image.Width, image.Height, resizeWidth, resizeHeight, maxDimension); - if (resize is not null) - image.Mutate(x => x.Resize(resize)); - image.Mutate(x => x.Flip(FlipMode.Vertical)); - - int width = image.Width; - int height = image.Height; - var rgbaData = new byte[width * height * 4]; - image.CopyPixelDataTo(rgbaData); - - CreateTextureAssetBundle(rgbaData, width, height, outputAbPath, assetName, containerPath, abName); - } - - /// - /// 算出这张图要缩放到多大,返回 null 表示不需要缩放。 - /// 显式尺寸优先于长边上限,避免两套缩放规则互相覆盖时行为变得不可预测。 - /// - private static ResizeOptions? GetResizeOptions(int width, int height, int? resizeWidth, int? resizeHeight, int? maxDimension) - { if (resizeWidth is > 0 && resizeHeight is > 0) - // Stretch 才是原来 x.Resize(w, h) 的行为;ResizeOptions 的默认值是 Crop,不显式指定会变成裁剪 - return new ResizeOptions + // 显式尺寸按拉伸处理,和原来的 x.Resize(w, h) 一致;ResizeOptions 默认 Mode 是 Crop,必须显式写 Stretch + image.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(resizeWidth.Value, resizeHeight.Value), Mode = ResizeMode.Stretch, Sampler = KnownResamplers.Bicubic, - }; + })); - if (maxDimension is > 0 && (width > maxDimension.Value || height > maxDimension.Value)) + // 上限独立判断,不是 else if:任何调用组合都不该写出超过上限的纹理 + if (maxDimension is > 0 && (image.Width > maxDimension.Value || image.Height > maxDimension.Value)) // 等比缩到长边不超过上限,只缩不放,避免改变原本的宽高比 - return new ResizeOptions + image.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(maxDimension.Value, maxDimension.Value), Mode = ResizeMode.Max, Sampler = KnownResamplers.Lanczos3, - }; + })); - return null; + image.Mutate(x => x.Flip(FlipMode.Vertical)); + + int width = image.Width; + int height = image.Height; + var rgbaData = new byte[width * height * 4]; + image.CopyPixelDataTo(rgbaData); + + CreateTextureAssetBundle(rgbaData, width, height, outputAbPath, assetName, containerPath, abName); } private static void CreateTextureAssetBundle(