Post-process shaders in glfx
Dec 30 2015 ยท Graphics and Rendering
Pushed an update to glfx to allow for post-process shading. When a post-process shader is defined, the scene is rendered to a screen-space quad (the size of the viewport), and that quad is then rendered to the viewport with the post-process shader applied.
The shader is loaded (asynchronously) like any other:
glfx.shaders.load('screenspace.fs', "frag-shader-screenspace", glfx.gl.FRAGMENT_SHADER);
Once loaded, we create the shader program, and get locations for whatever variables are used. The vertex shader isn’t anything special, it just transforms a vertex by the model-view and projection matrices, and passes along the texture coordinates.
glfx.whenAssetsLoaded(function() {
var postProcessShaderProgram = glfx.shaders.createProgram([glfx.shaders.buffer['vert-shader-basic'], glfx.shaders.buffer['frag-shader-screenspace']],
function(_shprog) {
// Setup variables for shader program
_shprog.vertexPositionAttribute = glfx.gl.getAttribLocation(_shprog, "aVertexPosition");
_shprog.pMatrixUniform = glfx.gl.getUniformLocation(_shprog, "uPMatrix");
_shprog.mvMatrixUniform = glfx.gl.getUniformLocation(_shprog, "uMVMatrix");
_shprog.textureCoordAttribute = glfx.gl.getAttribLocation(_shprog, "aTextureCoord");
_shprog.uPeriod = glfx.gl.getUniformLocation(_shprog, "uPeriod");
_shprog.uSceneWidth = glfx.gl.getUniformLocation(_shprog, "uSceneWidth");
_shprog.uSceneHeight = glfx.gl.getUniformLocation(_shprog, "uSceneHeight");
glfx.gl.enableVertexAttribArray(_shprog.vertexPositionAttribute);
glfx.gl.enableVertexAttribArray(_shprog.textureCoordAttribute);
});
...
We then tell glfx to apply our post-process shader program:
glfx.scene.setPostProcessShaderProgram(postProcessShaderProgram);
This call will result in different rendering path, which renders the scene to a texture, applies that texture to a screen-space quad, and renders the quad with the post-process shader.
Here is the shader for screenspace.fs, used in the demo shown above:
precision mediump float;
uniform float uPeriod;
uniform float uSceneWidth;
uniform float uSceneHeight;
uniform sampler2D uSampler;
varying vec2 vTextureCoord;
void main(void) {
vec4 sum = vec4( 0. );
float blurSampleOffsetScale = 2.8;
float px = (1.0 / uSceneWidth) * blurSampleOffsetScale;
float py = (1.0 / uSceneHeight) * blurSampleOffsetScale;
vec4 src = texture2D( uSampler, ( vTextureCoord ) );
sum += texture2D( uSampler, ( vTextureCoord + vec2(-px, 0) ) );
sum += texture2D( uSampler, ( vTextureCoord + vec2(-px, -py) ) );
sum += texture2D( uSampler, ( vTextureCoord + vec2(0, -py) ) );
sum += texture2D( uSampler, ( vTextureCoord + vec2(px, -py) ) );
sum += texture2D( uSampler, ( vTextureCoord + vec2(px, 0) ) );
sum += texture2D( uSampler, ( vTextureCoord + vec2(px, py) ) );
sum += texture2D( uSampler, ( vTextureCoord + vec2(0, py) ) );
sum += texture2D( uSampler, ( vTextureCoord + vec2(-px, py) ) );
sum += src;
sum = sum / 9.0;
gl_FragColor = src + (sum * 2.5 * uPeriod);
}
Note that it requires a few uniforms to be supplied to it, we use the glfx.scene.onPostProcessPreDraw() callback to setup the variables (before the post-processed scene is drawn):
var timeAcc = 0;
glfx.scene.onPostProcessPreDraw = function(tdelta) {
timeAcc += tdelta;
var timeScaled = timeAcc * 0.00107;
if(timeScaled > 2.0*Math.PI) {
timeScaled = 0;
timeAcc = 0;
}
var period = Math.cos(timeScaled);
glfx.gl.uniform1f(postProcessShaderProgram.uPeriod, period + 1.0);
glfx.gl.uniform1f(postProcessShaderProgram.uSceneWidth, glfx.gl.viewportWidth);
glfx.gl.uniform1f(postProcessShaderProgram.uSceneHeight, glfx.gl.viewportHeight);
};
What we’re doing is using the scene rendering time deltas to generate a periodic/sinusoidal wave. This results in the pulsing brightness/fading effect of the scene. The brightness effect itself is done by adding the source pixel to a blurred + brightened version of itself. The blurring allows for the soft fade in and fade out.