案例查看地址:点击这里
WebGL的另一项强大的技术,使用WebGL渲染三维图形,然后将渲染结果作为纹理贴到另一个三维物体上去。实际上,把渲染结果作为纹理使用,就是动态地生成图像,而不是向服务器请求加载外部图像。在纹理图像被贴上图形之前,我们还可以对其做一些额外的处理,比如生成如动态模糊或景深效果。
帧缓冲区对象和渲染缓冲区对象
在默认情况下,WebGL在颜色缓冲区中进行绘图,在开启隐藏面消除功能时,还会用到深度缓冲区。总之,绘制的结果图像是存储在颜色缓冲区中的。
帧缓冲区对象(framebuffer object)可以用来代替颜色缓冲区或深度缓冲区,如图,绘制在帧缓冲区中的对象并不会直接显示在<canvas>上,你可以先对帧缓冲区中的内容进行一些处理再显示,或者直接用其中的内容作为纹理对象。在帧缓冲区中进行绘制的过程又称为离屏绘制(offsetscreen drawing)。
图10.19显示了帧缓冲区对象的结构,它提供了颜色缓冲区和深度缓冲区的替代品。如你所见,绘制操作并不是直接发生在帧缓冲区的,而是发生在帧缓冲区所关联的对象(attachment)上。一个帧缓冲区有3个关联对象:颜色关联对象(color attachment)、深度关联对象(depth attachment)和模板关联对象(stencil attachment),分别用来替代颜色缓冲区、深度缓冲区和模板缓冲区。
经过一些设置,WebGL就可以向帧缓冲区的关联对象写入数据,就像写入颜色缓冲区或深度缓冲区一样。每个关联对象又可以是两种类型:纹理对象或渲染缓冲区对象(renderbuffer object)。当我们把纹理对象作为颜色关联关联到帧缓冲区对象后,WebGL就可以在纹理对象中绘图。渲染缓冲区对象表示一种更加通用的绘图区域,可以向其中写入多种类型的数据。
如何实现渲染到纹理
我们希望把WebGL渲染出的图像作为纹理使用。那么就需要将纹理对象作为颜色关联对象关联到帧缓冲区对象上,然后在帧缓冲区中进行绘制,此时颜色关联对象(即纹理对象)就替代了颜色缓冲区。此时仍然需要进行隐藏面消除,所以我们又创建了一个渲染缓冲区对象来作为帧缓冲区的深度关联对象,以替代深度缓冲区。
实现上述的配置需要8个步骤:
(1)创建帧缓冲区对象(gl.createFramebuffer())
(2)创建纹理对象并设置其尺寸和参数(gl.createTexture()、gl.bindTexture()、gl.texImage2D()、gl.Parameteri())。
(3)创建渲染缓冲区对象(gl.createRenderbuffer())。
(4)绑定渲染缓冲区对象并设置其尺寸(gl.bindRenderbuffer()、gl.renderbuffer-Storage())。
(5)将帧缓冲区的颜色关联对象指定为一个纹理对象(gl.frambufferTexture2D())。
(6)将帧缓冲区的深度关联对象指定为一个渲染缓冲区对象(gl.framebufferRenderbuffer())。
(7)检查帧缓冲区是否正确配置(gl.checkFramebufferStatus())。
(8)在帧缓冲区中进行绘制(gl.bindFramebuffer())。
案例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Title</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
margin: 0;
display: block;
}
</style>
</head>
<body οnlοad="main()">
<canvas id="canvas" height="800" width="800"></canvas>
</body>
<script src="lib/webgl-utils.js"></script>
<script src="lib/webgl-debug.js"></script>
<script src="lib/cuon-utils.js"></script>
<script src="lib/cuon-matrix.js"></script>
<script>
//设置WebGL全屏显示
var canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
//设置顶点着色器
var vertexShaderSource = "" +
"attribute vec4 a_Position;\n" +
"attribute vec2 a_TexCoord;\n" +
"uniform mat4 u_MvpMatrix;\n" +
"varying vec2 v_TexCoord;\n" +
"void main(){\n" +
" gl_Position = u_MvpMatrix * a_Position;\n" +
" v_TexCoord = a_TexCoord;\n" +
"}\n";
//设置片元着色器
var fragmentShaderSource = "" +
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"uniform sampler2D u_Sampler;\n" +
"varying vec2 v_TexCoord;\n" +
"void main(){\n" +
" gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n" +
"}\n";
//中间平面图形的高和宽
var offset_width = 256;
var offset_height = 256;
function main() {
var canvas = document.getElementById("canvas");
var gl = getWebGLContext(canvas);
if(!gl){
console.log("无法获取WebGL的上下文");
return;
}
//创建并初始化着色器
if(!initShaders(gl, vertexShaderSource, fragmentShaderSource)){
console.log("无法初始化着色器");
return;
}
//得到attribute变量和uniform变量的存储位置
var program = gl.program;//获取到程序对象
program.a_Position = gl.getAttribLocation(program, "a_Position");
program.a_TexCoord = gl.getAttribLocation(program, "a_TexCoord");
program.u_MvpMatrix = gl.getUniformLocation(program, "u_MvpMatrix");
if(program.a_Position < 0 || program.a_TexCoord < 0 || !program.u_MvpMatrix){
console.log("无法获取到变量的存储位置");
return;
}
//设置顶点的位置信息
var cube = initVertexBuffersForCube(gl);
var plane = initVertexBuffersForPlane(gl);
if(!cube || !plane){
console.log("存入缓冲区数据失败");
return;
}
//设置纹理
var texture = initTextures(gl);
if(!texture){
console.log("无法创建纹理缓冲区");
return;
}
//初始化帧缓冲区
var fbo = initFramebufferObject(gl);
if(!fbo){
console.log("无法初始化帧缓冲区");
return;
}
//开启隐藏面消除功能
gl.enable(gl.DEPTH_TEST);
//设置视图投影矩阵相关信息
var viewProjectMatrix = new Matrix4();
viewProjectMatrix.setPerspective(30.0, canvas.width/canvas.height, 1.0, 100.0);
viewProjectMatrix.lookAt(0.0, 0.0, 7.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
//设置帧缓冲区的视图投影矩阵
var viewProjectMatrixFbo = new Matrix4();
viewProjectMatrixFbo.setPerspective(30.0, offset_width/offset_height, 1.0, 100.0);
viewProjectMatrixFbo.lookAt(0.0, 2.0, 7.0,0.0, 0.0, 0.0,0.0,1.0,0.0);
//开始绘制
var currentAngle = 0.0;
function tick() {
currentAngle = animate(currentAngle); //更新旋转角度
draw(gl, canvas, fbo, plane, cube, currentAngle, texture, viewProjectMatrix, viewProjectMatrixFbo);
requestAnimationFrame(tick);
}
tick();
}
function draw(gl, canvas, fbo, plane, cube, angle, texture, viewProjectMatrix, viewProjectMatrixFbo) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); //更改绘图的目标为帧缓冲区
gl.viewport(0,0,offset_width, offset_height); //对帧缓冲区设置视口
gl.clearColor(0.2,0.2,0.4,1.0); //设置背景色
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //清除背景和隐藏面消除
//绘制立方体
drawTexturedCube(gl, gl.program, cube, angle, texture, viewProjectMatrixFbo);
gl.bindFramebuffer(gl.FRAMEBUFFER, null); //将绘制的目标修改为颜色缓冲区
gl.viewport(0,0,canvas.width, canvas.height); //将视口的大小设置为canvas的大小
gl.clearColor(0.0,0.0,0.0,1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //清除背景和隐藏面消除
//绘制平面
drawTexturedPlane(gl, gl.program, plane, angle, fbo.texture, viewProjectMatrix);
}
//声明坐标转换矩阵
var g_modelMatrix = new Matrix4();
var g_mvpMatrix = new Matrix4();
function drawTexturedPlane(gl, program, obj, angle, texture, viewProjectMatrix) {
//计算模型矩阵
g_modelMatrix.setTranslate(0.0,0.0,1.0);
g_modelMatrix.rotate(20.0,1.0,0.0,0.0);
g_modelMatrix.rotate(angle, 0.0, 1.0, 0.0);
//计算模型视图投影矩阵,并赋值给u_MvpMatrix
g_mvpMatrix.set(viewProjectMatrix);
g_mvpMatrix.multiply(g_modelMatrix);
gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.elements);
drawTexturedObject(gl, program, obj, texture);
}
function drawTexturedCube(gl, program, obj, angle, texture, viewProjectMatrix) {
//计算模型矩阵
g_modelMatrix.setRotate(20.0,1.0,0.0,0.0);
g_modelMatrix.rotate(angle, 0.0,1.0,0.0);
//计算模型视图投影矩阵,并赋值给u_mvpmatrix
g_mvpMatrix.set(viewProjectMatrix);
g_mvpMatrix.multiply(g_modelMatrix);
gl.uniformMatrix4fv(program.u_MvpMatrix,false, g_mvpMatrix.elements);
//绘制
drawTexturedObject(gl, program, obj, texture);
}
function drawTexturedObject(gl, program, obj, texture) {
//分配缓冲区对象并启用赋值
initAttributeVariable(gl, program.a_Position, obj.vertexBuffer); //顶点坐标
initAttributeVariable(gl, program.a_TexCoord, obj.texCoordBuffer); //纹理坐标
//将纹理对象绑定到目标
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
//绘制
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.indexBuffer);
gl.drawElements(gl.TRIANGLES, obj.numIndices, obj.indexBuffer.type, 0);
}
function initAttributeVariable(gl, a_attribute, buffer) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0);
gl.enableVertexAttribArray(a_attribute);
}
var angle_step = 30;
var last = +new Date();
function animate(angle) {
var now = +new Date();
var elapsed = now - last;
last = now;
var newAngle = angle + (angle_step*elapsed)/1000.0;
return newAngle%360;
}
function initFramebufferObject(gl) {
var framebuffer, texture, depthBuffer;
//定义错误函数
function error() {
if(framebuffer) gl.deleteFramebuffer(framebuffer);
if(texture) gl.deleteFramebuffer(texture);
if(depthBuffer) gl.deleteFramebuffer(depthBuffer);
return null;
}
//创建帧缓冲区对象
framebuffer = gl.createFramebuffer();
if(!framebuffer){
console.log("无法创建帧缓冲区对象");
return error();
}
//创建纹理对象并设置其尺寸和参数
texture = gl.createTexture();
if(!texture){
console.log("无法创建纹理对象");
return error();
}
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, offset_width, offset_height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
framebuffer.texture = texture;//将纹理对象存入framebuffer
//创建渲染缓冲区对象并设置其尺寸和参数
depthBuffer = gl.createRenderbuffer();
if(!depthBuffer){
console.log("无法创建渲染缓冲区对象");
return error();
}
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, offset_width, offset_height);
//将纹理和渲染缓冲区对象关联到帧缓冲区对象上
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER,depthBuffer);
//检查帧缓冲区对象是否被正确设置
var e = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if(gl.FRAMEBUFFER_COMPLETE !== e){
console.log("渲染缓冲区设置错误"+e.toString());
return error();
}
//取消当前的focus对象
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
return framebuffer;
}
function initTextures(gl) {
var texture = gl.createTexture();
if(!texture){
console.log("无法创建纹理缓冲区");
return null;
}
var u_Sampler = gl.getUniformLocation(gl.program, "u_Sampler");
if(!u_Sampler){
console.log("无法获取到缓冲区对象");
return null;
}
var img = new Image();
img.onload = function () {
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
gl.uniform1i(u_Sampler, 0);
gl.bindTexture(gl.TEXTURE_2D, null);
};
img.src = "./resources/sky_cloud.jpg";
return texture;
}
function initVertexBuffersForPlane(gl) {
// 创建一个面
// v1------v0
// | |
// | |
// | |
// v2------v3
// 顶点的坐标
var vertices = new Float32Array([
1.0, 1.0, 0.0, -1.0, 1.0, 0.0, -1.0,-1.0, 0.0, 1.0,-1.0, 0.0 // v0-v1-v2-v3
]);
// 纹理的坐标
var texCoords = new Float32Array([1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0]);
// 顶点的索引
var indices = new Uint8Array([0, 1, 2, 0, 2, 3]);
//将顶点的信息写入缓冲区对象
var obj = {};
obj.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
obj.texCoordBuffer = initArrayBufferForLaterUse(gl, texCoords, 2, gl.FLOAT);
obj.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE);
if(!obj.vertexBuffer || !obj.texCoordBuffer || !obj.indexBuffer) return null;
obj.numIndices = indices.length;
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
return obj;
}
function initVertexBuffersForCube(gl) {
// Create a cube
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
// 顶点的数据坐标
var vertices = new Float32Array([
1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0,-1.0, 1.0, 1.0,-1.0, 1.0, // v0-v1-v2-v3 front
1.0, 1.0, 1.0, 1.0,-1.0, 1.0, 1.0,-1.0,-1.0, 1.0, 1.0,-1.0, // v0-v3-v4-v5 right
1.0, 1.0, 1.0, 1.0, 1.0,-1.0, -1.0, 1.0,-1.0, -1.0, 1.0, 1.0, // v0-v5-v6-v1 up
-1.0, 1.0, 1.0, -1.0, 1.0,-1.0, -1.0,-1.0,-1.0, -1.0,-1.0, 1.0, // v1-v6-v7-v2 left
-1.0,-1.0,-1.0, 1.0,-1.0,-1.0, 1.0,-1.0, 1.0, -1.0,-1.0, 1.0, // v7-v4-v3-v2 down
1.0,-1.0,-1.0, -1.0,-1.0,-1.0, -1.0, 1.0,-1.0, 1.0, 1.0,-1.0 // v4-v7-v6-v5 back
]);
// 纹理的数据坐标
var texCoords = new Float32Array([
1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v0-v1-v2-v3 front
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, // v0-v3-v4-v5 right
1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, // v0-v5-v6-v1 up
1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v1-v6-v7-v2 left
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, // v7-v4-v3-v2 down
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 // v4-v7-v6-v5 back
]);
// 顶点的索引数据
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // right
8, 9,10, 8,10,11, // up
12,13,14, 12,14,15, // left
16,17,18, 16,18,19, // down
20,21,22, 20,22,23 // back
]);
//创建一个对象保存数据
var obj = {};
//将顶点信息写入缓冲区对象
obj.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
obj.texCoordBuffer = initArrayBufferForLaterUse(gl, texCoords, 2, gl.FLOAT);
obj.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE);
if(!obj.vertexBuffer || !obj.texCoordBuffer || !obj.indexBuffer) return null;
obj.numIndices = indices.length;
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
return obj;
}
function initArrayBufferForLaterUse(gl, data, num, type) {
var buffer = gl.createBuffer();
if(!buffer){
console.log("无法创建缓冲区对象");
return null;
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.num = num;
buffer.type = type;
return buffer;
}
function initElementArrayBufferForLaterUse(gl, data, type) {
var buffer = gl.createBuffer();
if(!buffer){
console.log("无法创建着色器");
return null;
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.type = type;
return buffer;
}
</script>
</html>
和之前的代码一样,body加载完成后触发main()函数,然后获取WebGL的上下文,初始化着色器,获取到变量的存储位置,设置顶点的数据并存入缓冲区。前面这些代码之前都接触过。新的知识在initFramebufferObject()函数内开始的(103行)。
函数在215行,先定义了一个错误函数,如果出现错误就调用这个方法return。里面也把创建的帧缓冲区对象删除。
然后,下面就开始书写创建帧缓冲区对象。
创建帧缓冲区对象(gl.createFramebuffer())
删除帧缓冲区对象(gl.deleteFramebuffer())
创建渲染缓冲区对象(gl.createRenderbuffer())
删除渲染缓冲区对象(gl.deleteRenderbuffer())
创建出来的渲染缓冲区对象将被指定为帧缓冲区的深度关联对象。在使用创建出的渲染缓冲区之前,还需要将其绑定到目标上,然后通过对目标做一些额外的操作来设置渲染缓冲区的尺寸等参数。
绑定渲染缓冲区(gl.bindRenderbuffer())
绑定完成后,我们就可以使用(gl.renderbufferStorage())函数来设置渲染缓冲区的格式、宽度、高度等。注意,作为深度关联对象的渲染缓冲区,其宽度和高度必须与作为颜色关联对象的纹理缓冲区一致。
接下来,我们就需要将它们关联到帧缓冲区上,并进行离屏绘图。
将纹理对象关联到帧缓冲区对象(gl.bindFramebuffer(),gl.framebufferTexture2D())
使用帧缓冲区对象的方式与使用渲染缓冲区类似:先将缓冲区绑定到目标上,然后通过操作目标来操作缓冲区对象,而不能直接操作缓冲区对象。
绑定帧缓冲区对象函数(gl.bindFramebuffer())
一旦帧缓冲区对象被绑定到target目标上,就可以通过target来使之与纹理对象进行关联。本例中,我们用一个纹理对象来代替颜色缓冲区,所以就将这个纹理对象指定为帧缓冲区的颜色关联对象。
使用(gl.framebufferTexture2D())函数来绑定纹理对象关联到target的帧缓冲区
注意,attachment参数的取值之一gl.COLOR_ATTACHMENT0,其名称中出现了一个0.这是因为在OpenGL中,帧缓冲区可以具有多个颜色关联对象(gl.COLOR_ATTACHMENT0,gl.COLOR_ATTACHMENT1等等),但是WebGL中只可以有1个。
现在我们已经把纹理对象指定为帧缓冲区的颜色关联对象了,下面来把渲染缓冲区对象指定为帧缓冲区的深度关联对象。
将渲染缓冲区对象关联到帧缓冲区对象(gl.framebufferRenderbuffer())
使用gl.framebufferRenderbuffer()函数来把渲染缓冲区对象关联到帧缓冲区对象上。这里,渲染缓冲区对象的作用是帮助进行隐藏面消除,所以我们将其指定为深度关联对象。
现在,我们已经完成了帧缓冲区上的所有的关联操作,只等WebGL在其中进行绘制了,但是在此之前,先检查一下帧缓冲区是否真的正确配置了。
检查帧缓冲区的配置(gl.checkFramebufferStatus())
显然,如果帧缓冲区对象没有被正确配置,就会发生错误。如你所见,前几节为帧缓冲区关联纹理对象和渲染缓冲区对象,它们的过程很复杂,有时会出现错误。
在帧缓冲区进行绘图
绘制函数draw()在133行书写,首先更改了绘图的目标为帧缓冲区fbo,并在其颜色关联对象,即在纹理对象中绘制了立方体。然后,我们把绘制目标切换回<canvas>,调用drawTexturedPlane()函数在颜色缓冲区中绘制矩形,同事把上一步在纹理对象中绘制的图形贴到矩形表面上。
首先调用gl.bindFramebuffer()函数绑定帧缓冲区对象,这样gl.drawArrays()和drawElements()函数就会在帧缓冲区中进行绘制。
gl.viewport()函数定义离线绘图的绘图区域
然后清除帧缓冲区中的颜色关联对象和深度关联对象,就像我们清除颜色缓冲区和深度缓冲区一样。接着绘制了立方体,其纹理是一副蓝天白云的图像。我们将背景色从黑色改成了紫蓝色,以突出显示矩形。这样,绘制在纹理缓冲区中的立方体就可以被当作纹理图像贴到了矩形上去。接下来绘制矩形plane,这是要在颜色缓冲区中绘制了,所以还得把绘制目标切换回来。调用gl.bindFramebuffer()函数并将第2个参数是定为null,解除了帧缓冲区的绑定,然后调用drawTexturedPlane()函数绘制了矩形。注意,我们将存储了离屏绘制结果的纹理对象fbo.texture作为参数传入了该函数,供绘制矩形时使用。
运行程序,你会发现矩形正反两个表面都被贴上了纹理,这是因为WebGL默认绘制图形的正反两个表面(虽然你只能看到一个)。我们可以使用gl.enable(glCULL_FACE)来开启消隐功能(culling function),让WebGL不再绘制图形的背面,以提高绘制速度(理想情况下达到两倍)。