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

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

2026-09-04 16:47:33作者:秋泉律Samson

本文以 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 )
参数 类型 说明 默认值
attributeName string 几何体上属性的名称,如 'position''color'
nodeType string 节点类型(着色器类型),如 'vec3''vec2''float' null

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

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

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

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

AttributeNode sets this property to true by default. Default is true. 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,源码 generateNodeTypeAttributeNode.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.getAttributeNodeBuilder.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;
}

但子类会覆写它。典型例子是 VertexColorNodeVertexColorNode.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.

基类实现返回 thisAttributeNode.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 ) 的命名规则(uvuv1……)与第 6 节 VertexColorNode 的解析式命名思路一致,体现了"名称可由运行时参数解析"这一设计的普遍性。

8.3 仓库中的其他真实用例

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

  • 粗线渲染:Line2NodeMaterial.jsattribute( 'instanceStart' )attribute( 'instanceEnd' )attribute( 'instanceDistanceStart' ) 等,引用的是该材质注入几何体的实例化属性;
  • 虚线材质:LineDashedNodeMaterial.js#L123varying( attribute( 'lineDistance' ).mul( dashScaleNode ) ),展示了"属性节点 → 运算 → varying"的典型组合;
  • PMREM 预处理:PMREMGenerator.js#L66attribute( '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 语义做声明去重、用顶点/片元分叉处理插值语义,并保留解析式命名与序列化的扩展点。理解了它,就理解了 positionGeometrynormalGeometryuv() 等一切内置访问器,以及自定义顶点数据在节点材质中流转的完整机制。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341