悬垂检测着色器 - 如何 return 顶点坐标?
Overhang detection shader - how to return the coordinates of the vertices?
我正在尝试使用 three.js 在浏览器中编写支持生成应用程序,我尝试了很多方法,但所有方法都很慢,所以现在我决定让着色器计算悬垂位置和我的程序构建支持这些点。
悬垂检测着色器输出:
现在的问题是我无法弄清楚如何 return 那些红色区域到 CPU /main JavaScript 应用程序来生成对这些点的简单支持,
我在这里的某个地方读到有关涉及 FBO 的 GPU CPU 方法,但无法理解这一点,有没有办法让红色区域坐标回到 CPU?
我也可以在顶点着色器中计算它以将非悬垂顶点的位置更新为 0,0,0,但问题是三个 JavaScript 中的顶点位置不会更新这样,如果有某种方法可以在顶点着色器执行后获取更新的顶点位置,它可能是一个解决方案。
也许改变反馈?如何使用来自 three.js 的转换反馈?
如果您只想获得渲染图像(就像您在问题中链接的图像),您可以使用 THREE 的包装器 readPixels
readRenderTargetPixels。这将为您提供图像像素值作为数组,您可以对其进行迭代并找到红色区域。此外,由于您的片段着色器似乎做了很多二元决策(黑色或红色),您可以使用其他通道来存储附加信息,例如在顶点着色器中:
// ...
varying vec3 position;
// ...
void main(void) {
// ...
position = gl_Position.xyz / gl_Position.w;
}
在片段着色器中:
// ...
varying highp vec3 position;
// ...
void main(void) {
// ...
gl_FragColor.xyz = 0.5 * (position + 1.0); // position'll be in (-1, 1) range, where as gl_FragColor's clamped to (0, 1)
gl_FragColor.w = isOverhang ? 1.0 : 0.0;
}
然后在JS代码中:
// ...
const pixelBuffer = new Uint8Array(4 * w * h);
renderer.readRenderTargetPixels(renderTarget, 0, 0, w, h, pixelBuffer);
for (let y = 0, offset = 0; y < h; ++y) {
for (let x = 0; x < w; ++x, offset += 4) {
// does pixel correspond to overhang area?
if (pixelBuffer[offset + 3] > 0) {
const posX = 2 * pixelBuffer[offset] / 255 - 1;
const posY = 2 * pixelBuffer[offset + 1] / 255 - 1;
const posZ = 2 * pixelBuffer[offset + 2] / 255 - 1;
// ...
}
}
}
但是,8 位精度可能不足以满足您的目的。在这种情况下,您可以使用 FLOAT
或 HALF_FLOAT
呈现目标(如果浏览器支持它们)。
您也可以试试GPGPU方式。基本上,大部分时间它使用片段着色器来计算一些值,然后将其存储在纹理中(通常也是 FLOAT
或 HALF_FLOAT
),然后读回 CPU 或在后续绘图中采样以使用计算值。 WebGL 中有很多关于 GPGPU 的信息,例如this.
关于变换反馈。是的,它专门用于将顶点着色器的结果存储在某个缓冲区中,可以再次将其读回 CPU(很少)或在 GPU 上重复使用,例如作为另一个甚至同一个顶点着色器的输入。但是 TF 仅在 WebGL 2 中可用。
您可以使用其他 FBO 或转换反馈。使用变换反馈,他唯一的问题是 AFAICT 没有办法丢弃顶点,所以就像你提到的那样,在这种情况下你能做的最好的事情就是为非重叠顶点写一些特殊值。
要使用 FBO,您需要制作一个浮点纹理并检查您是否可以渲染它。在 WebGL1 中,这意味着启用浮点纹理,将一个纹理绑定到帧缓冲区并调用 checkFramebufferStatus。在 WebGL 2 中,它意味着检查并启用 EXT_color_buffer_float
(并且仍然调用 checkFramebufferStatus)
然后创建一个只有计数 [0, 1, 2, 3, 4, 5, 6 等] 的缓冲区,使用它来生成一个 gl_Position
,它将写入FBO.
// WebGL2
varying uint count;
uniform uint2 resolutionOfFBO;
// compute output pixel
uint x = count % resolutinOfFBO.x;
uint y = count / resolutionOfFBO.x;
// set gl_Position so we'll write to that output pixel
gl_Position = vec4((vec2(x, y) + .5) / resolutionOfFBO, 0, 1);
将要写入的数据传递到 varying 中,然后将该数据写入片段着色器。然后用 POINTS
.
渲染
然后您可以使用 gl.readPixels
读回数据
对于这个问题,解释转换反馈似乎有点长,但这里有一个简单的例子:输入是 [1, 2, 3]
,输出是 [2, 4, 6]
function main() {
const gl = document.createElement("canvas").getContext("webgl2");
const vs = `#version 300 es
in float in_value;
out float out_value;
void main() {
out_value = in_value * 2.;
}
`;
const fs = `#version 300 es
precision mediump float;
layout (location = 0) out vec4 dummy;
void main() {
dummy = vec4(1);
}
`;
const prog = createProgram(gl, [vs, fs], ["out_value"]);
const inLoc = gl.getAttribLocation(prog, 'in_value');
const outLoc = 0;
const numVaryings = gl.getProgramParameter(prog, gl.TRANSFORM_FEEDBACK_VARYINGS);
const srcBuffer1 = createBuffer(gl, new Float32Array([1, 2, 3]));
const srcVAO1 = createVAO(gl, srcBuffer1, inLoc);
const dstBuffer = createBuffer(gl, Float32Array.BYTES_PER_ELEMENT * 3);
const srcVAO2 = createVAO(gl, dstBuffer, inLoc);
const tf = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
gl.useProgram(prog);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, dstBuffer);
// this binds the default (id = 0) TRANSFORM_FEEBACK buffer
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
// This line is onky because of a bug in Chrome
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
runFeedback(gl, prog, srcVAO1, tf);
checkGLError(gl);
const result = new Float32Array(3);
gl.bindBuffer(gl.ARRAY_BUFFER, dstBuffer);
gl.getBufferSubData(gl.ARRAY_BUFFER, 0, result);
log(result);
}
main();
function runFeedback(gl, prog, srcVAO, tf, dstBufferInfo) {
gl.enable(gl.RASTERIZER_DISCARD);
gl.useProgram(prog);
gl.bindVertexArray(srcVAO);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
gl.beginTransformFeedback(gl.TRIANGLES);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.endTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
gl.disable(gl.RASTERIZER_DISCARD);
}
function checkGLError(gl) {
const err = gl.getError();
if (err) {
log("GL ERROR:", err);
}
}
function createShader(gl, shaderSource, shaderType) {
var shader = gl.createShader(shaderType);
gl.shaderSource(shader, shaderSource);
gl.compileShader(shader);
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, shaderSources, outputs) {
const shaderTypes = [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER];
const program = gl.createProgram();
shaderSources.forEach(function(shaderSrc, ndx) {
gl.attachShader(program, createShader(gl, shaderSrc, shaderTypes[ndx]));
});
if (outputs) {
gl.transformFeedbackVaryings(program, outputs, gl.SEPARATE_ATTRIBS);
}
gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
function createBuffer(gl, dataOrSize) {
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, dataOrSize, gl.STATIC_DRAW);
return buf;
}
function createVAO(gl, buf, inLoc) {
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.enableVertexAttribArray(inLoc);
gl.vertexAttribPointer(inLoc, 1, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null); // this is not needed
gl.bindVertexArray(null);
return vao;
}
function log(...args) {
const elem = document.createElement("pre");
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
简短的解释是使用变换反馈,您的顶点着色器的输出变量被写入一个或多个缓冲区。
要做到这一点,您必须在 link 时间告诉您的着色器程序您的输出是 gl.transformFeedbackVaryings
。
然后创建一个变换反馈对象。变换反馈对象与顶点数组对象非常相似,只是它用于输出而不是输入。您可以通过为每个输出调用 gl.bindBufferBase
来指定输出,就像您为顶点数组对象上的每个输入调用 gl.vertexAttribPointer
一样。
要实际生成输出,您可能需要告诉 WebGL 不要 运行 片段着色器
gl.enable(gl.RASTERIZER_DISCARD);
然后绑定变换反馈对象,打开变换反馈并绘制
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
gl.beginTransformFeedback(gl.TRIANGLES);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.endTransformFeedback();
当您link程序时,您可以选择单独或交错的属性。使用单独的每个属性都可以转到不同的缓冲区,但是您可以写入的属性数量是有限制的(最小值至少为 4)。使用交错,所有输出都会被写入,但它们是交错的。例如,如果你同时写位置和法线,那么输出将是
position0, normal0, position1, normal1, position2, normal2
全部到同一个缓冲区。
我正在尝试使用 three.js 在浏览器中编写支持生成应用程序,我尝试了很多方法,但所有方法都很慢,所以现在我决定让着色器计算悬垂位置和我的程序构建支持这些点。
悬垂检测着色器输出:
现在的问题是我无法弄清楚如何 return 那些红色区域到 CPU /main JavaScript 应用程序来生成对这些点的简单支持, 我在这里的某个地方读到有关涉及 FBO 的 GPU CPU 方法,但无法理解这一点,有没有办法让红色区域坐标回到 CPU?
我也可以在顶点着色器中计算它以将非悬垂顶点的位置更新为 0,0,0,但问题是三个 JavaScript 中的顶点位置不会更新这样,如果有某种方法可以在顶点着色器执行后获取更新的顶点位置,它可能是一个解决方案。
也许改变反馈?如何使用来自 three.js 的转换反馈?
如果您只想获得渲染图像(就像您在问题中链接的图像),您可以使用 THREE 的包装器 readPixels
readRenderTargetPixels。这将为您提供图像像素值作为数组,您可以对其进行迭代并找到红色区域。此外,由于您的片段着色器似乎做了很多二元决策(黑色或红色),您可以使用其他通道来存储附加信息,例如在顶点着色器中:
// ...
varying vec3 position;
// ...
void main(void) {
// ...
position = gl_Position.xyz / gl_Position.w;
}
在片段着色器中:
// ...
varying highp vec3 position;
// ...
void main(void) {
// ...
gl_FragColor.xyz = 0.5 * (position + 1.0); // position'll be in (-1, 1) range, where as gl_FragColor's clamped to (0, 1)
gl_FragColor.w = isOverhang ? 1.0 : 0.0;
}
然后在JS代码中:
// ...
const pixelBuffer = new Uint8Array(4 * w * h);
renderer.readRenderTargetPixels(renderTarget, 0, 0, w, h, pixelBuffer);
for (let y = 0, offset = 0; y < h; ++y) {
for (let x = 0; x < w; ++x, offset += 4) {
// does pixel correspond to overhang area?
if (pixelBuffer[offset + 3] > 0) {
const posX = 2 * pixelBuffer[offset] / 255 - 1;
const posY = 2 * pixelBuffer[offset + 1] / 255 - 1;
const posZ = 2 * pixelBuffer[offset + 2] / 255 - 1;
// ...
}
}
}
但是,8 位精度可能不足以满足您的目的。在这种情况下,您可以使用 FLOAT
或 HALF_FLOAT
呈现目标(如果浏览器支持它们)。
您也可以试试GPGPU方式。基本上,大部分时间它使用片段着色器来计算一些值,然后将其存储在纹理中(通常也是 FLOAT
或 HALF_FLOAT
),然后读回 CPU 或在后续绘图中采样以使用计算值。 WebGL 中有很多关于 GPGPU 的信息,例如this.
关于变换反馈。是的,它专门用于将顶点着色器的结果存储在某个缓冲区中,可以再次将其读回 CPU(很少)或在 GPU 上重复使用,例如作为另一个甚至同一个顶点着色器的输入。但是 TF 仅在 WebGL 2 中可用。
您可以使用其他 FBO 或转换反馈。使用变换反馈,他唯一的问题是 AFAICT 没有办法丢弃顶点,所以就像你提到的那样,在这种情况下你能做的最好的事情就是为非重叠顶点写一些特殊值。
要使用 FBO,您需要制作一个浮点纹理并检查您是否可以渲染它。在 WebGL1 中,这意味着启用浮点纹理,将一个纹理绑定到帧缓冲区并调用 checkFramebufferStatus。在 WebGL 2 中,它意味着检查并启用 EXT_color_buffer_float
(并且仍然调用 checkFramebufferStatus)
然后创建一个只有计数 [0, 1, 2, 3, 4, 5, 6 等] 的缓冲区,使用它来生成一个 gl_Position
,它将写入FBO.
// WebGL2
varying uint count;
uniform uint2 resolutionOfFBO;
// compute output pixel
uint x = count % resolutinOfFBO.x;
uint y = count / resolutionOfFBO.x;
// set gl_Position so we'll write to that output pixel
gl_Position = vec4((vec2(x, y) + .5) / resolutionOfFBO, 0, 1);
将要写入的数据传递到 varying 中,然后将该数据写入片段着色器。然后用 POINTS
.
然后您可以使用 gl.readPixels
对于这个问题,解释转换反馈似乎有点长,但这里有一个简单的例子:输入是 [1, 2, 3]
,输出是 [2, 4, 6]
function main() {
const gl = document.createElement("canvas").getContext("webgl2");
const vs = `#version 300 es
in float in_value;
out float out_value;
void main() {
out_value = in_value * 2.;
}
`;
const fs = `#version 300 es
precision mediump float;
layout (location = 0) out vec4 dummy;
void main() {
dummy = vec4(1);
}
`;
const prog = createProgram(gl, [vs, fs], ["out_value"]);
const inLoc = gl.getAttribLocation(prog, 'in_value');
const outLoc = 0;
const numVaryings = gl.getProgramParameter(prog, gl.TRANSFORM_FEEDBACK_VARYINGS);
const srcBuffer1 = createBuffer(gl, new Float32Array([1, 2, 3]));
const srcVAO1 = createVAO(gl, srcBuffer1, inLoc);
const dstBuffer = createBuffer(gl, Float32Array.BYTES_PER_ELEMENT * 3);
const srcVAO2 = createVAO(gl, dstBuffer, inLoc);
const tf = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
gl.useProgram(prog);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, dstBuffer);
// this binds the default (id = 0) TRANSFORM_FEEBACK buffer
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
// This line is onky because of a bug in Chrome
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
runFeedback(gl, prog, srcVAO1, tf);
checkGLError(gl);
const result = new Float32Array(3);
gl.bindBuffer(gl.ARRAY_BUFFER, dstBuffer);
gl.getBufferSubData(gl.ARRAY_BUFFER, 0, result);
log(result);
}
main();
function runFeedback(gl, prog, srcVAO, tf, dstBufferInfo) {
gl.enable(gl.RASTERIZER_DISCARD);
gl.useProgram(prog);
gl.bindVertexArray(srcVAO);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
gl.beginTransformFeedback(gl.TRIANGLES);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.endTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
gl.disable(gl.RASTERIZER_DISCARD);
}
function checkGLError(gl) {
const err = gl.getError();
if (err) {
log("GL ERROR:", err);
}
}
function createShader(gl, shaderSource, shaderType) {
var shader = gl.createShader(shaderType);
gl.shaderSource(shader, shaderSource);
gl.compileShader(shader);
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, shaderSources, outputs) {
const shaderTypes = [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER];
const program = gl.createProgram();
shaderSources.forEach(function(shaderSrc, ndx) {
gl.attachShader(program, createShader(gl, shaderSrc, shaderTypes[ndx]));
});
if (outputs) {
gl.transformFeedbackVaryings(program, outputs, gl.SEPARATE_ATTRIBS);
}
gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
function createBuffer(gl, dataOrSize) {
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, dataOrSize, gl.STATIC_DRAW);
return buf;
}
function createVAO(gl, buf, inLoc) {
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.enableVertexAttribArray(inLoc);
gl.vertexAttribPointer(inLoc, 1, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null); // this is not needed
gl.bindVertexArray(null);
return vao;
}
function log(...args) {
const elem = document.createElement("pre");
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
简短的解释是使用变换反馈,您的顶点着色器的输出变量被写入一个或多个缓冲区。
要做到这一点,您必须在 link 时间告诉您的着色器程序您的输出是 gl.transformFeedbackVaryings
。
然后创建一个变换反馈对象。变换反馈对象与顶点数组对象非常相似,只是它用于输出而不是输入。您可以通过为每个输出调用 gl.bindBufferBase
来指定输出,就像您为顶点数组对象上的每个输入调用 gl.vertexAttribPointer
一样。
要实际生成输出,您可能需要告诉 WebGL 不要 运行 片段着色器
gl.enable(gl.RASTERIZER_DISCARD);
然后绑定变换反馈对象,打开变换反馈并绘制
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
gl.beginTransformFeedback(gl.TRIANGLES);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.endTransformFeedback();
当您link程序时,您可以选择单独或交错的属性。使用单独的每个属性都可以转到不同的缓冲区,但是您可以写入的属性数量是有限制的(最小值至少为 4)。使用交错,所有输出都会被写入,但它们是交错的。例如,如果你同时写位置和法线,那么输出将是
position0, normal0, position1, normal1, position2, normal2
全部到同一个缓冲区。