首页 > 解决方案 > 几何对 WebGL 中最终纹理输出的影响是什么?

问题描述

更新了更多关于我的困惑的解释

(这就是非图形开发人员想象渲染过程的方式!)

我指定通过两个三角形绘制一个 2x2 正方形。我将不再谈论三角形。广场要好很多。假设正方形被画成一块。

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

然后我调用draw()。

发生的情况是:无论我的纹理大小(1x1 或 10000x10000)如何,我的所有纹素都充满了我从片段着色器返回的数据(颜色)。这每次都完美无缺。

所以现在我试图向自己解释这一点:

我什至没有谈论我的碎片着色器被调用了多少次,因为这并不重要。无论如何,结果将是确定性的。

因此,考虑到我从未遇到过第 2 和第 3 种情况,即我的纹素中的值不是我预期的那样,我能得出的唯一结论是,GPU 尝试渲染像素的整个假设实际上是错误的。当我为它分配一个输出纹理(它应该一直延伸到我的 2x2 正方形上)时,GPU 会很高兴地负责,并且每个纹素都会调用我的碎片着色器。沿着这条线的某个地方,像素也会被着色。

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

希望以上关于 GPU 着色过程的精彩叙述为您提供了关于我哪里出错的线索。

原帖:

我们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

解决方案


新答案

我只是再次说同样的话(第三次?)

从下面复制

WebGL 是基于目标的。这意味着它将遍历它正在绘制的线/点/三角形的像素,并为每个点调用片段着色器并询问“我应该在此处存储什么值”?

它是基于目的地的。它将只绘制每个像素一次。对于那个像素,它会问“我应该做什么颜色”

基于目的地的循环

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

您可以在上面的循环中看到没有设置任何随机目的地。没有设置目的地的任何部分两次。对于目标中的每个像素,它只会从开始到结束运行start一次end,询问它应该使该像素成为什么颜色。

你如何设置开始和结束?同样,为了简单起见,让我们假设一个 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 设置为要在目的地中影响的子矩形(画布或纹理无关紧要)

  2. 您制作了一个顶点着色器,它以某种方式设置gl_Position为跨纹理的裁剪空间坐标(它们从 -1 到 +1)

  3. 这些剪辑空间坐标被转换为视口空间。将一个范围映射到另一个范围是基本的数学运算,但这并不重要。看起来很直观,-1 将绘制到viewport.x像素,+1 将绘制到viewport.x + viewport.width - 1像素。这就是“从剪辑空间映射到视口设置的含义”。

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

所以这只是留下你设置gl_Position的东西。就像本文中解释的那样,这些值位于剪辑空间中。

如果您愿意,可以通过将像素空间转换为剪辑空间来使其变得简单,就像本文中解释的那样

 zeroToOne = someValueInPixels / destinationDimensions;
 zeroToTwo = zeroToOne * 2.0;
 clipspace = zeroToTwo - 1.0; 
 gl_Position = clipspace;

如果您继续阅读这些文章,它们还将显示添加值(翻译)和乘以值(比例)

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

如果您继续阅读这些文章,您最终将了解使用矩阵完成数学运算的要点,但您可以随心所欲地进行数学运算。这就像问“我如何计算值 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*,然后举一个从像素转换到剪辑空间的例子(见上文),这只是一些可能的数学的一个例子。

旧答案

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

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

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

假设gl_Position.w为 1,剪辑空间在目标画布/纹理/渲染缓冲区的 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 是基于目标的。这意味着它将遍历它正在绘制的线/点/三角形的像素,并为每个点调用片段着色器并询问“我应该在此处存储什么值”?

让我们将其转换为 1D 中的 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);

让我们改变它以使用像坐标这样的剪辑空间

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);
}

这个函数现在就像前一个函数一样工作,除了使用剪辑空间值而不是数组索引

// 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,现在有 2 种方法。方式一和上一个使用 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();
  }
}

请注意,这与本文中所说的完全相同,并且在本文中有所澄清。


推荐阅读