three.js 虚线线条材质 LineDashedMaterial:从参数配置到着色器实现原理
导读
LineDashedMaterial 是 three.js 中用于渲染虚线线段的材质,它继承自 LineBasicMaterial,专为 THREE.Line、THREE.LineSegments、THREE.LineLoop 这类线条图元提供“实线 + 间隔”的周期化描边效果。阅读本文后,你将掌握 LineDashedMaterial 的完整参数语义(dashSize/gapSize/scale)、前置条件 computeLineDistances() 的正确用法,并能结合着色器源码理解虚线在 GPU 上到底是如何被“切断”出来的。本文以 官方材质文档 为主体骨架,同时对照 源码实现 与 官方示例 进行源码级展开。
LineDashedMaterial 是什么
一句话概括:材质(Material)决定了可渲染 3D 对象的外观,而 LineDashedMaterial 负责让线以虚线段的形式呈现。它的完整继承关系为:
EventDispatcher → Material → LineBasicMaterial → LineDashedMaterial
这一继承链意味着它自动拥有 Material 基类(透明度、深度测试、侧向剔除等)与 LineBasicMaterial(颜色、线宽等)的全部能力,仅在之上叠加了虚线专属的控制参数。
从源码角度确认类结构(src/materials/LineDashedMaterial.js):
- 构造时先
super()调用LineBasicMaterial初始化公共属性; - 随后注册类型标识
this.isLineDashedMaterial = true与字符串this.type = 'LineDashedMaterial'; - 声明三个专属属性的默认值:
scale = 1、dashSize = 3、gapSize = 1; - 末尾调用
this.setValues( parameters ),将传入的参数对象批量写入材质——这也是所有 three.js 内置材质统一的初始化协议(setValues定义于Material基类)。
补充:
isLineDashedMaterial这类布尔标记被 WebGL 渲染器广泛用于运行期类型分派(例如 WebGLMaterials.js 中先判断isLineBasicMaterial,再在isLineDashedMaterial为真时额外刷新虚线 uniform),因此它是一个只读的、面向引擎内部的类型探测标志,业务代码中一般不需要直接依赖它。
基础用法与最小可运行示例
官方文档给出的构造示例即为最标准的用法(文档原文):
const material = new THREE.LineDashedMaterial( {
color: 0xffffff,
scale: 1,
dashSize: 3,
gapSize: 1,
} );
但仅创建材质是不够的——虚线要真正显示,还必须在承载它的线条对象上调用 computeLineDistances()。完整的最小场景如下:
import * as THREE from 'three';
// 1. 准备一组顶点,生成非索引 BufferGeometry
const points = [];
points.push( new THREE.Vector3( - 10, 0, 0 ) );
points.push( new THREE.Vector3( 0, 5, 0 ) );
points.push( new THREE.Vector3( 10, 0, 0 ) );
const geometry = new THREE.BufferGeometry().setFromPoints( points );
// 2. 创建虚线材质
const material = new THREE.LineDashedMaterial( {
color: 0x00ff00,
dashSize: 3, // 每段虚线的长度(世界单位)
gapSize: 1, // 虚线之间间隔的长度(世界单位)
} );
// 3. 创建 Line 对象并「计算线距」——虚线显示的关键前置步骤
const line = new THREE.Line( geometry, material );
line.computeLineDistances();
scene.add( line );
对照仓库中的官方示例 webgl_lines_dashed.html,可以看到同样的模式被应用在两类对象上:
// 样条曲线采样得到的连续 Line
const line = new THREE.Line( geometrySpline,
new THREE.LineDashedMaterial( { color: 0xffffff, dashSize: 1, gapSize: 0.5 } ) );
line.computeLineDistances();
// 立方体线框使用的 LineSegments(分段连线)
const lineSegments = new THREE.LineSegments( geometryBox,
new THREE.LineDashedMaterial( { color: 0xffaa00, dashSize: 3, gapSize: 1 } ) );
lineSegments.computeLineDistances();
两份代码均在构造材质后紧跟着调用 computeLineDistances(),这正是虚线渲染的正确使用范式。
虚线的前置条件:computeLineDistances() 与 lineDistance 属性
为什么必须调用 computeLineDistances()?因为虚线着色器需要知道每个顶点相对线段起点的累计长度,才能均匀地把“实线/间隔”沿整条线铺开。
逐行解读源码算法
该方法定义在 src/objects/Line.js,核心逻辑非常清晰:
- 只处理**非索引(non-indexed)**几何体:若
geometry.index !== null,直接调用warn()提示“仅支持非索引 BufferGeometry”并返回,因为索引几何体的顶点共享方式会破坏逐点累计距离的正确性。 - 新建数组
lineDistances,首顶点到自身距离记为0。 - 从第 2 个顶点起逐个遍历
position属性,累加前一个顶点到当前顶点的直线距离:
lineDistances[ 0 ] = 0;
// 对 i = 1..count-1:
// _vStart = position[i-1]; _vEnd = position[i];
// lineDistances[i] = lineDistances[i-1] + _vStart.distanceTo( _vEnd );
- 最终把累计距离数组包装为
Float32BufferAttribute,写入几何体的名为lineDistance的顶点属性:
geometry.setAttribute( 'lineDistance', new Float32BufferAttribute( lineDistances, 1 ) );
也就是说,computeLineDistances() 的本质是:为几何体新增一个逐顶点的 lineDistance 属性,其值为“当前顶点沿折线到起点的折线长度”。随后虚线着色器读取该属性完成周期切分。若创建了虚线材质却不调用 computeLineDistances(),lineDistance 属性缺失或恒为 0,着色器将无法形成预期的虚实交替,虚线便不会正确显示——这是初学者最容易踩的坑。
使用限制小结
- 只适用于
Line及其子类(LineSegments、LineLoop),它们共享 Line.js 中的该方法;普通Mesh不可用。 - 几何体必须是非索引的
BufferGeometry。 - 因为距离在 CPU 侧预先算好,当几何体顶点位置发生动态修改时,需要重新调用
computeLineDistances()以刷新lineDistance属性。
专属属性详解:dashSize、gapSize 与 scale
文档页面为 LineDashedMaterial 定义了三个专属属性(文档原文),其默认值与含义归纳如下:
| 属性 | 类型 | 默认值 | 含义 |
|---|---|---|---|
dashSize |
number | 3 |
虚线中“实线段”的长度,与间隔一起构成一个完整周期。 |
gapSize |
number | 1 |
虚线中“间隔段”的长度。 |
scale |
number | 1 |
对虚线周期整体的缩放因子,值越大,单位长度内重复的虚实周期越少、视觉效果越“稀疏”。 |
三个属性的底层语义
三者并非简单的“样式描述”,它们的实际作用在 WebGL 渲染管线中被严格实现:
顶点着色器阶段(linedashed.glsl.js):
uniform float scale;
attribute float lineDistance;
varying float vLineDistance;
// ...
vLineDistance = scale * lineDistance;
即每个顶点的线距先被 scale 放大,作为变化量传入片元阶段。
片元着色器阶段(同文件 L33-L75):
uniform float dashSize;
uniform float totalSize;
varying float vLineDistance;
// ...
if ( mod( vLineDistance, totalSize ) > dashSize ) {
discard; // 丢弃像素 → 形成“间隔”空洞
}
Uniform 同步阶段(WebGLMaterials.js):
function refreshUniformsDash( uniforms, material ) {
uniforms.dashSize.value = material.dashSize;
uniforms.totalSize.value = material.dashSize + material.gapSize; // 周期总长 = 实线 + 间隔
uniforms.scale.value = material.scale;
}
综合三段代码可以得到完整的公式化理解:
- 周期总长
totalSize = dashSize + gapSize; - 每个像素所在的线距位置对
totalSize取模,落在(dashSize, totalSize]区间内的像素一律discard(被丢弃),于是视觉上呈现为间隔; - 落在
[0, dashSize]区间内的像素正常着色,即为实线段; scale作用于lineDistance,等价于把整条线上“厘米刻度”的间距拉伸,从而整体拉长虚实周期在真实世界坐标中的尺寸。
另外,渲染器在 ShaderLib.js 中为 dashed 程序预设了默认 uniforms:scale: 1、dashSize: 1、totalSize: 2(src/renderers/shaders/ShaderLib.js),正式绘制时会由上面的 refreshUniformsDash 依据材质实例刷新覆盖。
直观配置技巧
- 想让虚线更密集:把
dashSize与gapSize整体调小;想让虚线更稀疏:调大两者,或把scale调大。 scale与dashSize/gapSize的区别:前者是渲染阶段的整体缩放系数,后两者是“周期内部”的虚实比例与绝对尺寸。若只想改变疏密而不改变虚实占比,调整scale即可;若想改变“实线与间隔的比值”,则分别调整dashSize与gapSize。
文档措辞的小提醒
文档在 dashSize 一词下写有 “This is both the gap with the stroke.”(文档原文),这一表述沿袭了源码注释里的措辞,实际语义应理解为:dashSize 是与间隔 gapSize 配套、共同构成“一实一虚”一个周期的实线段长度——上文的着色器 mod() 逻辑即为最终裁决依据。
构造参数与继承属性
构造函数
new LineDashedMaterial( parameters : Object )
parameters 是可选的单个对象,可容纳该材质及其所有继承链材质上的任意属性。文档特别强调:颜色类属性(如 color)可接受任何 Color#set 支持的值类型,例如十六进制数 0xffffff、CSS 颜色字符串 'red'、'#ff0000'、'rgb(255,0,0)' 乃至 THREE.Color 实例。
除前面已详述的三个专属属性外,从 LineBasicMaterial 继承的常用属性还包括:
| 属性 | 默认值 | 说明 |
|---|---|---|
color |
0xffffff(白色) |
线条颜色,通过 LineBasicMaterial 的 setValues 写入 |
opacity |
1 |
不透明度,需配合 transparent = true 生效 |
transparent |
false |
是否启用透明混合 |
linewidth |
1 |
线宽。注意:多数 WebGL 平台对线宽有硬件限制(通常恒为 1px),视觉放大宽度需借助 fat lines 方案(见下文进阶小节) |
depthTest / depthWrite 等 |
—— | 自 Material 基类继承的渲染状态 |
从 LineBasicMaterial.js 源码可见,
LineBasicMaterial构造时同样以setValues( parameters )收尾,且其copy()会逐项复制color等属性;而 LineDashedMaterial 的 copy() 在调用super.copy()之后补抄scale、dashSize、gapSize,保证material.clone()/copy()时虚线参数不丢失。
isLineDashedMaterial : boolean(只读)
类型探测标识,默认 true。前面已说明其引擎用途;在业务代码中它也可以作为一种安全的鸭子类型判断:if ( material.isLineDashedMaterial ) { ... }。
虚线渲染的完整调用链(源码级串联)
把本文涉及到的部件按一次实际绘制串联起来:
- 用户侧:
new THREE.LineDashedMaterial({...})+line.computeLineDistances(),后者为几何体写入lineDistance顶点属性(Line.js)。 - 程序选择:WebGL 渲染器检测到材质携带
isLineDashedMaterial标记后,选用 ShaderLib 中名为dashed的着色程序(ShaderLib.js),其顶点/片元源码即linedashed.glsl.js。 - Uniform 刷新:每帧通过
refreshUniformsDash()将dashSize、totalSize = dashSize + gapSize、scale上传 GPU(WebGLMaterials.js)。 - GPU 切分:顶点着色器执行
vLineDistance = scale * lineDistance,片元着色器对vLineDistance按totalSize取模,模值大于dashSize的片元直接discard,剩余片元按常规颜色管线(颜色、雾、色彩空间、色调映射等)输出,最终形成“实线—间隔—实线”的周期条纹。
值得留意的是,虚线模式本质上是在片元阶段“丢弃像素”实现的,因此它不改变几何的拓扑结构,光线投射(Raycaster)、包围球等计算与普通 Line 完全一致,只是外观呈现周期性空洞。
应用场景、注意事项与更进一步的方案
典型应用场景
- 参考线、网格辅助线中的“虚线标注”,指示视觉上“不可穿越”或“待定”的区域;
- 图纸/工程类可视化中的隐藏线、剖切线、中心线(在
webgl_lines_dashed.html示例中,立方体线框即用LineSegments+ 虚线材质表达辅助信息); - 样条曲线/轨迹的“规划路径 vs 已行驶路径”区分,配合
dashSize: 1, gapSize: 0.5这类细密参数使用; - 交互中的选中态描边、飞行路径动画等。
注意事项汇总
- 必须
computeLineDistances():漏掉它,虚线不会正确渲染,这是最高频的误用。 - 仅限非索引几何体:索引化
BufferGeometry会触发引擎警告并跳过计算(Line.js)。 - 顶点动态更新后需重算:
lineDistance是静态预计算的顶点属性。 - 线宽受限:若业务需要任意像素宽度的虚线,内置
LineBasicMaterial/LineDashedMaterial不适用。此时可参考仓库中的 fat lines 方案:examples/jsm/lines/(Line2+LineMaterial)与对应演示 webgl_lines_fat_wireframe.html、webgpu_lines_fat.html 展示了粗细可控的虚线实现(其LineMaterial同样支持dashed、dashScale等参数)。 - WebGPU 路径:若渲染器切换为 WebGPU(
THREE.WebGPURenderer),应使用 TSL 节点材质体系的LineDashedNodeMaterial(src/materials/nodes/LineDashedNodeMaterial.js)而非内置的 GLSL 材质,以获得一致的虚线表现。
继续深入
- 查看材质完整参考:docs/pages/LineDashedMaterial.html(HTML 渲染版)与其源码注释版本 docs/pages/LineDashedMaterial.html.md。
- 阅读父类文档了解继承能力:LineBasicMaterial、Material。
- 阅读对象级实现:Line 对象源码(含
computeLineDistances、raycast)。 - 阅读着色器与同步逻辑:linedashed.glsl.js、WebGLMaterials.js、ShaderLib.js。
- 运行官方演示:examples/webgl_lines_dashed.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 StartedRust0627
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