news 2026/10/23 6:50:08

WebGL Fundamentals 实战:利用 per-vertex UV 坐标实现带拖尾效果的运动折线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WebGL Fundamentals 实战:利用 per-vertex UV 坐标实现带拖尾效果的运动折线
  • 图形学

【免费下载链接】webgl-fundamentals

WebGL lessons that start with the basics

项目地址:https://gitcode.com/gh_mirrors/we/webgl-fundamentals
点击查看免费下载

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),就能实现类似滚动的效果。

拆解一下,完整方案由三个要素组成:

  1. 为每个顶点传入一个标量u:它本质上是"沿线的一维 UV 坐标",取值范围、变化速率完全由你通过顶点数据控制。整条折线共享同一套u进度,就实现了"颜色沿整条线流动"。
  2. 顶点着色器原样透传:把u从attribute转成varying,交给片段着色器做插值。
  3. 片段着色器中用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);

两个值得注意的细节

  1. time: -time的负号:fract(v_u + time)中 time 递增会让图案向u增大的方向滚动;取负后图案反向滚动。方向完全由你决定,也可以做成 uniform 开关。
  2. requestAnimationFrame的入参是毫秒,所以第一行time *= 0.001将其换算成秒,保证动画速度与帧率无关(高刷新率屏幕下不会"跑得更快")。

背后的 twgl 辅助函数(源码级对照)

两个示例都使用仓库自带的 twgl 精简版库 3rdparty/twgl-full.module.js 来省略样板代码。逐条对照其实现,可确认每一步对应的原生 WebGL 调用:

twgl 调用对应原生操作源码位置
twgl.createProgramInfo(gl, [vs, fs])编译两个着色器、链接 program、生成 uniform/attribute setter3rdparty/twgl-full.module.js#L8420-L8445
twgl.createBufferInfoFromArrays(gl, arrays)为每个数组gl.createBuffer+bindBuffer+bufferData,并计算numElements3rdparty/twgl-full.module.js#L2569-L2584
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo)bindBuffer、enableVertexAttribArray、vertexAttribPointer3rdparty/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滚动机制,可以随时互换。

从示例到真实场景:如何画"会转弯"的轨迹线

把两个示例推广到真实的地图轨迹/数据流可视化,只需做三件事:

  1. 几何上:将一条轨迹(任意多段折线)拆成LINES顶点序列,每段两个顶点;由于gl.LINES不共享顶点,拐点处的顶点需要重复出现(示例中.7,-.3、-.1,.8各出现两次即是如此)。
  2. 属性上:为每个顶点生成沿线累计的u值。最简单的做法是令每段从 0 开始、按该段在整条折线上的长度比例或预设重复次数递增,如示例的[0,1]、[0,2]、[0,3]。若希望颜色流速全程一致,可以按各段实际长度累计u。
  3. 动画上:传入单调递增的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

项目地址:https://gitcode.com/gh_mirrors/we/webgl-fundamentals
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 16:48:13

CWRU轴承故障诊断:时频分析三法实战避坑指南

简介:本资源是一份面向机械故障诊断、信号处理与智能运维方向研究者及研究生的凯斯西储大学轴承故障数据集时频分析实践指南,聚焦振动信号的深度特征提取与故障辨识方法。文档系统讲解短时傅里叶变换(STFT)与连续小波变换&#xf…

作者头像 李华
网站建设 2026/10/18 19:13:29

Windows下CodeIgniter 4环境配置与Hello World实战:从Wamp到路由跑通

没编译环境、没搞过框架路由的人,第一次在Windows上跑CodeIgniter 4,十有八九会卡在同一道坎上:明明装好了Wamp,下载了框架源码,浏览器一打开却看到一堆目录列表,或者干脆是404。这不是你笨,而是…

作者头像 李华
网站建设 2026/10/19 8:36:13

SpringBoot多环境logback日志配置实战:从原理到生产落地

做SpringBoot开发这几年,logback日志配置算是我见过最容易被轻视、又最容易在关键时刻出问题的环节之一。之前带一个项目,开发环境日志打印得跟流水一样,到了生产环境还是同一套配置,结果线上磁盘被日志文件塞满,排查问…

作者头像 李华
网站建设 2026/10/20 18:55:48

规范的一键生成论文工具综合榜(2026 最新)

🏆 第一梯队:全流程学术解决方案(★★★★★) 排名工具名称综合评分核心优势适用场景价格模式1千笔AI94/100学术全链路赋能,免费 AIGC 不限次检测,查重精准,引用格式合规,适配国内学…

作者头像 李华
网站建设 2026/10/20 14:14:30

Vastbase G100手册实践:连接、表操作与接口避坑指南

简介:《VASTDATA Vastbase G100 V2.2用户手册》是一份面向信创场景的国产数据库操作指南,适合数据库管理员、应用开发者和国产化替代项目的运维人员研读,用于快速掌握基于openGauss打造的Vastbase G100的架构逻辑、事务管理与日常操作。包内共…

作者头像 李华