news 2026/9/9 12:37:24

three.js USDZExporter 完整指南:将 Three.js 场景导出为 USDZ,用于 iOS AR Quick Look

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
three.js USDZExporter 完整指南:将 Three.js 场景导出为 USDZ,用于 iOS AR Quick Look

three.js USDZExporter 完整指南:将 Three.js 场景导出为 USDZ,用于 iOS AR Quick Look

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

本指南围绕 three.js 的官方附加组件(addon)USDZExporter展开,讲解如何把Object3D/Scene场景导出为 Apple AR Quick Look 可用的 USDZ 文件。通过阅读本文,你将掌握parse/parseAsync等全部 API、各导出选项(AR 锚定、纹理压缩上限、动画烘焙等)的准确语义,以及导出器内部的 USD(Universal Scene Description)场景组织、材质贴图管线与动画时间采样原理,并学会用仓库自带的单元测试与官方示例验证导出结果。

USDZExporter的官方 API 文档位于 docs/pages/USDZExporter.html.md,其实现源码在 examples/jsm/exporters/USDZExporter.js,本文所有结论均可在该源码与配套测试中得到验证。

一、背景:USDZ、USD 与 AR Quick Look

USDZ 是 Apple 推出的基于 USD(Universal Scene Description,Pixar 通用场景描述)的压缩归档格式。一个.usdz文件本质上是未压缩(或低压缩)的 ZIP 归档,内部存放多个 USD 文本/二进制文件与配套贴图资源。其典型应用场景是 iOS/iPadOS 上的 AR Quick Look:网页中放置一个<a rel="ar">链接并指向.usdz文件,用户在 Safari 中点击即可在真实环境中预览并放置 3D 模型。

从 examples/jsm/exporters/USDZExporter.js 的实现可以看到该导出器产出的归档结构:

  • model.usda:主体场景描述文件(USD ASCII 文本),导出器刻意把它初始化为归档中的第一个文件(源码注释model file should be first in USDZ archive so we init it here);
  • geometries/Geometry_<id>.usda:被多个网格共享的几何描述;
  • materials/...:内嵌于model.usda的材质节点树;
  • textures/Texture_<id>.(png|jpg):导出时由canvas.toBlob重新编码的贴图资源。

归档压缩通过仓库内置的 examples/jsm/libs/fflate.module.js 的zipSync( files, { level: 0 } )完成。源码还针对 fflate 的已知对齐问题,为每个文件头做64 字节对齐(见 源码),保证某些 Apple 工具链能正确解析。由此可见,在浏览器中完成一次「内存 → USDZ」导出是纯客户端计算,不需要任何服务器支持。

二、快速上手:导入与最小示例

USDZExporter位于examples/jsm,是显式导入的附加模块(addon),不会随 three.js 核心包自动加载(项目约定见 examples/jsm/Addons.js 的export * from './exporters/USDZExporter.js')。使用 ES Module 与 import map 时,导入方式为:

import { USDZExporter } from 'three/addons/exporters/USDZExporter.js';

最简导出流程(文档 USDZExporter.html.md 的 Code Example 即此形式):

const exporter = new USDZExporter(); const arraybuffer = await exporter.parseAsync( scene );

parseAsync返回 Promise,resolve 为一个包含完整 USDZ 归档数据的ArrayBuffer。随后即可把它包装成 Blob 供下载或供 AR 链接使用:

const blob = new Blob( [ arraybuffer ], { type: 'application/octet-stream' } ); const link = document.getElementById( 'link' ); link.href = URL.createObjectURL( blob );

官方可运行示例见 examples/misc_exporter_usdz.html:它通过 GLTFLoader(配合 DRACOLoader、KTX2Loader)加载CarbonFrameBike.glb车型模型后执行导出,页面中的<a id="link" rel="ar" download="asset.usdz">会随导出结果更新 href——这正体现了 USDZ 在 iOS 上「点击即进入 AR」的典型用法。该示例还演示了一个容易遗漏的关键点:因为 GLB 模型包含 KTX2 等压缩纹理,所以在导出前必须调用exporter.setTextureUtils( WebGLTextureUtils )(源码注释为// for texture decompresssing)。

三、构造函数与实例属性

new USDZExporter()

构造一个新的 USDZ 导出器,无参数。从源码 examples/jsm/exporters/USDZExporter.js 看,构造器只初始化一个公开属性:

.textureUtils : WebGLTextureUtils | WebGPUTextureUtils

对「纹理工具模块」的引用,默认值为null

它只在需要导出压缩纹理(如 KTX2)时发挥作用。若场景中含有CompressedTexture而该属性仍为nullparseAsync会直接抛出错误:

THREE.USDZExporter: setTextureUtils() must be called to process compressed textures.

(见 源码。)

当使用WebGLRenderer时应注入 examples/jsm/utils/WebGLTextureUtils.js,其中的decompress( texture, maxTextureSize, renderer )通过离屏全屏四边形把压缩纹理渲染并读回为 CanvasTexture;当使用WebGPURenderer时应注入 examples/jsm/utils/WebGPUTextureUtils.js,其decompress为 async 实现。正确的渲染器-工具配对见下表:

渲染器注入的 textureUtils 模块
WebGLRenderer(见 docs/pages/WebGLRenderer.html)examples/jsm/utils/WebGLTextureUtils.js
WebGPURenderer(见 docs/pages/WebGPURenderer.html)examples/jsm/utils/WebGPUTextureUtils.js

若场景纹理全部为常规(非压缩)格式,可完全不设置该属性。

四、导出方法与回调类型

.parse( scene : Object3D, onDone : OnDone, onError : OnError, options : Options )

命令式(回调风格)的导出入口。scene为要导出的 3D 对象(Object3DScene);onDone在导出成功时被调用并接收生成的ArrayBufferonError在出错时被调用并接收Error对象;options为导出选项。从 源码 看,parse本质上只是对parseAsync的包装:

parse( scene, onDone, onError, options ) { this.parseAsync( scene, options ).then( onDone ).catch( onError ); }

.parseAsync( scene : Object3D, options : Options ) : Promise.<ArrayBuffer>

parse的异步版本,返回的 Promise 在导出成功时 resolve 为USDZ 数据(ArrayBuffer。官方示例与所有单元测试都优先使用该方法,属于推荐入口。scene传入Scene实例时,导出器会遍历其全部子节点层级;传入任意Object3D(非Scene)时则会以该对象为根直接构建节点(见 源码)。

.setTextureUtils( utils : WebGLTextureUtils | WebGPUTextureUtils )

设置上文所述.textureUtils属性,用于压缩纹理的解压处理:

exporter.setTextureUtils( WebGLTextureUtils ); // WebGLRenderer 场景 // 或 exporter.setTextureUtils( WebGPUTextureUtils ); // WebGPURenderer 场景

注意官方示例中这两个工具模块以命名空间导入方式使用:import * as WebGLTextureUtils from 'three/addons/utils/WebGLTextureUtils.js',因为模块对外导出的是一组函数而非单个类。

回调类型定义

  • .OnDone( result : ArrayBuffer ):导出完成的回调,result为生成的 USDZ 数据;
  • .OnError( error : Error ):导出失败的回调,error为错误对象;
  • .Options:导出选项对象,详见下一节。

五、导出选项 Options 全解析

Options的定义位于 源码,而运行时默认值parseAsync入口通过Object.assign合并(见 源码)。汇总如下:

选项类型默认值含义
maxTextureSizenumber1024导出贴图允许的最大尺寸(宽或高的上限)
includeAnchoringPropertiesbooleantrue是否写入 AR 锚定属性
onlyVisiblebooleantrue是否只导出可见对象
arObject见下文AR 锚定类型与平面对齐方式配置
quickLookCompatiblebooleanfalse是否针对 Apple QuickLook 的已知缺陷做兼容修正
animationsArray[]需要烘焙为xformOp时间采样的动画片段
animationFrameRatenumber60写入动画采样时使用的每秒时间码数

使用方式示例(来自官方示例 examples/misc_exporter_usdz.html 中对动画模型的实际调用):

const arraybuffer = await exporter.parseAsync( gltf.scene, { animations: gltf.animations, } );

下面逐项深入说明。

5.1maxTextureSize

导出的贴图若尺寸过大,会被等比缩小到不超过该上限。从 imageToCanvas 实现 可见其缩放逻辑:scale = maxTextureSize / Math.max( image.width, image.height ),仅当scale < 1时缩小(即从不放大贴图)。贴图最终经canvas.toBlob编码为 JPEG 或 PNG——编码格式取决于texture.userData.mimeType是否为'image/jpeg'(见 源码),否则一律输出 PNG。过大的默认上限(如原始纹理为 4096)可以通过调低该值来控制最终.usdz的体积。

5.2includeAnchoringPropertiesar

includeAnchoringPropertiestrue(默认)时,导出器会在场景节点上写入两条token属性(见 源码):

token preliminary:anchoring:type = "plane" token preliminary:planeAnchoring:alignment = "horizontal"

这两条属性即 iOS AR Quick Look 放置模型时的「锚定类型」与「平面对齐」提示,其值可通过options.ar覆盖。默认的ar对象为:

ar: { anchoring: { type: 'plane' }, planeAnchoring: { alignment: 'horizontal' }, }

也就是说options.ar.anchoring.typeoptions.ar.planeAnchoring.alignment分别对应上面两条preliminary属性的值。若你希望导出的文件完全不带 AR 语义(例如用于传统 3D 查看器),可设includeAnchoringProperties: false

5.3onlyVisible

默认true,表示跳过场景中visible === false的对象。相关逻辑在 buildNode:if ( object.visible === false && options.onlyVisible === true ) return;,被跳过的对象连同其整棵子树都不会出现在导出结果中。设为false则会连不可见对象一并导出。

这一行为有对应的单元测试佐证(见 test/unit/addons/exporters/USDZExporter.tests.js):测试构造两个 Box,其中一个visible = false;当onlyVisible: true时解压出的model.usda文本包含box1而不包含box2,当onlyVisible: false时两者都被包含。

5.4quickLookCompatible

默认false。置为true时,导出器会针对 Apple QuickLook 对贴图 repeat/offset 的解析缺陷修正纹理变换数学。源码注释明确指出了对应问题编号:Apple FeedbackFB10036297FB11442287(见 源码)。

具体差异在纹理 offset 修正逻辑上:

  • 非兼容模式:offset.x += sin(rotation) * repeat.x; offset.y += (1 - cos(rotation)) * repeat.y,注释说明其结果与 glTF 导出完全一致,并已在 usdview 中验证正确;
  • 兼容模式:offset.x = offset.x / repeat.x; offset.y = offset.y / repeat.y后再叠加旋转项,注释同时承认:This is NOT correct yet in QuickLook, but comes close for a range of models. It becomes more incorrect the bigger the offset is

如果你的主要目标是 iOS Quick Look,且模型在预览中出现贴图错位,可尝试开启此项;否则保持默认即可获得与 glTF 一致的纹理表现。

5.5animationsanimationFrameRate

animations接受一个AnimationClip数组(AnimationClip的 API 见 docs/pages/AnimationClip.html)。传入后,导出器会把相关动画烘焙为 USD 的xformOp时间采样,使导出的 USDZ 在支持的查看器/AR 中具备动画能力。对动画的具体约束如下(见 buildAnimationTracks):

  • 只导出轨道的positionquaternionscale三种属性,其他轨道(如骨骼、形态键)会被直接忽略;
  • 通过PropertyBinding.parseTrackName解析轨道名并调用PropertyBinding.findNode( scene, binding.nodeName )定位被动画的对象,找不到目标对象(null/undefined)的轨道会跳过;
  • 有动画的对象在写入时采用per-op 布局xformOp:translatexformOp:orientxformOp:scale分别写成timeSamples;无动画的属性仍保持静态值(见 addTransformProperties);
  • 需要注意四元数分量顺序的转换:three.js 中四元数存储为(x, y, z, w),而 USDquatf要求(w, x, y, z),源码在 buildQuaternionTimeSamples 显式处理了这一重排。

animationFrameRate(默认60)表示每秒时间码数(time codes per second)。写入的采样时间值为times[i] * fps(见 buildVector3TimeSamples)。当存在动画时,USD 头部还会附加时间轴元数据(见 buildHeader):

startTimeCode = 0 endTimeCode = <最长片段时长 × fps> timeCodesPerSecond = 60 framesPerSecond = 60

其中endTimeCodegetMaxClipDuration( options.animations ) * options.animationFrameRate计算(取所有片段duration的最大值)。

六、导出过程的内部结构与原理

了解内部输出结构有助于排查「导出结果与预期不符」的问题。从 parseAsync 的主流程看,导出分为以下阶段:

  1. 合并默认选项,并初始化空文件表与model.usda(置首);

  2. 收集动画轨道buildAnimationTracks

  3. 构建 USD 节点树:导出器用内部类USDNode把场景层级映射为 USD 语法(USDNode.toString()负责缩进排版与def/metadata/properties 的序列化,见 源码)。节点树固定为三层骨架:

    def Xform "Root" └─ def Scope "Scenes" (kind = "sceneLibrary") └─ def Xform "Scene" (含 anchoring 属性;子节点依次写入)
  4. 遍历场景层级buildHierarchy/buildNode):Mesh走几何与材质收集分支,CamerabuildCamera,其余对象一律作为Xform(见 源码)。每个对象的名字会先经过 getName 清洗:剔除非法字符、数字开头补下划线、空名回退为Camera/Object,重名自动追加_<object.id>以保持 USD 命名唯一;

  5. 生成几何子文件与材质节点buildMaterials);

  6. 编码纹理:逐一将texture.image通过imageToCanvas拷入画布,必要时flipY翻转(three.js 纹理坐标系与 USD 相反,导出时通过context.translate+context.scale(1, -1)修正),再toBlob写入归档;若纹理带isCompressedTexture标记则先调用.textureUtils.decompress,否则抛错;

  7. 序列化全部内容并 ZIP 打包(含 64 字节对齐),返回ArrayBuffer

坐标系与单位约定

生成的model.usda头部(buildHeader)固定声明:

#usda 1.0 ( customLayerData = { string creator = "Three.js USDZExporter" } defaultPrim = "Root" metersPerUnit = 1 upAxis = "Y" )

Y 轴向上单位米,默认图元(defaultPrim)为Root。因此导出时需注意你的 three.js 场景本身是否符合该轴约定(three.js 默认即 Y-up)。对象变换默认被折叠为一个matrix4d xformOp:transform;一旦对象带动画或带 pivot,则改用translate/orient/scale的 per-op 表达(必要时含xformOp:translate:pivot!invert!逆操作,见 addTransformProperties)。

网格与几何

几何以Mesh类型写入(见 buildMeshNode),包含:

  • int[] faceVertexCountsint[] faceVertexIndices(三角面,有无 index 均会被摊平/直写为三角形列表);
  • point3f[] pointsnormal3f[] normals(缺失 normal 时警告并填(0, 0, 0));
  • UV 集:primvars:stprimvars:st1st3(最多支持 4 套 UV);
  • 顶点色:color3f[] primvars:displayColor
  • uniform token subdivisionScheme = "none"(关闭细分)。

多材质网格(材质为数组或存在geometry.groups),导出器生成GeomSubset节点,通过familyName = "materialBind"elementType = "face"与面索引区间把不同面绑定到不同材质(见 buildMeshNode)。单材质网格则以rel material:binding = </Materials/Material_<id>>绑定,并复用按geometry.id去重的几何子文件。

材质与贴图

材质按 USD 的UsdPreviewSurface提议(源码注释引用的 Pixar 规范)导出。每份材质被写入为:

def Material "Material_<material.id>" token outputs:surface.connect = </Materials/Material_<id>/PreviewSurface.outputs:surface> def Shader "PreviewSurface" (info:id = "UsdPreviewSurface") ...

导出的贴图通道与PreviewSurface输入的对应关系在 buildMaterial 中实现,基本对应关系如下表:

three.js 材质槽位UsdPreviewSurface 输入连接通道
mapdiffuseColor/opacity贴图 rgb / a
emissiveMap/emissiveemissiveColorrgb(乘以emissiveIntensity
normalMapnormalrgb(scale/biasnormalScale.x换算)
aoMapocclusionr(乘以aoMapIntensity
roughnessMaproughnessg
metalnessMapmetallicb
alphaMapopacity+opacityThresholdr

未连接贴图的纯色属性则写为静态值,例如color3f inputs:diffuseColor = (r, g, b)float inputs:roughnessfloat inputs:metallicfloat inputs:opacity。对MeshPhysicalMaterial额外导出clearcoatclearcoatRoughnessioruseSpecularWorkflow固定写为0。贴图还会编码inputs:sourceColorSpacetexture.colorSpace === NoColorSpace时为raw,否则sRGB)以及wrapS/wrapTRepeatWrappingrepeatClampToEdgeWrappingclampMirroredRepeatWrappingmirror,见 buildTextureNodes)。

每张贴图在 USD 中并不是一个孤立的UsdUVTexture着色器,而是一条三节点链:

Shader "PrimvarReader_<map>" (UsdPrimvarReader_float2, varname = st/stN) Shader "Transform2d_<map>" (UsdTransform2d:rotation/scale/translation) Shader "Texture_<id>_<map>" (UsdUVTexture:inputs:file、inputs:st 连接上游)

其中Transform2d_<map>承担 three.js 纹理rotationrepeatoffset到 USD UV 变换的换算。注意 two.js 的 UV 起点约定不同:导出时 UV 的 V 分量会被1 - y翻转(见 buildVector2Array),并在 transform 2d 中修正offset.y = 1 - offset.y - repeat.y(见 buildTextureNodes)。

相机

场景中的相机对象会作为Camera节点导出(见 buildCamera),写入投影类型(perspective/orthographic)、clippingRangehorizontalAperture/verticalAperture(透视相机取自getFilmWidth/getFilmHeight,正交相机由视口范围换算),透视相机额外写focalLengthgetFocalLength)与focusDistance

七、受支持的类型与已知限制

结合源码中的显式警告路径,导出器对以下情况会打印console.warn(而非中断):

  • MeshStandardMaterial材质buildNode会提示USDZExporter: Use MeshStandardMaterial for best results.(见 源码)——因为材质导出逻辑面向MeshStandardMaterial/MeshPhysicalMaterial的槽位体系编写;
  • 双面材质material.side === DoubleSide时提示USDZ does not support double sided materials(见 源码);
  • 负缩放matrix.determinant() < 0时提示USDZ does not support negative scales(出现在buildXformbuildCamera,分别见 源码 与 源码);
  • 缺少法线buildVector3Array对缺失的 normal 属性填充(0, 0, 0)并警告(见 源码)。

另外需要重申的两个硬性条件:压缩纹理必须先setTextureUtils(),否则直接抛错;若传入了含压缩纹理的场景却不满足该条件,导出将失败。

八、测试与验证方法

仓库的 QUnit 单元测试位于 test/unit/addons/exporters/USDZExporter.tests.js,覆盖了导出器的主要契约,可作为验证与回归参考:

  1. API 形状:断言实例可构造且parseparseAsyncsetTextureUtils均为函数;
  2. 基础导出:对一个Scene(含绿色MeshStandardMaterial立方体)执行parseAsync,断言结果ArrayBuffer非空,并用unzipSync解包验证归档第一个文件model.usda且其首行为#usda 1.0
  3. onlyVisible行为:如上文所述,验证不可见网格在truefalse下的包含/排除差异(直接断言model.usda文本中是否出现对象名);
  4. 导出-导入回环:把立方体与球体导出后,用USDLoaderparse重新载入(测试导入路径 examples/jsm/loaders/USDLoader.js),断言对象名、position/scale/rotation、几何存在性以及材质的color/roughness/metalness均在 1e-7 容差内保持。

若想快速复现整条流程,推荐两个入口:运行上述单元测试检查导出正确性;或直接在浏览器打开官方示例 examples/misc_exporter_usdz.html 下载asset.usdz,在 iOS 设备上用 AR Quick Look 打开验证锚定与动画效果。对动画模型(如 GLB 自带动画片段),示例代码{ animations: gltf.animations }即为把AnimationClip数组接入导出器的标准写法。

九、小结

USDZExporter以约 1500 行的纯前端实现,把 three.js 对象层级、MeshStandardMaterial/MeshPhysicalMaterial材质体系与AnimationClip动画完整翻译为 USD 语法并打包为 Apple 生态可消费的 USDZ。把握住三条主线即可高效使用它:其一,按渲染器正确注入setTextureUtils以应对压缩纹理;其二,借助Options精确控制 AR 锚定属性(includeAnchoringProperties/ar)、可见性过滤(onlyVisible)、贴图体积(maxTextureSize)、QuickLook 兼容性以及动画(animations/animationFrameRate);其三,理解其「model.usda首位 + 几何子文件 + 纹理资源 + 64 字节对齐」的归档组织,以及 Y-up、米制、UsdPreviewSurface+ UV 纹理链等底层输出约定,以便在导出结果异常时快速定位是场景准备问题还是渲染目标兼容性问题。

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 12:36:56

EV2400刷成MSP430仿真器:固件烧录与调试实战指南

简介&#xff1a;面向MSP430F5529/5528等新型号MCU的EV2400固件刷写资源包&#xff0c;内置三种大小不同但功能一致的固件&#xff0c;最大版本适配F5529&#xff0c;最小版本适配F5528&#xff0c;并附带一块经测试的MSP430F5528开发板PCB文件。开发者可使用UNIFLASH配合EZFET…

作者头像 李华
网站建设 2026/9/9 12:36:53

热流平衡如何决定核聚变等离子体的密度天花板

逼近密度极限时等离子体为何会“漏水”&#xff1a;热流平衡如何决定核聚变的密度天花板在托卡马克装置上泡了这么多年实验&#xff0c;有一个现象我每次看都觉得很微妙&#xff1a;你小心翼翼地往等离子体里加燃料&#xff0c;密度一点一点爬升&#xff0c;眼看着离Greenwald密…

作者头像 李华
网站建设 2026/9/9 12:35:41

Rust轻量级流式编排引擎ruflo:核心抽象、背压机制与实战解析

前几天凌晨两点&#xff0c;线上群突然炸了。Kafka 的 lag 一路飙升&#xff0c;消费者明明在跑&#xff0c;消息就是消费不进去。我盯着监控面板看了半天&#xff0c;最后定位到问题出在流处理框架的配置上——一个字段类型写错了&#xff0c;整个拓扑直接卡死&#xff0c;既没…

作者头像 李华
网站建设 2026/9/9 12:34:03

杭州哪里有上门回收旧电脑?笔记本一体机回收流程

家里闲置的旧笔记本、一体机占地方又没用&#xff0c;想出手却不知道杭州哪里有能上门回收的商家&#xff0c;也不清楚完整的回收流程是怎样的&#xff0c;会不会要自己扛着电脑跑门店、会不会被压价、数据会不会泄露。2026 年杭州本地实体回收品牌万修电脑&#xff0c;提供全品…

作者头像 李华
网站建设 2026/9/9 12:33:03

代码化图表设计:用Mermaid把架构图变成可维护的软件资产

最近在帮团队梳理技术文档体系&#xff0c;我发现一个很有意思的现象&#xff1a;大家宁愿写一大段文字来描述模块间的调用关系&#xff0c;也不愿意画一张图。问了一圈&#xff0c;理由出奇一致——“画图太麻烦了&#xff0c;调整对齐就要半天”。但文档里的架构描述一旦超过…

作者头像 李华