WebGL Attributes
Previously we only drew one point per one draw call. How do we draw multiple primitives in one draw call? Uniforms are like shader constants, they do not vary between vertices, so they are not suitable for this.
WebGL attributes allow passing a buffer that describes some information (e.g. position, point size or any other input to shader calculations) for each vertex individually.
Basically:
uniform values
|---> vertex shader instance 0
|---> vertex shader instance 1
...
|---> fragment shader instance 0
|---> fragment shader instance 1
...
attribute value 0 --> vertex shader instance 0
attribute value 1 --> vertex shader instance 1
...
Since each vertex connects to its rendering fragment(s?), you can pass per-vertex information (e.g. color) downstream to corresponding fragement shaders. In WebGL1, this is called varying <type> v<Name>;
, e.g. varying vec4 vColor;
in both vertex/fragment shaders.
WebGL2 changes this to out vec4 vColor;
in the vertex shader and in vec4 vColor;
in the fragment shader. Values for a vertex are then passed downstream to corresponding fragment shader instances.
Limits and WebGL1/WebGL2 syntax
Each WebGL program is guaranteed at least 16 attributes (check gl.getParameter(gl.MAX_VERTEX_ATTRIBS)
)
Each attribute is guaranteed to have at least 65536 vertices.
// WebGL1, GLSL 1.0:
attribute vec4 aPosition;
attribute float aAlpha;
// WebGL2, GLSL 3.0 ES
in vec4 aPosition;
in float aAlpha;
// WebGL1, GLSL 1.0
varying vec4 vColor;
// WebGL2, GLSL 3.0 ES
out vec3 vColor; // vertex shader
in vec3 vColor; // fragment shader
in
and out
in WebGL2:
// vertex.glsl
#version 300 es
in vec4 aPosition; // from WebGL
out vec4 vColor; // to fragment shader
...
// fragment.glsl
#version 300 es
in vec4 vColor; // from vertex shader
out vec4 fragColor; // to fragment output
...
Attribute locations
Each vertex attribute (e.g. aPosition
, aColor
, etc) are identified by indexes called attribute locations. Attribute locations are just numbers, you get them via gl.getAttribLocation(program, 'aName')
. They are allocated dynamically, unless you pre-bind them to desired fixed numbers.
Attribute locations can be bound to fixed values before linking the shaders:
// bind aName to location 0
gl.bindAttribLocation(program, 0, 'aName');
or in GLSL 3.0:
// bind aName to location 0
layout(location = 0) in vec2 aName;
Drawing using attributes
Now let's draw using attributes in one call:
// vertex.glsl
#version 300 es
// set per vertex:
in float aPointSize;
in vec2 aPosition;
void main() {
gl_PointSize = aPointSize;
gl_Position = vec4(aPosition, 0.0, 1.0);
}
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aPointSize = gl.getAttribLocation(program, 'aPointSize');
// do check for -1
Vertex attributes must be enabled before use (otherwise they zeroed out?):
gl.enableVertexAttribArray(aPosition);
gl.enableVertexAttribArray(aPointSize);
Passing data to attributes
Let's say we have 2 vertices described by contiguous chunks of bytes (e.g. floats):
const bufferData = new Float32Array([
/* [0] */ /*position*/0.0, 0.0, /*size*/100,
/* [1] */ /*position*/0.4, 0.2, /*size*/50,
]);
const pointLen = 3; // one chunk is 3 contiguous floats
// position of a vertex starts at the 0th float in a chunk:
const pointPositionOffset = 0;
// position of a vertex occupies two floats:
const pointPositionSize = 2;
// point size of a vertex starts at the float [2] in a chunk:
const pointSizeOffset = 2;
const pointSizeSize = 1; // one float
To pass data to attributes, you need a WebGL buffer. You can have multiple buffers and bind them to a target, which is like a "slot" for WebGL functions to use. There are specific targets used by specific WebGL functions: e.g. gl.vertexAttribPointer
uses the gl.ARRAY_BUFFER
target and, correspondingly, the buffer currently bound to it.
const buffer = gl.createBuffer();
gl.bindBuffer(/*target*/gl.ARRAY_BUFFER, /*object*/buffer);
// Buffer data are passed to a target, not a buffer directly!
// Since `buffer` is currenly bound to gl.ARRAY_BUFFER, it will be used.
gl.bufferData(gl.ARRAY_BUFFER, bufferData, gl.STATIC_DRAW);
// Then we tell GPU the layout of the data,
// different portions of it describing different attributes:
const FLOAT_SIZE = Float32Array.BYTES_PER_ELEMENT; // = 4
gl.vertexAttribPointer(
/*attrib*/ aPosition,
/*size, type*/ pointPositionSize, gl.FLOAT,
/*normalized*/ false,
/*stride*/ pointLen * FLOAT_SIZE,
/*offset*/ pointPositionOffset * FLOAT_SIZE,
);
gl.vertexAttribPointer(
/*attrib*/ aPointSize,
/*size, type*/ pointSizeSize, gl.FLOAT,
/*normalized*/ false,
/*stride*/ pointLen * FLOAT_SIZE,
/*offset*/ pointSizeOffset * FLOAT_SIZE,
);
gl.drawArrays(gl.POINTS, /*start index*/0, /*n_vertices*/2);
Note that stride and offset are calcalated in bytes. Generally, you can have an array of arbitrary C structures containing floats in them.
Normalization
Useful when:
- converting uint8/uin16/int to float.
- float clamped to [0.0 .. 1.0].
Threading color into fragment shader
Let's add colors to our vertex data:
const bufferData = new Float32Array([
/* [0] */ /*position*/0.0, 0.0,
/*size*/ 100,
/*color*/ 1.0, 0.0, 0.0,
/* [1] */ /*position*/0.4, 0.2,
/*size*/50,
/*color*/ 0.0, 0.0, 1.0,
]);
const FLOAT_SIZE = 4;
const stride = 6 * FLOAT_SIZE;
const offsetPosition = 0;
const offsetPointSize = 2 * FLOAT_SIZE;
const offsetColor = 3 * FLOAT_SIZE;
// vertex.glsl
in vec3 aColor;
out vec3 vColor;
void main() {
// or calculate vColor depending on the location/size/anything!
vColor = aColor;
...
}
// fragment.glsl
in vec3 vColor;
out vec4 fragColor;
void main() {
fragColor = vColor;
}
const aColor = gl.getAttribLocation(program, 'aColor');
gl.enableVertexAttribArray(aColor);
gl.vertexAttribPointer(
aPosition, // attrib location
2, // size
gl.FLOAT, // type
false, // normalized int/uint
stride, // the stride size in bytes
offsetPosition,
);
gl.vertexAttribPointer(
aPointSize,
1,
gl.FLOAT,
false, // normalized, if converting from int/uint
stride, // the stride
offsetPointSize,
);
gl.vertexAttribPointer(
aColor,
3,
gl.FLOAT,
false,
stride,
offsetColor,
);
Generic attribute values
gl.vertexAttrib2fv()
does the same to attributes as gl.uniform2fv()
for uniforms.
Demo
The attributes demo draws three vertex points with their position, size and color defined via attribute values: