- 图形学
【免费下载链接】webgl-fundamentals
WebGL lessons that start with the basics
webgl-fundamentals仓库的问答篇(Q&A)收录了一个非常典型的可视化需求:如何绘制像 deck.gl 的 trip-routes(行程路线)示例那样、带有"拖尾"(trail)滚动效果且会拐弯的运动折线。本文以仓库中日语原文档 webgl-qna-how-to-achieve-moving-line-with-trail-effects.md(英文原版见 webgl-qna-how-to-achieve-moving-line-with-trail-effects.md)为核心骨架,结合仓库中两个可运行的完整示例与 twgl 辅助库源码,讲透"沿线的 UV 坐标 + 时间滚动"这一核心方案,并给出可直接复制的着色器与 JavaScript 代码。读完本文,你将掌握在 WebGL 中为任意折线生成滚动颜色/渐变纹理拖尾效果的标准做法。
问题背景:单条拖尾线容易,会转弯的线怎么办?
原文档记录的提问者困惑非常具有代表性:
像下面这个 demo 里的线是怎么画出来的?画一条带拖尾(trailing effects)效果的简单直线可能很容易,但这些线还在不断转弯(turning their directions)。
问题的参照对象是 deck.gl 官方示例中的custom-layers/trip-routes:一条条公交/行车轨迹线在底图上运动,轨迹尾部拖出渐变的"光带",并且整条轨迹是曲折的折线而非直线。
为什么"直线 + 拖尾"容易而"折线 + 拖尾"难?核心原因在于:WebGL 的gl.LINES原语只负责把每两个顶点连成一条 1 像素宽的线段,它本身不理解"整条折线的路径方向",更不知道"这条线从起点到终点走了多远"。要让颜色沿着整条折线(而不是每段各自)滚动,就必须自己把"沿线的距离/位置"这个信息编码进顶点数据——这正是本问答案的核心。
仓库中关于
LINES原语的适用场景与局限,可参考基础课 webgl-points-lines-triangles.md:LINES非常适合调试框、网格线或简单的发光效果,但在大多数平台上受限于 1 像素宽度,因此若需要粗线条或抗锯齿轮廓,通常要改用三角形条带等方式渲染。
核心思路:把每条折线当成一张可滚动的"UV 条带"
原文档的答案只有三句话,却是整套方案的纲领:
你可以为线传入 UV 坐标,或者生成一个 UV 坐标,然后用它给线着色。再传入一个时间值(time),就能实现类似滚动的效果。
拆解一下,完整方案由三个要素组成:
- 为每个顶点传入一个标量
u:它本质上是"沿线的一维 UV 坐标",取值范围、变化速率完全由你通过顶点数据控制。整条折线共享同一套u进度,就实现了"颜色沿整条线流动"。 - 顶点着色器原样透传:把
u从attribute转成varying,交给片段着色器做插值。 - 片段着色器中用
fract(v_u + time)制造滚动:time随时间连续变化,fract把结果约束在[0, 1)区间循环,颜色就会沿线段无限循环地"跑"起来。
为什么是fract(v_u + time)?
fract(x)返回x的小数部分,即x - floor(x)。当time从 0 匀速增长时,v_u + time每跨过整数就"归零重来",视觉上就是颜色图案沿折线方向匀速平移;每经过一个整数周期,图案完全重复一次。这就是拖尾"流动"的数学本质——无需在 JavaScript 侧更新任何顶点数据,动画完全在 GPU 上由 uniform 驱动。
折线数据怎么组织?
仓库示例(example-1)使用gl.LINES绘制三段首尾相接的线段,顶点与u数据一一对应:
position:6 个二维顶点,组成 3 条线段(.5,-.5 → .7,-.3、.7,-.3 → -.1,.8、-.1,.8 → -.8,.2),折线明显转弯;u:每段两个端点各一个值,[0,1]、[0,2]、[0,3]——这些数字定义了颜色图案在每一段线段上重复的次数:第 1 段从 0 到 1 重复 1 次,第 2 段从 0 到 2 重复 2 次,第 3 段从 0 到 3 重复 3 次。
注释在示例源码里写得很直白:// these numbers define how many times the pattern repeats for each line segment。由于 WebGL 会在同一图元内部对varying做线性插值,u从每段起点的 0 平滑插值到终点的 N,fract(v_u + time)就会在每一段内部生成 N 个周期的滚动条纹。
方案一:程序化颜色滚动(example-1)
这是最轻量的实现,颜色完全由 GLSL 计算,不依赖任何纹理资源。完整可运行代码见 webgl-qna-how-to-achieve-moving-line-with-trail-effects-example-1.html,核心部分如下。
着色器
// 顶点着色器:透传沿线坐标 u attribute vec4 position; attribute float u; varying float v_u; void main() { gl_Position = position; v_u = u; } // 片段着色器:用 fract(v_u + time) 滚动绿色条纹 precision mediump float; varying float v_u; uniform float time; void main() { float green = fract(v_u + time); gl_FragColor = vec4(0, green, 0, 1); }green在 0~1 之间随时间和沿线位置循环,产生由黑到绿的"彗尾"渐变,并在折线拐弯处自然延续——因为v_u是沿线全局插值的,拐弯不会打断滚动的连续性。
顶点数据与渲染循环
const gl = document.querySelector("canvas").getContext("webgl"); // 编译着色器、链接 program、查找 uniform 位置 const programInfo = twgl.createProgramInfo(gl, [vs, fs]); // 创建缓冲区(内部依次调用 gl.createBuffer / bindBuffer / bufferData) const bufferInfo = twgl.createBufferInfoFromArrays(gl, { position: { numComponents: 2, data: [ -.5, -.5, .7, -.3, .7, -.3, -.1, .8, -.1, .8, -.8, .2, ], }, u: { numComponents: 1, data: [ 0, 1, // 每段线上图案重复 1 次 0, 2, // 每段线上图案重复 2 次 0, 3, // 每段线上图案重复 3 次 ], }, }); function render(time) { time *= 0.001; // 毫秒转秒 gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(programInfo.program); // 内部调用 gl.bindBuffer / enableVertexAttribArray / vertexAttribPointer twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo); // 内部调用 gl.uniformXXX;time 取负,控制滚动方向 twgl.setUniforms(programInfo, { time: -time }); // 内部调用 gl.drawArrays,以 gl.LINES 方式绘制 twgl.drawBufferInfo(gl, bufferInfo, gl.LINES); requestAnimationFrame(render); } requestAnimationFrame(render);两个值得注意的细节
time: -time的负号:fract(v_u + time)中 time 递增会让图案向u增大的方向滚动;取负后图案反向滚动。方向完全由你决定,也可以做成 uniform 开关。requestAnimationFrame的入参是毫秒,所以第一行time *= 0.001将其换算成秒,保证动画速度与帧率无关(高刷新率屏幕下不会"跑得更快")。
背后的 twgl 辅助函数(源码级对照)
两个示例都使用仓库自带的 twgl 精简版库 3rdparty/twgl-full.module.js 来省略样板代码。逐条对照其实现,可确认每一步对应的原生 WebGL 调用:
| twgl 调用 | 对应原生操作 | 源码位置 |
|---|---|---|
twgl.createProgramInfo(gl, [vs, fs]) | 编译两个着色器、链接 program、生成 uniform/attribute setter | 3rdparty/twgl-full.module.js#L8420-L8445 |
twgl.createBufferInfoFromArrays(gl, arrays) | 为每个数组gl.createBuffer+bindBuffer+bufferData,并计算numElements | 3rdparty/twgl-full.module.js#L2569-L2584 |
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo) | bindBuffer、enableVertexAttribArray、vertexAttribPointer | 3rdparty/twgl-full.module.js#L8335-L8344 |
twgl.setUniforms(programInfo, {time}) | 按名字查找 setter 并调用gl.uniform1f等 | 3rdparty/twgl-full.module.js#L8174-L8193 |
twgl.drawBufferInfo(gl, bufferInfo, gl.LINES) | 无索引时调用gl.drawArrays(type, offset, numElements) | 3rdparty/twgl-full.module.js#L8523-L8542 |
可以看到,整个示例用 twgl 把"创建缓冲区 + 绑定属性 + 设置 uniform + 发起绘制"压缩到了约 10 行,而核心的着色器逻辑(fract(v_u + time))正是本文要掌握的技术本身。
方案二:用渐变纹理代替程序化颜色(example-2)
原文档紧接着指出:"当然,你也可以用纹理(texture)代替颜色代码。" 完整实现见 webgl-qna-how-to-achieve-moving-line-with-trail-effects-example-2.html。
着色器的唯一变化
与方案一相比,顶点着色器和顶点数据完全不变,只有片段着色器从"用代码计算颜色"改为"采样纹理":
precision mediump float; varying float v_u; uniform float time; uniform sampler2D tex; void main() { gl_FragColor = texture2D(tex, vec2(fract(v_u + time), 0.5)); }这里把fract(v_u + time)当作纹理的 U 坐标,固定取 V=0.5 采样一行纹理。由于纹理默认WRAP模式是重复(repeat),u越过 1.0 后纹理自然循环——这与方案一里fract的循环语义完全一致,只是把"循环色带"从代码换成了美术资源。
16×1 渐变纹理的构造
示例用twgl.createTexture直接以像素数组创建一张 16×1 的横向渐变色带(红 → 橙 → 黄 → 绿 → 青 → 蓝 → 紫 → 白),每个像素以 RGBA 十六进制字面量给出:
const texture = twgl.createTexture(gl, { height: 1, src: [ 0xFF, 0x00, 0x00, 0xFF, // 红 0xFF, 0x44, 0x00, 0xFF, // 橙 0xFF, 0x88, 0x00, 0xFF, 0xCC, 0xFF, 0x00, 0xFF, // 黄 // ... 青、蓝、紫渐变 ... 0xFF, 0xFF, 0xFF, 0xFF, // 白 ], });twgl.createTexture会处理纹理对象创建、绑定、texImage2D上传和参数设置(见 3rdparty/twgl-full.module.js#L6527-L6579)。渲染循环里只需把纹理一并作为 uniform 传入:
twgl.setUniforms(programInfo, { time: -time, tex: texture, // twgl 会自动完成 activeTexture + bindTexture });两种方案的取舍
- 程序化颜色(方案一):零资源开销、着色器内即可生成任意数学渐变,适合颜色规律简单、需要极致轻量的场景;缺点是复杂色带(如多段插值、品牌色)写起来冗长。
- 纹理渐变(方案二):把色带独立为 16×1 像素的小纹理,换色只需换纹理,甚至可以让不同线路共享纹理但使用不同
u缩放;复杂渐变、既有设计资源可以无缝复用。两种方案共享同一套u+time滚动机制,可以随时互换。
从示例到真实场景:如何画"会转弯"的轨迹线
把两个示例推广到真实的地图轨迹/数据流可视化,只需做三件事:
- 几何上:将一条轨迹(任意多段折线)拆成
LINES顶点序列,每段两个顶点;由于gl.LINES不共享顶点,拐点处的顶点需要重复出现(示例中.7,-.3、-.1,.8各出现两次即是如此)。 - 属性上:为每个顶点生成沿线累计的
u值。最简单的做法是令每段从 0 开始、按该段在整条折线上的长度比例或预设重复次数递增,如示例的[0,1]、[0,2]、[0,3]。若希望颜色流速全程一致,可以按各段实际长度累计u。 - 动画上:传入单调递增的
timeuniform,配合fract(v_u + time)完成 GPU 端滚动;颜色既可以用方案一的 GLSL 计算,也可以用方案二的色带纹理。
注意:
LINES在多数平台只能得到 1 像素宽的线,作为"轨迹高亮"往往偏细。仓库基础课 webgl-points-lines-triangles.md 明确提示,若需要结构化、可变宽度的粗线(类似 SVG/Illustrator 效果),应改用三角形渲染方案;但本文的u+time滚动机制与线的宽度实现方式正交,可以原样迁移到三角形条带方案中。
相关文档与源码索引
- 原文档(日文):webgl/lessons/ja/webgl-qna-how-to-achieve-moving-line-with-trail-effects.md
- 原文档(英文):webgl/lessons/webgl-qna-how-to-achieve-moving-line-with-trail-effects.md
- 示例一(程序化颜色):webgl/webgl-qna-how-to-achieve-moving-line-with-trail-effects-example-1.html
- 示例二(纹理渐变):webgl/webgl-qna-how-to-achieve-moving-line-with-trail-effects-example-2.html
- Q&A 总索引:webgl/lessons/webgl-qna.md
- 基础概念:点/线/三角形绘制方式 webgl/lessons/webgl-points-lines-triangles.md,WebGL 工作原理 webgl/lessons/webgl-how-it-works.md
- 依赖的 twgl 辅助库:3rdparty/twgl-full.module.js
- 图形学
【免费下载链接】webgl-fundamentals
WebGL lessons that start with the basics
相关推荐
three.js AfterimagePass 深入解析:实现运动拖尾(Afterimage)后期效果的原理与实践
three.js AfterimagePass 深入解析:实现运动拖尾(Afterimage)后期效果的原理与实践 本文围绕 three.js 官方文档 doc
前端3D渲染图形学PixiJS v8 MeshRope 实战指南:让纹理沿折线路径弯曲的绳索、蛇形与拖尾效果
PixiJS v8 MeshRope 实战指南:让纹理沿折线路径弯曲的绳索、蛇形与拖尾效果 MeshRope 是 PixiJS v8 场景系统中专用于"纹理沿折
前端图形学WebGL 纹理完全指南:从纹理坐标、Mipmap 到纹理图集(webgl-fundamentals 实战解析)
WebGL 纹理完全指南:从纹理坐标、Mipmap 到纹理图集(webgl fundamentals 实战解析) 本文基于 webgl fundamentals
图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考