Why do shaders have to be in html file for webgl program?

I have seen the following question where someone asked how to remove shaders from html: WebGL - is there an alternative to embedding shaders in HTML?

There are elaborate workarounds to load in a file containing the shader suggested in the answers to the question.

In the tutorial I saw, the shader code is embedded directly in the html. The javascript code refers to it using getElementById. But it's ugly embedding the shader directly in the html for many reasons. Why can't I just refer to it externally using the src= attribute?

<script type="x-shader/x-fragment" id="shader-fs" src="util/fs"></script>

The above doesn't work, I just want to know why not. This is clearly something to do with limitations on script itself, but I don't get it.

Answers


You don't have to use <script> tags at all to load a shader program. Most tutorials and examples just use them as a container to store a string in the DOM of the webpage. The script type "x-shader/x-fragment" is meaningless for web browsers, so they don't execute the script. They do, however, store the content of that tag as a string in the DOM which can then later be accessed by "real" scripts. This only works when the script content is in the HTML file. When you load the script via a src attribute, the content does not become a text childnode of the script tag and thus can not be accessed through the DOM tree.

You can just as well store the sourcecode for the shader as a string in a Javascript file:

// myVertextShader.glsl.js
var myVertexShaderSrc =         
        "attribute vec3 pos;"+      
        "void main() {"+        
        "   gl_Position = vec4(pos, 1.0);"+     
        "}"
    ;

You would then compile the shader like this:

var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, myVertexShaderSrc);
gl.compileShader(vertexShader);

gl.attachShader(program, vertexShader);

Update 2018

In 2018 I'd suggest using multiline template literals as in surround the shader with backticks and it can cross multiple lines

const someShaderSource = `
attribute vec4 position;
uniform mat4 matrix;
void main() {
  gl_Position = matrix * position;
}
`;

If you want to put shaders in separate files you can easily do this in 2018 using JavaScript modules. A shader file might look like this

// someshader.glsl.js
export default `
attribute vec4 position;
uniform mat4 matrix;
void main() {
  gl_Position = matrix * position;
}
`;

To use JavaScript modules your main JavaScript must be in a separate file. You can access the shader source by importing it

// main.js

import someShaderSource from './someshader.glsl.js';

// use someShadeSource

And you include it in your HTML with

<script src="main.js" type="module"></script>

Or you can use it from a script tag in the page itself like this

<script type="module">
import someShaderSource from './someshader.glsl.js';

// use someShadeSource
</script>

If you want to support older browsers you can use a tool like rollup which will read all the import statements and generate one large JavaScript file. This is what three.js does.

If you need to support IE11 you can use babel to convert the multiline templates. All other browsers have supported multiline templates for many years.

Original Answer

Why do shaders have to be in html file for webgl program?

They don't

You can put shaders in external javascript. For example

// --myshader.js--
var myFragmentShader = 
  "void main() {\n" +
  "  gl_FragColor = vec4(1,0,0,1);\n" +
  "}n\";

Or another common format

// --myshader.js--
var myFragmentShader = [
  "void main() {",
  "  gl_FragColor = vec4(1,0,0,1);", 
  "}",
].join("\n");

In all browsers that support WebGL you can use template literals

// --myshader.js--
var myFragmentShader = `
  void main() {
    gl_FragColor = vec4(1,0,0,1); 
  }
`;

Otherwise you can put them in text files and load them with XMLHTTPRequest

// --myshader.txt
  void main() {
    gl_FragColor = vec4(1,0,0,1); 
  }

Then in JavaScript do the following

function loadTextFile(url, callback) {
  var request = new XMLHttpRequest();
  request.open('GET', url, true);
  request.addEventListener('load', function() {
     callback(request.responseText);
  });
  request.send();
}

loadTextFile("myshader.txt", function(text) {
  // use text...
});

The reason people put them in the HTML is because it's easy, efficient, and synchronous.

easy: unlike the JS file versions you don't have to surround every line with quotes and other punctuation. Although now with es6 that's no longer an issue. Every browser that supports WebGL supports es6 template strings.

efficient: unlike the text and js files there's only one request to the server. Of course some people might run a concatenator on their js files to fix some of that.

synchronous: unlike the text files their usages is synchronous. No need for callbacks or promises or otherwise dealing with asynchronous issues of downloading files.

As for why your example doesn't work I'm pretty sure the reason is it would allow cross origin resource access. The <script> tag was designed before people figured out cross origin access was a problem so they couldn't turn off cross origin scripts without breaking a bunch of sites. They could make everything else more strict.

XMLHttpRequest for example does not allow cross origin access unless the server you're contacting gives permission. If script tags let you access that content you could use script tags to work around that restriction. In other words instead of making a XMLHttpRequest and reading the request.responseText for the result you'd just programmatically make a script tag, set its src to the URL you want and then read its text field when it finished. To make sure you can not do that you're not allowed to read the text field of a script tag that had a src attribute


Shader language scripts are just text. The text can be grabbed or generated from anywhere (that you can read or generate text). Many tutorials just skip over the part where the magic happens and and the WebGL shader instances are created from the string obtained. There's no reason you couldn't refer to the scripts externally like you propose, but you would need additional JavaScript to load the contents, not the browser. Script tags are likely used in the tutorials primarily because if you give a script tag a type that the browser doesn't understand, the browser skips execution of the tag's contents or retrieval of the script's source, so the tag's contents and attributes can be used however you desire.

Edit: Well, I have to take some things back. I decided to go through four browsers (Chrome, Firefox, IE9, Opera), and see what happens when you have a line

<script type="x-shader/x-fragment" id="shader-fs" src="util/fs"></script>

in your html. Turns out, the browser does load the file in every browser I tried, so I was wrong. However, that doesn't mean the browser knows what to do with the file besides cache it. I don't know what you mean by "Why doesn't src="util/fs" work???". In every browser I've tried,

alert(document.getElementById('shader-fs').src);

alerts the full path to the file when given a partial path. (Maybe this is your problem? You're expecting a partial path when the browser gives you a full one?) Besides that, I'm not sure how to interpret your problem.


Need Your Help

How to remove the % lines in xtable table output by Knitr

r knitr xtable

By using xtable and knitr, I add a table to my RMD document and export to PDF file.

git "revert" current directory

git version-control reset git-checkout revert

In svn it's possible to do svn revert ./* in which the current directory and ONLY the current directory gets reverted.