How to bind an array of textures to a WebGL shader uniform?
Asked Answered
L

1

16

I need to handle many objects which share only a few textures. Defining and loading textures one by one manually (as described in another post on SO) does not feel right... even more so since there's no switch (index) {case:...} statement in WebGL.

So I wanted to pass the texture to use for a vertex as a vertex attribute, and use this number as an index into some "array of textures" in the fragement shader. But the OpenGL wiki on Samplers (not quite the perfect reference for WebGL, but the one I found) says:

A variable of sampler can only be defined in one of two ways. It can be defined as a function parameter or as a uniform variable.

uniform sampler2D texture1;

That to me sounds like I can have no array of samplers. I've read a few pages on texture units, but until now, that remains a mystery to me.

In the SO post cited above, Toji hinted at a solution, but wanted a separate question - voila!

Thanks, nobi

PS: I know the other possibility of using a "texture atlas" - if this is more efficient or less complicated - I'd be happy to hear experiences!

Listless answered 25/10, 2013 at 14:41 Comment(1)
Attribute-tagging is a pretty good idea IMO. I know it goes against one's better instincts, but manual loading and if(aId == 1)c = texture2D(sampler1,p); else if(aID == 2)c = texture2D(sampler2... does at least work. :-/ Texture units are like "scratch pad registers" for textures. Some GL operations, including drawing, need to affiliate 1 texture with 1 texture unit, and it's up to us to dole them out as we see fit.Tameka
S
19

You have to index sampler arrays with constant values so you can do something like this

#define numTextures 4

precision mediump float;
varying float v_textureIndex;
uniform sampler2D u_textures[numTextures];

vec4 getSampleFromArray(sampler2D textures[4], int ndx, vec2 uv) {
    vec4 color = vec4(0);
    for (int i = 0; i < numTextures; ++i) {
      vec4 c = texture2D(u_textures[i], uv);
      if (i == ndx) {
        color += c;
      }
    }
    return color;
}

void main() {
    gl_FragColor = getSampleFromArray(u_textures, int(v_textureIndex), vec2(0.5, 0.5));
}

You also need to tell it which texture units to use

var textureLoc = gl.getUniformLocation(program, "u_textures");
// Tell the shader to use texture units 0 to 3
gl.uniform1iv(textureLoc, [0, 1, 2, 3]);

The sample above uses a constant texture coord just to keep it simple but of course you can use any texture coordinates.

Here's a sample:

var canvas = document.getElementById("c");
var gl = canvas.getContext('webgl');

// Note: createProgramFromScripts will call bindAttribLocation
// based on the index of the attibute names we pass to it.
var program = webglUtils.createProgramFromScripts(
    gl, 
    ["vshader", "fshader"], 
    ["a_position", "a_textureIndex"]);
gl.useProgram(program);
var textureLoc = gl.getUniformLocation(program, "u_textures[0]");
// Tell the shader to use texture units 0 to 3
gl.uniform1iv(textureLoc, [0, 1, 2, 3]);

var positions = [
      1,  1,  
     -1,  1,  
     -1, -1,  
      1,  1,  
     -1, -1,  
      1, -1,  
];
    
var textureIndex = [
    0, 1, 2, 3, 0, 1,
];

var vertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

var vertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(textureIndex), gl.STATIC_DRAW);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 1, gl.UNSIGNED_BYTE, false, 0, 0);

var colors = [
    [0, 0, 255, 255],
    [0, 255, 0, 255],
    [255, 0, 0, 255],
    [0, 255, 255, 255],
];

// make 4 textures
colors.forEach(function(color, ndx) {
    gl.activeTexture(gl.TEXTURE0 + ndx);
    var tex = gl.createTexture();
   gl.bindTexture(gl.TEXTURE_2D, tex);
   gl.texImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(color));
});


gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
canvas { border: 1px solid black; }
<script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>
<script id="vshader" type="whatever">
    attribute vec4 a_position;
    attribute float a_textureIndex;
    varying float v_textureIndex;
    void main() {
      gl_Position = a_position;
      v_textureIndex = a_textureIndex;
    }    
</script>
<script id="fshader" type="whatever">
#define numTextures 4
precision mediump float;
varying float v_textureIndex;
uniform sampler2D u_textures[numTextures];
    
vec4 getSampleFromArray(sampler2D textures[4], int ndx, vec2 uv) {
    vec4 color = vec4(0);
    for (int i = 0; i < numTextures; ++i) {
      vec4 c = texture2D(u_textures[i], uv);
      if (i == ndx) {
        color += c;
      }
    }
    return color;
}
    
void main() {
    gl_FragColor = getSampleFromArray(u_textures, int(v_textureIndex + 0.5), vec2(0.5, 0.5));
}
</script>
<canvas id="c" width="300" height="300"></canvas>
Sisile answered 26/10, 2013 at 18:56 Comment(8)
Thanks a lot, this looks promising. I am only wondering why you need to write if (ndx==0) {...textures[0]... instead of just writing textures[ndx]? And get rid of function getSampleFromArray entirely? I'll try this as soon as I'm back from vacation --Listless
Because WebGL, which is based on OpenGL ES 2.0, doesn't allow indexing a sampler array with anything but a constant-index-expression. See GLSL 1.0.17 Appendix A section 5 [khronos.org/registry/gles/specs/2.0/… which means you can index with a constant or a loop index based on constants but nothing else.Sisile
You could do other things like loop through all the textures and multiply by a color. Whether that's faster or slower I don't know. In other words uniform vec4 u_colors[4]; ... vec4 color = vec4(0,0,0,0); for (int i = 0; i < 4; ++i) { color += texture2D(u_samplers[i], uv) * u_colors[i]; }; gl_FragColor = color; ... then you can turn off individual textures by setting u_color. In your case I guess you could also do if (i == (int)(v_textureIndex)) { color = texture2D(...) }Sisile
works like a charm. It took me a while to figure out that gl.uniform1iv(textureLoc, [0, 1, 2, 3]); is setting u_textures[0] through u_textures[3], though (at least - I think it's doing that). And there was a limit of 32 textures (and consequently, texture units) being in use at any one time, correct? Thanks a lotListless
Yes, gl.uniform1iv is setting u_textures[0] -> [3]. The limit for texture units is GPU specific. WebGL requires at least 8. You can call gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) to find out how many you you get on the user's machine. Vertex shaders have their own limit gl.getParameter(gl.VERTEX_MAX_TEXTURE_IMAGE_UNITS) which can be 0.Sisile
Any easy way to do this using TWGL? Shouldn't it do it all automatically? Passing a 1-element array as uniform doesn't seem to work...Shammer
Oh, actually when the array size is 1, it only works by setting the uniform to the texture, not a 1-element array containing the texture, buf for other values it works just fine :)Shammer
It works just fine with a one element array. you have some other bug if it's not working for you.Sisile

© 2022 - 2024 — McMap. All rights reserved.