如何在WebGL中使用数据纹理。

4

我一直在学习WebGL,像webglfundamentals这样的教程,但现在遇到了一个问题——我相信我需要使用自己创建的纹理将信息直接传递到片段着色器中,但我似乎无法正确地索引该纹理。

目标是传递有关光源(位置和颜色)的信息,这些信息将影响片段的颜色。理想情况下,这些信息的值和长度都是动态的。

重现步骤

我在这个fiddle中创建了一个简化版本的问题:WebGL-数据纹理测试

以下是部分代码。

一次性设置中,我们创建一个纹理,填充数据,并对该纹理应用最简单的设置(无mips,无字节包装问题[?])

  // lookup uniforms
  var textureLocation = gl.getUniformLocation(program, "u_texture");

  // Create a texture.
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // fill texture with 1x3 pixels
  const level = 0;
  const internalFormat = gl.RGBA; // I've also tried to work with gl.LUMINANCE
  //   but it feels harder to debug
  const width = 1;
  const height = 3;
  const border = 0;
  const type = gl.UNSIGNED_BYTE;
  const data = new Uint8Array([
    // R,   G,   B, A (unused)    // : 'texel' index (?)
    64, 0, 0, 0, // : 0
    0, 128, 0, 0, // : 1
    0, 0, 255, 0, // : 2
  ]);
  const alignment = 1; // should be uneccessary for this texture, but 
  gl.pixelStorei(gl.UNPACK_ALIGNMENT, alignment); //   I don't think this is hurting
  gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border,
    internalFormat, type, data);

  // set the filtering so we don't need mips and it's not filtered
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

在“绘制”序列中(这仅发生一次,但可能重复),我们强调程序应该使用我们的纹理。
    // Tell the shader to use texture unit 0 for u_texture
    gl.activeTexture(gl.TEXTURE0);                  // added this and following line to be extra sure which texture is being used...
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.uniform1i(textureLocation, 0);

最后,在片元着色器中,我们试图可靠地使用一个“纹素”来传递信息。我似乎无法弄清如何可靠地检索我存储在纹理中的值。

precision mediump float;

// The texture.
uniform sampler2D u_texture;

void main() {

    vec4 sample_00 = texture2D(u_texture, vec2(0, 0)); 
    // This sample generally appears to be correct. 
    // Changing the provided data for the B channel of texel
    //   index 0 seems to add blue as expected

    vec4 sample_01 = texture2D(u_texture, vec2(0, 1));
    vec4 sample_02 = texture2D(u_texture, vec2(0, 2));
    // These samples are how I expected this to work since 
    //   the width of the texture is set to 1
    // For some reason 01 and 02 both show the same color

    vec4 sample_10 = texture2D(u_texture, vec2(1, 0));
    vec4 sample_20 = texture2D(u_texture, vec2(2, 0));
    // These samples are just there for testing - I don't think
    //   that they should work

    // choose which sample to display
    vec4 sample = sample_00;
    gl_FragColor = vec4(sample.x, sample.y, sample.z, 1);
}

问题

使用纹理是否是实现此目的的最佳方式?我听说过也可以传递向量数组,但纹理似乎更常见。

您应该如何创建纹理?(特别是当我指定“宽度”和“高度”时,我应该是参考结果像素维度还是我将用于构建纹理的gl.UNSIGNED_BYTE元素数量?texImage2D文档

在片段着色器中不使用“varying”类型时,您应该如何索引纹理? (即,我只想得到一个或多个特定纹素的值 - 没有插值[与顶点几乎没有关系])

其他资源

目前为止,我已经读了尽可能多的关于此问题的内容。 这里是一个非详尽列表:

编辑这是我刚刚找到的另一个资源:WebGL中访问数组的麻烦及其几种解决方法。 让我感到有希望。

这真的很烦人。

提前致谢!


为什么不将灯光作为一个数组传递给uniform变量uniform vec3 lights[4];?此外,查找纹理像素应该使用单位坐标而不是像素坐标。例如,texture2D(tex, vec2(0,2)) 应该是 texture2D(tex, vec2(0,0.5)) - Blindman67
1
请查看此教程:https://webglfundamentals.org/webgl/lessons/webgl-pulling-vertices.html - gman
2个回答

6

在WebGL1中,访问纹理中的单个像素使用以下公式

vec2 pixelCoord = vec2(x, y);
vec2 textureDimension = vec2(textureWidth, textureHeight)
vec2 texcoord = (pixelCoord + 0.5) / textureDimensions;
vec4 pixelValue = texture2D(someSamplerUniform, texcoord);

因为纹理坐标是沿边缘的。如果您有一个 2x1 的纹理

1.0+-------+-------+
   |       |       |
   |   A   |   B   |
   |       |       |
0.0+-------+-------+
  0.0     0.5     1.0

像素A中心的纹理坐标为0.25,0.5。像素B中心的纹理坐标为0.75,0.5。
如果您不按上述公式操作,而是使用pixelCoord / textureDimensions,则指向像素之间,导致数学错误会使您得到一个像素或另一个像素。
当然,如果您正在使用纹理进行数据处理,您可能还想设置gl.NEAREST过滤器。
在WebGL2中,您可以直接使用texelFetch。
ivec2 pixelCoord = ivec2(x, y);
int mipLevel = 0;
vec4 pixelValue = texelFetch(someSamplerUniform, texcoord, mipLevel);

一个使用纹理进行数据处理的工作示例在这里。点击这里查看。
“使用纹理是最好的方法吗?我听说过可以传递向量数组的能力,但是纹理似乎更常见。”
做什么?不清楚你想做什么。每个像素都会有不同的光源吗?
“你应该如何创建纹理?(特别是当我指定'width'和'height'时,我应该是在引用构造纹理所需的'gl.UNSIGNED_BYTE'元素数量还是绘制出的像素尺寸?texImage2D文档)”
做最容易或必要的事情。例如,如果您想要提取的每个数据有5个部分,我可能会将每个数据部分放在纹理中的单独一行上。然后,我可以执行:
vec4 datum1 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY0);
vec4 datum2 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY1);
vec4 datum3 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY2);
vec4 datum4 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY3);

当您需要计算indexTexCoordX和rowTexCoordY0-3时,请使用上面的公式。 rowTexCoordY0-3可能是常量。

纹理在尺寸方面有限制,因此如果您有更多数据,则需要更紧密地打包数据并进行更多数学运算以提取它。

请注意,纹理具有缓存,因此最好使您拉出的数据靠近您以前拉出的数据。 如果每次跳到下一个值的纹理时,性能将降低。 (虽然当然,根据您要做什么,它可能仍然比替代解决方案更快)

如果不使用“varying”类型,那么在片段着色器中如何索引纹理? (即,我只想要一个或多个特定纹素的值-无插值[与顶点几乎没有关系])

片段着色器的唯一可变输入是varyings,gl_FragCoord(正在写入像素的坐标)和gl_PointCoord ,仅在绘制POINTS时可用。 因此,您必须使用其中之一,否则所有其他值对于所有像素都是恒定的。


1
盲人的阳光,你的阴影选择纹理值的解释让我感到宽慰 - 我还以为我要疯了!这是一个非常好的补充,与使用均匀数组的替代方案相比更好。我真诚地感谢您在教程中对新手的支持。 - oclyke

2

使用统一数组

这篇文章有些旧了。WebGL通过 uniform[1234][if]v 提供对统一数组的支持,而且浏览器支持良好

当你可以使用统一数组来存储动态数据(如灯光)时,使用纹理来存储浮点数数据不是一个好主意。

下面的示例使用webGL(而不是更好的选项webGL2)具有两个统一数组。

// define number of lights
#define LIGHTS 5
uniform vec3 lights[LIGHTS];
uniform vec3 lightCols[LIGHTS];

它们是通过 gl.uniform3fv 设置的。请注意,v 代表向量(C语言数组)
这将节省您发送冗余数据(alpha字节)并使用浮点精度而不是纹理字节的工作(或通过扩展,您可以具有浮点纹理)
此外,您可以仅发送更改的数组项,而不是每次都发送所有数据。

const LIGHTS = 15; // Number of lights. MUST BE!!!! INT >= 1
const EXPOSURE = 1; // Scaler on final pixel colour
const shaders = {
    get locations() { return ["A_vertex","U_lights","U_lightCols"] },  // Prefix A_ or U_ used by JS name is after _
    get vertex() { return `
attribute vec2 vertex;
varying vec2 map;
void main() {
 map = vertex * 0.5 + 0.5; // map unit 0-1 over quad
 gl_Position = vec4(vertex, 0, 1);
}`;
    },
    get fragment() { return `
precision mediump float;
#define LIGHTS ${LIGHTS}
#define EXPOSURE ${EXPOSURE.toFixed(1)}
uniform vec3 lights[LIGHTS];
uniform vec3 lightCols[LIGHTS];
varying vec2 map;
void main() {
    vec3 light = vec3(0);
    for (int i = 0; i < LIGHTS; i++) {
        float level = pow(1.0-length(lights[i] - vec3(map, 0)), 4.0);
        light += lightCols[i] * level ;
    }
    gl_FragColor = vec4(light * EXPOSURE, 1.0);
}`;
    }
};
const gl = canvas.getContext("webgl", {alpha: false, depth: false, premultpliedAlpha: false, preserveDrawingBufer: true});
var shader, frame = 0; 
function resize() { 
    if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
        canvas.width = innerWidth;
        canvas.height = innerHeight;
        shader.gl.viewport(0, 0, canvas.width,canvas.height);
    }
}
requestAnimationFrame(update);
function update(timer){ // Main update loop
    frame += 1;
    if (shader === undefined) {
        shader = createRender(gl);
    }
    resize();
    gl.useProgram(shader.program);
    orbit(shader, frame);
    shader.gl.drawArrays(gl.TRIANGLES, 0, 6);
    requestAnimationFrame(update);
}


function orbit(shader, frame) {
    var i = LIGHTS;
    frame /= 100;
    const l = shader.lightsBuf, lp = shader.lights, c = shader.lightColsBuf, cp = shader.lightCols;
    while (i--) {
        const idx = i * 3, r = 0.1 + i / LIGHTS;
        l[idx    ] = lp[idx    ] + Math.cos(frame) * r * Math.sin(frame/2);
        l[idx + 1] = lp[idx + 1] + Math.sin(frame) * r * Math.sin(frame/2);
        l[idx + 2] = lp[idx + 2] + Math.cos(frame/2) * r;
        c[idx    ] = cp[idx    ] + (Math.sin(frame/3) * 0.5) * Math.cos(frame);
        c[idx + 1] = cp[idx + 1] + (Math.cos(frame/3)* 0.5) * Math.cos(frame);
        c[idx + 2] = cp[idx + 2] + Math.sin(frame) * 0.5;
        frame *= 1.2;
    }
    shader.gl.uniform3fv(shader.locations.lights, l, 0, LIGHTS * 3);
    shader.gl.uniform3fv(shader.locations.lightCols, c, 0, LIGHTS * 3);
}
    


function createShader(gl, vertSrc, fragSrc, locDesc) {
    var locations = {};
    const program = gl.createProgram();
    const vShader = gl.createShader(gl.VERTEX_SHADER);
    const fShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(vShader, vertSrc);
    gl.shaderSource(fShader, fragSrc);
    gl.compileShader(vShader);
    if (!gl.getShaderParameter(vShader, gl.COMPILE_STATUS)) { throw new Error(gl.getShaderInfoLog(vShader)) } 
    gl.compileShader(fShader);
    if (!gl.getShaderParameter(fShader, gl.COMPILE_STATUS)) { throw new Error(gl.getShaderInfoLog(fShader)) } 
    gl.attachShader(program, vShader);
    gl.attachShader(program, fShader);
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { throw new Error(gl.getProgramInfoLog(program)) } 
    gl.useProgram(program);
    for(const desc of locDesc) {
        const [type, name] = desc.split("_");
        locations[name] = gl[`get${type==="A" ? "Attrib" : "Uniform"}Location`](program, name);
    }
    return {program, locations, gl};
}

function createRender(gl) {
    const shader = createShader(gl, shaders.vertex, shaders.fragment, shaders.locations);
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW);
    gl.enableVertexAttribArray(shader.locations.vertex);
    gl.vertexAttribPointer(shader.locations.vertex, 2, gl.FLOAT, false, 0, 0);
    shader.lightsBuf = new Float32Array(LIGHTS * 3);
    shader.lightColsBuf = new Float32Array(LIGHTS * 3);
    const lights = shader.lights = new Float32Array(LIGHTS * 3);
    const cols = shader.lightCols = new Float32Array(LIGHTS * 3);
    var i = LIGHTS * 3;
    while (i--) { (cols[i] = Math.random(),lights[i] = Math.random())}
    return shader;
}
body {
  padding: 0px;
}

canvas {
  position: absolute;
  top: 0px;
  left: 0px;
}
<canvas id="canvas"></canvas>


1
数组统一变量是WebGL的基本功能之一,自1.0版本以来一直可用。没有任何不支持数组统一变量的WebGL版本,因此不知道为什么您觉得需要写“具有良好的浏览器支持”。凡是支持WebGL的浏览器都能够做到这一点。 - gman
@gman,这不是OP所想的,我写下我的答案是因为编辑链接,表明OP可能认为uniforms不能是数组。此外,OP还有另一个链接与uniform数组有关“...尝试使用vec3数组”进一步表明使用纹理不是OP首选的选项。OP想要传递光源位置和颜色,这两者都不适合以8位RGBA格式存储而不需要大量操作。 - Blindman67
@Blindman67 谢谢你的建议 - 看来统一数组确实是我的项目的正确解决方案。这个例子也非常有帮助。我对于被限制在8位值上并不感到兴奋,所以这太棒了! - oclyke
1
@Blindman67,我不是在否定你的解决方案。我是在评论你的第二句话,那是无意义的。没有所谓的“统一数组”的“良好浏览器支持”。任何支持WebGL的浏览器都支持统一数组。这是WebGL的基本功能,一直以来都是如此。它不是可选的。WebGL本身是可选的,作为子功能的统一数组不是。说WebGL中有良好的统一数组浏览器支持就像说JavaScript中有良好的加法支持,这暗示了某些版本的JavaScript不能将2个数字相加。 - gman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接