首页
/ three.js Line2(Fat Lines)宽线条完全指南:任意线宽、顶点颜色的折线渲染与射线拾取

three.js Line2(Fat Lines)宽线条完全指南:任意线宽、顶点颜色的折线渲染与射线拾取

2026-09-07 19:42:44作者:裴锟轩Denise

导读

Line2 是 three.js 提供的"宽线条"(俗称 Fat Lines)核心类,用于绘制连接顶点序列的折线(polyline)。相比内置的 Line,它支持任意像素级线宽世界单位线宽以及逐顶点颜色渐变,常用于高亮路径、轨迹线、轮廓描边、时序数据可视化等场景。阅读本文后,你将掌握 Line2 的完整用法:几何数据准备、材质参数调优、虚线渲染、WebGL / WebGPU 双后端适配,以及基于 Raycaster 的精确拾取技巧。

Line2 的定位与继承关系

Line2 是 three.js 示例目录(addons)中的模块。它在对象体系中处于如下继承链:

EventDispatcher → Object3D → Mesh → LineSegments2 → Line2

源码(examples/jsm/lines/Line2.js)可以看到,Line2 本身是一个非常薄的分支:它继承 LineSegments2,只额外声明了类型标识与默认构造参数:

class Line2 extends LineSegments2 {

	constructor( geometry = new LineGeometry(), material = new LineMaterial( { color: Math.random() * 0xffffff } ) ) {

		super( geometry, material );

		this.isLine2 = true;
		this.type = 'Line2';

	}

}

它与同族类的分工如下:

数据形态 用途
Line 顶点序列 + position 内置基础线条,仅支持 1 像素宽的屏幕线
LineSegments 顶点两两成对 内置基础线段集合
LineSegments2 instanceStart / instanceEnd 实例化缓冲 任意宽度的线段集合,是宽线系统的基类(源码
Line2 LineGeometry 自动把折线拆成首尾相接的线段对 任意宽度的折线(polyline),本主题主角
LineGeometry 顶点链 Line2 使用,把一串点转换为线段对数据
LineMaterial / Line2NodeMaterial WebGL / WebGPU 两端各自的宽线材质

简单说:LineSegments2 负责"如何画",Line2 把"一串顶点"自动翻译成 LineSegments2 需要的线段对,从而省去手工拆段

与内置 Line / LineSegments 的本质区别

文档(docs/pages/Line2.html.md)指出,Line2Line 基础上补足了两点能力:

  1. 任意线宽:传统 Line 在 WebGL 中受限于 1 像素宽度且各平台行为不一;Line2 通过把每条线段扩展成由 GPU 生成的四边形(quad)网格来实现真正的"粗线",线宽由材质统一控制。
  2. 世界单位线宽:可通过 LineMaterial.worldUnits 切换为世界单位,使线条粗细随镜头远近正确缩放,保持空间上的真实厚度。

渲染后端选择:WebGL 与 WebGPU

Line2 对渲染后端有明确区分(原文档也特别提示):

  • WebGLRenderer:从 lines/Line2.js 导入,配合 LineMaterial
  • WebGPURenderer:从 lines/webgpu/Line2.js 导入,配合 Line2NodeMaterial

WebGPU 版实现(examples/jsm/lines/webgpu/Line2.js) 与 WebGL 版结构完全一致,区别仅在于默认材质替换为 Line2NodeMaterial(node 材质,来自 three/webgpu)。在 WebGPU 管线中不要混用 WebGL 版导入路径,否则材质与后端不匹配会导致渲染异常。

快速开始:最小可用示例

原文档给出了核心代码骨架。将其补充为完整可运行的最小示例:

import * as THREE from 'three';
import { Line2 } from 'three/addons/lines/Line2.js';
import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
import { LineGeometry } from 'three/addons/lines/LineGeometry.js';

// 1. 准备顶点链(也可以直接用已包含 x,y,z 的扁平数组)
const positions = [];
for ( let i = 0; i <= 100; i ++ ) {
	const t = i / 100;
	positions.push( t * 20 - 10, Math.sin( t * Math.PI * 4 ) * 3, 0 );
}

// 2. 创建几何体:把折线内部转换成线段对
const geometry = new LineGeometry();
geometry.setPositions( positions );

// 3. 创建材质(必须显式设置 linewidth)
const material = new LineMaterial( {
	color: 0x00aaff,
	linewidth: 5,              // 屏幕像素单位(worldUnits=false 时)
	vertexColors: false,
	dashed: false,
} );

// 4. 组装并加入场景
const line = new Line2( geometry, material );
scene.add( line );

// 5. 渲染循环中更新 resolution,确保屏幕空间线宽精确
function animate() {
	// Line2 会通过 onBeforeRender 自动把 viewport 写入 material.uniforms.resolution
	// 前提是对象本身可见且参与了本次渲染
	renderer.render( scene, camera );
}

使用上与普通 Mesh 完全一致:Line2 继承自 Mesh,同样具备 positionscalerotationfrustumCulledraycast 等能力,可以像操作网格一样对它做变换。

数据准备:LineGeometry

Line2 使用的几何体是 LineGeometry文档源码)。它的关键设计是把折线数据自动扩展为线段对数据

setPositions( array )

传入扁平坐标数组 [x1,y1,z1, x2,y2,z2, ...]。内部算法把第 i 个点与第 i+1 个点组成一个线段对,生成 instanceStart/instanceEnd 需要的交错排列,再交给父类 LineSegmentsGeometry

setPositions( array ) {
	const length = array.length - 3;
	const points = new Float32Array( 2 * length );
	for ( let i = 0; i < length; i += 3 ) {
		// 把 (p[i], p[i+1]) 复制为 (start=i, end=i+1)
		...
	}
	super.setPositions( points );
	return this;
}

底层 LineSegmentsGeometry.setPositions源码)会把数组包装成 InstancedInterleavedBuffer,并以 stride=6 拆出两个三元组 attribute:

  • instanceStart(每个实例的线段起点)
  • instanceEnd(每个实例的线段终点)

并在写入后自动 computeBoundingBox() / computeBoundingSphere()

setFromPoints( points )

若手头是 Vector3/Vector2 数组,可直接用更直观的 setFromPoints

const points = [
	new THREE.Vector3( - 10, 0, 0 ),
	new THREE.Vector3( 0, 5, 0 ),
	new THREE.Vector3( 10, 0, 0 ),
];
const geometry = new LineGeometry();
geometry.setFromPoints( points );

注意 Vector2 会被自动补 z = 0(见源码 positions[6*i+2] = points[i].z || 0)。

setColors( array )

开启逐顶点颜色时使用,传入 [r1,g1,b1, r2,g2,b2, ...](0~1 范围),内部同样自动拆成 instanceColorStart / instanceColorEnd,实现"每段首尾颜色渐变、整条折线连续过渡"的效果。

其他辅助方法

方法 说明
applyMatrix4( matrix ) 直接对 instanceStart/instanceEnd 施加变换(源码
fromLine( line ) 从已有 Line 的 position 数组生成宽线几何
fromEdgesGeometry / fromWireframeGeometry / fromMesh / fromLineSegments LineSegmentsGeometry 使用,可把网格边线、线框等转成宽线数据

材质参数:LineMaterial 详解

LineMaterial 是宽线系统的着色器材质(文档源码)。核心参数表如下:

参数 类型 默认值 说明
color Color (1,1,1) 线条颜色,对应 shader 中的 diffuse uniform
vertexColors boolean 是否使用 geometry.setColors() 提供的顶点色(决定是否定义 USE_COLOR
linewidth number 1 线宽。worldUnits=false(默认)时单位是 CSS 像素worldUnits=true 时单位是世界单位
worldUnits boolean false 是否用世界单位定义线宽与虚线段长度,影响代码中 WORLD_UNITS
resolution Vector2 视口尺寸(像素),屏幕空间渲染精确性的关键,由渲染回调自动维护
dashed boolean false 是否虚线(定义 USE_DASH),开启后需要 computeLineDistances() 配合
dashScale number 1 虚线与间隔的整体缩放
dashSize number 1 虚线段长度
gapSize number 0 间隔长度
dashOffset number 0 虚线相位偏移,用于"流动虚线"动画
alphaToCoverage boolean false 开启后利用 MSAA 优化线条边缘抗锯齿,见下方说明
opacity number 1 整体不透明度

这些参数大多只是访问 shader uniforms 的 getter/setter。例如线宽:

get linewidth() {
	return this.uniforms.linewidth.value;
}
set linewidth( value ) {
	if ( ! this.uniforms.linewidth ) return;
	this.uniforms.linewidth.value = value;
}

dashedworldUnitsalphaToCoverage 是通过增删 definesUSE_DASHWORLD_UNITSUSE_ALPHA_TO_COVERAGE)来切换着色器分支,需要重新编译,因此 setter 中会触发 needsUpdate = true

resolution 为什么重要

屏幕空间渲染时,顶点着色器要用 resolution 把 NDC 偏移换算为像素(见 LineMaterial.js 顶点着色器)。LineSegments2.onBeforeRender 会在每次渲染前自动把当前视口写入:

onBeforeRender( renderer ) {
	const uniforms = this.material.uniforms;
	if ( uniforms && uniforms.resolution ) {
		renderer.getViewport( _viewport );
		this.material.uniforms.resolution.value.set( _viewport.z, _viewport.w );
	}
}

因此你无需手动维护 resolution,只要线条对象可见并正常参与渲染即可。若你在离屏、多视口或自定义 render target 中绘制,需保证该回调仍被触发。

worldUnits 模式

worldUnits = true 时,片元着色器不再依赖 UV 距离,而是用"世界坐标下视线射线与线段最近距离 / 线宽"判定覆盖(源码 closestLineToLine + norm = len / linewidth),因此线宽是真实世界尺寸。这对近距离大线宽、或者希望线条厚度随摄像机距离衰减的 CAD/工程可视化很有用;代价是片段着色计算更重。

抗锯齿建议

示例 examples/webgl_lines_fat.html 中开启 antialias: truealphaToCoverage: true。其原理是在片元边缘用 fwidth 计算梯度并做 smoothstep,让边界产生半透明过渡,从而与 MSAA 结合得到平滑的粗线边缘。若出现锯齿或边缘过淡,优先检查这两项组合。

Line2 构造函数与类型标识

new Line2( geometry, material )

  • geometryLineGeometry 实例。不传时使用默认 new LineGeometry()
  • materialLineMaterial 实例。不传时使用随机颜色的默认材质(源码 new LineMaterial( { color: Math.random() * 0xffffff } )),生产环境建议始终显式传入。

.isLine2 : boolean(只读)

类型测试标记,默认 true,配合 isLineSegments2isLineSegmentsGeometry 等标记,可在迭代中快速区分宽线族对象:

function isWideLine( obj ) {
	return !! obj.isLine2 || !! obj.isLineSegments2;
}

虚线渲染:computeLineDistances()

与内置 LineDashedMaterial 类似,虚线需要"沿线的累计长度"才能正确排布 dash。为此在开启 dashed 后必须调用 line.computeLineDistances()

material.dashed = true;
material.dashSize = 1;
material.gapSize = 0.5;
material.dashScale = 2;

line.computeLineDistances(); // 必须在材质 dashed=true 之后调用
scene.add( line );

该方法在 LineSegments2.js 中实现:遍历每对 instanceStart/instanceEnd,计算从折线起点开始的累计长度,写入 instanceDistanceStart / instanceDistanceEnd 两个交错的实例化 attribute。若没有调用,则无法得到正确的虚线分布。注意示例 examples/webgl_lines_fat.html 中始终调用 computeLineDistances(),即使当前不是虚线,以保证后续随时切换 dashed

射线拾取:Raycaster 与 threshold

宽线同样支持 Raycaster 拾取。拾取实现在 LineSegments2.raycast源码):

  • 它先判断对象是否命中,用包围球/包围盒做粗筛,并按线宽把包围体向外膨胀(getWorldSpaceHalfWidth);
  • 随后按命中精度细分:
    • worldUnits=false:走 raycastScreenSpace——把射线与线段投影到屏幕坐标,判断到线段中心线距离是否小于 linewidth * 0.5
    • worldUnits=true:走 raycastWorldUnits,在世界空间直接计算射线到线段的最短距离。

使用前必须设置 raycaster.camera(屏幕空间模式依赖相机的投影矩阵),且需要对象已经渲染过(resolution 已就绪),否则源码会直接 early-out。

可通过 raycaster.params.Line2.threshold 放大命中容差,方便点击较细的线条。参考示例 examples/webgl_lines_fat_raycasting.html

raycaster.params.Line2 = {};
raycaster.params.Line2.threshold = 0; // GUI 中可在 0~10 调整

// 每帧
raycaster.setFromCamera( pointer, camera );
const intersects = raycaster.intersectObject( line );
if ( intersects.length > 0 ) {
	// 命中
}

拾取命中区间正是 material.linewidth + threshold(见源码 _lineWidth = material.linewidth + threshold)。WebGPU 版同样有对应示例 examples/webgpu_lines_fat_raycasting.html

综合实战:结合官方示例

仓库中的官方示例是最好的配套资料:

  • examples/webgl_lines_fat.html:WebGL 端完整演示。它用 GeometryUtils.hilbert3D 生成 Hilbert 曲线控制点,经 CatmullRomCurve3 采样获得平滑折线,并按参数 t 生成 HSL 渐变色,随后 setPositions + setColors 喂给 Line2,配合 OrbitControls 与 lil-gui 实时调节线宽、虚线、alphaToCoverage。
  • examples/webgpu_lines_fat.html:WebGPU 端版本,使用 lines/webgpu/Line2.jsLine2NodeMaterial
  • 另有 WebGL/WebGPU 的射线拾取示例 webgl_lines_fat_raycasting.html / webgpu_lines_fat_raycasting.html,演示了多折线与拾取阈值可视化。

以下是综合绘制"多条带渐变的平滑折线"并支持缩放旋转的典型代码结构(可从上述示例裁剪):

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import * as GeometryUtils from 'three/addons/utils/GeometryUtils.js';

// 采样曲线得到稠密折线点
const pts = GeometryUtils.hilbert3D( new THREE.Vector3( 0, 0, 0 ), 20, 1, 0, 1, 2, 3, 4, 5, 6, 7 );
const spline = new THREE.CatmullRomCurve3( pts );
const divisions = Math.round( 12 * pts.length );

const positions = [], colors = [];
for ( let i = 0; i <= divisions; i ++ ) {
	const t = i / divisions;
	spline.getPoint( t, point );
	positions.push( point.x, point.y, point.z );
	color.setHSL( t, 1.0, 0.5 );
	colors.push( color.r, color.g, color.b );
}

const line = new Line2(
	new LineGeometry().setPositions( positions ).setColors( colors ),
	new LineMaterial( { color: 0xffffff, linewidth: 5, vertexColors: true, alphaToCoverage: true } )
);
line.computeLineDistances();
scene.add( line );

WebGPU 版要点:Line2NodeMaterial

在 WebGPU 后端(WebGPURenderer)下,应从 three/addons/lines/webgpu/Line2.js 导入 Line2,材质使用节点材质 Line2NodeMaterial(见 文档)。其实现与 WebGL 版保持相同的 isLine2 = truetype = 'Line2',方便两种后端之间无缝切换渲染对象与几何数据;只需同步更换材质与导入路径即可:

import { Line2 } from 'three/addons/lines/webgpu/Line2.js';
import { Line2NodeMaterial } from 'three/webgpu';

const material = new Line2NodeMaterial( { color: 0x00aaff, linewidth: 5 } );
const line = new Line2( geometry, material );

常见问题与注意事项

  • WebGL 中仍用 LineBasicMaterial 画不出宽线? 这是预期行为:任意线宽能力只存在于 LineMaterial(WebGL)与 Line2NodeMaterial(WebGPU)体系。原生 Line 仅 1 像素。
  • 渲染出现异常宽的"纸片"? 确认导入的 Line2LineMaterial 来自同一后端(WebGL 版 lines/ 目录、WebGPU 版 lines/webgpu/ 目录),不要交叉混用。
  • 虚线不生效? 检查三点:material.dashed = true;调用过 line.computeLineDistances()dashSize + gapSize > 0(片元着色器用 mod( vLineDistance + dashOffset, dashSize + gapSize ) > dashSize 判定丢弃,间隔为 0 会退化为实线效果)。
  • 拾取不准或拾取不到? 屏幕空间模式要求 raycaster.camera 已设置、对象已渲染过(resolution 非 0);细线可增大 raycaster.params.Line2.threshold
  • 大线宽下的走样与接缝:合理使用 alphaToCoverage + MSAA;端点与拐角的平滑外观依赖几何体为稠密采样折线(如官方示例中把曲线按 divisions 细分)。
  • resolution 何时需要手动维护? 正常场景无需——onBeforeRender 已自动同步 viewport 到 uniform。仅在绕过常规渲染流程(如离屏 RT、多视口排布)时考虑手动写入。

参考与延伸阅读

掌握 Line2 之后,同一套数据还可以通过 LineSegments2 承载离散线段(如网格边线、自定义虚线对),两者配合 LineMaterial 即可覆盖绝大多数宽线可视化需求。

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

项目优选

收起
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++
915
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