ShaderProgram
Two programs are needed to draw an object: a vertex shader that positions each vertex in the framebuffer and a fragment shader that colors the pixels between vertices. These shaders are written in the OpenGL Shading Language (GLSL).
Here's a very simple vertex shader that just echoes out the incoming position
attribute:
in vec3 position;
void main() {
gl_Position = vec4(position, 1.0);
}
in vec3 position; void main() { gl_Position = vec4(position, 1.0); }
Some graphics developers put shader code directly in a string embedded in the program. That's compact, but we want syntax highlighting and line numbers. Instead, put the vertex shader code in its own file named flat-vertex.glsl
in the apps/hello-cornflower
directory.
Any top-level variable declaration marked in
in a GLSL shader corresponds to an incoming vertex property that was pulled from a vertex buffer. This particular vertex shader only pulls in the property that we named position
. Its type is vec3
, a vector of three floats. It ignores the color
property.
The very least that a vertex shader must do is assign a vec4
to the gl_Position
variable, which tells the graphics card where on the framebuffer the vertex lands. As we develop more advanced renderers, we'll do more than just echo position
as we do here. We'll also discover why gl_Position
has a fourth element.
The job of the fragment shader is to emit a color for each pixel. This fragment shader does so by writing the color blue to the out
variable fragmentColor
:
out vec4 fragmentColor;
void main() {
fragmentColor = vec4(0.0, 0.0, 1.0, 1.0);
}
out vec4 fragmentColor; void main() { fragmentColor = vec4(0.0, 0.0, 1.0, 1.0); }
Put this code in a file named flat-fragment.glsl
in the apps/hello-cornflower
directory.
One of several things will happen to the color written to fragmentColor
. If the fragment is blocked or occluded by objects nearer to the viewer, the fragment will be discarded. If it's not blocked, the color will be written to the framebuffer. If the scene contains semi-transparent surfaces, the color will be blended with a color written earlier.
The vertex and fragment shaders must be compiled down to the GPU's machine code and linked together to form a complete shader program. The machinery to do this is also quite low-level, so we abstract it away with another class. Create the file lib/shader-program.ts
in your hello-cornflower
project and copy in this code:
export class ShaderProgram {
vertexShader: WebGLShader;
fragmentShader: WebGLShader;
program: WebGLProgram;
uniforms: {[name: string]: WebGLUniformLocation};
isBound: boolean;
constructor(vertexSource: string, fragmentSource: string, version: number = 300, precision = 'mediump') {
this.isBound = false;
// Compile.
this.vertexShader = this.compileSource(gl.VERTEX_SHADER, `#version ${version} es\n${vertexSource}`);
this.fragmentShader = this.compileSource(gl.FRAGMENT_SHADER, `#version ${version} es\nprecision ${precision} float;\n${fragmentSource}`);
// Link.
this.program = gl.createProgram()!;
gl.attachShader(this.program, this.vertexShader);
gl.attachShader(this.program, this.fragmentShader);
gl.linkProgram(this.program);
let isOkay = gl.getProgramParameter(this.program, gl.LINK_STATUS);
if (!isOkay) {
let message = gl.getProgramInfoLog(this.program);
gl.deleteProgram(this.program);
throw message;
}
// Query uniforms.
this.uniforms = {};
let nuniforms = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < nuniforms; ++i) {
let uniform = gl.getActiveUniform(this.program, i)!;
let location = gl.getUniformLocation(this.program, uniform.name)!;
this.uniforms[uniform.name] = location;
// If uniform is an array, find locations of other elements.
for (let elementIndex = 1; elementIndex < uniform.size; ++elementIndex) {
const elementName = uniform.name.replace(/\[0\]$/, `[${elementIndex}]`);
location = gl.getUniformLocation(this.program, elementName)!;
if (location) {
this.uniforms[elementName] = location;
}
}
}
this.unbind();
}
destroy() {
gl.deleteShader(this.vertexShader);
gl.deleteShader(this.fragmentShader);
gl.deleteProgram(this.program);
}
compileErrorReport(message: string, type: number, source: string) {
let report = `I found errors in a ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader.\n`;
let matches = message.matchAll(/^ERROR: (\d+):(\d+): (.*)$/gm);
for (let match of matches) {
let line = parseInt(match[2]) - 1;
let NEIGHBOR_COUNT = 2;
let lines = source.split(/\r?\n/);
let startLine = Math.max(0, line - NEIGHBOR_COUNT);
let endLine = Math.min(lines.length - 1, line + NEIGHBOR_COUNT);
let lineIndexWidth = (endLine + 1).toString().length;
report += `\nError on line ${line + 1}: ${match[3]}\n\n`;
for (let i = startLine; i <= endLine; ++i) {
if (i === line) {
report += `! ${(i + 1).toString().padStart(lineIndexWidth, ' ')} ${lines[i]}\n`;
} else {
report += ` ${(i + 1).toString().padStart(lineIndexWidth, ' ')} ${lines[i]}\n`;
}
}
}
return report;
}
compileSource(type: number, source: string) {
let shader = gl.createShader(type)!;
gl.shaderSource(shader, source);
gl.compileShader(shader);
let isOkay = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!isOkay) {
let message = gl.getShaderInfoLog(shader)!;
gl.deleteShader(shader);
throw new ShaderCompilationError(this.compileErrorReport(message, type, source));
}
return shader;
}
getAttributeLocation(name: string) {
return gl.getAttribLocation(this.program, name);
}
bind() {
gl.useProgram(this.program);
this.isBound = true;
}
unbind() {
gl.useProgram(null);
this.isBound = false;
}
assertUniform(name: string) {
if (!this.uniforms.hasOwnProperty(name)) {
console.warn(`${name} isn't a valid uniform.`);
}
}
setUniform1i(name: string, value: number) {
this.assertUniform(name);
gl.uniform1i(this.uniforms[name], value);
}
setUniform1f(name: string, value: number) {
this.assertUniform(name);
gl.uniform1f(this.uniforms[name], value);
}
setUniform2f(name: string, a: number, b: number) {
this.assertUniform(name);
gl.uniform2f(this.uniforms[name], a, b);
}
setUniform3f(name: string, a: number, b: number, c: number) {
this.assertUniform(name);
gl.uniform3f(this.uniforms[name], a, b, c);
}
setUniformMatrix4fv(name: string, elements: Float32Array) {
this.assertUniform(name);
gl.uniformMatrix4fv(this.uniforms[name], false, elements);
}
}
class ShaderCompilationError extends Error {
constructor(message: string) {
super(message);
}
}
export class ShaderProgram { vertexShader: WebGLShader; fragmentShader: WebGLShader; program: WebGLProgram; uniforms: {[name: string]: WebGLUniformLocation}; isBound: boolean; constructor(vertexSource: string, fragmentSource: string, version: number = 300, precision = 'mediump') { this.isBound = false; // Compile. this.vertexShader = this.compileSource(gl.VERTEX_SHADER, `#version ${version} es\n${vertexSource}`); this.fragmentShader = this.compileSource(gl.FRAGMENT_SHADER, `#version ${version} es\nprecision ${precision} float;\n${fragmentSource}`); // Link. this.program = gl.createProgram()!; gl.attachShader(this.program, this.vertexShader); gl.attachShader(this.program, this.fragmentShader); gl.linkProgram(this.program); let isOkay = gl.getProgramParameter(this.program, gl.LINK_STATUS); if (!isOkay) { let message = gl.getProgramInfoLog(this.program); gl.deleteProgram(this.program); throw message; } // Query uniforms. this.uniforms = {}; let nuniforms = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS); for (let i = 0; i < nuniforms; ++i) { let uniform = gl.getActiveUniform(this.program, i)!; let location = gl.getUniformLocation(this.program, uniform.name)!; this.uniforms[uniform.name] = location; // If uniform is an array, find locations of other elements. for (let elementIndex = 1; elementIndex < uniform.size; ++elementIndex) { const elementName = uniform.name.replace(/\[0\]$/, `[${elementIndex}]`); location = gl.getUniformLocation(this.program, elementName)!; if (location) { this.uniforms[elementName] = location; } } } this.unbind(); } destroy() { gl.deleteShader(this.vertexShader); gl.deleteShader(this.fragmentShader); gl.deleteProgram(this.program); } compileErrorReport(message: string, type: number, source: string) { let report = `I found errors in a ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader.\n`; let matches = message.matchAll(/^ERROR: (\d+):(\d+): (.*)$/gm); for (let match of matches) { let line = parseInt(match[2]) - 1; let NEIGHBOR_COUNT = 2; let lines = source.split(/\r?\n/); let startLine = Math.max(0, line - NEIGHBOR_COUNT); let endLine = Math.min(lines.length - 1, line + NEIGHBOR_COUNT); let lineIndexWidth = (endLine + 1).toString().length; report += `\nError on line ${line + 1}: ${match[3]}\n\n`; for (let i = startLine; i <= endLine; ++i) { if (i === line) { report += `! ${(i + 1).toString().padStart(lineIndexWidth, ' ')} ${lines[i]}\n`; } else { report += ` ${(i + 1).toString().padStart(lineIndexWidth, ' ')} ${lines[i]}\n`; } } } return report; } compileSource(type: number, source: string) { let shader = gl.createShader(type)!; gl.shaderSource(shader, source); gl.compileShader(shader); let isOkay = gl.getShaderParameter(shader, gl.COMPILE_STATUS); if (!isOkay) { let message = gl.getShaderInfoLog(shader)!; gl.deleteShader(shader); throw new ShaderCompilationError(this.compileErrorReport(message, type, source)); } return shader; } getAttributeLocation(name: string) { return gl.getAttribLocation(this.program, name); } bind() { gl.useProgram(this.program); this.isBound = true; } unbind() { gl.useProgram(null); this.isBound = false; } assertUniform(name: string) { if (!this.uniforms.hasOwnProperty(name)) { console.warn(`${name} isn't a valid uniform.`); } } setUniform1i(name: string, value: number) { this.assertUniform(name); gl.uniform1i(this.uniforms[name], value); } setUniform1f(name: string, value: number) { this.assertUniform(name); gl.uniform1f(this.uniforms[name], value); } setUniform2f(name: string, a: number, b: number) { this.assertUniform(name); gl.uniform2f(this.uniforms[name], a, b); } setUniform3f(name: string, a: number, b: number, c: number) { this.assertUniform(name); gl.uniform3f(this.uniforms[name], a, b, c); } setUniformMatrix4fv(name: string, elements: Float32Array) { this.assertUniform(name); gl.uniformMatrix4fv(this.uniforms[name], false, elements); } } class ShaderCompilationError extends Error { constructor(message: string) { super(message); } }
The constructor of this class expects the vertex and fragment shader source as strings, but we have them in separate files. We need to load the files in when the page loads using JavaScript's fetch
function. Because fetching could be slow, the function is asynchronous. Parsing the HTTP response is also asynchronous. So we write this wrapper function to manage the pipeline:
export async function fetchText(url: string) {
const response = await fetch(url);
const text = await response.text();
return text;
}
export async function fetchText(url: string) { const response = await fetch(url); const text = await response.text(); return text; }
Create the file lib/web-utilities.ts
and paste this code in. We'll add more functions to this file throughout the semester.
Back in initialize
in main.ts
, load, compile, and link the shaders together by constructing an instance of ShaderProgram
, like this:
const vertexSource = await fetchText('flat-vertex.glsl');
const fragmentSource = await fetchText('flat-fragment.glsl');
shaderProgram = new ShaderProgram(vertexSource, fragmentSource);
const vertexSource = await fetchText('flat-vertex.glsl'); const fragmentSource = await fetchText('flat-fragment.glsl'); shaderProgram = new ShaderProgram(vertexSource, fragmentSource);
Note that the shaderProgram
variable lacks a declaration like let
or const
. Other functions will need to access it, so it needs to have a bigger scope. Declare this variable at the top of main.ts
as a global:
let shaderProgram: ShaderProgram;
let shaderProgram: ShaderProgram;
You still won't see the line segment rendered. We need to marry the set of VertexAttributes
to the ShaderProgram
together and then issue a draw call.