教你用webgl快速建立一個小世界

收錄待用,修改轉載已取得騰訊雲受權html


做者:TAT.vorshengit

Webgl的魅力在於能夠創造一個本身的3D世界,但相比較canvas2D來講,除了物體的移動旋轉變換徹底依賴矩陣增長了複雜度,就連生成一個物體都變得很複雜。github

什麼?!爲何不用Threejs?Threejs等庫確實能夠很大程度的提升開發效率,並且各方面封裝的很是棒,可是不推薦初學者直接依賴Threejs,最好是把webgl各方面都學會,再去擁抱Three等相關庫。web

上篇矩陣入門中介紹了矩陣的基本知識,讓你們瞭解到了基本的仿射變換矩陣,能夠對物體進行移動旋轉等變化,而這篇文章將教你們快速生成一個物體,而且結合變換矩陣在物體在你的世界裏動起來。canvas

注:本文適合稍微有點webgl基礎的人同窗,至少知道shader,知道如何畫一個物體在webgl畫布中數組

爲何說webgl生成物體麻煩

咱們先稍微對比下基本圖形的建立代碼網絡

矩形:canvas2D性能

ctx1.rect(50, 50, 100, 100);
ctx1.fill();

webgl(shader和webgl環境代碼忽略)優化

var aPo = [
    -0.5, -0.5, 0,
    0.5, -0.5, 0,
    0.5, 0.5, 0,
    -0.5, 0.5, 0
];

var aIndex = [0, 1, 2, 0, 2, 3];

webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);

webgl.vertexAttrib3f(aColor, 0, 0, 0);

webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);

webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);

完整代碼地址:https://vorshen.github.io/simple-3d-text-universe/rect.html動畫

結果:

圓:canvas2D

ctx1.arc(100, 100, 50, 0, Math.PI * 2, false);
ctx1.fill();

webgl

var angle;
var x, y;
var aPo = [0, 0, 0];
var aIndex = [];
var s = 1;
for(var i = 1; i <= 36; i++) {
    angle = Math.PI * 2 * (i / 36);
    x = Math.cos(angle) * 0.5;
    y = Math.sin(angle) * 0.5;

    aPo.push(x, y, 0);

    aIndex.push(0, s, s+1);

    s++;
}

aIndex[aIndex.length - 1] = 1; // hack一下

webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);

webgl.vertexAttrib3f(aColor, 0, 0, 0);

webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);

webgl.drawElements(webgl.TRIANGLES, aIndex.length, webgl.UNSIGNED_SHORT, 0);

完整代碼地址:https://vorshen.github.io/simple-3d-text-universe/circle.html

結果:

總結:咱們拋開shader中的代碼和webgl初始化環境的代碼,發現webgl比canvas2D就是麻煩不少啊。光是兩種基本圖形就多了這麼多行代碼,抓其根本多的緣由就是由於咱們須要頂點信息。簡單如矩形咱們能夠直接寫出它的頂點,可是複雜一點的圓,咱們還得用數學方式去生成,明顯阻礙了人類文明的進步。
相比較數學方式生成,若是咱們能直接得到頂點信息那應該是最好的,有沒有快捷的方式獲取頂點信息呢?
有,使用建模軟件生成obj文件。

Obj文件簡單來講就是包含一個3D模型信息的文件,這裏信息包含:頂點、紋理、法線以及該3D模型中紋理所使用的貼圖。

下面這個是一個obj文件的地址:

https://vorshen.github.io/simple-3d-text-universe/assets/a1.obj

簡單分析一下這個obj文件

前兩行看到#符號就知道這個是註釋了,該obj文件是用blender導出的。Blender是一款很好用的建模軟件,最主要的它是免費的!

Mtllib(material library)指的是該obj文件所使用的材質庫文件(.mtl)

單純的obj生成的模型是白模的,它只含有紋理座標的信息,但沒有貼圖,有紋理座標也沒用

V 頂點vertex

Vt 貼圖座標點

Vn 頂點法線

Usemtl 使用材質庫文件中具體哪個材質

F是面,後面分別對應 頂點索引 / 紋理座標索引 / 法線索引

這裏大部分也都是咱們很是經常使用的屬性了,還有一些其餘的,這裏就很少說,能夠google搜一下,不少介紹很詳細的文章。

若是有了obj文件,那咱們的工做也就是將obj文件導入,而後讀取內容而且按行解析就能夠了。

先放出最後的結果,一個模擬銀河系的3D文字效果。

在線地址查看:https://vorshen.github.io/simple-3d-text-universe/index.html

在這裏順便說一下,2D文字是能夠經過分析得到3D文字模型數據的,將文字寫到canvas上以後讀取像素,獲取路徑。咱們這裏沒有采用該方法,由於雖然這樣理論上任何2D文字都能轉3D,還能作出相似input輸入文字,3D展現的效果。可是本文是教你們快速搭建一個小世界,因此咱們仍是採用blender去建模。

具體實現

一、首先建模生成obj文件

這裏咱們使用blender生成文字

[][https://vorshen.github.io/simple-3d-text-universe/doc/assets/help.gif](https://vorshen.github.io/simple-3d-text-universe/doc/assets/help.gif))

二、讀取分析obj文件

var regex = { // 這裏正則只去匹配了咱們obj文件中用到數據
    vertex_pattern: /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, // 頂點
    normal_pattern: /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, // 法線
    uv_pattern: /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, // 紋理座標
    face_vertex_uv_normal: /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/, // 面信息
    material_library_pattern: /^mtllib\s+([\d|\w|\.]+)/, // 依賴哪個mtl文件
    material_use_pattern: /^usemtl\s+([\S]+)/
};

function loadFile(src, cb) {
    var xhr = new XMLHttpRequest();

    xhr.open('get', src, false);

    xhr.onreadystatechange = function() {
        if(xhr.readyState === 4) {

            cb(xhr.responseText);
        }
    };

    xhr.send();
}

function handleLine(str) {
    var result = [];
    result = str.split('\n');

    for(var i = 0; i < result.length; i++) {
        if(/^#/.test(result[i]) || !result[i]) { // 註釋部分過濾掉
            result.splice(i, 1);

            i--;
        }
    }

    return result;
}

function handleWord(str, obj) {
    var firstChar = str.charAt(0);
    var secondChar;
    var result;

    if(firstChar === 'v') {

        secondChar = str.charAt(1);

        if(secondChar === ' ' && (result = regex.vertex_pattern.exec(str)) !== null) {
            obj.position.push(+result[1], +result[2], +result[3]); // 加入到3D對象頂點數組
        } else if(secondChar === 'n' && (result = regex.normal_pattern.exec(str)) !== null) {
            obj.normalArr.push(+result[1], +result[2], +result[3]); // 加入到3D對象法線數組
        } else if(secondChar === 't' && (result = regex.uv_pattern.exec(str)) !== null) {
            obj.uvArr.push(+result[1], +result[2]); // 加入到3D對象紋理座標數組
        }

    } else if(firstChar === 'f') {
        if((result = regex.face_vertex_uv_normal.exec(str)) !== null) {
            obj.addFace(result); // 將頂點、發現、紋理座標數組變成面
        }
    } else if((result = regex.material_library_pattern.exec(str)) !== null) {
        obj.loadMtl(result[1]); // 加載mtl文件
    } else if((result = regex.material_use_pattern.exec(str)) !== null) {
        obj.loadImg(result[1]); // 加載圖片
    }
}

代碼核心的地方都進行了註釋,注意這裏的正則只去匹配咱們obj文件中含有的字段,其餘信息沒有去匹配,若是有對obj文件全部可能含有的信息完成匹配的同窗能夠去看下Threejs中objLoad部分源碼

三、將obj中數據真正的運用3D對象中去

Text3d.prototype.addFace = function(data) {
    this.addIndex(+data[1], +data[4], +data[7], +data[10]);
    this.addUv(+data[2], +data[5], +data[8], +data[11]);
    this.addNormal(+data[3], +data[6], +data[9], +data[12]);
};

Text3d.prototype.addIndex = function(a, b, c, d) {
    if(!d) {
        this.index.push(a, b, c);
    } else {
        this.index.push(a, b, c, a, c, d);
    }
};

Text3d.prototype.addNormal = function(a, b, c, d) {
    if(!d) {
        this.normal.push(
            3 * this.normalArr[a], 3 * this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,
            3 * this.normalArr[b], 3 * this.normalArr[b] + 1, 3 * this.normalArr[b] + 2,
            3 * this.normalArr[c], 3 * this.normalArr[c] + 1, 3 * this.normalArr[c] + 2
        );
    } else {
        this.normal.push(
            3 * this.normalArr[a], 3 * this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,
            3 * this.normalArr[b], 3 * this.normalArr[b] + 1, 3 * this.normalArr[b] + 2,
            3 * this.normalArr[c], 3 * this.normalArr[c] + 1, 3 * this.normalArr[c] + 2,
            3 * this.normalArr[a], 3 * this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,
            3 * this.normalArr[c], 3 * this.normalArr[c] + 1, 3 * this.normalArr[c] + 2,
            3 * this.normalArr[d], 3 * this.normalArr[d] + 1, 3 * this.normalArr[d] + 2
        );
    }
};

Text3d.prototype.addUv = function(a, b, c, d) {
    if(!d) {
        this.uv.push(2 * this.uvArr[a], 2 * this.uvArr[a] + 1);
        this.uv.push(2 * this.uvArr[b], 2 * this.uvArr[b] + 1);
        this.uv.push(2 * this.uvArr[c], 2 * this.uvArr[c] + 1);
    } else {
        this.uv.push(2 * this.uvArr[a], 2 * this.uvArr[a] + 1);
        this.uv.push(2 * this.uvArr[b], 2 * this.uvArr[b] + 1);
        this.uv.push(2 * this.uvArr[c], 2 * this.uvArr[c] + 1);
        this.uv.push(2 * this.uvArr[d], 2 * this.uvArr[d] + 1);
    }
};

這裏咱們考慮到兼容obj文件中f(ace)行中4個值的狀況,導出obj文件中能夠強行選擇只有三角面,不過咱們在代碼中兼容一下比較穩妥

四、旋轉平移等變換

物體所有導入進去,剩下來的任務就是進行變換了,首先咱們分析一下有哪些動畫效果

由於咱們模擬的是一個宇宙,3D文字就像是星球同樣,有公轉和自轉;還有就是咱們導入的obj文件都是基於(0,0,0)點的,因此咱們還須要把它們進行平移操做

先上核心代碼

......
this.angle += this.rotate; // 自轉的角度

var s = Math.sin(this.angle);
var c = Math.cos(this.angle);

// 公轉相關數據
var gs = Math.sin(globalTime * this.revolution); // globalTime是全局的時間
var gc = Math.cos(globalTime * this.revolution);

webgl.uniformMatrix4fv(
    this.program.uMMatrix, false, mat4.multiply([
            gc,0,-gs,0,
            0,1,0,0,
            gs,0,gc,0,
            0,0,0,1
        ], mat4.multiply(
            [
                1,0,0,0,
                0,1,0,0,
                0,0,1,0,
                this.x,this.y,this.z,1 // x,y,z是偏移的位置
            ],[
                c,0,-s,0,
                0,1,0,0,
                s,0,c,0,
                0,0,0,1
            ]
        )
    )
);

一眼望去uMMatrix(模型矩陣)裏面有三個矩陣,爲何有三個呢,它們的順序有什麼要求麼?
由於矩陣不知足交換率,因此咱們矩陣的平移和旋轉的順序十分重要,先平移再旋轉和先旋轉再平移有以下的差別

(下面圖片來源於網絡)

先旋轉後平移:

先平移後旋轉:

從圖中明顯看出來先旋轉後平移是自轉,而先平移後旋轉是公轉

因此咱們矩陣的順序必定是 公轉 × 平移 × 自轉 × 頂點信息(右乘)

具體矩陣爲什麼這樣寫可見上一篇矩陣入門文章

這樣一個3D文字的8大行星就造成啦

四、裝飾星星

光禿禿的幾個文字確定不夠,因此咱們還須要一點點綴,就用幾個點看成星星,很是簡單

注意默認渲染webgl.POINTS是方形的,因此咱們得在fragment shader中加工處理一下

precision highp float;

void main() {
    float dist = distance(gl_PointCoord, vec2(0.5, 0.5)); // 計算距離
    if(dist < 0.5) {
        gl_FragColor = vec4(0.9, 0.9, 0.8, pow((1.0 - dist * 2.0), 3.0));
    } else {
        discard; // 丟棄
    }
}

結語

須要關注的是這裏我用了另一對shader,此時就涉及到了關因而用多個program shader仍是在同一個shader中使用if statements,這二者性能如何,有什麼區別,這裏將放在下一篇webgl相關優化中去說。


原文連接:https://www.qcloud.com/community/article/524548

相關文章
相關標籤/搜索