跳到主要内容

使用帧缓冲离屏渲染

到目前为止,我们已经使用了很多屏幕缓冲了:用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲,这些缓冲结合起来叫做帧缓冲(Framebuffer),它被储存在GPU内存中的某处。WebGL允许我们定义我们自己的帧缓冲,也就是说我们能够定义我们自己的颜色缓冲,甚至是深度缓冲、模板缓冲。

我们目前所做的所有操作都是在默认帧缓冲的渲染缓冲上进行的。默认的帧缓冲是在你创建窗口的时候生成和配置的(GLFW帮我们做了这些)。通过创建我们自己的帧缓冲,我们可以获得额外的渲染目标(target),也被称作离屏渲染。

你可能不能很快理解它的应用,但渲染你的场景到不同的帧缓冲能够让我们在场景中加入类似镜子的东西,或者做出很酷的后期处理效果。它的整个过程是把当前的场景作为一张纹理保存到显存中,然后再去其他场景中使用这个纹理,最终以一种特别的效果显示到屏幕上。

在我们库中支持WebGL的帧缓冲离屏渲染,并对底层复杂的接口进行了封装,提供了开发者友好的接口。这里让我们通过一个例子看看如何应用它。

您需要做两件事:

  • 开辟颜色缓冲区(颜色FBO):在当前屏幕缓冲区之外新开辟一个颜色缓冲区进行渲染操作。

  • 将场景渲染到颜色缓冲区:将某个场景的渲染结果,也就是颜色缓冲,保存到该缓冲区中

开辟颜色缓冲区

在这个例子中,通过TextureBuffer()接口,我们开辟了两个颜色缓冲区,将用来存储不同的渲染结果。

const color0 = TextureBuffer();
const color1 = TextureBuffer();

在申请一段新的颜色缓冲区时,您不需要考虑它的大小,在写入时我们的接口程序会自动处理。

构建离屏渲染通道

多重渲染通道是一个很重要的渲染对象组织方式,它允许我们将部分渲染物体组织在一个单独的通道内,从而可以在整个渲染流程中的某个单独阶段内对其进行处理。这个例子中我们会构建两个渲染通道,其中一个是用于离屏渲染。

离屏渲染通道的构建方式和其他场景中完全一样,定义几何、材质(材质参数、着色器),组织成渲染单元与渲染节点,然后定义了相机、渲染背景,与渲染节点组成一个渲染通道。

// 组织渲染单元
const primitive = Primitive({
geometry: targetGeometry,
material: targetMaterial
});
// 组织渲染节点
const targetNode: JNode = {
mesh: {
primitives: [primitive]
}
};
// 定义相机
const targetCamera = Camera();
// 定义渲染背景
const targetBackground = Background({
size: ivec2(canvas.width, canvas.height),
colorMask: clr4(0.9, 0.8, 0.7, 1.0)
});

这个渲染通道的特殊之处在于,它额外有一个target参数,该参数是我们之前开启好的两个颜色缓冲区。

const target: JRenderTarget = {
color0: TextureBuffer(),
color1: TextureBuffer()
};

// 定义渲染通道,并将该通道的渲染结果保存到了之前创建的颜色缓冲区中。
const targetPass = Pass({
background: targetBackground,
camera: targetCamera,
node:targetNode,
target: target
});

我们需要注意到,这个例子中的面元着色器包含了两段颜色缓冲,分别存储不同的颜色分布。

const fs = `#version 300 es
...

layout (location = 0) out vec4 fragColor0;
layout (location = 1) out vec4 fragColor1;

uniform mat3 normalMatrix;

void main() {
float nDotV = abs((normalMatrix * normalize(v_normal)).z);
float intensity = nDotV * 0.7 + 0.3;
fragColor0 = vec4(vec3(0.7, 0.7, 0.7) * intensity, 1.0);
fragColor1 = vec4(vec3(0.7, 0.2, 0.2) * intensity, 1.0);
}`;

纹理贴图方式:颜色缓冲区

现在我们有两个离屏的颜色缓冲区:color0color1,现在看看如何将它们使用在另一个渲染通道中。我们已经在添加纹理贴图中介绍了如何将颜色缓冲作为贴图,以绑定纹理的方式,渲染到某一个几何表面。

...
// 渲染节点channel0的材质中绑定了color0作为纹理贴图
const channel0: JNode = {
mesh: {
primitives: [{
material: {
states: {
texture0: Texture(target.color0)
}
}
}]
},
transform: transform0
};

// 渲染节点channel1的材质中绑定了color1作为纹理贴图
const channel1: JNode = {
mesh: {
primitives: [{
material: {
states: {
texture0: Texture(target.color1)
}
}
}]
},
transform: transform1
};

...
const node: JNode = {
children: [channel0, channel1, channel2, channel3]
};

const boundingBox = bbox(0, 5, 0, 5, 0, 0);
const camera = Camera();
const background = Background({
size: ivec2(canvas.width, canvas.height),
colorMask: clr4(0.769, 0.769, 0.769, 1.0)
});
const pass = Pass({node, background, camera});

// 我们将离屏渲染通道和屏幕渲染通道同时加入到渲染场景中。
const scene: JScene = {
name: 'scene',
passes: [targetPass, pass],
};
const renderer = new Renderer(canvas);
renderer.render(scene);

当我们使用navigation对象去操纵离屏渲染通道的相机,您可以看到下面的效果:

离屏渲染