WebGL编程指南学习(9)
Ragnarok!
9. 高级技巧(续)
α \alpha α混合
如何实现 α \alpha α混合
两个步骤:
- 开启混合功能
gl.enable(gl.BLEND)
- 指定混合函数
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
混合函数
gl.blendFund()
在进行
α
\alpha
α混合时,实际上用到了两个颜色,源颜色(source color)和目标颜色(destination color)。前者是“待被混合的颜色”,也就是原本的颜色;后者是“待被混合进去的颜色”。
gl.blendFunc(src_factor, dst_factor)
// 混合后颜色=源颜色*src_factor+目标颜色*dst_factor
这里的src_factor和dst_factor是有特指的,比如
gl.ZERO,gl.ONE_MINUS_DST_COLOR
等
半透明的三维物体
- α \alpha α混合发生在绘制片元的过程。当隐藏面消除功能开启时,被隐藏的片元不会被绘制,所以也就不会发生混合过程,更不会有半透明的效果。只要关闭隐藏面消除就可以达到半透明的效果
- 但是关闭隐藏面消除功能只是一个粗暴的解决方案。实际绘制三维场景时,场景中往往既有不透明的物体,也有半透明的物体,如果关闭隐藏面消除功能,那些不透明物体的前后关系就会乱套
同时实现隐藏面消除和半透明效果的机制
- 开启隐藏面消除
- 绘制所有不透明的物体
- 锁定用于进行隐藏面消除的深度缓冲区的写入操作,使之只读
- 绘制所有半透明的物体,注意它们应当按照深度排序,然后从后向前绘制
- 释放深度缓冲区,使之可读可写
gl.depthMask(mask)
// 锁定或释放深度缓冲区的写入操作,true/false
由于深度缓冲区被锁定了,绘制不透明物体后面的物体,即使是半透明的也不会显示;而深度在不透明物体前面的,会正常显示不透明的效果
切换着色器
对不同的物体,经常需要使用不同的着色器来绘制。每个着色器中可能有非常复杂的逻辑以实现各种不同的效果。我们可以准备多个着色器,然后根据需要来切换它们
渲染到纹理
使用WebGL渲染三维图形,然后将渲染结果作为纹理贴到另一个三维物体上去。
帧缓冲区对象和渲染缓冲区对象
默认情况下,WebGL在颜色缓冲区中进行绘图,在开启隐藏面消除功能室,还会用到深度缓冲区。总之,绘制的结果图像是存储在颜色缓冲区中的。
帧缓冲区对象(framebuffer object)可以代替颜色缓冲区或深度缓冲区。绘制在帧缓冲区中的对象并不会直接显示在canvas上。因此,我们就可以先对帧缓冲区中的内容进行一些处理再显示;或者直接用其中的内容作为纹理图像。
在帧缓冲区中进行绘制的过程又称为离屏绘制(offscreen drawing)
帧缓冲区提供了颜色缓冲区和深度缓冲区的替代品——所关联的对象(attachment)。一个帧缓冲区有3个关联对象:颜色关联对象(color attachment)、深度关联对象(depth attachment)和模板关联对象(stencil attachment),分别用来替代颜色缓冲区、深度缓冲区和模板缓冲区
- 经过一些设置,WebGL可以向帧缓冲区的关联对象写入数据
- 每个关联对象又可以是两种类型的:纹理对象或渲染缓冲区对象
如何实现渲染到纹理?
如果希望把WebGL渲染出的图像作为纹理使用,那么就需要:
- 将纹理对象作为颜色关联对象关联到帧缓冲区对象上;
- 在帧缓冲区中进行绘制
此时,颜色关联对象(即纹理对象)就替代了颜色缓冲区。由于此时仍然需要进行隐藏面消除,所以又创建一个渲染缓冲区对象来作为帧缓冲区的深度关联对象,以替代深度缓冲区
所以,最终帧缓冲区的配置是这样的——
颜色关联对象——纹理对象
深度关联对象——渲染缓冲区对象
注意这两个对象的绘图区域大小必须一致
例程:将一个旋转的立方体作为纹理贴在一个矩形上
算法步骤
- 创建帧缓冲区对象
- 创建纹理对象并设置其尺寸和参数
- 创建渲染缓冲区对象
- 绑定渲染缓冲区对象并设置其尺寸
- 将帧缓冲区的颜色关联对象指定为一个纹理对象
- 将帧缓冲区的深度关联对象指定为一个渲染缓冲区对象
- 检查帧缓冲区是否配置正确
- 在帧缓冲区进行绘制
前调
// 着色器
...
// 设置离屏绘制的尺寸
var OFFSCREEN_WIDTH = 256;
var OFFSCREEN_HEIGHT = 256;
function main() {
// 一些上下文设置
...
// 设置顶点信息
var cube = initVertexBufferForCube(gl);
var plane = initVertexBufferForPlane(gl);
...
// 设置纹理
var texture = initTextures(gl);
...
// 初始化帧缓冲区(Framebuffer object,FBO)
var fbo = initFramebufferObject(gl);
...
var viewProjMatrix = new Matrix4(); // 为颜色缓冲区所准备
viewProjMatrix.setPerspective(30, canvas.width/canvas.height, 1.0, 100.0);
viewProjMatrix.lookAt(0.0, 0.0, 7.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
var viewProjMatrixFBO = new Matrix4(); // 为帧缓冲区所准备
viewProjMatrixFBO.setPerspective(30.0, OFFSCREEN_WIDTH/OFFSCREEN_HEIGHT, 1.0, 100.0);
viewProjMatrixFBO.lookAt(0.0, 2.0, 7.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
// 开始绘制
var currentAngle = 0.0;
var tick = function() {
currentAngle = animate(currentAngle);
draw(gl, canvas, fbo, plane, cube, currentAngle, texture, viewProjMatrix, viewProjMatrixFBO);
window.requestAnimationFrame(tick, canvas);
};
tick();
}
function initVertexBuffersForCube(gl) {
// 创建一个cube
}
function initVertexBuffersForPlane(gl) {
// 创建一个face
}
function initArrayBufferForLaterUse(gl, data, num, type) {
// 创建一个buffer object
}
function initElementArrayBufferForLaterUse(gl, data, type) {
// 创建一个buffer object
}
创建帧缓冲区对象、创建纹理对象并设置其尺寸和参数、创建渲染缓冲区对象并设置其尺寸和参数、将纹理对象关联到帧缓冲区
function initFramebufferObject(gl) {
var frameBuffer, texture, depthBuffer;
...
// 创建一个FBO
framebuffer = gl.createFramebuffer();
...
// 创建一个纹理对象并设置其尺寸和参数
texture = gl.createTexture();
...
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texOarameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
framebuffer.texture = texture; // 保存纹理对象
// 创建一个渲染缓冲区对象(renderbuffer object),并设置其尺寸和参数
depthBuffer = gl.createRenderbuffer();
...
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT);
// 把纹理和渲染缓冲区对象关联到FBO
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebuferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
// 检查FBO是否配置正确
var e = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
...
}
渲染缓冲区的设置
gl.renderbufferStorage(target, internalformat, width, height);
其中,internalformat指定渲染缓冲区中的数据格式,包括gl.DEPTH_COMPONENT16(表示渲染缓冲区将替代深度缓冲区)、gl.STENCIL_INDEX8(表示渲染缓冲区将替代模板缓冲区)、gl.RGBA4(表示渲染缓冲区将替代颜色缓冲区),等
关联纹理对象到FBO
gl.framebufferTexture2D(target, attachment, textarget, texture, level); // 参数: // target:必须是gl.FRAMEBUFFER // attachent: // gl.COLOR_ATTACHMENT0 颜色关联对象 // gl.DEPTH_ATTACHMENT 深度关联对象 // textarget: 纹理类型gl.TEXTURE_2D|gl.TEXTURE_CUBE // texture: 要关联的纹理对象 // level: 0, 使用mipmap纹理时指定纹理的层级
关联帧缓冲区对象到FBO
gl.framebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer); // 参数 // attachment: // gl.COLOR_ATTACHMENT0: 颜色关联对象 // gl.DEPTH_ATTACHMENT: 深度关联对象 // gl.STENCIL_ATTACHMENT:模板关联对象
在FBO进行绘图
- 首先把绘制目标切换为FBO,并在其颜色关联对象,即纹理对象中绘制立方体;
- 然后把绘制目标切换回canvas,在颜色缓冲区中绘制矩形,同时把上一步在纹理对象中绘制的图像贴到矩形表面上
function draw(gl, canvas, fbo, plane, cube, angle, texture viewProjMatrix, viewProjMAtrixFBO) {
// 切换渲染目标为FBO
gl.bindFrameBuffer(gl.FRAMEBUFFER, fbo);
gl.viewport(0, 0, OFFSCREEN_WIDTH, OFFSCREEEN_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, viewProjMatrixFBO);
// 切换渲染目标为颜色缓冲区,实际上解绑帧缓冲区
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas,width, canvas.height);
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, viewProjMatrix);
}
注意fbo.texture作为参数传入了drawTexturedPlane,供绘制平面是使用。fbo.texture是离屏绘制结果的纹理对象