Эмуляция графики на основе палитры в WebGL V. s. Холст 2D

В настоящее время я использую контекст 2D canvas для рисования изображения, сгенерированного (ОТ ПИКСЕЛЯ ДО пикселя, но обновленного в целом буфером сразу после сгенерированного кадра) из JavaScript со скоростью около 25 кадров в секунду. Сгенерированное изображение всегда составляет один байт (целочисленный / типизированный массив) на пиксель, а для генерации конечного результата RGB используется фиксированная палитра. Масштабирование также необходимо принять к размеру холста (т. е. переход в полноэкранный режим) и/или по запросу пользователя (кнопки увеличения/уменьшения).

в 2D-контекста canvas в порядке для этой цели, однако мне любопытно, может ли WebGL обеспечить лучший результат и / или лучшую производительность. Обратите внимание: я не хочу помещать пиксели через webGL, я хочу поместить пиксели в свой буфер (который в основном Uint8Array) и использовать этот буфер (один раз) для обновления контекста. Я не знаю слишком много о WebGL, но использование необходимого сгенерированного изображения в качестве какой-то текстуры будет работать как-то, например? Тогда мне нужно будет обновить текстуру примерно на скорости 25fps, я догадка.

Это было бы действительно фантастически, если WebGL каким-то образом поддерживает преобразование цветового пространства. С 2D-контекстом мне нужно преобразовать 1 байт / пиксельный буфер в RGBA для imagedata в JavaScript для каждого пикселя ... Масштабирование (для 2D-контекста)теперь выполняется путем изменения стиля высоты/ширины холста, поэтому браузеры масштабируют изображение. Однако я думаю, что это может быть медленнее, чем то, что WebGL может сделать с поддержкой hw, а также (я надеюсь) WebGL может дать большую гибкость для управления масштабированием, например, с 2D-контекстом браузеры будут выполнять сглаживание, даже если я не хочу этого делать (например, целочисленный коэффициент масштабирования), и, возможно, по этой причине он иногда может быть довольно медленным.

Я уже пытался изучить несколько учебников WebGL, но все они начинаются с объектов, фигур, 3D - кубов и т. д. Мне не нужен какой - либо классический объект для визуализации только того, что может сделать 2D - контекст-в надежде, что WebGL может быть более быстрым решением для той же задачи! Конечно, если здесь нет победы с WebGL вообще, я бы продолжал использовать 2D-контекст.

чтобы быть ясным: это своего рода эмулятор компьютерного оборудования, выполненный в JavaScript, и его выход (что будет видно на подключенном к нему телевизоре PAL) отображается через контекст холста. Машина имеет фиксированную палитру с 256 элементами, внутренне ей нужен только один байт для пикселя для определения своего цвета.

2 ответов


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

текстура палитры 256x1 RGBA пикселей. Текстура изображения любого размера, но только один канал Альфа-текстуры. Затем вы можете посмотреть значение из изображения

    float index = texture2D(u_image, v_texcoord).a * 255.0;

и используйте это значение для поиска цвета в палитре

    gl_FragColor = texture2D(u_palette, vec2((index + 0.5) / 256.0, 0.5));

ваши шейдеры может быть что-то вроде этого

Вершинный Шейдер

attribute vec4 a_position;
varying vec2 v_texcoord;
void main() {
  gl_Position = a_position;

  // assuming a unit quad for position we
  // can just use that for texcoords. Flip Y though so we get the top at 0
  v_texcoord = a_position.xy * vec2(0.5, -0.5) + 0.5;
}    

фрагмент шейдеров

precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_image;
uniform sampler2D u_palette;

void main() {
    float index = texture2D(u_image, v_texcoord).a * 255.0;
    gl_FragColor = texture2D(u_palette, vec2((index + 0.5) / 256.0, 0.5));
}

тогда вам просто нужна текстура палитры.

 // Setup a palette.
 var palette = new Uint8Array(256 * 4);

 // I'm lazy so just setting 4 colors in palette
 function setPalette(index, r, g, b, a) {
     palette[index * 4 + 0] = r;
     palette[index * 4 + 1] = g;
     palette[index * 4 + 2] = b;
     palette[index * 4 + 3] = a;
 }
 setPalette(1, 255, 0, 0, 255); // red
 setPalette(2, 0, 255, 0, 255); // green
 setPalette(3, 0, 0, 255, 255); // blue

 // upload palette
 ...
 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA, 
               gl.UNSIGNED_BYTE, palette);

и ваш образ. Это альфа-только изображение, так что только 1 канал.

 // Make image. Just going to make something 8x8
 var image = new Uint8Array([
     0,0,1,1,1,1,0,0,
     0,1,0,0,0,0,1,0,
     1,0,0,0,0,0,0,1,
     1,0,2,0,0,2,0,1,
     1,0,0,0,0,0,0,1,
     1,0,3,3,3,3,0,1,
     0,1,0,0,0,0,1,0,
     0,0,1,1,1,1,0,0,
 ]);

 // upload image
 ....
 gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, 8, 8, 0, gl.ALPHA, 
               gl.UNSIGNED_BYTE, image);

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

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

вот пример:

var canvas = document.getElementById("c");
var gl = canvas.getContext("webgl");

// Note: createProgramFromScripts will call bindAttribLocation
// based on the index of the attibute names we pass to it.
var program = twgl.createProgramFromScripts(
    gl, 
    ["vshader", "fshader"], 
    ["a_position", "a_textureIndex"]);
gl.useProgram(program);
var imageLoc = gl.getUniformLocation(program, "u_image");
var paletteLoc = gl.getUniformLocation(program, "u_palette");
// tell it to use texture units 0 and 1 for the image and palette
gl.uniform1i(imageLoc, 0);
gl.uniform1i(paletteLoc, 1);

// Setup a unit quad
var positions = [
      1,  1,  
     -1,  1,  
     -1, -1,  
      1,  1,  
     -1, -1,  
      1, -1,  
];
var vertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

// Setup a palette.
var palette = new Uint8Array(256 * 4);

// I'm lazy so just setting 4 colors in palette
function setPalette(index, r, g, b, a) {
    palette[index * 4 + 0] = r;
    palette[index * 4 + 1] = g;
    palette[index * 4 + 2] = b;
    palette[index * 4 + 3] = a;
}
setPalette(1, 255, 0, 0, 255); // red
setPalette(2, 0, 255, 0, 255); // green
setPalette(3, 0, 0, 255, 255); // blue

// make palette texture and upload palette
gl.activeTexture(gl.TEXTURE1);
var paletteTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, paletteTex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, palette);

// Make image. Just going to make something 8x8
var image = new Uint8Array([
    0,0,1,1,1,1,0,0,
    0,1,0,0,0,0,1,0,
    1,0,0,0,0,0,0,1,
    1,0,2,0,0,2,0,1,
    1,0,0,0,0,0,0,1,
    1,0,3,3,3,3,0,1,
    0,1,0,0,0,0,1,0,
    0,0,1,1,1,1,0,0,
]);
    
// make image textures and upload image
gl.activeTexture(gl.TEXTURE0);
var imageTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, imageTex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, 8, 8, 0, gl.ALPHA, gl.UNSIGNED_BYTE, image);
    
gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/twgl.min.js"></script>
<script id="vshader" type="whatever">
    attribute vec4 a_position;
    varying vec2 v_texcoord;
    void main() {
      gl_Position = a_position;
    
      // assuming a unit quad for position we
      // can just use that for texcoords. Flip Y though so we get the top at 0
      v_texcoord = a_position.xy * vec2(0.5, -0.5) + 0.5;
    }    
</script>
<script id="fshader" type="whatever">
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_image;
uniform sampler2D u_palette;
    
void main() {
    float index = texture2D(u_image, v_texcoord).a * 255.0;
    gl_FragColor = texture2D(u_palette, vec2((index + 0.5) / 256.0, 0.5));
}
</script>
<canvas id="c" width="256" height="256"></canvas>

для анимации просто обновите изображение, а затем повторно загрузите его в текстуру

 gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, 8, 8, 0, gl.ALPHA, 
               gl.UNSIGNED_BYTE, image);

пример:

var canvas = document.getElementById("c");
var gl = canvas.getContext("webgl");

// Note: createProgramFromScripts will call bindAttribLocation
// based on the index of the attibute names we pass to it.
var program = twgl.createProgramFromScripts(
    gl, 
    ["vshader", "fshader"], 
    ["a_position", "a_textureIndex"]);
gl.useProgram(program);
var imageLoc = gl.getUniformLocation(program, "u_image");
var paletteLoc = gl.getUniformLocation(program, "u_palette");
// tell it to use texture units 0 and 1 for the image and palette
gl.uniform1i(imageLoc, 0);
gl.uniform1i(paletteLoc, 1);

// Setup a unit quad
var positions = [
      1,  1,  
     -1,  1,  
     -1, -1,  
      1,  1,  
     -1, -1,  
      1, -1,  
];
var vertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

// Setup a palette.
var palette = new Uint8Array(256 * 4);

// I'm lazy so just setting 4 colors in palette
function setPalette(index, r, g, b, a) {
    palette[index * 4 + 0] = r;
    palette[index * 4 + 1] = g;
    palette[index * 4 + 2] = b;
    palette[index * 4 + 3] = a;
}
setPalette(1, 255, 0, 0, 255); // red
setPalette(2, 0, 255, 0, 255); // green
setPalette(3, 0, 0, 255, 255); // blue

// make palette texture and upload palette
gl.activeTexture(gl.TEXTURE1);
var paletteTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, paletteTex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, palette);

// Make image. Just going to make something 8x8
var width = 8;
var height = 8;
var image = new Uint8Array([
    0,0,1,1,1,1,0,0,
    0,1,0,0,0,0,1,0,
    1,0,0,0,0,0,0,1,
    1,0,2,0,0,2,0,1,
    1,0,0,0,0,0,0,1,
    1,0,3,3,3,3,0,1,
    0,1,0,0,0,0,1,0,
    0,0,1,1,1,1,0,0,
]);
    
// make image textures and upload image
gl.activeTexture(gl.TEXTURE0);
var imageTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, imageTex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, width, height, 0, gl.ALPHA, gl.UNSIGNED_BYTE, image);
    
var frameCounter = 0;
function render() {  
  ++frameCounter;
    
  // skip 3 of 4 frames so the animation is not too fast
  if ((frameCounter & 3) == 0) {
    // rotate the image left
    for (var y = 0; y < height; ++y) {
      var temp = image[y * width];
      for (var x = 0; x < width - 1; ++x) {
        image[y * width + x] = image[y * width + x + 1];
      }
      image[y * width + width - 1] = temp;
    }
    // re-upload image
    gl.activeTexture(gl.TEXTURE0);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, width, height, 0, gl.ALPHA,
                  gl.UNSIGNED_BYTE, image);
    
    gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
  }
  requestAnimationFrame(render);
}
render();
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/twgl.min.js"></script>
<script id="vshader" type="whatever">
    attribute vec4 a_position;
    varying vec2 v_texcoord;
    void main() {
      gl_Position = a_position;
    
      // assuming a unit quad for position we
      // can just use that for texcoords. Flip Y though so we get the top at 0
      v_texcoord = a_position.xy * vec2(0.5, -0.5) + 0.5;
    }    
</script>
<script id="fshader" type="whatever">
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_image;
uniform sampler2D u_palette;
    
void main() {
    float index = texture2D(u_image, v_texcoord).a * 255.0;
    gl_FragColor = texture2D(u_palette, vec2((index + 0.5) / 256.0, 0.5));
}
</script>
<canvas id="c" width="256" height="256"></canvas>

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

вы также можете обновить палитру аналогично для анимации палитры. Просто измените палитру и повторно загрузите ее

 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA, 
               gl.UNSIGNED_BYTE, palette);

пример:

var canvas = document.getElementById("c");
var gl = canvas.getContext("webgl");

// Note: createProgramFromScripts will call bindAttribLocation
// based on the index of the attibute names we pass to it.
var program = twgl.createProgramFromScripts(
    gl, 
    ["vshader", "fshader"], 
    ["a_position", "a_textureIndex"]);
gl.useProgram(program);
var imageLoc = gl.getUniformLocation(program, "u_image");
var paletteLoc = gl.getUniformLocation(program, "u_palette");
// tell it to use texture units 0 and 1 for the image and palette
gl.uniform1i(imageLoc, 0);
gl.uniform1i(paletteLoc, 1);

// Setup a unit quad
var positions = [
      1,  1,  
     -1,  1,  
     -1, -1,  
      1,  1,  
     -1, -1,  
      1, -1,  
];
var vertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

// Setup a palette.
var palette = new Uint8Array(256 * 4);

// I'm lazy so just setting 4 colors in palette
function setPalette(index, r, g, b, a) {
    palette[index * 4 + 0] = r;
    palette[index * 4 + 1] = g;
    palette[index * 4 + 2] = b;
    palette[index * 4 + 3] = a;
}
setPalette(1, 255, 0, 0, 255); // red
setPalette(2, 0, 255, 0, 255); // green
setPalette(3, 0, 0, 255, 255); // blue

// make palette texture and upload palette
gl.activeTexture(gl.TEXTURE1);
var paletteTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, paletteTex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, palette);

// Make image. Just going to make something 8x8
var width = 8;
var height = 8;
var image = new Uint8Array([
    0,0,1,1,1,1,0,0,
    0,1,0,0,0,0,1,0,
    1,0,0,0,0,0,0,1,
    1,0,2,0,0,2,0,1,
    1,0,0,0,0,0,0,1,
    1,0,3,3,3,3,0,1,
    0,1,0,0,0,0,1,0,
    0,0,1,1,1,1,0,0,
]);
    
// make image textures and upload image
gl.activeTexture(gl.TEXTURE0);
var imageTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, imageTex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, width, height, 0, gl.ALPHA, gl.UNSIGNED_BYTE, image);
    
var frameCounter = 0;
function render() {  
  ++frameCounter;
    
  // skip 3 of 4 frames so the animation is not too fast
  if ((frameCounter & 3) == 0) {
    // rotate the 3 palette colors
    var tempR = palette[4 + 0];
    var tempG = palette[4 + 1];
    var tempB = palette[4 + 2];
    var tempA = palette[4 + 3];
    setPalette(1, palette[2 * 4 + 0], palette[2 * 4 + 1], palette[2 * 4 + 2], palette[2 * 4 + 3]);
    setPalette(2, palette[3 * 4 + 0], palette[3 * 4 + 1], palette[3 * 4 + 2], palette[3 * 4 + 3]);
    setPalette(3, tempR, tempG, tempB, tempA);

    // re-upload palette
    gl.activeTexture(gl.TEXTURE1);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA,
                  gl.UNSIGNED_BYTE, palette);
    
    gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
  }
  requestAnimationFrame(render);
}
render();
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/twgl.min.js"></script>
<script id="vshader" type="whatever">
    attribute vec4 a_position;
    varying vec2 v_texcoord;
    void main() {
      gl_Position = a_position;
    
      // assuming a unit quad for position we
      // can just use that for texcoords. Flip Y though so we get the top at 0
      v_texcoord = a_position.xy * vec2(0.5, -0.5) + 0.5;
    }    
</script>
<script id="fshader" type="whatever">
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_image;
uniform sampler2D u_palette;
    
void main() {
    float index = texture2D(u_image, v_texcoord).a * 255.0;
    gl_FragColor = texture2D(u_palette, vec2((index + 0.5) / 256.0, 0.5));
}
</script>
<canvas id="c" width="256" height="256"></canvas>

немного связан этот пример шейдера плитка http://blog.tojicode.com/2012/07/sprite-tile-maps-on-gpu.html


предположительно вы создаете массив javascript, который составляет около 512 x 512 (размер PAL)...

шейдер фрагмента WebGL определенно может сделать ваше преобразование палитры довольно красиво. Рецепт будет выглядеть примерно так:

  1. настройте WebGL с "геометрией" всего из двух треугольников, которые охватывают ваш видовой экран. (GL - это все треугольники.) Это самое большое беспокойство, если вы еще не GL свободно. Но все не так плохо. Проведите некоторое время с http://learningwebgl.com/blog/?page_id=1217 . Но это будет ~100 строк мелочи. Цена входа.
  2. создайте буфер кадров в памяти в 4 раза больше. (Я думаю, что текстуры всегда должны быть широко?) И заполните каждый четвертый байт, компонент R, своими значениями пикселей. Используйте новый Float32Array, чтобы выделить его. Можно использовать значения 0-255 или разделить их на 0,0-1,0. Мы передадим это webgl как текстуру. Это меняет каждый кадр.
  3. построить второй текстура 256 x 1 пикселей, которая является таблицей поиска палитры. Это никогда не меняется (если палитра не может быть изменена?).
  4. в шейдере фрагментов используйте эмулированную текстуру буфера кадров в качестве поиска в палитре. Доступ к первому пикселю в палитре осуществляется в местоположении (0.5 / 256.0, 0.5), в середине пикселя.
  5. на каждом кадре повторно отправьте эмулированную текстуру буфера кадров и перерисуйте. Нажатие пикселей на GPU дорого... но изображение размером с PAL довольно небольшой по современным меркам.
  6. Бонусный шаг: вы можете улучшить шейдер фрагментов для имитации линий развертки, переплетения видео или других милых артефактов эмуляции (точки люминофора?) на современных дисплеях с высоким разрешением, все бесплатно для вашего javascript!

Это просто набросок. Но это сработает. WebGL-довольно низкоуровневый API и довольно гибкий, но стоит усилий (если вам нравятся такие вещи, которые я делаю. :-) ).

снова, http://learningwebgl.com/blog/?page_id=1217 хорошо рекомендуется для общего руководства WebGL.