ARTICLE DETAIL

资讯详情

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

three.js ColladaParser 深度解析:从 XML 到中间数据结构的 Collada 解析管线

three.js ColladaParser 深度解析:从 XML 到中间数据结构的 Collada 解析管线 three.js ColladaParser 深度解析从 XML 到中间数据结构的 Collada 解析管线【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js本文以 three.js 仓库中 ColladaParser 的 API 文档为骨架结合源码实现完整讲解这个类在 Collada.dae加载链路中的职责它如何用 DOMParser 解析 Collada XML如何把library_*各资源库逐一转换成与渲染无关的中间数据结构intermediate data structures以及这些结构如何被 ColladaComposer 消费、最终组装成 three.js 的场景图。读完本文你将掌握new ColladaParser()的完整调用方式、parse()返回对象的字段含义、每一类 Collada 库动画、几何、节点、材质、运动学、物理的解析规则以及解析阶段与合成阶段的边界划分。1. ColladaParser 在 ColladaLoader 中的定位three.js 对 Collada 格式的加载被拆成清晰的三步下载ColladaLoader.load() 通过FileLoader拉取.dae文件文本解析本文主角ColladaParser.parse(text)把 XML 文本转成中间数据结构合成ColladaComposer.compose() 读取中间结构构建Group、Mesh、材质、AnimationClip等 three.js 对象。这种拆分在 ColladaLoader.parse() 中体现得最明显parse( text, path ) { if ( text.length 0 ) { return { scene: new Scene() }; } // Parse XML to library data const parser new ColladaParser(); const parseResult parser.parse( text ); if ( parseResult null ) { return null; } const { library, asset, collada } parseResult; // Setup texture loaders const textureLoader new TextureLoader( this.manager ); textureLoader.setPath( this.resourcePath || path ).setCrossOrigin( this.crossOrigin ); // Compose Three.js objects from library data const composer new ColladaComposer( library, collada, textureLoader, tgaLoader ); const { scene, animations, kinematics } composer.compose(); scene.animations animations; // Handle coordinate system conversion if ( asset.upAxis Z_UP ) { scene.rotation.set( - Math.PI / 2, 0, 0 ); } // Apply unit scale scene.scale.multiplyScalar( asset.unit ); return { kinematics: kinematics, library: library, scene: scene }; }从源码结构看ColladaParser只依赖Color、ColorManagement、MathUtils、Matrix4、Vector3等数学/颜色工具见 ColladaParser.js 顶部 import它不创建任何可渲染对象——这正是文档中 converts Collada XML to intermediate data structures 的工程含义解析层只负责把 XML 读懂合成层负责把数据画出来。2. 构造函数与实例状态API 文档给出的构造签名非常简单const parser new ColladaParser();对应实现见 constructor()constructor() { this.count 0; } generateId() { return three_default_ ( this.count ); }构造函数只初始化一个自增计数器count配合generateId()为缺失id属性的node元素生成three_default_N形式的兜底 ID。这个机制在prepareNodes()中被调用见第 6 节的节点解析——Collada 规范里id并非所有节点属性但 three.js 的节点库以id作为字典键因此解析前会先统一补齐。parse()成功执行后实例上还会挂两个额外字段供后续或调试时访问this.library15 个资源库构成的字典见第 3 节this.collada指向 XML 中的COLLADA根元素本身。3. parse()入口流程与返回的中间结构3.1 XML 解析与错误处理parse(text) 的流程如下parse( text ) { if ( text.length 0 ) { return null; } const xml new DOMParser().parseFromString( text, application/xml ); const collada getElementsByTagName( xml, COLLADA )[ 0 ]; const parserError xml.getElementsByTagName( parsererror )[ 0 ]; if ( parserError ! undefined ) { // ...提取错误文本console.error 后 return null } const version collada.getAttribute( version ); console.debug( THREE.ColladaLoader: File version, version ); const asset this.parseAsset( getElementsByTagName( collada, asset )[ 0 ] ); // ...逐库调用 parseLibrary(...) return { library: library, asset: asset, collada: collada }; }值得注意的三个细节非递归的getElementsByTagName文件顶部自定义了 getElementsByTagName()只扫描直接子节点xml.childNodes这与浏览器原生递归版行为不同。用它取COLLADA的根级段落library_*、asset能避免误匹配到子元素内部同名的标签。解析失败返回null空文本返回nullDOMParser 报parsererror时会先把错误转成可读文本Chrome 会把错误包在div里见 parserErrorToText() 的兼容处理console.error输出后再返回null。上游ColladaLoader.parse()收到null即终止并返回null。版本号只记录不校验COLLADA version...属性仅做console.debug解析逻辑本身与版本弱相关。3.2 asset单位与上方向parseAsset()产出被上层用于场景级变换的数据parseAsset( xml ) { return { unit: this.parseAssetUnit( getElementsByTagName( xml, unit )[ 0 ] ), upAxis: this.parseAssetUpAxis( getElementsByTagName( xml, up_axis )[ 0 ] ) }; }unit读取assetunit meter...缺省为1米upAxis读取up_axis文本缺省Y_UP。这两个值随后在 ColladaLoader.parse() 中生效Z_UP资源被整体旋转-π/2绕 X 轴仅旋转场景根节点顶点数据不转换源码中会打印警告unit作为缩放因子乘到scene.scale上。3.3 library15 个资源库的解析表parse()内部按固定顺序调用 15 次parseLibrary()把COLLADA下各段落灌入一个统一的library字典段落元素解析方法存入library_animationsanimationparseAnimationlibrary.animationslibrary_animation_clipsanimation_clipparseAnimationCliplibrary.clipslibrary_controllerscontrollerparseControllerlibrary.controllerslibrary_imagesimageparseImagelibrary.imageslibrary_effectseffectparseEffectlibrary.effectslibrary_materialsmaterialparseMateriallibrary.materialslibrary_camerascameraparseCameralibrary.cameraslibrary_lightslightparseLightlibrary.lightslibrary_geometriesgeometryparseGeometrylibrary.geometrieslibrary_nodesnodeparseNodelibrary.nodeslibrary_visual_scenesvisual_sceneparseVisualScenelibrary.visualSceneslibrary_jointsjointparseLibraryJointlibrary.jointslibrary_kinematics_modelskinematics_modelparseKinematicsModellibrary.kinematicsModelslibrary_physics_modelsphysics_modelparsePhysicsModellibrary.physicsModelsscene内的instance_kinematics_scene—parseKinematicsScenelibrary.kinematicsScenesparseLibrary() 是一个通用的段落驱动入口取COLLADA下第一个指定段落节点遍历其同名子元素并逐一交给对应解析方法。段落不存在时静默跳过——这意味着一个.dae文件可以只含其中一部分库解析依然成立。parse()最终返回return { library: library, asset: asset, collada: collada };4. 动画相关source / sampler / channel 三要素4.1 parseAnimation 与缺失 id 的 UUID 兜底parseAnimation() 递归处理animation树把三个子结构分别存入data.sources / data.samplers / data.channelssource id...→parseSource()得到的数值流sampler id...→ 记录其input的semantic → source id映射channel target...→ 记录通道寻址信息。一个容易忽略的健壮性处理叶子animation的id属性在规范中可选解析器在id缺失时用MathUtils.generateUUID()生成键值if ( hasChildren false ) { // since id attributes can be optional, its necessary to generate a UUID for unique assignment this.library.animations[ xml.getAttribute( id ) || MathUtils.generateUUID() ] data; }4.2 SID 寻址语法channel target 的完整解析parseAnimationChannel() 解析 Collada 的 SID Addressing Syntaxtarget形如nodeId/sid[/path].member或nodeId/sid(i)解析器会区分两种语法let parts target.split( / ); const id parts.shift(); // 目标节点 id let sid parts.shift(); // 目标节点内的 sid const arraySyntax ( sid.indexOf( ( ) ! - 1 ); // 数组访问 sid(0) const memberSyntax ( sid.indexOf( . ) ! - 1 ); // 成员访问 sid.x if ( memberSyntax ) { // member selection access parts sid.split( . ); sid parts.shift(); data.member parts.shift(); } else if ( arraySyntax ) { // array-access syntax表达一维向量或二维矩阵中的字段 const indices sid.split( ( ); sid indices.shift(); for ( let i 0; i indices.length; i ) { indices[ i ] parseInt( indices[ i ].replace( /\)/, ) ); } data.indices indices; } data.id id; data.sid sid; data.arraySyntax arraySyntax; data.memberSyntax memberSyntax; data.sampler parseId( xml.getAttribute( source ) );产出的通道描述对象包含id、sid、member如x/rotation、indices如[2, 3]、arraySyntax/memberSyntax标志和sampler引用合成器随后据此决定写的是位置的哪个分量还是矩阵的哪个元素。4.3 animation_clipparseAnimationClip() 记录片段的name缺省default、start/end时间缺省 0以及instance_animation引用的动画 id 列表存入library.clips。Composer 侧若无 clip 而存在动画会合成一个default播放片段见 ColladaComposer 约 L2811 的兜底逻辑。5. 材质链路image / effect / material 三层间接引用Collada 的贴图引用是三层间接的解析器忠实保留了这条链imageparseImage() 只记录init_from中的文件路径文本parseImage( xml ) { const data { init_from: getElementsByTagName( xml, init_from )[ 0 ].textContent }; this.library.images[ xml.getAttribute( id ) ] data; }effectparseEffect() 解析profile_COMMON其中newparam→parseEffectNewparamsurface通过init_from指回 imageparseEffectSurfacesampler2D记录sourceparseEffectSamplertechnique→ 识别constant/lambert/blinn/phong四种类型并用 parseEffectParameters() 提取emission、diffuse、specular、bump、ambient、shininess、transparency参数以及带opaque属性的transparent项缺省A_ONEextra→ 提取扩展项如double_sidedparseEffectExtraTechnique。纹理采样参数parseEffectParameterTextureExtraTechnique() 从extra/technique中读取repeatU/repeatV/offsetU/offsetV浮点与wrapU/wrapV含TRUE/FALSE文本的兼容解析这些值最终映射到 three.js 纹理的repeat/offset/wrapS/wrapT。materialparseMaterial() 记录name与instance_effect url#...指向的 effect id。颜色参数在解析阶段就完成了色彩空间处理——parseLightParameters 中对color使用ColorManagement.colorSpaceToWorking( data.color, SRGBColorSpace )转成工作色空间材质路径中的color则以浮点数组parseEffectParameter 的parseFloats形式保留由 Composer 在建材质时转换。6. 节点与几何场景树与三角形数据的中间表示6.1 parseNode变换累积与实例引用parseNode() 是解析器中最复杂的单一方法。它产出的每个节点数据结构包含name、type、id、sid、累积的matrixMatrix4、子节点 id 列表nodes以及六类实例引用数组instanceCameras、instanceControllers、instanceLights、instanceGeometries、instanceNodes加上动画可寻址的变换簿记transforms/transformData/transformOrder。四类变换子元素的处理规则各不相同子元素对data.matrix的影响transformData记录matrixmatrix.multiply(fromArray(...).transpose())Collada 行主序故转置{ type: matrix, array }translatemultiply(makeTranslation(x,y,z)){ type: translate, x, y, z }rotatemultiply(makeRotationAxis(axis, degToRad(angle))){ type: rotate, axis, angle }scalematrix.scale(...)非乘法链直接缩放{ type: scale, x, y, z }同时每个变换都会以sid为键登记进transforms并追加到transformOrder——这是动画通道targetid/sid寻址能够落回具体变换的依据。instance_geometry和instance_controller还会调用 parseNodeInstance()解析其中的bind_material → instance_materialsymbol → target材质映射表和skeleton骨骼引用。健壮性上有两处保护重复 id 告警hasNode(data.id)为真时打印There is already a node with ID ...并放弃入库避免后写覆盖先写缺 id 补齐prepareNodes() 在解析视觉场景前遍历全部node元素用generateId()为缺失id者补上three_default_N。6.2 parseGeometry / parseSource / parseGeometryPrimitiveparseGeometry() 要求geometry内含meshconvex_mesh、spline、brep尚不支持源码注释引用了相关 PR产出结构为const data { name: xml.getAttribute( name ), sources: {}, // source id - parseSource() 结果 vertices: {}, // semantic - source id primitives: [] // 原始图元列表 };parseSource() 把source中的数值流拍平float_array经parseFloats转成number[]Name_array转成字符串数组并读取technique_common/accessor的stride缺省 3。注意它不做步长切片保留完整扁平数组与 stride供 Composer 在构建BufferAttribute时按步长索引。parseGeometryPrimitive() 支持polygons、lines、linestrips、polylist、triangles五类图元记录materialmaterial属性、count、inputssemantic(set) → { id, offset }、stride取最大offset 1、hasUV存在TEXCOORD语义时置真vcount仅polylist/polygons与p索引流parseInts对polygons无vcount会按p.length / stride推断出单一vcount。6.3 骨骼与蒙皮parseSkin / parseJoints / parseVertexWeightsparseController() 区分skin完整解析与morph当前版本仅告警Morph target animation not supported yet不解析数据。parseSkin() 收集bind_shape_matrixparseFloats后的 16 元素数组sources全部source含关节 id 列表、INV_BIND、权重等jointsparseJoints与vertex_weightsparseVertexWeights两者都是inputssemantic → { id, offset }加索引流——vcount与v。Composer 消费这份数据构建SkinnedMesh例如仓库示例 examples/models/collada/elf/ 与webgl_loader_collada_skinning一类的蒙皮演示即走这条链路。6.4 相机与灯光相机parseCamera() →optics → technique_common → perspective/orthographicparseCameraParameters() 提取xfov/yfov/xmag/ymag/znear/zfar/aspect_ratio。灯光parseLight() 支持directional/point/spot/ambient四类parseLightParameters() 中有一个典型的解析期换算——quadratic_attenuation会被转换为物理意义上的距离data.distance f ? Math.sqrt( 1 / f ) : 0颜色同样做 sRGB→工作色空间转换。7. 运动学与物理kinematics 数据链这部分数据链贯穿三个库解析器把它们拆成四个环节parseLibraryJoint()joint以 id 入library.jointsparseKinematicsJointParameter()解析prismatic/revolute关节产出sid、axisVector3、limits.min/max并做两条推导——min max时标记static true关节被锁死以及计算middlePosition (min max) / 2供 UI/动画初始位置使用parseKinematicsModel() 与 parseKinematicsLink()technique_common下收集joint/instance_joint引用 1 中的库级关节与linklink 内含attachment_full附件子树与matrix/translate/rotate变换parseKinematicsTransformrotate的角度同样做degToRadmatrix做转置入库parseKinematicsScene()解析顶层scene中的instance_kinematics_scene其中 parseKinematicsBindJointAxis() 提取bind_joint_axis的target与axisparam文本并用字符串切割split(inst_).pop().split(axis)[0]反推出jointIndex——这是针对典型导出器命名惯例的启发式解析属于从源码结构看较为脆弱的部分依赖inst_N_axis形式的 param 文本。parsePhysicsModel() 则是轻量实现遍历rigid_body从technique_common中提取inertia浮点数组与mass取首个浮点数存入library.physicsModels。8. 视觉场景与库间引用关系parseVisualScene() 先把场景内节点 id 补齐prepareNodes再逐个parseNode()顶层node存入library.visualScenes[id] { name, children }。至此整张引用网成型visual_scene的节点通过instance_node指向library.nodes节点的instance_geometry→library.geometries内含sources/vertices/primitives节点的instance_controller→library.controllers内含 skin 的 joints/weights/sourcesbind_material的symbol → target映射 →library.materials→library.effects→library.images文件路径。这些字符串引用在解析阶段全部保留原样不解析延迟到 Composer 的compose()中按 id 查表组装——解析器与合成器通过library这份纯数据字典解耦ColladaComposer的构造签名( library, collada, textureLoader, tgaLoader )印证了这一契约见 ColladaLoader.parse()。9. 使用方式与实用示例9.1 直接调用解析器ColladaParser以 ES 模块导出文件末尾export { ColladaParser, getElementsByTagName, parseStrings, parseFloats, parseInts, parseId }工具函数也一并导出可用于自定义工具链import { ColladaParser } from three/addons/loaders/collada/ColladaParser.js; import { readFile } from node:fs/promises; // 或任何文本来源 const text await readFile( ./examples/models/collada/pump/pump.dae, utf8 ); const parser new ColladaParser(); const result parser.parse( text ); if ( result null ) { throw new Error( Collada XML 解析失败详见 console.error 输出 ); } const { library, asset, collada } result; console.log( library.geometries ); // { [id]: { name, sources, vertices, primitives } } console.log( asset ); // { unit, upAxis }仓库内的测试资产可用于本地验证如 examples/models/collada/pump/、Cobra_s350.dae、abb_irb52_7_120.dae工业机器人含运动学数据、skin_and_morph.dae等。9.2 常规路径经 ColladaLoader 加载绝大多数场景应直接使用ColladaLoader解析器在其内部自动完成import { ColladaLoader } from three/addons/loaders/ColladaLoader.js; const loader new ColladaLoader(); const result await loader.loadAsync( ./models/collada/elf/elf.dae ); scene.add( result.scene ); // 动画挂在 scene.animations 上result.animations 为兼容访问器会告警 const mixer new THREE.AnimationMixer( result.scene ); mixer.clipAction( result.scene.animations[ 0 ] ).play(); // 工业机器人等资产可读取 result.kinematics 驱动关节需要注意的兼容性事实均见 ColladaLoader.js 的类注释该加载器只支持 Collada 1.5 规范的一个子集Z-UP 资产通过旋转场景根节点转为 Y-UP顶点数据本身不转换空输入返回一个空Scene包装结果而ColladaParser.parse()对空文本/坏 XML 返回null——两者语义不同直接在解析器层调试时以null判断失败。10. 小结解析层的边界与可复用性ColladaParser的设计可以概括为三条边界原则XML 只读、数据只存所有parse*方法只做 DOM 读取与纯数据组装唯一的副作用是prepareNodes为缺 id 节点补写属性以及向this.library各库字典赋值引用延迟解析#id形式的跨库引用在解析期原样保留由 Composer 统一查表避免了解析顺序依赖数值换算前置角度degToRad、行列主序转置、quadratic_attenuation → distance、色彩空间转换等在解析期完成让中间结构里的数值即取即用。对想扩展 Collada 支持的开发者正确的切入点是在ColladaParser中补齐缺失的段落解析如morph控制器、convex_mesh并同步在 ColladaComposer 中消费新的中间结构而若只是想读取资产内容做检查工具直接复用parse()返回的library字典即可无需触碰合成层。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表