首页
/ three.js NodeObjectLoader 深度解析:用 JSON 场景格式加载 TSL 节点材质对象

three.js NodeObjectLoader 深度解析:用 JSON 场景格式加载 TSL 节点材质对象

2026-09-07 14:11:42作者:劳婵绚Shirley

NodeObjectLoader 是 three.js 中专门用于加载「以节点材质(Node Material)描述」的 3D 对象的加载器,继承自 ObjectLoader。在采用 TSL(Three Shading Language)工作流的项目中,对象、几何体与材质可以被序列化为 three.js 标准 JSON Object/Scene 格式;其中普通材质由 MaterialLoader 处理,而任何包含 TSL 节点的 NodeMaterial 则必须经由 NodeObjectLoader 还原为可编程着色器节点树。读完本文,你将理解该加载器的完整 API(构造、属性、parse/parseAsync/parseMaterials/parseNodes/setNodes/setNodeMaterials)、JSON 中节点与材质的序列化结构,以及它如何与 NodeLoaderNodeMaterialLoader 协作完成一次完整的反序列化。

定位与继承体系

按照官方 API 文档(docs/pages/NodeObjectLoader.html.md)的定义:

Inheritance: Loader → ObjectLoader → NodeObjectLoader

A special type of object loader for loading 3D objects using node materials.

从源码结构看,NodeObjectLoader 的类声明为 class NodeObjectLoader extends ObjectLoader,构造函数接收一个可选的 LoadingManager 引用并转发给父类:

import { NodeObjectLoader } from 'three/webgpu';

// 基本用法:加载包含节点材质的 JSON 场景
const loader = new NodeObjectLoader();
const object = await loader.loadAsync( 'models/scene.json' );
scene.add( object );

// 或解析一个已获取的 JSON 对象
const json = await ( await fetch( 'models/scene.json' ) ).json();
const object = await loader.parseAsync( json );

loadAsync 等方法继承自 Loader / ObjectLoader,真正体现节点语义的部分在于下面介绍的属性与 parse 系列方法。该加载器通过 Three.WebGPU.Nodes.jsThree.WebGPU.js 统一导出(export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js'),因此使用 WebGPU 入口引入 three/webgpu 时即可直接使用。

核心属性:节点类型字典

文档定义了两个属性,其默认值均为空对象(NodeObjectLoader.js#L23-L35):

属性 类型 含义
.nodes Object.<string, Node.constructor> 节点类型字典,键为节点类名(type),值为节点类构造器
.nodeMaterials Object.<string, NodeMaterial.constructor> 节点材质类型字典,键为材质类名,值为材质类构造器

这两个字典是加载器的「类型注册表」:JSON 里每个节点/材质条目都带有 type 字段(即类名),解析时用它反查构造器并 new 出对应实例。若某个 type 不在字典中,NodeLoader.createNodeFromType 会打印 NodeLoader: Node type not found 错误并回退为一个 float() 节点占位,使解析过程不至于整体中断。

构造字典的推荐方式是通过配套的 TSL 节点库(three/tsl 导出的 nodes 字典)整体注入,例如:

import { nodes, nodeMaterials } from 'three/tsl';

const loader = new NodeObjectLoader();
loader.setNodes( nodes )
      .setNodeMaterials( nodeMaterials );

从源码结构看,.nodes 字典会被透传给内部 NodeLoaderloader.setNodes( this.nodes )),而 .nodeMaterials 字典则透传给内部 NodeMaterialLoaderloader.setNodeMaterials( this.nodeMaterials )),两处注册表各司其职:前者负责还原节点实例,后者负责区分「节点材质」与「经典材质」。

parse / parseAsync:解析入口与节点上下文桥接

文档对两个解析入口的定义为:

  • .parse( json, onLoad ) : Object3D —— 从给定 JSON 解析节点对象,json 为 JSON 定义,onLoad 为回调函数,覆盖 ObjectLoader#parse
  • .parseAsync( json ) : Promise.<Object3D> —— parse 的异步版本,覆盖 ObjectLoader#parseAsync

实现(NodeObjectLoader.js#L80-L108)非常精简,其核心是一个私有的 _nodesJSON 桥接字段:

parse( json, onLoad ) {

	this._nodesJSON = json.nodes;

	const data = super.parse( json, onLoad );

	this._nodesJSON = null; // dispose

	return data;

}

这段代码揭示了整个加载流程的关键设计:

  1. 暂存节点库:JSON 顶层的 nodes 数组(全部节点定义)被先存入私有成员 _nodesJSON
  2. 走父类流程super.parse()ObjectLoader 的标准管线依次解析 animations、shapes、geometries、images、textures,然后调用 this.parseMaterials( json.materials, textures ) 解析材质——注意此处发生多态分派,实际执行的是 NodeObjectLoader 重写的 parseMaterials
  3. 及时释放:解析完成后将 _nodesJSON 置回 null,避免对大型 JSON 的引用滞留。

也就是说,parse 本身不直接触碰节点树,它的职责是建立「节点 JSON → 材质解析」的上下文通道。父类 ObjectLoader.parse 中的调用顺序 animations → shapes → geometries → images → textures → materials → object → skeletons 在这里被完整继承,节点材质的解析恰好插入在 textures 之后、object 组装之前——因为材质的 inputNodes 引用的纹理对象必须先于材质存在。

parseMaterials:节点库先行,逐材质实例化

文档定义:

.parseMaterials( json, textures ) : Object.<string, NodeMaterial> —— Parses the node objects from the given JSON and textures. json: The JSON definition. textures: The texture library.

实现(NodeObjectLoader.js#L140-L165):

parseMaterials( json, textures ) {

	const materials = {};

	if ( json !== undefined ) {

		const nodes = this.parseNodes( this._nodesJSON, textures );

		const loader = new NodeMaterialLoader();
		loader.setTextures( textures );
		loader.setNodes( nodes );
		loader.setNodeMaterials( this.nodeMaterials );

		for ( let i = 0, l = json.length; i < l; i ++ ) {

			const data = json[ i ];

			materials[ data.uuid ] = loader.parse( data );

		}

	}

	return materials;

}

执行逻辑可以拆成三步:

  1. 先把整个 nodes 库还原成实例字典{ uuid: Node }),这是后续所有材质 inputNodes 引用的查找表;
  2. 配置 NodeMaterialLoader:注入纹理库、刚解析出的节点实例库、以及构造期通过 setNodeMaterials 注册的材质类字典;
  3. 遍历 json.materials 数组,逐条调用 loader.parse( data ) 并以 uuid 为键存入结果字典。

值得注意的是,父类 ObjectLoader.parseMaterials 使用的是普通 MaterialLoader(其文档明确标注 “This loader does not support node materials. Use NodeMaterialLoader instead.”,见 MaterialLoader.js#L35)。而 NodeMaterialLoader.createMaterialFromType 的查找策略是「先查 nodeMaterials 字典,未命中再回退到父类 MaterialLoader 的经典材质注册表」——这意味着同一份 JSON 可以混合包含 MeshStandardMaterial 等经典材质与 MeshPhysicalNodeMaterial 等节点材质,由同一个加载器兼容处理。

NodeMaterialLoader.parse 的另一关键动作是还原 inputNodesNodeMaterialLoader.js#L41-L58):

parse( json ) {

	const material = super.parse( json );

	const nodes = this.nodes;
	const inputNodes = json.inputNodes;

	for ( const property in inputNodes ) {

		const uuid = inputNodes[ property ];

		material[ property ] = nodes[ uuid ];

	}

	return material;

}

即把序列化时以 UUID 字符串记录的节点引用(如 color: '<uuid>')重新绑定为真实的 Node 实例,完成材质到节点图的接线。

parseNodes:两遍式反序列化与 inputNodes 结构

文档定义 .parseNodes( json, textures ) : Object.<string, Node>,第一个参数是节点 JSON 数组,第二个参数是纹理库。NodeObjectLoader 的实现是一个轻量委托(NodeObjectLoader.js#L117-L131):

parseNodes( json, textures ) {

	if ( json !== undefined ) {

		const loader = new NodeLoader();
		loader.setNodes( this.nodes );
		loader.setTextures( textures );

		return loader.parseNodes( json );

	}

	return {};

}

真正的两遍式解析在 NodeLoader.parseNodes 中:

  • 第一遍(建实例):遍历 json 数组,按 type 从字典 createNodeFromType 创建空实例,并保留 uuid,形成 { uuid: Node } 依赖表;
  • 第二遍(填数据):构造 meta = { nodes, textures } 元数据并临时挂到每个 nodeJSON.meta 上,调用 node.deserialize( nodeJSON ) 写入属性与子节点引用,随后 delete nodeJSON.meta 清理现场。

deserialize 端由 Node.deserialize 实现,它按 inputNodes 条目的形状分三种情况还原:

if ( Array.isArray( json.inputNodes[ property ] ) ) {
	// 属性 → 节点数组,如多纹理输入
	this[ property ] = [ ...uuids.map( uuid => nodes[ uuid ] ) ];
} else if ( typeof json.inputNodes[ property ] === 'object' ) {
	// 属性 → 键控节点对象
	for ( const subProperty in json.inputNodes[ property ] ) {
		inputObject[ subProperty ] = nodes[ json.inputNodes[ property ][ subProperty ] ];
	}
	this[ property ] = inputObject;
} else {
	// 属性 → 单个节点引用
	this[ property ] = nodes[ json.inputNodes[ property ] ];
}

与之对应的序列化端是 Node.serialize:遍历 getSerializeChildren() 得到的子节点,带 index 的输入写入数组/对象形式的 inputNodes(键控输入用对象、索引输入用数组),无 index 的输入直接写入 UUID。这也解释了 JSON 中 inputNodes 的三种形态——单引用字符串、UUID 数组、UUID 对象——正好与 deserialize 的三个分支一一对应。

此外,Node.toJSON 会为每个节点生成 { uuid, type, metadata: { version: 4.7, type: 'Node', generator: 'Node.toJSON' } } 骨架,材质侧则由 NodeMaterial.toJSON 在标准 Material.prototype.toJSON 基础上追加 inputNodes。这些字段就是上述解析方法逐条消费的输入结构。

setNodes / setNodeMaterials:注册表配置 API

文档定义:

  • .setNodes( value : Object.<string, Node.constructor> ) : NodeObjectLoader —— 定义节点类型字典,value<classname, class> 形式的节点库,返回加载器自身引用以支持链式调用;
  • .setNodeMaterials( value : Object.<string, NodeMaterial.constructor> ) : NodeObjectLoader —— 定义节点材质类型字典,同理。

两者实现均为「赋值 + 返回 this」(NodeObjectLoader.js#L53-L71),因此标准用法是:

const loader = new NodeObjectLoader( loadingManager )
	.setNodes( nodes )
	.setNodeMaterials( nodeMaterials );

这里有两个实践要点:

  1. 注册表必须在 parse 之前完成配置parseNodes/parseMaterials 是临时创建 NodeLoader/NodeMaterialLoader 并当场注入字典的,晚于解析的配置不会生效;
  2. 只影响「查找」,不影响「数据」:字典决定 type 字符串映射到哪个类,节点的具体数值、纹理引用等数据仍完全来自 JSON 本身。

完整加载链路与源码索引

把整条链路串起来,一次 parseAsync 的调用关系为:

NodeObjectLoader.parseAsync(json)
  ├─ 暂存 json.nodes → this._nodesJSON
  ├─ ObjectLoader.parseAsync(json)            // 标准 JSON Object/Scene 管线
  │    ├─ parseGeometries / parseImagesAsync / parseTextures
  │    ├─ NodeObjectLoader.parseMaterials(json.materials, textures)   // 多态重写
  │    │    ├─ NodeObjectLoader.parseNodes(_nodesJSON, textures)
  │    │    │    └─ NodeLoader.parseNodes     // 两遍式:建实例 → deserialize
  │    │    └─ NodeMaterialLoader.parse      // 经典材质回退 + inputNodes 接线
  │    └─ parseObject / bindSkeletons / bindLightTargets
  └─ 释放 _nodesJSON → 返回 Object3D

相关源码索引,便于继续深入:

文件 职责
src/loaders/nodes/NodeObjectLoader.js 本文主角:parse/parseAsync/parseMaterials/parseNodes 与两个字典属性
src/loaders/nodes/NodeLoader.js 节点级加载器:parseNodes 两遍式解析、createNodeFromType 回退策略
src/loaders/nodes/NodeMaterialLoader.js 节点材质加载器:节点材质优先、经典材质回退、inputNodes 还原
src/loaders/ObjectLoader.js 父类管线:JSON Object/Scene 格式的几何、纹理、对象解析
src/nodes/core/Node.js 节点序列化协议:serialize/deserialize/toJSONinputNodes 结构
src/materials/nodes/NodeMaterial.js 节点材质序列化:在 Material.toJSON 基础上追加 inputNodes
docs/pages/NodeObjectLoader.html.md 本主题的官方 API 文档(Constructor / Properties / Methods / Source)

小结

NodeObjectLoader 的设计思路可以概括为三点:复用——完整继承 ObjectLoader 的 JSON Object/Scene 管线,只重写材质解析一环;分治——节点、材质、纹理各自成库,以 uuid 作为全局引用键,通过 inputNodes 完成跨库接线;可扩展——setNodes/setNodeMaterials 使加载器能够加载任何自定义 TSL 节点与自定义节点材质,而不必改动解析代码。对于需要在浏览器中持久化并回读「节点材质场景」的 TSL 项目(例如运行时编辑了 TSL 材质后保存场景再加载的场景),它是 NodeLoader 之上的正确选择:前者只解析单个节点依赖图,后者负责整棵对象树的完整还原。

登录后查看全文
热门项目推荐
相关项目推荐