WebGL基礎教程:第一部分

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

介紹

開始學習以前,有幾件事你是須要了解的。 WebGL是將3D內容渲染到HTML5的canvas元素上的一種JavaScript API。 它是利用"3D世界"中稱爲着色器的兩種腳原本實現這一點的。 這兩種着色器分別是:html

  • 頂點着色器
  • 片元着色器

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

若是你想完整地理解這個計算過程,你最好是問一個數學家,由於這個過程當中用到了高級的4x4矩陣乘法,實在是有點超過咱們這個"基礎"教程的範圍呀。 幸運的是,你並不須要知道它全部的工做原理,由於WebGL會處理背後大部分的細節。 那麼,咱們開始吧。java

第一步:設置WebGL

WebGL有許多細微的設置,並且每次你要在屏幕畫什麼東西以前都要設置一遍。 爲了節約時間,並使代碼整潔一些,咱們把全部"幕後"的代碼包裝成一個JavaScript對象,並存於一個獨立的文件中。 如今咱們要開始了,先建立一個新文件'WebGL.js',並寫入以下代碼:node

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的寬度除以它的高度。web

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

<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。瀏覽器

在片元着色器中,咱們取出定義在頂點着色器中的這個座標,而後用這個座標來對紋理進行'採樣'。 基本上,經過這個過程,咱們獲得了咱們幾何體上的當前點處的紋理的顏色。緩存

如今寫完了着色器,咱們可回過頭去在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); };複製代碼

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

我已經添加了兩個函數: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
    ];
}複製代碼

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

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

原文做者:Gabriel Manricks

相關文章
相關標籤/搜索