webGL入門-四階貝塞爾曲線繪製

隨着現代瀏覽器端技術不短進步,web也逐漸支持了3D繪圖技術。時至今日webGL這項技術已經在去年迎來了2.0版本。現現在數據可視化技術也日益繁雜起來,愈來愈多的數據可視化也逐漸從平面圖表增長了許多新的3D類型圖表,亦或者藉助webGL的力量讓數據渲染能力獲得大幅度提高。javascript

本人也是由於上司的工做安排,開始了webGL的學習之路。在學習的過程當中發現目前網上學習原生webGL的資料少之又少,手頭僅僅握着《webGL編程指南》這本書。java

   

沒錯,就是這本。這本書也的確足夠像我這樣的初心者看了,本篇文章一共涉及三個部分:着色器程序和編譯流程、四階貝塞爾曲線的頂點數據生成函數、緩衝區的使用。後面我會附上源碼,感興趣的同窗能夠去看一下。git

本篇內部份內容若是出現錯誤等,歡迎你們進行指點。github

進入正片:web

首先,webGL這一技術是由JavaScript和glsl着色器語言着兩種語言組成,那麼什麼是glsl着色器語言呢?編程

着色器語言(簡稱shader)是一段運行在顯卡上的程序,在webGL當中有兩種着色器,一個叫作頂點着色器,另外一個叫片元着色器。(來源:《webGL編程指南》)數組

通俗的講,頂點着色器的做用即時描述一個三維物體的物理空間點座標以及與這些座標相關的信息。咱們能夠看到下面的立方體中有八個點(v0-v7)(圖片來源:WebGL入門教程第1篇--六色立方 - CSDN博客瀏覽器

咱們能夠看到這個立方體是有着八個點圍城的面來組成的立方體,因此這些點被稱做「頂點」。性能優化

片元着色器即根據着色器程序控制像素的顏色渲染,片元也能夠理解爲屏幕上顯示的一個像素點。大體可理解爲下圖:(出處忘記了)函數

簡單的介紹過了兩個須要用到的着色器以後,咱們進入下一階段:


着色器程序編譯

webGL的着色器程一般會以字符串的形式提早準備好,咱們一塊兒來看一下這次咱們須要用到的着色器程序代碼:

const VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main(){\n' +
  ' gl_Position = a_Position;\n' +
  '}\n';

const FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform vec4 u_FragColor;\n' +
  'void main() {\n' +
  ' gl_FragColor = u_FragColor;\n' +
  '}\n';
複製代碼

靜態變量VSHADER_SOURCE就是頂點着色器代碼,下面的則是片元着色器代碼。

看代碼外觀上學過C語言的同窗會以爲頗有親切感,可是要注意的是着色器代碼並不是C語言的代碼。

咱們注意看頂點着色器的第一行代碼與片元着色器的第二行代碼,能夠看到他們的語句結構是一致的,由如下三部分組成:

存儲限定符表示後面的變量爲attribute變量,且該變量的數據將從着色器外部傳入。注意:attribute並不是表明着變量存儲類型,而是相似於一種傳輸數據的限定標記,與int、float這樣的聲明存儲空間的類型不一樣。變量a_Position的數據存儲類型是指前方的vec4。

咱們除去剛纔看到的attribute着個存儲限定符外,咱們還看到了另一個存儲限定符——uniform。

那麼咱們應該如何使用這兩個存儲限定符呢,根據《webGL編程指南》一書中的描述總結成一句話即:與頂點數據相關採用 attribute,與頂點數據無關採用uniform

頂點數據實際上就是用來描述每一個頂點的座標值,假設咱們須要畫一條線段,一條線段就須要兩個點來鏈接,那麼咱們的頂點數據便可以向下面那樣表示:

let position = [-1.0, 0.0, 0.0,  1.0, 0.0, 0.0];
               // x1, y1, z1, x2, y2, z2
複製代碼

咱們能夠發現全部的數據均按照必定規律存儲到了數組中,像這樣描述頂點座標值集合的數據就能夠採用attribute這一存儲限定符。

那麼就僅僅只有頂點數據才能夠採用attribute嗎,答案是否認的。

在實際的填坑過程當中,我發現像好比用來描述顏色的數據也能夠用attribute來修飾,目前我發現attribute存儲類型只會出如今頂點着色器中(此處仍待考證,若有錯誤請指正)。

uniform存儲限定符通常用做存儲顏色、各類矩陣(視圖、模型、投影三大矩陣,我我的簡稱MVP。。→_→)或其餘類型數據。

好了,廢了這麼多口舌,接下來終於進入到編譯階段,在這個階段一共分爲如下幾個步驟:

  1. 建立着色器對象
  2. 向着色器對象傳入着色器源碼
  3. 編譯源碼
  4. 建立着色器程序
  5. 爲着色器程序分配着色器對象
  6. 鏈接程序

接下來咱們一塊兒來看一下着色器程序源碼編譯流程這一部分的代碼:

// 分別建立頂點着色器對象與片元着色器對象
  let shader_V = gl.createShader(gl.VERTEX_SHADER);
  let shader_F = gl.createShader(gl.FRAGMENT_SHADER);

  // 向相應的着色器對象傳入字符串源碼
  gl.shaderSource(shader_V, VSHADER_SOURCE);
  gl.shaderSource(shader_F, FSHADER_SOURCE);

  // 編譯頂點着色器源碼
  gl.compileShader(shader_V);
  let isCompiled_V = gl.getShaderParameter(shader_V, gl.COMPILE_STATUS);
  if (!isCompiled_V) {
    throw new Error('compile Shader is failed');
  }

  // 編譯片元着色器源碼
  gl.compileShader(shader_F);
  let isCompiled_F = gl.getShaderParameter(shader_F, gl.COMPILE_STATUS);
  if (!isCompiled_F) {
    throw new Error('compile Shader is failed');
  }

  // 建立着色器程序
  let program = gl.createProgram();
  // 將着色器對象分配給着色器程序
  gl.attachShader(program, shader_V);
  gl.attachShader(program, shader_F);
  // 鏈接着色器程序
  gl.linkProgram(program);
  let isLinked = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (!isLinked) {
    throw new Error('link Shader is failed');
  }

  // 啓用指定的着色器程序
  gl.useProgram(program);
複製代碼

細心的同窗會發現,我最後一行代碼並無在剛纔的流程上出現。這是由於在多物體渲染中極可能存在使用不一樣的着色器程序代碼這種狀況,也所以會根據狀況產生多個着色器程序,而且在調用渲染函數時,webGL會以當前啓用的着色器程序爲基準去渲染,所以着色器程序須要在各類狀況下去切換,因此他不能被算做編譯流程當中。

獲取存儲限定符地址

編譯過着色器程序以後,咱們將兩個存儲限定符相關的變量地址獲取到,爲咱們下一步工做作準備。

// 獲取存儲限定符類型變量地址
  let a_Position = gl.getAttribLocation(program, 'a_Position');
  let u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
複製代碼

咱們能夠看到attribute、uniform這兩種存儲限定符變量獲取地址的API稍有不一樣,所以在使用的時候須要注意區分。

貝塞爾曲線公式

接下來咱們須要開始準備各類繪製圖形所需的頂點數據了。切合這次主題,咱們須要用到貝塞爾曲線公式來計算出頂點,從而生成貝塞爾曲線。

/** * 生成四階貝塞爾曲線定點數據 * @param p0 起始點 { x : number, y : number, z : number } * @param p1 控制點1 { x : number, y : number, z : number } * @param p2 控制點2 { x : number, y : number, z : number } * @param p3 終止點 { x : number, y : number, z : number } * @param num 線條精度 * @param tick 繪製係數 * @returns {{points: Array, num: number}} */
function create3DBezier(p0, p1, p2, p3, num, tick) {
  let pointMum = num || 100;
  let _tick = tick || 1.0;
  let t = _tick / (pointMum - 1);
  let points = [];
  for (let i = 0; i < pointMum; i++) {
    let point = getBezierNowPoint(p0, p1, p2, p3, i, t);
    points.push(point.x);
    points.push(point.y);
    points.push(point.z);
  }

  return points;
}

/** * 四階貝塞爾曲線公式 * @param p0 * @param p1 * @param p2 * @param p3 * @param t * @returns {*} * @constructor */
function Bezier(p0, p1, p2, p3, t) {
  let P0, P1, P2, P3;
  P0 = p0 * (Math.pow((1 - t), 3));
  P1 = 3 * p1 * t * (Math.pow((1 - t), 2));
  P2 = 3 * p2 * Math.pow(t, 2) * (1 - t);
  P3 = p3 * Math.pow(t, 3);

  return P0 + P1 + P2 + P3;
}

/** * 獲取四階貝塞爾曲線中指定位置的點座標 * @param p0 * @param p1 * @param p2 * @param p3 * @param num * @param tick * @returns {{x, y, z}} */
function getBezierNowPoint(p0, p1, p2, p3, num, tick) {
  return {
    x : Bezier(p0.x, p1.x, p2.x, p3.x, num * tick),
    y : Bezier(p0.y, p1.y, p2.y, p3.y, num * tick),
    z : Bezier(p0.z, p1.z, p2.z, p3.z, num * tick),
  }
}
複製代碼

若是咱們只須要獲取整條貝塞爾曲線上全部的頂點數據集,那麼咱們就須要調用create3DBezier()函數並填入指定參數便可。關於四階貝塞爾曲線公式等數學知識請自行百度,本人也僅是照着公式將函數敲出來了→_→。

// 傳入頂點數據
  let bezierPoint = create3DBezier(
    { x : -0.7,  y : 0,   z : 0 },    // p0
    { x : -0.25, y : 0.5, z : 0 },    // p1
    { x : 0.25,  y : 0.5, z : 0 },    // p2
    { x : 0.7,   y : 0,   z : 0 },    // p3
    20,
    1.0
  );
複製代碼

經過該函數,咱們獲得了頂點數據的集合,格式與上面咱們舉例子繪製線條的格式是同樣的,只是數據量上有差別。

頂點數據類型

獲得數據以後咱們並無完成對數據的處理,由於接下來咱們須要將數據類型進行轉換。那麼轉換的方式很是簡單:

let points = new Float32Array(bezierPoint);
複製代碼

咱們能夠看到直接將Array類型的數組變成了Float32Array類型了。那麼有的同窗就會問,爲何須要這麼作?這麼作的好處是什麼?

首先咱們把第一個問題先放一下,由於會在後面講到緩衝區操做相關時我會爲你們介紹,那麼咱們來看看用類型化數組的好處是什麼呢?

關於Array類型數組對於JavaScript功底比較好的同窗可能會知道,JavaScript的數組(Array)嚴格上來說還不能被稱爲是「數組」,由於他僅僅是一個相似於對象的存在,而且在內存上的存儲方式上與類型化數組有大相徑庭的方式。

從上圖中咱們能夠看出來,通常的Array類型數組在內存當中並非一串連續的內存空間地址,但Float32Array則是連續的,所以二者在數組存取上的速度確定有着明顯的區別。同時在數據進行大量生成時Array.push()這種方式雖然十分便利,但重複的分配空間操做會帶來很大的性能影響,而使用類型化數組雖然便利度比Array稍微遜色一點,但帶來的性能優化仍是很可觀的。曾經公司內部將6W條數據進行頂點數據的生成,裏面用到了concat、push等操做,就會發現性能問題至關明顯。

好了,解釋完了其中一個問題事後,咱們帶着一個未解決的問題進入下一個階段:

緩衝區操做

同窗們首先會好奇,這個緩衝區的做用究竟是什麼?不用他是否能夠繪製圖形?習慣性先回答第二個問題。。硬要較真的話不用緩衝區固然是能夠的。但不用緩衝區你也沒辦法繪製出絢麗的webGL圖形→_→

那麼回過頭來看,緩衝區的做用到底是什麼呢?藉助緩衝區咱們能夠一次性將大量頂點數據傳入到glsl着色器當中。這部份內容我建議你們去閱讀一下《webGL編程指南》一書,根據書的章節一點點了解,會更加貼切的理解爲何須要緩衝區。

那麼接下來讓咱們來看看緩衝區部分的代碼吧:

// 建立緩衝區
  let vertexBuffer = gl.createBuffer();
  // 綁定緩衝區
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  // 向緩衝區寫入數據
  gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
  // 分配緩衝區至指定着色器變量地址
  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
  // 鏈接地址
  gl.enableVertexAttribArray(a_Position);
複製代碼

從上至下咱們大致可分爲五個步驟:

  1. 建立緩衝區
  2. 綁定緩衝區
  3. 向緩衝區寫入數據
  4. 分配緩衝區至指定着色器變量地址
  5. 鏈接至該着色器地址

關於這幾個步驟的緩衝區操做實際上是能夠視狀況調用的,也就是說不必定非要按着順序來,在這裏只是給對webGL感興趣的同窗進行簡單的說明。

而且關於緩衝區的操做也能夠按照狀況能夠進行優化。就在這裏不進行細講了,若是已經有了必定基礎的同窗,能夠嘗試閱讀一下three.js的源碼,在three.js內對緩衝區操做進行了許多的優化。

到了這裏我該對上面殘留的那個類型化數組問題進行解答了。其實類型化數組不止Float32Array一種,類型化數組共分爲如下幾種:

Int8Array
Uint8Array
Int16Array
Uint16Array
Int32Array
Uint32Array
Float32Array
Float64Array
複製代碼

具體選用哪一種類型化數組是要和你的緩衝區數據類型相匹配的,關鍵就在於gl.vertexAttribPointer()這個API身上。那麼咱們來具體看一下這個函數的參數:

根據這個表中給出的信息咱們能夠看到第三個參數就是用來指定緩衝區數據類型的,那麼第三個參數均可以是哪些類型呢?咱們繼續看下面一個表:

這樣你們恐怕就清楚什麼狀況下采用何種類型化數組了吧。

以後咱們把剩下的顏色值也傳入到glsl中吧:

// 傳入顏色
  gl.uniform4fv(u_FragColor, [0.0, 1.0, 1.0, 1.0]);
複製代碼

繪製圖形

到了這裏就即將進入結尾階段了,首先咱們先將畫布做清空處理:

// 設置顏色緩衝區清空顏色
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  // 清空顏色緩衝區
  gl.clear(gl.COLOR_BUFFER_BIT);
複製代碼

清空畫布以後,咱們就開始調用繪製函數吧

// 繪製
  gl.drawArrays(gl.LINE_STRIP, 0, bezierPoint.length / 3);
複製代碼

關於gl.drawArrays()函數,首先第一個參數即指定繪圖類型,第二個參數是指從第幾個頂點開始繪製,第三個參數即繪製圖形須要使用多少個頂點。

那麼他的第一個參數都有哪些呢?咱們繼續看錶吧

以上就是webGL的基本繪圖類型,咱們這次繪製的是一個單條多點的線段,所以咱們採用的是gl.LINE_STRIP。

完成以上代碼後咱們來看看效果吧~

到此咱們的小案例就算完成了~

那麼我也爲你們展現一下目前公司內部運用原生webgl作出來的兩個類似的demo:

demo1

demo2

謝謝你們支持,若有任何問題請在留言處進行批評指正。

貝塞爾曲線demo地址:Axiny/webgl-bezier-demo

相關文章
相關標籤/搜索