steps3D - Tutorials - Работаем с WebGL

Работаем с WebGL

WebGL - это байндинги для java-script, позволяющие использовать OpenGL ES 2.0 в веб-страницах. Для этого используется являющийся частью HTML5 элемент canvas. Для рендеринга можно использовать все основные возможности OpenGL ES 2.0, включая шейдеры.

На данный момент IE8 не поддерживает WebGL, для всех остальных браузеров следует использовать developer builds. Здесь можно прочесть где достать версию браузера, поддерживающую WebGL и как ее установить.

В конце статьи приведен список различных ресурсов по WebGL, а для начала я приведу сайт learningwebgl.com, где приводится реализации на WebGL основных уроков с NeHe.

Важным отличием от обычного OpenGL является отсутствие так называемой immediate mode - все вершины передаются через вершинные буфера. Также нет стандартных матриц и функций для работы с ними - работа с необходимыми матрицами производится вручную и они передаются в шейдеры как uniform-переменные.

Существует несколько различных библиотек на java-script, облегчающих работу с матрицами - sylverster.js, glUtils.js и glu.js. Для их использования включите их в свою страницу, как показано ниже.

<script type="text/javascript" src="sylvester.js"></script>
<script type="text/javascript" src="glUtils.js"></script>

Следующим шагом будет добавление элемента canvas в страницу, для этого элемента необходимо задать его размер в пикселах. Также на самой странице следует задать функцию, которая будет вызываться при загрузке (onload).

<body onload="webGLStart();">
<P>
<canvas id="canvas" style="border: none;" width="500" height="500"><canvas>

В данном фрагменте страницы мы создали канвас размером 500 на 500 пикселов и задали функцию инициализации webGLStart, которые будет вызвана при загрузке - именно в этой функции и будет произвдена вся инициализация и настройка.

Обычно вводится глобальный объект (gl) используемый для обращения к OpenGL ES. Этот объект можно получить при помощи функции getContext объекта canvas вызвав ее с параметром "webgl".

После того как соответствующий объект (gl) получен, можно используя его напрамую, вызывать методы OpenGL, как показано в следующем примере.

var gl;

function webGLStart() 
{
	var canvas = document.getElementById("canvas");         // get canvas element from page

	if ( !canvas )
		alert("Could not initialise canvas, sorry :-(");
	
	try {
		gl = canvas.getContext("webgl");
		
	    if ( !gl )
			gl = canvas.getContext("experimental-webgl");
	} catch(e) {}

	if ( !gl )
		alert("Could not initialise WebGL, sorry :-(");

	initShaders ();
	initBuffers ();

	gl.clearColor ( 0.0, 0.0, 0.0, 1.0 );                   // setup OpenGL
	gl.clearDepth ( 1.0 );
	gl.enable     ( gl.DEPTH_TEST );
	gl.depthFunc  ( gl.LEQUAL );

	setInterval ( drawScene, 15 );
}

В приведенном коде легко узнаются вызовы стандартных функций OpenGL, только вместо glClearColor используется gl.clearColor, вместо glEnable - gl.enable и т.д.

Весь вывод осуществляется через шейдеры, поэтому необходимо текст вершинного и фрагментного шейдеров включить в страницу. Ниже приводятся простейшие варианты таких шейдеров, обратите внимание, что передача атрибутов вершины и матриц осуществляется через атрибуты и uniform-переменные.

<script id="shader-fs" type="x-shader/x-fragment">
    #ifdef GL_ES
       precision highp float;
    #endif
    varying vec4 color;
    void main(void) 
    {
        gl_FragColor = color;
    }
</script>
 
<script id="shader-vs" type="x-shader/x-vertex">
    attribute vec3 aVertexPosition;
    attribute vec4 aVertexColor;
    uniform mat4 mvMatrix;
    uniform mat4 prMatrix;
    varying vec4 color;

    void main(void) 
    {
	    gl_Position = prMatrix * mvMatrix * vec4 ( aVertexPosition, 1.0 );
	    color       = aVertexColor;
    }
</script>

Удобно сразу же вынести основные функции для инциализации WebGL, загрузки шейдеров и текстур в отдельный файл, в рассматриваемых в этой статье примерах используется файл glwrapper.js, содержимое которого приводится ниже (при этом для работы с матрицами используется библиотека sylverster.js).

function getGl ( canvas )
{
	if ( !canvas )
		alert("Could not initialise canvas, sorry :-(");
	
	try {
		gl = canvas.getContext("webgl");
		gl.viewportWidth  = canvas.width;
		gl.viewportHeight = canvas.height;
	} catch(e) {}

	if ( !gl )
		alert("Could not initialise WebGL, sorry :-(");

	gl.viewport(0, 0, canvas.width, canvas.height );
	gl.clearColor(0.0, 0.0, 0.0, 1.0);
	gl.clearDepth(1.0);
	gl.enable(gl.DEPTH_TEST);
	gl.depthFunc(gl.LEQUAL);
	
	return gl;
}

function getShader ( gl, id )
{
    var shaderScript = document.getElementById ( id );

    if (!shaderScript)
        return null;

    var str = "";
    var k = shaderScript.firstChild;

    while ( k ) 
    {
        if ( k.nodeType == 3 )
            str += k.textContent;

        k = k.nextSibling;
    }

    var shader;

    if ( shaderScript.type == "x-shader/x-fragment" )
        shader = gl.createShader ( gl.FRAGMENT_SHADER );
    else if ( shaderScript.type == "x-shader/x-vertex" )
        shader = gl.createShader(gl.VERTEX_SHADER);
    else
        return null;

    gl.shaderSource(shader, str);
    gl.compileShader(shader);

    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) 
    {
        alert(gl.getShaderInfoLog(shader));
        return null;
    }

    return shader;
}

function loadProgram ( gl, vertId, fragId ) 
{
    var fragmentShader = getShader ( gl, vertId );
    var vertexShader   = getShader ( gl, fragId );
    var shaderProgram  = gl.createProgram ();
    
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);

    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS))
    {
        alert("Could not initialise shaders");
        
        return null;
    }
    
    return shaderProgram;
}

function setMatrixUniform ( gl, program, name, mat )
{
    var loc  = gl.getUniformLocation ( program, name );
    
    gl.uniformMatrix4fv ( loc,  false, new Float32Array(mat.flatten())); 
}

function createGLTexture(gl, image, texture)
{
    gl.enable(gl.TEXTURE_2D);
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
    gl.generateMipmap(gl.TEXTURE_2D)
    gl.bindTexture(gl.TEXTURE_2D, null);
}

function loadImageTexture(gl, url)
{
    var texture          = gl.createTexture();
    texture.image        = new Image();
    texture.image.onload = function() { createGLTexture(gl, texture.image, texture) }
    texture.image.src    = url;
    
    return texture;
}

Рассмотрим теперь каким образом можно реализовать вывод треугольника с заданными цветами в вершинах. Для начала необходимо описать переменные, которые нам понадобятся. В число этих переменных кроме уже упомянутой переменной gl, также входят переменные, соответствующие модельно-видовой матрице (mvMatrix) и матрице проектирования (prMatrix), шейдеру (shaderProgram) и двум вершинным буферам - для координат вершин (posBuffer) и цветов (colorBuffer).

var mvMatrix;
var prMatrix;
var shaderProgram;
var vertexPositionAttribute;
var vertexColorAttribute;
var posBuffer;
var colorBuffer;

Дальнейшая инициализация состоит из загрузки шейдеров и создания вершинных массивов:

function initShaders () 
{
	shaderProgram = loadProgram ( gl, "shader-vs", "shader-fs" );
	gl.useProgram(shaderProgram);

	vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
	gl.enableVertexAttribArray(vertexPositionAttribute);

	vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
	gl.enableVertexAttribArray(vertexColorAttribute);  
}

function initBuffers() 
{
	posBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
	var vertices = [
		 0.0,  1.0,  0.0,
		-1.0, -1.0,  0.0,
		 1.0, -1.0,  0.0
	];
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
	posBuffer.itemSize = 3;
	posBuffer.numItems = 3;

	colorBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
	var colors = [
		1.0, 0.0, 0.0, 1.0,
		0.0, 1.0, 0.0, 1.0,
		0.0, 0.0, 1.0, 1.0,
	];
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
	colorBuffer.itemSize = 4;
	colorBuffer.numItems = 3;	
}

Обратите внимание на то, каким образом создаютя OpenGL-массивы по java-script-массивам. Для передачи блока памяти с необходимыми данными (строящимися по обычному массиву языка java-script) используются специальными классы Float32Array, Uint16Array и т.п.).

Поскольку к объекту в java-script можно добавить любые дополнительные поля, то удобно к объектам, соответствующим вершинным буферам, сразу же добавить их основные параметры - размер одного элемента (itemSize) и количество таких элементов в буфере (numItems), которые будут потом использоваться при выводе задаваемого буфером объекта.

Также удобно ввести небольшой набор простых функций для работы с матрицами. Ниже приводится сильно упрощенный вариант, в принципе легко можно создать аналог соответствующих функций в "классическом" OpenGL.

function loadIdentity() 
{
	mvMatrix = Matrix.I(4);
}

function multMatrix(m) 
{
	mvMatrix = mvMatrix.x(m);
}

function mvTranslate(v) 
{
	var m = Matrix.Translation($V([v[0], v[1], v[2]])).ensure4x4();
	
	multMatrix(m);
}

function perspective(fovy, aspect, znear, zfar) 
{
	prMatrix = makePerspective(fovy, aspect, znear, zfar);
}

function setMatrixUniforms() 
{
	setMatrixUniform ( gl, shaderProgram, "prMatrix", prMatrix );
	setMatrixUniform ( gl, shaderProgram, "mvMatrix", mvMatrix );
}

Заключительным элементом будет функция, осуществляющая вывод треугольника, задаваемого вершинными масивами, при помощи шейдера.

function drawScene() 
{
	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

	perspective  ( 45, 1.0, 0.1, 100.0 );
	loadIdentity ();
	mvTranslate  ( [0.0, 0.0, -5.0] );

	gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
	gl.vertexAttribPointer(vertexPositionAttribute, posBuffer.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
	gl.vertexAttribPointer(vertexColorAttribute, colorBuffer.itemSize, gl.FLOAT, false, 0, 0);

	setMatrixUniforms ();
	gl.drawArrays(gl.TRIANGLES, 0, posBuffer.numItems);
	gl.flush ();
}

По этой ссылке можно перейти на страницу, строящую треугольник средствами WebGL.

В качестве следующего примера рассмотрим рендеринг вращающегося текстурированного куба. Этому примеру соответсвует следующая ссылка.

Нам понадобится метод для домножения модельно-видовой матрицы на матрицу поворота (аналог glRotate) и функция, создающая все необходимые вершинные буфера (включая буфер с текстурными координатами).

function mvRotate(ang, v) 
{
	var arad = ang * Math.PI / 180.0;
	var m    = Matrix.Rotation(arad, $V([v[0], v[1], v[2]])).ensure4x4();
	
	multMatrix(m);
}

function initBuffers() 
{
	cubeVertexPositionBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
	vertices = [
		-1.0, -1.0,  1.0,               // Front face
		 1.0, -1.0,  1.0,
		 1.0,  1.0,  1.0,
		-1.0,  1.0,  1.0,

		-1.0, -1.0, -1.0,               // Back face
		-1.0,  1.0, -1.0,
		 1.0,  1.0, -1.0,
		 1.0, -1.0, -1.0,

		-1.0,  1.0, -1.0,               // Top face
		-1.0,  1.0,  1.0,
		 1.0,  1.0,  1.0,
		 1.0,  1.0, -1.0,

		-1.0, -1.0, -1.0,               // Bottom face
		 1.0, -1.0, -1.0,
		 1.0, -1.0,  1.0,
		-1.0, -1.0,  1.0,

		 1.0, -1.0, -1.0,               // Right face
		 1.0,  1.0, -1.0,
		 1.0,  1.0,  1.0,
		 1.0, -1.0,  1.0,

		-1.0, -1.0, -1.0,               // Left face
		-1.0, -1.0,  1.0,
		-1.0,  1.0,  1.0,
		-1.0,  1.0, -1.0,
	];
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
	cubeVertexPositionBuffer.itemSize = 3;
	cubeVertexPositionBuffer.numItems = 24;

	cubeVertexNormalBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexNormalBuffer);
	var vertexNormals = [
		0.0,  0.0,  1.0,                // Front face
		0.0,  0.0,  1.0,
		0.0,  0.0,  1.0,
		0.0,  0.0,  1.0,

		0.0,  0.0, -1.0,                // Back face
		0.0,  0.0, -1.0,
		0.0,  0.0, -1.0,
		0.0,  0.0, -1.0,

		0.0,  1.0,  0.0,                // Top face
		0.0,  1.0,  0.0,
		0.0,  1.0,  0.0,
		0.0,  1.0,  0.0,

		0.0, -1.0,  0.0,                // Bottom face
		0.0, -1.0,  0.0,
		0.0, -1.0,  0.0,
		0.0, -1.0,  0.0,

		1.0,  0.0,  0.0,                // Right face
		1.0,  0.0,  0.0,
		1.0,  0.0,  0.0,
		1.0,  0.0,  0.0,

		-1.0,  0.0,  0.0,               // Left face
		-1.0,  0.0,  0.0,
		-1.0,  0.0,  0.0,
		-1.0,  0.0,  0.0,
	];
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals), gl.STATIC_DRAW);
	cubeVertexNormalBuffer.itemSize = 3;
	cubeVertexNormalBuffer.numItems = 24;

	cubeVertexTextureCoordBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
	var textureCoords = [
		  0.0, 0.0,                   // Front face
		  1.0, 0.0,
		  1.0, 1.0,
		  0.0, 1.0,

		  1.0, 0.0,	                  // Back face
		  1.0, 1.0,
		  0.0, 1.0,
		  0.0, 0.0,

		  0.0, 1.0,                   // Top face
		  0.0, 0.0,
		  1.0, 0.0,
		  1.0, 1.0,

		  1.0, 1.0,	                  // Bottom face
		  0.0, 1.0,
		  0.0, 0.0,
		  1.0, 0.0,

		  1.0, 0.0,                   // Right face
		  1.0, 1.0,
		  0.0, 1.0,
		  0.0, 0.0,

		  0.0, 0.0,                   // Left face
		  1.0, 0.0,
		  1.0, 1.0,
		  0.0, 1.0,
	];
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
	cubeVertexTextureCoordBuffer.itemSize = 2;
	cubeVertexTextureCoordBuffer.numItems = 24;

	cubeVertexIndexBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
	var cubeVertexIndices = [
		0,  1,  2,    0,  2,  3,      // Front face
		4,  5,  6,    4,  6,  7,      // Back face
		8,  9,  10,   8,  10, 11,     // Top face
		12, 13, 14,   12, 14, 15,     // Bottom face
		16, 17, 18,   16, 18, 19,     // Right face
		20, 21, 22,   20, 22, 23      // Left face
	]
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
	cubeVertexIndexBuffer.itemSize = 1;
	cubeVertexIndexBuffer.numItems = 36;
} 

Также небольшие изменения претерпевает функция вывода.

function drawScene() 
{
	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

	perspective  ( 45, 1.0, 0.1, 100.0 );
	loadIdentity ();
	mvTranslate  ( [0.0, 0.0, -7.0] );
	mvRotate     ( xRot, [1, 0, 0] );
	mvRotate     ( yRot, [0, 1, 0] );

	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexNormalBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, cubeVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
	gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

	gl.activeTexture(gl.TEXTURE0);
	gl.bindTexture(gl.TEXTURE_2D, crateTexture);
	gl.uniform1i(shaderProgram.samplerUniform, 0);

	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
	setMatrixUniforms();
	gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

	xRot += 0.6;
	yRot += 0.3;
}

Для загрузки текстур используется функция loadImageTexture, определенная в файле glwrapper.js следующим образом:

function createGLTexture(gl, image, texture)
{
    gl.enable(gl.TEXTURE_2D);
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);    
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
    gl.generateMipmap(gl.TEXTURE_2D)
    gl.bindTexture(gl.TEXTURE_2D, null);
}

function loadImageTexture(gl, url)
{
    var texture = gl.createTexture();
    texture.image = new Image();
    texture.image.onload = function() { createGLTexture(gl, texture.image, texture) }
    texture.image.src = url;
    
    return texture;
}

Заключительный пример выводит тор, который можно вращать при помощи мыши. Для освещения тора используется модель Гуч (Gooch), положение источника света зафиксировано.

Основное отличие данного примера заключается в необходимости построения вершинных массивов - явно перечислятиь все точки в самой програмем было бы неудобно - их гораздо проще вычислить в момент загрузки.

function initBuffers() 
{
	var vertices  = [];
	var normals   = [];
	var tex       = [];
	var ind       = [];
	var rings     = 30;
	var sides     = 30;
	var r1        = 1.0;
	var r2        = 3.0;
	var ringDelta = 2.0 * 3.1415926 / rings;
	var	sideDelta = 2.0 * 3.1415926 / sides;
	var	invRings  = 1.0 / rings;
	var	invSides  = 1.0 / sides;
	var	index       = 0;
	var	numVertices = 0;
	var numFaces    = 0;
	var	i, j;

	for ( i = 0; i <= rings; i++ )
	{
		var theta    = i * ringDelta;
		var cosTheta = Math.cos ( theta );
		var sinTheta = Math.sin ( theta );

		for ( j = 0; j <= sides; j++ )
		{
			var phi    = j * sideDelta;
			var cosPhi = Math.cos ( phi );
			var sinPhi = Math.sin ( phi );
			var dist   = r2 + r1 * cosPhi;

			vertices.push ( cosTheta * dist );
			vertices.push ( -sinTheta * dist );
			vertices.push ( r1 * sinPhi );
			
			tex.push     ( j * invSides );
			tex.push     ( i * invRings );
			
			normals.push ( cosTheta * cosPhi );
			normals.push ( -sinTheta * cosPhi );
			normals.push ( sinPhi );

			numVertices++;
		}
	}
	
	for ( i = 0; i < rings; i++ )
		for ( j = 0; j < sides; j++ )
		{
			ind.push ( i*(sides+1) + j );
			ind.push ( (i+1)*(sides+1) + j );
			ind.push ( (i+1)*(sides+1) + j + 1 );
			
			ind.push ( i*(sides+1) + j );
			ind.push ( (i+1)*(sides+1) + j + 1 );
			ind.push ( i*(sides+1) + j + 1 );
			
			numFaces += 2;
		}
	
	posBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
	posBuffer.itemSize = 3;
	posBuffer.numItems = numVertices;

	normalBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
	normalBuffer.itemSize = 3;
	normalBuffer.numItems = numVertices;

	texCoordBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(tex), gl.STATIC_DRAW);
	texCoordBuffer.itemSize = 2;
	texCoordBuffer.numItems = numVertices;

	indexBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(ind), gl.STATIC_DRAW);
	indexBuffer.itemSize = 1;
	indexBuffer.numItems = 3*numFaces;		// set actual # of values
}

Для поддержки вращения тора при помощи мыши следует для объекта canvas задать функции-обработчики для основных событий, связанных с мышью.

function webGLStart() 
{
	var canvas = document.getElementById("canvas");

	gl = getGl ( canvas );

	initShaders ();
	initBuffers ();

	canvas.onmousedown = function ( ev )
	{
		drag  = 1;
		xOffs = ev.clientX;
		yOffs = ev.clientY;
	}
	
	canvas.onmouseup = function ( ev )
	{
		drag  = 0;
		xOffs = ev.clientX;
		yOffs = ev.clientY;
	}
	
	canvas.onmousemove = function ( ev )
	{
		if ( drag != 0 )
		{
			yRot -= xOffs - ev.clientX;
			xRot -= yOffs - ev.clientY;
		}
		
		xOffs = ev.clientX;
		yOffs = ev.clientY;
	}
	
	setInterval ( drawScene, 15 );
}

Код для вывода тора лишь незначительно отличается от соответствующего кода из предыдущего примера.

function drawScene() 
{
	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

	perspective(45, 1.0, 0.1, 100.0);
	loadIdentity();

	mvTranslate([0.0, 0.0, -15.0]);

	mvRotate(xRot, [1, 0, 0]);
	mvRotate(yRot, [0, 1, 0]);

	gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, posBuffer.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, normalBuffer.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
	gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, texCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	gl.uniform4f ( gl.getUniformLocation(shaderProgram, "lightPos" ), 10.0, 10.0, -10.0, 1.0 );
	gl.uniform4f ( gl.getUniformLocation(shaderProgram, "eyePos"   ), 0.0, 0.0, -15.0, 1.0 );	
	
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
	setMatrixUniforms();
	gl.drawElements(gl.TRIANGLES, indexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
}

По этой ссылке можно перейти к странице с тором.

Ниже привоодится ряд полезных ссылок по WebGL.

WebGL Specification

Learning WebGL

Planet WebGL.

Shader Toy - примеры ряда шейдерных эффектов, реализованных через WebGL.

nihilogic

Yohei Shimomae's blog

Mark Steel blog

Пример на WebGL + WebGLU в 25 строк.

WebGLU.

Escher-Droste effect in WebGL.

Vladimir Vukicevic.