Three.js TSL 中 AttributeNode 深度解析把几何体属性变成可组合的着色器节点【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js本文以 Three.js 官方 API 参考页AttributeNode为主体系统讲解 TSLThree 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} [nodeTypenull] - 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、floatnull结合 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-L44getHash( 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; }两条关键依赖都来自 NodeBuilderhasGeometryAttribute( 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.getAttributeNodeBuilder.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 ) : stringReturns 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-L100getAttributeName( /*builder*/ ) { return this._attributeName; }但子类会覆写它。典型例子是 VertexColorNodeVertexColorNode.js#L51-L57getAttributeName( /*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 ) : AttributeNodeSets the attribute name to the given value. … Returns: A reference to this node.基类实现返回thisAttributeNode.js#L80-L86支持链式调用同样供子类覆写。7. 序列化serialize / deserialize 支持节点编辑器往返参考页虽未展开但源码中AttributeNode实现了完整的序列化接口AttributeNode.js#L137-L153serialize( 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 )的命名规则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 trueAttributeNode.js#L24-L38.global覆盖Node#global默认true保证同一属性跨构建去重复用AttributeNode.js#L34getAttributeName( builder )返回属性名派生类可覆写为解析式命名AttributeNode.js#L96-L100setAttributeName( name )设置属性名并返回this可链式调用AttributeNode.js#L80-L86generate( builder )顶点阶段输出属性声明引用片元阶段经varying传递属性缺失时警告并降级为常量AttributeNode.js#L102-L135attribute( 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),仅供参考
