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

Webgl的魅力在於可以創造一個自己的3D世界,但相比較canvas2D來說,除了物體的移動旋轉變換完全依賴矩陣增加了複雜度,就連生成一個物體都變得很複雜。

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

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


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

爲什麼說webgl生成物體麻煩

我們先稍微對比下基本圖形的創建代碼
矩形:
canvas2D

Crayon Syntax Highlighter v_2.7.2_beta
ctx1.rect(50, 50, 100, 100);
ctx1.fill();
[Format Time: 0.0015 seconds]

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

Crayon Syntax Highlighter v_2.7.2_beta
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);
[Format Time: 0.0035 seconds]

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

圓:
canvas2D

Crayon Syntax Highlighter v_2.7.2_beta
ctx1.arc(100, 100, 50, 0, Math.PI * 2, false);
ctx1.fill();
[Format Time: 0.0007 seconds]

webgl

Crayon Syntax Highlighter v_2.7.2_beta
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);
[Format Time: 0.0053 seconds]

完整代碼地址: 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去建模。

具體實現

1、首先建模生成obj文件

這裏我們使用blender生成文字

2、讀取分析obj文件

Crayon Syntax Highlighter v_2.7.2_beta
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]); // 加載圖片
    }
}
[Format Time: 0.0162 seconds]

代碼核心的地方都進行了註釋,注意這裏的正則只去匹配我們obj文件中含有的字段,其他信息沒有去匹配,如果有對obj文件所有可能含有的信息完成匹配的同學可以去看下Threejs中objLoad部分源碼

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

Crayon Syntax Highlighter v_2.7.2_beta
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);
    }
};
[Format Time: 0.0141 seconds]

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

4、旋轉平移等變換

物體全部導入進去,剩下來的任務就是進行變換了,首先我們分析一下有哪些動畫效果
因爲我們模擬的是一個宇宙,3D文字就像是星球一樣,有公轉和自轉;還有就是我們導入的obj文件都是基於(0,0,0)點的,所以我們 還需要把它們進行平移操作
先上核心代碼~

Crayon Syntax Highlighter v_2.7.2_beta
......
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
            ]
        )
    )
);
[Format Time: 0.0043 seconds]

一眼望去uMMatrix(模型矩陣)裏面有三個矩陣,爲什麼有三個呢,它們的順序有什麼要求麼?
因爲矩陣不滿足交換率,所以我們矩陣的平移和旋轉的順序十分重要,先平移再旋轉和先旋轉再平移有如下的差異
(下面圖片來源於網絡)
先旋轉後平移:
先平移後旋轉:
從圖中明顯看出來 先旋轉後平移是自轉 ,而 先平移後旋轉是公轉
所以我們矩陣的順序一定是 公轉 * 平移 * 自轉 * 頂點信息(右乘)
具體矩陣爲何這樣寫可見上一篇矩陣入門文章
這樣一個3D文字的8大行星就形成啦

4、裝飾星星

光禿禿的幾個文字肯定不夠,所以我們還需要一點點綴,就用幾個點當作星星,非常簡單
注意 默認渲染webgl.POINTS是方形的 ,所以我們得在fragment shader中加工處理一下

Crayon Syntax Highlighter v_2.7.2_beta
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; // 丟棄
    }
}
[Format Time: 0.0017 seconds]

結語

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

本文就到這裏啦,有問題和建議的小夥伴歡迎留言一起討論~!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章