通过生成 MipMap 对 Webgl2 R32F 纹理中的值求和
Summing the values in a Webgl2 R32F Texture by generating a MipMap
如果我已将数据渲染到 R32F 纹理(2^18 (~250,000) 个纹素)并且我想计算这些值的总和,是否可以通过要求 gpu 生成 mipmap 来实现?
(想法是最小的 mipmap 级别将具有包含所有原始纹理元素的平均值的单个纹理元素)
我将使用什么 mipmap 设置(钳位等)来生成正确的平均值?
我不太擅长 webgl 体操,我希望能看到一段如何将 1 到 2^18 的数字渲染到 R32F 纹理中,然后对该纹理求和的方法。
对于这个数量的纹理元素,这种方法是否比尝试将纹理元素传输回 cpu 并在 javascript 中执行求和更快?
谢谢!
没有定义用于生成 mipmap 的算法的设置。钳位设置、过滤器设置没有影响。您可以使用 gl.hint
设置是否更喜欢质量而不是性能的提示,但 driver 甚至没有义务注意该标志。此外,每个 driver 都是不同的。生成 mipmap 的结果是用于指纹 WebGL 的差异之一。
在任何情况下,如果您不关心所使用的算法而只想读取生成 mipmap 的结果,那么您只需将最后一个 mip 附加到帧缓冲区并在调用 [=15 后读取像素=].
您可能不会将 1 到 2^18 的所有数字渲染到纹理中,但这并不难。您只需绘制一个 512x512 的四边形。片段着色器可能看起来像这样
#version 300 es
precision highp float;
out vec4 fragColor;
void main() {
float i = 1. + gl_FragCoord.x + gl_FragCoord.y * 512.0;
fragColor = vec4(i, 0, 0, 0);
}
当然,如果您想使用其他尺码,您可以将 512.0
作为制服传入。
渲染为浮点纹理是 WebGL2 的可选功能。台式机支持它,但截至 2018 年大多数移动设备不支持。同样,能够过滤浮点纹理也是一项可选功能,自 2018 年起,大多数移动设备通常也不支持该功能,但在桌面设备上支持。
function main() {
const gl = document.createElement("canvas").getContext("webgl2");
if (!gl) {
alert("need webgl2");
return;
}
{
const ext = gl.getExtension("EXT_color_buffer_float");
if (!ext) {
alert("can not render to floating point textures");
return;
}
}
{
const ext = gl.getExtension("OES_texture_float_linear");
if (!ext) {
alert("can not filter floating point textures");
return;
}
}
// create a framebuffer and attach an R32F 512x512 texture
const numbersFBI = twgl.createFramebufferInfo(gl, [
{ internalFormat: gl.R32F, minMag: gl.NEAREST },
], 512, 512);
const vs = `
#version 300 es
in vec4 position;
void main() {
gl_Position = position;
}
`;
const fillFS = `
#version 300 es
precision highp float;
out vec4 fragColor;
void main() {
float i = 1. + gl_FragCoord.x + gl_FragCoord.y * 512.0;
fragColor = vec4(i, 0, 0, 0);
}
`
// creates a buffer with a single quad that goes from -1 to +1 in the XY plane
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
const quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);
const fillProgramInfo = twgl.createProgramInfo(gl, [vs, fillFS]);
gl.useProgram(fillProgramInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, fillProgramInfo, quadBufferInfo);
// tell webgl to render to our texture 512x512 texture
// calls gl.bindBuffer and gl.viewport
twgl.bindFramebufferInfo(gl, numbersFBI);
// draw 2 triangles (6 vertices)
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
// compute the last mip level
const miplevel = Math.log2(512);
// get the texture twgl created above
const texture = numbersFBI.attachments[0];
// create a framebuffer with the last mip from
// the texture
const readFBI = twgl.createFramebufferInfo(gl, [
{ attachment: texture, level: miplevel },
]);
gl.bindTexture(gl.TEXTURE_2D, texture);
// try each hint to see if there is a difference
['DONT_CARE', 'NICEST', 'FASTEST'].forEach((hint) => {
gl.hint(gl.GENERATE_MIPMAP_HINT, gl[hint]);
gl.generateMipmap(gl.TEXTURE_2D);
// read the result.
const result = new Float32Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.FLOAT, result);
log('mip generation hint:', hint);
log('average:', result[0]);
log('average * count:', result[0] * 512 * 512);
log(' ');
});
function log(...args) {
const elem = document.createElement('pre');
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
}
main();
pre {margin: 0}
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
注意我用了twgl.js to make the code less verbose. If you don't know how to make a framebuffer and attach textures or how to setup buffers and attributes, compile shaders, and set uniforms then you're asking way too broad a question and I suggest you go read some tutorials.
让我指出如何不能保证此方法比其他方法更快。首先取决于 driver。 driver 有可能在软件中做到这一点(尽管不太可能)。
一个明显的加速是使用 RGBAF32 并让代码一次处理 4 个值,然后在最后读取所有 4 个通道(R、G、B、A)并对它们求和。
此外,由于您只关心最后一个 1x1 像素 mip,因此您要求代码渲染比更直接的方法多得多的像素。实际上,您只需要渲染 1 个像素,即结果。但是对于这个 2^18 值的示例,它是一个 512x512 纹理,这意味着一个 256x526、一个 128x128、一个 64x64、一个 32x32、一个 16x16、一个 8x8、一个 4x4 和一个 2x2 mip 都被分配和计算,这可以说是浪费时间。事实上,规范说所有 mip 都是从第一个 mip 生成的。当然,driver 可以自由地走捷径,并且很可能从 mip N-1 生成 mip N,因为结果将是相似的,但这不是规范定义的方式。但是,即使从前一个生成一个 mip,也会计算出您不关心的 87380 个值。
我只是猜测用比 2x2 更大的卡盘生成更快。同时还有纹理缓存,如果我理解正确的话,它们通常会缓存纹理的矩形部分,以便从 mip 中快速读取 4 个值。当您有纹理缓存未命中时,它真的会降低您的性能。所以,如果你的块太大,你可能会有很多缓存未命中。您基本上必须进行测试,并且每个 GPU 可能会显示不同的性能特征。
另一个加速方法是考虑使用多个绘图缓冲区,然后您可以在每个片段着色器迭代中写入 16 到 32 个值,而不是仅仅 4 个。
如果我已将数据渲染到 R32F 纹理(2^18 (~250,000) 个纹素)并且我想计算这些值的总和,是否可以通过要求 gpu 生成 mipmap 来实现?
(想法是最小的 mipmap 级别将具有包含所有原始纹理元素的平均值的单个纹理元素)
我将使用什么 mipmap 设置(钳位等)来生成正确的平均值?
我不太擅长 webgl 体操,我希望能看到一段如何将 1 到 2^18 的数字渲染到 R32F 纹理中,然后对该纹理求和的方法。
对于这个数量的纹理元素,这种方法是否比尝试将纹理元素传输回 cpu 并在 javascript 中执行求和更快?
谢谢!
没有定义用于生成 mipmap 的算法的设置。钳位设置、过滤器设置没有影响。您可以使用 gl.hint
设置是否更喜欢质量而不是性能的提示,但 driver 甚至没有义务注意该标志。此外,每个 driver 都是不同的。生成 mipmap 的结果是用于指纹 WebGL 的差异之一。
在任何情况下,如果您不关心所使用的算法而只想读取生成 mipmap 的结果,那么您只需将最后一个 mip 附加到帧缓冲区并在调用 [=15 后读取像素=].
您可能不会将 1 到 2^18 的所有数字渲染到纹理中,但这并不难。您只需绘制一个 512x512 的四边形。片段着色器可能看起来像这样
#version 300 es
precision highp float;
out vec4 fragColor;
void main() {
float i = 1. + gl_FragCoord.x + gl_FragCoord.y * 512.0;
fragColor = vec4(i, 0, 0, 0);
}
当然,如果您想使用其他尺码,您可以将 512.0
作为制服传入。
渲染为浮点纹理是 WebGL2 的可选功能。台式机支持它,但截至 2018 年大多数移动设备不支持。同样,能够过滤浮点纹理也是一项可选功能,自 2018 年起,大多数移动设备通常也不支持该功能,但在桌面设备上支持。
function main() {
const gl = document.createElement("canvas").getContext("webgl2");
if (!gl) {
alert("need webgl2");
return;
}
{
const ext = gl.getExtension("EXT_color_buffer_float");
if (!ext) {
alert("can not render to floating point textures");
return;
}
}
{
const ext = gl.getExtension("OES_texture_float_linear");
if (!ext) {
alert("can not filter floating point textures");
return;
}
}
// create a framebuffer and attach an R32F 512x512 texture
const numbersFBI = twgl.createFramebufferInfo(gl, [
{ internalFormat: gl.R32F, minMag: gl.NEAREST },
], 512, 512);
const vs = `
#version 300 es
in vec4 position;
void main() {
gl_Position = position;
}
`;
const fillFS = `
#version 300 es
precision highp float;
out vec4 fragColor;
void main() {
float i = 1. + gl_FragCoord.x + gl_FragCoord.y * 512.0;
fragColor = vec4(i, 0, 0, 0);
}
`
// creates a buffer with a single quad that goes from -1 to +1 in the XY plane
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
const quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);
const fillProgramInfo = twgl.createProgramInfo(gl, [vs, fillFS]);
gl.useProgram(fillProgramInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, fillProgramInfo, quadBufferInfo);
// tell webgl to render to our texture 512x512 texture
// calls gl.bindBuffer and gl.viewport
twgl.bindFramebufferInfo(gl, numbersFBI);
// draw 2 triangles (6 vertices)
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
// compute the last mip level
const miplevel = Math.log2(512);
// get the texture twgl created above
const texture = numbersFBI.attachments[0];
// create a framebuffer with the last mip from
// the texture
const readFBI = twgl.createFramebufferInfo(gl, [
{ attachment: texture, level: miplevel },
]);
gl.bindTexture(gl.TEXTURE_2D, texture);
// try each hint to see if there is a difference
['DONT_CARE', 'NICEST', 'FASTEST'].forEach((hint) => {
gl.hint(gl.GENERATE_MIPMAP_HINT, gl[hint]);
gl.generateMipmap(gl.TEXTURE_2D);
// read the result.
const result = new Float32Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.FLOAT, result);
log('mip generation hint:', hint);
log('average:', result[0]);
log('average * count:', result[0] * 512 * 512);
log(' ');
});
function log(...args) {
const elem = document.createElement('pre');
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
}
main();
pre {margin: 0}
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
注意我用了twgl.js to make the code less verbose. If you don't know how to make a framebuffer and attach textures or how to setup buffers and attributes, compile shaders, and set uniforms then you're asking way too broad a question and I suggest you go read some tutorials.
让我指出如何不能保证此方法比其他方法更快。首先取决于 driver。 driver 有可能在软件中做到这一点(尽管不太可能)。
一个明显的加速是使用 RGBAF32 并让代码一次处理 4 个值,然后在最后读取所有 4 个通道(R、G、B、A)并对它们求和。
此外,由于您只关心最后一个 1x1 像素 mip,因此您要求代码渲染比更直接的方法多得多的像素。实际上,您只需要渲染 1 个像素,即结果。但是对于这个 2^18 值的示例,它是一个 512x512 纹理,这意味着一个 256x526、一个 128x128、一个 64x64、一个 32x32、一个 16x16、一个 8x8、一个 4x4 和一个 2x2 mip 都被分配和计算,这可以说是浪费时间。事实上,规范说所有 mip 都是从第一个 mip 生成的。当然,driver 可以自由地走捷径,并且很可能从 mip N-1 生成 mip N,因为结果将是相似的,但这不是规范定义的方式。但是,即使从前一个生成一个 mip,也会计算出您不关心的 87380 个值。
我只是猜测用比 2x2 更大的卡盘生成更快。同时还有纹理缓存,如果我理解正确的话,它们通常会缓存纹理的矩形部分,以便从 mip 中快速读取 4 个值。当您有纹理缓存未命中时,它真的会降低您的性能。所以,如果你的块太大,你可能会有很多缓存未命中。您基本上必须进行测试,并且每个 GPU 可能会显示不同的性能特征。
另一个加速方法是考虑使用多个绘图缓冲区,然后您可以在每个片段着色器迭代中写入 16 到 32 个值,而不是仅仅 4 个。