渲染到纹理非常简单,创建一个确定大小的纹理,其实这个部分也是对对应图像处理的做一个更加细致的一个处理;

  1. // 创建渲染对象
  2. const targetTextureWidth = 256;
  3. const targetTextureHeight = 256;
  4. const targetTexture = gl.createTexture();
  5. gl.bindTexture(gl.TEXTURE_2D, targetTexture);
  6. {
  7. // 定义 0 级的大小和格式
  8. const level = 0;
  9. const internalFormat = gl.RGBA;
  10. const border = 0;
  11. const format = gl.RGBA;
  12. const type = gl.UNSIGNED_BYTE;
  13. const data = null;
  14. gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
  15. targetTextureWidth, targetTextureHeight, border,
  16. format, type, data);
  17. // 设置筛选器,不需要使用贴图
  18. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  19. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  20. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  21. }

注意 data 是 null,我们不需要提供数据,只需要让WebGL分配一个纹理。

接下来创建一个帧缓冲(framebuffer),帧缓冲只是一个附件集,附件是纹理或者 renderbuffers, 我们之前讲过纹理,Renderbuffers 和纹理很像但是支持纹理不支持的格式和可选项,同时, 不能像纹理那样直接将 renderbuffer 提供给着色器。
让我们来创建一个帧缓冲并和纹理绑定

  1. // 创建并绑定帧缓冲
  2. const fb = gl.createFramebuffer();
  3. gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
  4. // 附加纹理为第一个颜色附件
  5. const attachmentPoint = gl.COLOR_ATTACHMENT0;
  6. gl.framebufferTexture2D(
  7. gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level);

与纹理和缓冲相似,在创建完帧缓冲后我们需要将它绑定到 FRAMEBUFFER 绑定点, 那样所有的方法都会作用到绑定的帧缓冲,无论是哪个帧缓冲。

绑定帧缓冲后,每次调用 gl.clear, gl.drawArrays, 或 gl.drawElements WebGL都会渲染到纹理上而不是画布上。
将原来的渲染代码构造成一个方法,就可以调用两次,一次渲染到纹理,一次渲染到画布。

  1. function drawCube(aspect) {
  2. // 告诉它使用的程序(着色器对)
  3. gl.useProgram(program);
  4. // 启用位置属性
  5. gl.enableVertexAttribArray(positionLocation);
  6. // 绑定到位置缓冲
  7. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  8. // 告诉位置属性如何从 positionBuffer (ARRAY_BUFFER) 中读取数据
  9. var size = 3; // 每次迭代需要三个单位数据
  10. var type = gl.FLOAT; // 单位数据类型为 32 位浮点型
  11. var normalize = false; // 不需要单位化
  12. var stride = 0; // 每次迭代移动的距离
  13. var offset = 0; // 从缓冲起始处开始
  14. gl.vertexAttribPointer(
  15. positionLocation, size, type, normalize, stride, offset)
  16. // 启用纹理坐标属性
  17. gl.enableVertexAttribArray(texcoordLocation);
  18. // 绑定纹理坐标缓冲
  19. gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
  20. // 告诉纹理坐标属性如何从 texcoordBuffer 读取数据
  21. var size = 2; // 每次迭代两个单位数据
  22. var type = gl.FLOAT; // 单位数据类型是 32 位浮点型
  23. var normalize = false; // 不需要单位化数据
  24. var stride = 0; // 每次迭代移动的数据
  25. var offset = 0; // 从缓冲起始处开始
  26. gl.vertexAttribPointer(
  27. texcoordLocation, size, type, normalize, stride, offset)
  28. // 计算投影矩阵
  29. var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  30. var projectionMatrix =
  31. m4.perspective(fieldOfViewRadians, aspect, 1, 2000);
  32. var cameraPosition = [0, 0, 2];
  33. var up = [0, 1, 0];
  34. var target = [0, 0, 0];
  35. // 计算相机矩阵
  36. var cameraMatrix = m4.lookAt(cameraPosition, target, up);
  37. // 根据相机矩阵计算视图矩阵
  38. var viewMatrix = m4.inverse(cameraMatrix);
  39. var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
  40. var matrix = m4.xRotate(viewProjectionMatrix, modelXRotationRadians);
  41. matrix = m4.yRotate(matrix, modelYRotationRadians);
  42. // 设置矩阵
  43. gl.uniformMatrix4fv(matrixLocation, false, matrix);
  44. // 使用纹理单元 0
  45. gl.uniform1i(textureLocation, 0);
  46. // 绘制几何体
  47. gl.drawArrays(gl.TRIANGLES, 0, 6 * 6);
  48. }

注意到我们需要传入 aspect 计算投影矩阵,因为目标纹理的比例和画布不同。 然后这样调用

  1. // 绘制场景
  2. function drawScene(time) {
  3. ...
  4. {
  5. // 通过绑定帧缓冲绘制到纹理
  6. gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
  7. // 使用 3×2 的纹理渲染立方体
  8. gl.bindTexture(gl.TEXTURE_2D, texture);
  9. // 告诉WebGL如何从裁剪空间映射到像素空间
  10. gl.viewport(0, 0, targetTextureWidth, targetTextureHeight);
  11. // 清空画布和深度缓冲
  12. gl.clearColor(0, 0, 1, 1); // clear to blue
  13. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  14. const aspect = targetTextureWidth / targetTextureHeight;
  15. drawCube(aspect)
  16. }
  17. {
  18. // 渲染到画布
  19. gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  20. // 立方体使用刚才渲染的纹理
  21. gl.bindTexture(gl.TEXTURE_2D, targetTexture);
  22. // 告诉WebGL如何从裁剪空间映射到像素空间
  23. gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  24. // 清空画布和深度缓冲
  25. gl.clearColor(1, 1, 1, 1); // clear to white
  26. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  27. const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  28. drawCube(aspect)
  29. }
  30. requestAnimationFrame(drawScene);
  31. }

对应的结果输出 结果输出
十分重要的是要记得调用 gl.viewport 设置要绘制的对象的大小, 在这个例子中第一次渲染到纹理,所以设置视图大小覆盖纹理, 第二次渲染到画布所以设置视图大小覆盖画布。
同样的当我们计算投影矩阵的时候需要使用正确的比例,需要设置对应的gl .viewport

  1. function bindFrambufferAndSetViewport(fb, width, height) {
  2. gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
  3. gl.viewport(0, 0, width, height);
  4. }

然后使用这个方法改变渲染对象,就不容易忘记了。
还有一个需要注意的事情是我们的帧缓冲没有深度缓冲,只有纹理。这意味着没有深度检测, 所以三维就不能正常体现,如果我们绘制三个立方体就会看到这样。
image.png
如果仔细看中间的立方体,会看到 3 个垂直绘制的立方体,一个在后面,一个在中间另一个在前面, 但是我们绘制的三个立方体是相同深度的,观察画布上水平方向的 3 个立方体就会发现他们是正确相交的。 那是因为在帧缓冲中没有深度缓冲,但是画布有。

image.png
想要加深度缓冲就需要创建一个,然后附加到帧缓冲中

  1. // 创建一个深度缓冲
  2. const depthBuffer = gl.createRenderbuffer();
  3. gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
  4. // 设置深度缓冲的大小和targetTexture相同
  5. gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, targetTextureWidth, targetTextureHeight);
  6. gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);

有了这个以后的结果。 输出结果
现在帧缓冲附加了深度缓冲以后内部的立方体也能正确相交了。
image.png
需要特别注意的是WebGL只允许三种附件组合形式。 根据规范 一定能正确运行的附件组合是:

  • COLOR_ATTACHMENT0 = RGBA/UNSIGNED_BYTE texture
  • COLOR_ATTACHMENT0 = RGBA/UNSIGNED_BYTE texture + DEPTH_ATTACHMENT = DEPTH_COMPONENT16 renderbuffer
  • COLOR_ATTACHMENT0 = RGBA/UNSIGNED_BYTE texture + DEPTH_STENCIL_ATTACHMENT = DEPTH_STENCIL renderbuffer

对于其他的组合就需要检查用户系统/gpu/驱动/浏览器的支持情况。 要检查的话需要创建帧缓冲,附加附件,然后调用

  1. var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);

如果状态是 FRAMEBUFFER_COMPLETE 那这种组合就能使用,反之不能使用。 你就需要告诉用户他们不走运或者撤销一些方法。

其实 Canvas 本身就是一个纹理

只是一点小事,浏览器使用上方的技术实现的画布,在背后它们创建了一个颜色纹理, 一个深度缓冲,一个帧缓冲,然后绑定到当前的帧缓冲, 当你调用你的渲染方法时就会绘制到那个纹理上, 然后再用那个纹理将画布渲染到网页中。