WebGL: External GLSL Files
Introduction
Tutorials I see on WebGl seem to have the GLSL either in script
tags:
<script src="fragment.glsl" type="x-shader/x-fragment">
precision mediump float;
void main()
{
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
</script>
<script src="vertex.glsl" type="x-shader/x-vertex">
precision mediump float;
attribute vec2 vertPosition;
void main() {
gl_Position = vec4(vertPosition, 0.0, 1.0);
}
</script>
Or (even worse) in constants/variables embedding in the JS:
const vertexShaderText = [
'precision mediump float;',
'attribute vec2 vertPosition;',
'void main() {',
' gl_Position = vec4(vertPosition, 0.0, 1.0);',
'}',
].join('\n');
const fragmentShaderText = [
'precision mediump float;',
'void main()',
'{',
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);',
'}',
].join('\n');
These are horrible to work with.
In order to maximise the use of your IDEs abilities these need to be in separate *.glsl
files (or whatever extension you’ve set up in the IDE to mean GLSL). Getting them is a bit of a faff, but I’ve written a nice function to do it for you.
Code
/**
* Fetch the fragment and vertex shader text from external files.
* @param vertexShaderPath
* @param fragmentShaderPath
* @returns {Promise<{vertexShaderText: string | null, fragmentShaderText: string | null}>}
*/
export async function fetchShaderTexts(vertexShaderPath, fragmentShaderPath) {
const results = {
vertexShaderText: null,
fragmentShaderText: null,
};
let errors = [];
await Promise.all([
fetch(vertexShaderPath)
.catch((e) => {
errors.push(e);
})
.then(async (response) => {
if (response.status === 200) {
results.vertexShaderText = await response.text();
} else {
errors.push(
`Non-200 response for ${vertexShaderPath}. ${response.status}: ${response.statusText}`
);
}
}),
fetch(fragmentShaderPath)
.catch((e) => errors.push(e))
.then(async (response) => {
if (response.status === 200) {
results.fragmentShaderText = await response.text();
} else {
errors.push(
`Non-200 response for ${fragmentShaderPath}. ${response.status}: ${response.statusText}`
);
}
}),
]);
if (errors.length !== 0) {
throw new Error(
`Failed to fetch shader(s):\n${JSON.stringify(errors, (key, value) => {
if (value?.constructor.name === 'Error') {
return {
name: value.name,
message: value.message,
stack: value.stack,
cause: value.cause,
};
}
return value;
}, 2)}`
);
}
return results;
}
If you’re wondering what’s the deal with the function being passed into the
JSON.stringify()
function then you should read my article on Serializing the Error Object in JavaScript.
This function accepts a path for each of the vertex and shader files, and fetches them for you.
Conclusion
I was finding this so annoying, it;s pretty trivial to write a function to handle fetching the shader text without having to resort to nasty non-solutions that just confuse readers who are trying to learn an already difficult technology.
If you’re using WebStorm then there’s a great plugin for syntax highlighting, code completion and some basic error checking too:
GLSL — IntelliJ IDEs Plugin | Marketplace (jetbrains.com)
I recommend installing it.