前言
地圖的渲染其實可以分解為線棘钞、面、紋理干毅、文字的渲染宜猜。為了了解地圖渲染的實現(xiàn)原理并實際練習(xí)WebGL,進行了這個系列的練習(xí)硝逢,線是第一步姨拥。
本文不贅述WebGL的基本知識,只對運用到的知識點進行一下簡單的回顧:
著色器
WebGL需要兩種著色器:頂點著色器和片元著色器趴捅,以O(shè)penGL 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';
考慮到繪制一條線使用同一種顏色霹疫,與頂點無關(guān)拱绑,所以在片元著色器中定義了一個uniform變量u_Color。
三角形
WebGL繪制模型的基本單位是三角形丽蝎,繪制一條有寬度的線并不能像Canvas2D那樣設(shè)置strokeStyle之后調(diào)用stroke()即可猎拨,而是需要將整條線拆分成多個小三角形膀藐,這個過程稱為三角剖分。
線段本身的三角剖分是很簡單的红省,即矩形剖分為兩個三角形额各。但是折線有拐角(lineJoin)和端頭(lineCap),且需要支持不同的樣式吧恃,這部分的剖分會稍微復(fù)雜一點虾啦,后文會詳細分析。
WebGL的drawArrays
方法支持多種模式進行多個三角形的繪制痕寓,如下所示:
矢量
三角剖分的計算過程中使用到了矢量和矩陣的一些基本運算傲醉,涉及到了矢量的加減法、乘法呻率、單位化硬毕、旋轉(zhuǎn)等,這些讀者應(yīng)自行了解和掌握礼仗。本文封裝了二維矢量的相關(guān)計算方法到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;
}
/**
* Vector2.prototype.normalize 單位化
* Vector2.prototype.scalarProduct 與標量相乘
* Vector2.prototype.dotProduct 與矢量點乘
* Vector2.prototype.add 與矢量相加
* Vector2.prototype.minus 與矢量相減
* Vector2.prototype.rotate 旋轉(zhuǎn)角度
* Vector2.prototype.copy 復(fù)制
* Vector2.prototype.getVertical 獲取單位法向量
* /
繪制目標
線這里專指折線,使用線段將一組離散的坐標點依次連接而形成元践。由于地圖是呈現(xiàn)在z=0平面上韭脊,本文也只探討在同一平面上延伸的線(扁平的),所以線的坐標點不用關(guān)心z坐標单旁,使用二維矢量(x, y)即可乾蓬。后文以coords
表示線的坐標數(shù)組。
除了coords
慎恒,線的樣式也是其重要的屬性任内。如下例所示,線可設(shè)置寬度融柬、顏色死嗦,同時可設(shè)置邊線的寬度和顏色;端頭以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' // 拐角樣式
}
[站外圖片上傳中...(image-c011bf-1545711490022)]
為了之后的一系列練習(xí),本文封裝了一個Shape
類用于WebGL繪制基本圖形箱熬,抽象出了一個構(gòu)造的接口和通用的方法类垦、屬性如下:
- 構(gòu)造函數(shù):
new Shape(opts)
狈邑,參數(shù)說明如下
字段名 | 類型 | 說明 |
---|---|---|
type | String | 圖形類型:polyline , polygon , circle
|
glCtx | WebGLRenderingContext | WebGL繪圖上下文 |
camera | Matrix4 | 視圖投影矩陣 |
coords | Array.<Number> | 坐標 |
style | Object | 樣式(不同圖形類型支持的樣式字段不同) |
- 方法
方法 | 返回值 | 說明 |
---|---|---|
setCamera(camera: Matrix4) | None | 設(shè)置視圖投影矩陣 |
setCoords(coords: Array.<Number>) | None | 設(shè)置坐標 |
setStyle(style: Object) | None | 設(shè)置樣式 |
另外還封裝了WebglColor
、Matrix4
蚤认、Vector2
米苹,最終使用示例如下:
/**
* 創(chuàng)建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.setPerspective(fov, aspect, near, far);
camera.lookAt(0, 0, distance, 0, 0, 0, 0, 1, 0);
camera.rotate(pitch, 1, 0, 0);
return camera;
}
var canvas = document.getElementById('webgl');
var gl = canvas.getContext('webgl');
var camera = createCamera(canvas.clientWidth, canvas.clientHeight, -30); // 構(gòu)建視圖投影矩陣
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
}
});
// 構(gòu)造完成或重置屬性之后會自動繪制圖形
具體實現(xiàn)
繪制流程
我們先了解一下繪制的整體流程,然后依次詳解每個步驟砰琢。
function drawSolidLine(gl, camera, coords, style) {
var mvpMatrix = camera;
var color = style.color;
// 三角剖分
var triangulation = getLineTriangulation(coords, style);
// 創(chuàng)建并初始化著色器蘸嘶,獲取變量存儲位置
var locations = initUColorShader(gl);
if (!locations) {
return;
}
// 創(chuàng)建緩沖區(qū)并傳入數(shù)據(jù)
var vertices = triangulation.vertices;
if (!initVertexBuffers(gl, vertices)) {
return;
}
// 變量賦值
gl.uniformMatrix4fv(locations.u_MvpMatrix, false, mvpMatrix.elements);
gl.uniform4f(locations.u_Color, color.r, color.g, color.b, color.a);
gl.vertexAttribPointer(locations.a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(location.a_Position);
// 執(zhí)行繪制任務(wù)
var tasks = triangulation.tasks;
tasks.forEach(function(task) {
gl.drawArrays(gl[task.mode], task.start, task.cnt);
});
}
如代碼所示:
- 三角剖分:不同圖形的剖分過程不同,最終返回剖分后的頂點數(shù)組陪汽、繪制任務(wù)亏较。每個繪制任務(wù)指明了頂點索引范圍及繪制模式。
triangulation = {
vertices: [x0, y0, z, x1, y1, z, ...]
tasks: [task0, task1, ...]
}
- 創(chuàng)建并初始化著色器掩缓,獲取變量存儲位置:
initUColorShader
創(chuàng)建一個單一顏色的著色器雪情,然后創(chuàng)建、使用程序你辣,獲取并返回著色器中每個變量的存儲位置巡通。
locations = {
a_Position: ..,
u_MvpMatrix: ..,
u_Color: ..
}
- 創(chuàng)建緩沖區(qū)并傳入數(shù)據(jù):
進行緩沖區(qū)的創(chuàng)建、綁定等操作舍哄,將三角剖分后得到的頂點數(shù)組triangulation.vertices
寫入緩沖區(qū) - 變量賦值:
為著色器中的變量賦值宴凉,向存儲位置locations
寫入數(shù)據(jù) - 執(zhí)行繪制任務(wù):
遍歷triangulation.tasks
,按指定的模式表悬、索引范圍進行繪制
下文詳細講解每個步驟的具體實現(xiàn)弥锄。
三角剖分
線的剖分可以分解為三個部分,一是線段蟆沫,二是端頭籽暇,三是拐角。
1. 準備工作
轉(zhuǎn)換coords
為二維點饭庞,并計算每個線段的單位法向量戒悠。因為需要在路徑上進行垂直擴寬,且寬度與線段長度無關(guān)舟山,所以法向量取單位長度即可绸狐。
// 將坐標轉(zhuǎn)換為點、線段矢量累盗、線段單位法向量
var path = [],
segments = [],
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 segment = pathPoint.minus(prePoint);
segments.push(segment);
verticalVectors.push(segment.getVertical());
}
pathLength++;
}
2. 線段剖分
線段剖分比較簡單寒矿,在路徑點坐標上加擴寬的法向量即可,需注意連接兩個線段的路徑點需要根據(jù)兩條線段的法向量若债,拓展出4個頂點符相。
path.forEach((pathPoint, index) => {
// basePoints為擴寬后的頂點坐標
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) {
basePoints.push(pathPoint.add(v0));
basePoints.push(pathPoint.minus(v0));
}
if (v1) {
basePoints.push(pathPoint.add(v1));
basePoints.push(pathPoint.minus(v1));
}
});
3. 端頭剖分
端頭只需要在首尾路徑點上進行擴展。端頭支持三種樣式:butt
不需要增加坐標點拆座,square
需要擴展出半個正方形主巍,邊長為線寬,round
需要擴展出半個圓形垛耳,直徑為線寬江场。
square
端頭剖分需要找到正方形的頂點嘁捷,只需將線段法向量旋轉(zhuǎn)90度,即可得到偏移向量offsetVector
搞旭,示意圖如下:
round
端頭剖分需要在圓形弧線上找到等距且密集的點,只需將線段法向量以小角度旋轉(zhuǎn)n次直到2*PI菇绵,即可得到弧線上的頂點肄渗,最終將圓心與頂點以TRIANGLE_FAN的方式繪制即可實現(xiàn)圓形,示意圖如下:function getLineCapTrigl(pathPoint, verticalVector, style, isHead) {
var subPoints = [];
var mode = "TRIANGLE_STRIP";
var width = style.width / 2;
var v = verticalVector.copy().scalarProduct(width);
switch (style.lineCap) {
case 'butt':
break;
case 'square':
var offsetVector = v.getVertical().scalarProduct(width);
if (isHead) {
subPoints.push(pathPoint.add(v).add(offsetVector));
subPoints.push(pathPoint.minus(v).add(offsetVector));
} else {
subPoints.push(pathPoint.add(v).minus(offsetVector));
subPoints.push(pathPoint.minus(v).minus(offsetVector));
}
subPoints.push(pathPoint.add(v));
subPoints.push(pathPoint.minus(v));
break;
case 'round':
subPoints.push(pathPoint);
var rotateVector;
for (let angle = 0; angle < 2.1 * Math.PI; angle += Math.PI/16) {
rotateVector = v.rotate(angle);
subPoints.push(pathPoint.add(rotateVector));
}
mode = "TRIANGLE_FAN";
break;
default:
console.error('Invalid lineCap:' + style.lineCap);
}
return {
points: subPoints,
mode: mode
};
}
4. 拐角剖分
拐角是在除去首尾兩端的路經(jīng)點上進行擴展咬最。支持三種樣式:bevel
不需要增加坐標點(線段剖分后連接處自然形成了平角)翎嫡,miter
需要填補線段延長線交匯出的尖角,round
需要填補扇形永乌,直徑為線寬惑申。
miter
的剖分相對來說比較復(fù)雜一點,如下圖所示翅雏,并非是一個菱形圈驼,而是兩個以線段法向量為直角邊的直角三角形拼接而成,計算公式如下:
function getLineJoinTrigl(pathPoint, v0, v1, style) {
var subPoints = [];
var mode = "TRIANGLE_STRIP";
var width = style.width / 2;
var v0_scale = v0.copy().scalarProduct(width);
var v1_scale = v1.copy().scalarProduct(width);
switch (style.lineJoin) {
case 'miter':
var length = width / Math.sqrt((v0.dotProduct(v1) + 1) / 2);
var joinVector = v0.add(v1).normalize().scalarProduct(length);
subPoints.push(pathPoint);
subPoints.push(pathPoint.add(v0_scale));
subPoints.push(pathPoint.add(joinVector));
subPoints.push(pathPoint.add(v1_scale));
subPoints.push(pathPoint.minus(v0_scale));
subPoints.push(pathPoint.minus(joinVector));
subPoints.push(pathPoint.minus(v1_scale));
mode = "TRIANGLE_FAN";
break;
case 'bevel':
break;
case 'round':
subPoints.push(pathPoint);
var rotateVector;
for (let angle = 0; angle < 2.1 * Math.PI; angle += Math.PI/16) {
rotateVector = v0_scale.rotate(angle);
subPoints.push(pathPoint.add(rotateVector));
}
mode = "TRIANGLE_FAN";
break;
default:
console.error('Invalid lineJoin:' + style.lineJoin);
}
return {
points: subPoints,
mode: mode
};
}
初始化著色器
initUColorShader
負責(zé)建立和初始化著色器望几,主要分為三個步驟绩脆,一是通過UColorShader()
獲取單一顏色著色器代碼;二是創(chuàng)建并使用程序橄抹;三是獲取變量位置靴迫。
/**
* 創(chuàng)建并初始化著色器
* @param {WebGLRenderingContext} gl
*/
function initUColorShader(gl) {
// 獲取著色器代碼
var shaders = UColorShader();
// 創(chuàng)建并使用程序
if (!initShaders(gl, shaders.vshader, shaders.fshader)) {
console.error('Failed to intialize shaders.');
return null;
}
// 獲取變量位置
return getLocations();
}
1. 著色器代碼
如前文所述,UColorShader
用以生成單一顏色著色器楼誓,代碼如下:
/**
* UColorShader: 單顏色著色器
* 單一顏色u_Color矢劲,支持矩陣變換u_MvpMatrix, 頂點坐標a_Position
*/
function UColorShader() {
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';
return {
vshader: VSHADER_SOURCE,
fshader: FSHADER_SOURCE
};
}
2. 創(chuàng)建并使用程序
initShaders
這部分是WebGL繪制流程中通用的步驟,不進行過多的解釋慌随,主要有以下7個步驟芬沉。
- 創(chuàng)建著色器對象:
gl.createShader(type)
- 填充著色器源代碼:
gl.shaderSource(shader, source)
- 編譯著色器:
gl.compileShader(shader)
- 創(chuàng)建程序?qū)ο螅?code>gl.createProgram()
- 為程序?qū)ο蠓峙渲鳎?code>gl.attachShader(program, shader) // 注:頂點著色器、片元著色器需要分別分配
- 連接程序?qū)ο螅?code>gl.linkProgram(program) // 注:將頂點著色器與片元著色器連接
- 使用程序?qū)ο螅?code>gl.useProgram(program)
3. 獲取變量位置
至此阁猜,我們創(chuàng)建好了一個具有三個屬性變量的著色程序丸逸,之后我們需要為這三個變量賦值,所以需要獲取到這三個變量的存儲位置剃袍。a_Position
和u_MvpMatrix
黄刚、u_Color
的變量聲明不同,獲取存儲位置的方法也相應(yīng)的不同:
function getLocations() {
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
var u_Color = gl.getUniformLocation(gl.program, 'u_Color');
return {
a_Position: a_Position,
u_MvpMatrix: u_MvpMatrix,
u_Color: u_Color
};
}
數(shù)據(jù)緩沖區(qū)
因為需要一次性將全部頂點傳入頂點著色器民效,所以需要initVertexBuffers
負責(zé)創(chuàng)建數(shù)據(jù)緩沖區(qū)并寫入數(shù)據(jù)憔维。
/**
* 創(chuàng)建緩沖區(qū)并傳入數(shù)據(jù)
* @param {WebGLRenderingContext} gl
* @param {Float32Array} vertices
*/
function initVertexBuffers(gl, vertices) {
// 創(chuàng)建緩沖區(qū)
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
console.error('Failed to create the buffer object');
return false;
}
// 綁定緩沖區(qū)對象:指明其用途
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 寫入數(shù)據(jù)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
return true;
}
變量賦值
u_MvpMatrix
和u_Color
變量可直接調(diào)用對應(yīng)類型的方法進行一次傳值涛救,比如:
gl.uniformMatrix4fv(locations.u_MvpMatrix, false, mvpMatrix.elements);
WebGLRenderingContext.uniformMatrix[234]fv(location, transpose, value)
用于給矩陣類型的變量賦值,2业扒、3检吆、4表示矩陣的維度。
a_Position
變量賦值需要從緩沖區(qū)中讀取數(shù)據(jù)程储,需要調(diào)用vertexAttribPointer
方法將緩沖區(qū)對象分配給變量a_Position
蹭沛,并開啟訪問權(quán):
gl.vertexAttribPointer(locations.a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(location.a_Position);
其中3
表示每個頂點的分量數(shù),a_Position
是一個vec4
變量章鲤,這里讀取三個分量的數(shù)據(jù)賦值給x摊灭、y、z败徊,第4位會自動補1帚呼。gl.FLOAT
表示數(shù)據(jù)格式為浮點型。false
標明無需將數(shù)據(jù)歸一化皱蹦。最后兩個0
表示頂點數(shù)據(jù)間無間隔萝挤,數(shù)據(jù)無偏移。
執(zhí)行繪制任務(wù)
三角剖分步驟中生成了繪制任務(wù)tasks = [{mode, start, cnt}, ...]
根欧,每個任務(wù)指定了模式(TRIANGLE_STRIP
/TRIANGLE_FAN
/TRIANGLES
)怜珍、起始點索引值、繪制點數(shù)量凤粗,所以遍歷繪制任務(wù)并調(diào)用drawArrays
進行繪制即可:
tasks.forEach(function(task) {
gl.drawArrays(gl[task.mode], task.start, task.cnt);
});
至此酥泛,繪制線的流程就結(jié)束了。
demo演示
利用上文中構(gòu)造的Shape
類嫌拣,最終實現(xiàn)了如下的demo柔袁,繪制了一條S折線,并且可以動態(tài)改變其顏色异逐、寬度捶索、端頭、拐角樣式灰瞻,同時通過鍵盤方向鍵控制Camera
腥例,動態(tài)改變視圖投影矩陣。