WebGL學習(1) - 三角形


原文地址:WebGL學習(1) - 三角形
還記得第一次看到canvas的粒子特效的時候,真的把我給驚艷到了,原來在瀏覽器也能做出這么棒的效果。結合《HTML5 Canvas核心技術》和網上的教程,經過半年斷斷續續的學習,對canvas的學習終於完結,對常用的canvas特效基本能做到信手拈來的。canvas特效請看:樣例列表

眾所周知,canvas是2D繪圖技術,雖然可以通過坐標變換,位置計算也能做到3D的效果。但3D場景數據量畢竟比2D要高一個數量級的,純粹用canvas的話,不管是性能和開發的復雜度會成為一個瓶頸。

這也是webGL出現的原因,解決web端3D渲染的場景。webGL會調用到GPU,處理大量重復的3D場景數據時,性能非常有優勢。同時webGL是基於openGL ES 2.0, 因此它處理3D場景是非常成熟的。但為什么不直接學習three.js呢?因為本人對圖形學感興趣,只是希望做一些自己喜歡的效果的同時深入了解計算機圖形學,沒指望通過它做商業項目。

為了讓學習更有動力和目的性,我們以實例為導向學習webGL,再從中展開到需要學習哪些知識點。這次我們來實現如下的動畫,該教程參考了《WebGL編程指南》

實際效果請看:旋轉的三角形

webGL渲染流程

webGL的渲染流程如下,其中第2,3,4步是重點,里面細節比較多。接着我們就按這個流程一步一步解決問題

  1. 獲取webGL繪圖上下文
  2. 初始化着色器
  3. 創建、綁定緩沖區對象
  4. 向頂點着色器和片元着色器寫入數據
  5. 設置canvas背景色,清空canvas
  6. 繪制

webGL繪圖上下文

webGL是canvas基礎之上的3D繪圖技術,只是上下文不同,get3DContext函數作用就是依次降級獲取上下文。

var canvas = document.getElementById("canvas"),
    gl = get3DContext(canvas, true);
function get3DContext(canvas, opt) {
    var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
    var context = null;
    for (var i = 0, len = names.length; i < len; i++) {
        try {
            context = canvas.getContext(names[i], opt);
        } catch (e) {}
        if (context) {
            break;
        }
    }
    return context;
}

着色器

着色器就是嵌入到js中的webGL代碼,是由GLSL語言編寫的,可以把着色器看成是js代碼連接webGL的中間件。頂點着色器和片元着色器分別用於操作頂點和顏色光照,《WebGL編程指南》中是把着色器寫成字符串,但從可維護性考慮,還是寫在script標簽中比較好。GLSL語言與C語言非常像,只要熟悉了GLSL特有的部分,其實還是比較簡單的。

限定符
限定符只能用於全局變量,有3種類型:

  • attribute用於表示頂點信息
  • uniform用於表示除頂點外的其他信息,可以是除結構體和數組之外的任意類型
  • varying用於頂點着色器向片元着色器傳輸數據

GLSL特有的數據類型

  1. 向量:

    vec2, vec3, vec4 : 表示有2,3,4個浮點數的向量
    ivec2, ivec3, ivec4 : 表示有2,3,4個整形的向量
    bvec2, bvec3, bvec4 : 表示有2,3,4個布爾值的向量

  2. 矩陣:
    mat2, mat3, mat4 : 表示有2x2,3x3,4x4的浮點數的矩陣

頂點着色器

<script type="x-shader/x-vertex" id="vs">
attribute vec4 a_Position; //頂點,4個浮點的矢量,attribute變量傳輸與頂點有關的數據,表示逐頂點的信息
uniform mat4 u_xformMatrix; //變換矩陣,4*4浮點矩陣, uniform變量傳輸的是所有頂點都相同的數據
void main() { 
		gl_Position=u_xformMatrix*a_Position;
} 
</script>

片元着色器

<script type="x-shader/x-fragment" id="fs">
precision mediump float; // 精度限定
uniform vec4 u_FragColor;  // 顏色
void main() {
		gl_FragColor = u_FragColor;
}
</script>

接着就是創建着色器了,首先從頁面script標簽取出着色器代碼,初始化着色器;接着創建程序對象,最后連接程序對象。中間的步驟其實非常的啰嗦,已經把這幾個步驟封裝,我們只需要調用createShaders就可以了。

/**
 * 根據script id創建着色器
 * @param  {Object} gl  context
 * @param  {String} vid script id
 * @param  {String} fid script id
 * @return {Boolen}
 */
function createShaders(gl, vid, fid) {
    var vshader, fshader, element, program;
    [vid, fid].forEach(function(id) {
        element = document.getElementById(id);
        if (element) {
            switch (element.type) {
                // 頂點着色器的時候
                case "x-shader/x-vertex":
                    vshader = element.text;
                    break;
                // 片段着色器的時候
                case "x-shader/x-fragment":
                    fshader = element.text;
                    break;
                default:
                    break;
            }
        }
    });
    if (!vshader) {
        console.log("VERTEX_SHADER String not exist");
        return false;
    }
    if (!fshader) {
        console.log("FRAGMENT_SHADER String not exist");
        return false;
    }
    program = createProgram(gl, vshader, fshader);
    if (!program) {
        console.log("Failed to create program");
        return false;
    }

    gl.useProgram(program);
    gl.program = program;
    return true;
}

/**
 * 創建連接程序對象
 * @param  {Object} gl       上下文
 * @param  {String} vshader  頂點着色器代碼
 * @param  {String} fshader  片元着色器代碼
 * @return {Object}
 */
function createProgram(gl, vshader, fshader) {
    // 創建着色器對象
    var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
    var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
    if (!vertexShader || !fragmentShader) {
        return null;
    }

    // 創建程序對象
    var program = gl.createProgram();
    if (!program) {
        return null;
    }

    // 連接着色器對象
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);

    // 連接程序對象
    gl.linkProgram(program);

    // 檢查連接結果
    var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (!linked) {
        var error = gl.getProgramInfoLog(program);
        console.log("Failed to link program: " + error);
        gl.deleteProgram(program);
        gl.deleteShader(fragmentShader);
        gl.deleteShader(vertexShader);
        return null;
    }
    return program;
}

/**
 * 加載着色器
 * @param  {Object} gl     上下文
 * @param  {Object} type   類型
 * @param  {String} source 代碼字符串
 * @return {Object}
 */
function loadShader(gl, type, source) {
    // 創建着色器對象
    var shader = gl.createShader(type);
    if (shader == null) {
        console.log("unable to create shader");
        return null;
    }

    // 設置着色器程序
    gl.shaderSource(shader, source);

    // 編譯着色器
    gl.compileShader(shader);

    // 檢查編譯結果
    var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!compiled) {
        var error = gl.getShaderInfoLog(shader);
        console.log("Failed to compile shader: " + error);
        gl.deleteShader(shader);
        return null;
    }

    return shader;
}

緩沖區

創建好緩沖區對象后,需要把它分配給變量,然后使它生效。注意頂點數組使用的是類型化數組Float32Array,這樣更加高效。vertexAttribPointer方法這里指定了每個頂點分量的個數為2,因為我們目前只定義x,y坐標,z坐標使用系統默認。

/**
 * 創建緩沖區
 * @param  {Array} data
 * @param  {Object} bufferType
 * @return {Object}
 */
function createBuffer(data, bufferType) {
    // 生成緩存對象
    var buffer = gl.createBuffer();
    if (!buffer) {
        console.log("Failed to create the buffer object");
        return null;
    }
    // 綁定緩存(gl.ARRAY_BUFFER<頂點>||gl.ELEMENT_ARRAY_BUFFER<頂點索引>)
    gl.bindBuffer(bufferType || gl.ARRAY_BUFFER, buffer);

    // 向緩存中寫入數據
    gl.bufferData(bufferType || gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

    // 將綁定的緩存設為無效
    // gl.bindBuffer(gl.ARRAY_BUFFER, null);

    // 返回生成的buffer
    return buffer;
}

// 創建緩沖區並傳人頂點
var vertices = new Float32Array([-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5]);
if (!createBuffer(vertices)) return;

// 分配緩沖區對象給a_Position變量
// (地址,每個頂點分量的個數<1-4>,數據類型<整形,符點等>,是否歸一化,指定相鄰兩個頂點間字節數<默認0>,指定緩沖區對象偏移量<默認0>)
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

// 啟動
gl.enableVertexAttribArray(a_Position);

寫入數據

首先要獲取變量的地址,然后再給變量賦值,感覺挺麻煩的。attribute標記的變量使用getAttribLocation獲取,同理uniform標記的變量使用getUniformLocation獲取。

我們的動畫要使圖形繞坐標原點旋轉,那么這就需要用到矩陣的變換,矩陣相關的知識就不詳細說明了。要注意webGL使用的是列主序的矩陣,計算好變換矩陣后,把值賦予變量就ok。

// 獲取 u_FragColor變量的存儲地址並賦值
var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
if (!u_FragColor) return;
//顏色模式為rgba,值范圍0~1
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);

// 繞z軸旋轉
var deg=Math.PI/180*(angle++),
    cos=Math.cos(deg),
    sin=Math.sin(deg);

//  webgl中是按列主序 旋轉加位移
var xformMatrix=new Float32Array([
    cos,sin,0.0,0.0,
    -sin,cos,0.0,0.0,
    0.0,0.0,1.0,0.0,
    0.3,0.0,0.0,1.0
]);

// v表示可以向着色器傳輸多個數值(地址變量,webgl中必須false,矩陣)
gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix);

背景操作

每次執行動畫前進行清屏,和canvas中的設置fillStyle,執行clearRect,效果一樣。

// 設置清屏顏色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 清屏
gl.clear(gl.COLOR_BUFFER_BIT);

繪制

最后渲染圖形,注意第一個參數,指定不同的值,它就渲染為不同的圖形,大家可以用不同的值試試效果。

  • POINTS 點
  • LINES 線段
  • LINE_STRIP 線條
  • LINE_LOOP 回路
  • TRIANGLES 三角形
  • TRIANGLE_STRIP 三角帶
  • TRIANGLE_FAN 三角扇
// (基本圖形,第幾個頂點,執行幾次),修改基本圖形項可以生成點,線,三角形,矩形,扇形等
gl.drawArrays(gl.TRIANGLES, 0, 3);

最后主體代碼如下:

var canvas = document.getElementById("canvas"),
    gl = get3DContext(canvas, true);

function main() {
    if (!gl) {
        console.log("Failed to get the rendering context for WebGL");
        return;
    }

    if (!createShaders(gl, "fs", "vs")) {
        console.log("Failed to intialize shaders.");
        return;
    }

    // 創建緩沖區並傳人頂點
    var vertices = new Float32Array([ -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5 ]);
    if (!createBuffer(vertices)) {
        console.log("Failed to create the buffer object");
        return;
    }

    // 獲取頂點位置
    var a_Position = gl.getAttribLocation(gl.program, "a_Position");
    if (a_Position < 0) {
        console.log("Failed to get the storage location of a_Position");
        return;
    }

    // 分配緩沖區對象給a_Position變量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(a_Position);

    // 獲取 u_FragColor變量的存儲地址並賦值
    var u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor");
    if (!u_FragColor) {
        console.log("Failed to get the storage location of u_FragColor");
        return;
    }
    gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);

    // 獲取矩陣變量
    var u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
    if (!u_xformMatrix) {
        console.log("Failed to get the storage location of u_xformMatrix");
        return;
    }

    var xformMatrix,
        angle = 0;
    // 設置清屏顏色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);

    // 執行動畫
    (function animate() {
        var deg = (Math.PI / 180) * angle++,
            cos = Math.cos(deg),
            sin = Math.sin(deg);

        // 旋轉加位移
        xformMatrix = new Float32Array([ 
            cos, sin, 0.0, 0.0,
            -sin, cos, 0.0, 0.0,
            0.0, 0.0, 1.0, 0.0,
            0.3, 0.0, 0.0, 1.0
        ]);

        // v表示可以向着色器傳輸多個數值(地址變量,webgl中必須false,矩陣)
        gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);

        gl.clear(gl.COLOR_BUFFER_BIT);

        // (基本圖形,第幾個頂點,執行幾次),修改基本圖形項可以生成點,線,三角形,矩形,扇形等
        gl.drawArrays(gl.TRIANGLES, 0, 3);

        requestAnimationFrame(animate);
    })();
}

main();

總結

相比canvas,webGL的api要原始得多,涉及到很多底層的openGL細節,但經過封裝后,我們可以把那部分細節看成一個黑箱。大部分的操作都是基於矩陣變換,盡管有很多方便的第三方矩陣庫,但有牢固的線性代數基礎還是大有裨益的,GLSL編程語言也是一樣需要熟練掌握。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM