WebGL:是否有一种高效的方法仅将图像/画布的一部分上传为纹理?

6
我正在开发一个基于层的2D应用程序,希望使用WebGL进行合成。这些层可能相对于彼此移动,每一帧只有每个层的一个小矩形部分可能会发生改变。但是,该矩形部分的宽度和高度可能不可预测。
我想为每个图层使用一个画布(2D)和一个纹理,并且在每个画布上仅重绘已修改的图层部分,然后简单地将该小区域上传到GPU以更新相应的纹理,之后GPU会为我进行合成。然而,我没有找到有效的方法来将图像的部分上传到纹理的部分。似乎texSubImage2D()可以更新纹理的一部分,但只能采用完整的图像/画布,并且似乎无法指定要使用的图像的矩形区域。
我想了几种方法来解决这个问题,但每种方法都有明显的额外开销:
  • 使用getImageData()+texSubImage2D()仅将更改的部分上传到GPU(在将画布像素数据转换为ImageData时存在开销)
  • 每帧重新上传整个图层画布texImage2D()
  • 或者创建/调整大小的小canvas2D,以适应每个图层修改的正确尺寸,然后使用texSubImage2D()将其发送以更新相关的纹理(存在内存预留开销)
那么,有没有一种方法来为纹理指定图像/画布的一部分?类似于texImagePart2D()texSubImagePart2D(),它们都会接受四个更多参数:sourceXsourceYsourceWidthsourceHeight,以指定要使用的矩形区域?

在大多数嵌入式GPU中,TexSubImage2D API的速度会比较慢。使用TexImage2D本身可能会更好。 - Prabindh
那么,难道重新上传整个纹理是最快的方式,即使只修改了部分内容吗?但我不明白,如果只上传小矩形而不是整个纹理,为什么速度会变慢呢?这是因为修改现有纹理很慢吗? - Jor
1个回答

8

很不幸,目前没有办法上传画布/图像的一部分。

WebGL基于OpenGL ES 2.0,而OpenGL ES 2.0并没有提供这样的功能。OpenGL ES 3.0提供了一种上传源的较小矩形或纹理的一部分的方法,因此可能下一个版本的WebGL会提供该功能。

现在,您可以使用单独的画布来帮助上传。首先将画布大小调整为要上传的部分的大小。

canvasForCopying.width = widthToCopy;
canvasForCopying.height= heightToCopy;

然后将要复制的画布部分复制到用于复制的画布中。
canvasForCopying2DContext.drawImage(
    srcCanvas, srcX, srcY, widthToCopy, heightToCopy,
    0, 0, widthToCopy, heightToCopy);

然后使用该图像上传到您想要的贴图位置。
gl.texSubImage2D(gl.TEXTURE_2D, 0, destX, destY, gl.RGBA, gl.UNSIGNED_BYTE, 
                 canvasForCopying);
getImageData可能会比drawImage慢,因为浏览器需要调用readPixels来获取图像数据,这会阻塞图形管道。而对于drawImage,浏览器不需要这样做。
至于为什么texImage2D有时比texSubImage2D快,这取决于驱动程序/GPU,但显然有时可以使用DMA来实现texImage2D,而texSubImage2D则不能。通过制作新纹理并懒惰地丢弃旧纹理,texImage2D也可以被分段处理,而texSubImage2D则不能。
个人认为不必担心,但如果您想检查,请使用texImage2DtexSubImage2D上传成千上万个纹理的时间(不要只计时一个,因为图形是分段处理的)。如果您的纹理很大,要更新的部分小于纹理的25%,那么您可能会发现texSubImage2D更快。至少这是我上次检查时发现的。大多数当前的驱动程序都已经优化了,如果您调用texSubImage2D并恰好替换了整个内容,它们会在内部调用texImage2D代码。

更新

有几件事情您可以做
  1. 对于图像,您可以使用fetchImageBitmap将图像的一部分加载到ImageBitamp中,然后上传该部分。下面是获取图像一部分的示例

    在下面的示例中,我们调用fetch,然后获取一个Blob,使用该Blob制作只包含图像一部分的ImageBitmap。结果可以传递给texImage2D,但为了简洁起见,示例仅在2D画布中使用它。

fetch('https://i.imgur.com/TSiyiJv.jpg', {mode: 'cors'})
.then((response) => {
  if (!response.ok) {
    throw response;
  }
  return response.blob();
})
.then((blob) => {
  const x = 451;
  const y = 453;
  const width = 147;
  const height = 156;
  return createImageBitmap(blob, x, y, width, height);
}).then((bitmap) => {
  useit(bitmap);
}).catch(function(e) {
  console.error(e);
});

// -- just to show we got a portion of the image

function useit(bitmap) {
  const ctx = document.createElement("canvas").getContext("2d");
  document.body.appendChild(ctx.canvas);
  ctx.drawImage(bitmap, 0, 0);
}

  1. 在WebGL2中,有gl.pixelStorei设置。

    UNPACK_ROW_LENGTH // 源的一行有多少像素 UNPACK_SKIP_ROWS // 从源的开头跳过多少行 UNPACK_SKIP_PIXELS // 从源的左侧跳过多少像素

    因此,使用这三个设置,您可以告诉WebGL2源更宽,但您想要的部分更小。您向texImage2D传递较小的宽度,上述3个设置帮助告诉WebGL如何获取较小的部分以及从哪里开始。


非常感谢您提供如此详细的答案。我想我会使用drawImage/texSubImage2D这个解决方法。最好的祝福。 - Jor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接