公司动态

关于WebGL绘制粗线条那些事

📅 2026/7/19 9:29:39
关于WebGL绘制粗线条那些事
前言由于项目中有一些在网页中绘图的需求关于普通的2D绘图使用Canvas 2D就能搞定但是如果要实现3D绘图WebGL就是绕不开的。在Canvas 2D中将数个路径点绘制成一段路径是很简单的一件事。先从Canvas中获取一个CanvasRenderingContext2D对象ctx ctx.beginPath(); 开始路径 ctx.moveTo(x, y); 移到路径开始点然后一连串 ctx.lineTo(x, y); 传入路径点ctx.strokeStyle设置颜色ctx.lineWidth 设置线宽最后 ctx.stroke(); 就绘制出一条带宽度的路径了。在WebGL中也有 gl.drawArrays(gl.LINE_STRIP, first, count) 可以将各个顶点用线条串成一段路径。Api中也有 gl.lineWidth(w) 的函数可设置宽度。但实际上使用才发现 lineWidth() 函数压根不生效不管传多少最终都只能绘制出宽度1px的细线。最后还是只能继续用一堆三角形才能实现粗线条的绘制。本文的重点就是如何将那些路径点变成一堆三角形绘制出带宽度的路径。我找了网上几篇实现方法评估利弊最后整理出我目前找到最优的绘制方案。参考方案第一版各线段独立朝法向量方向扩展左右端点第二版根据前后线段的“法向量平均”来扩展转角处的左右端点https://blog.csdn.net/dabaooooq/article/details/139361903第一版在上面这篇文章优化前每一段路径分别扩展最终会得到转角处割裂的线段。效果图就看上面那篇文章。第二版后来他根据前后点的平均法向量将转角处的前后端点合并了得到了一个较平滑的线段。问题大角度转弯时宽度被压缩此方案定义的“宽度”是各个连接处的宽度当路径转弯的时候线段宽度就被挤压了。我们希望的是路径宽度一致这个效果严重失真淘汰。第三版根据前后线段的“角平分线”来扩展转角处的左右端点https://blog.csdn.net/xuanhun521/article/details/110212818问题大角度转弯时外突上图红点就是路径点当锐角转弯时前后线段外框在角平分线的交点会突出来。路径整体宽度是ok的备选。我的基础方案第四版上述其他人的方案主要思路都是合并转角处的端点来维持转弯处的连续性。我的思路就不合并转角处的端点了就是在第一版的基础上在转角加个小三角形把转角的缺口填平。思路很简单但是还要根据是向左转还是向右转来决定填哪一边的三角形。有没有更简单的方法呢有的就是不管方向直接把前后线段的4个端点都连起来就对了。在代码中就是按照统一的规则一直填矩形两个三角形就对了。ABC 填第一个三角形CBD填第二个三角形完成第一个矩形绘制接下来CD - EF 的连接过程跟绘制第一个矩形一样。先是CDE根据右手坐标系的规则CDE的面方向是朝屏幕内的方向如果开启了背面剪裁这个三角形是看不到的。但是无所谓这个三角形本来就在线段范围内看不到就算了。接下来重头戏 EDF可看到EDF把我们想要的右下角填起来了而且面方向也是对着屏幕外我们看得到完美。然后这个 ABC – CBD – CDE – DEF 的填充方式就是三角带TRIANGLE_STRIPhttps://blog.csdn.net/qq526495605/article/details/47688841如此在我们的代码就很简单了先准备好所有线段的4个端点再利用 TRIANGLE_STRIP 直接绘制就完成了转角平滑的路径了。实现代码!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleWebGL 宽路径绘制/title style body { margin: 0; height: 100vh; overflow: hidden; background: black; display: flex; justify-content: center; align-items: center; } canvas { display: block; border: 1px solid #444; box-shadow: 0 0 30px rgba(0,0,0,0.8); background: #222; } /style /head body canvas idglCanvas width600 height600/canvas script // 工具: 计算法线 (垂直方向) function getNormal(p1, p2) { const dx p2.x - p1.x; const dy p2.y - p1.y; // 两点长度 const len Math.sqrt(dx * dx dy * dy); // // p1 - p2 的向量 // let dir { x: dx, y: dy }; // // 向量归一化 // if(len ! 0) { // 避免除0 // dir.x / len; // dir.y / len; // } // // 将 p12 向量 转90度就是法线方向 // const normal {x: -dir.y, y: dir.x}; // 上面那一段注释是原理说明计算的中间体我们都不要要的就是下面这个结果 return { x: -dy / len, y: dx / len }; } // 工具: 编译着色器 function compileShader(gl, source, type) { const shader gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error(Shader error:, gl.getShaderInfoLog(shader)); return null; } return shader; } function run() { // 初始化webgl环境 const canvas document.getElementById(glCanvas); let gl canvas.getContext(webgl2); if(!gl) gl canvas.getContext(webgl); if (!gl) { alert(您的浏览器不支持 WebGL。); return; } // 路径点 const points [ { x: -0.8, y: 0 }, { x: -0.4, y: 0 }, { x: 0, y: 0.4 }, { x: 0.8, y: 0 }, { x: 0.5, y: -0.2 }, ]; // 路径宽度(半径) const pathWidth 0.05; // n 个点组成 n-1 段路径 const lineCount points.length - 1; if(lineCount 0) return; // 至少要2个点才能画路径 // 顶点数量每段路径是 4 个顶点组成的矩形 const vCount lineCount * 4; // 组装 webgl 要用的顶点 BUFFER 数组 const vertices new Float32Array(vCount * 2); // vCount * 2 每个顶点由 (x,y) 2个值组成 let index 0; for(let i 0 ; i lineCount ; i) { const p1 points[i]; // 线段起始点 const p2 points[i 1]; // 线段终点 const normal getNormal(p1, p2); // 四个顶点 vertices[index] p1.x normal.x * pathWidth; // p0.x 左上由 p1 朝 法线方向 偏移 vertices[index] p1.y normal.y * pathWidth; // p0.y vertices[index] p1.x - normal.x * pathWidth; // p1.x 左下由 p1 朝 -法线方向 偏移 vertices[index] p1.y - normal.y * pathWidth; // p1.y vertices[index] p2.x normal.x * pathWidth; // p2.x 右上由 p2 朝 法线方向 偏移 vertices[index] p2.y normal.y * pathWidth; // p2.y vertices[index] p2.x - normal.x * pathWidth; // p3.x 右下由 p2 朝 -法线方向 偏移 vertices[index] p2.y - normal.y * pathWidth; // p3.y } // 着色器 const vertexShaderSource attribute vec2 a_position; void main() { gl_Position vec4(a_position, 0.0, 1.0); } ; const fragmentShaderSource precision mediump float; void main() { gl_FragColor vec4(1.0, 1.0, 1.0, 1.0); } ; const vs compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER); const fs compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER); if (!vs || !fs) { alert(着色器编译失败); return; } const program gl.createProgram(); gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); gl.useProgram(program); // glsl变量索引 const posAttr gl.getAttribLocation(program, a_position); // 创建缓冲区 const vbo gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 传入顶点 gl.enableVertexAttribArray(posAttr); gl.vertexAttribPointer( posAttr, // 顶点属性的索引 2, // 每个顶点组成数量 2 个值我们只传入 xy 坐标。如果是三维传入 xyz 这里就要传 3 gl.FLOAT, // 顶点类型 float false, // 是否归一化到特定的范围对 FLOAT 类型数据设置无效 2 * Float32Array.BYTES_PER_ELEMENT, // stride 步长 数组中一行长度每个顶点 2 个 float2 * 4 8 0 // offset 字节偏移量 ); // 清屏 gl.clearColor(0.12, 0.12, 0.12, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三角带 gl.drawArrays(gl.TRIANGLE_STRIP, 0, vCount); gl.disableVertexAttribArray(posAttr); } run(); /script /body /html基础方案进阶着色器计算顶点在前个方案的代码中是在JavaScript中计算偏移的顶点坐标的全在CPU中计算。然后由于参考了第二版的代码他是将计算顶点偏移的工作放到着色器中实现也就是可以透过GPU加速顶点的计算可以有更高的计算效率。第五版原理我需要在每个顶点传入2个顶点例如 p1 – p2其左上顶点 A 需要传入 p1、p2的xy坐标再加上法线方向偏移量决定是朝哪个方向偏。例如A顶点传正值就是 p1点朝 p1-2的法线方向偏B顶点传负值就是朝p1-2的“负”法线方向偏。如此每个线段需要传入 4 个顶点。每个顶点 5个值(p1x, p1y, p2x, p2y, sideWidth)。每个线段共20个值。实现代码!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleWebGL 宽路径绘制/title style body { margin: 0; height: 100vh; overflow: hidden; background: black; display: flex; justify-content: center; align-items: center; } canvas { display: block; border: 1px solid #444; box-shadow: 0 0 30px rgba(0,0,0,0.8); background: #222; } /style /head body canvas idglCanvas width600 height600/canvas script // 工具: 编译着色器 function compileShader(gl, source, type) { const shader gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error(Shader error:, gl.getShaderInfoLog(shader)); return null; } return shader; } function run() { // 初始化webgl环境 const canvas document.getElementById(glCanvas); let gl canvas.getContext(webgl2); if(!gl) gl canvas.getContext(webgl); if (!gl) { alert(您的浏览器不支持 WebGL。); return; } // 路径点 const points [ { x: -0.8, y: 0 }, { x: -0.4, y: 0 }, { x: 0, y: 0.4 }, { x: 0.8, y: 0 }, { x: 0.5, y: -0.2 }, ]; // 路径宽度(半径) const pathWidth 0.05; // n 个点组成 n-1 段路径 const lineCount points.length - 1; if(lineCount 0) return; // 至少要2个点才能画路径 // 顶点数量每段路径是 4 个顶点组成的矩形 const vCount lineCount * 4; // 组装 webgl 要用的顶点 BUFFER 数组 // vCount * 5 每个顶点由 p1x, p1y, p2x, p2y, width 组成p1提供原点p2提供 p1-p2的方向用以计算法线width提供沿法线方向的偏移量 const vertices new Float32Array(vCount * 5); let index 0; for(let i 0 ; i lineCount ; i) { const p1 points[i]; // 线段起始点 const p2 points[i 1]; // 线段终点 // 左上点 vertices[index] p1.x; vertices[index] p1.y; vertices[index] p2.x; vertices[index] p2.y; vertices[index] pathWidth; // 左下点 vertices[index] p1.x; vertices[index] p1.y; vertices[index] p2.x; vertices[index] p2.y; vertices[index] -pathWidth; // 右上点 vertices[index] p2.x; vertices[index] p2.y; vertices[index] p1.x; vertices[index] p1.y; vertices[index] -pathWidth; // 由于此顶点计算的向量方向是 p2 - p1法线方向是反的所以传入的 width 是负的 // 右下点 vertices[index] p2.x; vertices[index] p2.y; vertices[index] p1.x; vertices[index] p1.y; vertices[index] pathWidth; } // 着色器 const vertexShaderSource attribute vec2 a_position; attribute vec2 a_position2; attribute float a_width; void main() { vec2 p1 a_position; vec2 p2 a_position2; float len length(p2 - p1); // 计算 p1-p2 的长度 vec2 dir; // p1-p2向量 if (len ! 0.0) { dir normalize(p2 - p1); // 计算 p1-p2 向量并归一化 } else { dir vec2(0.0, 0.0); // p1,p2 是同一点给一个0向量到时计算偏移量也是0 } vec2 normal vec2(-dir.y, dir.x); // 向量转90度变法线方向 vec2 offset normal * a_width; // 偏移量法线向量 * 偏移宽度 vec2 finalPos p1 offset; // 计算最终偏移点 gl_Position vec4(finalPos, 0.0, 1.0); } ; const fragmentShaderSource precision mediump float; void main() { gl_FragColor vec4(1.0, 1.0, 1.0, 1.0); } ; const vs compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER); const fs compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER); if (!vs || !fs) { alert(着色器编译失败); return; } const program gl.createProgram(); gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); gl.useProgram(program); // glsl变量索引 const p1Attr gl.getAttribLocation(program, a_position); const p2Attr gl.getAttribLocation(program, a_position2); const widthAttr gl.getAttribLocation(program, a_width); // 创建缓冲区 const vbo gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 步长每个组数据 5 个 float5 * 4 20 const stride 5 * Float32Array.BYTES_PER_ELEMENT; // bufferData 每组的前两个值是p1顶点 vec2 a_position gl.enableVertexAttribArray(p1Attr); gl.vertexAttribPointer(p1Attr, 2, gl.FLOAT, false, stride, 0); // bufferData 每组的第3,4个值是p2顶点 vec2 a_position2 gl.enableVertexAttribArray(p2Attr); gl.vertexAttribPointer(p2Attr, 2, gl.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT); // 2 * Float32Array.BYTES_PER_ELEMENT 偏移首2个值 // bufferData 每组第5个值是偏移宽度 float a_width gl.enableVertexAttribArray(widthAttr); gl.vertexAttribPointer(widthAttr, 1, gl.FLOAT, false, stride, 4 * Float32Array.BYTES_PER_ELEMENT); // 清屏 gl.clearColor(0.12, 0.12, 0.12, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三角带 gl.drawArrays(gl.TRIANGLE_STRIP, 0, vCount); gl.disableVertexAttribArray(p1Attr); gl.disableVertexAttribArray(p2Attr); gl.disableVertexAttribArray(widthAttr); } run(); /script /body /html方案进阶2通过顶点ID优化传入值第六版在前个方案的代码中每个顶点要传5个值有点冗余。有没有办法再优化一下呢后来找到了在GLSL中有 gl_VertexID 这么一个参数可使用用以判断是哪一个顶点这样就不需要传 sideWidth 了每个顶点就只要传4个值。不过gl_VertexID 需要在 opengles 3.0 才有需要注意兼容性。实现代码!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleWebGL 宽路径绘制/title style body { margin: 0; height: 100vh; overflow: hidden; background: black; display: flex; justify-content: center; align-items: center; } canvas { display: block; border: 1px solid #444; box-shadow: 0 0 30px rgba(0,0,0,0.8); background: #222; } /style /head body canvas idglCanvas width600 height600/canvas script // 工具: 编译着色器 function compileShader(gl, source, type) { const shader gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error(Shader error:, gl.getShaderInfoLog(shader)); return null; } return shader; } function run() { // 初始化webgl环境 const canvas document.getElementById(glCanvas); let gl canvas.getContext(webgl2); if(!gl) gl canvas.getContext(webgl); if (!gl) { alert(您的浏览器不支持 WebGL。); return; } // 路径点 const points [ { x: -0.8, y: 0 }, { x: -0.4, y: 0 }, { x: 0, y: 0.4 }, { x: 0.8, y: 0 }, { x: 0.5, y: -0.2 }, ]; // 路径宽度(半径) const pathWidth 0.05; // n 个点组成 n-1 段路径 const lineCount points.length - 1; if(lineCount 0) return; // 至少要2个点才能画路径 // 顶点数量每段路径是 4 个顶点组成的矩形 const vCount lineCount * 4; // 组装 webgl 要用的顶点 BUFFER 数组 // vCount * 4 每个顶点由 p1x, p1y, p2x, p2y 组成p1提供原点p2提供 p1-p2的方向用以计算法线 const vertices new Float32Array(vCount * 4); let index 0; for(let i 0 ; i lineCount ; i) { const p1 points[i]; // 线段起始点 const p2 points[i 1]; // 线段终点 for(let j 0 ; j 4 ; j) { vertices[index] p1.x; vertices[index] p1.y; vertices[index] p2.x; vertices[index] p2.y; } } // 着色器 const vertexShaderSource #version 300 es in vec2 a_position; in vec2 a_position2; uniform float u_width; void main() { vec2 p1 a_position; vec2 p2 a_position2; float len length(p2 - p1); // 计算 p1-p2 的长度 vec2 dir; // p1-p2向量 if (len ! 0.0) { dir normalize(p2 - p1); // 计算 p1-p2 向量并归一化 } else { dir vec2(0.0, 0.0); // p1,p2 是同一点给一个0向量到时计算偏移量也是0 } vec2 normal vec2(-dir.y, dir.x); // 向量转90度变法线方向 int vid gl_VertexID % 4; // 获取线段点的索引 0~3 float side (vid 0 || vid 2) ? 1.0 : -1.0; // 左上右上点朝 法线方向左下右下朝 -法线方向 vec2 offset normal * u_width * side; // 偏移量法线向量 * 偏移宽度 vec2 p vid 2 ? p1 : p2; // vid 2 就是从左侧的点开始偏移不然就是右侧的点 vec2 finalPos p offset; // 计算最终偏移点 gl_Position vec4(finalPos, 0.0, 1.0); } ; const fragmentShaderSource #version 300 es precision mediump float; out vec4 fragColor; void main() { fragColor vec4(1.0, 1.0, 1.0, 1.0); } ; const vs compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER); const fs compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER); if (!vs || !fs) { alert(着色器编译失败); return; } const program gl.createProgram(); gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); gl.useProgram(program); // glsl变量索引 const p1Attr gl.getAttribLocation(program, a_position); const p2Attr gl.getAttribLocation(program, a_position2); const uWidth gl.getUniformLocation(program, u_width); // 创建缓冲区 const vbo gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 步长每个组数据 4 个 float4 * 4 16 const stride 4 * Float32Array.BYTES_PER_ELEMENT; // bufferData 每组的前两个值是p1顶点 vec2 a_position gl.enableVertexAttribArray(p1Attr); gl.vertexAttribPointer(p1Attr, 2, gl.FLOAT, false, stride, 0); // bufferData 每组的第3,4个值是p2顶点 vec2 a_position2 gl.enableVertexAttribArray(p2Attr); gl.vertexAttribPointer(p2Attr, 2, gl.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT); // 2 * Float32Array.BYTES_PER_ELEMENT 偏移首2个值 // 设置宽度值 gl.uniform1f(uWidth, pathWidth); // 清屏 gl.clearColor(0.12, 0.12, 0.12, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三角带 gl.drawArrays(gl.TRIANGLE_STRIP, 0, vCount); gl.disableVertexAttribArray(p1Attr); gl.disableVertexAttribArray(p2Attr); } run(); /script /body /htmlCPU方案与GPU方案对比上面第四版在JavaScript中计算顶点的我称为“CPU方案”第五、六版在着色器中计算顶点的我称为“GPU方案”。以单次计算效率来说GPU方案是多核并行运算效率可能更好。但是在JS运算中我们可以复用计算结果。例如一个线段的4个顶点其法向量值是一样的只要计算一次就能复用4次。在GPU方案就没办法这样复用每个顶点都要重新计算法线方向这可能造成计算资源的浪费更耗电。另外如果我们是在顶点不变的情况下进行动画切换视角等CPU方案的顶点计算只要算一次就一直复用了GPU方案则是在每次渲染都要算算算实在不值。还有CPU方案每个顶点都是算好的只要传入算好的 x,y两个值。GPU方案至少要传2个点四个值更耗内存。综上所述CPU方案是我认为更好的方案项目中使用首选GPU方案仅做为知识扩展。转角优化上面的方案在一般情况下表现都没太大问题。但是如果遇到两个点距离很接近的情况其转角处就有问题了。例如下面这组数据const points [{ x: -0.8, y: 0 },{ x: 0, y: 0 },{ x: 0, y: 0.01 },{ x: 0.4, y: 0.5 },];第2、3点距离非常接近绘制出来出现缺口为什么这样呢我们把最后一段数据去掉看看对比两张图就可以知道由于p2-p3距离很短只稍微向上走一点点还不及路径宽。此时下个点往右上走右上走的起点往右下扩展就超出右下那个斜边范围了。那怎么解决呢我们参考一下 Canvas 2D 是怎么处理转角问题的。https://www.twle.cn/l/yufei/canvas/canvas-basic-path-linejoin.htmllineJoin 默认值是 miter实测效果与第三版一致锐角转弯会有明显的尖突。而 bevel 的效果就与我们上述的一致有短距离线段时也会出现这个缺口。我怀疑 Canvas 2D 的底层也是用跟我一样那个方案处理的最后效果最完美的就是 round转角用圆角填充。第七版那要怎么实现呢其实就是在上述线段画完之后在每个顶点加一个圆点那就完美解决了。WebGL绘制圆点的方法参照如下https://download.csdn.net/blog/column/12309266/133282078最终效果实现代码!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleWebGL 宽路径绘制/title style body { margin: 0; height: 100vh; overflow: hidden; background: black; display: flex; justify-content: center; align-items: center; } canvas { display: block; border: 1px solid #444; box-shadow: 0 0 30px rgba(0,0,0,0.8); background: #222; } /style /head body canvas idglCanvas width600 height600/canvas script // 工具: 计算法线 (垂直方向) function getNormal(p1, p2) { const dx p2.x - p1.x; const dy p2.y - p1.y; // 两点长度 const len Math.sqrt(dx * dx dy * dy); // // p1 - p2 的向量 // let dir { x: dx, y: dy }; // // 向量归一化 // if(len ! 0) { // 避免除0 // dir.x / len; // dir.y / len; // } // // 将 p12 向量 转90度就是法线方向 // const normal {x: -dir.y, y: dir.x}; // 上面那一段注释是原理说明计算的中间体我们都不要要的就是下面这个结果 return { x: -dy / len, y: dx / len }; } // 工具: 编译着色器 function compileShader(gl, source, type) { const shader gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error(Shader error:, gl.getShaderInfoLog(shader)); return null; } return shader; } function run() { // 初始化webgl环境 const canvas document.getElementById(glCanvas); let gl canvas.getContext(webgl2); if(!gl) gl canvas.getContext(webgl); if (!gl) { alert(您的浏览器不支持 WebGL。); return; } // 路径点 const points [ { x: -0.8, y: 0 }, { x: -0.4, y: 0 }, { x: 0, y: 0.4 }, { x: 0.8, y: 0 }, { x: 0.5, y: -0.2 }, ]; // 路径宽度(半径) const pathWidth 0.05; // n 个点组成 n-1 段路径 const lineCount points.length - 1; if(lineCount 0) return; // 至少要2个点才能画路径 // 顶点数量每段路径是 4 个顶点组成的矩形 const vCount lineCount * 4; // 组装 webgl 要用的顶点 BUFFER 数组 const vertices new Float32Array(vCount * 2); // vCount * 2 每个顶点由 (x,y) 2个值组成 let index 0; for(let i 0 ; i lineCount ; i) { const p1 points[i]; // 线段起始点 const p2 points[i 1]; // 线段终点 const normal getNormal(p1, p2); // 四个顶点 vertices[index] p1.x normal.x * pathWidth; // p0.x 左上由 p1 朝 法线方向 偏移 vertices[index] p1.y normal.y * pathWidth; // p0.y vertices[index] p1.x - normal.x * pathWidth; // p1.x 左下由 p1 朝 -法线方向 偏移 vertices[index] p1.y - normal.y * pathWidth; // p1.y vertices[index] p2.x normal.x * pathWidth; // p2.x 右上由 p2 朝 法线方向 偏移 vertices[index] p2.y normal.y * pathWidth; // p2.y vertices[index] p2.x - normal.x * pathWidth; // p3.x 右下由 p2 朝 -法线方向 偏移 vertices[index] p2.y - normal.y * pathWidth; // p3.y } // 着色器 const vertexShaderSource attribute vec2 a_position; void main() { gl_Position vec4(a_position, 0.0, 1.0); } ; const fragmentShaderSource precision mediump float; void main() { gl_FragColor vec4(1.0, 1.0, 1.0, 1.0); } ; const vs compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER); const fs compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER); if (!vs || !fs) { alert(着色器编译失败); return; } const program gl.createProgram(); gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); gl.useProgram(program); // glsl变量索引 const posAttr gl.getAttribLocation(program, a_position); // 创建缓冲区 const vbo gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 传入顶点 gl.enableVertexAttribArray(posAttr); gl.vertexAttribPointer( posAttr, // 顶点属性的索引 2, // 每个顶点组成数量 2 个值我们只传入 xy 坐标。如果是三维传入 xyz 这里就要传 3 gl.FLOAT, // 顶点类型 float false, // 是否归一化到特定的范围对 FLOAT 类型数据设置无效 2 * Float32Array.BYTES_PER_ELEMENT, // stride 步长 数组中一行长度每个顶点 2 个 float2 * 4 8 0 // offset 字节偏移量 ); // 清屏 gl.clearColor(0.12, 0.12, 0.12, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三角带 gl.drawArrays(gl.TRIANGLE_STRIP, 0, vCount); gl.disableVertexAttribArray(posAttr); // 绘制圆点 // 圆点的顶点 const ciecleVertices new Float32Array(points.length * 2); // 每个顶点由 (x,y) 2个值组成 index 0; for(let i 0 ; i points.length ; i) { const p points[i]; ciecleVertices[index] p.x; ciecleVertices[index] p.y; } // 着色器 const ciecleVertexShaderSource attribute vec2 a_position; uniform float u_pointSize; void main() { gl_Position vec4(a_position, 0.0, 1.0); gl_PointSize u_pointSize; } ; const ciecleFragmentShaderSource precision mediump float; void main() { float d distance(gl_PointCoord, vec2(0.5, 0.5)); if (d 0.5) { gl_FragColor vec4(1.0, 1.0, 1.0, 1.0); } else { discard; } } ; const circleVs compileShader(gl, ciecleVertexShaderSource, gl.VERTEX_SHADER); const circleFs compileShader(gl, ciecleFragmentShaderSource, gl.FRAGMENT_SHADER); if (!circleVs || !circleFs) { alert(着色器编译失败); return; } const circleProgram gl.createProgram(); gl.attachShader(circleProgram, circleVs); gl.attachShader(circleProgram, circleFs); gl.linkProgram(circleProgram); gl.useProgram(circleProgram); // glsl变量索引 const circlePosAttr gl.getAttribLocation(circleProgram, a_position); const uPointSize gl.getUniformLocation(circleProgram, u_pointSize); // 创建缓冲区 const circleVbo gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, circleVbo); gl.bufferData(gl.ARRAY_BUFFER, ciecleVertices, gl.STATIC_DRAW); // 传入顶点 gl.enableVertexAttribArray(circlePosAttr); gl.vertexAttribPointer( circlePosAttr, // 顶点属性的索引 2, // 每个顶点组成数量 2 个值我们只传入 xy 坐标。如果是三维传入 xyz 这里就要传 3 gl.FLOAT, // 顶点类型 float false, // 是否归一化到特定的范围对 FLOAT 类型数据设置无效 2 * Float32Array.BYTES_PER_ELEMENT, // stride 步长 数组中一行长度每个顶点 2 个 float2 * 4 8 0 // offset 字节偏移量 ); // 传入圆点大小 gl.uniform1f(uPointSize, pathWidth * 600); // 绘制圆点 gl.drawArrays(gl.POINTS, 0, points.length); gl.disableVertexAttribArray(circlePosAttr); } run(); /script /body /html3D绘图的转角优化上面的代码基本把WebGL在2D的场景绘制粗路径处理完美了但是我们之所以要用WebGL是为了实现3D绘制的。在第七版中使用圆点填充转角在场景“立起来”后就不适用了这个方式画的圆点永远面向观察者没办法让它“躺”在地上。如果要在3D绘制中绘制圆形相信略懂3D绘图的都知道就是用一堆三角形去围成一圈近似一个圆。function creatCircle() { const r 0.2; const angleStep 16; const buffer: number[] []; const pathHeight Map3dConfig.PATH_HEIGHT; buffer.push(0); buffer.push(0); // 原点 buffer.push(pathHeight); for(let i 0 ; i angleStep ; i) { const a i * Math.PI * 2 / angleStep; buffer.push(r * Math.cos(a)); buffer.push(r * Math.sin(a)); buffer.push(pathHeight); } buffer.push(r * Math.cos(0)); buffer.push(r * Math.sin(0)); buffer.push(pathHeight); circleVertexes new Float32Array(buffer); circleCount angleStep 2; }为了避免每个顶点都生成圆浪费效率又耗内存我们可以只生成一个圆然后再用矩阵位移到每个路径点去执行绘制圆。gl.drawArrays(gl.TRIANGLE_FAN, 0, circleCount);由于设计视角转换的代码太多了这边就不放了只要知道思路就知道怎么处理了。最后附上我在3D场景绘制粗路径的图