WebGL JavaScript

2D Transformation Order

This JavaScript program demonstrates how transformations are applied and how operations are ordered in WebGL. Initially, nine points are rendered in a 3-by-3 grid of point at the x,y coordinates at -.5, 0, and .5. The lightest, largest points are the initial, untransformed points. The transformations consist of simple shears. The next darkest points are the single shears with the different sizes for each shear. The next darkest points are the result of two shears. The relative sizes correspond to the points from the single shearing. Lastly, the white dots illustrate the matrix multipication equivalent of two shears.

Note the order of the transformations. What looks like a column operation is actually a row operation and vice versa. Moreover, the order of operations are reversed in the c programs, as well.

Transformations2D.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>XoaX.net's WebGL</title>
		<style>
			#idCanvas {
				border: 1px solid black;
				width: 800px;
				height: 600px
			}
		</style>
		<script  id="idVertexShader" type="c">
			attribute vec2 av2Position;
			uniform mat2 um2Matrix;
			uniform mat2 um2Matrix2;
			uniform float ufPointSize;
			attribute vec4 av4Color;
			varying vec4 vv4Color;

			void main() {
				vv4Color = av4Color;
				vec2 v2Transformed = um2Matrix2*um2Matrix*av2Position;
				gl_Position = vec4(v2Transformed, 0, 1);
				gl_PointSize = ufPointSize;
			}
		</script>
		<script  id="idFragmantShader" type="c">
			precision mediump float;

			varying vec4 vv4Color;

			void main() {
				gl_FragColor = vv4Color;
			}
		</script>
		<script type="text/javascript" src="Transformations2D.js"></script>
  </head>
  <body onload="Render()">
    <canvas id="idCanvas"></canvas>
  </body>
</html>

Transformations2D.js

function CreateProgram(kqGL) {

	// Compile the vertex shader
	const ksVertexShaderCode = document.getElementById("idVertexShader").innerHTML;
	let qVertexShader = kqGL.createShader(kqGL.VERTEX_SHADER);
	kqGL.shaderSource(qVertexShader, ksVertexShaderCode);
	kqGL.compileShader(qVertexShader);
	if (!kqGL.getShaderParameter(qVertexShader, kqGL.COMPILE_STATUS)) {
		alert("The vertex shader failed to compile!");
		kqGL.deleteShader(qVertexShader);
		return null;
	}

	// Compile the fragment shader
	const ksFragmentShaderCode = document.getElementById("idFragmantShader").innerHTML;
	let qFragmentShader = kqGL.createShader(kqGL.FRAGMENT_SHADER);
	kqGL.shaderSource(qFragmentShader, ksFragmentShaderCode);
	kqGL.compileShader(qFragmentShader);
	if (!kqGL.getShaderParameter(qFragmentShader, kqGL.COMPILE_STATUS)) {
		alert("The fragment shader failed to compile!");
		kqGL.deleteShader(qFragmentShader);
		return null;
	}

	// Compile and link the program
	let qProgram = kqGL.createProgram();
	kqGL.attachShader(qProgram, qVertexShader);
	kqGL.attachShader(qProgram, qFragmentShader);
	kqGL.linkProgram(qProgram);
	if (!kqGL.getProgramParameter(qProgram, kqGL.LINK_STATUS)) {
		alert("The program failed to initialize", kqGL.getProgramInfoLog(qProgram));
		return null;
	}

	return qProgram;
}

function Render() {

	const qCanvas = document.getElementById("idCanvas");
	qCanvas.width = qCanvas.clientWidth;
	qCanvas.height = qCanvas.clientHeight;
	const kqGL = qCanvas.getContext("webgl");

	let qProgram = CreateProgram(kqGL);

  // Tell WebGL how to convert from clip space to pixels
  kqGL.viewport(0, 0, kqGL.canvas.width, kqGL.canvas.height);

  // Clear the canvas
  kqGL.clearColor(0.85, 0.85, 0.85, 1.0);
  kqGL.clear(kqGL.COLOR_BUFFER_BIT);

	kqGL.useProgram(qProgram);

	let faPositions = new Float32Array([
		-.5, -.5,  0.0, -.5,  0.5, -.5,
		-.5, 0.0,  0.0, 0.0,  0.5, 0.0,
		-.5, 0.5,  0.0, 0.5,  0.5, 0.5
	]);

	let faColors = new Float32Array(36);
	for (let i = 0; i < 9; ++i) {
		faColors[4*i] = 1.0;
		faColors[4*i + 1] = .5*(i%3);
		faColors[4*i + 2] = .5*(Math.floor(i/3));
		faColors[4*i + 3] = 1.0;
	}

	const kqPositionsBuffer = kqGL.createBuffer();
	kqGL.bindBuffer(kqGL.ARRAY_BUFFER, kqPositionsBuffer);
	kqGL.bufferData(kqGL.ARRAY_BUFFER, faPositions, kqGL.STATIC_DRAW);

	const kiPositionLocation = kqGL.getAttribLocation(qProgram, 'av2Position');
	kqGL.vertexAttribPointer(kiPositionLocation, 2, kqGL.FLOAT, false, 0, 0);
	kqGL.enableVertexAttribArray(kiPositionLocation);

	const kqColorsBuffer = kqGL.createBuffer();
	kqGL.bindBuffer(kqGL.ARRAY_BUFFER, kqColorsBuffer);
	kqGL.bufferData(kqGL.ARRAY_BUFFER, faColors, kqGL.STATIC_DRAW);

	const kiColorLocation = kqGL.getAttribLocation(qProgram, 'av4Color');
	kqGL.vertexAttribPointer(kiColorLocation, 4, kqGL.FLOAT, false, 0, 0);
	kqGL.enableVertexAttribArray(kiColorLocation);


	let faIdentity = new Float32Array([
		1.0, 0.0,
		0.0, 1.0
	]);
	let faMatrix = new Float32Array([
		1.0, 0.5,
		0.0, 1.0
	]);
	let faMatrix2 = new Float32Array([
		1.0, 0.0,
		0.25, 1.0
	]);
	let dPointSize = 20.0;
	Redraw(kqGL, qProgram, faIdentity, faIdentity, dPointSize, faColors);
	
	// Use the Matrix = [1.0, .5, 0.0, 1.0]. This is a shear along y. So, think of the matrix as its transpose.
	dPointSize = 16.0;
	for (let i = 0; i < 9; ++i) {
		faColors[4*i] *= 0.5;
		faColors[4*i+1] *= 0.5;
		faColors[4*i+2] *= 0.5;
	}
	Redraw(kqGL, qProgram, faIdentity, faMatrix, dPointSize, faColors);
	
	// Use the Matrix2 = [1.0, 0.0, 0.25, 1.0].
	dPointSize = 10.0;
	Redraw(kqGL, qProgram, faIdentity, faMatrix2, dPointSize, faColors);
	
	// Multiple Matrix2 by Matrix
	dPointSize = 12.0;
	for (let i = 0; i < 9; ++i) {
		faColors[4*i] *= 0.5;
		faColors[4*i+1] *= 0.5;
		faColors[4*i+2] *= 0.5;
	}
	Redraw(kqGL, qProgram, faMatrix2, faMatrix, dPointSize, faColors);

	// Reverse the order of the matrix multiplication
	dPointSize = 5.0;
	Redraw(kqGL, qProgram, faMatrix, faMatrix2, dPointSize, faColors);

	// Calculate the product manually
	let faProduct = MatrixProduct(faMatrix, faMatrix2);
	dPointSize = 2.0;
	for (let i = 0; i < 9; ++i) {
		faColors[4*i] = 1.0;
		faColors[4*i + 1] = 1.0;
		faColors[4*i + 2] = 1.0;
		faColors[4*i + 3] = 1.0;
	}
	Redraw(kqGL, qProgram, faIdentity, faProduct, dPointSize, faColors);
	
	// To summarize, we can think of the matrix M = [M11, M12, M21, M22] as the being multiplied by row vectors on the left.
	// So, the product M*V of V = [V1, V2] and M should be thought of as V*M, where the result is [V1*M11 + V2*M21, V1*M12 + V2*M22]
	// The, for two matrices M and N, the product M*N is just as we would expect, and V*M*N is (V*M)*N.
	// However, in the c code, the product V*M*N is written N*M*V.
}

function Redraw(kqGL, qProgram, faMatrix2, faMatrix, dPointSize, faColors) {
	const kiMartrixLoc = kqGL.getUniformLocation(qProgram, 'um2Matrix');
	const kbTranspose = false;
	kqGL.uniformMatrix2fv(kiMartrixLoc, kbTranspose, faMatrix);

	const kiMartrixLoc2 = kqGL.getUniformLocation(qProgram, 'um2Matrix2');
	kqGL.uniformMatrix2fv(kiMartrixLoc2, kbTranspose, faMatrix2);

	const kiPointSizeLoc = kqGL.getUniformLocation(qProgram, 'ufPointSize');
	kqGL.uniform1f(kiPointSizeLoc, dPointSize);

	kqGL.bufferData(kqGL.ARRAY_BUFFER, faColors, kqGL.STATIC_DRAW);

	const kiCount = 9;
	const kiOffset = 0;
	kqGL.drawArrays(kqGL.POINTS, kiOffset, kiCount);
}

function MatrixProduct(faA, faB) {
	let faProduct = new Float32Array([
		0.0, 0.0,
		0.0, 0.0
	]);
	for (let c = 0; c < 2; ++c) {
		for (let r = 0; r < 2; ++r) {
			faProduct[2*c + r] = 0.0;
			for (let t = 0; t < 2; ++t) {
				// Row of A times the column of B
				faProduct[2*c + r] += faA[2*c + t]*faB[r + 2*t];
			}
		}		
	}
	return faProduct;
}
 

Output

 
 

© 2007–2026 XoaX.net LLC. All rights reserved.