three.js FlipNode 源码解析:理解 TSL 中的 `x = 1 - x` 分量翻转机制
本篇技术指南围绕 three.js TSL(Three Shading Language)节点体系中的 FlipNode(官方文档)展开,讲解这一着色器生成期"翻转"节点的工作原理、构造方式、属性与方法,以及它在引擎内部的真实调用链——例如 uvNode.flipY()、flipRGBA() 这类链式调用究竟如何落地为 GPU 着色器中的 1.0 - v.y 表达式。读完本文,你将理解 FlipNode 在整个 TSL 管线中的定位,并能准确地在自定义着色节点与材质中使用各种分量翻转写法。
FlipNode 是什么:一个只服务于 TSL 内部的翻转节点
FlipNode 位于 TSL 核心模块中(源码见 src/nodes/utils/FlipNode.js),通常不会在应用层代码里被直接实例化。它的职责是在着色器生成(shader generation)阶段对一个节点所代表的向量/标量值执行"翻转"运算——翻转的数学含义是对归一化值做取补运算:
x = 1 - x;
例如一个分量 y = 0.3 经过翻转后变为 0.7。这种操作对处于 [0, 1] 归一化区间的 UV 坐标、颜色值尤为常见,常被用来做纹理坐标的上下镜像、坐标系的 y 轴方向反转等。
从类继承关系看(这也是该文档标题行所标注的继承链):
EventDispatcher → Node → TempNode → FlipNode
在源码中它声明为 class FlipNode extends TempNode,并通过静态访问器注册节点类型名 'FlipNode'(FlipNode.js):
static get type() {
return 'FlipNode';
}
之所以继承 TempNode,是因为翻转后的结果往往需要以临时变量的形式参与后续计算(详见后文对 generate() 的分析)。
三种命名法:flipXYZW / flipRGBA / flipSTPQ
官方文档明确指出:FlipNode 在引擎内部被用来实现所有挂在 Node 对象上的 flipXYZW()、flipRGBA() 与 flipSTPQ() 方法调用。这与 TSL 中访问分量时同时支持三套别名是一脉相承的:
| 命名空间 | 分量别名 | 用途 |
|---|---|---|
xyzw |
x, y, z, w |
位置/向量(position、vector) |
rgba |
r, g, b, a |
颜色(color) |
stpq |
s, t, p, q |
纹理坐标(texture coordinate) |
例如在 ReflectorNode.js 内部,反射平面的默认 UV 就直接使用了 screenUV.flipX():
const _defaultUV = screenUV.flipX();
再如官方文档给出的最典型示例——把 UV 的 y 分量翻转(常用于修正纹理的纵向方向):
uvNode = uvNode.flipY();
用户代码拿到的是一个经过翻转语义封装的新节点,而不是直接拿到 FlipNode 的原始输出;真正实例化 FlipNode 的动作发生在 TSL 核心的注册逻辑中。
从 uvNode.flipY() 到 new FlipNode(...)
在 src/nodes/tsl/TSLCore.js 中,TSL 通过原型扩展把所有分量翻转方法统一挂到 Node.prototype 上:
// Set methods for flip properties
Node.prototype[ 'flip' + propUpper ] =
Node.prototype[ 'flip' + altAUpper ] =
Node.prototype[ 'flip' + altBUpper ] = function () {
const swizzle = parseSwizzleAndSort( property );
return new FlipNode( this, swizzle );
};
其中 parseSwizzleAndSort 先把 rgba / stpq 别名统一归一化为 xyzw 表示,再按字母序排序(TSLCore.js):
const parseSwizzle = ( props ) => props.replace( /r|s/g, 'x' ).replace( /g|t/g, 'y' ).replace( /b|p/g, 'z' ).replace( /a|q/g, 'w' );
const parseSwizzleAndSort = ( props ) => parseSwizzle( props ).split( '' ).sort().join( '' );
随后的四重循环(TSLCore.js)会遍历 x/y/z/w 命名空间并同时生成 rgba、stpq 的等价别名,覆盖从单分量到四分量(x、xy、xyz、xyzw,以及对应的 r…、s… 写法)的全部组合。因此:
uvNode.flipY()→flip+Y,归一化为'y',排序后仍是'y',等价于uvNode.flipT()(t 即纹理坐标 y 轴);colorNode.flipRGBA()→ 归一化得到'xyzw',翻转全部四个颜色分量;uvNode.flipXB()之类则是先归一化为'xz'并排序后传入。
也就是说:传给 FlipNode 的 components 始终是以 xyzw 顺序排序的字符串。
构造函数与属性
new FlipNode( sourceNode : Node, components : string )
构造一个翻转节点,构造函数在 FlipNode.js 中的实现非常简单——只做字段赋值,不执行任何计算(真正的计算留到着色器生成期):
constructor( sourceNode, components ) {
super();
this.sourceNode = sourceNode;
this.components = components;
}
| 参数 | 类型 | 说明 |
|---|---|---|
sourceNode |
Node |
需要翻转分量值的源节点(例如某个 UV 节点、颜色节点或任意 TSL 表达式节点) |
components |
string |
需要翻转的分量集合,例如 'x' 只翻转 x,'xy' 同时翻转 x 与 y |
.sourceNode : Node
被翻转的源节点。它是节点类型的“参照物”——翻转节点的类型完全由它推导而来(见下节 generateNodeType)。
.components : string
需要翻转的分量字符串,例如 'x' 或 'xy'。由于 TSL 层在调用前已经做过归一化与排序,进入生成器时该字符串的顺序即 xyzw 标准序。
方法:generateNodeType 与 generate
.generateNodeType( builder : NodeBuilder ) : string
generateNodeType( builder ) {
return this.sourceNode.getNodeType( builder );
}
这个方法被覆写(override)的原因是:FlipNode 自身并不持有固定的节点类型,它的类型必须从源节点推断。构造函数中调用 super() 时没有传入任何 nodeType,因此类型只能“跟着源节点走”:对 vec2 UV 执行 flipY(),结果仍是 vec2;对颜色节点执行 flipRGBA(),结果仍与源节点类型一致。
在基类 Node.js 的 getNodeType() 中,类型会按节点数据缓存起来,避免重复推导;默认实现(Node.js)返回 this.nodeType,而 FlipNode 覆写后的 generateNodeType 则改从 sourceNode 侧递归取得类型。这与基类 TempNode(TempNode)的默认行为不同,也是文档中特别标注“Overrides”的原因。
.generate( builder : NodeBuilder ):源码级的翻转生成逻辑
generate() 才是翻转真正发生的地方,完整逻辑在 FlipNode.js。逐行拆解:
- 取类型、构建源节点、分配临时变量
const sourceType = this.getNodeType( builder ); // 通过上面的覆写得到源节点类型
const sourceSnippet = sourceNode.build( builder ); // 先让源节点生成着色器代码片段
const sourceCache = builder.getVarFromNode( this ); // 为本节点申请一个临时变量
const sourceProperty = builder.getPropertyName( sourceCache );
builder.addLineFlowCode( sourceProperty + ' = ' + sourceSnippet, this );
为什么非要先写进一个临时变量?因为翻转结果中未翻转的分量也要继续引用源值,而翻转分量又是对源值做算术变换,如果源表达式代价很高(例如包含纹理采样、函数调用),就必须只求值一次,避免生成重复计算的着色器代码。这正是继承自 TempNode 的“缓存 + 临时变量”思想的体现(可对照 TempNode.js 中 build() 对重复节点生成临时变量的处理)。
- 按类型长度逐分量拼装输出
const length = builder.getTypeLength( sourceType );
const snippetValues = [];
let componentIndex = 0;
for ( let i = 0; i < length; i ++ ) {
const component = vectorComponents[ i ]; // ['x','y','z','w']
if ( component === components[ componentIndex ] ) {
snippetValues.push( '1.0 - ' + ( sourceProperty + '.' + component ) );
componentIndex ++;
} else {
snippetValues.push( sourceProperty + '.' + component );
}
}
return `${ builder.getType( sourceType ) }( ${ snippetValues.join( ', ' ) } )`;
vectorComponents 定义于 src/nodes/core/constants.js,即 ['x', 'y', 'z', 'w']。循环遍历源类型的全部分量(getTypeLength 按类型返回 1/2/3/4),凡落在 components 中的分量就输出 1.0 - 临时变量.分量 的表达式,其余分量则原样透传,最后用类型构造器把分量包起来返回。
以 uvNode.flipY()(假定源为 vec2 类型、临时变量名为 flipNode)为例,生成过程等价于产出形如:
vec2( flipNode.x, 1.0 - flipNode.y )
其中该 flipNode 临时变量会由前一步的 addLineFlowCode 先完成赋值。若要翻转全部颜色分量(colorNode.flipRGBA() 相当于翻转 xyzw),则每个分量都会生成 1.0 - c.a 形式的取补表达式。
在真实项目中的调用场景
虽然文档强调 FlipNode“不在应用层直接使用”,但通过各 flip…() 链式方法,它在渲染管线里被频繁触发,仓库中可以找到多处真实证据:
- 背景(background)纹理采样:src/renderers/common/nodes/NodeManager.js 中通过
texture( background, screenUV.flipY() ).setUpdateMatrix( true )翻转屏幕空间 UV,把场景背景正确贴到全屏四边形上; - 反射平面默认 UV:src/nodes/utils/ReflectorNode.js 的
screenUV.flipX(); - 渐进式光照贴图:examples/jsm/misc/ProgressiveLightMapGPU.js 与
#L295中分别使用uv( 1 ).flipY()与uv().flipY().toVar(); - 阴影查看器:examples/jsm/utils/ShadowMapViewerGPU.js 中
textureLoad( new DepthTexture(), uv().flipY().mul( textureDimension ) ); - 示例页:examples/webgpu_materials.html 的
texture( uvTexture, screenUV.flipY() )、examples/webgpu_textures_2d-array_compressed.html 的uv().flipY()等。
从中可以看到一个通用模式:只要需要对采样坐标的纵向/横向做镜像,或修正坐标系方向差异,就可以直接对 uv() / screenUV 等节点链式调用 flipY() / flipX(),再交给 texture() 等节点消费。由于返回值仍是普通节点,翻转结果还可以继续参与 mul()、toVar() 等链式运算。
与 swizzle 家族节点的关系
FlipNode 不是孤立的——它在 TSL 内部与另外几个“分量操作”节点构成一套互补机制,它们共同注册在同一套原型循环中:
- 读取(swizzle):
.xxx、.rgba这类属性访问返回 SplitNode(TSLCore.js),用于抽取子分量; - 写入(assign):
setXYZW()系列与assign()使用 SetNode(TSLCore.js),用于覆盖指定分量; - 翻转(flip):
flipXYZW()/flipRGBA()/flipSTPQ()系列使用 FlipNode,用于对指定分量执行1 - x取补。
三者在 src/nodes/tsl/TSLCore.js 的 setProtoSwizzle 中共享同一套 xyzw / rgba / stpq 命名空间的循环展开机制,区别仅在于返回值分别落到 SplitNode、SetNode 与 FlipNode。此外 uv()、screenUV 等访问器与 texture() 等材质节点均从 src/nodes/Nodes.js 统一导出,FlipNode 的实例化对使用者完全透明。
小结与使用建议
- FlipNode 是 three.js TSL 着色器生成阶段的内部基础设施,承载“对归一化值做
x = 1 - x翻转”这一单一职责; - 应用层几乎永远不需要
new FlipNode(...),只需对任意节点调用flipX/Y/Z/W、flipR/G/B/A或flipS/T/P/Q(含flipXY等多分量组合与flipRGBA等别名),即可得到翻转后的新节点; - 翻转节点的类型由源节点推断(
generateNodeType覆写),生成期会先经临时变量保存源值,再产出如vec2( v.x, 1.0 - v.y )的着色器代码,确保复杂源表达式只求值一次; - 当需要镜像纹理坐标、反转坐标系朝向或做颜色取补时,优先使用
uv().flipY()、screenUV.flipX()这类链式写法,它们会干净地融入 TSL 的节点图与缓存机制。
如需继续深入,可对照阅读 FlipNode.js 的生成实现、TSLCore.js 的方法链注册逻辑,以及 TempNode 的基类行为。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00