webgl - 几何对 WebGL 中最终纹理输出的影响是什么?
问题描述
更新了更多关于我的困惑的解释
(这就是非图形开发人员想象渲染过程的方式!)
我指定通过两个三角形绘制一个 2x2 正方形。我将不再谈论三角形。广场要好很多。假设正方形被画成一块。
我没有为我的绘图指定任何单位。在我的代码中我做这样的事情的唯一地方是:画布大小(在我的例子中设置为 1x1)和视口(我总是将它设置为我的输出纹理的尺寸)。
然后我调用draw()。
发生的情况是:无论我的纹理大小(1x1 或 10000x10000)如何,我的所有纹素都充满了我从片段着色器返回的数据(颜色)。这每次都完美无缺。
所以现在我试图向自己解释这一点:
- GPU 只关心像素的着色。
- 像素是 GPU 处理的最小单位(颜色)。
- 根据我的 2x2 正方形映射到多少像素,我应该遇到以下 3 种情况之一:
- 像素数(要着色)和我的输出纹理暗度一一匹配:在这种理想情况下,对于每个像素,都会为我的输出纹理分配一个值。对我来说很清楚。
- 像素数少于我的输出纹理暗淡。在这种情况下,我应该期望一些输出纹素具有完全相同的值(这是落入像素的颜色)。例如,如果 GPU 最终绘制 16x16 像素并且我的纹理是 64x64,那么我将拥有 4 个纹素的块,它们获得相同的值。无论我的纹理大小如何,我都没有观察到这种情况。这意味着我们永远不会得到更少的像素(真的很难想象——让我们继续吧)
- 像素的数量最终超过了纹素的数量。在这种情况下,GPU 应该决定分配给我的纹素的值。它会平均像素颜色吗?如果 GPU 为 64x64 像素着色并且我的输出纹理是 16x16,那么我应该期望每个纹素获得它包含的 4x4 像素的平均颜色。无论如何,在这种情况下,我的纹理应该完全填充我不打算专门针对它们的值(如平均),但事实并非如此。
我什至没有谈论我的碎片着色器被调用了多少次,因为这并不重要。无论如何,结果将是确定性的。
因此,考虑到我从未遇到过第 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 是基于目标的。这意味着它将遍历它正在绘制的线/点/三角形的像素,并为每个点调用片段着色器并询问“我应该在此处存储什么值”?
它是基于目的地的。它将只绘制每个像素一次。对于那个像素,它会问“我应该做什么颜色”
基于目的地的循环
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
是什么viewport
?viewport
是你调用 `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。
简单的答案是
您将 gl.viewport 设置为要在目的地中影响的子矩形(画布或纹理无关紧要)
您制作了一个顶点着色器,它以某种方式设置
gl_Position
为跨纹理的裁剪空间坐标(它们从 -1 到 +1)这些剪辑空间坐标被转换为视口空间。将一个范围映射到另一个范围是基本的数学运算,但这并不重要。看起来很直观,-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();
}
}
推荐阅读
- javascript - JS加后缀整数并从1旋转到4
- algorithm - Pascal's Triangle Scala:使用尾递归方法计算 Pascal 三角形的元素
- c++ - 通过附加和克隆子字符串来构建字符串的算法
- python - 如何更改 sqlalchemy 的 create_engine 中的编码选项?
- python - 用户提供的文件中的 2 个 ascii 字符计数器
- angular - 如何在另一个 Angular 组件中显示 Angular 组件
- string - 如何使用 sparql 在字符串中查找特定字符的位置
- c++ - 如何正确实现 C++ 类析构函数
- pdf-generation - Hybris MediaModel 如何获取字节
- c# - DICOM多平面图像重建