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