three.js Line2(Fat Lines)宽线条完全指南:任意线宽、顶点颜色的折线渲染与射线拾取
导读
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)指出,Line2 在 Line 基础上补足了两点能力:
- 任意线宽:传统
Line在 WebGL 中受限于 1 像素宽度且各平台行为不一;Line2通过把每条线段扩展成由 GPU 生成的四边形(quad)网格来实现真正的"粗线",线宽由材质统一控制。 - 世界单位线宽:可通过
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,同样具备 position、scale、rotation、frustumCulled、raycast 等能力,可以像操作网格一样对它做变换。
数据准备: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;
}
而 dashed、worldUnits、alphaToCoverage 是通过增删 defines(USE_DASH、WORLD_UNITS、USE_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: true 与 alphaToCoverage: true。其原理是在片元边缘用 fwidth 计算梯度并做 smoothstep,让边界产生半透明过渡,从而与 MSAA 结合得到平滑的粗线边缘。若出现锯齿或边缘过淡,优先检查这两项组合。
Line2 构造函数与类型标识
new Line2( geometry, material )
geometry:LineGeometry实例。不传时使用默认new LineGeometry();material:LineMaterial实例。不传时使用随机颜色的默认材质(源码new LineMaterial( { color: Math.random() * 0xffffff } )),生产环境建议始终显式传入。
.isLine2 : boolean(只读)
类型测试标记,默认 true,配合 isLineSegments2、isLineSegmentsGeometry 等标记,可在迭代中快速区分宽线族对象:
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.js与Line2NodeMaterial。 - 另有 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 = true 与 type = '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 像素。 - 渲染出现异常宽的"纸片"? 确认导入的
Line2与LineMaterial来自同一后端(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、LineGeometry、LineSegmentsGeometry、LineMaterial、Line2NodeMaterial
- 源码实现:Line2.js、LineSegments2.js、LineGeometry.js、LineSegmentsGeometry.js、LineMaterial.js
- 官方示例:webgl_lines_fat.html、webgl_lines_fat_raycasting.html、webgpu_lines_fat.html、webgpu_lines_fat_raycasting.html
掌握 Line2 之后,同一套数据还可以通过 LineSegments2 承载离散线段(如网格边线、自定义虚线对),两者配合 LineMaterial 即可覆盖绝大多数宽线可视化需求。
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