引子
在看 How I built a wind map with WebGL 的時候,裡面用到了 framebuffer ,就去查了下資料單獨嘗試了一下。
幀緩衝區物件
WebGL 有一個能力是將渲染結果作為紋理使用,使用到的就是幀緩衝區物件(framebuffer object)。
在預設情況下,WebGL 最終繪圖結果儲存在顏色緩衝區,幀緩衝區物件可以用來代替顏色緩衝區,如下圖所示,繪製在幀緩衝區中的物件並不會直接顯示在 Canvas 上,因此這種技術也被稱為離屏繪製(offscreen drawing)。
示例
為了驗證上面的功能,這個示例會在幀緩衝區裡面繪製一張圖片,然後將其作為紋理再次繪製顯示出來。
基於使用圖片示例的邏輯,主要有下面幾個方面的變化:
- 資料
- 幀緩衝區物件
- 繪製
資料
在幀緩衝區裡面繪製跟正常的繪製一樣,只是不顯示,所以也要有對應的繪製區域大小、頂點座標和紋理座標。
offscreenWidth: 200, // 離屏繪製的寬度
offscreenHeight: 150, // 離屏繪製的高度
// 部分程式碼省略
// 針對幀緩衝區繪製的頂點和紋理座標
this.offScreenBuffer = this.initBuffersForFramebuffer(gl);
// 部分程式碼省略
initBuffersForFramebuffer: function (gl) {
const vertices = new Float32Array([
0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5,
]); // 矩形
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
const texCoords = new Float32Array([
1.0,
1.0, // 右上角
0.0,
1.0, // 左上角
0.0,
0.0, // 左下角
1.0,
0.0, // 右下角
]);
const obj = {};
obj.verticesBuffer = this.createBuffer(gl, gl.ARRAY_BUFFER, vertices);
obj.indexBuffer = this.createBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, indices);
obj.texCoordsBuffer = this.createBuffer(gl, gl.ARRAY_BUFFER, texCoords);
return obj;
},
createBuffer: function (gl, type, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(type, buffer);
gl.bufferData(type, data, gl.STATIC_DRAW);
gl.bindBuffer(type, null);
return buffer;
}
// 部分程式碼省略
頂點著色器和片元著色器都可以新定義,這裡為了方便公用了一套。
幀緩衝區物件
想要在幀緩衝區繪製,需要建立對應的幀緩衝區物件。
// 幀緩衝區物件
this.framebufferObj = this.createFramebufferObject(gl);
// 部分程式碼省略
createFramebufferObject: function (gl) {
let framebuffer = gl.createFramebuffer();
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
this.offscreenWidth,
this.offscreenHeight,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null
);
// 反轉圖片 Y 軸方向
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
// 紋理座標水平填充 s
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
// 紋理座標垂直填充 t
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// 紋理放大處理
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// 紋理縮小處理
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
framebuffer.texture = texture; // 儲存紋理物件
// 關聯緩衝區物件
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
texture,
0
);
// 檢查配置是否正確
var e = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (gl.FRAMEBUFFER_COMPLETE !== e) {
console.log("Frame buffer object is incomplete: " + e.toString());
return;
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
return framebuffer;
}
// 部分程式碼省略
- createFramebuffer 函式建立幀緩衝區物件,刪除物件的函式是 deleteFramebuffer 。
- 建立好後,需要將幀緩衝區的顏色關聯物件指定一個紋理物件,示例建立的紋理物件有幾個特點:1 紋理的寬高跟繪製區域寬高一致;2 使用
texImage2D
時最後一個引數為null
,也就是預留了一個空白的儲存紋理物件的區域;3 建立好的紋理物件放到了幀緩衝區物件上,就是這行程式碼framebuffer.texture = texture
。 - bindFramebuffer 函式將幀緩衝區繫結到目標上,然後使用 framebufferTexture2D 將前面建立的紋理物件繫結到幀緩衝區的顏色關聯物件
gl.COLOR_ATTACHMENT0
上。 - checkFramebufferStatus 檢查幀緩衝區物件配置是否正確。
繪製
繪製時候主要的區別是有切換的過程:
// 部分程式碼省略
draw: function () {
const gl = this.gl;
const frameBuffer = this.framebufferObj;
this.canvasObj.clear();
const program = this.shaderProgram;
gl.useProgram(program.program);
// 這個就讓繪製的目標變成了幀緩衝區
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
gl.viewport(0, 0, this.offscreenWidth, this.offscreenHeight);
this.drawOffFrame(program, this.imgTexture);
// 解除幀緩衝區繫結,繪製的目標變成了顏色緩衝區
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
this.drawScreen(program, frameBuffer.texture);
},
// 部分程式碼省略
- 先使用
bindFramebuffer
讓繪製的目標變成幀緩衝區,需要指定對應的視口。 - 幀緩衝區繪製完成後解除繫結,恢復到正常預設的顏色緩衝區,同樣需要指定對應的視口,還要比較特別的是使用了緩衝區物件的紋理,這個表明就是從幀緩衝區得到的繪製結果。
觀察及思考
網上找的相關示例感覺比較複雜,嘗試簡化的過程中有下面的一些觀察和思考。
framebuffer.texture
是本來就有的屬性還是人為新增的 ?
在建立幀緩衝區物件的時候有這個邏輯: framebuffer.texture = texture
,那麼幀緩衝區物件本身就有 texture
屬性嗎?
列印日誌發現剛建立的時候並沒有這個屬性,所以推測應該是人為的新增。
framebuffer.texture
什麼時候有的內容 ?
初始化幀緩衝區物件的時候,儲存的紋理是空白的,但從最終結果來看,在幀緩衝區繪製之後,紋理就有內容了,那麼 framebuffer.texture
屬性是什麼時候有了內容?
在繪製邏輯中,跟紋理相關語句有:
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(program.uSampler, 0);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
推測是 gl.drawElements
方法繪製結果儲存在幀緩衝區的顏色關聯物件,幀緩衝區的顏色關聯物件又在初始化時關聯了建立的空白紋理 物件,framebuffer.texture
指向的也是同一個空白紋理物件,所以最終就有了內容。
最終的顯示為什麼沒有鋪滿整個畫布?
最終繪製可顯示的內容時,可以發現頂點對應整個畫布,紋理座標對應的整個完整的紋理,但為什麼沒有鋪滿整個畫布?
最終繪製可顯示內容時使用的紋理來自幀緩衝區的繪製結果,而幀緩衝區的頂點對應的是整個緩衝區域的一半,如果把整個幀緩衝區繪製結果當做一個紋理,按照最終繪製可視區比例縮放,那麼最後的繪製沒有鋪滿就是預期正確的結果。
這個是鋪滿畫布的示例,只需將緩衝區頂點調整為對應整個緩衝區大小。