webGL离屏渲染framebuffer的详解

4 阅读7分钟

离屏canvas绘画

问题1. 在一个 canvas 画布中能同时执行 canvas 2D 和 WebGL 绘画吗?

不能,不能同时在同一个 canvas 上并行使用 2d 和 webgl 上下文。

  • canvas.getContext('2d') 和 canvas.getContext('webgl') 是互斥的,一个 canvas 一旦绑定了某种类型的上下文,再想获取另一种类型会失败或者导致原有上下文失效。
  • 若你尝试如下操作:
const ctx2d = canvas.getContext('2d');
const gl = canvas.getContext('webgl'); // 会返回 null 或清空 2d 上下文
  • 所以不能共用一个 canvas 同时绘制 2D 和 WebGL 内容

解决方法

  • 创建 两个 canvas,一个用于 WebGL,一个用于 2D,再把两个合成:

    • 用 CSS position: absolute 或 drawImage(另一个canvas) 把一个绘制到另一个上。
    • 或者用一个 canvas 作为 texture,将 2D 画布绘制为 WebGL 的材质。

问题2. 在 JS 中创建一个 canvas 元素但不插入 DOM,可以离屏作画或生成图像吗?

可以,而且这是很常见的操作!

你可以在内存中创建 canvas 并进行绘图操作,比如:

const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 512;
offscreenCanvas.height = 512;

const ctx = offscreenCanvas.getContext('2d');
// 开始绘图
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);

// 导出为图片
const imgURL = offscreenCanvas.toDataURL(); // base64 png

也可以把这个 canvas 用作 WebGL 材质:

const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA,
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  offscreenCanvas // 用作纹理来源
);

离屏webgl绘画

WebGL 中使用其中一个 canvas 或其附属资源作为 渲染目标(render target) ,而不是直接渲染到屏幕(默认帧缓冲),这个过程在图形学中叫做:

Render To Texture(RTT)Framebuffer Object(FBO)渲染****

WebGL离屏渲染使用的 framebuffer

标准方式(单 canvas):

  • 使用一个 canvas 创建 WebGL 上下文;
  • 创建一个 framebuffer,附加一个 texture(或 renderbuffer);
  • 渲染内容写入该 framebuffer;
  • 渲染完成后再将其作为纹理贴图到主画面上。
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);

// 创建离屏纹理
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
  gl.TEXTURE_2D, 0, gl.RGBA,
  width, height, 0,
  gl.RGBA, gl.UNSIGNED_BYTE, null
);

// 将纹理附加到 framebuffer 上
gl.framebufferTexture2D(
  gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
  gl.TEXTURE_2D, texture, 0
);

// 渲染目标变成了 framebuffer,而非默认 canvas
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.viewport(0, 0, width, height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

// ...进行绘制...

// 最后再切回默认帧缓冲(屏幕)
gl.bindFramebuffer(gl.FRAMEBUFFER, null);

webgl其他离屏幕技术

✅ 多 canvas 的应用方式(少见)

虽然多数离屏渲染是在一个 canvas 中创建 framebuffer 实现的,但确实也有人使用两个 canvas 的方式,常见用法如下:

✅ 用途 1:

一个 canvas 渲染,一个 canvas 显示(合成/纹理)

[canvasA - WebGL context] 渲染内容 → 画到一个 texture → 
[canvasB - 显示用] 利用 2D 或 WebGL 把 canvasA 内容绘制出来
  • canvasA.getContext('webgl'):离屏渲染;
  • canvasB.getContext('2d'):显示 canvasA 的渲染结果;
ctxB.drawImage(canvasA, 0, 0);

✅ 用途 2:

双缓冲机制模拟(仅限于某些高刷动态场景)

模拟“双缓冲”机制来减少闪烁感:

  • 在 canvasA 上绘制场景 A;

  • 在 canvasB 上绘制场景 B;

  • 在每一帧交替把某一个 canvas 显示到主 DOM(或使用 GPU 合成);

但这种方式并不常见或推荐,现代浏览器内部已经实现了自己的 GPU 双缓冲机制。


你并不需要多个 canvas 就可以完成离屏渲染,哪怕只有一个 canvas,也能:

  1. 渲染到 framebuffer;
  2. framebuffer 输出是纹理;
  3. 再把这个纹理贴图到屏幕。

类比理解(打个比方):

模式类比
默认 WebGL 渲染拿画笔直接在纸上画
framebuffer 离屏渲染先画在塑料膜(纹理)上,画完贴到纸上
多 canvas 离屏渲染有多个纸张(canvas),每张上画内容,然后你自己负责合成和显示

如果你能说一下你的应用场景(比如画场景贴图?画 UI?合成滤镜?)、目标需求,我可以帮你更具体地设计结构(比如该不该用两个 canvas、还是单 canvas + FBO 方案更好)。

framebuffer原理

✅ 简单理解 framebuffer 是什么

在 WebGL(或者 OpenGL)中,framebuffer(帧缓冲)其实是一个渲染目标的容器,你可以把它理解为:

“我画出来的所有图像,画到哪里去?”****

  • 默认情况下,WebGL 会把渲染内容画到 canvas 的显示区域(屏幕) 上;
  • 但如果你不想直接画在屏幕上,而是画到一张 纹理(texture) 上,就需要创建一个 framebuffer;
  • framebuffer 可以附加一张纹理,当你绑定这个 framebuffer 后,所有绘图操作都会写入这张纹理里。

📌 举个例子(流程图):

1. 默认渲染路径(你不显式使用 framebuffer):

draw() → WebGL 内部默认 framebuffer → canvas 显示区域(屏幕)

2. 使用 framebuffer 的路径(离屏渲染):

draw() → 自定义 framebuffer → 绑定了一张 texture → 得到一张画好的图像
        ↓
    把这张 texture 用作材质,再绘制到默认 framebuffer → canvas 显示区域(屏幕)

“framebuffer 是一个离屏渲染容器,WebGL 将渲染结果写入一张纹理(不是直接显示),你可以再将这张纹理作为图像用于后续渲染或展示。”


✅ framebuffer 的常见用途:

用途说明
🌈 后期处理效果(模糊、高斯、泛光)把主场景渲染到 framebuffer,处理纹理,再贴回去
🔁 多阶段渲染(多通道光照等)多个 framebuffer 串联渲染,最终合成
🖼️ 生成材质贴图(动态纹理)渲染到纹理用于角色身上的衣服、镜子、电视屏等
🧪 截图或图像导出framebuffer 输出为图片数据,再用 JS 导出

🖼️ 示意图理解:

                   +-----------------+
                   |  framebuffer    |
draw scene 1  ---> |  texture target | ---> 生成图像A
                   +-----------------+
                                      ↓
                             贴图为材质,再画到主屏幕

如果你想做某种特殊的渲染效果,或者对离屏结果如何导出感兴趣(如导出 PNG 图像),可以继续问我,我可以提供完整代码方案。

何时使用framebuffer

✅ 简要回答:

当你不希望直接把渲染结果显示在屏幕上,而是想将渲染结果保存成纹理、图片、离屏图像用于后处理或再加工,这时候就应该使用 framebuffer(FBO)


🧠 具体来说,以下场景是典型使用场景:


1. 🎨 后期处理 / 图像特效

关键词:模糊、高斯模糊、泛光、色彩调整、HDR****

你希望对整个画面进行“滤镜”处理时,一般会先把整个画面渲染进 framebuffer 的纹理中,再进行二次渲染处理。

流程:

场景渲染 → framebuffer → 得到 textureA
         ↓
    用 textureA 绘制屏幕 → 加模糊等 shader 效果

2. 📸 动态生成贴图 / 镜面反射 / 安全摄像头

关键词:反射贴图、电视屏、动态贴图、摄像头画面****

想象一个房间里有一个镜子或一个电视屏,电视里面显示的是某个摄像头的画面。你就需要:

  • 把摄像头视角下的场景渲染进 framebuffer(离屏);
  • 然后把 framebuffer 里的纹理作为电视屏上的贴图材质。

3. 🔍 模型点击选择(Color Picking)

正如你前面提到的模型交互:

  • 渲染一张“不可见”的图,每个模型是不同的颜色;
  • 用户点击后读取 framebuffer 像素;
  • 拿到颜色还原成模型 ID,实现精准拾取。

4. 🧱 多通道渲染(Deferred Rendering)

关键词:G-buffer、多个 pass、法线贴图、光照贴图****

一些高级渲染管线(如延迟渲染)会将:

  • 颜色、深度、法线、材质等信息分别渲染到多个 framebuffer(G-buffer)中;

  • 最后统一进行光照计算。

这在现代 3D 游戏引擎中非常常见。


5. 🖼️ 截图 / 导出图片

如果你希望将当前画面保存为图片,不直接从屏幕上导出,而是从 framebuffer 拿到更干净的像素数据,避免 UI 干扰或做高清截图。


6. 🧪 GPU 计算(GPGPU)

一些人用 WebGL 做计算(比如模拟流体、粒子物理、神经网络)时:

  • 会用 framebuffer 写入一张“数值纹理”;

  • 再读取或处理这张纹理,相当于 GPU 端的数据交换。

这种方式也叫 Ping-Pong Buffer 技术,在 fluid / CA / AI 模拟中常用。


✅ 总结一张图:什么时候用 framebuffer?

应用场景是否用 framebuffer?说明
普通渲染显示❌ 不用直接渲染到 canvas 即可
后处理效果(模糊、HDR)✅ 必须用渲染到 framebuffer,再贴到屏幕
模型拾取✅ 常用渲染特殊纹理到 framebuffer,读取点击像素
反射、水面、监控画面✅ 推荐framebuffer + 纹理贴图
截图导出✅ 推荐framebuffer 渲染 + readPixels
多通道渲染✅ 必须用framebuffer + MRT(多 render target)
GPU 计算模拟✅ 必须用framebuffer + shader 操作数值纹理