three.js 节点材质指南:Line2NodeMaterial 宽线渲染原理与实践
Line2NodeMaterial 是 three.js 基于 TSL(Three Shading Language)节点体系实现的「宽线(fat line)」节点材质,它绕开 WebGL 对线段宽度的 1 像素限制,把线条表示成实例化(instanced)网格来获得任意宽度。本文以官方 API 文档为核心骨架,结合 Line2NodeMaterial 源码 与示例,讲解其继承体系、全部属性/方法语义、虚线(dashed)与像素/世界单位(worldUnits)两种尺寸模型背后的着色器实现,帮助读者完整掌握这套 WebGPU 下高质量线条渲染方案。
一、它解决什么问题:从 1px 线条到可扩展的宽线
原生 WebGL / three.js 的 Line 对象基于 gl.LINES/gl.LINE_STRIP 图元绘制,硬件对线宽的支持通常被钳制在 1px,无法在 WebGPU 路径下渲染宽线、虚线或圆头线帽。Line2NodeMaterial 采用与 examples/jsm/lines/ 中 WebGL 版 LineMaterial(见 examples/jsm/lines/LineMaterial.js)相同的思想:用实例化网格代替图元——每条线段在几何体中被拆成 instanced quad,顶点着色器负责把线段在屏幕/世界中“撑宽”,片元着色器负责计算覆盖度与形状。
官方文档将其核心特征概括为一句:This node material can be used to render lines with a size larger than one by representing them as instanced meshes.(该节点材质通过把线条表示为实例化网格,用于渲染宽度大于 1 的线段。)与传统的 LineMaterial(基于 JS 侧 Uniform 与旧版 onBeforeCompile)不同,Line2NodeMaterial 把整条渲染管线用 TSL 节点重写,完全并入 three.js 新一代 NodeMaterial 体系,其实现位于 src/materials/nodes/Line2NodeMaterial.js,并经由 src/materials/nodes/NodeMaterials.js 统一导出,可从 three/webgpu 模块直接引入。
使用前提:该材质是 WebGPU 路径的节点材质,通常配合 WebGPURenderer 与 examples/jsm/lines/webgpu/Line2.js、examples/jsm/lines/webgpu/LineSegments2.js 一起使用;若运行在 WebGLRenderer 下,需使用 lines/Line2.js(旧版非节点实现)。
二、继承体系与构造
继承关系(官方文档):
EventDispatcher → Material → NodeMaterial → Line2NodeMaterial
因此它天然具备 NodeMaterial 的 colorNode、opacityNode、positionNode 等全部节点属性,并可参与 TSL 场景图。
构造函数
new Line2NodeMaterial( parameters : Object )
parameters 为配置对象,默认 {}。构造逻辑(src/materials/nodes/Line2NodeMaterial.js#L409-L487)非常直白:
- 设置
isLine2NodeMaterial = true类型标记; - 调用
this.setDefaultValues( _defaultValues ),其中_defaultValues是一个复用的new LineDashedMaterial()实例(同文件第 18 行),用于继承传统材质默认值(如color: 0xffffff、linewidth: 1、transparent: false等); - 显式初始化文档列出的各项属性(
vertexColors、dashOffset、offsetNode、dashScaleNode、dashSizeNode、gapSizeNode、blending); - 依据
parameters.dashed设定内部标记_useDash,并固定_useAlphaToCoverage = true、_useWorldUnits = false; - 调用
this.setValues( parameters )让传入参数覆盖默认值。
典型的用法来自 examples/webgpu_lines_fat.html:
import { Line2NodeMaterial } from 'three/webgpu';
import { LineGeometry } from 'three/addons/lines/LineGeometry.js';
import { Line2 } from 'three/addons/lines/webgpu/Line2.js';
const geometry = new LineGeometry();
geometry.setPositions( positions ); // 折线顶点序列
geometry.setColors( colors ); // 可选,逐实例颜色
const material = new Line2NodeMaterial( {
color: 0xffffff,
linewidth: 5, // worldUnits=false 时单位为像素
vertexColors: true,
dashed: false,
alphaToCoverage: false,
} );
const line = new Line2( geometry, material );
line.computeLineDistances(); // 虚线渲染前必须调用
scene.add( line );
Line2 的默认材质即 new Line2NodeMaterial( { color: Math.random() * 0xffffff } )(见 examples/jsm/lines/webgpu/Line2.js#L27),所以即便不传材质也能直接得到一个随机颜色的宽线。
三、属性详解(含默认值与源码语义)
下表完整覆盖官方文档属性条目,并补充底层行为:
| 属性 | 类型 | 默认值 | 语义 |
|---|---|---|---|
alphaToCoverage |
boolean | true |
是否启用 alpha-to-coverage 抗锯齿(仅在渲染器开启 MSAA、renderer.currentSamples > 0 时生效) |
blending |
number | 0(NoBlending) |
固定为 NoBlending,因为透明度暂不支持 |
dashOffset |
number | 0 |
虚线偏移量,叠加在沿线累计距离上 |
dashScaleNode |
Node<float> | null |
自定义虚线缩放节点;未设置时回退到内置 materialLineScale |
dashSizeNode |
Node<float> | null |
自定义虚线段长度节点;未设置时回退到内置 materialLineDashSize |
dashed |
boolean | false |
是否渲染虚线 |
gapSizeNode |
Node<float> | null |
自定义间隔长度节点;未设置时回退到内置 materialLineGapSize |
isLine2NodeMaterial |
boolean(只读) | true |
类型测试标志 |
lineColorNode |
Node<vec3> | — | 已废弃(r185),请改用 NodeMaterial#colorNode |
offsetNode |
Node<float> | null |
自定义虚线偏移节点;未设置时回退到内置 materialLineDashOffset |
vertexColors |
boolean | false |
是否启用逐实例顶点颜色(需几何体提供 instanceColorStart/instanceColorEnd) |
worldUnits |
boolean | false |
false 时线宽单位为像素;true 时线宽单位为世界单位(带透视大小衰减) |
3.1 三个“开关型”属性与 needsUpdate
dashed、worldUnits、alphaToCoverage 分别由 getter/setter 包裹,实际状态存储于内部标记 _useDash、_useWorldUnits、_useAlphaToCoverage(源码 L569-L630)。它们的 setter 在值发生真实变化时会置 this.needsUpdate = true,通知渲染器重新编译节点,因此运行时切换这三种模式后无需手动调用任何更新方法(示例中的 GUI 也会额外再设一次 needsUpdate,属冗余保险写法)。
3.2 lineColorNode 的弃用与替代
官方文档明确标注:lineColorNode 自 r185 起弃用。从源码看(L548-L560),它已成为 colorNode 的别名:
get lineColorNode() { return this.colorNode; }
set lineColorNode( value ) {
warnOnce( 'Line2NodeMaterial: "lineColorNode" has been deprecated. Use "colorNode" instead.' );
this.colorNode = value;
}
即设置它时打印一次性告警并透传给 colorNode。新代码应直接使用 material.colorNode 或用 color 构造参数赋色。
3.3 vertexColors 与实例颜色属性
与普通 BufferGeometry 的 color 属性不同,宽线材质读取的是实例化颜色属性 instanceColorStart/instanceColorEnd(由 examples/jsm/lines/LineSegmentsGeometry.js 从用户传入的颜色数组交错打包而来,每段两条端点各一个 RGB)。片元阶段通过 positionGeometry.y < 0.5 选择起点/终点颜色并乘入 diffuseColor.rgb(源码 L501-L510)。这解释了示例中 vertexColors: true 必须配合 geometry.setColors(...) 的原因。
四、顶点管线:自定义 MVP 与线段“撑宽”算法
官方文档记录了该方法的作用:
.setupModelViewProjection( builder : NodeBuilder ) : Node<vec4>—— 设置宽线在顶点阶段的 clip space 位置,覆盖默认的 model-view-projection,返回扩展后的宽线顶点坐标。
需要说明:文档以 setupModelViewProjection 命名记录该职责;在当前仓库主分支源码中,顶点阶段逻辑已被拆分为一个独立的 mvpLine TSL 自定义节点,并由 setupPosition 方法驱动(源码 L526-L540)。整体流程如下。
4.1 setupPosition:从 clip 空间反算局部顶点
setupPosition( builder ) {
const localPosition = modelWorldMatrixInverse.mul( cameraWorldMatrix )
.mul( cameraProjectionMatrixInverse ).mul( mvpLine );
positionLocal.assign( localPosition.xyz.div( localPosition.w ) );
// ...
return super.setupPosition( builder );
}
mvpLine 输出的是“撑宽后的 clip 坐标”,材质再通过“模型世界逆 × 相机世界 × 相机投影逆”把它反算回局部坐标交给通用顶点流程,相当于把一条线段从局部空间映射到屏幕后再扩展,绕开了常规 MVP 的限制。
4.2 mvpLine:像素模式与世界单位模式的分支
mvpLine(源码 L112-L291)核心步骤:
- 读取实例属性:
instanceStart、instanceEnd(虚线还读取instanceDistanceStart/instanceDistanceEnd)并变换到相机空间(modelViewMatrix)。 - 近平面裁剪:当线段一端在相机近平面之后时,用
trimSegmentAlpha(L57-L69)计算插值系数把线段“截短”,避免线段穿过相机平面产生翻转错误。代码注释也提到该问题需针对 reversed depth buffer 做不同近平面估算(a > 0判定)。 - 像素模式(worldUnits=false):
- 投影到 NDC,取线段方向
dir,乘以视口宽高比aspect = viewport.z / viewport.w归一化得到屏幕方向; - 法线方向
offset = (dir.y, -dir.x)作为垂直于线段的屏幕偏移,按positionGeometry.x正负翻向两侧,按positionGeometry.y判断是起点还是终点,并沿dir前后推出端帽(y<0减、y>1加,形成两端矩形延展); offset乘以materialLineWidth(线宽)、除以viewport.w / screenDPR(NDC→屏幕换算)、最后乘以clip.w恢复 clip 空间,得到最终顶点。
- 投影到 NDC,取线段方向
- 世界单位模式(worldUnits=true):
- 计算线段的世界方向
worldDir、worldUp、worldFwd正交基,以世界空间的hw = materialLineWidth * 0.5为半宽把顶点偏移到线段两侧,并沿worldDir添加端帽; - 渲染虚线时不加端帽(
if ( ! useDash )分支,L224-L239),因为虚线模式会丢弃端帽片元,避免多出不该有的顶点; - 投影得到 clip 坐标,再把 z 值替换为原始线段对应端点的 NDC z 以保证深度衔接正确。
- 计算线段的世界方向
4.3 虚线时的沿线累计距离
在 useDash 分支中(L178-L188),按顶点的 positionGeometry.y < 0.5 选择端点累计距离 distanceStart/distanceEnd,乘上 dashScaleNode || materialLineScale,再加上 offsetNode || materialLineDashOffset,存入 varying lineDistance 供片元阶段做虚线区间判断。这正是 Line2#computeLineDistances()(examples/jsm/lines/webgpu/LineSegments2.js#L274-L301)所做的工作——它生成 instanceDistanceStart/instanceDistanceEnd 交错缓冲并挂到几何体上,因此虚线渲染前必须调用 line.computeLineDistances()。
五、片元管线:覆盖度、虚线间隔与圆帽
官方文档记录的片元侧入口为:
.setupDiffuseColor( builder : NodeBuilder )—— 在片元阶段设置线条材质的漫反射颜色,覆盖基类实现以纳入线/虚线渲染与混合。
源码中的 setupDiffuseColor(L495-L518)先调用 super.setupDiffuseColor(builder) 得到基础颜色,再将 alphaLine 节点产出的覆盖率乘入 diffuseColor.a:
diffuseColor.a.mulAssign( alphaLine );
随后处理上面提到的 instanceColorStart/End 顶点颜色与 transparent 情况下的不透明背景合成。alphaLine(L300-L388)承担所有形状判定:
- 虚线(useDash):用
uv().y判断端帽区域(vUv.y < -1 || vUv.y > 1)直接discard;核心区间用模运算lineDistance % (dashSize + gapSize) > dashSize决定丢弃(即只保留 dash 段),dashSize/gapSize优先取材质节点的dashSizeNode/gapSizeNode,否则用内置materialLineDashSize/materialLineGapSize。 - 像素模式圆帽:端帽区域(
abs(vUv.y) > 1)内做圆判定len2 > 1丢弃;若开启 MSAA 且renderer.currentSamples > 0,改用smoothstep生成渐变 alpha 形成抗锯齿圆帽边缘(useAlphaToCoverage生效前提)。 - 世界单位模式:此时片元需要真实几何判断。算法取片元所在视点光线与线段(
worldStart→worldEnd)求两条三维直线最近点,函数closestLineToLine(L82-L103)返回参数化最近点;再用片元到线段的距离len除以线宽得到归一化距离norm,MSAA 下用fwidth做平滑过渡,否则norm > 0.5丢弃。
由此可见 alphaToCoverage 的真正语义:只在多采样开启时提供 1px 级抗锯齿边缘,否则回退到硬边 discard。这也是示例示例材质里默认 alphaToCoverage: false(与构造函数默认 true 不同)仍能正确渲染的原因——是否启用取决于渲染器采样数。
六、像素单位还是世界单位:worldUnits 模式选择
worldUnits 是宽线材质最核心的视觉开关,官方文档说明:When set to false the unit is pixel.(为 false 时单位为像素。)
| 场景 | 推荐设置 | 效果 |
|---|---|---|
| 类似 CAD / 蓝图叠加层、UI 标注线,希望线宽恒定不受相机远近影响 | worldUnits = false |
线宽固定为像素(默认,1~10px 典型取值) |
| 希望线在 3D 世界中拥有“真实粗细”,远小近大 | worldUnits = true |
线宽以世界单位计(示例 GUI 中典型 0.1~0.5) |
在 examples/webgpu_lines_fat.html 的 GUI 中切换 world units 时,宽度滑杆范围会从 1~10(pixels) 切换到 0.1~0.5(world units),正是上述两种模式的直观演示。世界单位模式还改变了深度写入与光线拾取(raycast)方式,见下文。
七、拾取(Raycast)与虚线所需的配套属性
宽线以实例网格渲染,不能依赖 WebGL 默认的三角形拾取。LineSegments2 提供了专用 raycast(examples/jsm/lines/webgpu/LineSegments2.js#L316-L415):
- 先对
boundingSphere/boundingBox按线宽扩展做快速剔除; - 像素模式下调用
raycastScreenSpace:把射线与线段投影到屏幕坐标做二维距离判断,因此要求raycaster.camera已设置,且线条至少被渲染过一次(视口分辨率_resolution在onBeforeRender中由renderer.getViewport采集); - 世界单位模式下调用
raycastWorldUnits:直接对世界空间线段求三维最近点判断; - 额外容差可通过
raycaster.params.Line2.threshold传入,实际拾取线宽为material.linewidth + threshold。
LineSegments2#computeLineDistances(同上文件 L274-L301)则把相邻折线顶点的世界距离累计成 instanceDistanceStart/End,是虚线渲染的必备几何属性。参考示例 examples/webgpu_lines_fat_raycasting.html。
八、与相关节点材质的边界
LineBasicNodeMaterial/LineDashedNodeMaterial(src/materials/nodes/ 目录下)面向传统gl.LINES图元,无法自由控制宽度;examples/webgpu_lines_fat.html 同时创建了Line+LineDashedNodeMaterial与Line2+Line2NodeMaterial,用于对比“系统线宽”与“宽线”两种方案的差异。LineDashedMaterial被Line2NodeMaterial复用为默认值来源(其dashed、dashSize、gapSize、scale、dashOffset等字段语义一致,可视为宽线的 JS 属性接口来源)。- 该材质同目录下还有
VolumeNodeMaterial、ShadowNodeMaterial等 WebGPU 专用节点材质,共同构成 three.js 新一代NodeMaterial家族。
九、调试建议与常见陷阱
- 虚线不出现:确认已调用
line.computeLineDistances(),且material.dashed = true;通过dashSize/gapSize控制虚实比例(示例 GUI 提供 2:1、1:1、1:2 三档),dashScale控制整体疏密(示例取 0.5~2),dashOffset控制相位(0~5)。 - 抗锯齿无效:
alphaToCoverage只在 MSAA 渲染器上生效;请确认renderer.currentSamples > 0。 - 透明度不可用:
blending恒为NoBlending,官方已声明 transparency 暂不支持,若强行设置transparent: true会走setupDiffuseColor中的不透明背景合成分支而非混合排序。 - 节点级精细控制:若默认属性不够,可把
dashSizeNode、gapSizeNode、dashScaleNode、offsetNode设为任意 TSL 表达式(如time驱动的动效节点),实现随时间流动的虚线等高级效果,这是老版LineMaterial无法比拟的扩展点。 - 类型识别:渲染器内部与业务代码可优先用只读标志
material.isLine2NodeMaterial === true判断,它比instanceof更稳健(跨模块复制时不受影响)。
通过本文对官方 API 文档的逐项解读与源码印证,可以看到 Line2NodeMaterial 并非简单地把宽度乘大,而是由顶点端帽、相机近平面裁剪、虚线累计距离、片元覆盖度与光线求交共同组成的完整“宽线渲染子系统”。掌握上述属性与两条渲染管线(像素/世界单位)后,即可在 WebGPU 场景中稳定地产出抗锯齿、可虚线、可拾取、可动画的高质量线条。
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 StartedRust0629
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