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 函数编写自定义节点材质,并理解内置的positionGeometry、normalGeometry、uv()等访问器背后的完整调用链。
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 )| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
attributeName | string | 几何体上属性的名称,如'position'、'color' | 无 |
nodeType | string | 节点类型(着色器类型),如'vec3'、'vec2'、'float' | null |
结合 AttributeNode.js#L24-L38 的实现,有两个细节值得注意:
nodeType为null时并非"无类型",而是"延迟推断"。源码中的generateNodeType( builder )方法会在构建期动态决定类型(见第 4 节),若几何体上存在该属性,则按实际BufferAttribute推导,否则回退为'float'。- 构造函数同时把
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):根据BufferAttribute的itemSize、底层 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 ); } }可以总结出三条规则:
- 顶点阶段:通过
builder.getAttribute( name, type )拿到(必要时新建并注册的)NodeAttribute声明,输出该属性的着色器变量名与类型。NodeBuilder.getAttribute(NodeBuilder.js#L1495-L1521)会先遍历已声明属性做去重,找不到才new NodeAttribute( name, type )并注册声明——这保证了多个节点引用同一属性时只声明一次。 - 片元阶段:顶点属性在 fragment shader 中不可直接读取,源码会构造
varying( this )节点,把该属性自动转成顶点着色器中声明、插值后传给片元的 varying 变量。这就是"同一属性节点在两个阶段写出不同代码"的机制。 - 属性缺失的降级:若几何体上没有该属性,不会抛出异常,而是打印警告
AttributeNode: Vertex attribute "xxx" not found on geometry.并生成一个类型常量为零值(builder.generateConst( nodeType )),保证着色器仍可编译。
6. 名称接口:getAttributeName与setAttributeName
官方参考页 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 : '' ); }顶点颜色支持多套(color、color1……),属性名无法在构造时静态确定,必须按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的现成实例,可作为编写自定义节点时的参照:
| 内置节点 | 定义位置 | 等价写法 |
|---|---|---|
positionGeometry | Position.js#L33 | attribute( 'position', 'vec3' ) |
normalGeometry | Normal.js#L15 | attribute( 'normal', 'vec3' ) |
tangentGeometry | Tangent.js#L14 | attribute( 'tangent', 'vec4' ) |
uv( index ) | UV.js#L11 | attribute( 'uv' + ( index > 0 ? index : '' ), 'vec2' ) |
| skinIndex / skinWeight | Skinning.js#L234-L235 | attribute( 'skinIndex', 'uvec4' )、attribute( 'skinWeight', 'vec4' ) |
uv( index )的命名规则(uv、uv1……)与第 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 = true | AttributeNode.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语义做声明去重、用顶点/片元分叉处理插值语义,并保留解析式命名与序列化的扩展点。理解了它,就理解了positionGeometry、normalGeometry、uv()等一切内置访问器,以及自定义顶点数据在节点材质中流转的完整机制。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考