three.js TSL 中的 BitcastNode:在着色器中零成本重解释比特模式
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.
一个广为人知的验证事实(同样被仓库测试用例采用):float 值 1.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 的核心在 generateNodeType 与 generate 两个方法(见 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 把向量类型的分量换成声明的分量类型(源码注释举例:vec4→uvec4),标量时直接使用声明类型; - 若
inputType为null,输入类型直接取valueNode的实际节点类型; - 最终调用
builder.getBitcastMethod( 输出类型, 输入类型 )得到具体的内置函数名,再包裹源值构建出的表达式。
后端映射:GLSL 与 WGSL 各自的实现
getBitcastMethod 由两个渲染器后端分别实现,这正是 BitcastNode 跨 WebGL/WebGPU 可用的关键。
WebGL(GLSL)后端
GLSLNodeBuilder 按 bitcast_${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 切换。这也解释了为什么 generate 中 inputType 的声明能驱动正确的 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.js 用 gpuTest 在真实 GPU 上下文中执行断言,覆盖了三类关键性质:
- 方向正确性:
bitcast( float( 1.0 ), 'int' )精确等于int( 1065353216 )(0x3F800000),反向bitcast( int( 1065353216 ), 'float' )精确等于float( 1.0 ); - 往返无损性:
3.140625经过float → int → float后逐位恢复原值,确认 bitcast 是纯重解释而非数值转换; - 与便捷封装的一致性:测试注释明确指出,通用
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 |
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 StartedRust0624
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