Summary:

Tutorial 2 - Shaders and Draw Loop

This tutorial will further expand on how the shader system works in WebRender and provide and example how to use the callback system. This example is built off the code provided in the first tutorial, so sections that have not changed will not be explained again.Some aspects of this tutorial may be obvious, however including as much detail as possible should help others aviod making mistakes.

Tutorial Files (12.8 KB)

Result

The end result of this tutorial should be this:

This the HTML for this example:


<html>
	<head>	
		<title>WebRender Tutorial</title>
		<script src="matrix.js"></script>
		<script src="webrender.js"></script>
		<script src="example.js"></script>
	</head>
	<body onLoad="main()">	
		<canvas id="screen" width="600" height="600"></canvas>
	</body>
</html>

This the JavaScript for this example:


var render;
var vbo;
var timeCounter;

function main() {
	var canvas = document.getElementById("screen");
	
	render = new WebRender(canvas, callback);
	render.initialise(canvas.width, canvas.height);
	render.clearColor(0, 0, 0, 255);
	
	var shaderID = render.getSBOId();
	render.assignSBOVertexShader(shaderID, "index.js", "vertexShader");
	render.assignSBOPixelShader(shaderID, "index.js", "fragmentShader");
	render.addSBOVertexAttribute(shaderID, "VERTEX", "POSITION", 3);
	render.addSBOVertexAttribute(shaderID, "PIXEL", "POSITION", 4);
	render.loadSBO(shaderID);
	render.bindSBO(shaderID);

	vbo = render.getVBOId();
	render.loadVBO(vbo, [0, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0]);
	
	var colour = (0 << 0) | (0 << 8) | (255 << 16) | (255 <<  24);
	render.setShaderVariable("FRAG_colour", colour);
	
	timeCounter = 0;
	
	draw();
	
	return;
}

//----------------------------------------------------------------
// Draw Loop
//----------------------------------------------------------------
function draw() {
	timeCounter += 0.01;

	var objectMatrix = new Matrix();
	objectMatrix.makeZRotationMatrix(Math.sin(timeCounter));
	
	render.setShaderVariable("VERTEX_objectMatrix", objectMatrix);
	
	render.clearBuffer(0);
	render.draw(vbo, 0, 3);
	render.getBuffer();
}

function callback() {
	setTimeout(draw, 0);
}

//----------------------------------------------------------------
// Shaders
//----------------------------------------------------------------
var VERTEX_objectMatrix;
var FRAG_colour;

function vertexShader(render, inputVertex, outputVertex) {
	outputVertex[0] = inputVertex[0];
	outputVertex[1] = inputVertex[1];
	outputVertex[2] = inputVertex[2];
	outputVertex[3] = 1;
	
	render.vecByMatrixCol(outputVertex, VERTEX_objectMatrix.data, 0);
}

function fragmentShader(render, x, y, z, w) {
	return FRAG_colour;
}
//----------------------------------------------------------------

Explanation - HTML


		<script src="matrix.js"></script>

The only change is the inclusion of the matrix.js script file, which is a simple JavaScript matrix library. Other third party matrix libraries could be used instead.

Explanation - JavaScript

The JavaScript is broken into five functions, main(), draw(), callback(), vertexShader() and fragmentShader(). All five are different from the previous tutorial.


var render;
var vbo;
var timeCounter;

These are three global variables that the application uses to keep track of things between functions. render is our instance of WebRender, vbo is the id of the vertex buffer we are using and timeCounter is an incrementing number to determine how many cycles we have had. These will be explined fully as they are used in the code.


function main() {
	var canvas = document.getElementById("screen");
	
	var render = new WebRender(canvas, callback);
	render.initialise(canvas.width, canvas.height);
	render.clearColor(0, 0, 0, 255);

Once again, the WebRender instance is created, however this time we also include a callback function. This callback function will be called by WebRender after the pixel buffer has been copied to the canvas.


	var shaderID = render.getSBOId();
	render.assignSBOVertexShader(shaderID, "index.js", "vertexShader");
	render.assignSBOPixelShader(shaderID, "index.js", "fragmentShader");
	render.addSBOVertexAttribute(shaderID, "vertex", "position", 3);
	render.addSBOVertexAttribute(shaderID, "pixel", "position", 4);
	render.loadSBO(shaderID);
	render.bindSBO(shaderID);

The shaders have been set up the same as the first tutorial, however this time we will explain what is happening. getSBOId() is used to fetch an id for the shader we are creating, this is used to refer to the shader each time. assignSBOVertexShader() and assignSBOPixelShader() are used to define what functions are acting as shaders. They take in the name of the function (as a string) and the file that the shader is located in (relative to the location of webrender.js).

Next addSBOVertexAttribute is used to define what the data that the shader uses looks like. It takes in what shader it is being applied to ("vertex" or "pixel"), what type of data it is ("position", "texcoord", "other") and how many floats there are for that data. In that example, we store 3 position floats for each veretx in the vertex buffer, and the vertex shader sends 4 position floats to the rendering pipeline, which the fragment/pixel shader will expect to recieve. A shader must have 4 position data for the pixel shader as the rendering pipeline requires XYZW format.

To finalise the shader we then call loadSBO() which creates the shader and all the generated code it requires. Then bindSBO() is used to make this shader the current one used for drawing. In this example we only have one shader and can bind it staright away, for an application with multiple shaders you will want to bind it during the draw phase.


	var vbo = render.getVBOId();
	render.loadVBO(vbo, [0, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0]);

We are against using the same vertex data of the triangle as the previous tutorial. As you cna see, the data here correlates with the format we defined in the previous code segment.


	var colour = (0 << 0) | (0 << 8) | (255 << 16) | (255 <<  24);
	render.setShaderVariable("FRAG_colour", colour);

Here we are defining what colour to make the triangle, in this case it is set to blue. Colour values are stored as 32-bit unsigned integers in WebRender, so in order to avoid calculating the colour in the shader, we store our colour in the same format. With the colour value created, we then send to the shaders using the setShaderVariable() function, with the variable name and the value.

In this example the shader varaible FRAG_colour, is just a global variable, however in order to make your program thread safe it is advised you use the setShaderVariable() function.


	timeCounter = 0;
	
	draw();
	
	return;
}

Since we are going to be redrawing the image to provide animation, we use a global called timeCounter that will increment upwards. Here we initialise it. Lastly we call the draw() function which will begin our draw loop and is defined next.


function draw() {
	timeCounter += 0.01;

	var objectMatrix = new Matrix();
	objectMatrix.makeZRotationMatrix(Math.sin(timeCounter));
	
	render.setShaderVariable("VERTEX_objectMatrix", objectMatrix);
	
	render.clearBuffer(0);
	render.draw(vbo, 0, 3);
	render.getBuffer();
}

This is the looping draw function that is run as often as possible. It increments our time counter, creates the transformation matrix and updates it to the shaders and then draws the triangle the same way as the first tutorial.


function callback() {
	setTimeout(draw, 0);
}

This is our callback function which is run after each frame is copyied to the canvas. In order to prevent the browser from freezing we use the setTimeout() function to continually repeat our draw function.


var VERTEX_objectMatrix;

function vertexShader(render, inputVertex, outputVertex) {
	outputVertex[0] = inputVertex[0];
	outputVertex[1] = inputVertex[1];
	outputVertex[2] = inputVertex[2];
	outputVertex[3] = 1;
	
	render.vecByMatrixCol(outputVertex, VERTEX_objectMatrix.data, 0);
}

This is our vertex shader, and it is the same as the first tutorial except for one additional line to apply our transformation matrix to the vertices. The WebRender library provides two functions to apply a matrix to a vector, one for column-major matrices, vecByMatrixCol(), and the other for row-major matrices, vecByMatrixRow(). These take a reference to the vertex data, an array with 16 elements representing a matrix, and an offset that defines where the XYZW data is.


var FRAG_colour;

function fragmentShader(render, x, y, z, w) {
	return FRAG_colour;
}

This is the fragment shader, which simply returns the colour that we defined in FRAG_colour back in the main() function.

Conclusion

You should now understand the basics of the shader system in WebRender. The next tutorial is about how to use textures with WebRender, along with how to load images to act as textures in JavaScript.