clicktorelease

Hi! My name is Jaume Sanchez!
I like to do things with the browser
(NOT those kind of things, you pervert!)

Find me on twitter or GitHub

Using hooks to make development of shaders a bit easier

This article aims to show how to use function augmentation via hooks to add behaviour that makes developing GLSL shaders a bit easier.

Using some JavaScript code to hook function to modify the native WebGL API, some regExp-fu and the DOM, we can give useful information to the developer about what's going on inside the shader compilation process.

Note: After writing this article, I've found that the handling of errors in shader compilation was perfectly covered by Florian Bösch (@pyalot) in his article How to write portable WebGL, a very interesting read.

The problem (or rather, the nuisance)

You're coding your WebGL project, doing some changes in your GLSL code, and after reloading everything is broken. What's happened?

May be you have static code analysis tools to tell you what's wrong before executing, but most of us rely on the browser's ability to give us some insight about why a shader won't compile.

And to get to that information, something similar to this happens after opening the console:

That's right, of course it was a semicolon. But that message was lost amongst 32 other error messages, and no less than 257 warnings! I'm aware that you can filter by log type in the console, but that's beside the point: it's cumbersome to get right to the relevant line. That can happen several times in a session when coding shaders.

Some editors (like the GLSL Sandbox or ShaderToy) have some kind of integrated error reporting integrated with the code editor. But what happens if we don't have it to begin with?

An easy and fast solution

Thanks to JavaScript, we can easily add a bit of code to augment a function. In this case, we're going to augment a function from the WebGL context object, and display some useful info right into the web page. Something that looks like this:

Creating a hook function

We'll start by adding a generic hook function. By using the syntax f = _h( f, function() {} );, it takes a function, keeps the reference and replaces it by a closure that executes the original function and the provided callback.

function _h( f, c ) {
	return function() {
		var res = f.apply( this, arguments );
		c.apply( this, arguments );
		return res;
	}
}

Augmenting gl.compileShader to help us

We're going to use our hook function to add some behaviour to the compileShader function in the WebGL context object. We're going to modify the method in the prototype, so it automatically applies to all instances of WebGLRenderingContext:

WebGLRenderingContext.prototype.compileShader = _h( 
	WebGLRenderingContext.prototype.compileShader, 
	function( shader ) {
 
		if ( !this.getShaderParameter( shader, this.COMPILE_STATUS ) ) {

			var errors = this.getShaderInfoLog( shader );
			var source = this.getShaderSource( shader );

			processErrors( errors, source );

		}
	} 
);

Now we have the function hooked, and the new code that is called after the real compileShader function is called checks the result of that process, by querying the value of COMPILE_STATUS. If it's true, it was compiled successfully. If it's false, we have some more info to dig in: we'll retrieve the errors string and the current shader source, and call processErrors().

Processing the errors with RegExp

We're going to use a regular expression to extract error message and line number from the errors string returned by the compiler.

Regular expressions are incredibly powerful and make extracting values from strings in a very compact way, but they are not easy to get started. I advise using regex101 or RegExr.

The error message are like the following:

ERROR: 0:33: 'uniform' : syntax error 
ERROR: 0:55: 'tDiffuse' : undeclared identifier
ERROR: 0:55: 'texture2D' : no matching overloaded function found

This regExp that we are going to use tries to match strings that look like that, where 33 or 55 would be the number of the line in the source code where the error is detected (doesn't mean the error is actually in that line!), and the text after that would be the error message.

The "human" reading of the regExp is "Match any sentence that goes like `ERROR`, a colon, a space, any number, a colon, any number that we'll keep, a colon, a space, and then any combination of characters until the end of the line that we'll keep. Do that regardless case, for all the lines in text, and get as much values as you can".

function processErrors( errors, source ) {

	var re = /ERROR: [\d]+:([\d]+): (.+)/gmi; 

	var m;
	while ((m = re.exec( errors )) != null) {
		if (m.index === re.lastIndex) {
			re.lastIndex++;
		}
		/* m contains our match */
	}

}

Adding DOM with information about errors

Now that we have extracted all the matches, we can do something with them. We're going to add them to the DOM in an unordered list. First we inject a CSS style block, then add a ul element to the page, and populate it with li elements. This is the easiest way of adding CSS on the fly to a web page: create a style tag and append it to the head of the document.

This is our improved processErrors function:

function processErrors( str, string ) {

	var css = '#shaderReport{ box-sizing: border-box; position: absolute; left: 0; top: 0; \
	right: 0; font-family: monaco, monospace; font-size: 12px; z-index: 1000; \
	background-color: #b70000; color: #ffffff; white-space: normal; \
	text-shadow: 0 -1px 0 rgba(0,0,0,.6); line-height: 1.2em; list-style-type: none; \
	padding: 0; margin: 0; max-height: 300px; overflow: auto; } \
	#shaderReport li{ padding: 10px; border-top: 1px solid rgba( 255, 255, 255, .2 ); \
	border-bottom: 1px solid rgba( 0, 0, 0, .2 ) } \
	#shaderReport li p{ padding: 0; margin: 0 } \
	#shaderReport li:nth-child(odd){ background-color: #c9542b }\
	#shaderReport li p:first-child{ color: #eee }';

	var el = document.createElement( 'style' );
	document.getElementsByTagName( 'head' )[ 0 ].appendChild( el );
	el.textContent = css;
	 
	var report = document.createElement( 'ul' );
	report.setAttribute( 'id', 'shaderReport' );
	document.body.appendChild( report );

	var re = /ERROR: [\d]+:([\d]+): (.+)/gmi; 
	var lines = source.split( '\n' );

	var m;
	while ((m = re.exec( errors )) != null) {
		if (m.index === re.lastIndex) {
			re.lastIndex++;
		}
		var li = document.createElement( 'li' );
		var code = '<p>ERROR "<b>' + m[ 2 ] + '</b>" in line ' + m[ 1 ] + '</p>'
			code += '<p>' + lines[ m[ 1 ] - 1 ].replace( /^[ \t]+/g, '' ) + '</p>';
		li.innerHTML = code;
		report.appendChild( li );
	}
	
}

So that's it. Just remember to wrap the code in a closure so it doesn't leak to the global scope. You can grab the code on GitHub.

Grab the code on GitHub

This module can be deployed anywhere. The beauty of it is that it will work with any page or library that uses WebGL and compiles shaders.

So there you have it!

I hope this article is clear enough to show how to augment functions from JavaScript APIs, and offer a glimpse into the wide range of possibilities that such power opens.

We've seen the generic hook function (which can take many forms, and be more complex), the augmentation of a native method (compileShader in this case), and use regular expressions and DOM+CSS to generate a report to the show the user.

As further development, there could be a nice addition and show the full code, with the line where the error is annotated with the error message, and may be even add some insight on the error.

That's all. As always, leave a comment to discuss this post, or contact me on twitter or e-mail.

Enjoy!