14
votes

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!

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.david van brink

1 Answers

18
votes

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>