几何图形对 WebGL 中最终纹理输出的影响是什么?

What's the effect of geometry on the final texture output in WebGL?

针对我的困惑更新了更多解释

(非图形开发者想象的渲染过程是这样的!)

我指定用两个三角形绘制一个 2x2 正方形。我不会再谈论三角形了。广场好多了。假设这个正方形被画成一块。

我没有为我的绘图指定任何单位。在我的代码中,我唯一做类似事情的地方是:canvas 大小(在我的例子中设置为 1x1)和视口(我总是将其设置为我的输出纹理的尺寸)。

然后我调用 draw()。

发生了什么:无论我的纹理大小(1x1 或 10000x10000)如何,我所有的纹素都填充了我从碎片着色器返回的数据(颜色)。这每次都很完美。

所以现在我想向自己解释一下:

我什至没有谈论我的片段着色器被调用了多少次,因为这无关紧要。无论如何,结果都是确定性的。

所以考虑到我从来没有 运行 进入第二种和第三种情况,在这种情况下,我的纹素中的值不是我所期望的,我能得出的唯一结论是 GPU 尝试的整个假设渲染像素实际上是错误的。当我为它分配一个输出纹理时(它应该一直延伸到我的 2x2 正方形上)然后 GPU 会很乐意为每个纹素调用我的碎片着色器。沿着这条线的某个地方,像素也会变色。

但上述疯狂的解释也无法回答为什么如果我将我的几何图形拉伸为 1x1 或 4x4 而不是 2x2,我最终在我的纹素中没有值或不正确的值。

希望上面关于 GPU 着色过程的精彩叙述能为您提供线索,让您知道我在哪里弄错了。

原文Post:

我们使用 WebGL 进行一般计算。因此,我们创建一个矩形并在其中绘制 2 个三角形。最终我们想要的是映射到这个几何体的纹理中的数据。

我不明白的是,如果我将矩形从 (-1,-1):(1,1) 更改为说 (-0.5,-0.5):(0.5,0.5),数据会突然从绑定到帧缓冲区的纹理中删除。

如果有人能让我理解相关性,我将不胜感激。输出纹理的实际尺寸唯一起作用的地方是调用 viewPort()readPixels().

下面是相关的代码片段,供您查看我在做什么:

  ... // canvas is created with size: 1x1
  ... // context attributes passed to canvas.getContext()
  contextAttributes = {
    alpha: false,
    depth: false,
    antialias: false,
    stencil: false,
    preserveDrawingBuffer: false,
    premultipliedAlpha: false,
    failIfMajorPerformanceCaveat: true
  };
  ... // default geometry
    // Sets of x,y,z (for rectangle) and s,t coordinates (for texture)
    return new Float32Array([
      -1.0, 1.0,  0.0, 0.0, 1.0,  // upper left
      -1.0, -1.0, 0.0, 0.0, 0.0,  // lower left
      1.0,  1.0,  0.0, 1.0, 1.0,  // upper right
      1.0,  -1.0, 0.0, 1.0, 0.0 // lower right
    ]);  
... 
    const geometry = this.createDefaultGeometry();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, geometry, gl.STATIC_DRAW);
... // binding to the vertex shader attribs
    gl.vertexAttribPointer(positionHandle, 3, gl.FLOAT, false, 20, 0);
    gl.vertexAttribPointer(textureCoordHandle, 2, gl.FLOAT, false, 20, 12);
    gl.enableVertexAttribArray(positionHandle);
    gl.enableVertexAttribArray(textureCoordHandle);
... // setting up framebuffer; I set the viewport to output texture dimensions (I think this is absolutely needed but not sure)
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
    gl.framebufferTexture2D(
        gl.FRAMEBUFFER,        // The target is always a FRAMEBUFFER.
        gl.COLOR_ATTACHMENT0,  // We are providing the color buffer.
        gl.TEXTURE_2D,         // This is a 2D image texture.
        texture,               // The texture.
        0);                    // 0, we aren't using MIPMAPs
    gl.viewport(0, 0, width, height);
... // reading from output texture
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.framebufferTexture2D(
        gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture,
        0);
    gl.readPixels(0, 0, width, height, gl.FLOAT, gl.RED, buffer);

新答案

我又说了同样的话(第三次?)

从下方复制

WebGL is destination based. That means it's going to iterate over the pixels of the line/point/triangle it's drawing and for each point call the fragment shader and ask 'what value should I store here`?

基于目的地。它将精确地绘制每个像素一次。对于那个像素,它将询问 "what color should I make this"

基于目标的循环

for (let i = start; i < end; ++i) {
  fragmentShaderFunction();  // must set gl_FragColor
  destinationTextureOrCanvas[i] = gl_FragColor;

您可以在上面的循环中看到没有设置任何随机目的地。没有两次设置目的地的任何部分。它只是从 startend 运行 并且在开始和结束之间的目的地中的每个像素恰好一次询问它应该使该像素成为什么颜色。

如何设置开始和结束?同样,为了简单起见,我们假设一个 200x1 的纹理,这样我们就可以忽略 Y。它的工作原理如下

 vertexShaderFunction(); // must set gl_Position
 const start = clipspaceToArrayspaceViaViewport(viewport, gl_Position.x);

 vertexShaderFunction(); // must set gl_Position
 const end = clipspaceToArrayspaceViaViewport(viewport, gl_Position.x);

 for (let i = start; i < end; ++i) {
   fragmentShaderFunction();  // must set gl_FragColor
   texture[i] = gl_FragColor;
 }

见下文 clipspaceToArrayspaceViaViewport

什么是viewportviewport 是调用`gl.viewport(x, y, width, height)

时设置的

所以,设置gl_Position.x为-1和+1,viewport.x为0和viewport.width = 200(纹理的宽度)然后start将是0, end 将是 200

gl_Position.x设置为.25和.75,viewport.x设置为0并且viewport.width = 200(纹理的宽度)。 start 将是 125,end 将是 175

老实说,我觉得这个答案会让您误入歧途。远没有这么复杂。您无需了解任何这些内容即可使用 WebGL IMO。

简单的答案是

  1. 您将gl.viewport设置为您要在目的地影响的子矩形(canvas或纹理无关紧要)

  2. 你制作了一个顶点着色器,它以某种方式设置 gl_Position 以在纹理上裁剪 space 坐标(它们从 -1 到 +1)

  3. 那些剪辑 space 坐标被转换为视口 space。 It's basic math to map one range to another range 但它主要 不重要 。看起来很直观,-1 将绘制到 viewport.x 像素,+1 将绘制到 viewport.x + viewport.width - 1 像素。这就是"maps from clip space to the viewport settings means"。

最常见的视口设置为(x = 0,y = 0,宽度 = 目标纹理的宽度或 canvas,高度 = 目标纹理的高度或 canvas)

这样就只剩下您设置的 gl_Position 了。这些值在剪辑 space just like it explains in this article 中。

如果您愿意,可以通过将像素 space 转换为剪辑 space just like it explains in this article

来简化操作
 zeroToOne = someValueInPixels / destinationDimensions;
 zeroToTwo = zeroToOne * 2.0;
 clipspace = zeroToTwo - 1.0; 
 gl_Position = clipspace;

如果您继续阅读文章,它们还会显示添加一个值 (translation) and multiplying by a value (scale)

仅使用这两个东西和一个单位正方形(0 到 1),您就可以选择屏幕上的任何矩形。想要效果 123 到 127。这是 5 个单位,所以比例 = 5,平移 = 123。然后应用上面的数学从像素转换为剪辑 space,你会得到你想要的矩形。

如果您继续阅读这些文章,您最终会达到 that math is done with matrices 的目的,但您可以随心所欲地进行数学运算。这就像问"how do I compute the value 3"。嗯,1 + 1 + 1,或 3 + 0,或 9 / 3,或 100 - 50 + 20 * 2 / 30,或 (7^2 - 19) / 10,或 ????

我不能告诉你如何设置gl_Position。我只能告诉你 make up whatever math you want and set it to *clip space*,然后给出一个从像素转换为 clipspace 的例子(见上文)作为一些可能数学的例子。

旧答案

我知道这可能不太清楚我不知道如何提供帮助。 WebGL 在二维数组中绘制线、点或三角形。该 2D 数组是 canvas、纹理(作为帧缓冲区附件)或渲染缓冲区(作为帧缓冲区附件)。

区域的大小由 canvas、纹理、渲染缓冲区的大小定义。

你写了一个顶点着色器。当您调用 gl.drawArrays(primitiveType, offset, count) 时,您是在告诉 WebGL 调用您的顶点着色器 count 次。假设 primitiveType 是 gl.TRIANGLES 那么对于你的顶点着色器生成的每 3 个顶点,WebGL 将绘制一个三角形。您可以通过在 clip space 中设置 gl_Position 来指定该三角形。

假设 gl_Position.w 为 1,剪辑 space 在目标 canvas/texture/renderbuffer 的 X 和 Y 中从 -1 变为 +1。 (gl_Position.x 和 gl_Position.y 除以 gl_Position.w)这对你的情况并不重要。

要转换回实际像素,您的 X 和 Y 将根据 gl.viewport 的设置进行转换。让我们做 X

pixelX = ((clipspace.x / clipspace.w) * .5 + .5) * viewport.width + viewport.x

WebGL 是基于目的地的。这意味着它将迭代正在绘制的 line/point/triangle 的像素,并为每个点调用片段着色器并询问“我应该在此处存储什么值”?

让我们将其转换为一维的 JavaScript。假设您有一个一维数组

const dst = new Array(100);

让我们创建一个接受开始和结束并将值设置在

之间的函数
function setRange(dst, start, end, value) {
  for (let i = start; i < end; ++i) {
    dst[i] = value;
  }
}

你可以用123填充整个100个元素的数组

const dst = new Array(100);
setRange(dst, 0, 99, 123);

将数组的后半部分设置为 456

const dst = new Array(100);
setRange(dst, 50, 99, 456);

让我们把它改成像坐标

一样使用剪辑space
function setClipspaceRange(dst, clipStart, clipEnd, value) {
  const start = clipspaceToArrayspace(dst, clipStart);
  const end = clipspaceToArrayspace(dst, clipEnd);
  for (let i = start; i < end; ++i) {
    dst[i] = value;
  }
}

function clipspaceToArrayspace(array, clipspaceValue) {
  // convert clipspace value (-1 to +1) to (0 to 1)
  const zeroToOne = clipspaceValue * .5 + .5;

  // convert zeroToOne value to array space
  return Math.floor(zeroToOne * array.length);
}

此函数现在与前一个函数一样工作,除了采用剪辑 space 值而不是数组索引

// fill entire array with 123
const dst = new Array(100);
setClipspaceRange(dst, -1, +1, 123);

将数组的后半部分设置为 456

setClipspaceRange(dst, 0, +1, 456);

现在再抽象一次。不要使用数组的长度,而是使用设置

// viewport looks like `{ x: number, width: number} `

function setClipspaceRangeViaViewport(dst, viewport, clipStart, clipEnd, value) {
  const start = clipspaceToArrayspaceViaViewport(viewport, clipStart);
  const end = clipspaceToArrayspaceViaViewport(viewport, clipEnd);
  for (let i = start; i < end; ++i) {
    dst[i] = value;
  }
}

function clipspaceToArrayspaceViaViewport(viewport, clipspaceValue) {
  // convert clipspace value (-1 to +1) to (0 to 1)
  const zeroToOne = clipspaceValue * .5 + .5;

  // convert zeroToOne value to array space
  return Math.floor(zeroToOne * viewport.width) + viewport.x;
}

现在用 123 填充整个数组

const dst = new Array(100);
const viewport = { x: 0, width: 100; }
setClipspaceRangeViaViewport(dst, viewport, -1, 1, 123);

将数组的后半部分设置为 456 现在有两种方法。方式一就像前面使用 0 到 +1

setClipspaceRangeViaViewport(dst, viewport, 0, 1, 456);

您还可以将视口设置为从阵列的一半开始

const halfViewport = { x: 50, width: 50; }
setClipspaceRangeViaViewport(dst, halfViewport, -1, +1, 456);

我不知道这是否有帮助。

唯一要添加的另一件事是将 value 替换为一个函数,该函数在每次迭代时都被调用以提供 value

function setClipspaceRangeViaViewport(dst, viewport, clipStart, clipEnd, fragmentShaderFunction) {
  const start = clipspaceToArrayspaceViaViewport(viewport, clipStart);
  const end = clipspaceToArrayspaceViaViewport(viewport, clipEnd);
  for (let i = start; i < end; ++i) {
    dst[i] = fragmentShaderFunction();
  }
}

请注意,这与 this article and clearified somewhat in this article 中所说的完全相同。