如何在 WebGL 中使用数据纹理

How to use data textures in WebGL

我一直在研究像 webglfundamentals 这样的 WebGL 教程并且 运行 遇到了一个绊脚石 - 我相信我将需要使用我创建的纹理将信息直接传递给片段着色器,但我似乎无法正确索引纹理。

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

复制

我在这个 fiddle 中创建了问题的简化版本:WebGL - Data Texture Testing

这是一些代码。

一次性设置中,我们创建一个纹理,用数据填充它,然后在该纹理上应用看起来最简单的设置(没有 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);

draw 序列中(只发生一次但可以重复)我们强调程序应该使用我们的纹理

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

最后,在片段着色器中,我们试图可靠地使用一个 'texel' 作为传达信息的手段。我似乎无法弄清楚如何可靠地检索我存储在纹理中的值。

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

问题

使用纹理是最好的方法吗?我也听说过传递向量数组的能力,但纹理似乎更常见。

你应该如何创建纹理? (特别是当我指定 'width' 和 'height' 时,我应该指的是生成的纹素尺寸还是我将用于构建纹理的 gl.UNSIGNED_BYTE 元素的数量??texImage2D documentation

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

其他资源

目前我已经尽可能多地阅读了这方面的内容。这是一个非详尽的列表:

编辑 这是我刚找到的另一个资源:Hassles with array access in WebGL, and a couple of workarounds。让我充满希望。

这真让我烦恼。

提前致谢!

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

为数据使用纹理的一个工作示例是 here

Is using a texture the best way to do this? I have heard of the ability to pass arrays of vectors as well, but textures seem to be more common.

做什么?目前尚不清楚您要做什么。每个像素都会有不同的光源吗?

How are you supposed to create the texture? (particularly when I specify 'width' and 'height' should I be referring to resulting texel dimensions or the number of gl.UNSIGNED_BYTE elements that I will use to construct the texture?? texImage2D documentation)

做最简单或需要的事。例如,如果您想要提取每件东西的 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 甚至可能是常量。

尽管纹理在维度上有限制,所以如果你有更多的数据然后将适合一个维度,那么你将不得不更紧密地打包数据并做更多的数学运算才能将其提取出来。

请注意纹理有缓存,因此理想情况下,您希望提取的数据接近您之前提取的数据。如果你每次跳过纹理以获得下一个值,你的性能就会下降。 (尽管当然它可能仍然比替代解决方案更快,具体取决于您在做什么)

How are you supposed to index into the texture in a fragment shader when not using 'varying' types? (i.e. I just want the value of one or more particular texels - no interpolation [having little to nothing to do with vertices])

片段着色器的唯一变化输入是变量,gl_FragCoord(写入的坐标像素)和 gl_PointCoord,仅在绘制时可用 POINTS。所以你必须使用其中之一,否则所有其他值对于所有像素都是不变的。

使用统一数组

链接的文章有些旧。 WebGL 通过 uniform[1234][if]v 支持统一数组 good browser support

当您可以简单地使用统一数组来保存动态数据(例如灯光)时,使用纹​​理来存储浮点数据并不是一个好主意。

下面使用 webGL 的示例(不是 webGL2,这是更好的选择)有两个统一数组

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

它们是通过 gl.uniform3fv 设置的。注意v代表vector(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>