click to release dot com home code articles talks contact

Article | Posted on March 28, 2014 Using hooks to make development of shaders a bit easier Reading time: 3 minutes Topics: Web Audio API, getUserMedia, Windows Get the code on GitHub 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.

Generic hook function JavaScript - library 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:

Hooking into compileShader JavaScript - library 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:

Example of an error message Returned by compileShader 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".

Extracting line numbers and errors with Regular Expressions JavaScript - library 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:

Building the DOM to show formatted errors on the page JavaScript - library 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( '

' ); 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( /^[ ]+/g, '' ) + '</p>'; li.innerHTML = code; report.appendChild( li ); } }