WebGL绘制基本图形--线
前⾔
地图的渲染其实可以分解为线、⾯、纹理、⽂字的渲染。为了了解地图渲染的实现原理并实际练习WebGL,进⾏了这个系列的练习,线是第⼀步。
本⽂不赘述WebGL的基本知识,只对运⽤到的知识点进⾏⼀下简单的回顾:
一句话个性签名着⾊器
WebGL需要两种着⾊器:顶点着⾊器和⽚元着⾊器,以OpenGL ES着⾊器语⾔进⾏编写,本⽂中使⽤的着⾊器如下:
var VSHADER_SOURCE =
晋江鞋'attribute vec4 a_Position;\n' + // 顶点坐标
'uniform mat4 u_MvpMatrix;\n' + // 模型视图投影矩阵
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
'}\n';
太阳挂在树顶上打一字谜var FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform vec4 u_Color;\n' +
'void main() {\n' +
' gl_FragColor = u_Color;\n' + // 颜⾊
'}\n';
复制代码
考虑到绘制⼀条线使⽤同⼀种颜⾊,与顶点⽆关,所以在⽚元着⾊器中定义了⼀个uniform变量u_Color。
三⾓形
WebGL绘制模型的基本单位是三⾓形,绘制⼀条有宽度的线并不能像Canvas2D那样设置strokeStyle之后调⽤stroke()即可,⽽是需要将整条线拆分成多个⼩三⾓形,这个过程称为三⾓剖分。
线段本⾝的三⾓剖分是很简单的,即矩形剖分为两个三⾓形。但是折线有拐⾓(lineJoin)和端头(lineCap),且需要⽀持不同的样式,这部分的剖分会稍微复杂⼀点,后⽂会详细分析。
WebGL的drawArrays⽅法⽀持多种模式进⾏多个三⾓形的绘制,如下所⽰:
⽮量
三⾓剖分的计算过程中使⽤到了⽮量和矩阵的⼀些基本运算,涉及到了⽮量的加减法、乘法、单位化、旋转等,这些读者应⾃⾏了解和掌握。本⽂封装了⼆维⽮量的相关计算⽅法到Vector2类中。
/**
* Constructor of Vector2
* If opt_src is specified, new vector is initialized by opt_src.
* @param opt_src source vector(option)
*/
function Vector2(opt_src) {
var v = new Float32Array(2);
if (opt_src && typeof opt_src === 'object') {
v[0] = opt_src[0]; v[1] = opt_src[1];
}
this.elements = v;
}
/**
* alize 单位化
* Vector2.prototype.scalarProduct 与标量相乘
* Vector2.prototype.dotProduct 与⽮量点乘
* Vector2.prototype.add 与⽮量相加
* Vector2.prototype.minus 与⽮量相减
* ate 旋转⾓度
* py 复制
* Vertical 获取单位法向量
* /
复制代码
绘制⽬标
线这⾥专指折线,使⽤线段将⼀组离散的坐标点依次连接⽽形成。由于地图是呈现在z=0平⾯上,本⽂也只探讨在同⼀平⾯上延伸的线(扁平的),所以线的坐标点不⽤关⼼z坐标,使⽤⼆维⽮量(x, y)即可。后⽂以coords表⽰线的坐标数组。
除了coords,线的样式也是其重要的属性。如下例所⽰,线可设置宽度、颜⾊,同时可设置边线的宽度和颜⾊;端头以canvas为标准,可⽀持三种样式:butt-平头,square-⽅头,round-圆头;拐⾓以canvas为标准,⽀持三种样式:bevel-平⾓,miter-尖⾓,round-圆⾓。
defaultLineStyle = {
strokeColor: new WebglColor(0.5, 0.5, 1, 1), // 边线颜⾊
strokeWidth: 5, // 边线宽度
fillColor: new WebglColor(0.9, 0.9, 1, 1), // 线颜⾊
fillWidth: 20, // 线宽度
lineCap: 'butt', // 端头样式
lineJoin: 'bevel' // 拐⾓样式
}
复制代码
书连为了之后的⼀系列练习,本⽂封装了⼀个Shape类⽤于WebGL绘制基本图形,抽象出了⼀个构造的接⼝和通⽤的⽅法、属性如下:构造函数:new Shape(opts),参数说明如下
字段名类型说明
type String图形类型:polyline, polygon, circle
glCtx WebGLRenderingContext WebGL绘图上下⽂
camera Matrix4视图投影矩阵
coords Array.坐标
style Object样式(不同图形类型⽀持的样式字段不同)
⽅法
⽅法返回值说明
tCamera(camera: Matrix4)None设置视图投影矩阵tCoords(coords: Array.)None设置坐标tStyle(style: Object)None设置样式
另外还封装了WebglColor、Matrix4、Vector2,最终使⽤⽰例如下:
/**
* 创建Camera矩阵
* @param {Number} width 画布宽度
* @param {Number} height 画布⾼度
* @param {Number} pitch 视线俯仰⾓
*/
function createCamera(width, height, pitch) {
var camera = new Matrix4();
var fov = 60;
var distance = height / 2 / Math.tan(fov / 2 / 180 * Math.PI);
var near = 1;
var far = 1.5 * distance;
var aspect = width / height;
camera.tPerspective(fov, aspect, near, far);
camera.lookAt(0, 0, distance, 0, 0, 0, 0, 1, 0);
风扇简笔画ate(pitch, 1, 0, 0);
return camera;
}
var canvas = ElementById('webgl');
var gl = Context('webgl');
var camera = createCamera(canvas.clientWidth, canvas.clientHeight, -30); // 构建视图投影矩阵
var polyline = new Shape({
type: 'polyline',
glCtx: gl,
camera: camera,
coords: [100,100,-100,100,-100,0,100,0,100,-100,-100,-100],
style: {
strokeColor: new WebglColor(0.5, 0.5, 1, 1),
strokeWidth: 5,
fillColor: new WebglColor(0.9, 0.9, 1, 1),
fillWidth: 20
}
});
// 构造完成或重置属性之后会⾃动绘制图形
复制代码
具体实现
绘制流程
我们先了解⼀下绘制的整体流程,然后依次详解每个步骤。
function drawSolidLine(gl, camera, coords, style) {
var mvpMatrix = camera;
var color = lor;
// 三⾓剖分
var triangulation = getLineTriangulation(coords, style);
/
/ 创建并初始化着⾊器,获取变量存储位置
var locations = initUColorShader(gl);
if (!locations) {
return;
}
// 创建缓冲区并传⼊数据
var vertices = triangulation.vertices;
if (!initVertexBuffers(gl, vertices)) {
return;
}
// 变量赋值
gl.uniformMatrix4fv(locations.u_MvpMatrix, fal, mvpMatrix.elements);
gl.uniform4f(locations.u_Color, color.r, color.g, color.b, color.a);
gl.vertexAttribPointer(locations.a_Position, 3, gl.FLOAT, fal, 0, 0);
// 执⾏绘制任务
var tasks = triangulation.tasks;
tasks.forEach(function(task) {
gl.drawArrays(de], task.start, );
});
}
复制代码
孙文海
如代码所⽰:
1. 三⾓剖分:不同图形的剖分过程不同,最终返回剖分后的顶点数组、绘制任务。每个绘制任务指明了顶点索引范围及绘制模式。
triangulation = {
vertices: [x0, y0, z, x1, y1, z, ...]
tasks: [task0, task1, ...]
}
复制代码
2. 创建并初始化着⾊器,获取变量存储位置: initUColorShader创建⼀个单⼀颜⾊的着⾊器,然后创建、使⽤程序,获取并返回着⾊器
中每个变量的存储位置。
locations = {
a_Position: ..,
u_MvpMatrix: ..,
u_Color: ..
}
复制代码
3. 创建缓冲区并传⼊数据: 进⾏缓冲区的创建、绑定等操作,将三⾓剖分后得到的顶点数组triangulation.vertices写⼊缓冲区
4. 变量赋值: 为着⾊器中的变量赋值,向存储位置locations写⼊数据
新概念2课后答案
5. 执⾏绘制任务: 遍历triangulation.tasks,按指定的模式、索引范围进⾏绘制
下⽂详细讲解每个步骤的具体实现。
三⾓剖分
线的剖分可以分解为三个部分,⼀是线段,⼆是端头,三是拐⾓。
1. 准备⼯作
转换coords为⼆维点,并计算每个线段的单位法向量。因为需要在路径上进⾏垂直扩宽,且宽度与线段长度⽆关,所以法向量取单位长度即可。
// 将坐标转换为点、线段⽮量、线段单位法向量
var path = [],
gments = [],
verticalVectors = [],
pathLength = 0;
for (let index = 0; index < coords.length; index += 2) {
let x = coords[index];
let y = coords[index + 1];
let pathPoint = new Point2([x, y]);
path.push(pathPoint);
if (pathLength) {
// 相邻两点相减得到线段⽮量
let prePoint = path[pathLength - 1];
let gment = pathPoint.minus(prePoint);
gments.push(gment);
verticalVectors.Vertical());
}
pathLength++;
}
复制代码
2. 线段剖分
线段剖分⽐较简单,在路径点坐标上加扩宽的法向量即可,需注意连接两个线段的路径点需要根据两条线段的法向量,拓展出4个顶点。
path.forEach((pathPoint, index) => {
// baPoints为扩宽后的顶点坐标
var width = style.width / 2;
var v0 = index == 0 ? null : verticalVectors[index-1].copy().scalarProduct(width);
var v1 = index == pathLength - 1 ? null : verticalVectors[index].copy().scalarProduct(width);
if (v0) {
baPoints.push(pathPoint.add(v0));
baPoints.push(pathPoint.minus(v0));华山好玩吗
}
if (v1) {
baPoints.push(pathPoint.add(v1));
baPoints.push(pathPoint.minus(v1));
}
});
复制代码
3. 端头剖分
端头只需要在⾸尾路径点上进⾏扩展。端头⽀持三种样式:butt不需要增加坐标点,square需要扩展出半个正⽅形,边长为线宽,round需要扩展出半个圆形,直径为线宽。 square端头剖分需要找到正⽅形的顶点,只需将线段法向量旋转90度,即可得到偏移向量offtVector,⽰意图如下: