首页
/ three.js TSL 中的 BitcastNode:在着色器中零成本重解释比特模式

three.js TSL 中的 BitcastNode:在着色器中零成本重解释比特模式

2026-09-06 20:33:02作者:平淮齐Percy

BitcastNode 是 three.js 节点着色语言(TSL)中用于"比特重解释"(bitcast)操作的节点:它不改变数值本身的二进制位,而是把同一串比特按另一种类型解读,典型场景是 float ↔ int/uint 之间的 IEEE-754 位模式转换。本文基于官方 API 文档页与 src/nodes/math/BitcastNode.js 源码,完整讲解其构造参数、四个便捷函数、GLSL/WGSL 双后端下的代码生成机制,以及测试用例验证的正确性结论,帮助你在 TSL 材质中安全地实现位级数据交换。

什么是比特重解释(Bitcast)

比特重解释与普通的数值转换(如 int(x) 这种会做舍入/截断的转换)有本质区别:bitcast 只改变"解读类型",不改变任何一位比特,因此它是无损的、可逆的。

GLSL 与 WGSL 都提供了一组内置的位重解释函数,three.js 的 TSL 则通过 BitcastNode 将这一能力封装成节点,使其可以像其他 TSL 节点一样参与组合、缓存与跨后端编译。官方 API 文档页对它的定义是:

This node represents an operation that reinterprets the bit representation of a value in one type as a value in another type.

一个广为人知的验证事实(同样被仓库测试用例采用):float1.0 的 IEEE-754 位模式是 0x3F800000,对应无符号整数值 1065353216。将 1.0f bitcast 为 int 得到的就是这个整数;反过来再 bitcast 回 float 恰好恢复 1.0

类层级与继承关系

文档页声明的继承链为:

EventDispatcher → Node → TempNode → BitcastNode

BitcastNode 继承自 TempNode。从源码结构看,TempNode 的作用是在节点被其他节点多次使用时生成临时变量缓存(hasDependencies 判断 usageCount > 1,并在 build 阶段生成一次 propertyName = snippet 的赋值语句),从而避免重复求值。也就是说,BitcastNode 默认享有 TSL 的中间结果缓存机制:同一个 bitcast 节点在表达式中被引用多次时,着色器中只会计算一次。

类还通过静态 getter 暴露了类型标识:

static get type() {
    return 'BitcastNode';
}

构造函数与属性

new BitcastNode( valueNode, conversionType, inputType = null )

参数 类型 说明
valueNode Node 待转换的源值节点。
conversionType string 目标类型(bitcast 后按此类型解读),如 'int''uint''float'
inputType string(可选) 期望的输入数据类型,默认为 null。传入时用于显式指定输入端类型(用于 GLSL 别名解析);不传时输入类型直接取自 valueNode 的实际节点类型。

对应源码中的属性声明(见 BitcastNode.js):

constructor( valueNode, conversionType, inputType = null ) {

    super();

    this.valueNode = valueNode;          // 待 bitcast 的数据
    this.conversionType = conversionType; // bitcast 的目标类型
    this.inputType = inputType;          // 期望的输入类型,默认 null
    this.isBitcastNode = true;          // 只读类型测试标志
}

属性一览(与文档页一一对应)

属性 类型 说明
.conversionType string 值将被转换(重解释)到的目标类型。
.inputType string 期望的输入数据类型,默认 null
.isBitcastNode boolean(只读) 类型测试标志,默认 true
.valueNode Node 参与 bitcast 的源数据节点。

TSL 便捷函数:bitcast 与四个方向封装

BitcastNode.js 底部除了默认导出的类,还导出了五个 TSL 函数(均标记 @tsl,可直接从 three/tsl 入口引入):

// 通用形式:重解释 x 的比特为类型 y
export const bitcast = nodeProxyIntent( BitcastNode ).setParameterLength( 2 );

// 四个方向化的便捷封装(固定 inputType)
export const floatBitsToInt  = ( value ) => new BitcastNode( value, 'int',   'float' );
export const floatBitsToUint = ( value ) => new BitcastNode( value, 'uint',  'float' );
export const intBitsToFloat  = ( value ) => new BitcastNode( value, 'float', 'int'   );
export const uintBitsToFloat = ( value ) => new BitcastNode( value, 'float', 'uint'  );

四个便捷函数分别对应 GLSL 同名的内置函数语义:floatBitsToInt 把浮点(或浮点向量)重解释为相同元素大小的有符号整数类型,其余三个方向以此类推。bitcast( x, y ) 则是通用形式——第二参数就是目标类型字符串,在官方 TSL 函数参考表中登记为:

bitcast( x, y ) — Reinterpret the bits of a value as a different type.(见 docs/TSL.md 的函数参考)

一个可直接对照的最小用法

下面的写法直接对应仓库测试用例 test/unit/addons/tsl/TSLBitOps.tests.js 中在 GPU 上执行并断言通过的表达式:

import { bitcast, float, int, floatBitsToInt, intBitsToFloat } from 'three/tsl';

// float → int:1.0 的位模式是 0x3F800000,即 1065353216
bitcast( float( 1.0 ), 'int' ) === int( 1065353216 );      // 等价于 floatBitsToInt( float( 1.0 ) )

// int → float:0x3F800000 重解释回 float 恰好是 1.0
intBitsToFloat( int( 1065353216 ) ) === float( 1.0 );

// 往返无损:bitcast 是纯重解释,转出去再转回来必须逐位一致
bitcast( bitcast( float( 3.140625 ), 'int' ), 'float' ) === float( 3.140625 );

源码深潜:类型解析与代码生成

BitcastNode 的核心在 generateNodeTypegenerate 两个方法(见 BitcastNode.js),它们决定了节点最终输出的类型与着色器片段。

generateNodeType:决定输出类型

generateNodeType( builder ) {

    // GLSL aliasing
    if ( this.inputType !== null ) {

        const valueType = this.valueNode.getNodeType( builder );
        const valueLength = builder.getTypeLength( valueType );

        return builder.getTypeFromLength( valueLength, this.conversionType );

    }

    return this.conversionType;
}

逻辑分两支:

  • 未指定 inputType:输出类型就是 conversionType 本身(标量情形,如 float);
  • 指定了 inputType:保留源值的"分量个数"(getTypeLength,如 vec4 长度为 4),再用 getTypeFromLength 以目标分量类型重组,即 vec4 → ivec4 这类向量形式的对齐——注释中写明这是为了 GLSL 别名(aliasing)场景。

generate:拼出最终着色器表达式

generate( builder ) {

    const type = this.getNodeType( builder );
    let inputType = '';

    if ( this.inputType !== null ) {

        const valueType = this.valueNode.getNodeType( builder );
        const valueTypeLength = builder.getTypeLength( valueType );

        inputType = valueTypeLength === 1
            ? this.inputType
            : builder.changeComponentType( valueType, this.inputType );

    } else {

        inputType = this.valueNode.getNodeType( builder );

    }

    return `${ builder.getBitcastMethod( type, inputType ) }( ${ this.valueNode.build( builder, inputType ) } )`;
}

其中:

  • inputType 已声明且源值是向量,则调用 NodeBuilder.changeComponentType 把向量类型的分量换成声明的分量类型(源码注释举例:vec4uvec4),标量时直接使用声明类型;
  • inputTypenull,输入类型直接取 valueNode 的实际节点类型;
  • 最终调用 builder.getBitcastMethod( 输出类型, 输入类型 ) 得到具体的内置函数名,再包裹源值构建出的表达式。

后端映射:GLSL 与 WGSL 各自的实现

getBitcastMethod 由两个渲染器后端分别实现,这正是 BitcastNode 跨 WebGL/WebGPU 可用的关键。

WebGL(GLSL)后端

GLSLNodeBuilderbitcast_${inputType}_${type} 的键名查表:

const glslMethods = {
    // ...
    bitcast_float_int:  'floatBitsToInt',
    bitcast_int_float:  'intBitsToFloat',
    bitcast_uint_float: 'uintBitsToFloat',
    bitcast_float_uint: 'floatBitsToUint',
    bitcast_uint_int:   'tsl_bitcast_uint_to_int',
    bitcast_int_uint:   'tsl_bitcast_int_to_uint',
    // ...
};

前四个方向直接落到 GLSL ES 内置函数;int ↔ uint 两个方向在 GLSL ES 3.0 中没有内置 bitcast,因此仓库提供了 polyfill(见 GLSLNodeBuilder.js):

uint tsl_bitcast_int_to_uint ( int x ) { return floatBitsToUint( intBitsToFloat ( x ) ); }
uint tsl_bitcast_uint_to_int ( uint x ) { return floatBitsToInt( uintBitsToFloat ( x ) ); }

即经由 float 中转两次重解释完成 int↔uint 切换。这也解释了为什么 generateinputType 的声明能驱动正确的 GLSL 别名选择。

WebGPU(WGSL)后端

WGSLNodeBuilder.getBitcastMethod 直接生成 WGSL 的泛型语法:

getBitcastMethod( type ) {
    const dataType = this.getType( type );
    return `bitcast<${ dataType }>`;
}

即同一个 BitcastNode 在 WebGPU 下会输出如 bitcast<i32>( x )bitcast<f32>( x ) 这样的 WGSL 表达式。仓库中的 TSL→WGSL 转译器 WGSLEncoder 也维护了同样的方向映射(floatBitsToInt → bitcast<i32>intBitsToFloat → bitcast<f32> 等),可作为转译链路中这一映射的旁证。

正确性验证:测试用例说明了什么

单元测试 TSLBitOps.tests.jsgpuTest 在真实 GPU 上下文中执行断言,覆盖了三类关键性质:

  1. 方向正确性bitcast( float( 1.0 ), 'int' ) 精确等于 int( 1065353216 )0x3F800000),反向 bitcast( int( 1065353216 ), 'float' ) 精确等于 float( 1.0 )
  2. 往返无损性3.140625 经过 float → int → float 后逐位恢复原值,确认 bitcast 是纯重解释而非数值转换;
  3. 与便捷封装的一致性:测试注释明确指出,通用 bitcast() 构造器与 floatBitsToInt() / uintBitsToFloat() 等便捷封装共用同一个节点实现,便捷函数只是"薄封装"(thin calls)。

使用场景与注意事项

  • 典型场景:在 TSL 中把整型数据按浮点解读(如半分辨率打包/解包流程中的中间步骤、噪声或哈希函数内部处理位模式)、跨类型传递同一位模式而又不引入转换误差。仓库示例 webgl_buffergeometry_attributes_none.html 中就出现了对 uintBitsToFloat 的原始 GLSL 用法(在解包 12 位定点随机数后 uintBitsToFloat( m ) - 1.0),说明这类位操作是图形程序中的常见手法。
  • 元素大小必须匹配:四个方向化封装的语义都强调"to a corresponding ... type with the same element size",即 32 位 float ↔ 32 位 int/uint 这类等宽转换,这也是 GLSL/WGSL 内置函数的前提。
  • inputType 何时该传:走便捷封装(floatBitsToInt 等)时 inputType 已被固定;只有在直接用 new BitcastNode 或通用 bitcast() 且需要显式约束输入端类型(尤其是 GLSL 向量别名场景)时才需要自行传入。
  • 跨后端行为一致:同一 TSL 表达式在 WebGL 下展开为 GLSL 内置函数(含 int↔uint 的 polyfill),在 WebGPU 下展开为 bitcast<T> 泛型调用,开发者无需为两套后端编写分支代码。

参考路径

内容 路径
节点实现 src/nodes/math/BitcastNode.js
基类 TempNode(临时变量缓存) src/nodes/core/TempNode.js
类型分量改写工具 src/nodes/core/NodeBuilder.js
GLSL 后端方法与 polyfill src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js
WGSL 后端方法 src/renderers/webgpu/nodes/WGSLNodeBuilder.js
GPU 测试用例 test/unit/addons/tsl/TSLBitOps.tests.js
TSL 函数参考 docs/TSL.md
官方 API 文档页 docs/pages/BitcastNode.html
登录后查看全文
热门项目推荐
相关项目推荐