three.js EXRExporter 深度指南:导出 OpenEXR 高动态范围图像(浮点纹理与 PMREM 渲染目标实战)
摘要导读
本指南以 three.js 仓库内 EXRExporter(EXR 导出器)为核心,讲解如何将 WebGL/WebGPU 渲染结果或 DataTexture 导出为电影工业标准的高动态范围 OpenEXR(.exr)图像。你将掌握 EXRExporter 的导入方式、parse() 两种调用变体、type 与 compression 选项的取值与默认行为,并通过官方示例 examples/misc_exporter_exr.html 和源码实现了解 RGBA 像素如何被重组、压缩并封装为符合规范的 EXR 文件,最终可自行实现「HDR 结果一键落盘」的完整工具链。
本文默认你已具备 three.js 基础。所有结论均来自仓库内文档、源码与官方示例;运行相关示例前请先在仓库根目录启动静态文件服务器(如
python3 -m http.server),浏览器访问对应页面。
EXR 是什么:为什么 3D 渲染要用它
EXR(Extended Dynamic Range)是一种面向电影工业的开放图像格式规范(由 AcademySoftwareFoundation 维护),其设计目标是精确且高效地表示高动态范围(HDR)的 scene-linear(场景线性)图像数据以及相关元数据。原文档明确指出:在精确度要求苛刻的宿主软件中(照片级真实感渲染、纹理访问、图像合成、深度合成、DI 调色)该格式被广泛使用。
与普通 8-bit LDR 图像不同,EXR 的核心价值在于:
- 半浮点(half float)/ 单精度浮点像素:可存储远超 0–1 的动态范围,避免光照计算结果的截断与条带化;
- Scene-linear 色彩空间:与渲染器内部的线性工作流天然对齐,可无损保留 lighting / compositing 所需的真实物理量;
- 支持任意附加通道与元数据、多种压缩算法:兼顾精度、文件体积与读取速度。
在 three.js 生态里,EXR 的读取侧由 examples/jsm/loaders/EXRLoader.js 承担(其源码注释声明支持 uncompressed、ZIP(S)、RLE、PIZ、B44/A、DWA/B 等压缩变体),而写出侧就是本文主角 EXRExporter——它能让「渲染到浮点渲染目标 → 导出成 .exr 文件 → 交给合成软件或再次用 EXRLoader 读回」的完整闭环在本仓库内直接打通。
导入与实例化
EXRExporter 位于 three/examples/jsm 体系,属于需要显式导入的 addon(不随 three 核心模块自动加载)。官方用法:
import { EXRExporter } from 'three/addons/exporters/EXRExporter.js';
同时,该模块还会导出三个压缩常量,供配置 Options 使用:
import { EXRExporter, ZIP_COMPRESSION, ZIPS_COMPRESSION, NO_COMPRESSION } from 'three/addons/exporters/EXRExporter.js';
从源码来看,这些常量与类位于 examples/jsm/exporters/EXRExporter.js(常量的实际数值定义见该文件第 11–13 行:NO_COMPRESSION = 0、ZIPS_COMPRESSION = 2、ZIP_COMPRESSION = 3),并经由 examples/jsm/Addons.js 的 export * 统一转发到 three/addons/ 命名空间。因此两条导入路径都可用,但以 three/addons/ 为推荐写法。
构造十分轻量,无任何参数:
const exporter = new EXRExporter();
parse() 方法:两种调用变体
EXRExporter 的核心方法只有 parse(),且是 async 方法。它依据第一个参数的类型自动分流(源码实现见 examples/jsm/exporters/EXRExporter.js):
parse( arg1, arg2, arg3 ) : Promise<Uint8Array>
| 变体 | arg1 | arg2 | arg3 | 适用场景 |
|---|---|---|---|---|
| 变体 A:导出 DataTexture | DataTexture |
Options(可选) |
— | 导出程序化生成的浮点纹理数据,如法线/数据纹理 |
| 变体 B:导出渲染目标 | WebGLRenderer 或 WebGPURenderer |
RenderTarget(如 PMREM 渲染目标) |
Options(可选) |
导出离屏渲染结果、环境贴图(PMREM)等 |
对应官方示例里的两段真实调用:
// 变体 A:导出数据纹理(RGBAFormat / FloatType 的 DataTexture)
result = await exporter.parse( dataTexture, { type: exportType, compression: exportCompression } );
// 变体 B:导出渲染目标(例如 PMREMGenerator 生成的渲染目标)
result = await exporter.parse( renderer, renderTarget, { type: exportType, compression: exportCompression } );
返回值:Promise,resolve 为一个 Uint8Array,即完整的、可直接写盘的 EXR 二进制内容。官方示例在拿到 result 后,用 Blob(MIME 类型 image/x-exr)+ 临时 <a download> 触发浏览器下载:
const blob = new Blob( [ result ], { type: 'image/x-exr' } );
const link = document.createElement( 'a' );
link.href = URL.createObjectURL( blob );
link.download = 'output.exr';
link.click();
值得注意的错误提示分支(源码第 49–51 行):若 arg1 既不是 WebGLRenderer、WebGPURenderer 也不是 DataTexture,parse 会直接抛出 EXRExporter.parse: Unsupported first parameter...。此外,当传入的渲染目标与纹理不满足输入约束时,同样会抛错(见下节)。
输入约束:能被导出的数据长什么样
EXRExporter 并非“什么都能导”,它对输入有严格的格式限定。这两组校验分别对应源码中的 supportedRTT()(第 85–111 行,针对渲染目标)与 supportedDT()(第 113–145 行,针对 DataTexture):
渲染目标(RenderTarget)约束:
- 第二参数必须是
WebGLRenderTarget实例; - 不支持 Cube / 3D / Array 等非平面渲染目标(会抛出
Unsupported render target type); - 其
.texture.type必须为FloatType或HalfFloatType; - 其
.texture.format必须为RGBAFormat。
DataTexture 约束:
.type必须为FloatType或HalfFloatType;.format必须为RGBAFormat;.image.data必须存在;- 类型与底层数组必须严格匹配:
FloatType→Float32Array,HalfFloatType→Uint16Array(这正是 half float 的底层存储形式)。
也就是说:导出通道固定为 RGBA 四通道,官方示例(见下)中以 Float32Array 构造的 800×800 法线数据纹理即满足 FloatType + RGBAFormat 条件。
实操提示:若你的渲染目标使用 RGB 或默认格式,请先通过
WebGLRenderTarget构造参数显式指定RGBAFormat与浮点 type;纹理类(如DataTexture)同理,构造时传( data, width, height, THREE.RGBAFormat, THREE.FloatType )并设置needsUpdate = true。
Options 选项:输出类型与压缩算法
parse 的第三个(或第二个)可选参数是导出选项对象,其结构与默认值定义见 examples/jsm/exporters/EXRExporter.js 的 @typedef,官方类型定义页面 docs/pages/EXRExporter.html.md 亦有完整罗列:
| 选项键 | 取值 | 默认值 | 作用 |
|---|---|---|---|
type |
HalfFloatType | FloatType |
HalfFloatType |
决定 EXR 文件内像素的存储类型 |
compression |
NO_COMPRESSION | ZIP_COMPRESSION | ZIPS_COMPRESSION |
ZIP_COMPRESSION |
决定采用的压缩算法 |
其中压缩常量对应源码顶部的数值:NO_COMPRESSION = 0、ZIPS_COMPRESSION = 2、ZIP_COMPRESSION = 3(注意取值顺序并不连续,3 与 2 的含义见下)。
type:half 与 float 的取舍
当 type: HalfFloatType 时,导出器把像素量化为 16-bit half float(每个通道 2 字节);当 type: FloatType 时为 32-bit 单精度浮点(每通道 4 字节)。从源码的 buildInfoRTT/buildInfoDT(第 147–213 行)可看到具体换算逻辑:
OUT_TYPE = ( EXPORTER_TYPE === FloatType ) ? 2 : 1,
dataSize = 2 * OUT_TYPE, // HalfFloat → 2 字节/通道,Float → 4 字节/通道
对应到 EXR 文件头部的 channels 属性(info.dataType 被写入每个通道像素类型字段,见 fillHeader 第 440–462 行)。因此在选择时:
- 追求文件体积小、多数合成需求够用 →
HalfFloatType(每像素 8 字节); - 追求更高的精度余量、避免 deep shadow / 特殊通道量化损失 →
FloatType(每像素 16 字节),文件体积与带宽同步翻倍。
compression:三种压缩算法的真实差异
从源码第 147–153 行的 compressionSizes 映射可以明确三种算法的本质区别在于每个压缩 block 所包含的扫描行数:
const compressionSizes = {
0: 1, // NO_COMPRESSION
2: 1, // ZIPS_COMPRESSION
3: 16 // ZIP_COMPRESSION
};
NO_COMPRESSION:数据不做任何压缩(compressNONE直接原样返回),blockLines = 1,文件最大但零 CPU 开销;ZIPS_COMPRESSION:每个 block 只有 1 条扫描线,进行 ZIP(deflate)压缩——压缩率偏低但便于流式/局部读取,适合超大图需要随机访问单行的场景;ZIP_COMPRESSION:每个 block 含 16 条扫描线(blockLines = 16),一并压缩——压缩率更高,是官方默认选项,也是通用 EXR 工作流里最常用的选择。
numBlocks = Math.ceil( height / blockLines )(第 174/208 行)进一步说明文件会被切成 numBlocks 个压缩块,每块在 offset table 中单独登记偏移,这也是 EXR 支持块级随机访问的结构基础。
实战:官方演示逐行拆解
仓库提供了开箱即用的完整演示 examples/misc_exporter_exr.html,它同时覆盖了上述两种 parse 变体,非常适合当作模版。
其核心逻辑(关键行见上文件第 185–208 行)可以概括为三条分支:
// 1. 把 GUI 字符串映射为 three.js 常量
if ( params.type == 'HalfFloatType' ) exportType = THREE.HalfFloatType;
else exportType = THREE.FloatType;
// 2. 把压缩选项映射为模块导出的常量
if ( params.compression == 'ZIP' ) exportCompression = ZIP_COMPRESSION;
else if ( params.compression == 'ZIPS' ) exportCompression = ZIPS_COMPRESSION;
else exportCompression = NO_COMPRESSION;
// 3. 按输入类型调用对应的 parse 变体
if ( params.target == 'pmrem' )
result = await exporter.parse( renderer, renderTarget, { type: exportType, compression: exportCompression } );
else
result = await exporter.parse( dataTexture, { type: exportType, compression: exportCompression } );
演示场景准备了两种输入来源:
- PMREM 渲染目标(pmrem 分支):用
HDRLoader加载一张.hdr高清环境图(textures/equirectangular/san_giuseppe_bridge_2k.hdr),标记EquirectangularReflectionMapping后交给PMREMGenerator.fromEquirectangular()生成renderTarget并用作scene.background——这正是将 HDR 环境光照结果导出给下游合成软件使用的典型流程。上文配图即该演示的场景画面。 - DataTexture(data-texture 分支):以
Float32Array程序化生成一张 800×800 的球形法线纹理(RGBAFormat + FloatType,A 通道为 1),作为验证导出链路的最小输入。
页面上的 lil-gui 面板可实时切换 Input(pmrem / data-texture)、Output Options(FloatType / HalfFloatType;ZIP / ZIPS / NONE),点击 Export EXR 即可触发下载。对接到你自己的项目时,把第 3 步的结果数组交给上文的 Blob 下载代码即可。
源码纵深:像素从 buffer 到 .exr 文件的四步流水线
若把 parse() 展开(源码第 47–81 行),无论哪种变体,底层都遵循同一条流水线,这正是理解导出器行为的关键:
buildInfo( ) → getPixelData( ) → reorganizeDataBuffer( ) → compressData( ) → fillData( )
第 1 步:信息收集(buildInfoRTT / buildInfoDT,L147–213):汇总宽高、像素类型、通道数、压缩方式,并计算 blockLines、numBlocks、dataType、dataSize 等后续步骤依赖的元数据。
第 2 步:读回像素(getPixelData,L215–241):WebGL 渲染器路径会先按 FloatType/HalfFloatType 预分配 Float32Array 或 Uint16Array,再调用 renderer.readRenderTargetPixelsAsync( rtt, 0, 0, w, h, dataBuffer ) 异步读回;WebGPU 渲染器路径直接复用同一 API 由后端返回 buffer。DataTexture 路径则跳过读回,直接取 texture.image.data。
第 3 步:数据重组(reorganizeDataBuffer,L243–288):这是最关键的一步,做了三件事:
- 从交错的
RGBA平面排列转为 EXR 的分通道平面排列,且通道顺序按A、B、G、R写入(见第 271–280 行的四个setValue调用); - 做 Y 轴翻转(
line = ( h - y - 1 ) * ...,第 266 行),使第 0 行对应图像顶部,符合 EXR 自底向上的 scanline 语义; - 执行类型转换:输入若为
HalfFloatType(Uint16Array的 half 位型),先经decodeFloat16(第 581–596 行)解码为真实浮点值;输出若为 half,则经DataUtils.toHalfFloat()(第 545–551 行)重新编码为 16-bit 写入。
第 4 步:压缩(compressData + compressZIP,L290–382):按 numBlocks 逐个 block 压缩。ZIP 压缩并非简单的 deflate,而是严格按 OpenEXR 规范实现的:
- 像素重排:偶数位字节依次填入前一半、奇数位字节填入后一半(第 348–362 行);
- 差值预测:对重排后的字节序列做增量编码
d = byte[i] - byte[i-1] + 384(第 368–376 行),把平滑区域的数值压缩到窄分布以提升压缩率; - 最后交给 fflate 的
zlibSync(第 378 行)做标准的 deflate 输出。
NO_COMPRESSION 则直接透传原始字节。
第 5 步:文件封装(fillData / fillHeader,L384–508):把前面产出的块写成合法 EXR 容器,其中包括:
- 文件魔数
20000630与版本字段(第 389–390 行); - 头部属性:
compression、screenWindowCenter(v2f)、screenWindowWidth(写入 1.0)、pixelAspectRatio(写入 1.0)、lineOrder(写入 0,即 increasing Y)、dataWindow/displayWindow(均为box2i,坐标为 (0,0)-(w-1,h-1))、channels(chlist,按A B G R顺序声明,含各通道的像素类型与采样率 1×1); - 块偏移表:为每个 block 写 8 字节的 64 位起始偏移(第 471–479 行);
- 每个数据块前再写该块起始行号(
y坐标)与压缩后字节数(第 496–501 行)。
所有多字节数值均以小端序写入(dv.setUint32( ..., true ))。理解这条流水线后,你会明白为何 FloatType 输入却能输出 HalfFloatType 文件(类型转换发生在第 3 步),以及为何同一份数据配合不同 compression 会得到体积差异悬殊的文件。
闭环验证:用 EXRLoader 读回导出结果
导出的 .exr 文件是否合法?最好的验证方式是用仓库自己的 EXRLoader 读回。EXRLoader(examples/jsm/loaders/EXRLoader.js)支持 uncompressed、ZIP(S)、RLE、PIZ、B44/A、DWA/B 压缩,自然覆盖 EXRExporter 能产出的全部三种模式。一个简单的自校验片段(参照 examples/webgl_loader_texture_exr.html 的加载姿势):
new EXRLoader().load( 'output.exr', ( texture ) => {
texture.mapping = THREE.EquirectangularReflectionMapping; // 若为环境图
scene.environment = texture;
} );
若你的导出来自 PMREM 渲染目标,用 EXRLoader 将其作为 scene.environment 读回,即可肉眼比对导出前后间接光照是否一致——这正是环境贴图导出最常见的验收手段。
常见问题与注意事项
- 输入必须是 RGBA + 浮点:RGBFormat、UnsignedByteType 等输入会直接抛错。若源数据是 LDR 纹理,先在 CPU/GPU 侧转为浮点 RGBA 再交给导出器。
- Cube/3D/Array 渲染目标不支持:如需导出 cube 环境贴图,请逐面导出 6 张 2D EXR 或在读回后自行拼接。
- half 量化误差:
type: HalfFloatType时小于约 6e-5、大于 65504 的数值会超出 half 表示范围,若渲染结果含极亮区域且后续需要大幅提亮,建议type: FloatType。 - 体积与性能权衡:
ZIPS比ZIP压缩率低(单行 block 破坏长程冗余),但文件更利于局部随机读取;NONE体积最大,一般仅在调试或追求零编码开销时选用。 - 类型严格匹配:DataTexture 为
FloatType时其.image.data必须是Float32Array;HalfFloatType时必须是Uint16Array(Uint16Array中存的是 half 位型,这是 three.js half float 纹理的标准布局),否则导出器抛错。
参考资料(仓库内)
- 类型文档:本指南依托的原始 API 文档 docs/pages/EXRExporter.html.md(渲染页面为同目录
EXRExporter.html) - 源码实现:examples/jsm/exporters/EXRExporter.js(含压缩常量、选项类型定义与全部导出逻辑)
- 官方演示:examples/misc_exporter_exr.html(PMREM / DataTexture 双输入 + 下载落盘)
- 反向读取:examples/jsm/loaders/EXRLoader.js、examples/webgl_loader_texture_exr.html
- addon 统一入口:examples/jsm/Addons.js
至此,你已经具备了在 three.js 项目中把 HDR 渲染结果完整导出为规范 OpenEXR 文件所需的全部知识与可复制的代码路径。
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
