Thursday, March 21, 2013

WebGL: Fixing "INVALID_OPERATION: drawArrays: attribs not setup correctly"

I kept getting the WebGL: INVALID_OPERATION: drawArrays: attribs not setup correctly error in my simple WebGL app, which wasn't very helpful to me. Ok, I get it's not setup correctly, but what could be wrong? It turns out the problem was being caused by me using two different shader programs, with each program having a different number of attributes. I want to walk through this clearly, because most of the things I found online when searching for solutions were... lacking, to put it kindly. Let's look at my shaders:

Shader Program #1

It's a pretty simple program. Given a set of 2D vertices, make them all white.

Vertex shader:
attribute vec2 vertex;

void main(void)
{
    gl_Position = vec4(vertex, 0.0, 1.0);
}
Fragment shader:
precision mediump float;

void main(void)
{
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

Shader Program #2

Not too complicated. Given a set of 2D vertices and corresponding texture coordinates, render a textured object.

Vertex shader:
attribute vec2 vertex;
attribute vec2 textureCoord;

varying vec2 fragTextureCoord;

void main(void)
{
    gl_Position = vec4(vertex, 0.0, 1.0);
    fragTextureCoord = textureCoord;
}
Fragment shader:
precision mediump float;

uniform sampler2D sampler;

varying vec2 fragTextureCoord;

void main(void)
{
    gl_FragColor = texture2D(sampler, fragTextureCoord);
}

The problem

The problem is rooted in the shaders. The first vertex shader only has one attribute (vertex), while the second vertex shader has two attributes (vertex and textureCoord). When switching from one program to another, the enabled attributes weren't getting switched! In other words, when I loaded shader program #1 and called enableVertexAttribArray to enable the vertex attribute, and then later when I loaded shader program #2 and called enableVertexAttribArray to enable the vertex and textureCoord attributes, the enabling of the attributes isn't bound to a specific program. Enabling an attribute is a global operation, regardless of whether or not you create or use a different program. So when using shader program #1, the textureCoord attribute was still enabled! This resulted in an error (which seems sensible). There are two ways to work around this.

Solution #1: disableVertexAttribArray

One option is to just disable the extra attributes. When we use shader program #1, we can call disableVertexAttribArray to disable the extra attributes that shader program #2 uses. You have to watch out a little if you do this, because if you aren't explicitly assigning index numbers to your attributes, it's possible the vertex attribute in shader programs #1 and #2 will have different index values. One way to do this is to track which program you were using and which program you're going to use now, and then disable any extra attributes (or enable any missing attributes). Something like:

function switchPrograms(currentProgram, newProgram)
{
    // Gets the number of attributes in the current and new programs
    var currentAttributes = gl.getProgramParameter(currentProgram, gl.ACTIVE_ATTRIBUTES);
    var newAttributes = gl.getProgramParameter(newProgram, gl.ACTIVE_ATTRIBUTES);

    // Fortunately, in OpenGL, attribute index values are always assigned in the
    // range [0, ..., NUMBER_OF_VERTEX_ATTRIBUTES - 1], so we can use that to
    // enable or disable attributes
    if (newAttributes > currentAttributes) // We need to enable the missing attributes
    {
        for (var i = currentAttributes; i < newAttributes; i++)
        {
            gl.enableVertexAttribArray(i);
        }
    }
    else if (newAttributes < currentAttributes) // We need to disable the extra attributes
    {
        for (var i = newAttributes; i < currentAttributes; i++)
        {
            gl.disableVertexAttribArray(i);
        }
    }

    // With all the attributes now enabled/disabled as they need to be, let's switch!
    gl.useProgram(newProgram);
}

Note that the above solution assumes all your attributes are array attributes. If you have an attribute that isn't an array (i.e. it shouldn't be enabled with enableVertexAttribArray), you'll have to come up with a more robust solution. Hopefully the above at least gives you some ideas and can help point you in the right direction in your quest to come up with a valid solution.

Solution #2: vertexAttribPointer

Enable all the attributes! Alright, so the whole reason we were getting this INVALID_OPERATION error comes from here (I suggest you read it and try to understand it!). The key here is in the last statement: "If a vertex attribute is enabled as an array, a buffer is bound to that attribute, but the attribute is not consumed by the current program, then regardless of the size of the bound buffer, it will not cause any error to be generated during a call to drawArrays or drawElements."

This means that if your program doesn't use the attribute, it's okay to fill it with a tiny junk array (note: not null, as that has other implications), as it won't actually be used. This means you can bind a tiny junk buffer with bindBuffer and then set the superfluous attribute to that tiny junk buffer with vertexAttribPointer, and then continue on your merry way. All WebGL wants is for your enabled array attributes to be associated with some valid (non-null) array (which is why you get the INVALID_OPERATION error if they're not). If you enable all the attributes, but your program doesn't actually use an enabled attribute, WebGL doesn't care how big the array is that's associated with it (so you won't get any out-of-bounds errors). It's just happy it has an array associated with the enabled attribute.

1 comment:

  1. Hi Michael,
    I appreciated this article. I am trying to get a three.js program working where I use two different shaders. I have never dealt directly with webgl itself before and was having trouble figuring out how exactly to implement what you solution #2

    Thanks!

    Eric

    ReplyDelete

Note: Only a member of this blog may post a comment.