news 2026/9/5 22:24:52

Three.js TSL 中 AttributeNode 深度解析:把几何体属性变成可组合的着色器节点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Three.js TSL 中 AttributeNode 深度解析:把几何体属性变成可组合的着色器节点

Three.js TSL 中 AttributeNode 深度解析:把几何体属性变成可组合的着色器节点

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

本文以 Three.js 官方 API 参考页AttributeNode为主体,系统讲解 TSL(Three Shading Language)中"属性节点"这一基础构件:它如何把BufferGeometry上的顶点属性(position、normal、uv、color……)包装成参与节点图求值的对象,覆盖其构造函数、global标志、getAttributeName/setAttributeName接口,并结合仓库源码深入剖析类型推断、顶点/片元两阶段代码生成与序列化机制。读完后,你可以正确使用attribute()TSL 函数编写自定义节点材质,并理解内置的positionGeometrynormalGeometryuv()等访问器背后的完整调用链。

1. AttributeNode 是什么:TSL 属性访问的基类

官方文档页 AttributeNode.html.md 对它的定义只有一句话:Base class for representing shader attributes as nodes(表示着色器属性的节点基类)。在 three.js 的节点体系中,"属性"(attribute)指几何体上传给 GPU 的每顶点数据;而 TSL 不允许在节点图里直接写字符串引用这些缓冲区,必须通过一个节点对象作为桥梁——AttributeNode就是所有这类桥梁的公共基类。

其继承链为:

EventDispatcher → Node → AttributeNode

即它先继承EventDispatcher的事件能力,再继承 Node 提供的节点图通用机制(名称、缓存、哈希、代码生成入口generate(builder)等),最后加上"属性名"这一核心概念。

源码入口为 src/nodes/core/AttributeNode.js,类声明与核心成员如下:

class AttributeNode extends Node { static get type() { return 'AttributeNode'; } constructor( attributeName, nodeType = null ) { super( nodeType ); this.global = true; this._attributeName = attributeName; } // ... }

值得注意的是,AttributeNode在模块尾部额外导出了一个 TSL 函数attribute(见 AttributeNode.js#L159-L168),这是用户真正日常调用的接口:

/** * TSL function for creating an attribute node. * * @tsl * @function * @param {string} name - The name of the attribute. * @param {?string} [nodeType=null] - The node type. * @returns {AttributeNode} */ export const attribute = ( name, nodeType = null ) => new AttributeNode( name, nodeType );

官方 docs/TSL.md 的属性访问器表格中也将其列为标准成员:attribute( name, type = null )—— "Getting geometry attribute using name and type"(按名称与类型获取几何体属性)。

2. 构造函数与两个核心参数

官方参考页给出的构造函数签名为:

new AttributeNode( attributeName : string, nodeType : string )
参数类型说明默认值
attributeNamestring几何体上属性的名称,如'position''color'
nodeTypestring节点类型(着色器类型),如'vec3''vec2''float'null

结合 AttributeNode.js#L24-L38 的实现,有两个细节值得注意:

  1. nodeTypenull时并非"无类型",而是"延迟推断"。源码中的generateNodeType( builder )方法会在构建期动态决定类型(见第 4 节),若几何体上存在该属性,则按实际BufferAttribute推导,否则回退为'float'
  2. 构造函数同时把this.global置为true,这是本类相对父类Node最显眼的行为差异。

3..global属性:为何属性节点默认是"全局"的

官方参考页 Properties 一节明确说明:

AttributeNodesets this property totrueby default. Default istrue.Overrides:Node#global

源码印证了这一点(AttributeNode.js#L28-L34):

/** * `AttributeNode` sets this property to `true` by default. * * @type {boolean} * @default true */ this.global = true;

在 Node 体系中,global决定了节点是否跨构建上下文共享缓存(同一个节点对象被多次引用时只生成一份声明)。属性之所以默认global,是因为同一个顶点属性往往会在节点图中被多次引用——例如positionGeometry这个模块级常量在整个着色器构建中被多处使用,若每次都声明一遍变量会造成冲突或冗余。这一点也体现在 Node.js#L633 的注释里:attribute( 'uv' )被多次使用时,构建期会做去重复用。

与之配套的还有getHash( builder )的实现(AttributeNode.js#L40-L44):

getHash( builder ) { return this.getAttributeName( builder ); }

即两个属性节点的哈希以属性名区分——同名属性共享同一构建结果,不同名属性各自独立。

4. 类型推断:generateNodeType如何为null类型兜底

当构造时未指定nodeType,源码generateNodeType(AttributeNode.js#L46-L70)按如下逻辑处理:

generateNodeType( builder ) { let nodeType = this.nodeType; if ( nodeType === null ) { const attributeName = this.getAttributeName( builder ); if ( builder.hasGeometryAttribute( attributeName ) ) { const attribute = builder.geometry.getAttribute( attributeName ); nodeType = builder.getTypeFromAttribute( attribute ); } else { nodeType = 'float'; } } return nodeType; }

两条关键依赖都来自 NodeBuilder:

  • hasGeometryAttribute( name )(NodeBuilder.js#L1482-L1486):检查this.geometry.getAttribute( name ) !== undefined,即当前正在构建的几何体上是否存在该属性。
  • getTypeFromAttribute( attribute )(NodeBuilder.js#L1728-L1748):根据BufferAttributeitemSize、底层 TypedArray 类型以及normalized标志(Float16BufferAttribute与非归一化属性除外)推导着色器类型,例如 3 分量 Float32 数组推出vec3

也就是说:attribute( 'position' )不写类型也能工作——构建器会查几何体真实数据并推出vec3;而查不到属性时安全地退化为float,避免构建期崩溃。

5. 代码生成:顶点阶段与片元阶段的行为分叉

generate( builder )是节点参与最终着色器输出的核心方法(AttributeNode.js#L102-L135),其行为按渲染阶段严格分叉:

generate( builder ) { const attributeName = this.getAttributeName( builder ); const nodeType = this.getNodeType( builder ); const geometryAttribute = builder.hasGeometryAttribute( attributeName ); if ( geometryAttribute === true ) { const attribute = builder.geometry.getAttribute( attributeName ); const attributeType = builder.getTypeFromAttribute( attribute ); const nodeAttribute = builder.getAttribute( attributeName, attributeType ); if ( builder.shaderStage === 'vertex' ) { return builder.format( nodeAttribute.name, attributeType, nodeType ); } else { const nodeVarying = varying( this ); return nodeVarying.build( builder, nodeType ); } } else { warn( `AttributeNode: Vertex attribute "${ attributeName }" not found on geometry.` ); return builder.generateConst( nodeType ); } }

可以总结出三条规则:

  1. 顶点阶段:通过builder.getAttribute( name, type )拿到(必要时新建并注册的)NodeAttribute声明,输出该属性的着色器变量名与类型。NodeBuilder.getAttribute(NodeBuilder.js#L1495-L1521)会先遍历已声明属性做去重,找不到才new NodeAttribute( name, type )并注册声明——这保证了多个节点引用同一属性时只声明一次。
  2. 片元阶段:顶点属性在 fragment shader 中不可直接读取,源码会构造varying( this )节点,把该属性自动转成顶点着色器中声明、插值后传给片元的 varying 变量。这就是"同一属性节点在两个阶段写出不同代码"的机制。
  3. 属性缺失的降级:若几何体上没有该属性,不会抛出异常,而是打印警告AttributeNode: Vertex attribute "xxx" not found on geometry.并生成一个类型常量为零值(builder.generateConst( nodeType )),保证着色器仍可编译。

6. 名称接口:getAttributeNamesetAttributeName

官方参考页 Methods 一节列出的两个方法,是派生类定制属性名的"扩展点",文档原文强调:derived classes 可以覆写这两个方法,以在需要解析式计算最终名称时实现

6.1.getAttributeName( builder : NodeBuilder ) : string

Returns the attribute name of this node. The method can be overwritten in derived classes if the final name must be computed analytically.

基类实现极其简单(AttributeNode.js#L96-L100):

getAttributeName( /*builder*/ ) { return this._attributeName; }

但子类会覆写它。典型例子是 VertexColorNode(VertexColorNode.js#L51-L57):

getAttributeName( /*builder*/ ) { const index = this.index; return 'color' + ( index > 0 ? index : '' ); }

顶点颜色支持多套(colorcolor1……),属性名无法在构造时静态确定,必须按index解析式计算——这正是文档所说"analytically computed"的场景。VertexColorNode还把缺失颜色属性时的降级值从"零值"改成了白色(1,1,1,1)(VertexColorNode.js#L59-L79),说明子类覆写generate可进一步定制兜底行为。

6.2.setAttributeName( attributeName : string ) : AttributeNode

Sets the attribute name to the given value. … Returns: A reference to this node.

基类实现返回this(AttributeNode.js#L80-L86),支持链式调用,同样供子类覆写。

7. 序列化:serialize / deserialize 支持节点编辑器往返

参考页虽未展开,但源码中AttributeNode实现了完整的序列化接口(AttributeNode.js#L137-L153):

serialize( data ) { super.serialize( data ); data.global = this.global; data._attributeName = this._attributeName; } deserialize( data ) { super.deserialize( data ); this.global = data.global; this._attributeName = data._attributeName; }

global标志与属性名两个状态都可无损写入/恢复,配合仓库中的节点编辑能力(如 webgpu_tsl_editor.html 示例所依托的节点序列化机制),属性节点可以作为图中可保存、可回放的一等公民存在。

8. 实践:attribute()函数与内置访问器

8.1 直接使用attribute()

最简单的用法就是按名称取属性,并显式指定类型:

import { attribute, material, color } from 'three/tsl'; // 在几何体上定义 aRandom 属性(float),然后在节点图中引用 const geom = new THREE.IcosahedronGeometry( 1, 4 ); const count = geom.attributes.position.count; const random = new Float32Array( count ); for ( let i = 0; i < count; i ++ ) random[ i ] = Math.random(); geom.setAttribute( 'aRandom', new THREE.BufferAttribute( random, 1 ) ); const materialNode = material( color( 'black' ).mul( attribute( 'aRandom' ) ) ); // mesh.material = materialNode

显式传类型(如attribute( 'position', 'vec3' ))可以跳过第 4 节的推断路径,语义更明确。

8.2 内置访问器:全是attribute()的实例

three.js 内置的几何体访问器本身就是AttributeNode的现成实例,可作为编写自定义节点时的参照:

内置节点定义位置等价写法
positionGeometryPosition.js#L33attribute( 'position', 'vec3' )
normalGeometryNormal.js#L15attribute( 'normal', 'vec3' )
tangentGeometryTangent.js#L14attribute( 'tangent', 'vec4' )
uv( index )UV.js#L11attribute( 'uv' + ( index > 0 ? index : '' ), 'vec2' )
skinIndex / skinWeightSkinning.js#L234-L235attribute( 'skinIndex', 'uvec4' )attribute( 'skinWeight', 'vec4' )

uv( index )的命名规则(uvuv1……)与第 6 节VertexColorNode的解析式命名思路一致,体现了"名称可由运行时参数解析"这一设计的普遍性。

8.3 仓库中的其他真实用例

从源码结构看,attribute()也被用在内置管线里,例如:

  • 粗线渲染:Line2NodeMaterial.js 中attribute( 'instanceStart' )attribute( 'instanceEnd' )attribute( 'instanceDistanceStart' )等,引用的是该材质注入几何体的实例化属性;
  • 虚线材质:LineDashedNodeMaterial.js#L123 中varying( attribute( 'lineDistance' ).mul( dashScaleNode ) ),展示了"属性节点 → 运算 → varying"的典型组合;
  • PMREM 预处理:PMREMGenerator.js#L66 中attribute( 'outputDirection' ).normalize()

这些用法共同说明:AttributeNode不仅是用户侧的 API,也是 TSL 管线内部把各种顶点级数据纳入节点图求值的统一手段。

9. 小结与要点回顾

成员作用源码依据
constructor( attributeName, nodeType = null )创建属性节点;类型可为null延迟推断;同时置global = trueAttributeNode.js#L24-L38
.global覆盖Node#global,默认true,保证同一属性跨构建去重复用AttributeNode.js#L34
getAttributeName( builder )返回属性名;派生类可覆写为解析式命名AttributeNode.js#L96-L100
setAttributeName( name )设置属性名并返回this,可链式调用AttributeNode.js#L80-L86
generate( builder )顶点阶段输出属性声明引用;片元阶段经varying传递;属性缺失时警告并降级为常量AttributeNode.js#L102-L135
attribute( name, nodeType )TSL 工厂函数,日常使用入口AttributeNode.js#L168

一句话概括:AttributeNode是 TSL 中"GPU 顶点数据"进入节点图的统一入口——用名称定位数据、用类型系统(显式或推断)约束求值、用global语义做声明去重、用顶点/片元分叉处理插值语义,并保留解析式命名与序列化的扩展点。理解了它,就理解了positionGeometrynormalGeometryuv()等一切内置访问器,以及自定义顶点数据在节点材质中流转的完整机制。

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

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

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

YOLO26训练自己的数据集:从标注规范到格式转换的完整实战指南

干这行久了&#xff0c;你会发现真正决定YOLO系列检测模型能不能落地的人&#xff0c;往往不是那个写网络结构的人&#xff0c;而是那个在后面做数据集的同事。模型结构再强&#xff0c;喂进去的标注乱七八糟&#xff0c;训练时loss掉不下去&#xff0c;测试时mAP上不来&#x…

作者头像 李华
网站建设 2026/9/5 22:13:43

NocoBase 数据可视化实战指南:如何从零搭建专业图表看板

NocoBase 数据可视化实战指南&#xff1a;如何从零搭建专业图表看板 【免费下载链接】nocobase NocoBase is an open-source AI no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven…

作者头像 李华