Interpolation
In the last chapter we mentioned that vertex properties are interpolated across a primitive. Now that we're rasterizing triangles, interpolation becomes critical as the card fills in the many pixels between their vertices.
Suppose we give each vertex of a triangle a different color. The pixels in the middle will be given a weighted blend of the colors at the three vertices, like this:
The graphics card performs linear interpolation, which means that the blending formula is a linear equation. Suppose we are rasterizing the fragments between two endpoints on a line segment. Let's give names to the following quantities:
- The value of the vertex property at the starting endpoint is \(a\).
- The value of the vertex property at the ending endpoint is \(b\).
- We are at proportion \(t\) between the two endpoints.
The interpolated value at proportion \(t\) is computed with this equation:
For example, if we are 75% of the way through the segment, then we'll blend 75% of \(b\) with 25% of \(a\). If we are 10% of the way through, we'll blend 90% of \(a\) with 10% of \(b\). The nearer endpoint has a stronger influence on the blend.
When the graphics card interpolates between vertices, it is guessing what the intermediate values might be. Linear interpolation is just one guessing strategy. It is popular because it's fast and simple. However, it also produces sharp discontinuities when two interpolated ramps meet at a shared edge, as seen in this renderer:
Other interpolation strategies achieve smoother transitions by taking into account more than two neighboring values.
To get linear interpolation in a renderer, we must do the following:
- Include the properties we want to interpolate in a vertex buffer.
-
Receive the properties as
in
variables in the vertex shader. -
Assign the properties to
out
variables in the vertex shader. -
Receive the interpolated properties as
in
variables in the fragment shader.
This code adds a color
attribute to the VBO:
const positions = [
-0.5, -0.5, 0, // 0
0.5, -0.5, 0, // 1
-0.5, 0.5, 0, // 2
];
const colors = [
1, 0, 0, // 0
0, 1, 0, // 1
0, 0, 1, // 2
];
attributes = new VertexAttributes();
attributes.addAttribute('position', 3, 3, positions);
attributes.addAttribute('color', 3, 3, colors);
const positions = [ -0.5, -0.5, 0, // 0 0.5, -0.5, 0, // 1 -0.5, 0.5, 0, // 2 ]; const colors = [ 1, 0, 0, // 0 0, 1, 0, // 1 0, 0, 1, // 2 ]; attributes = new VertexAttributes(); attributes.addAttribute('position', 3, 3, positions); attributes.addAttribute('color', 3, 3, colors);
The vertex shader receives the color just like it receives the position, as an in
variable. The color isn't employed in any meaningful computation in the vertex shader, but it is copied to an out
variable named mixColor
:
in vec3 position;
in vec3 color;
out vec3 mixColor;
void main() {
gl_Position = vec4(position, 1.0);
mixColor = color;
}
in vec3 position; in vec3 color; out vec3 mixColor; void main() { gl_Position = vec4(position, 1.0); mixColor = color; }
There's no standard naming convention for interpolated variables, but perhaps there should be. We use the prefix mix
.
The graphics card automatically performs interpolation on all of a vertex shader's out
variables and assigns the blend to a fragment shader's corresponding in
variables. We can use the interpolated in
variables like any other variable. Here it is used to assign the fragment color:
in vec3 mixColor;
out vec4 fragmentColor;
void main() {
fragmentColor = vec4(mixColor, 1.0);
}
in vec3 mixColor; out vec4 fragmentColor; void main() { fragmentColor = vec4(mixColor, 1.0); }
Any numeric type can be interpolated. Once we add lighting and textures, we'll interpolate spatial and material properties. For now, we'll stick to just color.