WebGL繪製基本圖形--線

前言

地圖的渲染其實能夠分解爲線、面、紋理、文字的渲染。爲了瞭解地圖渲染的實現原理並實際練習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

三角剖分
線段自己的三角剖分是很簡單的,即矩形剖分爲兩個三角形。可是折線有拐角(lineJoin)和端頭(lineCap),且須要支持不一樣的樣式,這部分的剖分會稍微複雜一點,後文會詳細分析。

WebGL的drawArrays方法支持多種模式進行多個三角形的繪製,以下所示: 函數

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 設置樣式

另外還封裝了WebglColorMatrix4Vector2,最終使用示例以下:

/**
 * 建立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);
  });
}
複製代碼

如代碼所示:

  1. 三角剖分:不一樣圖形的剖分過程不一樣,最終返回剖分後的頂點數組、繪製任務。每一個繪製任務指明瞭頂點索引範圍及繪製模式。
triangulation = {
  vertices: [x0, y0, z, x1, y1, z, ...]
  tasks: [task0, task1, ...]
}
複製代碼
  1. 建立並初始化着色器,獲取變量存儲位置: initUColorShader建立一個單一顏色的着色器,而後建立、使用程序,獲取並返回着色器中每一個變量的存儲位置。
locations = {
  a_Position: ..,
  u_MvpMatrix: ..,
  u_Color: ..
}
複製代碼
  1. 建立緩衝區並傳入數據: 進行緩衝區的建立、綁定等操做,將三角剖分後獲得的頂點數組triangulation.vertices寫入緩衝區
  2. 變量賦值: 爲着色器中的變量賦值,向存儲位置locations寫入數據
  3. 執行繪製任務: 遍歷triangulation.tasks,按指定的模式、索引範圍進行繪製

下文詳細講解每一個步驟的具體實現。

三角剖分

線的剖分能夠分解爲三個部分,一是線段,二是端頭,三是拐角。

1. 準備工做

轉換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++;
}
複製代碼

準備工做

2. 線段剖分

線段剖分比較簡單,在路徑點座標上加擴寬的法向量便可,需注意鏈接兩個線段的路徑點須要根據兩條線段的法向量,拓展出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));
  }
});
複製代碼

basePoints

TRIANGLE_STRIP方式繪製

3. 端頭剖分

端頭只須要在首尾路徑點上進行擴展。端頭支持三種樣式:butt不須要增長座標點,square須要擴展出半個正方形,邊長爲線寬,round須要擴展出半個圓形,直徑爲線寬。 square端頭剖分須要找到正方形的頂點,只需將線段法向量旋轉90度,便可獲得偏移向量offsetVector,示意圖以下:

square端頭
round端頭剖分須要在圓形弧線上找到等距且密集的點,只需將線段法向量以小角度旋轉n次直到2*PI,便可獲得弧線上的頂點,最終將圓心與頂點以TRIANGLE_FAN的方式繪製便可實現圓形,示意圖以下:
round端頭

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. 拐角剖分

拐角是在除去首尾兩端的路經點上進行擴展。支持三種樣式:bevel不須要增長座標點(線段剖分後鏈接處天然造成了平角),miter須要填補線段延長線交匯出的尖角,round須要填補扇形,直徑爲線寬。 miter的剖分相對來講比較複雜一點,以下圖所示,並不是是一個菱形,而是兩個以線段法向量爲直角邊的直角三角形拼接而成,計算公式以下:

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();
}
複製代碼

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. 建立並使用程序

initShaders這部分是WebGL繪製流程中通用的步驟,不進行過多的解釋,主要有如下7個步驟。

  1. 建立着色器對象:gl.createShader(type)
  2. 填充着色器源代碼:gl.shaderSource(shader, source)
  3. 編譯着色器:gl.compileShader(shader)
  4. 建立程序對象:gl.createProgram()
  5. 爲程序對象分配着色器:gl.attachShader(program, shader) // 注:頂點着色器、片元着色器須要分別分配
  6. 鏈接程序對象:gl.linkProgram(program) // 注:將頂點着色器與片元着色器鏈接
  7. 使用程序對象:gl.useProgram(program)

3. 獲取變量位置

至此,咱們建立好了一個具備三個屬性變量的着色程序,以後咱們須要爲這三個變量賦值,因此須要獲取到這三個變量的存儲位置。a_Positionu_MvpMatrixu_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_MvpMatrixu_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);
});
複製代碼

至此,繪製線的流程就結束了。

demo演示

利用上文中構造的Shape類,最終實現了以下的demo,繪製了一條S折線,而且能夠動態改變其顏色、寬度、端頭、拐角樣式,同時經過鍵盤方向鍵控制Camera,動態改變視圖投影矩陣。

webgl繪製基本圖形-線

相關文章
相關標籤/搜索