Shader Source

Writing Vertex and Fragment Shader Source

shaderSource() takes a plain string, so GLSL can live wherever JavaScript can read text:

Wherever it lives, the source must begin with #version 300 es. A <script> element's text starts with the line break after >, which pushes the directive to line 2, and ANGLE 2,592 rejects the shader with "#version directive must occur on the first line of the shader". GLH.shader() trims the source for that reason.

Shader source from a template literal and from script elements, and why the text must be trimmedHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .names { display: flex; max-width: 600px; text-align: center; }
  .names div { flex: 1; padding: 4px 6px; }
  .names small { display: block; color: #b5452f; font-family: monospace; font-size: 10px; }
</style>
<!-- The browser never runs a script with an unknown type: it is just a place to keep text. -->
<script type="x-shader/x-fragment" id="cover-fs">
#version 300 es
precision mediump float;
in vec2 vUV; uniform vec3 uColor; out vec4 outColor;
void main() {
  float spine = mix(0.55, 1.0, smoothstep(0.08, 0.12, vUV.x));
  outColor = vec4(uColor * spine * mix(1.2, 0.75, vUV.y), 1.0);
}
</script>
<canvas id="view" width="600" height="200"></canvas>
<div class="names">
  <div>template literal</div>
  <div>&lt;script&gt; textContent, trimmed</div>
  <div>&lt;script&gt; textContent, not trimmed<small id="msg"></small></div>
</div>
<script>
const gl = document.getElementById('view').getContext('webgl2');

// 1. A template literal: the backtick is followed directly by #version.
const vertexSource = `#version 300 es
in vec2 aPos; out vec2 vUV;
void main() { vUV = vec2(aPos.x, 1.0 - aPos.y); gl_Position = vec4(aPos * 2.0 - 1.0, 0.0, 1.0); }`;
const literalFragment = `#version 300 es
precision mediump float;
in vec2 vUV; uniform vec3 uColor; out vec4 outColor;
void main() { outColor = vec4(uColor * (0.7 + 0.3 * step(0.5, fract(vUV.y * 6.0))), 1.0); }`;

// 2. A script element: its text starts with the line break after the opening tag.
const scriptText = document.getElementById('cover-fs').textContent;

function build(fsSource) {
  const program = gl.createProgram();
  for (const [type, src] of [[gl.VERTEX_SHADER, vertexSource], [gl.FRAGMENT_SHADER, fsSource]]) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, src);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return { error: gl.getShaderInfoLog(shader) };
    gl.attachShader(program, shader);
  }
  gl.bindAttribLocation(program, 0, 'aPos');
  gl.linkProgram(program);
  return { program };
}

gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 1,0, 1,1, 0,0, 1,1, 0,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

const panels = [
  [build(literalFragment), [0.12, 0.37, 0.55]],
  [build(scriptText.trim()), [0.88, 0.6, 0.06]],   // trim() puts #version back on line 1
  [build(scriptText), [0.71, 0.27, 0.18]],          // #version now sits on line 2
];
panels.forEach(([result, color], i) => {
  gl.viewport(i * 200 + 50, 10, 100, 180);
  if (result.error) {                               // draw a gray placeholder instead
    gl.enable(gl.SCISSOR_TEST);
    gl.scissor(i * 200 + 50, 10, 100, 180);
    gl.clearColor(0.8, 0.78, 0.74, 1); gl.clear(gl.COLOR_BUFFER_BIT);
    gl.disable(gl.SCISSOR_TEST);
    document.getElementById('msg').textContent = result.error.replace(/\s+/g, ' ').trim();
    return;
  }
  gl.useProgram(result.program);
  gl.uniform3fv(gl.getUniformLocation(result.program, 'uColor'), color);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
});
</script>