ASTC纹理压缩与Arm-astc-engine:移动端显存优化从原理到落地
发布时间:2026/9/8 6:40:09 锦皓数字建站

做移动端图形渲染的早晚都要过纹理内存这一关。一张 2048×2048 的 RGBA32 贴图未压缩就是 16MB换到现在动辄几十张甚至上百张贴图的 PBR 工作流里显存和带宽分分钟被打满。ETC2、PVRTC 这些老牌压缩格式各有各的短板ETC2 的 RGB 压缩比尚可但 RGBA 和 HDR 的使用门槛偏高PVRTC 对纹理的长宽比、mipmap 生成方式都很挑剔处理不好就会出现严重渗色。ASTC 的出现算是把这个问题解决得比较彻底而 Arm-astc-engine——ARM 官方开源的那套编码器则是把 ASTC 压缩能力落到工程实践最直接的路径。这篇文章我会从源码架构、压缩内核审计、图形项目落地三个层面拆一遍适合正在做引擎贴图管线、资源打包工具、或者想在移动端压榨显存的朋友参考。1. 为什么吃透 astcencoder 对图形项目有实在收益很多团队把 ASTC 简单理解成“Unity 里勾一个选项就能用的纹理格式”这没错但等于没理解。真正吃透 Arm-astc-engine你在项目里能拿到的东西远比一个压缩按钮多压缩时间和画质的平衡点、不同平台上的兼容性边界、批量压缩工具的内存峰值、以及遇到异常纹理时怎么定位是格式问题还是压缩参数问题。1.1 ASTC 相比 ETC2/PVRTC 到底强在哪ASTCAdaptive Scalable Texture Compression最核心的卖点是“可缩放块尺寸”。ASTC 规定每个 block 固定占用 128 bit也就是 16 字节但 block 可以覆盖 4×4 到 12×12 的 texel 区域。也就是说你可以在“更高压缩比”和“更好画质”之间自由选择而不是被某个固定压缩比绑死。格式固定块尺寸最小压缩比是否支持 HDR是否支持 3D 纹理跨平台覆盖ETC24×44 bpp不支持不支持主流 Android / iOSPVRTC4×4 / 8×84 / 2 bpp部分不支持iOS 为主ASTC4×4~12×128~0.89 bpp支持支持ES 3.2 / Vulkan / Metal 3这个“可缩放”对项目管线的影响非常大。举个例子普通 UI 贴图和 UI 图集可以压到 8×8 甚至 10×10视觉上几乎看不出差异但内存直接砍十倍以上而角色身上的高光贴图、法线贴图需要保留更多细节就用 5×5 或 6×6法线贴图还可以搭配 ASTC 的 2-plane 模式来更好地保持方向和强度信息。另外 ASTC 还支持 3D 纹理体素渲染、体积雾、烘焙光照探针这类场景可以直接用 ASTC 压缩 3D 贴图这在以前几乎是不敢想的。PVRTC 和 ETC2 在这块完全是空白。1.2 Arm-astc-engine 在压缩链路上的角色Arm-astc-engine 是 ARM 维护的开源 ASTC 编码器仓库里核心是一个命令行工具astcenc同时也提供 C API。它在整个图形管线里扮演的是“离线资产管理”角色它在 CPU 上跑把高精度的源图PNG、TGA、HDR、EXR 等压缩成 ASTC 格式的二进制数据然后游戏引擎在运行时直接加载这些.astc数据流交给 GPU 解码。需要明确一点ASTC 的解码是 GPU 硬件实时完成的几乎所有支持 Vulkan 或 OpenGL ES 3.2 的移动 GPU 都内置了解码单元而“编码”这一侧到目前为止还没有硬件能在实时渲染里高质量完成。所以 Arm-astc-engine 的定位就是“生成 ASTC 资产”的离线工具。它不是为运行时设计的虽然 API 支持运行时压缩但没人会傻到在每一帧去跑。从工程角度看这套编码器解决了两个实际问题一是提供了一个跨平台可复现的压缩基准任何 CI 环境都能编译运行二是它的源码完全开源你能清楚地知道压缩过程中每个比特是怎么排布的这对做底层引擎、图形兼容性测试、以及自研压缩工具链的人都很有价值。2. 源码级架构全景一条压缩请求从入口到比特流的旅程2.1 仓库结构和模块划分Arm-astc-engine 的源码组织得非常工程化Source目录下每个文件基本对应一个独立职责模块。我按压缩链路上的依赖关系整理一份核心文件清单读源码时照着这个顺序读会顺畅很多文件主要职责astcenc_entry.cpp编码器上下文创建、配置校验、压缩/解压高层入口astcenccli.cpp命令行参数解析、文件输入输出、CLI 工具组装astcenc_image_load_store.cpp图像文件读取PNG/TGA/EXR、像素格式转换astcenc_block_sizes.cppblock 尺寸合法性和数据类型约束astcenc_partition_tables.cpp预生成的 1024 个 partition 模式查找表astcenc_compute_variance.cpp块内方差计算用于预判纹理复杂度astcenc_averages_and_directions.cpp颜色均值、主方向分析为分区提供依据astcenc_ideal_endpoints_and_weights.cpp理想颜色端点和 texel 权重求解astcenc_find_best_partitionings.cpp在候选 partition 中搜索最优结果astcenc_pick_best_endpoint_format.cpp从多种端点编码格式中选择最优astcenc_quantization.cpp端点颜色和权重量化astcenc_integer_sequence.cpp整数序列编码ISE实现astcenc_compress_symbolic.cpp符号化中间表示生成压缩主流程astcenc_symbolic_physical.cpp符号化块到 128 位物理比特流的转换astcenc_decompress_symbolic.cpp符号化解码用于压缩质量校验astcenc_mathlib.cpp数学库向量运算、PCA 等这里面最需要分清的一组概念是“符号化中间表示symbolic block”和“物理比特流physical block”。ASTC 编码不是一个一步到位的“像素向量量化”而是先把每个 block 转换成一个带有语义的结构体symbolic_compressed_block里面记录分区方式、平面数、权重、颜色端点等中间信息最后再由symbolic_to_physical把这些语义数据按规范打包成 128 位物理块。这种分层设计对源码审计来说非常友好你想分析“为什么这个块画质差”可以直接在 symbolic 层打印权重和端点分布而不用去逆向比特流。2.2 核心数据流Block 拆分、Partition 搜索、权重与端点一条完整的压缩请求在astcenc_compress_image里长这样先把输入图像按选定的 block 尺寸拆成若干 16 字节的目标块每个块覆盖一个 texel 区域。对每个 block读入 texel 数据计算块内颜色分布、方差、方向特征。根据特征尝试 1-plane 或 2-plane 编码并选择一个 partition 候选集。对每个候选 partition计算每个分区内的理想颜色端点和每个 texel 的权重。根据误差指标从候选 partition 中挑出最优结果。在最优结果上继续选择颜色端点格式、量化级别把符号化数据打包成物理块。用文字描述比较抽象可以看一下伪代码实际源码里的主控逻辑跟这个流程高度一致for each block in image: compute_alpha_usage(block) compute_block_variance(block) for each candidate_partition in candidate_set: compute_ideal_endpoints_and_weights(block, partition) evaluate_error(block, candidate_results) pick_best_partition(candidate_results) pick_best_endpoint_format(best_partition) quantize_endpoints(best_partition) quantize_weights(best_partition) symbolic_to_physical(best_symbolic_block, physical_block)这里有个很重要的设计决策值得注意compute_ideal_endpoints_and_weights并不是一次性求出全局最优解而是先把“端点”和“权重”拆开分别估算再交替优化。原因是如果同时优化端点和权重会变成一个高维非线性问题在 CPU 上跑离线压缩器都扛不住更别说批量打包上千张贴图。源码里采用的做法是先假设权重已知用最小二乘拟合端点再固定端点反推每个 texel 的最优权重这一步本身还是近似所以紧接着会用误差评估去验证结果是否足够好。2.3 质量档位对搜索深度的调控命令行里的-quality 0到-quality 100是大家接触最多的参数但很多人不知道它到底在调节什么。它不是简单“多跑几次迭代”而是从源头上控制了搜索分支数。源码里会把质量档位映射到一组“搜索配置”上包括每个 block 尝试的 partition 候选数量对每个 partition 是否执行多轮端点/权重交替优化是否启用 block 能量优先的 early-out是否尝试 2-plane 模式是否对 3D 纹理做额外方向搜索。低质量档比如-fastest的优化目标是“每秒钟压多少张图”所以会跳过大量候选只做一次粗略端点拟合。高质量档-quality 100则会把候选 partition 列表拉满每个分区都做多轮精修甚至还会尝试用不同量化级别反推重建误差取最优。我用一张 1024×1024 RGBA 图片简单跑过一组对比压缩到 ASTC 6×6quality压缩耗时相对视觉 PSNR 表现适用场景0fastest1×基准略低批量预览、打包时快速替代60medium4-6×接近最优常规项目默认90thorough10-15×肉眼无明显差异最终发布资源100exhaustive20-30×极限画质离线高精度资产真实项目里我建议最终发布资源至少跑-quality 90而中间迭代过程用-quality 50左右足够了没必要每次全量跑最高档。如果你想在画质和速度之间做更精细调整完全可以改源码里的搜索配置表这也是源码审计的硬收益之一。3. 源码审计压缩内核里不能错过的关键细节源码审计不是把每个文件读一遍就完事而是要抓住几个决定压缩质量的关键函数和数据表。下面这几个点是我认为最值得深入看的。3.1 从 astcenc_compress_symbolic.cpp 追踪编码主循环compress_symbolic是编码内核的入口。符号化中间块的数据结构symbolic_compressed_block里最有意思的字段是partition_count当前块分成几个区域plane_count1 还是 22-plane 意味着 RGB 和 A 分别用一套权重partitions每个 texel 属于哪个分区color_formats每个分区的颜色端点格式color_values量化前的端点颜色值weight_quant权重使用的量化级别texel_weights每个 texel 的权重索引。这些字段所对应的物理布局最终都会精确映射到 ASTC 规范的 128 位 block 里。审计时可以先修改compress_symbolic里的评估函数比如强制某个 block 只用 1-plane或者禁止某个分区数量再观察 PSNR 变化这样能快速理解每种编码手段对画质的影响。源码里编码主体是一个巨大的循环会遍历候选 partition、候选权重量化级别。真正让人冒汗的是这个循环里的“提前终止”逻辑很多分支不是每条都跑完的而是先用一个快速误差估计筛掉明显不行的候选只有误差接近当前最优的候选才会进入精修。理解这个逻辑后你再去看-quality不同档位的差异会清晰很多质量档位就是这些分支阈值的开关组。3.2 Partition 表、各向异性与启发式瘦身ASTC 规范里有一个包含 1024 个 partition 模式的固定表每个模式描述了一个 block 如何被切分成 2、3 或 4 个区域。这些模式还带有很多对称性比如水平翻转、垂直翻转、对角线翻转后的结果在表里会被标记为对当前搜索等价。astcenc_find_best_partitionings.cpp的核心思路就是不要真的去测试全部 1024 种 partition而是先用块内颜色方差和方向信息缩小到一个很小的候选子集。源码里预计算的perpendicular partition table能快速判断某个分区是否沿着纹理边缘分布。如果 block 内的颜色变化方向很强就优先考虑能顺着这个方向切分的 partition如果块内颜色接近均匀干脆用单个分区直接拟合。这个启发式瘦身是性能的关键。如果完全暴力尝试 1024 个分区再叠加 2-plane 和各种量化级别ASTC 编码时间会膨胀到不可接受。所以源码里所有“候选生成”逻辑都在平衡“搜索广度和单候选评估成本”。3.3 权重拟合与端点估算为什么这一步决定画质ASTC 的 block 里texel weight 负责表现纹理细节color endpoint 负责表现颜色基准。权重本质上是一个从量化查找表映射出来的标量或向量端点则是每个分区里的基础颜色。astcenc_ideal_endpoints_and_weights.cpp这一段在数学上最有含金量。它先通过 PCA 分析 block 内的颜色分布找出颜色变化的主方向然后据此决定权重是否应该用 2 组也就是 2-plane。对法线贴图这类方向性很强的纹理2-plane 编码能把法线方向误差控制得很好这是 ASTC 能逼平甚至超过 DXT5 法线图的关键原因。源码里交替优化端点和权重还有一层原因端点量化后会产生误差这个量化误差会进一步影响权重的拟合效果。所以源码在精修循环里会把“量化后的端点”当成已知量再重新计算一遍权重。也就是说你最终拿到的权重并不是针对原始完美端点优化的而是针对量化端点的最优权重。这层“量化感知”优化对最终画质提升相当明显也是 astcenc 能在同类编码器里保持很高压缩质量的原因之一。3.4 量化与整数序列编码16 字节块内部如何排布ASTC 和普通纹理压缩最大的不同在于它把端点颜色和权重都索引成一种“有界整数序列”然后通过astcenc_integer_sequence.cpp里的 ISE 编码把这些整数尽量紧凑地塞进比特流。要理解这段源码首先要掌握 ASTC 的量化等级概念。ASTC 的端点和权重支持多种量化位数比如权重可以从 4 个级别一路量化到 256 个级别。量化级别越多权重表达越细腻但占用的比特数也越多。编码器需要在“块尺寸对应的总比特预算”和“量化级别带来的质量提升”之间做权衡。源码里pick_best_endpoint_format做的就是这件事对不同候选格式计算总比特数剔除超预算选项再通过重建误差比较选最优。ISE 编码本身也很有意思它不是简单把整数变成二进制位。ASTC 的比特流采用的是“分组、交错、位移”的组合编码方式把 5 个整数编成 4 个 8 位字节、或者把 3 个整数编成 2 个 8 位字节以此减少浪费。审计时如果发现某些纹理压缩后出现奇怪的块状噪点可以重点看分析量化级别是否被压得过低以及 ISE 解码后取值是否落在预期范围内。4. 图形项目落地从源码构建到接入引擎4.1 跨平台构建与 SIMD 后端Arm-astc-engine 的构建逻辑非常轻量用 CMake 就行。常规 Release 构建命令git clone https://github.com/ARM-software/astc-encoder.git cd astc-encoder cmake -S . -B build -DCMAKE_BUILD_TYPERelease cmake --build build -j构建完会生成astcenc命令行可执行文件。默认情况下CMake 会根据当前 CPU 架构自动选择 SIMD 后端x86 平台走 SSE/AVX2ARM 平台走 NEON。源码里有一组ASTCENC_ISA_*编译选项可以用来强制指定cmake -S . -B build -DASTCENC_ISA_AVX2ON有一点要特别注意同一份 ASTC 二进制流在不同 GPU 上解码结果是一致的但编码器的 SIMD 后端只影响 CPU 端的压缩速度不影响输出格式。所以在 CI 机上用 AVX2 版本压缩出的.astc文件在 ARM 手机上完全兼容。如果你要把 ASTC 压缩集成到自己的资源打包工具链里我建议直接链接静态库而不是每次命令行起进程。命令行进程在大批量处理时有固定的进程创建开销而且不好做细粒度进度反馈。4.2 关键 API 调用示例用 C API 压缩一张图核心步骤是创建配置、创建压缩上下文、调用压缩函数。代码如下#include astcenc.h astcenc_config config; astcenc_config_init( ASTCENC_PRF_LDR_SRGB, // 目标颜色空间 6, 6, 1, // block 6x61 slice ASTCENC_PRE_MEDIUM, // 预设质量 ASTCENC_HD_NONE, // 不启用 HDR 0, // 不使用 3D 分块 config); astcenc_context* ctx; astcenc_context_alloc(config, 8, ctx); // 8 线程这里ASTCENC_PRF_LDR_SRGB和ASTCENC_PRF_LDR_LINEAR是关键区别。sRGB 预设会针对 Gamma 空间纹理做感知优化法线贴图、粗糙度贴图这类线性空间数据必须用ASTCENC_PRF_LDR_LINEAR否则会引入可见的压缩噪点。接着准备输入图像数据调用压缩接口astcenc_image image; image.dim_x width; image.dim_y height; image.dim_z 1; // 传入 RGBA8 或 RGBA16F 等像素数据 astcenc_compress_image(ctx, image, data, output_buffer, output_size, 0);压缩完后输出的字节流就是完整的 ASTC 纹理数据。注意这里输出是按 block 排布的不是你熟悉的“按行扫描”的像素排布。加载进渲染 API 时需要把整个数据直接作为纹理源传输给 GPU由 GPU 按 ASTC 格式解码。4.3 参数选型离线、近实时、实时三档配置我整理了一套项目里可复用的配置参考使用场景block 尺寸quality颜色空间建议UI 图集8×8 或 10×1090LDR_SRGB大平色区域压缩比可以拉高场景漫反射6×6 或 8×890LDR_SRGB体积/内存优先时 8×8角色贴图5×5 或 6×6100LDR_SRGB角色离镜头近画质优先法线贴图5×5 或 6×6100LDR_LINEAR必须用 linear且开启 2-planeHDR 环境贴图6×690HDR_LINEAR需要支持 HDR 的硬件运行时自动降级6×660按纹理类型仅用于预览和占位最容易被忽略的是非法线贴图也必须用LDR_LINEAR的场景。像遮罩贴图、金属度贴图、粗糙度贴图虽然人眼看着是灰度图但在着色器里是当成线性数据参与计算的压缩时如果走了 sRGB gamma 优化会在中间调上出现肉眼难以发现但着色器能感知的偏差。4.4 GPU 侧加载 ASTC图形 API 的接入要点用 Vulkan 或 OpenGL ES 加载 ASTC 纹理时需要把格式枚举指定为 ASTC 对应块尺寸的格式。例如 6×6 的 LDR 纹理在 Vulkan 里是VK_FORMAT_ASTC_6x6_UNORM_BLOCK或VK_FORMAT_ASTC_6x6_SRGB_BLOCK。加载时要特别注意纹理宽高不一定是 block 尺寸的整数倍GPU 驱动通常能处理非对齐尺寸但不保证所有设备行为一致。使用 mipmap 时各级 mip 的压缩块边界统一按原图尺寸计算不要单独对每一级做 padding。大多数移动 GPU 对 ASTC 解码是硬件加速的但部分桌面平台在 Vulkan 下需要验证是否支持对应的块尺寸。Metal 3 在 Apple Silicon 上支持完整 ASTC而旧款 macOS/Intel 设备可能只支持部分配置需要判断兜底方案。5. 落地过程中绕不开的坑5.1 纹理尺寸与 Seam 问题ASTC 的解码是按 block 独立进行的不会像 PVRTC 那样需要读取相邻 block 做插值所以常规的接缝问题比 PVRTC 轻得多。但 block 边缘仍然会引入一定程度的独立压缩误差尤其是当 UV 寻址模式是 Repeat 时相邻 block 的误差不连续可能会在法线贴图或高度图上产生可见的“网格状”条纹。排查时先在引擎里把纹理转成调试模式单独显示压缩后的色块基本一眼就能看出来是压缩原因还是 UV 原因。解决方向一般是提高 block 密度从 8×8 降到 6×6或者对源图在压缩前做一次边缘羽化。5.2 线性空间与 sRGB 不能混这个问题在项目里踩到过不止一次。很多美术出图时给的是一张“看起来正常”的 sRGB 贴图程序侧顺手就用SRGB预设压缩了。但如果这张贴图在 shader 里被当线性数据采样比如用于 roughness/metallic压缩器对中间调的优化方向和最终使用方式对不上出来的效果就会奇怪。统一规则是先确认贴图在渲染管线里最终以什么颜色空间被采样再决定压缩预设。凡是 shader 里直接参与光照计算的数据基本都可以视为线性数据只有最终用于呈现给眼睛的非数据贴图才用 sRGB 预设。这个规则比“按文件后缀判断”可靠得多。5.3 多线程与内存峰值astcenc 的 API 支持设置线程数命令行工具内部也默认启用了多线程。实际压大图时尤其是 4K 分辨率和-quality 100组合单线程可能要吃满好几个核心几十秒。批量压缩管线里一定要控制并发任务数不要简单按 CPU 核心数全开因为每个压缩线程内部还会分配临时缓冲区叠加起来内存峰值很高。我遇到过的一个典型案例是批量脚本同时跑 16 个 astcenc 进程结果 32GB 内存的机器直接被吃满压缩中途大量 swap反而比 8 进程还慢。合理做法是按“核心数 / 2”或者“内存总量 / 单进程峰值”来并发。这个单进程峰值可以在 CI 机器上跑一张最大分辨率贴图实测获得。5.4 解码端兼容性边界ASTC 在移动端的覆盖已经非常广但仍有一些老设备只支持部分 ASTC 格式或者不支持 HDR ASTC。做多平台发布时建议在资源构建期就生成好兼容性元数据而不是运行时再判断。比如一个设备不支持 10×10 的 HDR 块而你的压缩资源用了 8×8 HDR那就需要准备一套 LDR 回退资源。另外有一点经常被忽略ASTC 支持 3D 纹理但部分桌面 GPU 驱动对 3D ASTC 的支持并不像 2D 那么完善。做体积雾、流体仿真可视化这类 3D 纹理压缩时一定要在目标硬件上做真机验证不能只看规范文档。最后分享一个我自己的实操习惯每次调整压缩参数前会在项目里挑 10~20 张有代表性的贴图分别跑-quality 50和-quality 100用 PSNR 和 SSIM 两个指标批量统计差异。如果两条曲线几乎重合说明这套内容用高两档的收益不大直接把项目默认质量调低能省一大笔构建时间如果某些贴图差异明显再单独给它们建一个资源组统一走高质量档。压缩参数不是玄学而是可以在数据驱动下一点一点收敛的工程决策。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。