经过这一个月来欲仙欲死的摸索,总算在摸索出了一些入门webGl的门道。关于webGl的学习,我建议大家去入手一本《webGl编程指南》和《线性代数》,里面的内容非常详细,这里也不需要在多说了。还有,编程指南那本书的代码结构写得还是值得人吐槽的,所以,遇到问题也请多多善用搜索引擎,或者:https://stackoverflow.com/search?q=你的问题
。
本文不做关于webgl的任何教程内容,本文旨在分享一下我在摸索webgl中的一些姿势和一些坑,帮助一些初学者学习得更舒服一点。
第一,webGL!==web 3D
我不知道多少人最开始学习webGL是把它当web 3d方面去学习的,至少最开始我以为webGL就是用来绘制3D模型的。
Naive!
webGL是比canvas.getContext('2d')
更加底层的图形绘制接口。而它的工作原理,实际上就是遍历每一个像素点,然后给各个像素点填充颜色,然后才构成一幅2d或者3d的图像。至于你想直接先搞3d方面的东西, 使用three.js
比直接撸webGL舒服多了。
而且,如果你愿意,webGL更适合去做图像处理。
第二,shaders
webGL工作的基本单位是shaders,中文唤作着色器。
而我们亲爱的js在这个环境里能做的就只有跑跑腿传传值,并不能像ctx.stroke()
那样亲自上阵。而着色器,完蛋了,根本就是一门新语言,叫glsl
。
我们的js,是跑在浏览器里的语言。而glsl,它是跑在显卡里面的,它需要手动使用js去调用WebGL编译它的方法,然后变成二进制包,然后让浏览器把它塞入显卡里,最后才能够使用。
所以webGL绘制比js去绘制的好处在于,webGL占用的是显卡里的资源,并不过多占用内存,性能比起canvas2D来,那是不知道高到哪里去了。
开始学习shaders语言的时候,建议跟着编程指南的例子去敲,不过这里有一个坑,是我学习的时候遇到的。我跟着书里的例子去敲,却发现书里的例子无论如何也无法通过编译,它会报一行这样的错:
经过谷歌、百度、stackoverflow等多方询问,最终的解决办法也非常简单,在你每一个着色器程序头部加上这样一行:
precision mediump float;
这句话的意思是,设定中等精度为float型。显卡程序里面有三种精度:
- 高精度
highp
- 中等精度
mediump
- 低精度
lowp
那这些精度是干嘛用的呢?当然是用来精确计算的。(隔壁连0.1+0.2都算不准的js酱躲在墙角默默哭泣)。比如说一个3d模型,它每个点的位置最好使用highp精度去计算,这样定位准确。而这个3d模型的贴图纹理,其实都是图片,对于图片像素位置的计算,使用中等精度的mediump就行了。最后的lowp,适合去计算像素的颜色值。
然后说了这么多,还只是科普一下而已,因为不同设备对这三种精度的支持不一致(前端人深有体会,万恶的兼容),对三种精度的默认设置也不一致,比如某些垃圾的设备就把mediump这个级别设定为int型整数,这个计算精度一下子就下降了。
所以在webGL里面要加上这句,统一设置mediump的默认值。这样,程序就可以通过编译了。
第三,shaders,着色器程序glsl的加载
就目前看到的大部分教程来看,加载shaders程序的方法无非以下几种:
- 写在html里面,在html里面插入一个
<script type="text/plain">
,然后把glsl写在里面。而js这边就需要写一个获取这个script的innerHTML的方法,读取到glsl的源码,再去编译。
不过这样有个缺点,当你的代码编辑器,比如vscode,存在html代码格式优化这种功能的时候,会傻逼傻逼地将glsl源码压缩成一行。。。 - 直接使用字符串拼接,就是
var vShaderSource='precision mediump float;'+
'attribute vec4 a_Position;'+
...
就跟我们使用 jquery拼接html一样去拼接glsl的源码。不得不说,很烦。
- ajax加载,这个就可以舒舒服服把glsl的源码写在
.glsl
文件里,然后通过ajax加载进来。如果你的代码编辑器可以的话,甚至有.glsl
文件的语法高亮,就像vscode安装了高亮插件之后:
不过呢,作为新时代的前端人,掌握了webpack工程化开发习惯的我们怎么能忍受上面几种类似jq时代的写法呢?
什么?配置babel然后使用es6的字符串模板写源码?
Naive!
webpack连css都能读进来,区区glsl!?这里我直接是使用了
row-loader
这个加载器去加载.glsl
文件,然后既不用考虑ajax的异步同步问题,还能够保持.glsl
文件语法高亮,通过一句var vGlsl=require('./xxx/xxx/xx.glsl')
就能够将源码引入到js中,十分方便。
懒得配置webpack的同学,这里我给你写好了一个简单的webpack模板了:
直接去我的github里面clone一下就好了,里面还有一个我写的小demo:
https://github.com/Char-Ten/webGl-Webpack-Template
第四 纹理加载的一些小问题
如果你参照《webGL编程指南》的demo去写添加纹理,如果你是在网上随便找一张自己的图片的话,你可能会发现纹理渲染不出来,即便你的代码和例子一摸一样。它会报这样一行错:
这里的解决办法非常简单,最简单的解决办法,是先检查你使用的贴图尺寸。如果长和宽的大小都不是2的n次幂(即错误信息里面所说的non-power-of-2
),那么请用PS等图像处理软件把它的长和宽分别处理为2的n次幂,如:1x1 2x2 4x4 8x8 16x16 32x32 64x64 128x128 256x256 512x512.....
一般来说这样就能够解决了,然后参考一下stackoverflow一位dalao给的代码,你可以这样写一个创建纹理的函数:
/**创建纹理贴图
* @param {WebGLRenderingContext} webgl - 使用webgl的上下文
* @param {Canvas||Image} image - 要作为纹理的图片对象
* @return {WebglTexture} texture对象
*/
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
}
/**检查数字是否为2的指数
* @param {Number} value - 要检查的值
* @return {Boolean}
*/
function isPowerOf2(value) {
return !(value & (value - 1));
}
当图片的尺寸不满足2的指数的时候,你要写满四个texParameteri
方法。
这个方法是用来设定纹理贴图参数的,有四个值可以设定,分别是
- TEXTURE_MAG_FILTER 设定图片放大后像素点的取值方式
- TEXTURE_MIN_FILTER 设定图片缩小后像素点的取值方式
- TEXTURE_WRAP_S 设定图片横向平铺样式
- TEXTURE_WRAP_T 设定图片垂直平铺样式
默认贴图在webgl中是平铺的,只有设定为不平铺时(webgl.CLAMP_TO_EDGE
),才能够渲染出来。
至于这个原因呢,很简单,对于尺寸不是2的指数的图片,GPU对其遍历是十分消耗性能的。所以,你想做一个平铺重复的纹理,就必须使用符合规则的图片。
第五,关于glsl语言的debug
glsl这门语言不像js那样有console
打印或者浏览器断点调试那样方便去调试一个程序。换句话说,当js以buffer的形式丢一个值给glsl,你没办法在glsl里面打印这个值是否正确。
更何况glsl是静态类型语言,有时候忘记写类型声明,或者不同类型的值赋值的时候就会报错,甚至你后面少写了个分号都会报错,都会导致编译不通过。
对于上面两种情况,首先是打印这个问题,没办法,glsl不能打印的时候你只能去猜这个变量到底是个什么值,然后给每个像素的颜色RGB设定为这个值,然后观察绘制的结果,通过颜色去验证数值正不正确,只是我目前能够用到的debug方法。。。期待有更好的方法出现。
第二种情况,这个一方面依赖于自己对于glsl语言的学习,同时你也可以通过你的代码编辑器去检查是否有语法错误,或者,如果你在chrome调试的话,你可以去下载这些个chrome插件:
它们可以更好的帮助你检查程序错误已经其他问题。
这就是我目前学习的过程中踩到的一些坑或者解决的一些小问题吧,而且学了一个月都还只在2d绘制上搞来搞去,想往3d方向走,需要的数学知识要更多,这些都只是基础而已。
最后以一张作品图作为这篇文章的结尾吧(当然glsl的内容都是从网络上“移植”下来的。。。法线贴图生成算法移植自某位lua的dalao手笔),今后学习如果遇到新坑会继续写文章回填的。