WebGL基礎教程:第一部分

前端達人發表於2019-01-13

WebGL是一種基於OpenGL的瀏覽器內建3D渲染器,它可以讓你在HTML5頁面中直接顯示3維內容。 在本教程中,我會介紹你使用此框架所需的所有基礎內容。

介紹

開始學習之前,有幾件事你是需要了解的。 WebGL是將3D內容渲染到HTML5的canvas元素上的一種JavaScript API。 它是利用"3D世界"中稱為著色器的兩種指令碼來實現這一點的。 這兩種著色器分別是:

  • 頂點著色器
  • 片元著色器

聽到這些名詞時也不要過於驚慌;它們只不過是"位置計算器"和"顏色選擇器"的另一種說法罷了。 片元著色器容易理解;它只是告訴WebGL,模型上的指點定應該是什麼顏色。 而頂點著色器解釋起來就需要點技術了,不過基本上它起到將3維模型轉變為2維座標的作用。 因為所有的計算機顯示器都是2維平面,當你在螢幕上看3維物體時,它們只不過是透視後的幻象。

如果你想完整地理解這個計算過程,你最好是問一個數學家,因為這個過程中用到了高階的4x4矩陣乘法,實在是有點超過我們這個"基礎"教程的範圍呀。 幸運的是,你並不需要知道它所有的工作原理,因為WebGL會處理背後大部分的細節。 那麼,我們開始吧。

第一步:設定WebGL

WebGL有許多細微的設定,而且每次你要在螢幕畫什麼東西之前都要設定一遍。 為了節約時間,並使程式碼整潔一些,我們把所有"幕後"的程式碼包裝成一個JavaScript物件,並存於一個獨立的檔案中。 現在我們要開始了,先建立一個新檔案'WebGL.js',並寫入如下程式碼:

function WebGL(CID, FSID, VSID){
    var canvas = document.getElementById(CID);
    if(!canvas.getContext("webgl") && !canvas.getContext("experimental-webgl"))
        alert("Your Browser Doesn't Support WebGL");
    else
    {
        this.GL = (canvas.getContext("webgl")) ? canvas.getContext("webgl") : canvas.getContext("experimental-webgl");

        this.GL.clearColor(1.0, 1.0, 1.0, 1.0); // this is the color 
        this.GL.enable(this.GL.DEPTH_TEST); //Enable Depth Testing
        this.GL.depthFunc(this.GL.LEQUAL); //Set Perspective View
        this.AspectRatio = canvas.width / canvas.height;

        //Load Shaders Here
    }
} 複製程式碼

這個建構函式的引數是canvas無形的ID,以及兩個著色器物件。 首先,我們要獲得canvas元素,並確保它是支援WebGL的。 如果支援WebGL,我們就將WebGL上下文賦值給一個區域性變數,稱為"GL"。 清除顏色(clearColor)其實就是設定背景顏色,值得一提的是,WebGL中大部分引數的取值範圍都是0.0到1.0,所以我們需要讓通常的rgb值除以255。 所以,我們的示例中,1.0,1.0,1.0,1.0表示背景為白色,且100%可見 (即無透明)。 接下來兩行要求WebGL計算深度和透視,這樣離你近的物件會擋住離你遠的物件。 最後,我們設定寬高比,即canvas的寬度除以它的高度。

繼續前行之前,我們要準備好兩個著色器。 我把這些著色器寫到HTML檔案裡去,這個HTML檔案裡還包含了我們的畫布元素 (canvas)。 建立一個HTML檔案,並將下面的兩個script元素放在body標籤之前。

<script id="VertexShader" type="x-shader/x-vertex">

    attribute highp vec3 VertexPosition;
    attribute highp vec2 TextureCoord;


    uniform highp mat4 TransformationMatrix;
    uniform highp mat4 PerspectiveMatrix;

    varying highp vec2 vTextureCoord;

    void main(void) {
    gl_Position = PerspectiveMatrix * TransformationMatrix * vec4(VertexPosition, 1.0);
    vTextureCoord = TextureCoord;
}
</script>

<script id="FragmentShader" type="x-shader/x-fragment">
    varying highp vec2 vTextureCoord;

uniform sampler2D uSampler;

void main(void) {
    highp vec4 texelColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
    gl_FragColor = texelColor;
}
</script>複製程式碼

先來看頂點著色器,我們定義了兩個屬性 (attributes):

  • 頂點位置,它儲存了當前頂點 (你的模型上的點) 的位置,包括x,y,z座標。
  • 紋理座標,即賦給這個點的紋理在紋理影象中的位置

接下來,我們建立變換和透視矩陣等變數。 它們被用於將3D模型轉化為2D影象。 下一行是建立一個與片元著色器共享的變數vTextureCoord,在主函式中,我們計算gl_Position (即最終的2D位置)。 然後,我們將'當前紋理座標'賦給這個共享變數vTextureCoord。

在片元著色器中,我們取出定義在頂點著色器中的這個座標,然後用這個座標來對紋理進行'取樣'。 基本上,通過這個過程,我們得到了我們幾何體上的當前點處的紋理的顏色。

WebGL基礎教程:第一部分

現在寫完了著色器,我們可回過頭去在JS檔案中載入這些著色器。 將"//Load Shaders Here"換成如下程式碼:

var FShader = document.getElementById(FSID);
var VShader = document.getElementById(VSID);

if(!FShader || !VShader)
    alert("Error, Could Not Find Shaders");
else
{
    //Load and Compile Fragment Shader
    var Code = LoadShader(FShader);
    FShader = this.GL.createShader(this.GL.FRAGMENT_SHADER);
    this.GL.shaderSource(FShader, Code);
    this.GL.compileShader(FShader);

    //Load and Compile Vertex Shader
    Code = LoadShader(VShader);
    VShader = this.GL.createShader(this.GL.VERTEX_SHADER);
    this.GL.shaderSource(VShader, Code);
    this.GL.compileShader(VShader);

    //Create The Shader Program
    this.ShaderProgram = this.GL.createProgram();
    this.GL.attachShader(this.ShaderProgram, FShader);
    this.GL.attachShader(this.ShaderProgram, VShader);
    this.GL.linkProgram(this.ShaderProgram);
    this.GL.useProgram(this.ShaderProgram);

    //Link Vertex Position Attribute from Shader
    this.VertexPosition = this.GL.getAttribLocation(this.ShaderProgram, "VertexPosition");
    this.GL.enableVertexAttribArray(this.VertexPosition);

    //Link Texture Coordinate Attribute from Shader
    this.VertexTexture = this.GL.getAttribLocation(this.ShaderProgram, "TextureCoord");
    this.GL.enableVertexAttribArray(this.VertexTexture);
}複製程式碼

你的紋理必須是偶數字節大小,否則會出錯。。。比如2x2,4x4,16x16,32x32。。。

首先,我們要確保這些著色器是存在的,然後,我們逐一地載入它們。 這個過程基本上是:得到著色器原始碼,編譯,附著到核心的著色程式上。 從HTML檔案中提取著色器原始碼的程式碼,封裝到了一個函式中,稱為LoadShader;稍後會講到。 我們使用這個'著色器程式'將兩個著色器連結起來,通過它,我們可以訪問到著色器中的變數。 我們將資料儲存到定義在著色器中的屬性;然後,我們就可以將幾何體輸入到著色器中了。

現在,讓我們看一下LoadShader函式,你應該將它置於WebGL函式之外。

function LoadShader(Script){
    var Code = "";
    var CurrentChild = Script.firstChild;
    while(CurrentChild)
    {
        if(CurrentChild.nodeType == CurrentChild.TEXT_NODE)
            Code += CurrentChild.textContent;
        CurrentChild = CurrentChild.nextSibling;
    }
    return Code;
}複製程式碼

基本上,這個函式通過遍歷著色器來收集原始碼。

第二步:“簡單”立方體

為了在WebGL中畫出物件,你需要如下三個陣列:

  • 頂點 (vertices):構造你的物件的那些點
  • 三角形 (triangles):告訴WebGL如何將頂點連線成面
  • 紋理座標 (texture coordinates):定義頂點如何被對映到紋理影象上

這個過程稱為UV對映。 我們的例子是構造一個簡單的立方體。 我將這個立方體分成4個頂點一組,每一組又連成兩個三角形。 我們可以用一個變數來儲存立方體的這些陣列。

var Cube = {
    Vertices : [ // X, Y, Z Coordinates

        //Front

        1.0,  1.0,  -1.0,
        1.0, -1.0,  -1.0,
        -1.0,  1.0,  -1.0,
        -1.0, -1.0,  -1.0,

        //Back

        1.0,  1.0,  1.0,
        1.0, -1.0,  1.0,
        -1.0,  1.0,  1.0,
        -1.0, -1.0,  1.0,

        //Right

        1.0,  1.0,  1.0,
        1.0, -1.0,  1.0,
        1.0,  1.0, -1.0,
        1.0, -1.0, -1.0,

        //Left

        -1.0,  1.0,  1.0,
        -1.0, -1.0,  1.0,
        -1.0,  1.0, -1.0,
        -1.0, -1.0, -1.0,

        //Top

        1.0,  1.0,  1.0,
        -1.0, -1.0,  1.0,
        1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0,

        //Bottom

        1.0, -1.0,  1.0,
        -1.0, -1.0,  1.0,
        1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0

    ],
    Triangles : [ // Also in groups of threes to define the three points of each triangle
        //The numbers here are the index numbers in the vertex array

        //Front

        0, 1, 2,
        1, 2, 3,

        //Back

        4, 5, 6,
        5, 6, 7,

        //Right

        8, 9, 10,
        9, 10, 11,

        //Left

        12, 13, 14,
        13, 14, 15,

        //Top

        16, 17, 18,
        17, 18, 19,

        //Bottom

        20, 21, 22,
        21, 22, 23

    ],
    Texture : [ //This array is in groups of two, the x and y coordinates (a.k.a U,V) in the texture
        //The numbers go from 0.0 to 1.0, One pair for each vertex

        //Front

        1.0, 1.0,
        1.0, 0.0,
        0.0, 1.0,
        0.0, 0.0,


        //Back

        0.0, 1.0,
        0.0, 0.0,
        1.0, 1.0,
        1.0, 0.0,

        //Right

        1.0, 1.0,
        1.0, 0.0,
        0.0, 1.0,
        0.0, 0.0,

        //Left

        0.0, 1.0,
        0.0, 0.0,
        1.0, 1.0,
        1.0, 0.0,

        //Top

        1.0, 0.0,
        1.0, 1.0,
        0.0, 0.0,
        0.0, 1.0,

        //Bottom

        0.0, 0.0,
        0.0, 1.0,
        1.0, 0.0,
        1.0, 1.0
    ]
};複製程式碼

這樣一個簡單的立方體用到的資料似乎有點過多,不過,在我們教程的第二部分中,我們寫一個匯入3D模型的指令碼,所以你現在不必計較這些。

你可能還在想,為什麼需要24個頂點 (每一面4個) 呢,實際上只有8個呀? 我這樣做是因為,你可以只用為每個頂點指定一個紋理座標;而如果你用8個頂點,則整個立方體將看起來一樣,因為它會將一個紋理值傳播到頂點接觸的所有面上。 通過我們的方式,每個面都有它獨有的點,所以我們可以在每一面上指定不同的紋理區域。

現在,我們有了這樣一個立方體變數 cube,然後,我們可以準備畫它了。 我們還是回到WebGL方法中,並新增一個Draw函式。

第三步:Draw函式

WebGL中繪製物件的過程有許多步驟;所以最好是將每個步驟寫成函式,來簡化這個過程的程式碼。 基本的想法是將三個陣列載入到WebGL的快取中去。 然後,我們將這些快取連線到著色器中定義的屬性,以及變換和透視矩陣。 接下來,我們需要將紋理載入到記憶體中,並且最後呼叫draw命令。 那麼,我們開始吧。

接下來的程式碼進入到WebGL函式中:

this.Draw = function(Object, Texture)
{
    var VertexBuffer = this.GL.createBuffer(); //Create a New Buffer

    //Bind it as The Current Buffer
    this.GL.bindBuffer(this.GL.ARRAY_BUFFER, VertexBuffer);

    // Fill it With the Data
    this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Object.Vertices), this.GL.STATIC_DRAW);

    //Connect Buffer To Shader's attribute
    this.GL.vertexAttribPointer(this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0);

    //Repeat For The next Two
    var TextureBuffer = this.GL.createBuffer();
    this.GL.bindBuffer(this.GL.ARRAY_BUFFER, TextureBuffer);
    this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Object.Texture), this.GL.STATIC_DRAW);
    this.GL.vertexAttribPointer(this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0);
    var TriangleBuffer = this.GL.createBuffer();
    this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, TriangleBuffer);
    //Generate The Perspective Matrix
    var PerspectiveMatrix = MakePerspective(45, this.AspectRatio, 1, 10000.0);

    var TransformMatrix = MakeTransform(Object);

    //Set slot 0 as the active Texture
    this.GL.activeTexture(this.GL.TEXTURE0);

    //Load in the Texture To Memory
    this.GL.bindTexture(this.GL.TEXTURE_2D, Texture);

    //Update The Texture Sampler in the fragment shader to use slot 0
    this.GL.uniform1i(this.GL.getUniformLocation(this.ShaderProgram, "uSampler"), 0);

    //Set The Perspective and Transformation Matrices
    var pmatrix = this.GL.getUniformLocation(this.ShaderProgram, "PerspectiveMatrix");
    this.GL.uniformMatrix4fv(pmatrix, false, new Float32Array(PerspectiveMatrix));

    var tmatrix = this.GL.getUniformLocation(this.ShaderProgram, "TransformationMatrix");
    this.GL.uniformMatrix4fv(tmatrix, false, new Float32Array(TransformMatrix));

    //Draw The Triangles
    this.GL.drawElements(this.GL.TRIANGLES, Object.Trinagles.length, this.GL.UNSIGNED_SHORT, 0);
};複製程式碼
WebGL基礎教程:第一部分

頂點著色器對你的物件進行放置,旋轉和縮放時,依據的都是變換和透視矩陣。 在本教程第二部分中,我們會更深入地介紹變換。

我已經新增了兩個函式:MakePerspective()和MakeTransform()。 它們只不過生成了WebGL所需的4x4矩陣。 MakePerspective()函式接受幾個引數:視場豎直高度,寬高比,最近和最遠點。 任何比1個單位近或比10000個單位遠的物件都不會被顯示,但是你可以調整這些值,以得到你所期望的效果。 現在,讓我們看一看這兩個函式:

function MakePerspective(FOV, AspectRatio, Closest, Farest){
    var YLimit = Closest * Math.tan(FOV * Math.PI / 360);
    var A = -( Farest + Closest ) / ( Farest - Closest );
    var B = -2 * Farest * Closest / ( Farest - Closest );
    var C = (2 * Closest) / ( (YLimit * AspectRatio) * 2 );
    var D = (2 * Closest) / ( YLimit * 2 );
    return [
        C, 0, 0, 0,
        0, D, 0, 0,
        0, 0, A, -1,
        0, 0, B, 0
    ];
}
function MakeTransform(Object){
    return [
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, -6, 1
    ];
}複製程式碼

這些矩陣都會影響到你的物件的最終視覺效果,但透視矩陣影響的是你的“3維世界”,比如視場和可見物件,而變換矩陣影響的是單個物件,比如它們的旋轉和位置。 完成這些之後,我們幾何可以開始畫了,剩下的工作只是將一個影象轉變為一個WebGL紋理。

第四步:載入紋理

載入一個紋理分兩步。 首先,我們要用JavaScript的標準做法來載入一幅影象,然後,我們將其轉化為一個WebGL紋理。 所以,我們先從第二步開始吧,畢竟我們正在討論的是JS檔案。 將下面的程式碼加到WebGL函式的底部,恰好在Draw命令之後。

this.LoadTexture = function(Img){
    //Create a new Texture and Assign it as the active one
    var TempTex = this.GL.createTexture();
    this.GL.bindTexture(this.GL.TEXTURE_2D, TempTex);

    //Flip Positive Y (Optional)
    this.GL.pixelStorei(this.GL.UNPACK_FLIP_Y_WEBGL, true);

    //Load in The Image
    this.GL.texImage2D(this.GL.TEXTURE_2D, 0, this.GL.RGBA, this.GL.RGBA, this.GL.UNSIGNED_BYTE, Img);

    //Setup Scaling properties
    this.GL.texParameteri(this.GL.TEXTURE_2D, this.GL.TEXTURE_MAG_FILTER, this.GL.LINEAR);
    this.GL.texParameteri(this.GL.TEXTURE_2D, this.GL.TEXTURE_MIN_FILTER, this.GL.LINEAR_MIPMAP_NEAREST);
    this.GL.generateMipmap(this.GL.TEXTURE_2D);

    //Unbind the texture and return it.
    this.GL.bindTexture(this.GL.TEXTURE_2D, null);
    return TempTex;
};複製程式碼

值得一提的是,你的紋理大小必須是偶數字節,否則你會得到錯誤資訊;比如它們可能的維度包括:2x2,4x4,16x16,32x32,等等。 我另加了一行來翻轉Y座標,只是因為我的3D應用的Y座標是朝後的,但是否這樣做完全取決於你。 這是因為一些程式取Y的零點為左上角,而其它則為左下角。 我設定的這些縮放性質只是告訴WebGL,影象應該如何向上取樣和向下取樣。 你可以使用其它的選項來得到不同的效果,不過我認為這個組合效果最佳。

現在,我們完成了JS檔案,我們可以回到HTML檔案,來完成最後一步了。

第五步:合起來

如前所述,WebGL是在canvas元素上畫畫。 因此,在body部分裡,我們所需要的就只是一個canvas畫布。 在新增canvas元素之後,你的html頁面看起來像下面這樣:

<html>
<head>
    <!-- Include Our WebGL JS file -->
    <script src="WebGL.js" type="text/javascript"></script>
    <script>

    </script>
</head>
<body onload="Ready()">
<canvas id="GLCanvas" width="720" height="480">
    Your Browser Doesn't Support HTML5's Canvas.
</canvas>

<!-- Your Vertex Shader -->

<!-- Your Fragment Shader -->

</body>
</html>複製程式碼

這個頁面相當簡單。 在head區域,我連結了JS檔案。 現在,讓我們實現Ready函式,它在頁面載入時呼叫。

//This will hold our WebGL variable
var GL;

//Our finished texture
var Texture;

//This will hold the textures image 
var TextureImage;

function Ready(){
    GL = new WebGL("GLCanvas", "FragmentShader", "VertexShader");
    TextureImage = new Image();
    TextureImage.onload = function(){
        Texture = GL.LoadTexture(TextureImage);
        GL.Draw(Cube, Texture);
    };
    TextureImage.src = "Texture.png";
}複製程式碼

所以,我們建立一個新的WebGL物件,並將canvas和著色器的ID傳遞進去。 接下來,我們載入紋理影象。 一旦載入完成,我們對立方體Cube和紋理Texture呼叫Draw()方法。 如果你一路跟下來,你的螢幕上應該有一個覆蓋有紋理的靜止立方體。

雖然我說了下一次再講變換,但我們不可能只丟給你一個靜止矩形,這還不夠三維。 讓我們回過頭去,再新增一個小小的旋轉吧。 在HTML檔案中,修改onload函式,使之如下面的程式碼:

TextureImage.onload = function(){
    Texture = GL.LoadTexture(TextureImage);
    setInterval(Update, 33);
};複製程式碼

這會使得每隔33毫秒呼叫一個稱為Update()的函式,因而我們得到約30fps的幀率。 下面是這個更新函式:

function Update(){
    GL.GL.clear(16384 | 256);
    GL.Draw(GL.Cube, Texture);
}複製程式碼

這個函式相當簡單;它只不過清除螢幕,然後繪製更新後的立方體。 現在,讓我們進入JS檔案,新增旋轉程式碼。

第六步:新增一些旋轉

我們不會完全實現變換的程式碼,因為我說了要等到下次現說,這次我們只是加一個繞Y軸的旋轉。 要做的第一件事就是在Cube物件中加一個Rotation變數。 它會跟蹤當前的角度,並讓我們可以遞增地保持旋轉。 所以你的Cube變數的頂部程式碼應該如下面這樣:

var Cube = {
    Rotation : 0,
    //The Other Three Arrays
};複製程式碼

現在,讓我們修改MakeTransform()函式,新增旋轉功能:

function MakeTransform(Object){
    var y = Object.Rotation * (Math.PI / 180.0);
    var A = Math.cos(y);
    var B = -1 * Math.sin(y);
    var C = Math.sin(y);
    var D = Math.cos(y);
    Object.Rotation += .3;
    return [
        A, 0, B, 0,
        0, 1, 0, 0,
        C, 0, D, 0,
        0, 0, -6, 1
    ];
}複製程式碼

WebGL基礎教程:第一部分

更多精彩內容,請微信關注”前端達人”公眾號!

WebGL基礎教程:第一部分

原文連結:https://code.tutsplus.com/zh-hans/articles/webgl-essentials-part-i--net-25856

原文作者:Gabriel Manricks


相關文章