three.js DRACOExporter:将 Mesh 与点云导出为 Draco 压缩 .drc 文件的完整指南
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
本文基于 three.js 仓库中的 DRACOExporter 官方文档 与 源码实现 展开,讲解如何使用该 Addon 把Mesh或Points对象导出为 Draco 压缩的.drc二进制数据。读完本文,你将掌握parseAsync()的完整调用方式、全部 7 个导出选项(decodeSpeed、encodeSpeed、encoderMethod、quantization、exportUvs、exportNormals、exportColor)的默认值与底层作用,以及从源码层面理解编码器如何组织顶点、面索引与量化的整个流程。
一、Draco 是什么,DRACOExporter 能做什么
DRACOExporter 是 three.js 提供的一个导出器(exporter),用于借助 Google 开源的 Draco 库压缩 3D 网格与点云几何体。压缩后的几何数据可以显著减小体积,代价是客户端需要付出额外的解码时间。
关于独立 Draco 文件,文档中明确了它的“能”与“不能”:
- 独立
.drc文件包含:顶点位置(POSITION)、法线(NORMAL)、颜色(COLOR)以及其他顶点属性; - 独立
.drc文件不包含:材质(materials)、贴图(textures)、动画(animation)、节点层级(node hierarchies)。
这一点决定了.drc的定位:它只保存“几何数据”,是纯粹的几何压缩容器。如果需要导出带材质、动画的完整场景并使用 Draco 压缩,正确做法是把 Draco 几何体嵌入 glTF 文件中(three.js 的GLTFExporter也支持此流程),而不是依赖DRACOExporter本身。
从源码头部注释看,DRACOExporter.js 第 5~27 行 的类文档与官方文档完全一致,并给出了最小用法示例:
const exporter = new DRACOExporter(); const data = await exporter.parseAsync( mesh, options );二、前置条件:必须先加载 Draco 编码器
DRACOExporter依赖一个全局脚本DracoEncoderModule(Draco 的 WebAssembly/JS 编码器构建产物)。这是使用该导出器的硬性前置条件,也是很多初学者踩的坑。
官方示例 misc_exporter_draco.html 的做法是在页面中先引入编码器(以 1.5.7 版本为例):
<script src="https://cdn.jsdelivr.net/gh/google/draco@1.5.7/javascript/draco_encoder.js"></script>编辑器 editor/index.html 同样以相同方式引入了该全局脚本,说明这是仓库内所有使用DRACOExporter的场景的统一做法。
源码中对此有显式校验——第 53~57 行:
if ( typeof DracoEncoderModule === 'undefined' ) { throw new Error( 'THREE.DRACOExporter: required the draco_encoder to work.' ); }也就是说,如果忘记加载编码器,parseAsync()会直接抛出THREE.DRACOExporter: required the draco_encoder to work.错误,而不是静默失败。另外,源码 第 63~65 行 还处理了新旧编码器构建的兼容问题:
let dracoEncoder = DracoEncoderModule(); // older encoder builds expose the module synchronously, newer builds return a promise if ( dracoEncoder.Encoder === undefined ) dracoEncoder = await dracoEncoder;旧版编码器同步暴露模块,新版返回 Promise,parseAsync对两者都做了适配,因此无论加载哪个版本构建都能工作。
三、导入方式与构造函数
DRACOExporter属于 three.js 的 Addon,需要显式导入(而非从three主包引入):
import { DRACOExporter } from 'three/addons/exporters/DRACOExporter.js';构造函数无任何参数:
const exporter = new DRACOExporter();实例化后即可反复调用parseAsync()导出多个对象,编码器实例是在每次parseAsync()内部创建并销毁的(见后文源码分析),导出器本身是无状态的。
四、核心 API:parseAsync( object, options )
4.1 方法签名
.parseAsync( object : Mesh | Points, options : DRACOExporter~Options ) : Promise.<Int8Array> (async)| 参数 | 说明 |
|---|---|
object | 要导出的对象,只支持Mesh或Points两种类型,其他类型会抛出THREE.DRACOExporter: Unsupported object type.错误 |
options | 导出选项,可省略,缺省时使用全部默认值 |
| 返回值 | 一个 Promise,resolve 的结果是Int8Array,即完整的.drc二进制内容 |
注意旧 API 的状态:文档标注.parse()已被弃用,源码中它现在只是一个抛错的占位实现(第 240~244 行):
parse() { throw new Error( 'THREE.DRACOExporter: parse() has been replaced by parseAsync().' ); }如果你在旧教程中看到exporter.parse( mesh, callback )的写法,迁移到新版本的唯一方式就是换成await exporter.parseAsync( mesh, options )。
4.2 Options 参数完整参考
options对象支持以下 7 个字段,缺省值来自 源码第 43~51 行 的Object.assign:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
decodeSpeed | number | 5 | 提示编码器如何针对解码速度调优。取值 0~10,0 表示解码最快但压缩质量(压缩率)最差,10 反之 |
encodeSpeed | number | 5 | 提示编码器如何调优编码参数。0 表示编码最快但压缩质量最差,10 反之 |
encoderMethod | number | 1(即MESH_EDGEBREAKER_ENCODING) | 编码方式:0为顺序编码(几乎不压缩),1为 Edgebreaker 编码。Edgebreaker 以确定的螺旋状方式遍历网格三角形,能提供该数据格式的大部分压缩收益 |
quantization | Array<number> | [ 16, 8, 8, 8, 8 ] | 按(POSITION, NORMAL, COLOR, TEX_COORD, GENERIC)的顺序,指定 draco 文件中每类数据使用的量化位数(即精度) |
exportUvs | boolean | true | 是否导出 UV(贴图坐标) |
exportNormals | boolean | true | 是否导出法线 |
exportColor | boolean | false | 是否导出顶点颜色 |
几个值得注意的调优要点:
- 速度参数方向:源码 第 169 行 注释写明“从 0(最慢速度,但最佳压缩)到 10(最快,但压缩最差)”,随后调用
encoder.SetSpeedOptions( encodeSpeed, decodeSpeed )。所以追求文件体积最小应把两个速度值调小(如 0),追求编码/解码实时性则调大。 - 量化位数与体积/精度权衡:
quantization数组对应 5 类属性。默认位置属性 16 位量化,其余 8 位——位置保留较高精度以保证形状保真,法线/颜色/UV 则用 8 位大幅压缩。源码 第 186~198 行 会对数组前 5 个有效元素逐个调用encoder.SetAttributeQuantization( i, bits ),数组中缺失的项(undefined)会被跳过,因此你只传[ 12, 8 ]这样的部分数组也是合法的。 - Edgebreaker vs 顺序编码:
encoderMethod通过encoder.SetEncodingMethod()设置(第 178~182 行)。默认值就是 Edgebreaker,对三角面网格压缩收益最大;顺序编码基本不做压缩,一般只在特殊场景使用。
4.3 最小可运行示例
结合官方示例 misc_exporter_draco.html 的核心逻辑,一次完整的导出 + 保存流程如下:
<!-- 前置条件:先加载编码器全局脚本(同官方示例 misc_exporter_draco.html) -->import * as THREE from 'three'; import { DRACOExporter } from 'three/addons/exporters/DRACOExporter.js'; // 创建场景与被导出的网格 const geometry = new THREE.TorusKnotGeometry( 0.75, 0.2, 200, 30 ); const material = new THREE.MeshPhongMaterial( { color: 0x00ff00 } ); const mesh = new THREE.Mesh( geometry, material ); // 执行导出 const exporter = new DRACOExporter(); async function exportFile() { const result = await exporter.parseAsync( mesh ); // 返回 Int8Array // 保存为文件 const link = document.createElement( 'a' ); link.href = URL.createObjectURL( new Blob( [ result ], { type: 'application/octet-stream' } ) ); link.download = 'file.drc'; link.click(); }导出结果是Int8Array,官方示例把它包装成Blob触发浏览器下载,得到可直接被 Draco 工具链、DRACOLoader或 PLY/OBJ 转换工具消费的.drc文件。
五、源码级流程解析:parseAsync 内部发生了什么
阅读 parseAsync 完整实现 可以还原出完整的处理链路,这有助于理解各选项到底影响了什么。
5.1 按对象类型分两条流水线
parseAsync(object) ├─ object.isMesh === true │ → MeshBuilder + Mesh │ → AddFloatAttributeToMesh(POSITION) // 顶点位置(必导) │ → AddFacesToMesh( 面索引 ) // 有索引用索引,无索引则自动构造 │ → 可选: NORMAL / TEX_COORD / COLOR 属性 └─ object.isPoints === true → PointCloudBuilder + PointCloud → AddFloatAttribute(POSITION) // 顶点位置(必导) → 可选: COLOR 属性关键点:
- 位置属性始终导出,且是唯一不受选项开关控制的属性;法线、UV、颜色分别在
exportNormals、exportUvs、exportColor为true且几何体确实存在对应 attribute 时才会加入(第 99~135 行)。 - 无索引几何体的自动兜底:如果
Mesh的geometry.getIndex()为null,源码 第 85~97 行 会现场生成一个顺序索引数组(顶点数超过 65535 时自动选用Uint32Array,否则Uint16Array)再交给AddFacesToMesh。这意味着即使你直接拿new THREE.PlaneGeometry()之类默认就带索引的几何体、或手动去除了索引,导出也不会失败。 - 非 Mesh / Points 直接抛错(第 159~163 行),例如
Group、Sprite都不支持。
5.2 编码、量化的落点
// [第 171~198 行] const encodeSpeed = ( options.encodeSpeed !== undefined ) ? options.encodeSpeed : 5; const decodeSpeed = ( options.decodeSpeed !== undefined ) ? options.decodeSpeed : 5; encoder.SetSpeedOptions( encodeSpeed, decodeSpeed ); if ( options.encoderMethod !== undefined ) { encoder.SetEncodingMethod( options.encoderMethod ); } for ( let i = 0; i < 5; i ++ ) { if ( options.quantization[ i ] !== undefined ) { encoder.SetAttributeQuantization( i, options.quantization[ i ] ); } }三类参数最终都映射到 Draco C++ 编码器经 emscripten 封装出的 API:速度 →SetSpeedOptions,编码方式 →SetEncodingMethod,量化 →SetAttributeQuantization(按属性类型 0~4 逐个设置,数组按(POSITION, NORMAL, COLOR, TEX_COORD, GENERIC)顺序对应这些类型下标)。
5.3 顶点颜色有一个容易被忽略的 sRGB 转换
exportColor: true时,顶点颜色不是直接复制原始 Float32 数据,而是先经过 createVertexColorSRGBArray:
function createVertexColorSRGBArray( attribute ) { // While .drc files do not specify colorspace, the only 'official' tooling // is PLY and OBJ converters, which use sRGB. We'll assume sRGB is expected // for .drc files, but note that Draco buffers embedded in glTF files will // be Linear-sRGB instead. _color.fromBufferAttribute( attribute, i ); ColorManagement.workingToColorSpace( _color, SRGBColorSpace ); ... }原因在注释里写得很清楚:.drc格式本身不声明色彩空间,而官方生态(PLY/OBJ 转换器)默认按 sRGB 处理,因此 three.js 会主动把 working color space 的颜色转换到 sRGB 再写入;反之,Draco 缓冲区嵌入 glTF 时则使用 Linear-sRGB。如果你在往返测试中发现顶点颜色“偏亮/偏暗”,这里就是差异来源。
5.4 输出拷贝与资源释放
编码完成后(第 200~233 行):
const encodedData = new dracoEncoder.DracoInt8Array(); if ( object.isMesh === true ) { length = encoder.EncodeMeshToDracoBuffer( dracoObject, encodedData ); } else { length = encoder.EncodePointCloudToDracoBuffer( dracoObject, true, encodedData ); }- 返回长度为 0 时视为编码失败,抛出
THREE.DRACOExporter: Draco encoding failed.; - 随后把 WASM 侧的
DracoInt8Array逐字节拷贝到一个全新的Int8Array(new ArrayBuffer( length ))中再返回——这样调用方拿到的就是纯 JS 侧内存,与 emscripten 堆解耦; - 最后显式
destroy了dracoObject、encodedData、encoder、builder四个 WASM 对象,防止长时间导出多个模型时的内存泄漏。
5.5 类上的一组常量
除了文档列出的两个编码方式常量,源码还在类上定义了完整的属性类型枚举(第 283~317 行):
| 常量 | 值 | 用途 |
|---|---|---|
MESH_EDGEBREAKER_ENCODING | 1 | Edgebreaker 编码(默认),对应options.encoderMethod |
MESH_SEQUENTIAL_ENCODING | 0 | 顺序编码,几乎不压缩 |
POINT_CLOUD | 0 | 几何类型枚举 |
TRIANGULAR_MESH | 1 | 几何类型枚举 |
POSITION/NORMAL/COLOR/TEX_COORD/GENERIC | 0~4 | 属性类型,即quantization数组下标顺序 |
INVALID | -1 | 无效属性 |
其中MESH_EDGEBREAKER_ENCODING与MESH_SEQUENTIAL_ENCODING是文档明确列出的“Properties”;后几组是内部流程使用的类型编号,理解quantization数组时正好可以对照。
六、官方示例:misc_exporter_draco
仓库自带一个可交互演示 misc_exporter_draco.html(截图即文首配图),完整演示了本文的全部要点:
- 页面顶部加载
draco_encoder.js全局脚本(第 18 行); - 通过 import map 把
three/addons/映射到仓库的./jsm/目录(第 20~27 行),因此该示例可以直接在源码仓库中静态托管运行; - 用
TorusKnotGeometry( 0.75, 0.2, 200, 30 )建了一个带光照与阴影的网格场景(第 85~90 行); - 通过 lil-gui 提供 “Export DRC” 按钮,点击后调用
await exporter.parseAsync( mesh )并把结果保存为file.drc(第 134~139 行)。
注意该示例没有传options,即完全使用默认参数导出;若需调整压缩率/精度,按第四节表格传参即可。
七、仓库内其他使用场景:three.js 编辑器
DRACOExporter在 three.js 自带的场景编辑器里也是一等公民。editor/js/Menubar.File.js 第 239~267 行 实现了 “File → Export → DRC” 菜单项,其用法值得参考——它展示了按几何体实际内容自适应选项的写法:
const options = { decodeSpeed: 5, encodeSpeed: 5, encoderMethod: DRACOExporter.MESH_EDGEBREAKER_ENCODING, quantization: [ 16, 8, 8, 8, 8 ], exportUvs: true, exportNormals: true, exportColor: object.geometry.hasAttribute( 'color' ) }; const result = await exporter.parseAsync( object, options ); saveArrayBuffer( result, 'model.drc' );可以看到编辑器只在选中对象是 Mesh 时才允许导出(否则弹出 noMeshSelected 提示),并且把exportColor动态设置为“几何体是否真的带 color 属性”——比无脑true更严谨,因为源码中颜色导出在colors === undefined时本来就会静默跳过,但显式控制可以让选项语义与几何体状态一致。编辑器页面 editor/index.html 也已预加载了编码器脚本。
八、使用限制与注意事项
- 对象类型受限:仅支持
Mesh与Points,传Group/Sprite等会抛错;导出的也是单个对象,不支持整场景遍历。 .drc不含场景语义:无材质、贴图、动画、层级。需要完整场景 + Draco 压缩时,应走 glTF 路线(Draco 嵌入 glTF),而不是用本导出器。- 编码器版本:仓库示例锁定 draco 1.5.7 的
draco_encoder.js,且该脚本必须作为全局脚本先于parseAsync()加载;源码同时兼容同步/异步两种模块暴露方式。 - 往返精度:位置默认 16 位量化、其余 8 位,导出后再加载回 three.js 的几何体会存在轻微数值偏差;若你的应用对位置精度敏感(如 CAD 类),可上调
quantization[ 0 ]到 17~20(Draco 上限 24 位)。 - 旧代码迁移:
parse()已弃用且直接抛错,一律改用parseAsync()并配合await。 - 色彩空间:顶点颜色导出时会转换到 sRGB(见 5.3 节),往返对比颜色时需注意。
九、小结
DRACOExporter的 API 面很小——一个无参构造函数、一个parseAsync()、七个选项,但它把“three.js 内存中的几何体 → Draco 压缩二进制”这条链路封装得非常干净:自动处理索引兜底、WASM 编码器新旧版本兼容、量化参数逐属性下发、sRGB 颜色转换与 WASM 资源释放。默认配置(Edgebreaker + 速度 5 +[16,8,8,8,8]量化)适合大多数网格压缩场景;需要更极致的压缩率时,调低encodeSpeed/decodeSpeed并提高量化位数即可。官方示例 misc_exporter_draco.html 与 editor/js/Menubar.File.js 中的编辑器实现,是两个可以直接对照抄写的实战范本。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考