Summary:

Tutorial 3 - Textures

This tutorial will cover how to load and display textures in WebRender. 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 avoid making mistakes.

Tutorial Files (14.1 KB)

Textures in WebRender must be square, however do not need to have a side with a length that is a power of 2.

Result

The end result of this tutorial should be this:

This the HTML for this example:


<html>
	<head>	
		<title>WebRender Tutorial 3</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 canvas;
var render;
var vbo;
var tbo;

var timeCounter = 0;

function main() {
	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, "VERTEX", "TEXCOORD", 2);
	render.addSBOVertexAttribute(shaderID, "PIXEL", "POSITION", 4);
	render.addSBOVertexAttribute(shaderID, "PIXEL", "TEXCOORD", 2);
	render.loadSBO(shaderID);
	render.bindSBO(shaderID);

	vbo = render.getVBOId();
	render.loadVBO(vbo, [0, 0.5, 0, 0.5, 0, 
						-0.5, -0.5, 0, 0, 1, 
						0.5, -0.5, 0, 1, 1]);
	
	tbo = render.getTBOId();
	loadTexture("texture.png", tbo);
	
	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.bindTBO(tbo, 0);
	
	render.clearBuffer(0);
	render.draw(vbo, 0, 3);
	render.getBuffer();
}

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

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

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

function fragmentShader(render, x, y, z, w, u, v) {
	return render.getTexturePixelColor(u, v, render.TEXTURE_0);
}

//----------------------------------------------------------------
// Texture Loading Code
//----------------------------------------------------------------
function loadTexture(texturePath, tbo) {	
	var texture = new Image();
	texture.id = tbo + ':Texture' + texturePath;
	texture.src = texturePath;
	texture.onload = imageLoaded;
	return;
}

function imageLoaded(e) {
	
	//Get the ID
	var source = e.target;
	if (source == undefined) {	//for IE
		source = e.srcElement;
	}
	var id = (source.id).split(":")[0];
	
	//drawing the texture onto our canvas.
	var texture = source;
	
	//resize canvas to size of texture
	var originalWidth = canvas.width;
	var originalHeight = canvas.height;
	canvas.width = texture.width;
	canvas.height = texture.height;
	
	//draw the texture and then grab the pixel array
	var loaderContext = canvas.getContext("2d");
	loaderContext.clearRect(0, 0, texture.width, texture.height);
	loaderContext.drawImage(texture, 0, 0);
	var textureData = loaderContext.getImageData(0, 0, texture.width, texture.height);
	
	canvas.width = originalWidth;
	canvas.height = originalHeight;
	
	//load the texture into WebRender
	render.loadTBO(id, textureData.data, textureData.width, textureData.height);
	
	return;
}
//----------------------------------------------------------------

Explanation - HTML

There is no change in the HTML for this tutorial.

Explanation - JavaScript

Two new functions that deal with texture loading have been added, loadTexture() and imageLoaded(), and most of the existing functions have been modified from the previous tutorial.


var canvas;
var render;
var vbo;
var tbo;

var timeCounter = 0;

Along with the the three existing global variables, the canvas has been moved to be global, along with a new variable for the texture buffer ID. The reason cavnas has become global is so that the it can be used when loading an image for a texture. This will be explained in more detail later.


function main() {
	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, "vertex", "texcoord", 2);
	render.addSBOVertexAttribute(shaderID, "pixel", "position", 4);
	render.addSBOVertexAttribute(shaderID, "pixel", "texcoord", 2);
	render.loadSBO(shaderID);
	render.bindSBO(shaderID);

The canvas and render objects have been set up the same as before, however we have drastically changed the shader. The vertex shader now takes in a vertex with 5 values, an xyz positional value and uv texture coordinates. IMPORTANT! You must give the texture coordinate values the data value of "texcoord" in order for WebRender to process the coordinates correctly. The pixel shader also has the UV values included, as they are required for looking up the texture.

Otherwise the shaders are loaded and used the same as the previous tutorial.


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

In order to use texture coordinates they must be included in our model. This is the the same data representing our triangle from the previous tutorial but each one having a UV coordinate on the end.


	tbo = render.getTBOId();
	loadTexture("texture.png", tbo);

This is our new functions for loading a texture from an image. First we use getTBOId() function to get an id for a texture buffer and store it in tbo. Then using our defined loadTexture() function to load the image at "texture.png" into the buffer with the id we just got. loadTexture() will be defined later in this tutorial.


	timeCounter = 0;
	
	draw();
	
	return;
}

We have removed the colour setting that was used in the previous tutorial, as now with textures we do not need it anymore.


function draw() {
	timeCounter += 0.01;

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

The only change for this tutorial is that we bind our texture to texture position 0 using the bindTBO() function. Otherwise this function is the same as the previous tutorial.


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

Same as previous tutorial.


var VERTEX_objectMatrix;

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

Our vertex shader is functioning the same as the previous tutorial, however it has been expanded to cover the UV texture coordinates. NOTE! Since our vertex data does not have a W value, the array indices after the W value is set are not the same between the outputVertex and inputVertex. Since vecByMatrixCol() only applies to the four indices after the given index (in this it is 0), it will not effect or be effected by the UV values.


function fragmentShader(render, x, y, z, w, u, v) {
	return render.getTexturePixelColor(u, v, render.TEXTURE_0);
}

The fragment shader is still small, but now uses the getTexturePixelColor() in order to sample our texture using our UV values. getTexturePixelColor() takes in a texture coordinate, u for horizontal space, v for vertical in the range of 0.0 - 1.0, along with the reference to the texture slot our texture was bound to. Earlier we used render.bindTBO(0, tbo);, so in the shader we use render.TEXTURE_0. WebRender supports up to 8 textures bound at once.

The function getTexturePixelColor() returns a 32-bit interger in the format RGBA, therefore if you wish to modify the colour you will need to seperate out the components. For example, to get the green value of the colour, you need to AND with the mask 65280, then bitshift right 8 places. Eg: var green =((pixelColor & 65280) >>> 8);, see a full shader below.


function fragmentShader(render, x, y, z, w, u, v) {
	var pixelColor = render.getTexturePixelColor(u, v, render.TEXTURE_0) << 0;
	
	var red = (pixelColor & 255);
	var green = (pixelColor & 65280) >>> 8;
	var blue = (pixelColor & 16711680) >>> 16;
	var alpha = (pixelColor & 4278190080) >>> 24;

	//Lighting or Colour changing code goes here.
	
	return (alpha << 24) | (blue << 16) | (green <<  8) | red;
}

Here is a shader that seperates out each colour channel, then recombines them to send back to WebRender. You note each channel has all the other colour values stripped out with a mask (where the AND is applied), then bit shifted right until it is at the end of the byte to form a number from 0-255. When recomibing, it is bit-shift left back to it's proper position and merged together with OR.


function loadTexture(texturePath, tbo) {	
	var texture = new Image();
	texture.id = tbo + ':Texture' + texturePath;
	texture.src = texturePath;
	texture.onload = imageLoaded;
	return;
}

This is the function we used back in main() that we sent the image path and the texture buffer id to. This function is quite simple and simply loads the image at the given path, and stores the buffer id in this image id. Once the image has been loaded, it will be passed to the imageLoaded() function where the actual image data is processed.


function imageLoaded(e) {
	
	//Get the ID
	var source = e.target;
	if (source == undefined) {	//for IE
		source = e.srcElement;
	}
	var id = (source.id).split(":")[0];
	
	//drawing the texture onto our canvas.
	var texture = source;
	
	//resize canvas to size of texture
	var originalWidth = canvas.width;
	var originalHeight = canvas.height;
	canvas.width = texture.width;
	canvas.height = texture.height;
	
	//draw the texture and then grab the pixel array
	var loaderContext = canvas.getContext("2d");
	loaderContext.clearRect(0, 0, texture.width, texture.height);
	loaderContext.drawImage(texture, 0, 0);
	var textureData = loaderContext.getImageData(0, 0, texture.width, texture.height);
	
	canvas.width = originalWidth;
	canvas.height = originalHeight;
	
	//load the texture into WebRender
	render.loadTBO(id, textureData.data, textureData.width, textureData.height);
	
	return;
}

Once the image has been loaded by the browser, this function will be called. First we separate the image id apart in order to fetch the buffer id we stored in it. In order to get the image data in the format we need, we use our canvas and draw the image to it. We then get the texture data of the canvas, which is completely filled by our texture, and load that into WebRender using the buffer id. The texture is loaded using the loadTBO() function which takes in the buffer id, the texture data, texture width and the texture height.

The reason we load the texture into the canvas and copy it out is to get an array of pixel data where each pixel is represented by 4 8-bit integers. These are combined by WebRender into a single 32-bit number for each pixel.

Conclusion

You should now know how to load an image into WebRender to act as a texture. The next tutorial will show how to expand this example to use a 3D model instead of this 2D triangle.