ARTICLE DETAIL

资讯详情

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

three.js 节点化渲染基石:深入解读 Node 基类与 TSL 的构建管线

three.js 节点化渲染基石:深入解读 Node 基类与 TSL 的构建管线 three.js 节点化渲染基石深入解读 Node 基类与 TSL 的构建管线【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsNode 是 three.js 节点化材质/着色器系统TSLThree Shading Language中所有节点的统一基类从最简单的常量、属性取值节点到整个材质输出节点都派生自它。本文将逐条剖析 Node.html.md 记录的构造函数、全部属性与方法并对照 Node.js 源码 还原其在 setup → analyze → generate 三阶段构建管线、节点缓存去重、序列化与逐帧更新中的真实行为读完你便能独立分析任意内置节点的源码并写出符合规范的自定义 TSL 节点。Node 在 three.js 中的定位在 three.js 的节点系统中Node 是所有节点的基类Base class for all nodes。源码中它的继承关系为Node extends EventDispatcher见 src/nodes/core/Node.js因此每个节点天然具备事件派发能力如dispose事件。围绕 Node 基类src/nodes/core 目录中还聚集了一整套支撑设施理解它们有助于读懂 Node 的每一个方法NodeBuilder.js构建器负责驱动整个构建流程、维护节点缓存与生成代码NodeFrame.js节点帧对象由渲染器在动画循环中驱动节点运行期更新constants.js定义了NodeUpdateType、着色阶段、数据类型的枚举常量NodeUtils.js提供hash/hashArray/hashString等缓存键哈希工具Node.js 顶部即从它导入StackTrace.js用于调试的调用栈捕获辅助类。类型标识上Node 在构造时设置了this.isNode true节点系统内部普遍通过node.isNode true来判断一个值是否为节点例如 getChildren 的子节点发现这与 three.js 场景对象中isObject3D、isMesh的惯例一致。构造与身份标识new Node( nodeType : string )构造器签名如下见 src/nodes/core/Node.js#L35constructor( nodeType null ) { super(); this.nodeType nodeType; // 节点类型如 float / vec3 // ... }其中nodeType表示该节点的结果类型result type默认null。构造时还会初始化一批默认属性详见下文属性表并通过模块级计数器_nodeId分配全局唯一自增id源码 L15/L152。直接构造 Node 实例本身没有实用意义通常写自定义节点时会class MyNode extends Node并在构造函数里传入自身的结果类型。基类本身在构建期有着确定的默认行为子节点发现靠反射_getChildren、setup默认登记全部子节点、generate默认把工作转交给 setup 阶段的输出节点outputNode这些机制构成了整个节点系统组合即生成代码的基础。三个易混淆的身份属性id、uuid、type属性类型含义.id : number只读number由模块级计数器分配的全局唯一整数 ID构造时即生成源码.uuid : string只读string惰性生成的 UUID首次访问时经MathUtils.generateUUID()生成并缓存用于 JSON 序列化时跨对象引用源码.type : string只读string类类型通常等于构造器名。实现上是调用this.constructor.type源码即每个子类都要定义static get type()id与uuid的区别要重点留意id面向构建器内部的哈希共享与去重getHash()默认返回String(this.id)uuid面向 JSON 持久化时的引用还原serialize会把子节点以 uuid 形式写入inputNodes。属性全景默认值、用途与源码对照文档列出的全部属性在源码构造器中都有对应初始值汇总如下属性默认值作用与源码依据.nodeType : stringnull节点结果类型如float、vec3L45.name : string节点名称调试与序列化时用.id : number只读自增全局唯一整数 IDL152.uuid : string只读惰性生成用于序列化的 UUIDL193.type : string只读构造器名constructor.type的快捷方式L211.isNode : boolean只读true类型测试标志L113.version : number只读0版本号needsUpdate被设为true时自动 1L78/L181.global : booleanfalse是否全局节点。对内部节点缓存系统有意义只需声明一次的节点应置true典型例子是 AttributeNodeL96.parents : booleanfalse构建过程中是否为该节点收集父节点列表.needsUpdatesetterfalse置为true时节点版本递增触发重新生成与缓存失效L177-L185.updateType : stringnoneupdate()的更新频度取值见 NodeUpdateTypeL53.updateBeforeType : stringnoneupdateBefore()的更新频度L61.updateAfterType : stringnoneupdateAfter()的更新频度L69.stackTrace : stringnull调试用调用栈仅当静态开关Node.captureStackTrace true时在构造期被赋值L160-L166.captureStackTrace : booleanfalse类级静态开关。文档将其列为属性源码中它实际是挂在类上的静态变量Node.captureStackTrace文件末尾 L1215开启后所有新构造节点都会自动捕获 stackTraceupdateType系列的合法取值NodeUpdateTypeupdateType/updateBeforeType/updateAfterType的取值集中定义在 constants.js 的NodeUpdateType中值含义NONE(none)不执行对应的更新方法默认FRAME(frame)每帧执行一次RENDER(render)每次渲染调用执行一次一帧可能由多次 render 产生因此比 FRAME 更细粒度OBJECT(object)每个使用该节点的 Object3D 执行一次同一份 constants.js 还定义了三个构建阶段defaultBuildStages [setup, analyze, generate]这正是 Node 构建流程三阶段名的直接来源。三阶段构建流程build是核心入口build()是节点参与着色器生成的入口方法。文档明确给出三阶段的语义源码 L845-L1022 进一步揭示了实现细节阶段返回行为setupNode准备节点及其子节点可能新建节点返回节点自身或其变体输出节点analyzenull分析节点层级为代码生成阶段做优化准备generatestring生成该节点的着色器代码字符串build()的真实执行顺序与源码行号一一对应共享解析先调用getShared(builder)若返回的不是自身则直接委托给共享节点构建L847-L853。这是attribute(uv)被多处使用时只生成一次的关键。前序节点处理通过before(node)注册的前置节点L857-L871。父阶段回溯若当前构建阶段缺少前置阶段记录如直接进入 generate 却没 setup会通过_parentBuildStage映射analyze → setup、generate → analyze强制先补齐父阶段L879-L893。按阶段分发L908-L1016setup把节点加入构建器、调用updateReference(builder)、执行setup(builder)对返回的 outputNode 与所有子节点递归build随后将节点登记为顺序节点同一节点只初始化一次properties.initialized。analyze调用analyze(builder, output)。generate根据generate函数的形参个数分派——this.generate.length 2表示节点不处理输出类型转换generate once其代码片段snippet会被缓存由构建器统一按各请求的输出类型做builder.format(...)转换否则直接调用generate(builder, output)。若生成结果是空串且请求了具体输出类型会抛出TSL: Invalid generated code错误并回退为常量L1006-L1014。递归保护如果同一节点在 generate 阶段被重复进入nodeData.generated已置位会输出Recursion detected警告并返回注释占位防止无限循环L984-L989。对应地三个阶段的钩子方法在基类中有各自的默认实现.setup( builder : NodeBuilder ) : Node文档说明它是构建流程第一步常被子类覆写以准备输出节点。基类默认实现L705-L721会遍历getChildren()把每个子节点存入构建器属性node0 / node1 / ...然后返回已有的outputNode。因此大多数无需额外逻辑的节点可以完全不写 setup。.analyze( builder : NodeBuilder, output : Node )构建第二步。基类实现L730-L761会用builder.increaseUsage()统计节点使用次数若parents true则记录各着色阶段的输出引用当某节点首次被分析时usageCount 1递归对 setup 阶段登记的所有子节点执行build。.generate( builder : NodeBuilder, output : string ) : string构建第三步。基类默认逻辑L771-L781十分简洁若存在 outputNode 则转交其build否则不产生代码——真正生成代码的节点常量、计算、纹理采样等都会自行覆写该方法。其它构建辅助方法.getShared( builder ) : Node确保等价节点只被构建一次。先按getHash(builder)从构建器哈希表取共享节点再处理builder.context.overrideNodes中的节点覆写回调带isOverwritten防递归标记最后返回共享节点或thisL640-L683。.getScope() : Node返回节点自身引用默认this供上下文/作用域相关场景使用L481-L485。.before( node )源码中额外提供的注册方法允许在自身 build 前先构建指定节点L825-L833本文档未单列但存在于实现中。类型推断系统节点系统需要在不实际生成代码的前提下确定这个节点算出什么类型。基类提供了完整的一套类型查询方法核心是借助构建器按节点缓存结果.getNodeType( builder, output null ) : string返回节点类型output非空时按{ output → type }逐类型缓存nodeData.typeFromOutput为空时缓存到nodeData.type避免重复计算L571-L607。.generateNodeType( builder, output null ) : string实际的类型推导若 setup 阶段产出了 outputNode则委托给它否则回退到构造时传入的this.nodeTypeL616-L628。.getElementType( builder ) : string返回复合类型的分量类型。文档举例恰如其分——vec3由三个float组成实现上委托builder.getElementType( type )L542-L549。.getMemberType( builder, name ) : string返回命名成员的类型基类默认返回voidL558-L562结构体类节点会覆写。.getArrayCount( builder ) : number返回数组节点元素个数基类默认nullL691-L695。这套方法体系保证了构建器在builder.format(snippet, type, output)转换时能拿到可靠的类型信息——它服务于 generate 阶段先算出类型、再按需转换输出格式的缓存策略见上文 build 步骤 4。缓存、共享与哈希机制节点系统对重复代码的抑制依赖一套贯穿id/version/global/cacheKey/hash的机制这是本文档中篇幅最大也最需要结合源码理解的部分缓存键.customCacheKey()与.getCacheKey( force, ignores ).customCacheKey()默认返回this.idL470-L474子类可按自身参数参与哈希例如把某个影响结果的配置值 hash 进去。.getCacheKey( force false, ignores null )L436-L463会递归遍历_getChildren(ignores)获取所有子节点用属性名去掉末尾的Node等后缀片段 子节点缓存键组成数组最终hash(hashArray(values), customCacheKey())得出总缓存键。其中force在this.version ! this._cacheKeyVersion时也会被强制置真即缓存键随needsUpdate引发的版本号变化自动失效。共享与去重.getShared()、.getHash()getHash()返回用于在构建器哈希表中识别节点的字符串。默认实现返回String(this.id)L495-L499注意源码注释仍写作 uuid实际为 id 字符串而getShared()正是用该哈希去查询builder.getNodeFromHash(hash)实现去重的。子类可以覆写getHash来让逻辑等价的节点共享同一缓存。典型范例是 AttributeNode它把global置为trueL34并把getHash覆写为返回属性名L40-L44这样用户代码里多处attribute(uv)实际只对应一条顶点属性声明。全局节点.isGlobal()、.global.isGlobal(builder)默认返回this.global标志L307-L311子类可按着色阶段做解析式判断。文档强调只需声明一次的节点应把global设为true——典型如 AttributeNode顶点属性声明、uniform 等本就不应重复出现这也是它们被放进全局缓存、跨节点共享的判据。序列化与反序列化节点图可以持久化为 three.js 的 JSON 场景格式也可以从中还原.toJSON( meta ) : ObjectL1133-L1205以{ uuid, type }为条目写入meta.nodes缓存避免同一节点重复序列化metadata标记版本4.7、类型Node、生成器Node.toJSON当meta缺省即根节点调用时自行构建{ textures: {}, images: {}, nodes: {} }容器并在收尾时把缓存展平回data.textures/images/nodes。.serialize( json )L1040-L1072通过getSerializeChildren()即_getChildren()L1029-L1033找到全部子节点将它们的toJSON().uuid按属性名写入json.inputNodes支持数组、普通对象与单值三种形态见 L1046-L1070。.deserialize( json )L1079-L1125逆操作——从json.meta.nodes中按 uuid 找回节点并把数组/对象/单值分别还原回对应属性。子节点发现与层级遍历.getChildren() : Node生成器函数逐个 yield 子节点L319-L327。.traverse( callback )深度优先遍历整个节点层级先回调自身再递归子节点L351-L361。支撑它们的私有方法_getChildren(ignores)L370-L427值得单独说明它并不维护显式的子节点数组而是通过Object.getOwnPropertyNames(this)反射扫描实例自有属性凡是以_开头的私有属性一律跳过再对属性值为以下三种形态的情况收集其中isNode true的值Node 数组记录{ property, index, childNode }单值 Node记录{ property, childNode }普通对象Object.getPrototypeOf Object.prototype内的 Node 子属性。同时把已访问节点加入ignores集合以避免环形引用。这解释了为何 TSL 中把子节点作为普通实例属性挂载即可被构建系统自动发现——无需手动登记。运行期更新updateType、onUpdate系列与 NodeFrame纯着色器节点在 GPU 端工作而有些节点需要在 JS 侧逐帧维护内部状态如随时间变化的 uniform。文档将update、updateBefore、updateAfter均标注为 abstract基类实现仅输出Abstract function.警告L791-L823提示子类实现。更新频度由属性决定updateBefore(frame)的执行频度由updateBeforeType控制update(frame)由updateType控制updateAfter(frame)由updateAfterType控制。 三者对应的读取方法getUpdateBeforeType()/getUpdateType()/getUpdateAfterType()就在 L506-L532。便捷注册方法基类提供一组链式便捷方法内部都只是设置更新类型 绑定回调方法等价行为.onUpdate(callback, updateType)核心实现设置updateType把update绑定为回调L224-L231.onFrameUpdate(callback)自动设置类型为FRAMEL240-L244.onRenderUpdate(callback)自动设置类型为RENDERL253-L257.onObjectUpdate(callback)自动设置类型为OBJECTL266-L270.onReference(callback)便捷定义updateReferenceL278-L284.updateReference( state )与引用跟随节点可能引用其它对象如材质、场景对象。updateReference允许基于当前 stateNodeFrame 或 builder动态更新该引用L293-L297默认返回this。它在更新频度判定的去重键中扮演关键角色。NodeFrame 如何驱动更新NodeFrame.js 的updateNode/updateBeforeNode/updateAfterNodeL147-L292负责真正执行这三个钩子其去重逻辑值得逐条解读FRAME 类型以(reference, updateMap)记录frameId仅当frameId ! this.frameId时才执行RENDER 类型同理比较renderIdOBJECT 类型每次直接执行不做帧/渲染去重update返回布尔值有语义以updateNode为例仅当update(this) ! false才记录frameIdL262-L270即返回false表示本次未真正更新命中内部缓存下一帧会再次尝试——这正是文档所述指示是否真正执行了更新的用途。updateBefore采用相反写法达到同样效果返回false时回退已记录的 frameIdL156-L166。另外要注意updateReference的返回值reference是这套 WeakMap 索引的键L150/L256因此多物体共享同一节点时会按当前引用对象分别记录更新状态这也是多物体下 uniform 能按物体正确刷新的关键。生命周期事件.dispose()dispose()仅做一件事——派发dispose事件L333-L336。因为 Node 继承 EventDispatcher外部可通过标准的.addEventListener(dispose, ...)或编辑器场景中的监听辅助注册清理逻辑例如释放 GPU 相关资源。文档建议可用该事件注册清理任务的监听器。实战编写自定义 TSL 节点时应覆写什么综合上文源码级结论自定义节点最常涉及以下钩子每种情况都给出基类行为作为对照确定结果类型构造时传nodeType或覆写generateNodeType(builder, output)更灵活可按属性/配置动态推导——AttributeNode 就是按几何体属性反推类型见其实现。决定是否生成代码纯组合节点不覆写generate基类默认转发给 setup 的 outputNode叶节点覆写generate输出着色器片段。若generate声明形参少于两个源码用this.generate.length 2判断会被当作generate once代码片段缓存后由构建器统一做输出类型转换build 中 L965-L999。子节点连接直接把子节点赋为实例属性即可_getChildren()反射会自动发现它们需要自定义子节点发现时覆写getChildren。参与缓存与共享同值/同参数节点应覆写getHash并考虑置global true参照 AttributeNode需要随参数变化使缓存失效的节点把参数指纹并入customCacheKey()。逐帧/逐渲染更新在update/updateBefore/updateAfter中维护状态并配合onFrameUpdate/onRenderUpdate/onObjectUpdate或直接设置对应*Type属性决定执行频度。持久化需要序列化的节点在serialize/deserialize中记录额外配置字段json 编号沿用toJSON的meta.nodes缓存机制。以三个典型的内置子类为例佐证这套扩展点AttributeNode展示了globaltrue 覆写getHash 覆写generateNodeType与generate顶点阶段直接取属性名、片元阶段经varying转发L102-L126setup的默认 outputNode 转发模式则被大量算子节点复用而constants.js中NodeTypebool/int/float/vec2...mat4为整套类型推断提供了统一的类型字符串字典。速查与进一步阅读本文主体 API 定义见 docs/pages/Node.html.md含属性与方法的最简说明含new Node( nodeType )的nodeType默认null等细节。基类完整实现src/nodes/core/Node.js1217 行含大量 JSDoc 与行为注释。配套支撑模块NodeBuilder.js、NodeFrame.js、constants.js、NodeUtils.js、StackTrace.js。继承链上游EventDispatcherNode 事件能力的来源。派生类范例AttributeNode、同一目录下 src/nodes/core 中的UniformNode、VarNode、ConstNode、PropertyNode等 40 余个核心节点实现。节点系统整体语境可进一步阅读项目内的 docs/TSL.md 与节点着色阶段vertex/fragment/compute相关文档。掌握 Node 基类就等于拿到了阅读全部 200 余个src/nodes下节点的语法表——三阶段构建决定代码如何生成id/version/cacheKey/hash 决定代码如何去重update 体系决定 JS 状态如何刷新serialize/toJSON 决定节点图如何持久化四套机制环环相扣共同构成了 three.js TSL 声明式着色器系统的内核。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表