經(jīng)過這一個(gè)月來欲仙欲死的摸索,總算在摸索出了一些入門webGl的門道。關(guān)于webGl的學(xué)習(xí),我建議大家去入手一本《webGl編程指南》和《線性代數(shù)》,里面的內(nèi)容非常詳細(xì),這里也不需要在多說了。還有,編程指南那本書的代碼結(jié)構(gòu)寫得還是值得人吐槽的,所以,遇到問題也請(qǐng)多多善用搜索引擎,或者:https://stackoverflow.com/search?q=你的問題 。
本文不做關(guān)于webgl的任何教程內(nèi)容,本文旨在分享一下我在摸索webgl中的一些姿勢和一些坑,幫助一些初學(xué)者學(xué)習(xí)得更舒服一點(diǎn)。
第一,webGL!==web 3D
我不知道多少人最開始學(xué)習(xí)webGL是把它當(dāng)web 3d方面去學(xué)習(xí)的,至少最開始我以為webGL就是用來繪制3D模型的。
Naive!
webGL是比canvas.getContext('2d')更加底層的圖形繪制接口。而它的工作原理,實(shí)際上就是遍歷每一個(gè)像素點(diǎn),然后給各個(gè)像素點(diǎn)填充顏色,然后才構(gòu)成一幅2d或者3d的圖像。至于你想直接先搞3d方面的東西, 使用three.js比直接擼webGL舒服多了。
而且,如果你愿意,webGL更適合去做圖像處理。
第二,shaders
webGL工作的基本單位是shaders,中文喚作著色器。
而我們親愛的js在這個(gè)環(huán)境里能做的就只有跑跑腿傳傳值,并不能像ctx.stroke()那樣親自上陣。而著色器,完蛋了,根本就是一門新語言,叫glsl。
我們的js,是跑在瀏覽器里的語言。而glsl,它是跑在顯卡里面的,它需要手動(dòng)使用js去調(diào)用WebGL編譯它的方法,然后變成二進(jìn)制包,然后讓瀏覽器把它塞入顯卡里,最后才能夠使用。
所以webGL繪制比js去繪制的好處在于,webGL占用的是顯卡里的資源,并不過多占用內(nèi)存,性能比起canvas2D來,那是不知道高到哪里去了。
開始學(xué)習(xí)shaders語言的時(shí)候,建議跟著編程指南的例子去敲,不過這里有一個(gè)坑,是我學(xué)習(xí)的時(shí)候遇到的。我跟著書里的例子去敲,卻發(fā)現(xiàn)書里的例子無論如何也無法通過編譯,它會(huì)報(bào)一行這樣的錯(cuò):

經(jīng)過谷歌、百度、stackoverflow等多方詢問,最終的解決辦法也非常簡單,在你每一個(gè)著色器程序頭部加上這樣一行:
precision mediump float;這句話的意思是,設(shè)定中等精度為float型。顯卡程序里面有三種精度:
- 高精度
highp - 中等精度
mediump - 低精度
lowp
那這些精度是干嘛用的呢?當(dāng)然是用來精確計(jì)算的。(隔壁連0.1+0.2都算不準(zhǔn)的js醬躲在墻角默默哭泣)。比如說一個(gè)3d模型,它每個(gè)點(diǎn)的位置最好使用highp精度去計(jì)算,這樣定位準(zhǔn)確。而這個(gè)3d模型的貼圖紋理,其實(shí)都是圖片,對(duì)于圖片像素位置的計(jì)算,使用中等精度的mediump就行了。最后的lowp,適合去計(jì)算像素的顏色值。
然后說了這么多,還只是科普一下而已,因?yàn)椴煌O(shè)備對(duì)這三種精度的支持不一致(前端人深有體會(huì),萬惡的兼容),對(duì)三種精度的默認(rèn)設(shè)置也不一致,比如某些垃圾的設(shè)備就把mediump這個(gè)級(jí)別設(shè)定為int型整數(shù),這個(gè)計(jì)算精度一下子就下降了。
所以在webGL里面要加上這句,統(tǒng)一設(shè)置mediump的默認(rèn)值。這樣,程序就可以通過編譯了。
第三,shaders,著色器程序glsl的加載
就目前看到的大部分教程來看,加載shaders程序的方法無非以下幾種:
- 寫在html里面,在html里面插入一個(gè)
<script type="text/plain">,然后把glsl寫在里面。而js這邊就需要寫一個(gè)獲取這個(gè)script的innerHTML的方法,讀取到glsl的源碼,再去編譯。
不過這樣有個(gè)缺點(diǎn),當(dāng)你的代碼編輯器,比如vscode,存在html代碼格式優(yōu)化這種功能的時(shí)候,會(huì)傻逼傻逼地將glsl源碼壓縮成一行。。。 - 直接使用字符串拼接,就是
var vShaderSource='precision mediump float;'+
'attribute vec4 a_Position;'+
...
就跟我們使用 jquery拼接html一樣去拼接glsl的源碼。不得不說,很煩。
- ajax加載,這個(gè)就可以舒舒服服把glsl的源碼寫在
.glsl文件里,然后通過ajax加載進(jìn)來。如果你的代碼編輯器可以的話,甚至有.glsl文件的語法高亮,就像vscode安裝了高亮插件之后:

不過呢,作為新時(shí)代的前端人,掌握了webpack工程化開發(fā)習(xí)慣的我們怎么能忍受上面幾種類似jq時(shí)代的寫法呢?
什么?配置babel然后使用es6的字符串模板寫源碼?
Naive!
webpack連css都能讀進(jìn)來,區(qū)區(qū)glsl!?這里我直接是使用了
row-loader這個(gè)加載器去加載.glsl文件,然后既不用考慮ajax的異步同步問題,還能夠保持.glsl文件語法高亮,通過一句var vGlsl=require('./xxx/xxx/xx.glsl')就能夠?qū)⒃创a引入到j(luò)s中,十分方便。
懶得配置webpack的同學(xué),這里我給你寫好了一個(gè)簡單的webpack模板了:

直接去我的github里面clone一下就好了,里面還有一個(gè)我寫的小demo:
https://github.com/Char-Ten/webGl-Webpack-Template
第四 紋理加載的一些小問題
如果你參照《webGL編程指南》的demo去寫添加紋理,如果你是在網(wǎng)上隨便找一張自己的圖片的話,你可能會(huì)發(fā)現(xiàn)紋理渲染不出來,即便你的代碼和例子一摸一樣。它會(huì)報(bào)這樣一行錯(cuò):

這里的解決辦法非常簡單,最簡單的解決辦法,是先檢查你使用的貼圖尺寸。如果長和寬的大小都不是2的n次冪(即錯(cuò)誤信息里面所說的non-power-of-2),那么請(qǐng)用PS等圖像處理軟件把它的長和寬分別處理為2的n次冪,如:1x1 2x2 4x4 8x8 16x16 32x32 64x64 128x128 256x256 512x512.....
一般來說這樣就能夠解決了,然后參考一下stackoverflow一位dalao給的代碼,你可以這樣寫一個(gè)創(chuàng)建紋理的函數(shù):
/**創(chuàng)建紋理貼圖
* @param {WebGLRenderingContext} webgl - 使用webgl的上下文
* @param {Canvas||Image} image - 要作為紋理的圖片對(duì)象
* @return {WebglTexture} texture對(duì)象
*/
function createTexByImage(webgl, image) {
var texture = webgl.createTexture();
webgl.bindTexture(webgl.TEXTURE_2D, texture);
webgl.texImage2D(webgl.TEXTURE_2D, 0, webgl.RGBA, webgl.RGBA, webgl.UNSIGNED_BYTE, image);
if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
return texture
}
webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_MIN_FILTER, webgl.NEAREST);
webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_MAG_FILTER, webgl.NEAREST);
webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_WRAP_S, webgl.CLAMP_TO_EDGE);
webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_WRAP_T, webgl.CLAMP_TO_EDGE);
return texture
}
/**檢查數(shù)字是否為2的指數(shù)
* @param {Number} value - 要檢查的值
* @return {Boolean}
*/
function isPowerOf2(value) {
return !(value & (value - 1));
}
當(dāng)圖片的尺寸不滿足2的指數(shù)的時(shí)候,你要寫滿四個(gè)texParameteri方法。
這個(gè)方法是用來設(shè)定紋理貼圖參數(shù)的,有四個(gè)值可以設(shè)定,分別是
- TEXTURE_MAG_FILTER 設(shè)定圖片放大后像素點(diǎn)的取值方式
- TEXTURE_MIN_FILTER 設(shè)定圖片縮小后像素點(diǎn)的取值方式
- TEXTURE_WRAP_S 設(shè)定圖片橫向平鋪樣式
- TEXTURE_WRAP_T 設(shè)定圖片垂直平鋪樣式
默認(rèn)貼圖在webgl中是平鋪的,只有設(shè)定為不平鋪時(shí)(webgl.CLAMP_TO_EDGE),才能夠渲染出來。
至于這個(gè)原因呢,很簡單,對(duì)于尺寸不是2的指數(shù)的圖片,GPU對(duì)其遍歷是十分消耗性能的。所以,你想做一個(gè)平鋪重復(fù)的紋理,就必須使用符合規(guī)則的圖片。
第五,關(guān)于glsl語言的debug
glsl這門語言不像js那樣有console打印或者瀏覽器斷點(diǎn)調(diào)試那樣方便去調(diào)試一個(gè)程序。換句話說,當(dāng)js以buffer的形式丟一個(gè)值給glsl,你沒辦法在glsl里面打印這個(gè)值是否正確。
更何況glsl是靜態(tài)類型語言,有時(shí)候忘記寫類型聲明,或者不同類型的值賦值的時(shí)候就會(huì)報(bào)錯(cuò),甚至你后面少寫了個(gè)分號(hào)都會(huì)報(bào)錯(cuò),都會(huì)導(dǎo)致編譯不通過。
對(duì)于上面兩種情況,首先是打印這個(gè)問題,沒辦法,glsl不能打印的時(shí)候你只能去猜這個(gè)變量到底是個(gè)什么值,然后給每個(gè)像素的顏色RGB設(shè)定為這個(gè)值,然后觀察繪制的結(jié)果,通過顏色去驗(yàn)證數(shù)值正不正確,只是我目前能夠用到的debug方法。。。期待有更好的方法出現(xiàn)。
第二種情況,這個(gè)一方面依賴于自己對(duì)于glsl語言的學(xué)習(xí),同時(shí)你也可以通過你的代碼編輯器去檢查是否有語法錯(cuò)誤,或者,如果你在chrome調(diào)試的話,你可以去下載這些個(gè)chrome插件:

它們可以更好的幫助你檢查程序錯(cuò)誤已經(jīng)其他問題。
這就是我目前學(xué)習(xí)的過程中踩到的一些坑或者解決的一些小問題吧,而且學(xué)了一個(gè)月都還只在2d繪制上搞來搞去,想往3d方向走,需要的數(shù)學(xué)知識(shí)要更多,這些都只是基礎(chǔ)而已。
最后以一張作品圖作為這篇文章的結(jié)尾吧(當(dāng)然glsl的內(nèi)容都是從網(wǎng)絡(luò)上“移植”下來的。。。法線貼圖生成算法移植自某位lua的dalao手筆),今后學(xué)習(xí)如果遇到新坑會(huì)繼續(xù)寫文章回填的。
