three.js FrontFacingNode 完全指南:用 TSL 精准判断图元正面与背面
FrontFacingNode 是 three.js 节点材质体系(Node Material / TSL)中一个轻量但功能关键的内置节点,用于在片元着色阶段判断当前渲染的图元是正面(front facing)还是背面(back facing)。本文以 FrontFacingNode 官方文档 为主线,结合其在 TSL 源码、各渲染后端与内置材质中的实际调用链,系统讲解其类结构、生成逻辑、派生 TSL 符号(frontFacing、faceDirection、negateOnBackSide),并给出背面着色、双面法线修正等可直接落地的实战示例。读完本文,你将掌握如何在 WebGL 与 WebGPU 两条渲染管线下稳定地实现“正反面差异化”的材质效果。
一、节点的定位:它解决什么问题
在三维渲染中,每个三角形在投影后都有一个“旋向”(winding order)。渲染管线根据视口内的环绕方向判定该三角形相对观察者而言是正面还是背面,这一判定结果通常只存在于片元/片段阶段。经典的应用包括:
- 剖面/切片可视化:切开模型后,对暴露出的“内壁”(背面)赋予不同颜色或发光效果,用于区分内部结构;
- 双面材质修正:法线贴图、切线空间的扰动需要依赖面的朝向对法线做翻转修正,否则背面会出现光照“凹陷”;
- 体积外壳(shell)、边缘光、正反面剔除等效果。
FrontFacingNode 正是 three.js 节点体系中对这一内置判定能力的抽象。根据其官方文档描述:“This node can be used to evaluate whether a primitive is front or back facing.”(该节点可用于评估一个图元是正面还是背面)。
在类层次上,它继承自核心的 Node(EventDispatcher → Node → FrontFacingNode),输出类型为布尔值(bool)。
二、构造函数与类签名
new FrontFacingNode()
构造一个 front facing 判定节点,无任何参数。源码实现非常简洁:
// src/nodes/display/FrontFacingNode.js
class FrontFacingNode extends Node {
static get type() {
return 'FrontFacingNode';
}
constructor() {
super( 'bool' ); // 节点输出类型为布尔
this.isFrontFacingNode = true;
}
// ...
}
两点值得注意:
super( 'bool' )表明该节点的输出槽位是布尔量,可参与not()、and()等逻辑运算,或经运算转换为浮点值;static get type()返回'FrontFacingNode',这是 three.js 节点系统的序列化/类型标识,供节点加载与反序列化使用。
三、属性:.isFrontFacingNode
该节点只有一个公开属性:
| 属性 | 类型 | 说明 |
|---|---|---|
.isFrontFacingNode |
boolean(只读) | 用于类型测试的标志位,默认值为 true |
在代码中可通过该标志快速区分节点类型,例如:
import { FrontFacingNode } from 'three';
const node = new FrontFacingNode();
console.log( node.isFrontFacingNode ); // true
四、源码级剖析:generate() 的三种分支
节点的核心逻辑在 generate( builder ) 方法中(FrontFacingNode.js):
generate( builder ) {
// 仅片元阶段存在“面向性”概念,其余阶段恒定返回 true
if ( builder.shaderStage !== 'fragment' ) return 'true';
const { material } = builder;
// 若材质仅渲染背面,则必然处于背面
if ( material.side === BackSide ) {
return 'false';
}
// 其余情况下交给后端返回底层的着色语言内置量
return builder.getFrontFacing();
}
这条生成逻辑可以拆解为三条分支,每条都对应一个明确的物理语义:
- 非片元阶段(顶点/几何/计算等):着色管线只有在片元(fragment)阶段才提供面向性判定能力,因此顶点等其他阶段统一返回常量
true,保证在非片元阶段引用该节点时不会编译出错。 - 材质
.side === BackSide:当材质被设置为只渲染背面时,能进入片元的三角形必然是背面的,因此直接编译为常量false,可视为编译器层面的常量折叠优化。相应地,BackSide、FrontSide、DoubleSide均来自 constants.js。 - 其他情况(
FrontSide/DoubleSide):真正把判定交给底层渲染后端,即builder.getFrontFacing()。
底层内建量:gl_FrontFacing 与 WGSL front_facing
getFrontFacing() 是定义在 NodeBuilder 上的抽象方法,由各渲染后端的 Builder 实现:
- WebGL 后端(GLSLNodeBuilder)直接返回 GLSL 内建量:
getFrontFacing() {
return 'gl_FrontFacing';
}
- WebGPU 后端(WGSLNodeBuilder)则映射为 WGSL 的
front_facing内建量:
getFrontFacing() {
return this.getBuiltin( 'front_facing', 'isFront', 'bool' );
}
这意味着你通过 frontFacing 编写的材质逻辑可以不加修改地同时运行在 WebGL2 与 WebGPU 两条渲染管线上,这正是节点材质(Node Material / TSL)作为跨后端抽象层的价值所在。作为对照,three.js 传统着色器(非节点)中对应实现位于 normal_fragment_begin.glsl.js:
float faceDirection = gl_FrontFacing ? 1.0 : - 1.0;
五、TSL 便捷符号:frontFacing 与 faceDirection
直接 new FrontFacingNode() 在实际项目中并不常见。节点材质(TSL)体系在同文件中导出了不可变单例式访问器,使用上更加顺手:
export const frontFacing = /*@__PURE__*/ nodeImmutable( FrontFacingNode );
frontFacing 可直接参与 TSL 运算,例如:
import { frontFacing } from 'three/tsl';
// 当图元为背面时输出红色,正面输出白色
material.colorNode = frontFacing
.select( color( 0xffffff ), color( 0xff0000 ) );
由于 frontFacing 是布尔节点,它天然适配 TSL 的 If() 分支与 .not()、.select() 等运算。
faceDirection:把布尔翻转成 ±1
同一个模块还导出了派生符号 faceDirection,将布尔语义翻译成便于与向量相乘的数值语义:
// src/nodes/display/FrontFacingNode.js
export const faceDirection = /*@__PURE__*/ float( frontFacing ).mul( 2.0 ).sub( 1.0 );
其数学含义是:正面返回 1,背面返回 -1。这一形式在法线、切线等方向量需要按朝向翻转时极为常用——乘上 faceDirection 即可让背面片元的向量自动取反。
negateOnBackSide:按材质 .side 规则翻转向量
negateOnBackSide 封装了“若处于背面则翻转给定向量”的通用逻辑,并考虑了材质侧(side)配置的三种情况(FrontFacingNode.js):
export const negateOnBackSide = Fn( ( [ vector ], { material } ) => {
const side = material.side;
if ( side === BackSide ) {
// 只渲染背面:恒定取反
vector = vector.mul( - 1.0 );
} else if ( side === DoubleSide ) {
// 双面渲染:仅对背面片元取反(乘 faceDirection)
vector = vector.mul( faceDirection );
}
// FrontSide:保持不变
return vector;
} );
处理规则非常清晰,在编写时可直接当作规范来引用:
- 材质为
BackSide时:所见片元全部为背面,向量恒定乘以-1; - 材质为
DoubleSide时:正、背面都可能出现,乘以faceDirection(正面+1、背面-1)实现精确翻转; - 材质为
FrontSide(默认)时:向量原样返回,不做处理。
历史沿革:早期该函数名为
directionToFaceDirection(),自 r185 起重命名为negateOnBackSide()。旧名称在 FrontFacingNode.js 中保留了带warnOnce警告的兼容转发实现,新代码请统一使用negateOnBackSide。
六、three.js 内部如何消费这些符号
FrontFacingNode 不是孤立的存在,它深度嵌入了 three.js 的节点材质与法线计算管线。几个典型的内部调用点可以直接佐证其正确用法:
| 消费位置 | 用法 | 说明 |
|---|---|---|
| Normal.js | negateOnBackSide( node ) |
获取视图空间法线时,在非平面着色(flat shading)下按背向翻转法线,保证双面材质光照正确 |
| Tangent.js、Bitangent.js | negateOnBackSide( node ) |
切线/副切线同样随朝向翻转,维持 TBN 基的正确性 |
| NormalMapNode.js | negateOnBackSide( scale ) |
平面着色模式下,法线贴图强度因子按朝向取反(见 issue #28839 相关修复) |
| BumpMapNode.js | dot(...).mul( faceDirection ) |
凹凸映射的雅可比行列式乘以 faceDirection,避免双面模型凹凸方向颠倒 |
| MeshBasicNodeMaterial.js | negateOnBackSide( normalViewGeometry ) |
基础节点材质在构建视图法线时同样依赖该函数修正背面几何法线 |
这些既成事实印证了一个结论:任何涉及“方向量随面朝向翻转”的节点材质逻辑,都应优先复用 negateOnBackSide / faceDirection,而不是自行调用 frontFacing 做手写分支——前者已经完整处理了 BackSide / DoubleSide 的组合语义,后者则需要你自行重复实现这些判断。
上述 TSL 符号统一从 three/tsl 导出。在 Three.TSL.js 与 Three.TSL.js 中可分别看到 faceDirection 与 negateOnBackSide 的再导出记录,即按 import { faceDirection, negateOnBackSide } from 'three/tsl' 使用即可。
七、实战示例
示例 1:切片内壁(背面)着色
仓库自带的 webgpu_tsl_angular_slicing.html 是 frontFacing 最典型的工程化应用:它用角度蒙版切掉齿轮模型的外壳,并对剖切暴露出的“内壁”背面单独着色(示例关键代码):
// 切片材质:背面显示内部切面颜色
slicedMaterial.outputNode = Fn( () => {
const finalOutput = output;
// 若当前片元位于背面(内壁),则强制输出切面颜色
If( frontFacing.not(), () => {
finalOutput.assign( vec4( sliceColor, 1 ) );
} );
return finalOutput;
} )();
这里的 frontFacing.not() 语义即“此片元为背面”,配合 If() 分支即可在不改动模型几何的前提下,让切开后的内部截面呈现出与外壳截然不同的材质。配合 maskNode 控制切片蒙版(示例第 122–125 行),就构成了一个完整的剖面可视化方案。
示例 2:最小的正反面双色材质
不依赖外部模型,仅用一个带 side: DoubleSide 的平面即可直观验证节点效果:
import * as THREE from 'three';
import { color, frontFacing } from 'three/tsl';
const material = new THREE.MeshBasicNodeMaterial();
material.side = THREE.DoubleSide; // 双面渲染才能看到背面
material.colorNode = frontFacing
.select( color( 0xffffff ), color( 0x44aaff ) ); // 正面白、背面蓝
const mesh = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1 ), material );
当相机旋转到平面背面时,可见颜色切换为蓝色;若去掉 DoubleSide(保持默认 FrontSide),背面会被剔除,则永远只能看到白色正面。
示例 3:手动翻转向量与使用内置工具函数的等价写法
当你需要基于背向翻转某条自定义方向向量时,两种写法的对比:
import { frontFacing, negateOnBackSide, vec3 } from 'three/tsl';
// 写法 A:手写分支(正确但繁琐,需自行处理三种 side)
const myVec = vec3( 0, 1, 0 );
const flippedA = frontFacing.select( myVec, myVec.negate() );
// 写法 B:内置工具函数(推荐,自动兼容 BackSide/DoubleSide/FrontSide)
const flippedB = negateOnBackSide( myVec );
注意两种写法对 material.side === BackSide 场景的差异:写法 A 只在背面片元翻转,而写法 B 在 BackSide 材质下是恒定翻转。需要严格一致的行为时,应优先统一采用 negateOnBackSide 以避免歧义。
八、适用前提与注意事项
- 仅片元阶段有效:面向性判定是片元着色阶段的专属能力。
FrontFacingNode在顶点等其他阶段被编译为常量true(见 generate 实现),不要期望在顶点阶段基于它做变化。 - 依赖后端支持:
getFrontFacing()是抽象方法,仅由实际渲染后端(WebGL 的gl_FrontFacing、WebGPU 的front_facing内建量)提供具体实现,因此节点必须经由 WebGPURenderer / WebGLRenderer 的节点管线编译,脱离渲染器单独求值没有意义。 - 与
.side的联动:该节点返回结果的物理含义受材质side配置影响(BackSide材质中任何可见片元都是背面)。若在DoubleSide材质中结合背面剔除、模板测试等高级特性,需自行验证与预期的一致性。 - 弃用 API:
directionToFaceDirection()已自 r185 起更名为negateOnBackSide(),旧名称会触发一次性警告,应避免在新代码中使用。
结语
FrontFacingNode 虽然只有寥寥几十行代码,却是 three.js 节点材质中打通“几何朝向 → 片元决策”的关键枢纽:上承 GLSL gl_FrontFacing 与 WGSL front_facing 两套后端内建量,下接 frontFacing、faceDirection、negateOnBackSide 三个被法线、切线、凹凸与法线贴图等核心节点广泛复用的 TSL 符号。理解它的分支生成逻辑与派生符号语义,你就掌握了在节点材质中正确处理双面渲染、背面着色与方向量翻转的基础方法论。更进一步,可以阅读 docs/pages/FrontFacingNode.html.md 对照 API 文档,并结合 examples/webgpu_tsl_angular_slicing.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