原文首發於簡書,今天試了一下掘金的文章編輯器,簡直好用!之後文章就都發表在掘金了。。。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方法繪製正方形,點精靈能夠一次性繪製上千個正方形,並且你能夠在片元着色器內,在正方形內部填充不一樣的顏色或者圖案。
你們能夠訪問這個demo感覺一下 或者拿出手機掃一掃:
若是你們感興趣,這部分之後也能夠單獨拎出來說一講23333如今開始動手寫代碼了,首先是編寫頂點着色器的代碼。頂點着色器的做用很簡單,就是肯定點精靈的位置和大小用的,不過,由於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