three.js EXRLoader 实战指南:加载 OpenEXR HDR 纹理的压缩格式支持、类型控制与数据管线解析
OpenEXR(.exr)是影视级 HDR 图像事实标准格式,常用于环境贴图与光照探针。本文以 three.js 仓库中的 EXRLoader 为线索,完整讲解其继承关系、API、可配置类型/格式参数及底层解码流程,并通过仓库内真实示例(webgl_loader_texture_exr、webgl_materials_envmaps_exr)给出可直接运行的接入方案。读完本文,你将掌握如何在 three.js 中正确加载各种压缩方式的 EXR 纹理、控制输出为半浮点/浮点数据并用于 HDR 环境映射。
EXRLoader 是什么
EXRLoader 是 three.js 中负责加载 OpenEXR 纹理格式的加载器,完整源码位于 examples/jsm/loaders/EXRLoader.js。它是加载二进制纹理格式的抽象基类 DataTextureLoader 的派生类,官方文档标注的继承链为:
Loader → DataTextureLoader → EXRLoader
在 src/loaders/DataTextureLoader.js 中可看到,DataTextureLoader 本身就是为 RGBE、EXR、TGA 这类二进制纹理格式设计的:它通过 FileLoader 以 arraybuffer 响应类型拉取文件,并约定派生类必须实现 parse() 方法(文件头注释 Derived classes have to implement the parse() method),随后把解析结果写入一个 DataTexture 并设置 needsUpdate = true。
压缩格式支持范围
按 EXRLoader.js 的类注释与官方 API 文档,EXRLoader 目前支持以下压缩方式的解码:
- 无压缩(uncompressed)
- ZIP / ZIPS(DEFLATE 压缩,基于
fflate的unzlibSync) - RLE(游程编码)
- PIZ(基于小波变换 + Huffman 的压缩)
- B44 / B44A(有损压缩,面向 GPU 纹理)
- DWA / DWAB(基于 DCT 的压缩)
读入方向支持 UnsignedByte、HalfFloat(半浮点)与 Float(浮点)三种数据类型的 DataTexture。需要说明:官方 API 文档中 .type 属性的可取值标注为 HalfFloatType | FloatType(默认 HalfFloatType),而源码类注释同时提到可读出 UnsignedByte 数据,实际以你使用的具体版本 API 标注为准。
引入方式(Addon 显式导入)
EXRLoader 属于 examples 目录下的 addon 模块,不包含在 three.js 核心构建产物中,必须显式导入。官方推荐写法:
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
该模块同时被登记在 examples/jsm/Addons.js 的 addons 清单中,因此 three/addons/... 的别名映射路径是有效的。在仓库内使用 importmap 的示例中,对应的别名配置为 "three/addons/": "./jsm/",即实际文件位于 examples/jsm/loaders/EXRLoader.js。如不使用构建工具与 importmap,也可直接以模块路径引用该文件。
快速上手
基础异步加载
官方文档给出的最小示例:
const loader = new EXRLoader();
const texture = await loader.loadAsync( 'textures/memorial.exr' );
loadAsync 是 Loader 基类基于 load() 封装的 Promise 版本,成功后即可把返回的 DataTexture 赋给材质的 map、envMap 或场景背景。仓库中也有对应的实际示例纹理 examples/textures/memorial.exr 与 examples/textures/piz_compressed.exr。
链式配置后再加载
setDataType、setOutputFormat、setPart 三个方法都返回加载器自身,支持链式调用:
const loader = new EXRLoader()
.setDataType( THREE.FloatType ) // 以 32 位浮点输出
.setOutputFormat( THREE.RGBAFormat );// 输出 RGBA
const texture = await loader.loadAsync( 'textures/piz_compressed.exr' );
只解析内存中的 ArrayBuffer
parse( buffer ) 直接接收 ArrayBuffer,适合已经拿到的文件字节(如拖拽文件或从服务端获取的二进制数据),返回符合 DataTextureLoader~TexData 约定的对象;若希望直接生成纹理而不走网络加载,还可调用基类 DataTextureLoader 提供的 createDataTexture( buffer )(见 src/loaders/DataTextureLoader.js)。
构造函数与参数
new EXRLoader( manager : LoadingManager )
构造一个 EXR 加载器。参数 manager 是 LoadingManager(关联文档内部链接对应页面),用于统一管理进度、onStart/onProgress/onError 事件,可省略。该参数最终透传给父类 Loader / DataTextureLoader 构造器(源码 EXRLoader.js)。
属性详解
.type : HalfFloatType | FloatType
纹理的数据类型。默认 HalfFloatType(半浮点),即每个通道 16 位浮点。源码中在构造函数内显式初始化为 this.type = HalfFloatType(EXRLoader.js)。半浮点在带宽与内存上比全浮点省一半,同时足以表达高动态范围,是 HDR 纹理的常用选择;对精度敏感的场景(例如需要精确的数值运算)可通过 .type = THREE.FloatType 切换为 32 位浮点。
.outputFormat : RGBAFormat | RGFormat | RedFormat
纹理输出格式,默认 RGBAFormat。源码默认值为 this.outputFormat = RGBAFormat(EXRLoader.js)。三通道图像会被归一化为四通道 RGBA(见下文“RGBA 输出”),从而规避部分设备上的软件模拟开销。若你的 EXR 为灰度(只有 Y 通道)等场景,可尝试设置为 RGFormat 或 RedFormat 减少通道数。
.part : number
多分区(multi-part)EXR 文件中要加载的分区索引,默认 0。源码默认 this.part = 0(EXRLoader.js)。真正解析时会对该值做边界收敛:partIndex = Math.max( 0, Math.min( this.part, EXRHeaders.length - 1 ) )(EXRLoader.js),即使传入越界索引也会安全地落在有效分区范围内。
方法详解
.parse( buffer : ArrayBuffer ) : DataTextureLoader~TexData
解析给定的 EXR 二进制数据,重写(Overrides)自 DataTextureLoader 的 parse() 抽象约定。返回对象包含以下字段(见 EXRLoader.js):
| 字段 | 值 |
|---|---|
header |
当前分区的 EXR 头信息(含压缩类型、数据窗口、通道列表等) |
width / height |
由数据窗口 dataWindow.xMax - xMin + 1 等计算出的像素尺寸 |
data |
解码后的字节数组(TypedArray 底层 buffer) |
format |
解码器按输出格式决定的纹理格式 |
colorSpace |
默认线性颜色空间 LinearSRGBColorSpace |
type |
即当前 .type |
minFilter / magFilter |
均为 LinearFilter |
generateMipmaps |
false(EXR 图像常常是非 2 的幂次即 NPOT,故默认不生成 mipmap) |
flipY |
false |
基类 DataTextureLoader.js 的 _applyTexData() 随后会把上述字段逐一应用到 DataTexture 上:填充 image.width/height/data,默认 wrapS/wrapT 为 ClampToEdgeWrapping,并把 colorSpace、format、type、minFilter/magFilter 等同步到纹理对象,最后置 needsUpdate = true。
.setDataType( value : HalfFloatType | FloatType ) : EXRLoader
设置纹理数据类型,写入 this.type 并返回 this 供链式调用(源码见 EXRLoader.js)。与直接赋值 .type = value 等价。
.setOutputFormat( value : RGBAFormat | RGFormat | RedFormat ) : EXRLoader
设置输出格式,写入 this.outputFormat 并返回 this(EXRLoader.js)。默认 RGBAFormat。
.setPart( value : number ) : EXRLoader
对多分区文件设置要加载的分区索引,写入 this.part 并返回 this(EXRLoader.js)。分区编号从 0 开始。
仓库实例:把 EXR 加载到场景中
基础示例:EXR 贴图显示
完整可运行的实现见 examples/webgl_loader_texture_exr.html。其核心片段:
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
new EXRLoader()
.load( 'textures/memorial.exr', function ( texture, textureData ) {
// memorial.exr 是 NPOT 尺寸
const material = new THREE.MeshBasicMaterial( { map: texture } );
const quad = new THREE.PlaneGeometry( 1.5 * textureData.width / textureData.height, 1.5 );
const mesh = new THREE.Mesh( quad, material );
scene.add( mesh );
} );
几点可验证的细节:
load()的回调形参是( texture, textureData ),其中textureData即parse()的返回对象,可通过textureData.width/height反推四边形宽高比,这正是示例第 77 行的用法;- 示例注释(第 70-73 行)明确指出
EXRLoader会设置这些默认值:texture.generateMipmaps = false、texture.minFilter = LinearFilter、texture.magFilter = LinearFilter——与parse()返回值中的generateMipmaps: false、LinearFilter完全对应; - HDR 画面需要开启色调映射才能正确显示,示例使用
renderer.toneMapping = THREE.ReinhardToneMapping并提供曝光参数 GUI 调节。
环境映射示例:EXR 作为 HDR 环境贴图
见 examples/webgl_materials_envmaps_exr.html,展示了 EXR 的典型高级用法——加载等距柱状投影(equirectangular)HDR 全景,配合 PMREMGenerator 预滤波生成用于 PBR 反射的环境立方纹理:
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
import { PMREMGenerator } from 'three/addons/...'; // 视实际版本而定
const pmremGenerator = new THREE.PMREMGenerator( renderer );
new EXRLoader().load( 'textures/piz_compressed.exr', function ( texture ) {
texture.mapping = THREE.EquirectangularReflectionMapping;
exrCubeRenderTarget = pmremGenerator.fromEquirectangular( texture );
exrBackground = texture;
} );
注意这里使用的素材恰是 piz_compressed.exr——PIZ 压缩在工业界广泛用于合成器输出,正好检验加载器的 PIZ 解码路径。
深入:EXR 解码管线(源码级)
从 parse() 到像素数据的完整流程可分为五步,全部位于 examples/jsm/loaders/EXRLoader.js:
1. 解析头部并校验格式
parseHeader( bufferDataView, buffer, offset ) 读取 EXR 头,得到按分区组织的 EXRHeaders 数组(支持 2.0 规范中的 multi-part)。随后按 this.part 选取要解码的分区。
2. 处理多分区 / 深度数据的偏移表
源码 EXRLoader.js:当头部 multiPart 或 deepFormat 为真时,对目标分区记录 chunk 偏移表 _chunkOffsets,对其余分区则仅用 parseInt64 跳过偏移表,保证读取位置正确。
3. 按压缩类型装配解码器
setupDecoder() 依据头部 compression 字段做 switch 分发(EXRLoader.js):
| 头部压缩字段 | 解码函数 | blockHeight(每块行数) |
|---|---|---|
NO_COMPRESSION |
uncompressRAW |
1 |
RLE_COMPRESSION |
uncompressRLE |
1 |
ZIPS_COMPRESSION |
uncompressZIP |
1 |
ZIP_COMPRESSION |
uncompressZIP |
16 |
PIZ_COMPRESSION |
uncompressPIZ |
32 |
PXR24_COMPRESSION |
uncompressPXR |
16 |
B44_COMPRESSION / B44A_COMPRESSION |
uncompressB44 |
32 |
DWAA_COMPRESSION |
uncompressDWA |
32 |
DWAB_COMPRESSION |
uncompressDWA |
256 |
| 其他 | 抛出 THREE.EXRLoader: ... is unsupported |
— |
其中 uncompressZIP 调用 fflate 的 unzlibSync(文件顶部 import { unzlibSync } from '../libs/fflate.module.js')完成 DEFLATE 解压。注意源码还额外实现了 PXR24_COMPRESSION,而官方文档的支持列表(uncompressed、ZIP(S)、RLE、PIZ、B44/A、DWA/B)未把 PXR24 列入,属于实现层面比文档列举更多的细节。
4. 校验通道组合,确定输出通道数
源码 EXRLoader.js 会检查输入通道名并决定解码策略:
- 存在
Y、RY、BY(亮度 + 色差)→ 按 4 通道输出并标记yCbCr = true,后续做 YCbCr → RGB 颜色转换; - 存在
R、G、B→ 按 4 通道输出(RGB); - 仅存在
Y(灰度/luminance)→ 按 1 通道输出; - 否则抛出
file contains unsupported data channels.。
5. 按输出格式做通道排布与色彩转换
按 .outputFormat 设置最终的 format、decodeChannels 映射(EXRLoader.js):
- RGBAFormat(默认):RGBA 四通道,缺省 A 通道时填充
fillAlpha = true,解码后若原图只有 RGB,则通过字节后处理把 RGB 复制为 RGBA 或填充 Alpha=1(见shouldExpand逻辑,EXRLoader.js),颜色空间为LinearSRGBColorSpace; - RGFormat / RedFormat:输出两通道/单通道,相应地只把 R(或 Y)写入对应位置;
- Y/Cb/Cr 图像的第二遍转换:当
EXRDecoder.yCbCr为真时,利用 Rec.709 系数就地完成亮度色差到 RGB 的换算:R = (1 + RY) * Y、B = (1 + BY) * Y、G = (Y - R*0.2126 - B*0.0722) / 0.7152(EXRLoader.js),且对负值做Math.max( 0, ... )截断;HalfFloat 路径会先用decodeFloat16还原数值再经DataUtils.toHalfFloat写回半浮点。
解码完成后即构造前面列举的 TexData 返回对象,交由基类完成 DataTexture 的配置。
使用注意事项小结
- 类型精度与内存:默认
HalfFloatType是 HDR 加载的均衡之选;改用FloatType会成倍增加纹理内存,仅在数值精度需求明确时使用。 - 不生成 mipmap:因为 EXR 多为 NPOT 图像,加载器默认
generateMipmaps = false、过滤为LinearFilter,直接用于贴图时缩放渲染可能出现闪烁/锯齿,可按需自行对纹理做尺寸规整后另行开启 mipmap。 - 颜色空间:解析结果的颜色空间被标记为线性
LinearSRGBColorSpace,HDR 环境光/贴图应按线性数据参与光照计算,输出到屏幕前再交给渲染器的色调映射与色彩管理。 - 多分区文件:EXR 2.0 的多分区(multi-part,如含独立深度/AOV 分区的合成文件)需通过
setPart()指定目标分区。 - 格式一致性:
setDataType/setOutputFormat影响纹理的type与format,需要与材质/渲染目标所需的像素布局匹配,例如 RGBA 半浮点对绝大多数 PBR 材质与 PMREM 流程是通用配置。
延伸阅读
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00