首页
/ three.js FlipNode 源码解析:理解 TSL 中的 `x = 1 - x` 分量翻转机制

three.js FlipNode 源码解析:理解 TSL 中的 `x = 1 - x` 分量翻转机制

2026-09-06 18:59:24作者:廉彬冶Miranda

本篇技术指南围绕 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 命名空间并同时生成 rgbastpq 的等价别名,覆盖从单分量到四分量(xxyxyzxyzw,以及对应的 r…s… 写法)的全部组合。因此:

  • uvNode.flipY()flip + Y,归一化为 'y',排序后仍是 'y',等价于 uvNode.flipT()(t 即纹理坐标 y 轴);
  • colorNode.flipRGBA() → 归一化得到 'xyzw',翻转全部四个颜色分量;
  • uvNode.flipXB() 之类则是先归一化为 'xz' 并排序后传入。

也就是说:传给 FlipNodecomponents 始终是以 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.jsgetNodeType() 中,类型会按节点数据缓存起来,避免重复推导;默认实现(Node.js)返回 this.nodeType,而 FlipNode 覆写后的 generateNodeType 则改从 sourceNode 侧递归取得类型。这与基类 TempNodeTempNode)的默认行为不同,也是文档中特别标注“Overrides”的原因。

.generate( builder : NodeBuilder ):源码级的翻转生成逻辑

generate() 才是翻转真正发生的地方,完整逻辑在 FlipNode.js。逐行拆解:

  1. 取类型、构建源节点、分配临时变量
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.jsbuild() 对重复节点生成临时变量的处理)。

  1. 按类型长度逐分量拼装输出
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…() 链式方法,它在渲染管线里被频繁触发,仓库中可以找到多处真实证据:

从中可以看到一个通用模式:只要需要对采样坐标的纵向/横向做镜像,或修正坐标系方向差异,就可以直接对 uv() / screenUV 等节点链式调用 flipY() / flipX(),再交给 texture() 等节点消费。由于返回值仍是普通节点,翻转结果还可以继续参与 mul()toVar() 等链式运算。

与 swizzle 家族节点的关系

FlipNode 不是孤立的——它在 TSL 内部与另外几个“分量操作”节点构成一套互补机制,它们共同注册在同一套原型循环中:

  • 读取(swizzle).xxx.rgba 这类属性访问返回 SplitNodeTSLCore.js),用于抽取子分量;
  • 写入(assign)setXYZW() 系列与 assign() 使用 SetNodeTSLCore.js),用于覆盖指定分量;
  • 翻转(flip)flipXYZW() / flipRGBA() / flipSTPQ() 系列使用 FlipNode,用于对指定分量执行 1 - x 取补。

三者在 src/nodes/tsl/TSLCore.jssetProtoSwizzle 中共享同一套 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/WflipR/G/B/AflipS/T/P/Q(含 flipXY 等多分量组合与 flipRGBA 等别名),即可得到翻转后的新节点;
  • 翻转节点的类型由源节点推断(generateNodeType 覆写),生成期会先经临时变量保存源值,再产出如 vec2( v.x, 1.0 - v.y ) 的着色器代码,确保复杂源表达式只求值一次;
  • 当需要镜像纹理坐标、反转坐标系朝向或做颜色取补时,优先使用 uv().flipY()screenUV.flipX() 这类链式写法,它们会干净地融入 TSL 的节点图与缓存机制。

如需继续深入,可对照阅读 FlipNode.js 的生成实现、TSLCore.js 的方法链注册逻辑,以及 TempNode 的基类行为。

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