ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ASTC纹理压缩与Arm-astc-encoder源码深度解析

ASTC纹理压缩与Arm-astc-encoder源码深度解析 1. 为什么是ASTC移动端纹理压缩的现状与痛点1.1 纹理压缩为什么这么重要做移动端图形开发的朋友应该都有体会纹理带宽和存储开销往往是性能瓶颈里最容易被忽视的一环。一块2048x2048的RGBA8纹理裸数据就有16MB在移动GPU上这种规格的纹理一旦采样频繁带宽压力立刻就会反映到发热、耗电和帧率抖动上。我见过不少项目Shader写得再优化模型面数控制得再好结果纹理放了几张大的帧率照样被拉垮。纹理压缩就是为了解决这个问题存在的把显存占用压下去、带宽占用降下来同时尽量保住画面质量。目前市面上的压缩方案各有一批拥护者。ETC2是OpenGL ES 3.0之后的强制标准所有移动设备都支持但它的压缩比和质量上限其实一般。PVRTC是PowerVR GPU的老牌方案质量不错但跨平台支持较差。DXT/BC系列在PC和主机上很好用可惜移动端GPU对它的支持参差不齐。而ASTCAdaptive Scalable Texture Compression是ARM在2012年左右推出的方案后来被纳入OpenGL ES 3.2和Vulkan标准最大特点就是“自适应可扩展”从最小4x4像素块到最大12x12像素块支持数十种压缩码率还原生支持HDR和3D纹理。可以这么说ASTC是目前移动端兼容性最好、灵活度最高的纹理压缩格式没有之一。1.2 Arm-astc-encoder到底解决了什么问题Arm-astc-encoder是ARM官方开源的ASTC编码器实现仓库地址在GitHub上采用Apache License 2.0协议可以自由用在商业项目里。它负责把普通的RGBA/RGB图像转成ASTC格式的压缩数据同时提供解码功能方便回读验证。这里有一个很多新手会搞混的点GPU硬件对ASTC是“只解不编”的也就是说GPU负责在采样时实时解压ASTC数据但把普通纹理转成ASTC格式的编码工作得在离线或运行时由CPU完成。Arm-astc-encoder就是这个编码环节的官方参考实现也是目前质量和性能最均衡的ASTC编码器之一。它支持的码率从8Kbps到接近无损的2.4bppbit per pixel都有对应档位具体取决于你选的块尺寸。读这套源码的意义不光是“把纹理转个格式”这么简单。你的项目里如果要做PBR材质压缩、法线贴图压缩、HDR纹理处理甚至想在引擎里集成自定义纹理导入管线那了解ASTC编码器的内部工作原理、数据布局和哪些参数对质量影响最大是绕不开的一步。这也是我写这篇评测的初衷从架构到源码从算法到落地把它彻底拆开讲清楚。2. 源码架构全景如何快速读懂这套C代码库2.1 顶层模块划分与目录结构先说明一点本文的源码版本是当前主线代码基于GitHub上比较新的提交具体行号可能会随版本变动但核心架构在这个项目里已经稳定了很多年不会有大变化。克隆仓库后目录结构大概是这样arm-astc-encoder/ ├── CMakeLists.txt # CMake构建入口 ├── Source/ │ ├── astcenc_averages_and_directions.cpp │ ├── astcenc_compress_symbolic.cpp │ ├── astcenc_compute_variance.cpp │ ├── astcenc_decompress_symbolic.cpp │ ├── astcenc_diagnostic_trace.cpp │ ├── astcenc_entry.cpp # 公开API入口最核心文件 │ ├── astcenc_find_best_partitionings.cpp │ ├── astcenc_find_candidate_partitions.cpp │ ├── astcenc_internal.h # 内部数据结构定义 │ ├── astcenc_ideal_endpoints_and_weights.cpp │ ├── astcenc_image.cpp # 图像加载、缩放、裁剪、边框处理 │ ├── astcenc_integer_sequence.cpp │ ├── astcenc_mathlib.cpp │ ├── astcenc_partition_tables.cpp │ ├── astcenc_percentile_tables.cpp │ ├── astcenc_pick_best_endpoint_format.cpp │ ├── astcenc_platform.h # 平台抽象层 │ ├── astcenc_quantization.cpp │ ├── astcenc_symbolic_physical.cpp │ └── astcenc_vecmathlib.h # SIMD向量数学库 └── Utils/ └── (命令行工具astcenc-avx2 / astcenc-neon等)如果你之前读过类似x264、LZ4这类C编解码器看这套代码会有一种“既熟悉又陌生”的感觉。熟悉是因为它的模块切分方式很规整编码流程的各阶段基本是流水线式的函数调用陌生则是因为纹理压缩的算法抽象和视频压缩差异非常大里面充斥着分块block、分区partition、端点endpoint、权重weight这些概念初次接触需要一点时间消化。2.2 三层分层API层、核心编码层、工具层我个人习惯把整个代码库按逻辑分成三层来看这样理解起来更清晰。API层对应astcenc_entry.cpp它暴露了完整的C接口包括ctx创建、配置初始化、压缩单张图像、解压回读、销毁上下文等。这层的函数命名都很规整比如astcenc_compress_image、astcenc_decompress_image只要你不是要在引擎里做底层魔改用这层API就够了不需要碰内部实现。核心编码层就是Source目录下那一堆astcenc_*.cpp文件。每个文件聚焦一个职责比如分区选择partitioning、端点优化endpoint、权重优化weight、量化quantization、HDR处理在epsilon和ldr/hdr相关逻辑里。这种按“编码器内部阶段”来切分的做法模块复用性很好也方便做局部性能优化毕竟SIMD优化集中在vecmathlib里算法逻辑和向量化逻辑是分开的。工具层是Utils目录下的命令行程序支持类似astcenc-avx2 -cl source.png encoded.astc 6x6 -medium这样的调用方式。工具层其实就是在API层之上包了一层文件解析、参数解析和进度打印生产环境里你一般不会直接用命令行而是把API层集成到引擎的资源导入工具里。理解这个分层之后你再去看源码就不会迷路想搞API怎么调用去astcenc_entry.cpp想搞清楚颜色端点是怎么算出来的去ideal_endpoints_and_weights.cpp想优化编码速度去研究SIMD向量化里的热点函数。3. 核心数据结构与编码原理它内部到底怎么工作的3.1 block、texel、partition理解ASTC的基础概念ASTC压缩的本质是“分块有损压缩”。它会先把图像切成NxN像素的小块block然后对每个块做独立的编码和量化。这里的N就是压缩比参数常见的是4x4最高质量1bpp对应的其实是8x8这种4x4约等于8bpp到12x12最大压缩约0.89bpp。块尺寸的选择直接影响画质和体积这是ASTC相对其他格式最灵活的维度。每个block在编码时会选择一种“分区模式”partition count把块内的像素划分成1个、2个、3个或最多4个区域。每个区域内的像素共享一组颜色端点endpoint和权重weight。举个例子如果block内有明显的边界比如高光边缘用2个或3个分区会比1个分区效果好得多但分区本身也要消耗bit存储所以编码器需要在“分区带来的质量增益”和“分区消耗的bit开销”之间做权衡。这个决策过程在源码里主要是find_best_partitionings和find_candidate_partitions这两个阶段完成的。ASTC标准定义了约2000多种分区模式Arm-astc-encoder不会全部穷举而是通过预计算表、方差分析和多种启发式策略快速筛选出少量“有希望”的候选分区再做精细评估。这也是它速度和质量的平衡点所在。3.2 编码流水线从像素输入到ASTC bit输出光讲概念可能还不够直观我直接把Arm-astc-encoder的编码主流程拆成下面这张流水线来看你就知道它内部要经过多少道工序了图像预处理把输入图像转成线性空间根据压缩选项决定是否丢弃alpha、是否做sRGB转换、是否生成mipmap工具层会处理mipmapAPI层需要调用方传入mip级别。块划分遍历图像的所有block对每个block提取像素数据。方差/主方向分析计算每个block的颜色方差、亮度方差、主色调方向等统计量为后续的分区和权重计算提供依据。候选分区搜索基于统计量在预计算表中查找若干候选分区模式。理想端点计算对于每个候选分区计算出区域内的理想颜色端点理想情况下能完全还原该区域颜色的端点这是一个最小二乘法拟合的过程。权重计算根据端点和像素的映射关系求解每个texel的权重值。ASTC的编码本质上就是“端点权重插值”这三种信息的组合权重就是这个插值过程的关键系数。量化编码把端点、权重、分区索引等信息用不同的bit位数量化然后打包成整数序列。质量评估和选择把所有候选中质量最好、误差最小的方案挑出来写入最终的bit流。这中间最核心的数学问题是给定一组端点和权重如何让重构图像与原图的误差最小。Arm-astc-encoder在这里用的是“理想端点和权重交替优化”的策略代码里对应astcenc_ideal_endpoints_and_weights.cpp中一组复杂的迭代函数。3.3 一个重要的内部结构symbolic_compressed_block看源码时你会发现一个出现频率极高的结构体——symbolic_compressed_block。这是编码过程中的“中间表示”它保存的是尚未做最终bit打包的“符号化”压缩结果。什么意思呢就是压缩信息在里面对人类来说还是可读的color_endpoints是浮点值weights是浮点数组partition_count是整数一切都还没经过定点化量化。之所以设计这个中间层是因为编码算法需要反复迭代、评估多种候选方案。如果每次都直接生成最终bit流再做误差计算代价太高。用symbolic_compressed_block先表示“理想化的结果”把它传进误差评估函数算完再决定接下来往哪个方向优化最后才调用compress_symbolic_block把它转成真正的物理bit流这样流程上干净利落。与之对应的物理表示是physical_compressed_block在astcenc_symbolic_physical.cpp中实现两者互转。你在做任何涉及ASTC数据解读的二次开发时大概率都会用到这两组结构体和它们的转换函数。4. 编码质量与速度的取舍源码里值得学的一组平衡术4.1 编码质量档位exhaustive / thorough / medium / fastArm-astc-encoder暴露了四个质量档位从低到高分别是fast、medium、thorough、exhaustive。不同档位影响的是搜索深度和候选数量而不会改变ASTC格式本身的编码结构。用一个简单的比喻来说同一个数学题fast档位让你用排除法快速猜答案exhaustive档位则是把所有可能的解都验证一遍再选最优当然质量更好耗时也明显更长。从源码角度看质量档位主要影响这几处候选分区的数量fast档可能只测试10~20种分区模式thorough会测试几百种exhaustive则逼近全量枚举。端点格式搜索深度ASTC端点有多种量化bit配置搜索越深越能找到更优配置。权重优化迭代次数weight优化可以多轮迭代逼近最优低质量档位会提前终止迭代。是否启用部分更精细的估计器有些误差评估函数在高档位才会被调用。我自己实测下来medium到thorough的视觉差距在大多数法线贴图场景下不明显但耗时差距可能达到2~3倍。如果项目里纹理数量巨大建议先用medium做批量压缩只在个别重要纹理比如角色脸、UI大图上用thorough甚至exhaustive单独压一版。4.2 显著性感知为什么人眼看不出某些瑕疵ASTC编码器内部其实做了一些符合“人眼感知”的优化而不是纯粹从像素误差出发。比如法线贴图的通道分布规律法线贴图的R、G通道往往是小范围变化的高频信息B通道接近常数1。如果按普通RGBA的误差权重来优化B通道可能被分配过多bit造成浪费而R/G通道压缩不足出现明显接缝。Arm-astc-encoder在计算误差时可以针对特定的通道权重和“显著性感知”做调整。源码里和这个直接相关的文件是astcenc_percentile_tables.cpp它保存了从大量图像统计出的、各通道各模式的显著性百分位表。这个表的作用是在编码决策时对当前block的颜色分布和已知显著性规律做匹配估算“哪些信息丢了人眼会更敏感”。实际效果就是编码器会倾向于把bit分配给亮度和对比度更敏感的区域。这一点对游戏项目尤其重要。你压一张松林纹理可能视觉上很好接受但压一张带文字标题的UI贴图文字边缘稍微糊一点就很明显。利用通道权重和感知优化参数比如-cw命令选项调整各通道权重可以在UI贴图上显著改善文字可读性。4.3 分块自适应的双刃剑ASTC的分块尺寸有一个“全局性”你指定了块尺寸比如6x6整张图的所有block都用这个尺寸。这和BPTC等格式类似并没有跨block的动态码率分配机制。换句话说你不能在同一张纹理里一部分用4x4、一部分用8x8——这是格式本身的限制。这个限制带来的实际影响是如果一张纹理里有细节密集的区域草地、网格又有平坦的区域天空、墙面你必须按最坏情况来选择块尺寸否则细节区域会严重失真。也因此很多项目团队会倾向于把纹理按内容拆分细节多的用高码率细节少的用低码率而不是一张图一刀切。我见过一个大型开放世界项目就是这么干的地表材质直接拆成多张分层贴图每张单独选码率最终包体瘦身效果非常明显而且画质几乎无损。这正是ASTC“可自适应”的正确打开方式——不是指望编码器在一张图内部自适应而是在项目资源管理层面做自适应。5. 关键代码实现细读从入口函数到编解码全流程5.1 入口与上下文astcenc_ctx的构建和管理Arm-astc-encoder的API设计遵循“创建上下文、复用上下文、销毁上下文”的模式。这和很多编解码库是一致的目的是避免重复分配工作内存。看一下核心入口部分astcenc_ctx* ctx nullptr; astcenc_config config; astcenc_config_init(ASTCENC_PRF_LDR_SRGB, 6, 6, 1.0f, ASTCENC_PRE_MEDIUM, ASTCENC_FLG_DECOMPRESS_ONLY, config); astcenc_context_alloc(config, 1, ctx);astcenc_config_init前几个参数可以简单理解为第一个参数是色彩空间配置ASTCENC_PRF_LDR_SRGB表示输入是sRGB空间的非HDR图像还有ASTCENC_PRF_LDR线性空间和ASTCENC_PRF_HDR_RGB、ASTCENC_PRF_HDR_RGBA等选项。第二、三个参数是块宽高这里是6x6。第四个参数是质量参数1.0表示默认水平。第五个参数是编码质量预设medium。第六个参数是flagASTCENC_FLG_DECOMPRESS_ONLY表示这个ctx只做解压不做压缩会省掉很多内部缓冲的分配。在使用压缩功能时要传入images结构体包含图像宽高、数据和数据格式。然后用astcenc_compress_image执行压缩得到编码后的buffer再用astcenc_compress_reset重置上下文状态方便下一轮压缩。这里有个细节astcenc_compress_image不是线程安全的同一个ctx不能并发调用要么每个线程单独创建ctx要么加锁串行。多线程压缩的推荐做法是每个worker线程各自分配一个ctx因为ctx内存占用并不夸张但省去了互斥等待。5.2 压缩主流程compress_image内部做了什么我们直接看astcenc_compress_image的简化伪码不用太长但能看出阶段划分astcenc_error astcenc_compress_image( astcenc_ctx* ctx, astcenc_image* image, const uint8_t* compress_buffer, size_t compress_buffer_size, uint32_t flags) { // 1. 校验参数检查宽高、格式、buffer大小等 // 2. 初始化内部的任务队列和区块划分 // 3. 对每行每列的block依次调用编码函数 for (int block_y 0; block_y block_count_y; block_y) { for (int block_x 0; block_x block_count_x; block_x) { compress_block(ctx, image, block_x, block_y, ...); } } // 4. 返回压缩后的数据写入compress_buffer }在启用多线程的情况下ctx在创建时的thread_count参数1它会把block遍历拆成多个任务通过一个轻量的线程池并行处理。每个block的编码相互独立天然适合并行。不过实际压缩时内存带宽可能比CPU计算更容易成为瓶颈线程数建议设置为物理核心数而不是逻辑核心数效果更好。5.3 解压主流程astcenc_decompress_image的对称性解压比压缩简单很多因为ASTC解码不需要做复杂的搜索只需要按照bit流规则还原每个block的端点、权重、分区然后做插值和反量化。核心流程对应的是astcenc_decompress_symbolic.cpp里的decompress_symbolic_block函数。值得留意的是ASTC解压性能也是需要关注的。游戏运行时如果用了ASTC纹理GPU硬解压不消耗CPU但在编辑器预览、资源导入流程里你会频繁调用软解压来生成缩略图或做质量对比。Arm-astc-encoder的解压速度大约是压缩的数十倍到上百倍这个差异很正常因为压缩是“搜索最优解”解压只是“按规则还原”。5.4 工具类实现图片加载与边框处理astcenc_image.cpp里除了图片格式转换还有一个不容忽视的逻辑纹理边缘处理edge handling。ASTC编码时如果图像尺寸不是块尺寸的整数倍最边缘的block会覆盖到图像边界之外。这个位置的数据怎么填充Arm-astc-encoder支持wrap和clamp两种方式wrap模式在重复纹理tileable texture中很有用边缘会取另一侧的像素来填充能避免重复拼接时出现接缝。clamp模式则简单地复制边缘像素适合普通非重复纹理。编程时你需要确保传给编码器的图像本来就是tileable或已经处理好了边缘否则编码器内部的边缘填充策略可能和引擎里的纹理寻址模式不匹配最终导致纹理边缘出现颜色异常。这种bug调试起来很恶心因为不是每次都肉眼可辨需要拉大对比才能看出来。6. 编译、集成与工程化落地6.1 不同平台的编译方式Arm-astc-encoder原生支持CMake编译比较省心。在Windows上推荐直接用Visual Studio的CMake插件git clone https://github.com/ARM-software/astc-encoder cd astc-encoder mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease cmake --build . --config Release在Linux服务器上确保有g和cmake即可sudo apt-get install build-essential cmake cmake .. -DCMAKE_BUILD_TYPERelease make -j$(nproc)它会在源码目录产生astcenc-avx2、astcenc-sse4.1、astcenc-neon之类的可执行文件前缀对应的是编译时启用的SIMD指令集。如果是给其他程序链接使用可以编译成静态库然后头文件只需包含astcenc.h这个公开头文件#include astcenc.h在移动端Android NDK / iOS上编译也不复杂armv7/arm64平台会自动启用NEON优化。这里有个工程建议把编解码器作为一个独立的native模块编译通过JNI或FFI暴露给上层引擎调用比在引擎主进程里直接用源码编译更利于维护。6.2 SIMD优化为什么ASTC编码器速度差距能差好几倍Arm-astc-encoder对SIMD的重度依赖是它性能和普通实现拉开差距的根本原因。它的vecmathlib层封装了SSE2、SSE4.1、AVX2和NEON等不同指令集内部大量的数学计算比如颜色拟合、点积、方差计算、量化等全部是向量化处理的。同一套代码AVX2版本的编码速度通常比标量版本快2~4倍甚至更多。这也是为什么官方发布的预编译二进制会有不同后缀的原因。你用astcenc-avx2和astcenc-sse2压同一张图耗时差异非常明显。在集成时建议在运行时做CPU指令集检测动态选择最合适的二进制库。如果有条件直接编译成AVX2版本但对一些老旧服务器和开发机的兼容性要多测一轮。6.3 一个完整的最小集成示例这里我用C代码展示一个最简集成压缩与解压到内存Buffer方便你做实验#include cstdint #include cstdio #include vector #include astcenc.h int main() { const int width 512; const int height 512; const int channels 4; std::vectoruint8_t src(width * height * channels, 128); // 生成一张带渐变和噪点的测试图 for (int y 0; y height; y) for (int x 0; x width; x) { int i (y * width x) * channels; src[i] (x * 255) / width; src[i 1] (y * 255) / height; src[i 2] (x ^ y) % 255; src[i 3] 255; } // 1. 初始化上下文 astcenc_config config; astcenc_config_init(ASTCENC_PRF_LDR_SRGB, 6, 6, 1.0f, ASTCENC_PRE_MEDIUM, ASTCENC_FLG_NONE, config); astcenc_ctx* ctx; astcenc_context_alloc(config, 1, ctx); // 单线程 // 2. 组装输入图像结构 astcenc_image image; image.dim_x width; image.dim_y height; image.dim_z 1; image.data_type ASTCENC_TYPE_U8; image.data new uint8_t*[height]; // 这里是一个指针数组指向每行数据 for (int y 0; y height; y) image.data[y] src.data() y * width * channels; // 3. 计算压缩后大小并执行压缩 size_t comp_size astcenc_get_compressed_size(image, 6, 6); std::vectoruint8_t compressed(comp_size); astcenc_error err astcenc_compress_image(ctx, image, compressed.data(), compressed.size(), 0); if (err ! ASTCENC_SUCCESS) { printf(compress failed: %s\n, astcenc_get_error_string(err)); return 1; } astcenc_compress_reset(ctx); // 4. 解压回RGBA std::vectoruint8_t decoded(width * height * channels); astcenc_image d_image image; d_image.data new uint8_t*[height]; for (int y 0; y height; y) d_image.data[y] decoded.data() y * width * channels; err astcenc_decompress_image(ctx, compressed.data(), compressed.size(), d_image, 0, 0); if (err ! ASTCENC_SUCCESS) { printf(decompress failed: %s\n, astcenc_get_error_string(err)); return 1; } // 5. 清理 astcenc_context_free(ctx); delete[] image.data; delete[] d_image.data; printf(OK: compressed %zu - %zu bytes\n, src.size(), comp_size); return 0; }这段代码踩了两个坑我都标出来一是image.data是uint8_t**每行一个指针不能直接把数据展平的指针塞进去二是astcenc_compress_reset(ctx)必须在下一张图压缩前调用它会重置内部统计和任务状态。编译时链接静态库或动态库都行记得加编译宏ASTCENC_SSE或ASTCENC_NEONCMake会自动配好。如果你的项目启用了LTO建议在Release模式下做一次完整回归SIMD代码在LTO下有极小概率触发编译器优化bug。7. 图形项目落地实操从命令行到引擎管线的完整路径7.1 命令行批量处理纹理的正确姿势如果不打算改引擎直接用命令行走一遍批量压缩是最快的验证方式。典型命令是这样的astcenc-avx2 -cl hero_character.png hero_character.astc 8x8 -medium -thorough解释一下参数-clcompress LDR适合普通sRGB贴图。如果是线性空间贴图用-cl时还要注意色彩空间选择建议-cs参数显式指定线性。8x8块尺寸8x8大约是2bpp的码率对于大多数场景贴图是画质体积比不错的选择。如果你追求高质量4x48bpp或5x55.12bpp更合适。-medium质量档位。命令行还支持-fast、-thorough、-exhaustive。注意-thorough和-exhaustive同时存在时后者优先级更高但命令行里一般只用其中一个避免混淆。在批处理脚本里利用find配合循环可以快速压完整个目录find textures/ -name *.png | while read f; do base${f%.png} astcenc-avx2 -cs $f $base.astc 6x6 -medium done这里用了-cs而不是-cl是为了强制按线性空间处理避免PNG里嵌入的色彩配置导致压缩时颜色串空间。对于sRGB图一些版本里-cl会自动处理sRGB曲线-cs则是显式线性。如果搞错了压完的图在引擎里颜色可能会偏淡或偏灰这个坑我踩过不止一次。7.2 和Unity、Unreal引擎的集成经验Unity对ASTC的支持比较成熟只要在Player Settings里把Texture Compression设置为ASTC并选择合适的块尺寸即可。但要注意Unity有自己的一套压缩逻辑未必会调用Arm-astc-encoder的API所以如果你想用特定参数比如自定义质量档位、HDR ASTC需要自己写一个Texture Postprocessor脚本或者用AssetPostprocessor接入。Unreal Engine 4/5同样支持ASTC在纹理导入设置的压缩设置里选ASTC即可。但UE里有一部分纹理比如UI图、法线贴图默认的压缩设置可能和ASTC不完全适配。比如法线贴图压缩建议关闭“Use Alpha as Mask”之类的选项并设置合适的压缩质量否则可能的γ处理和符号问题会导致压缩后法线方向出现噪点。另外UE的ASTC压缩在部分平台版本上走的是自己的实现如果你发现UE压出来的ASTC纹理有肉眼可见的瑕疵可以通过构建脚本调用Arm-astc-encoder重新压一版再把生成好的.astc作为预压缩纹理导入。7.3 运行时加载ASTCOpenGL/Vulkan的纹理上传路径打开上传路径之前先确认你的渲染API支持ASTC格式。OpenGL ES 3.1以上和Vulkan都支持GL_COMPRESSED_RGBA_ASTC_4x4_KHR等扩展Desktop OpenGL需要4.3以上或相应扩展。桌面平台DX12不支持ASTC一般也不推荐在桌面上用ASTC。正确的加载流程是读出.astc文件头部信息有几个字节的magic和header记录了块宽、块高、图像尺寸等。把header之后的数据作为压缩payload传给GPU。获取块尺寸计算mip级别需要的数据偏移。对每一层mip调用glCompressedTexImage2D。在OpenGL ES上的代码大概是int bw, bh, block_x, block_y; // 解析header得到block width/height 6/6然后计算每个block占16字节 // 压缩数据大小 块数 * 16字节 glGenTextures(1, tex); glBindTexture(GL_TEXTURE_2D, tex); glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_ASTC_6x6_KHR, width, height, 0, data_size, data_ptr);这里有个经常出错的点glCompressedTexImage2D的data_size必须严格等于实际压缩数据大小不能用宽高去猜因为ASTC最后一行block可能会被裁剪块数并不总是整行。稳妥做法是让编码器帮你算出精确大小就是前面示例里的astcenc_get_compressed_size。我在一个项目里就是没注意这个最后一行纹理加载后整块出现花屏排查了大半天。7.4 码率选型给不同纹理类型一个参考基准不同用途的纹理适合的ASTC块尺寸差别很大。我根据项目经验总结了一张表可以作为初始参考纹理类型建议块尺寸约等效码率说明UI大图/图标4x48bppUI模糊非常显眼宁可体积大一些角色Diffuse5x5 / 6x65.12 / 3.56bpp视觉重点需要比较高的保真度场景Diffuse6x6 / 8x83.56 / 2bpp在移动端性价比很高法线贴图5x55.12bpp法线方向错误比颜色错误更难容忍HDR环境贴图6x63.56bpp视反射质量需求可降到8x8地形/噪声图8x8/10x102 / 1.28bpp噪声图对细节要求不高这个表不是绝对的。如果你压完看质量有问题优先微调块尺寸而不是无脑拉高全局质量档位因为块尺寸的影响是结构性的质量档位更多影响搜索深度。8. 常见问题排查与避坑实录8.1 压缩后出现接缝或边缘发暗这个问题根源通常是边缘处理模式和引擎寻址模式不匹配。比如你引擎里纹理Wrap Mode设置的是Repeat但压缩时用了Clamp边缘填充那么纹理在重复采样时边缘那一圈像素可能和相邻tile不连续。解决办法是对可平铺纹理压缩时明确指定wrap模式Arm-astc-encoder命令行里有-esw之类的边缘处理相关参数源码里对应的是ASTCENC_FLG_USE_WRAP_EDGE等flag并在引擎里把纹理寻址也设置为Repeat。两边保持一致接缝就不会出现。如果已经压坏了那就得重新压这个没法在运行时补救。8.2 法线贴图压缩后光影闪烁法线贴图压缩本身是有损的如果块尺寸太大比如8x8法线精度不够会在光照计算中产生高频闪烁。更隐蔽的问题是在压缩前法线贴图必须确保切线空间的符号信息被正确编码。有些DCC工具导出的法线贴图是OpenGL风格绿色朝上有些是DirectX风格绿色朝下压缩不会改变方向但如果你把法线图做了sRGB转换再压会导致颜色空间混乱法线还原后方向就不对了。排查方法在CPU上把ASTC解压回RGBA和原始法线贴图做逐像素对比生成误差热力图。如果误差集中在绿色通道且方向性明显大概率是颜色空间问题如果误差均匀分布在R/G/B通道说明块尺寸太大或质量档位太低建议换小号块尺寸或提高档位。8.3 HDR纹理压缩后的色阶断层ASTC是支持HDR编码的但HDR和LDR的压缩质量表现差异很大。如果你用LDR模式去压HDR纹理浮点值的尾数会被截断轻则色阶断层重则高光区域完全失真。解决办法是使用对应的HDR配置astcenc_config_init(ASTCENC_PRF_HDR_RGBA, 6, 6, 1.0f, ASTCENC_PRE_MEDIUM, 0, config);另外HDR纹理压缩后建议做一轮颜色校准因为不同GPU在HDR纹理采样时使用的传递函数可能有差异特别是半浮点纹理和RGBM编码混用的时候。Arm-astc-encoder的HDR输出是基于RGBM编码风格的你拿到引擎里要注意和渲染管线的Tonemapping顺序对齐否则最终颜色会偏。8.4 压缩耗时过长如果你压一百张图压了一整夜还没完最常见的两个原因第一个是你错误地跑在exhaustive档位上第二个是线程数没设置。exhaustive比medium慢十倍以上但它带来的质量提升往往肉眼不可见生产环境除非是封面图或者特别重要的UI否则别用。多线程配置在命令行中可以用-j 8指定线程数在API里是astcenc_context_alloc的thread_count参数。但实际压图时如果机器同时还在跑编译任务过高的线程数反而会因为超线程争抢导致总吞吐下降。我一般在8核机器上设6个线程留2个给系统保底。8.5 ASTC纹理在部分GPU上出现花屏ASTC的支持层级因GPU架构不同有差异。比如部分早期GPU只支持2D ASTC不支持3D纹理有些GPU只支持LDR不支持HDR。处理办法是写一个运行时能力检测模块在游戏启动时查询GL_MAX_TEXTURE_SIZE以及ASTC扩展列表对不支持的设备自动降级到ETC2或RGBA8888。如果花屏出现在Mipmap上还要检查是不是用错误尺寸生成了mip。ASTC的block在mip层级的尺寸必须和主纹理一致也就是说块尺寸不会随着mip级别缩小而缩细这让mip的正确生成比RGBA纹理更严格。有人在生成mip时直接对ASTC纹理做邻近采样导致mip层级上的block错位就出现花屏了。正确做法是先解压成RGBA生成mip再对每层重新压缩。9. 从源码中还能学到什么同类项目横向对比与思考9.1 和ETC2、PVRTC、Basis Universal的对比看完Arm-astc-encoder源码你再看别的压缩方案会有一种“看山不是山”的感觉。ETC2的编码器逻辑简单得多它本质上是固定块格式加少量模式选择所以编码速度快、实现简单但压缩率上限低。PVRTC因为依赖全局的低频调制函数编码时的全局优化策略和ASTC的局部块搜索完全不同实现复杂度也不在一个量级。Basis Universal是另一种思路它先做全局的端点聚类再做局部block编码在BC7和ETC2的兼容性上做文章性能表现也不错但它不是ASTC这种直接对应GPU硬件格式的方案。从代码质量上看Arm-astc-encoder对算法的工程化水平值得学习。它的可读性虽然不算特别好大量数学公式直接摊在循环里但模块化程度极高几乎每个阶段都能独立调试。相比同类编码器它的注释量算比较克制的关键算法都有简短注释注释里也经常能读到“this is a heuristic”这类诚实标注这点我在阅读时好感拉满。9.2 学习它的SIMD抽象层vecmathlib的价值vecmathlib这个东西单独拿出来都值得好好研究。它把不同指令集下的向量类型和数学运算统一成一套接口然后通过宏切换实现跨平台。我在做其他数据密集任务时也借鉴了它的分层思路算法层不直接写Intel intrinsic或NEON intrinsic而是通过一个薄薄的封装层调用平台相关实现。这样做的好处是算法代码只写一份平台移植时只需要适配封装层。它的代价也很明显封装层本身有学习成本如果算法需要深度使用某个平台的特性比如AVX512的mask、NEON的lane操作封装层可能变成短板。Arm-astc-encoder的AVX2版本之所以比AVX512版本更好用也是因为这个原因——它没有过度依赖AVX512的新特性而是用AVX2已经能覆盖绝大部分核心计算兼容性还更好。9.3 从源码审计视角看它的工程成熟度如果以“生产级开源项目”的标准来审视Arm-astc-encoder它的工程成熟度相当高。单元测试覆盖了主要编解码路径CI配置在GitHub Actions里跑主流架构的编译和测试。错误码设计完善astcenc_error枚举基本覆盖了所有错误场景不会出现返回NULL让你自行猜测的API。文档质量也不错官方还维护了一套参数调优指南。有个小细节它在部分辅助函数里保留了调试统计和诊断选项astcenc_diagnostic_trace.cpp比如可以通过环境变量开启trace输出查看每个block的编码耗时、候选分区数量等。这个功能在做二次开发和性能分析时相当有用。你在线上项目排查纹理压缩瓶颈时不用改代码就能通过环境变量拿到压缩过程中的内部统计这是很多开源库做不到的。10. 项目视角的实用建议我的几个落地心得最后分享几段我在实际项目里磨出来的体会不一定每个项目都适用但应该能帮你少走弯路。第一不要把ASTC当成万能药。ASTC在移动端GPU上是硬件解码确实又快又省带宽但在不支持ASTC的桌面设备或旧移动设备上你需要一套完整的降级方案。比较好的策略是在构建流水线里同时产出ASTC和ETC2或RGBA8888版本按目标设备动态选择。第二压缩质量评估不要只看PSNR或主观肉眼。纹理压缩的误差在高动态范围、法线贴图、UI文字上的表现差异很大一定要做PBR光照环境下的实机测试。我曾经压过一张金属粗糙度贴图PSNR很高但在金属反射高光下会出现明显的带状瑕疵原因就是粗糙度通道在极暗区域的量化步长过大。这个用普通肉眼预览根本看不出来。第三如果项目纹理量极大建议做“按纹理角色分类预设码率”的流水线而不是每一张图手动调参。把纹理分成UI、角色、场景、法线、HDR等类型每种类型固定一组ASTC参数然后做批量压缩。既保证了质量一致性又极大节省了美术同学的沟通成本。需要个别微调的再例外处理走特殊通道。第四Arm-astc-encoder的API迭代比较频繁如果集成到引擎里尽量锁定版本并做自动化回归测试。它的2.x版本和3.x版本有一些API变动直接升级主版本可能会破坏构建。我自己的仓库里是把它作为submodule固定的commit有需要时才手动升级并跑一遍全套压测。第五留意CPU压缩耗时对迭代效率的影响。开发期如果每次导入纹理都跑-thorough几百张贴图会严重拖慢周转。开发环境用-fast打包发布时才切-medium或-thorough这是很多商业引擎的做法理由也很简单开发期要的是“能看个大概”发布期才需要“压到极致”。
返回列表