three.js LineDashedNodeMaterial 指南:虚线节点材质的属性覆盖、TSL 接入与着色器变体原理
LineDashedNodeMaterial 是 three.js 中 LineDashedMaterial(经典虚线线段材质)的 NodeMaterial 版本,它把虚线渲染所需的 dash 尺寸、gap 尺寸、缩放与偏移量全部暴露为可编程节点属性,可直接融入 TSL(Three Shading Language)着色流程。本文以该类的 官方 API 文档 为骨架,结合其在 src/materials/nodes/LineDashedNodeMaterial.js 中的完整实现与相关节点源码,讲解其构造方式、每个属性的语义与默认值、setupVariants() 背后真实的虚线剔除逻辑,并给出可运行的实战示例,帮助读者在使用 WebGL/WebGPU 渲染器绘制虚线几何体时灵活实现动态虚线、程序化虚线等效果。
一、类定位:继承链与它在 three.js 中的角色
从文档开头的继承链可以看出:
EventDispatcher → Material → NodeMaterial → LineDashedNodeMaterial
即它继承自 NodeMaterial,而 NodeMaterial 是 three.js 节点体系的核心材质基类。因此 LineDashedNodeMaterial 拥有节点材质的一切能力:colorNode、opacityNode 等可编程属性、通过 .setup()/.setupVariants() 参与着色器编译等,同时也必须满足普通线段材质的使用前提(例如几何体需要携带逐顶点距离数据)。
在实现层面,源码用一个纯函数生成的经典材质实例作为默认值模板:
const _defaultValues = /*@__PURE__*/ new LineDashedMaterial();
// src/materials/nodes/LineDashedNodeMaterial.js 第 9 行
构造函数通过 this.setDefaultValues( _defaultValues ) 把 LineDashedMaterial 的默认参数(scale = 1、dashSize = 3、gapSize = 1)继承到节点材质上,再经由 this.setValues( parameters ) 应用用户传入的构造参数。这样既保证了与传统材质参数写法完全兼容(color、transparent、opacity 等直接可用),又在此之上叠加了专属的节点属性。
二、构造函数与 parameters
new LineDashedNodeMaterial( parameters : Object )
构造一个新的虚线节点材质。parameters 是可选的配置对象,其内容可以是材质继承链上任意可写属性(包括 NodeMaterial 与 Material 的属性),取值规则与经典材质一致(颜色可用任何 Color.set() 接受的类型)。
典型用法与传统材质几乎无差别:
const material = new THREE.LineDashedNodeMaterial( {
color: 0xffffff,
transparent: true,
scale: 1,
dashSize: 3,
gapSize: 1,
} );
// 虚线依赖逐顶点 lineDistance,需要先计算线段距离
const geometry = new THREE.BufferGeometry().setFromPoints( points );
geometry.computeLineDistances();
const line = new THREE.Line( geometry, material );
仓库示例 examples/webgpu_lines_fat.html 中正是用构造参数方式创建了 LineDashedNodeMaterial,并配合 scale: 2, dashSize: 1, gapSize: 1 与 computeLineDistances() 使用。
三、属性全景:经典属性 + 六个可编程节点属性
经典属性(来自 LineDashedMaterial 默认值)
| 属性 | 类型 | 默认值 | 含义 |
|---|---|---|---|
.scale |
number | 1 |
虚线整体缩放系数,可理解为“世界单位 ↔ 线段距离”的换算比例 |
.dashSize |
number | 3 |
单个虚线段(实线笔画)的长度 |
.gapSize |
number | 1 |
相邻虚线段之间空白间隔的长度 |
.dashOffset |
number | 0 |
虚线起始位置的偏移量 |
上述默认值定义于 LineDashedMaterial 构造函数,并通过默认值模板被节点材质继承。
专属节点属性(文档核心内容)
这六个属性中,dashOffset 为普通数字,其余四个 *Node 属性是本材质区别于经典材质的核心——它们允许用节点来完全覆盖(overwrite)对应数字属性的推导值。
.dashOffset : number
虚线起始偏移量,默认 0。它是一条普通数值属性;当你不显式设置 offsetNode 时,MaterialNode 在 源码实现 中会判断该值是否存在:
node = ( material.dashOffset ) ? this.getFloat( scope ) : float( 0 );
即 .dashOffset 为 0 时直接使用常量 float(0),为真值时才读取材质属性,从而避免无意义的常量分支。
.offsetNode : Node<float>(默认 null)
虚线偏移量的节点版本。默认虚线偏移量来自 .dashOffset 属性;设置该节点后会完全覆盖 .dashOffset 的推导值。若你不想整体覆盖、只想在既有值基础上做修改,应改用 TSL 访问器 materialLineDashOffset(见下文)。
.dashScaleNode : Node<float>(默认 null)
虚线缩放系数的节点版本。默认推导自 .scale 属性;设置后以节点结果覆盖默认 scale。覆盖失败——即不打算替换而是想增量修改——时应使用 materialLineScale。
.dashSizeNode : Node<float>(默认 null)
虚线笔画的节点版本,覆盖默认的 .dashSize 推导值;增量修改请用 materialLineDashSize。
.gapSizeNode : Node<float>(默认 null)
虚线间隔的节点版本,覆盖默认的 .gapSize 推导值;增量修改请用 materialLineGapSize。
.isLineDashedNodeMaterial : boolean(只读)
类型判断标志,固定为 true,用于在渲染管线或业务代码中快速识别本材质类型(与 isLineDashedMaterial 在同名经典材质中的角色一致)。
四个 *Node 属性在源码中均被定义为 null 初始值(见 构造函数),语义非常清晰:为 null 即“回退到经典数字属性”,为非 null 即“以节点结果接管”。 这种“数字属性兜底 + 节点覆盖”的双轨设计,保证了老代码无需改动即可获得节点化能力。
四、方法 setupVariants( builder : NodeBuilder ) 与虚线剔除的源码原理
setupVariants() 在材质编译阶段负责搭建虚线专属的着色器变量与剔除逻辑。它覆盖了 NodeMaterial.setupVariants,节点构建器 builder 在 WebGL/WebGPU 后端编译时都会调用它。源码完整逻辑如下(src/materials/nodes/LineDashedNodeMaterial.js):
setupVariants( /* builder */ ) {
const offsetNode = this.offsetNode ? float( this.offsetNode ) : materialLineDashOffset;
const dashScaleNode = this.dashScaleNode ? float( this.dashScaleNode ) : materialLineScale;
const dashSizeNode = this.dashSizeNode ? float( this.dashSizeNode ) : materialLineDashSize;
const gapSizeNode = this.gapSizeNode ? float( this.gapSizeNode ) : materialLineGapSize;
dashSize.assign( dashSizeNode );
gapSize.assign( gapSizeNode );
const vLineDistance = varying( attribute( 'lineDistance' ).mul( dashScaleNode ) );
const vLineDistanceOffset = offsetNode ? vLineDistance.add( offsetNode ) : vLineDistance;
vLineDistanceOffset.mod( dashSize.add( gapSize ) ).greaterThan( dashSize ).discard();
}
这段代码揭示了虚线的完整工作机制:
- 四路取值回退:每个节点属性若为
null,就落到对应的 TSL 材质访问器(materialLineDashOffset/materialLineScale/materialLineDashSize/materialLineGapSize),这些访问器由 MaterialNode 静态作用域常量 定义并导出(src/nodes/accessors/MaterialNode.js),最终读取材质上同名数字属性。从而“节点优先、属性兜底”在编译期即可确定。 - 写入着色器全局变量:
dashSize与gapSize是 three.js 在 PropertyNode 中导出的 TSL 全局变量,即 shader 中的同名变量;.assign()将解析好的节点值写进去,供后续片元计算使用。 - 距离插值:
lineDistance是几何体上由computeLineDistances()生成的逐顶点距离属性(顶点着色器中按视角插值,因此用varying()声明),乘上dashScaleNode后得到“考虑了整体缩放的世界距离”。 - 可选的偏移:若存在
offsetNode(无论来自.offsetNode还是默认的materialLineDashOffset常量float(0)),则把插值距离整体平移。 - 核心剔除公式:
(lineDistance % (dashSize + gapSize)) > dashSize时discard()。这意味着一个虚线周期长度为dashSize + gapSize,只有落在前dashSize范围内的片元才被保留——实线笔画之外的部分被直接丢弃,产生虚线观感。
同一套“周期取模 + discard”思路也被更复杂的宽线段节点材质 Line2NodeMaterial 采用,可见它是 three.js 节点化虚线渲染的通用手法。
五、关键配套 TSL 访问器:增量修改而非覆盖
上一节提到四个配套 TSL 访问器,它们在 src/nodes/accessors/MaterialNode.js 中被定义,同时从 src/Three.TSL.js 顶层导出,使用时直接 import:
import { materialLineScale, materialLineDashSize, materialLineGapSize, materialLineDashOffset } from 'three/tsl';
它们的差别需要重点区分:
- 设置
.dashSizeNode、.gapSizeNode、.dashScaleNode、.offsetNode属于整体替换(overwrite)——材质原本的数值属性被忽略; - 引用
materialLineDashSize、materialLineGapSize、materialLineScale、materialLineDashOffset则把当前材质属性值当作可读信号引入节点图,你可以在其基础上继续运算,属于增量修改(modify)。
例如“在不改材质 dashSize 数值的前提下,让虚线笔画随时间波动”,正确写法是:
import * as THREE from 'three';
import { materialLineDashSize, sin, timerLocal } from 'three/tsl';
const material = new THREE.LineDashedNodeMaterial( { dashSize: 3, gapSize: 2, scale: 1 } );
// 增量:在材质自带 dashSize(3) 的基础上叠加正弦波动
material.dashSizeNode = materialLineDashSize.mul( sin( timerLocal() ).mul( 0.5 ).add( 1 ) );
如果这里错误地直接写 material.dashSizeNode = someNode,则材质上的 dashSize: 3 将被彻底覆盖,这就是文档反复强调“不想覆盖而是修改就用 materialLine* 系列”的原因。
六、实战示例:从静态虚线到动态 TSL 虚线
1. 静态虚线(兼容传统写法)
import * as THREE from 'three';
const points = [
new THREE.Vector3( - 10, 0, 0 ),
new THREE.Vector3( 10, 0, 0 ),
];
const geometry = new THREE.BufferGeometry().setFromPoints( points );
geometry.computeLineDistances(); // 必须:生成 lineDistance 属性
const material = new THREE.LineDashedNodeMaterial( {
color: 0x00ffff,
transparent: true,
scale: 2,
dashSize: 2,
gapSize: 1,
} );
const line = new THREE.Line( geometry, material );
scene.add( line );
要点:虚线材质(无论是经典版还是节点版)依赖顶点着色器中的 lineDistance 属性,因此创建 Line 后务必调用 geometry.computeLineDistances(),否则所有片元距离为 0,整条线会被当成一个周期而显示异常。
2. 节点驱动的动态虚线
利用 *Node 属性与 TSL,可以将任何计算源接入虚线参数,实现动画或程序化纹理一样的动态效果:
import * as THREE from 'three';
import { float, uv, materialLineDashSize, materialLineGapSize } from 'three/tsl';
const material = new THREE.LineDashedNodeMaterial();
material.dashSizeNode = materialLineDashSize.mul( 2 ); // 加长笔画
material.gapSizeNode = materialLineGapSize; // 保持默认 gap
material.offsetNode = uv().x.mul( 4 ); // 让偏移随 UV 变化,形成流动感
material.dashScaleNode = float( 1.5 ); // 固定整体缩放
这种方式下,setupVariants() 编译出的 shader 会直接使用你传入的节点表达式替换默认的 dashSize、gapSize 与偏移量,虚线外观完全由 TSL 图决定。
七、使用建议与注意事项
- 虚线参数均为长度单位:
dashSize/gapSize的观感会随相机距离与scale变化,调整时建议同时考虑.scale,避免在世界空间和屏幕空间换算上出现偏差。 - 半透明虚线记得开
transparent:LineDashedMaterial默认不透明,与经典材质一致,需要透明度时必须设置transparent: true及opacity。 - 类型检测:运行时用
material.isLineDashedNodeMaterial === true判断即可,无需instanceof或字符串比对。 - 只覆盖不修改 vs. 增量修改:需要完全接管某参数时设置对应的
*Node属性;需要继承材质数值继续计算时使用materialLine*访问器(materialLineScale、materialLineDashSize、materialLineGapSize、materialLineDashOffset)。这是最容易出错、也最值得记住的设计要点。 - 渲染后端通用:
LineDashedNodeMaterial基于NodeMaterial,同时支持 WebGL 与 WebGPU 渲染管线;只要几何体带有lineDistance属性即可正常工作(仓库中 webgpu_lines_fat.html 即为 WebGPU 侧的实践参考)。
相关资源
- 类文档:docs/pages/LineDashedNodeMaterial.html.md
- 源码实现:src/materials/nodes/LineDashedNodeMaterial.js
- 经典版本对照:src/materials/LineDashedMaterial.js
- 基类实现:src/materials/nodes/NodeMaterial.js
- 材质访问器(
materialLine*系列):src/nodes/accessors/MaterialNode.js - 着色器全局变量
dashSize/gapSize:src/nodes/core/PropertyNode.js - TSL 顶层导出:src/Three.TSL.js
- 同族宽线虚线实现:src/materials/nodes/Line2NodeMaterial.js
- TSL 总体介绍:docs/TSL.md
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
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
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