地圖的渲染其實能夠分解爲線、面、紋理、文字的渲染。爲了瞭解地圖渲染的實現原理並實際練習WebGL,進行了這個系列的練習,線是第一步。web
本文不贅述WebGL的基本知識,只對運用到的知識點進行一下簡單的回顧:canvas
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。bash
WebGL繪製模型的基本單位是三角形,繪製一條有寬度的線並不能像Canvas2D那樣設置strokeStyle以後調用stroke()便可,而是須要將整條線拆分紅多個小三角形,這個過程稱爲三角剖分。 ide
WebGL的drawArrays
方法支持多種模式進行多個三角形的繪製,以下所示: 函數
三角剖分的計算過程當中使用到了矢量和矩陣的一些基本運算,涉及到了矢量的加減法、乘法、單位化、旋轉等,這些讀者應自行了解和掌握。本文封裝了二維矢量的相關計算方法到Vector2
類中。webgl
/**
* 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 旋轉角度
* Vector2.prototype.copy 複製
* Vector2.prototype.getVertical 獲取單位法向量
* /
複製代碼
線這裏專指折線,使用線段將一組離散的座標點依次鏈接而造成。因爲地圖是呈如今z=0平面上,本文也只探討在同一平面上延伸的線(扁平的),因此線的座標點不用關心z座標,使用二維矢量(x, y)便可。後文以coords
表示線的座標數組。ui
除了coords
,線的樣式也是其重要的屬性。以下例所示,線可設置寬度、顏色,同時可設置邊線的寬度和顏色;端頭以canvas爲標準,可支持三種樣式:butt-平頭,square-方頭,round-圓頭;拐角以canvas爲標準,支持三種樣式:bevel-平角,miter-尖角,round-圓角。this
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繪製基本圖形,抽象出了一個構造的接口和通用的方法、屬性以下:spa
new Shape(opts)
,參數說明以下字段名 | 類型 | 說明 |
---|---|---|
type | String | 圖形類型:polyline , polygon , circle |
glCtx | WebGLRenderingContext | WebGL繪圖上下文 |
camera | Matrix4 | 視圖投影矩陣 |
coords | Array. | 座標 |
style | Object | 樣式(不一樣圖形類型支持的樣式字段不一樣) |
方法 | 返回值 | 說明 |
---|---|---|
setCamera(camera: Matrix4) | None | 設置視圖投影矩陣 |
setCoords(coords: Array.) | None | 設置座標 |
setStyle(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.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); // 構建視圖投影矩陣
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 = style.color;
// 三角剖分
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, 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);
// 執行繪製任務
var tasks = triangulation.tasks;
tasks.forEach(function(task) {
gl.drawArrays(gl[task.mode], task.start, task.cnt);
});
}
複製代碼
如代碼所示:
triangulation = {
vertices: [x0, y0, z, x1, y1, z, ...]
tasks: [task0, task1, ...]
}
複製代碼
initUColorShader
建立一個單一顏色的着色器,而後建立、使用程序,獲取並返回着色器中每一個變量的存儲位置。locations = {
a_Position: ..,
u_MvpMatrix: ..,
u_Color: ..
}
複製代碼
triangulation.vertices
寫入緩衝區locations
寫入數據triangulation.tasks
,按指定的模式、索引範圍進行繪製下文詳細講解每一個步驟的具體實現。
線的剖分能夠分解爲三個部分,一是線段,二是端頭,三是拐角。
轉換coords
爲二維點,並計算每一個線段的單位法向量。由於須要在路徑上進行垂直擴寬,且寬度與線段長度無關,因此法向量取單位長度便可。
// 將座標轉換爲點、線段矢量、線段單位法向量
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++;
}
複製代碼
線段剖分比較簡單,在路徑點座標上加擴寬的法向量便可,需注意鏈接兩個線段的路徑點須要根據兩條線段的法向量,拓展出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));
}
});
複製代碼
端頭只須要在首尾路徑點上進行擴展。端頭支持三種樣式:butt
不須要增長座標點,square
須要擴展出半個正方形,邊長爲線寬,round
須要擴展出半個圓形,直徑爲線寬。 square
端頭剖分須要找到正方形的頂點,只需將線段法向量旋轉90度,便可獲得偏移向量offsetVector
,示意圖以下:
round
端頭剖分須要在圓形弧線上找到等距且密集的點,只需將線段法向量以小角度旋轉n次直到2*PI,便可獲得弧線上的頂點,最終將圓心與頂點以TRIANGLE_FAN的方式繪製便可實現圓形,示意圖以下:
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
};
}
複製代碼
拐角是在除去首尾兩端的路經點上進行擴展。支持三種樣式:bevel
不須要增長座標點(線段剖分後鏈接處天然造成了平角),miter
須要填補線段延長線交匯出的尖角,round
須要填補扇形,直徑爲線寬。 miter
的剖分相對來講比較複雜一點,以下圖所示,並不是是一個菱形,而是兩個以線段法向量爲直角邊的直角三角形拼接而成,計算公式以下:
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
負責創建和初始化着色器,主要分爲三個步驟,一是經過UColorShader()
獲取單一顏色着色器代碼;二是建立並使用程序;三是獲取變量位置。
/**
* 建立並初始化着色器
* @param {WebGLRenderingContext} gl
*/
function initUColorShader(gl) {
// 獲取着色器代碼
var shaders = UColorShader();
// 建立並使用程序
if (!initShaders(gl, shaders.vshader, shaders.fshader)) {
console.error('Failed to intialize shaders.');
return null;
}
// 獲取變量位置
return getLocations();
}
複製代碼
如前文所述,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
};
}
複製代碼
initShaders
這部分是WebGL繪製流程中通用的步驟,不進行過多的解釋,主要有如下7個步驟。
gl.createShader(type)
gl.shaderSource(shader, source)
gl.compileShader(shader)
gl.createProgram()
gl.attachShader(program, shader)
// 注:頂點着色器、片元着色器須要分別分配gl.linkProgram(program)
// 注:將頂點着色器與片元着色器鏈接gl.useProgram(program)
至此,咱們建立好了一個具備三個屬性變量的着色程序,以後咱們須要爲這三個變量賦值,因此須要獲取到這三個變量的存儲位置。a_Position
和u_MvpMatrix
、u_Color
的變量聲明不一樣,獲取存儲位置的方法也相應的不一樣:
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
};
}
複製代碼
由於須要一次性將所有頂點傳入頂點着色器,因此須要initVertexBuffers
負責建立數據緩衝區並寫入數據。
/**
* 建立緩衝區並傳入數據
* @param {WebGLRenderingContext} gl
* @param {Float32Array} vertices
*/
function initVertexBuffers(gl, vertices) {
// 建立緩衝區
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
console.error('Failed to create the buffer object');
return false;
}
// 綁定緩衝區對象:指明其用途
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 寫入數據
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
return true;
}
複製代碼
u_MvpMatrix
和u_Color
變量可直接調用對應類型的方法進行一次傳值,好比:
gl.uniformMatrix4fv(locations.u_MvpMatrix, false, mvpMatrix.elements);
複製代碼
WebGLRenderingContext.uniformMatrix[234]fv(location, transpose, value)
用於給矩陣類型的變量賦值,二、三、4表示矩陣的維度。
a_Position
變量賦值須要從緩衝區中讀取數據,須要調用vertexAttribPointer
方法將緩衝區對象分配給變量a_Position
,並開啓訪問權:
gl.vertexAttribPointer(locations.a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(location.a_Position);
複製代碼
其中3
表示每一個頂點的份量數,a_Position
是一個vec4
變量,這裏讀取三個份量的數據賦值給x、y、z,第4位會自動補1。gl.FLOAT
表示數據格式爲浮點型。false
標明無需將數據歸一化。最後兩個0
表示頂點數據間無間隔,數據無偏移。
三角剖分步驟中生成了繪製任務tasks = [{mode, start, cnt}, ...]
,每一個任務指定了模式(TRIANGLE_STRIP
/TRIANGLE_FAN
/TRIANGLES
)、起始點索引值、繪製點數量,因此遍歷繪製任務並調用drawArrays
進行繪製便可:
tasks.forEach(function(task) {
gl.drawArrays(gl[task.mode], task.start, task.cnt);
});
複製代碼
至此,繪製線的流程就結束了。
利用上文中構造的Shape
類,最終實現了以下的demo,繪製了一條S折線,而且能夠動態改變其顏色、寬度、端頭、拐角樣式,同時經過鍵盤方向鍵控制Camera
,動態改變視圖投影矩陣。