Shader anatomy & data flow
What goes into a shader and what comes out of it, from the vertex buffer to the color on screen.
How it works
A shader never runs on its own. Code on the CPU (JavaScript, in WebGL2) prepares everything and then issues a draw
call: “draw these 3000 vertices as triangles, with this program”. The program is a pair of shaders linked together, a
vertex shaderThe GPU program that runs once for every vertex of a mesh. It places the vertex on screen by writing gl_Position, and passes values on to the fragment shader. Vertex shader The GPU program that runs once for every vertex of a mesh. It places the vertex on screen by writing gl_Position, and passes values on to the fragment shader. Fragment shader The GPU program that runs once for every pixel a triangle covers and decides its color.
Four kinds of data
- Attributes are per vertex: a position, a normal, a UV, read from buffers the host code filled. The vertex
shader declares them with
in, and each run gets the values of one vertex. - UniformsA value the host code sets before a draw call that stays the same for every vertex and pixel of that draw, like the time, the canvas size or a color.Rendering
Uniform
A value the host code sets before a draw call that stays the same for every vertex and pixel of that draw, like the time, the canvas size or a color.
are per draw call: the time, the canvas size, a color, a transform matrix. The host sets them withgl.uniform*()before drawing, and every run of both shaders sees the same value. Textures arrive this way too, assampler2Duniforms. - Varyings carry data from the vertex shader to the fragment shader. The vertex shader writes an
out, the fragment shader reads aninwith the same name and type, and in between the rasterizerThe fixed GPU step that turns each triangle into fragments, one for every pixel center it covers, and interpolates the vertex shader's outputs for each of them.RenderingRasterization
The fixed GPU step that turns each triangle into fragments, one for every pixel center it covers, and interpolates the vertex shader's outputs for each of them.
blends the three vertices’ values for every pixel of the triangle. - Outputs leave the fragment shader with
out vec4and land in the framebuffer: the canvas, or a texture the host chose to draw into.
Some values are built in rather than declared. The vertex shader must write gl_Position, where the vertex lands on
screen. The fragment shader can read gl_FragCoord, the pixel’s position in the window.
A complete program
This is the smallest useful pair in GLSL ES 3.00, the language of WebGL2. The vertex shader passes the UV along and places the vertex; the fragment shader turns the interpolated UV into a color that pulses with time:
#version 300 es
// vertex shader
in vec3 aPosition; // attribute: this vertex's position
in vec2 aUv; // attribute: this vertex's UV
uniform mat4 uModelViewProjection; // uniform: the same for the whole draw
out vec2 vUv; // varying: handed to the fragment shader
void main() {
vUv = aUv;
gl_Position = uModelViewProjection * vec4(aPosition, 1.0);
}
#version 300 es
// fragment shader
precision highp float;
in vec2 vUv; // the varying, interpolated for this pixel
uniform float uTime; // seconds, set by the host every frame
out vec4 fragColor; // what lands in the framebuffer
void main() {
float pulse = 0.5 + 0.5 * sin(uTime);
fragColor = vec4(vUv, pulse, 1.0);
}
A few lines here are boilerplate that the compiler insists on. #version 300 es must be the very first line, with
nothing before it, not even a comment or a blank line; a shader without it is read as the older GLSL ES 1.00. The fragment shader has no default precision for floats, so
precision highp float; is required there; the vertex shader defaults to highp. When the program is linked, every
in of the fragment shader must match an out of the vertex shader by name and type, or linking fails.
The same data, hidden
Most tools write this boilerplate for you and hand you only the interesting part:
- Shadertoy-style pages draw one rectangle over the whole canvas and call your
mainImage(out vec4 fragColor, in vec2 fragCoord)for every pixel, with uniforms like the time and resolution already declared. - Godot writes
main(), the attributes and the matrices itself. You fill invertex()andfragment()functions and talk to it through built-ins: readUV,NORMALandTIME, writeVERTEXinvertex()andALBEDOorALPHAinfragment(). Auniformyou declare becomes a property in the material’s inspector (a slider when it hashint_range), and avaryingcarries your own data between the two functions. - The cards on this site go one step further: you write
vec4 effect(Inputs i)and readi.uv,i.time,i.normaland the rest from one struct. The site’s frame declares the real attributes, uniforms and varyings around it, for WebGL2 and for Godot alike (see UV coordinatesA 2D position on a surface or the screen, usually from 0 to 1 on both axes. Shaders draw shapes and patterns as functions of it.FundamentalUV coordinates
A 2D position on a surface or the screen, usually from 0 to 1 on both axes. Shaders draw shapes and patterns as functions of it.
uv = gl_FragCoord.xy / resolutionSources).- gl_FragCoord (opens in a new tab)Reference
- UV mapping (opens in a new tab)Reference
The names change from tool to tool, but the flow doesn’t: per-vertex data in, per-draw settings beside it, values handed from vertex to fragment, one color out.