webgl笔记(四) ——— 离屏渲染的使用

1,666 阅读13分钟

摘要

上一篇文章介绍了webgl里三角形的绘制,以及利用纹理如何绘制图片。这篇文章将引入另一个webgl的功能——离屏渲染,以及利用离屏渲染做拾取判断。

离屏渲染

  • 首先,canvas之所以能绘制出图片,本质是执行了webgl程序后,往缓存中储存了颜色深度等信息,再把这部分数据渲染到页面的canvas里,而这个缓存,实际上是一个叫帧缓冲的对象(FBO, FramBuffer Object),这个帧缓冲对象会默认被绘制到页面的canvas中;
  • 而这个帧缓冲,除了webgl默认自带的这个,也可以自己另外创建,并且可以在其上执行shader程序,将gpu的运行结果储存进去,同时这个自己创建的帧缓冲,并不会渲染到页面上,所以也叫离屏渲染;
  • 创建帧缓冲的方法就是gl.createFramebuffer

帧缓冲对象

  • 帧缓冲一般包括以下三个关联对象(attachments)

    • 颜色关联对象 (color attachment)
    • 深度关联对象 (depth attachment)
    • 模版关联对象 (stencil attachment)
  • color attachment储存的是颜色相关的信息,可以理解为整个帧缓存对象就是一张纹理,这个纹理的颜色就保存在color attachment

  • depth attachment储存的是深度相关的信息,其影响绘制图案的层级,以及绘制阴影的绘制,要想深度关联对象起效,需要开启深度测试

gl.enable(gl.DEPTH_TEST);
  • stencil attachment,模版关联对象,这个模版其实理解为筛子更贴切,其控制,经过颜色关联以及深度关联后生成的图案,有哪些可以被绘制到帧缓冲里面,简单可以理解为,模版里储存了0或1的数据,与其余关联对象重叠,0的不绘制,1的绘制(参考)
  • 这里我们只谈论颜色关联对象,因为他最常用到;

帧缓冲对象有什么用

  • 假设我们要实现一个功能,在鼠标滑过canvas时,显示局部的预览图,我们可以怎么实现?
  • 一种方法是,通过gl.readPixels读取canvas上某一块图片范围的像素,然后将这个像素集合生成一个图片,再把它导入图片中,最后渲染出来;
  • 另外一种方法就是离屏渲染,通过计算鼠标与图片的相对位置,可以利用离屏渲染,在帧缓冲中绘制局部图像,最后将与之绑定的纹理叠加渲染到原图之上,就实现了在原图的某个区域内显示局部放大了的图片

局部渲染实现

  • shader沿用上一篇笔记中的代码,如下
  <script id="vertex-shader-2d" type="notjs">
    attribute vec2 a_position;
    uniform mat4 u_proj;
    attribute vec2 a_texCoord;
    varying vec2 v_texCoord;

    void main(void) {
      gl_Position = u_proj * vec4(a_position, 0, 1.0);
      v_texCoord = a_texCoord;
    }
  </script>
  <script id="fragment-shader-2d" type="notjs">
    precision mediump float;
    varying vec2 v_texCoord;
    uniform sampler2D u_image; 

    void main() {
      vec4 texture = texture2D(u_image, v_texCoord);
      gl_FragColor = texture;
    }
  </script>
  • 绘制图片等流程都与上一篇笔记一致,先创建一个纹理,再把纹理和shader中的纹理变量关联,然后将图片赋予该纹理,最后确定坐标,运行shader
  • 这里主要记录一下,如何创建一个帧缓冲对象,并将空纹理与之绑定,最终将图片绘制到该空纹理中
  • 首先创建一个帧缓冲对象
const framebuffer = gl.createFramebuffer();
  • 绑定纹理
function attachTexture(width, height, framebuffer, level) {
    // 创建纹理
    const texture = gl.createTexture();
    // 绑定为当前操作对象
    gl.bindTexture(gl.TEXTURE_2D, texture);
    // 纹理参数
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    // 设置纹理的存储格式,长宽等规格
    gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, width, height, level, gl.RGBA, gl.UNSIGNED_BYTE, null);
    // 设置当前的缓冲操作对象为自定义的帧缓冲对象
    // 如果这里framebuffer = null,就相当于当前操作的是绘制缓冲,将被输出到画面之上
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    // 绑定纹理到当前的缓冲对象上
    // COLOR_ATTACHMENT0是颜色关联对象
    // DEPTH_ATTACHMENT是深度关联对象
    // STENCIL_ATTACHMENT是模版关联对象
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, level);

    return texture;
}

const aTexture = attachTexture(gl.canvas.width, gl.canvas.height, framebuffer, 0);
  • 注意,这里绑定纹理的时候,顺便生成了一个新的纹理,纹理的长宽是canvas的长宽,这里主要是为了绘制的时候能沿用绘制的坐标,实际你可以设置任意的长宽,但需要注意将该纹理重新绘制到canvas商的时候,有可能需要进行长宽的变换
  • 另外,纹理越大,存储的信息就越多,为了绘制放大图片的时候,能得到一个较为清晰图案,可以适当的等比放大新建纹理的长宽,放大后,绘制坐标需相应改变,这里为了简便,使帧缓冲纹理与绘制纹理大小一致
  • 参数level是用来设置纹理细致程度的,普通纹理给0即可
  • 定义绘制函数
// 预览框的宽
const previewW = 60;
// 预览框的高
const previewH = 120;

const { clientWidth, clientHeight } = canvas;

function drawImage (framebuffer, array, texCoord) {
    // 绑定当前操作的帧缓冲
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    // 绘制之前需要设置一次viewport
    if (framebuffer) {
      gl.viewport(0, 0, previewW, previewH);
      gl.clearColor(0, 0, 0, 1.0);
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    } else {
      gl.viewport(0, 0, clientWidth, clientHeight);
    }
    // 顶点数据
    const posData = new Float32Array(array);
    const buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, posData, gl.STATIC_DRAW);
    const aPos = gl.getAttribLocation(program, 'a_position');
    gl.enableVertexAttribArray(aPos);
    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

    // 纹理坐标数据
    const texData = new Float32Array(texCoord);
    const texBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, texData, gl.STATIC_DRAW);
    const aTex = gl.getAttribLocation(program, 'a_texCoord');
    gl.enableVertexAttribArray(aTex);
    gl.vertexAttribPointer(aTex, 2, gl.FLOAT, false, 0, 0);

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
  • 这里我们定义了一个绘制函数,无论是原图的渲染,还是局部图的放大,都将调用这个函数,他接收的参数有
    • framebuffer: 绘制在哪个缓冲帧上,如果为null,则会直接渲染在canvas上
    • array: 绘制在画布上的坐标
    • texCoord: 绘制图案的纹理坐标
  • 注意到这里有个特殊处理gl.viewport,该函数可以自动将绘制坐标和实际展示的坐标进行转换,省去了自己计算的麻烦;简单的说,就是在我们生产局部预览图案的纹理时,我们先按照canvas的宽高进行绘制,然后利用gl.viewport,可以将绘制的图案缩小到我们定义的纹理宽高上进行保存,从而生成我们期望得到的纹理
  • 接下来我们监听鼠标的移动,进行图片的绘制
// 预览倍数
const previewScale = 1.2;

// 预览图在原图上的实际尺寸
const previewSize = {
  width: previewW / previewScale,
  height: previewH / previewScale
};

// 绘制到帧缓冲上时,使用的绘制坐标
// 直接按canvas的宽高进行绘制,后面通过viewport会将图案变换成对应的纹理宽高
const previewDrawArr = [
  0, 0,
  clientWidth, 0,
  0, clientHeight,
  clientWidth, clientHeight
];

// 绘制局部预览图时使用的纹理坐标
const previewDrawTexCoord = [
  0, 1,
  1, 1,
  0, 0,
  1, 0,
];

canvas.addEventListener('mousemove', (e) => {
    // img为image元素,存储图片信息
    if (!img || !img.src) return;
    // 获取鼠标在屏幕上的点
    let { clientX, clientY } = e;
    // 限定坐标的边界
    if (clientX - previewSize.width / 2 < 0) clientX = previewSize.width / 2;
    else if (clientX + previewSize.width / 2 > imgW) clientX = imgW - previewSize.width / 2;
    if (clientY - previewSize.height / 2 < 0) clientY = previewSize.height / 2;
    else if (clientY + previewSize.height / 2 > imgH) clientY = imgH - previewSize.height / 2;
    // 将鼠标坐标转换为纹理坐标
    // 纹理坐标范围是0~1,所以要除以宽高
    // The texture coordinate (0, 0) does not refer to the top-left or bottom-left pixel of a texture, it refers to the first pixel of a texture. Similarly (1, 1) does not refer to the bottom-right or top-right pixel but rather to the last pixel
    // https://stackoverflow.com/questions/48124001/opengl-texture-coordinates-are-opposite-when-reading-from-a-framebuffer-object
    const previewTexCoord = [
      (clientX - previewSize.width / 2) / imgW, (clientY - previewSize.height / 2) / imgH,
      (clientX + previewSize.width / 2) / imgW, (clientY - previewSize.height / 2) / imgH,
      (clientX - previewSize.width / 2) / imgW, (clientY + previewSize.height / 2) / imgH,
      (clientX + previewSize.width / 2) / imgW, (clientY + previewSize.height / 2) / imgH,
    ];
    
    // 使用原图
    gl.bindTexture(gl.TEXTURE_2D, texture);
    // 截取原图中的一部分生成纹理
    drawImage(framebuffer, previewDrawArr, previewTexCoord);
    // 先将原图绘制在canvas上
    drawImage(null, positionArr, texCoord);
    // 切换成帧缓冲绑定的纹理
    gl.bindTexture(gl.TEXTURE_2D, aTexture);
    // 将帧缓冲中的图片绘制到canvas上,实现局部预览
    drawImage(null, previewPosArr, previewDrawTexCoord);
});
  • 在监听鼠标移动的过程中,我们限定了坐标的边界,这是因为如果不限制,算出来的纹理坐标将会存在负数,而如果是负数的纹理坐标,绘制过程中,将会对边界进行拉伸或重复处理,具体的处理与我们定义纹理时,通过gl.texParameteri设置的纹理参数有关
  • 另外,注意我们定义局部预览图案对应的纹理坐标时,截取坐标与绘制坐标的区别;按上一章的内容,因为图片的储存格式是从上往下,从左往右,所以图案的y轴是从上往下的,但一旦我们通过绘制将图案保存为纹理之后,这个纹理内的坐标将按照webgl内定义的坐标进行保存,即左下角为原点,这也是为什么previewTexCoord是按照y轴从上往下为正向进行取值,而previewDrawTexCoord是按照y轴从下往上为正向进行取值的,参考
  • 接下来运行一下看看效果 Kapture 2022-12-23 at 17.49.32.gif
  • demo源码

拾取判断

  • 接下来,我们来实现拾取判断,在实现该判断前,我们先要有一个方法,来分别给每个图像分配一个唯一id,由于在webgl上,所有数据都将被绘制成像素,那我们也可以利用一个独特的像素值来标记这个图案
  • 这里为了简便,我们考虑一个像素有rgba4个值,这里我们令a恒定为1,以区分背景色,同时rgb各等于0和1,这样我们就可以标记8个图形,足够演示了
  • 当然你也可以通过更复杂的算法,去计算每个图像独特的像素值,由于一个像素值可以通过8位字节表示,理论上可以区分2的32次方(rgba四个数,共32个字节)种组合
  • 接下来改造一下上面的程序,先新增一个fragment shader,用来给帧缓冲绘制包含id信息的颜色数据
<script id="fragment-shader-2d-pick" type="notjs">
    precision mediump float;
    uniform vec4 u_color;

    void main() {
      gl_FragColor = u_color;
    }
</script>

// 新增一个绘制program
const pickFragShaderSource = window.document.querySelector('#fragment-shader-2d-pick').innerHTML;
const pickFragShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(pickFragShader, pickFragShaderSource);
gl.compileShader(pickFragShader);
const pickProgram = gl.createProgram();
// vertexShader就是原来的顶点着色器
gl.attachShader(pickProgram, vertexShader);
gl.attachShader(pickProgram, pickFragShader);
gl.linkProgram(pickProgram);
  • 接着,允许选择多张图片,并按固定大小展示在canvas的四个角
<input id="input" type="file" accept="image/webp,image/png,image/jpeg" multiple></input>

input.addEventListener('change', (e) => {
  const files = e.target.files;
  if (files && files.length) {
    Array.prototype.slice.call(files, 0, 4).forEach((file, index) => {
      // 最多渲染4张图
      const _URL = window.URL || window.webkitURL;
      const url = _URL.createObjectURL(file);
      const { name } = file;
      imgList.splice(0, imgList.length);
      const img = new Image();
      img.onload = () => onImgLoad(img, index, name);
      img.src = url;
    });
  }
});

function onImgLoad (img, index, name) {
  // 区分4个角的标记,也是后续id构造参数
  const flag1 = index % 2 === 0;
  const flag2 = index < 2;
  
  // 四角坐标
  const left = flag1 ? 0 : clientWidth / 2 + 30;
  const right = flag1 ? clientWidth / 2 - 30 : clientWidth;
  const top = flag2 ? 0 : clientHeight / 2  + 30;
  const bottom = flag2 ? clientHeight / 2 - 30 : clientHeight;

  const positionArr = [
    left, top,
    right, top,
    left, bottom,
    right, bottom
  ];
  
  // 简单区分一下即可,4个数分别作为帧缓冲的rgba值
  const uid = [flag1 ? 255 : 0, flag2 ? 255 : 0, 0, 255];
  
  // 记录下来,后续用
  imgList.push({
    src: img,
    posArr: positionArr,
    uid,
    name
  });
  
  // 这里的program是使用了texture的fragment shader,不是用于帧缓冲绘制的fragment shader
  gl.useProgram(program);
  const texture = createTexture(img, 0);
  drawImage(positionArr, texCoord);
  // 绘制缓冲帧
  drawPick(framebuffer, positionArr, uid);
}
  • 接着设置帧缓冲如何绘制
function drawPick (framebuffer, array, colorArr) {
  // 保证当前使用的是pickProgram
  gl.useProgram(pickProgram);
  // 且绘制到帧缓冲中
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
  // 顶点数据
  const posData = new Float32Array(array);
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, posData, gl.STATIC_DRAW);
  const aPos = gl.getAttribLocation(pickProgram, 'a_position');
  gl.enableVertexAttribArray(aPos);
  gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

  // 颜色uid
  // 使用Uint8Array的目的是为了让输入的数值与后续readpixel出来的数值是一样的,否则,输入会默认已0~1的浮点数输入,输出是0~255的Uint8Array,无法直接对比
  const uColor = gl.getUniformLocation(pickProgram, 'u_color');
  gl.uniform4fv(uColor, new Uint8Array(colorArr));

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
  • 最后与之前预览例子类似,监听鼠标移动,判断当前鼠标下的是哪一张图片
function drawImage (array, texCoord) {
  // 绘制前,先确保绘制到canvas上,且使用的着色器是使用texture的
  gl.useProgram(program);
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  ...
}

canvas.addEventListener('mousemove', (e) => {
    if (!imgList.length) return;
    let { clientX, clientY } = e;
    render({ pick: true });
    const pixels = new Uint8Array(4);
    gl.readPixels(clientX, clientHeight - clientY, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
    const floatArr = Array.from(pixels)
    // 对比图片uid,判断当前鼠标停留在哪张图上
    // floatArr[3]区分背景色
    const pick = imgList.filter(floatArr[3] && img => img.uid[0] === floatArr[0] && img.uid[1] === floatArr[1])[0];
    if (pick) console.log(pick.name);
});
  • 看看效果 Kapture 2022-12-29 at 11.47.22.gif

  • 这里需要注意的是,由于使用了两个片元着色器,生成了两个program,需要对两个着色器里的所有变量都做一次赋值,且赋值与绘制的时候,要区分好使用哪个program,否则会报错,另外也需要区分好是绘制到cnavas上还是离线屏上,着色器与缓冲是相互独立的,只是这里恰好一个一个缓冲使用一个着色器

  • 代码参考

平移与旋转矩阵

  • 之前文章里我们推导过坐标变换的矩阵,这里我们再看一下平移和旋转矩阵是怎么样的

平移矩阵

  • 假设平移前坐标为(X, Y),平移后得到的坐标为(X', Y'),可得

X' - X = dx

Y' - Y = dy

X + dx = X‘

Y + dy = Y’

  • 平移前后的坐标可以表示为向量A,A‘
[XY1]\begin{bmatrix} X & Y & 1 \\ \end{bmatrix}
[XY1]\begin{bmatrix} X' & Y' & 1 \\ \end{bmatrix}
  • 构造一个矩阵,使A乘矩阵等于A’,这个矩阵就是平移矩阵
[XY1][100010dxdy1]=[XY1]\begin{bmatrix} X & Y & 1 \\ \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ dx & dy & 1 \\ \end{bmatrix} = \begin{bmatrix} X' & Y' & 1 \\ \end{bmatrix}

旋转矩阵

  • 设某点与原点连线和X轴夹角为b度,以原点为圆心,逆时针转过a度 , 原点与该点连线长度为R, (X, Y)为变换前坐标, (X', Y')为变换后坐标,则有

X = R * cos(b)

Y = R * sin(b)

X' = R * cos(a + b) = R * cosa * cosb − R * sina * sinb

即:X' = X * cosa − Y * sina;

Y' = R * sin(a + b) = R * sina * cosb + R * cosa * sinb

即:Y' = X * sina + Y * cosa;

  • 同理坐标可用向量代替,构造一个矩阵使上述等式成立,有
[XY1][cosAcosA0sinAcosA0001]=[XY1]\begin{bmatrix} X & Y & 1 \\ \end{bmatrix} * \begin{bmatrix} cosA & cosA & 0 \\ -sinA & cosA & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} = \begin{bmatrix} X' & Y' & 1 \\ \end{bmatrix}

拾取优化

  • 基于上述讨论,我们可以加入点击移动的功能,先改造一下顶点着色器,加入平移矩阵
<script id="vertex-shader-2d" type="notjs">
    attribute vec2 a_position;
    uniform mat4 u_proj;
    // 平移矩阵
    uniform mat4 u_translate;
    attribute vec2 a_texCoord;
    varying vec2 v_texCoord;

    void main(void) {
      // webgl矩阵乘法从右往左
      // 顺序不能换,先平移后变换坐标,如果加入了旋转,则先旋转,后平移,最后坐标变换,顺序也不能变
      gl_Position = u_proj * u_translate * vec4(a_position, 0, 1.0);
      v_texCoord = a_texCoord;
    }
</script>

const createTranslateMat = (tx, ty) => { // 创建平移矩阵
  return [
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      tx, ty, 0, 1
  ]
};
  • 修改绘制的数据结构,新增平移数据
function onImgLoad (img, index, name) {
  ...
  // 新增originClick,记录点击的坐标
  // 新增translate,结合移动坐标计算实际移动距离
  const imgInfo = {
    src: img,
    posArr: positionArr,
    uid,
    name,
    originClick: { x: 0, y: 0 },
    translate: { x: 0, y: 0 }
  };

  imgList.push(imgInfo);
  ...
}
  • 修改绘制函数,主要添加平移矩阵的设置
function drawImage (info) {
  const array = info.posArr;
  ...
  // 平移矩阵
  const transMat = createTranslateMat(info.translate.x, info.translate.y);
  const uTrans = gl.getUniformLocation(program, 'u_translate');
  gl.uniformMatrix4fv(uTrans, false, transMat);

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

function drawPick (info) {
  const array = info.posArr;
  const colorArr = info.uid;
  ...
  // 平移矩阵
  const transMat = createTranslateMat(info.translate.x, info.translate.y);
  // 注意这里从对应的program获取地址
  const uTrans = gl.getUniformLocation(pickProgram, 'u_translate');
  gl.uniformMatrix4fv(uTrans, false, transMat);

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

function render (opt = { pick: true, canvas: true }) {
  // 总的绘制函数
  // 可分别绘制canvas或缓冲帧
  imgList.forEach((img) => {
    if (opt.canvas) {
      gl.useProgram(program);
      const texture = createTexture(img.src, 0);
      drawImage(img.posArr, texCoord);
    }
    if (opt.pick) {
      drawPick(framebuffer, img.posArr, img.uid);
    }
  });
}
  • 最后修改监听函数,新增点击和松开鼠标的监听,在点击时判断选择哪个图片,松开时重置状态,移动时同步修改平移矩阵,重新绘制图片
// 用于记录选中哪个图片,由于图片信息是一个对象,这里获取到之后实际是引用地址,直接修改对原数据结构也会生效
let pick = undefined;

canvas.addEventListener('mousedown', (e) => {
  if (!imgList.length) return;
  let { clientX, clientY } = e;
  // 选中不会产生移动,只判断离线屏即可
  render({ pick: true });
  const pixels = new Uint8Array(4);
  // 判断鼠标点击选中的图片
  gl.readPixels(clientX, clientHeight - clientY, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
  const floatArr = Array.from(pixels)
  pick = imgList.filter(img => floatArr[3] && img.uid[0] === floatArr[0] && img.uid[1] === floatArr[1])[0];
  if (pick) {
    console.log('pick up:', pick.name);
    // 再次点击时,可能已经产生移动,需要将已产生移动的距离扣减,下次再发生移动时,才会从所在位置开始移动,否则会回到原点再移动
    // 而首次点击,由于初始值是0,所以也不会有影响
    pick.originClick = { x: clientX - pick.translate.x, y: clientY - pick.translate.y };
  }
});

canvas.addEventListener('mousemove', (e) => {
  // 有选中才往下走
  if (!pick) return;
  let { clientX, clientY } = e;
  const dx = clientX - pick.originClick.x;
  const dy = clientY - pick.originClick.y;
  pick.translate = { x: dx, y: dy };
  // 移动不需要判断是否选中,只更新canvas即可
  render({ canvas: true });
});

canvas.addEventListener('mouseup', (e) => {
  // 松开时清除选择
  pick = undefined;
});

  • 另外还需要特别注意,由于这次我们的图片可以拖动,导致缓冲帧上的数据是被污染的,即拖动过程中,经过的像素点都被赋予了图片的uid颜色,而未被清除;根据webgl的文档,canvas上的图案,只管绘制,不管保存,并不会保留上一次绘制的数据,每一次draw都会被清除,清除时机由浏览器控制,所以无需手动清除(参考),实践也确实如此,但离线屏上的数据实践下来,发现是会被保留下来的,这点并没有找到相关文档说明,只能说实验结果如此,所以在离线屏的绘制之前,加上清除操作;
function clearBuffer (framebuffer) {
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
  gl.clearColor(0, 0, 0, 0.0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}

function render (opt = { pick: true, canvas: true }) {
  clearBuffer(framebuffer);
  ...
}
  • 看看最后的效果

Kapture 2022-12-29 at 15.55.04.gif

写在最后

  • 通过离屏渲染,可以对图案的整体或局部进行加工,并且随着功能的增加,需要用到不止一个program以及framebuffer,如何管理以及控制在对的时候,用对的program和framebuffer,是一个值得关注的问题,同时如果在开发过程中,发现canvas没有像预料的渲染,也可以先排查是不是这方面的问题;
  • 最后,我们现在点击判断击中哪张图片的时候,是根据是否击中整个图片宽高内的四边形区域来进行判断的,如果这个图片是png格式,且他周围存在透明的像素,图案是个不规则的图形,是否能判断击中不透明区域才算选中这个图案呢,答案当然是可以的,只需要在离线屏渲染的时候,读取原texture,判断当前点是否为透明,再进行颜色绘制即可,具体流程就不展开了,有兴趣的朋友可以自己试试,这里附上实现了的参考

Kapture 2022-12-29 at 17.13.06.gif