原文首发于简书,今天试了一下掘金的文章编辑器,简直好用!之后文章就都发表在掘金了。。。javascript
自从2017年2月份,写了一个基于canvas2d的字符串动画的玩具以后,就一直想着怎么样把那个玩具性能优化一下。并且那玩意局限性很大,只能渲染纯色单色的字,并且经过每一帧疯狂调用CanvasRenderingContext2d.fillText
方法,致使绘制效率十分低下,很是吃cpu资源,cpu很差的话,很是容易卡顿。
当时还写了一篇文章,《直播敲代码?你可能须要它》来介绍它,有兴趣的朋友能够去翻一下,无论你用什么方式去实现,基本原理都是那样。 正好从去年下半年开始跳进了webGL这个天坑,今天我就用webGL从新实现一下它,把它当成一个练习。这个练习主要针对如下几个内容:html
注意哦,webGL!== 3d。webGL只是个底层的绘制API,我仅仅是使用webGL去绘制2d的内容,全部操做均不依赖其余框架,跟threejs无关,跟babylonjs无关,仅仅是个原生wegGL练习。前端
github page demo,ios12如下不支持getUserMedia
,andorid x5内核存在canvas绘制video画面卡顿的bug(听说是尚不支持webGL视频纹理)。 java
gl.drawArray(gl.POINTS,0,3)
复制代码
只须要在gl.drawArray
方法的第一个参数传入gl.POINTS
常量,就能开启点精灵绘制,片元着色器也只为顶点上色。利用这个特性咱们能够简单绘制马赛克。 相比使用canvas2d的fillRect方法绘制正方形,点精灵能够一次性绘制上千个正方形,并且你能够在片元着色器内,在正方形内部填充不一样的颜色或者图案。
如今开始动手写代码了,首先是编写顶点着色器的代码。顶点着色器的做用很简单,就是肯定点精灵的位置和大小用的,不过,由于webGL里面的坐标系跟咱们日常在网页开发里面的坐标系不同,咱们日常用的什么offsetLeft或者offsetTop,都是相对左上角原点去算的。而wegGL的原点是在图像的中间且y轴是反过来的,所以在顶点着色器里面咱们还要翻转一下坐标,方便后续js的计算。ios
precision mediump float;// 设置浮点精度:中
attribute vec2 a_position;// 点精灵位置
uniform vec2 u_resolution;// canvas的宽高
uniform float u_size; // 点精灵大小
varying vec2 v_position; //将点精灵的位置传递给片元着色器
void main(){
// 从像素坐标转换到 [0.0,1.0]这个区间内
vec2 st = a_position / u_resolution;
// 而后再把[0.0,1.0]映射到[-1.0,1.0]这个区间内,而后y轴翻转
vec2 position = (2.0 * st - 1.0) * vec2(1,-1);
// 把st丢给片元着色器,图像采样要用到
v_position = st;
// 肯定点的大小
gl_PointSize=u_size;
// 肯定点的位置
gl_Position=vec4(position,0.0,1.0);
}
复制代码
着色器实际上就是一个函数,逻辑也不复杂,语法也简单。对于前端来讲,须要注意的是类型问题,还有就是一行代码结尾必定要带分号,否则webGL分分钟给你罢工。git
比较复杂的就是片元着色器了,虽然说它的工做就是肯定像素点的颜色值,可是涉及到两个纹理:视频纹理与文字纹理的处理。github
视频纹理的处理很简单,咱们只须要拿到顶点着色器丢过来的那个st坐标点,得到视频纹理在这个坐标点的颜色就能够了。web
而文字纹理就不同了,由于我是经过将一长串文字用绘制在一个canvas上,而后直接把这个canvas当成纹理丢进片元着色器。所以,在片元着色器里面,咱们须要肯定当前绘制的点精灵要使用这一长串文字中的哪个,而后把这个字裁剪出来。ajax
那么,片元着色器里面究竟要用一长串文字中的哪个?这个咱们能够根据颜色灰度来决定,第一个字表明白色,最后一个字表明黑色,而后中间那些字对应各个阶段的灰度值,这个规则是沿用以前的作法,只不过,以前是使用js来判断使用哪一个字,在这里,咱们将判断权交给webGL,交个片元着色器,让webGL的glsl语言来判断。编程
precision mediump float; // 设置浮点精度:中
uniform sampler2D u_tex1; // 视频纹理(一个video)
uniform sampler2D u_tex2; // 文字纹理(一个canvas)
uniform vec2 u_resolution; // canvas的宽高
uniform float u_len; // 文字的数量
varying vec2 v_position; // 点精灵的坐标
void main(){
// 点精灵对应在视频纹理里面的颜色
vec4 color = texture2D(u_tex1 , v_position);
// 算一下color的灰度,用来决定用哪个字
float gray = (color.r + color.g + color.b)/3.0;
// 算一下,一个字在文字纹理里面有多宽
float s = 1.0/u_len;
// 根据灰度,和字体宽度,算一下咱们要的那个字从文字纹理里面的第几个像素开始
// 由于字数确定是整数,这里须要使用floor函数来丢掉小数部分
// 而后算出是第几个字而后再乘以字体宽度,获得咱们要的字在文字纹理的位置
float p = floor((1.0-gray)/s)*s;
// 从文字纹理拿字
vec4 text_color = texture2D(u_tex2,vec2(
gl_PointCoord.x/u_len + p,
gl_PointCoord.y
));
// 记录一下咱们拿到的文字纹理的alpha通道
float alpha = text_color.a;
// 输出颜色,让有笔画的部分着色,没有笔画的透明
gl_FragColor = vec4(color.rgb,alpha);
}
复制代码
着色器跟C差很少,语法上真的不难。
难的是,你要如何肯定每个像素点的颜色。 包括之后要学习的3D部分,也是一样的道理。
下面基本是教科书式的代码 首先是用webGL建立一个着色器程序对象,为顶点着色器和片元着色器的链接作准备。
var cvs = document.getElementById("cvs");
var gl = cvs.getContext("webgl");
var progarm = gl.createProgram();
复制代码
接着是建立顶点着色器和片元做色器,把上面的着色器源码拿给浏览器去编译。
//建立一个顶点着色器对象
var vShader = gl.createShader(gl.VERTEX_SHADER);
//将顶点着色器的源码怼进去
gl.shaderSource(vShader,`伪装是上面的顶点做色器源码`);
//而后开始编译源码
gl.compileShader(vShader);
//编译完以后,叫上面的着色器程序对象过来收货
gl.attachShader(program,vShader);
//而后建立片元着色器对象
var fShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fShader,`伪装是上面的片元做色器源码`);
gl.complieShader(fShader);
gl.attachShader(program,fShader);
复制代码
当program
对象收到顶点片元两个着色器以后,就能够帮这两个着色器链接起来。以前的顶点着色器里面说到把st
变量丢给片元着色器,这个就是program
对象帮忙丢的。
// 链接起两个程序
gl.linkProgram(program);
//而后跟webgl说,我要使用这个程序
gl.useProgram(program);
复制代码
这个过程很是繁琐,咱们能够封装一下,方便使用与记忆
/** * @name createProgram * @desc 建立着色器程序 * @param {WebGLRenderingContext} gl - webGl的context * @param {String} vsource - 顶点着色器源码字符串 * @param {String} fsource - 片元着色器源码字符串 * @return {WebGLProgram} - 着色器程序对象 */
function createProgram(gl,vsource,fsource){
const program = gl.createProgram();
const createShader = (source,type)=>{
const shader = gl.createShader(type);
gl.shaderSource(shader,source);
gl.compileShader(shader);
gl.attachShader(program,shader);
return shader;
}
createShader(vsource,gl.VERTEX_SHADER);
createShader(fsource,gl.FRAGMENT_SHADER);
gl.linkProgram(program );
return program ;
}
//使用
var cvs =document.createElement("canvas");
var gl = cvs.getContext("webgl");
var program = createProgram(
gl,
`伪装是顶点着色器源码`,
`伪装是片元着色器源码`
);
gl.useProgram(program )
复制代码
这样使用就简单多了。
关于纹理的建立以及一些小问题,以前的文章《webGL入门小贴士》里面多多少少有涉及,你们能够参考看一下,这里我就直接贴代码了。 建立纹理的方法封装:
/**建立纹理贴图 * @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));
}
复制代码
而后使用的话,就直接createTexByImage(gl,image);
传入canvas/image/video建立纹理。
文字纹理canvas的绘制 首先用canvas2D画出32*32的格子,而后把文字fillText
进去,只要就能保证字体宽度相等,否则中文与英文字母混编的话,字体不统一,在glsl
里面就很是难计算
/** * 建立文字纹理 * @param {String} text - 要成为纹理的文字 * @param {String} fontFamily - 文字的字体 * @return {HTMLCanvasElement} */
function createTextTextrue(text, fontFamily) {
var cvs = document.createElement("canvas");
var ctx = cvs.getContext("2d");
cvs.width = 32 * text.length;
cvs.height = 32;
ctx.font = "32px " + fontFamily;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
text.split("").forEach(function(word, i) {
ctx.fillText(word, i * 32 + 16, 16);
});
return cvs;
}
复制代码
结合上面的建立纹理的函数,咱们就能够这样使用:
createTexture(gl,createTextTextrue('文字','微软雅黑'))
复制代码
一个文字纹理就被建立出来准备给webGL用了。
视频纹理,直接用上面的函数,createTexure(gl,video)
把video传进去就能够了。只不过有一点要注意,传入的时候video要处于有画面的状态,若是video还没有播放,传进去会报错。
采样点也就是那些顶点的坐标,知道canvas的尺寸,以及字体的大小,而后就能够生成坐标了。
由于数据也简单,就只有x,y值,因此,给webGL传值能够说至关容易了。 咱们直接用一个buffer传过去
/**建立采样点 */
function createSampPoints(width, height, step) {
var a = [];
for (var i = 0; i <= height; i += step) {
for (var j = 0; j <= width; j += step) {
a.push(j, i);
}
}
return a;
}
// 建立顶点
var points = new Float32Array(createSampPoints(
cvs.width,
cvs.height,
32
));
//建立buffer
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
//将顶点写入内存
gl.bufferData(gl.ARRAY_BUFFER, points , gl.STATIC_DRAW);
// 获取a_position的内存地址
var index = gl.getAttribLocation(program,'a_position'),
// 激活a_position
gl.enableVertexAttribArray(index);
// 往a_position写值(规定a_position读取buffer的规则)
// 读两个点,float类型,不须要归一化,两次点集相隔0,从0位开始读取
gl.vertexAttribPointer(index,2, gl.FLOAT, false, 0, 0)
复制代码
这样着色器里面就可以读到a_position的值了,也就是咱们丢过去的采样点。
仍是老样子,先使用getUserMedia
读到视频流,而后让video播放它。
而webGL这边,能够开一个requestAnimationFrame动画,不断查询video的播放状态和上面那些操做是否就绪,若是符合条件的话就开始绘制,不符合的话就跳过。还有就是,由于我这边是经过ajax来请求两个着色器的源码的,因此视频开始播放的时候,可能我ajax请求还在路上,因此根本无法监听video的play事件,只能疯狂轮询了。若是你能肯定上面那些操做在视频开始播放的时候就已经就绪了,能够大胆地监听play事件。
绘制的话,由于视频画面会更新的缘故,因此每一帧你都须要更新一下视频纹理,可是这里千万要注意的是,更新纹理不是建立纹理!!!,千万别在requestAnimationFrame调用gl.createTexture
方法,每一帧都建立纹理对内存的消耗远远大于GC的收集速度,进而致使内存泄漏。正确的作法是,找到以前那个视频纹理,从新激活它,而后使用gl.texImage2D
方法去更新纹理。
function draw(){
if(/**判断一下是否能够绘制*/){
requestAnimationFrame(draw);
//直接下一帧
return
}
// u_tex0表明的是视频纹理,因此咱们激活一下TEXTURE0
gl.activeTexture(gl.TEXTURE0);
// 假设videoTexture是以前经过createTexture建立出来的纹理
// 这里的绑定是绑上面的TEXTURE0纹理,将videoTexture从新赋值给它
gl.bindTexture(gl.TEXTURE_2D, videoTexture);
// 将视频当前帧传入进去
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
video
);
// 清画面
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制
gl.drawArrays(
gl.POINTS,
0,
pointes.length / 2
);
requestAnimationFrame(draw);
}
复制代码
一样的对文字纹理的更新也遵循此办法。 绘制的事情几乎与js无关,js在这里面的做用就是,配置好一切、更新纹理,而后调用绘制而已,对cpu的开销也小,绘制过程当中连一次循环什么的都不须要,最主要的,是在移动端的表现至关流畅,webGL这种技术简直跟亲妈同样强大。
请各位同窗千万别问这玩意在现实中有什么用,能实现什么需求。看标题,这个只是个练习而已,仅仅是为了好玩。 否则你打开《webgl编程指南 》这本书,每一个例子都是画三角形,画三角形,我画到如今对三角形有阴影了。。。 嘛,原本学习就是一件枯燥的事情(对我这种学渣来讲),若是不在这个过程当中找到乐趣所在,很容易就放弃的。多多利用学到的知识,再结合之前学到的,去写一些有趣的练习吧,触类旁通,这样对知识的理解或更深入。 何况在这个练习里面,赶上了内存泄漏的问题而且解决掉了它,可谓是意外之喜呢。毕竟书里没写这部分的内容对不对,遇到就是赚到2333