webGL渲染到纹理

帧缓冲

当你使用WebGL API渲染3D内容时,显卡需要一块缓冲区来存储渲染的最终结果,我们称为帧缓冲。如果你没有指定,会存在一个默认的 frame buffer,它代表最终会被显示在网页中的缓冲区域。
WebGL提供了接口给我们自定义帧缓冲区,我们可以创建了一个自己的frame buffer,指定WebGL将渲染结果输送到所创建的frame buffer中,并指定它使用一张纹理作为存储像素颜色的缓冲区。这样就可以将渲染的结果显示出来。

在游戏开发中,有时候我们会在主渲染之外创建一个离屏的canvas,在这个canvas上进行一些自定义的渲染,然后将渲染的结果保存到图片中用于展示。cocos creator中的label就是使用这个方式实现的,cocos会为每个label创建一个canvas(当然,label是复用的),将文字的渲染结果保存到贴图中,再使用贴图展示文字。游戏中截屏的原理也是类似的,通过获取帧缓冲中指定区域的像素颜色集合,将其写入纹理对象中,最后保存为图片。

渲染到纹理的demo

这个demo加载了一个笔记本的模型,然后在我们自定义的frame buffer中缓存的一个旋转箱子的渲染结果,并保存在一张纹理中,最后在笔记本的屏幕上使用这个纹理将箱子渲染出来。

webGL代码

我们首先需要申请一块区域来存储每个像素渲染后的颜色结果,同时,我们还需要一个depth buffer(深度缓冲区)来处理视线遮挡,这样,远处的像素点在做深度测试的时候就会被丢弃,在视觉上给人一种被挡住的感觉。下面是具体的webGL代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

var rttFramebuffer;//frame buffer
var rttTexture;//用于存储渲染结果的纹理贴图

function initTextureFramebuffer() {
//创建frame buffer
rttFramebuffer = gl.createFramebuffer();
//绑定frame buffer
gl.bindFramebuffer(gl.FRAMEBUFFER, rttFramebuffer);
//设置buffer的宽高
rttFramebuffer.width = 512;
rttFramebuffer.height = 512;

//创建纹理贴图
rttTexture = gl.createTexture();
//绑定纹理贴图
gl.bindTexture(gl.TEXTURE_2D, rttTexture);
//设置纹理缩放处理
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
gl.generateMipmap(gl.TEXTURE_2D);
//加载图片到纹理,这里我们不需要加载任何图片,所以我们最后一个图片数据参数传了null
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, rttFramebuffer.width, rttFramebuffer.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

//创建深度缓冲
var renderbuffer = gl.createRenderbuffer();
//绑定深度缓冲
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
//调用gl.renderbufferStorage来通知WebGL当前render buffer用作深度缓冲区
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, rttFramebuffer.width, rttFramebuffer.height);
//指定使用rttTexture作为当前frame buffer的颜色缓冲
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, rttTexture, 0);
//指定使用renderbuffer作为当前frame buffer的深度缓冲
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer);

//解除绑定
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

//渲染主循环
function drawScene() {
//绑定rttFramebuffer为当前的帧缓冲
gl.bindFramebuffer(gl.FRAMEBUFFER, rttFramebuffer);
//渲染屏幕中的箱子,并将结果保存在rttTexture中
drawSceneOnLaptopScreen();

//恢复到默认的frame buffer
gl.bindFramebuffer(gl.FRAMEBUFFER, null);

......

//设置笔记本模型的顶点坐标,法线向量,顶点索引
gl.bindBuffer(gl.ARRAY_BUFFER, laptopVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, laptopVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, laptopVertexNormalBuffer);
gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, laptopVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, laptopVertexIndexBuffer);
//绘制笔记本
gl.drawElements(gl.TRIANGLES, laptopVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

......

//设置屏幕上用于渲染rttTexture的顶点坐标,纹理坐标,顶点索引
gl.bindBuffer(gl.ARRAY_BUFFER, laptopScreenVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, laptopScreenVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, laptopScreenVertexNormalBuffer);
gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, laptopScreenVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, laptopScreenVertexTextureCoordBuffer);
gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, laptopScreenVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

//绑定rttTexture
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, rttTexture);
gl.uniform1i(shaderProgram.samplerUniform, 0);

//渲染屏幕的内容
gl.drawArrays(gl.TRIANGLE_STRIP, 0, laptopScreenVertexPositionBuffer.numItems);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

var moonAngle = 180;
var cubeAngle = 0;

function drawSceneOnLaptopScreen() {
//设置viewport尺寸
gl.viewport(0, 0, rttFramebuffer.width, rttFramebuffer.height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

pMatrix = okMat4Proj(45, rttFramebuffer.width / rttFramebuffer.height, 0.1, 100.0);

......

//设置箱子的顶点坐标,法线向量,纹理坐标和顶点索引
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexNormalBuffer);
gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, cubeVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);

//绑定箱子纹理
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, crateTexture);
gl.uniform1i(shaderProgram.samplerUniform, 0);

//绘制箱子
gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

//解除绑定
gl.bindTexture(gl.TEXTURE_2D, rttTexture);
gl.generateMipmap(gl.TEXTURE_2D);
gl.bindTexture(gl.TEXTURE_2D, null);
}