首页
/ three.js 节点材质指南:Line2NodeMaterial 宽线渲染原理与实践

three.js 节点材质指南:Line2NodeMaterial 宽线渲染原理与实践

2026-09-07 18:02:38作者:乔或婵

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 路径的节点材质,通常配合 WebGPURendererexamples/jsm/lines/webgpu/Line2.jsexamples/jsm/lines/webgpu/LineSegments2.js 一起使用;若运行在 WebGLRenderer 下,需使用 lines/Line2.js(旧版非节点实现)。

二、继承体系与构造

继承关系(官方文档):

EventDispatcher → Material → NodeMaterial → Line2NodeMaterial

因此它天然具备 NodeMaterial 的 colorNodeopacityNodepositionNode 等全部节点属性,并可参与 TSL 场景图。

构造函数

new Line2NodeMaterial( parameters : Object )

parameters 为配置对象,默认 {}。构造逻辑(src/materials/nodes/Line2NodeMaterial.js#L409-L487)非常直白:

  1. 设置 isLine2NodeMaterial = true 类型标记;
  2. 调用 this.setDefaultValues( _defaultValues ),其中 _defaultValues 是一个复用的 new LineDashedMaterial() 实例(同文件第 18 行),用于继承传统材质默认值(如 color: 0xfffffflinewidth: 1transparent: false 等);
  3. 显式初始化文档列出的各项属性(vertexColorsdashOffsetoffsetNodedashScaleNodedashSizeNodegapSizeNodeblending);
  4. 依据 parameters.dashed 设定内部标记 _useDash,并固定 _useAlphaToCoverage = true_useWorldUnits = false
  5. 调用 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 0NoBlending 固定为 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

dashedworldUnitsalphaToCoverage 分别由 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)核心步骤:

  1. 读取实例属性instanceStartinstanceEnd(虚线还读取 instanceDistanceStart/instanceDistanceEnd)并变换到相机空间(modelViewMatrix)。
  2. 近平面裁剪:当线段一端在相机近平面之后时,用 trimSegmentAlphaL57-L69)计算插值系数把线段“截短”,避免线段穿过相机平面产生翻转错误。代码注释也提到该问题需针对 reversed depth buffer 做不同近平面估算(a > 0 判定)。
  3. 像素模式(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 空间,得到最终顶点。
  4. 世界单位模式(worldUnits=true)
    • 计算线段的世界方向 worldDirworldUpworldFwd 正交基,以世界空间的 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 ) —— 在片元阶段设置线条材质的漫反射颜色,覆盖基类实现以纳入线/虚线渲染与混合。

源码中的 setupDiffuseColorL495-L518)先调用 super.setupDiffuseColor(builder) 得到基础颜色,再将 alphaLine 节点产出的覆盖率乘入 diffuseColor.a

diffuseColor.a.mulAssign( alphaLine );

随后处理上面提到的 instanceColorStart/End 顶点颜色与 transparent 情况下的不透明背景合成。alphaLineL300-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 生效前提)。
  • 世界单位模式:此时片元需要真实几何判断。算法取片元所在视点光线与线段(worldStartworldEnd)求两条三维直线最近点,函数 closestLineToLineL82-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 提供了专用 raycastexamples/jsm/lines/webgpu/LineSegments2.js#L316-L415):

  • 先对 boundingSphere/boundingBox 按线宽扩展做快速剔除;
  • 像素模式下调用 raycastScreenSpace:把射线与线段投影到屏幕坐标做二维距离判断,因此要求 raycaster.camera 已设置,且线条至少被渲染过一次(视口分辨率 _resolutiononBeforeRender 中由 renderer.getViewport 采集);
  • 世界单位模式下调用 raycastWorldUnits:直接对世界空间线段求三维最近点判断;
  • 额外容差可通过 raycaster.params.Line2.threshold 传入,实际拾取线宽为 material.linewidth + threshold

LineSegments2#computeLineDistances(同上文件 L274-L301)则把相邻折线顶点的世界距离累计成 instanceDistanceStart/End,是虚线渲染的必备几何属性。参考示例 examples/webgpu_lines_fat_raycasting.html

八、与相关节点材质的边界

  • LineBasicNodeMaterial / LineDashedNodeMaterialsrc/materials/nodes/ 目录下)面向传统 gl.LINES 图元,无法自由控制宽度;examples/webgpu_lines_fat.html 同时创建了 Line + LineDashedNodeMaterialLine2 + Line2NodeMaterial,用于对比“系统线宽”与“宽线”两种方案的差异。
  • LineDashedMaterialLine2NodeMaterial 复用为默认值来源(其 dasheddashSizegapSizescaledashOffset 等字段语义一致,可视为宽线的 JS 属性接口来源)。
  • 该材质同目录下还有 VolumeNodeMaterialShadowNodeMaterial 等 WebGPU 专用节点材质,共同构成 three.js 新一代 NodeMaterial 家族。

九、调试建议与常见陷阱

  1. 虚线不出现:确认已调用 line.computeLineDistances(),且 material.dashed = true;通过 dashSize/gapSize 控制虚实比例(示例 GUI 提供 2:1、1:1、1:2 三档),dashScale 控制整体疏密(示例取 0.5~2),dashOffset 控制相位(0~5)。
  2. 抗锯齿无效alphaToCoverage 只在 MSAA 渲染器上生效;请确认 renderer.currentSamples > 0
  3. 透明度不可用blending 恒为 NoBlending,官方已声明 transparency 暂不支持,若强行设置 transparent: true 会走 setupDiffuseColor 中的不透明背景合成分支而非混合排序。
  4. 节点级精细控制:若默认属性不够,可把 dashSizeNodegapSizeNodedashScaleNodeoffsetNode 设为任意 TSL 表达式(如 time 驱动的动效节点),实现随时间流动的虚线等高级效果,这是老版 LineMaterial 无法比拟的扩展点。
  5. 类型识别:渲染器内部与业务代码可优先用只读标志 material.isLine2NodeMaterial === true 判断,它比 instanceof 更稳健(跨模块复制时不受影响)。

通过本文对官方 API 文档的逐项解读与源码印证,可以看到 Line2NodeMaterial 并非简单地把宽度乘大,而是由顶点端帽、相机近平面裁剪、虚线累计距离、片元覆盖度与光线求交共同组成的完整“宽线渲染子系统”。掌握上述属性与两条渲染管线(像素/世界单位)后,即可在 WebGPU 场景中稳定地产出抗锯齿、可虚线、可拾取、可动画的高质量线条。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388