在 WebGL 与 Canvas 2D 中模拟基于调色板的图形

IT技术 javascript canvas webgl scaling color-palette
2021-03-15 01:41:07

目前,我正在使用 2D 画布上下文以大约 25fps 的速率从 JavaScript 绘制生成的图像(从像素到像素,但在生成帧后作为整个缓冲区刷新一次)。生成的图像始终为每个像素一个字节(整数/类型数组),并且使用固定调色板生成 RGB 最终结果。还需要缩放以适应画布的大小(即:进入全屏)和/或根据用户请求(放大/缩小按钮)。

画布的 2D 上下文可以用于此目的,但是我很好奇 WebGL 是否可以提供更好的结果和/或更好的性能。请注意:我不想通过 webGL 放置像素,我想将像素放入我的缓冲区(基本上是 Uint8Array),并使用该缓冲区(一次)刷新上下文。我对 WebGL 不太了解,但是例如使用所需的生成图像作为某种纹理会以某种方式起作用吗?然后我需要以大约 25fps 的速率刷新纹理,我猜。

如果 WebGL 以某种方式支持颜色空间转换,那就太棒了。对于 2D 上下文,我需要将 JavaScript 中每个像素的图像数据的 1 字节/像素缓冲区转换为 RGBA ......现在通过改变画布的高度/宽度样式来完成缩放(对于 2D 上下文),因此浏览器会缩放图像然后。但是我想它可能比 WebGL 在硬件支持下可以做的要慢,而且(我希望)WebGL 可以提供更大的灵活性来控制缩放,例如使用 2D 上下文,即使我不想,浏览器也会进行抗锯齿做(例如:整数缩放因子),也许这就是它有时会很慢的原因。

我已经尝试学习了几个 WebGL 教程,但它们都是从对象、形状、3D 立方体等开始的,我不需要任何 - 经典 - 对象来仅渲染 2D 上下文可以做的事情 - 希望对于同样的任务,WebGL 可以是一个更快的解决方案!当然,如果 WebGL 在这里根本没有胜利,我会继续使用 2D 上下文。

需要明确的是:这是一种用 JavaScript 完成的计算机硬件模拟器,它的输出(可以在连接到它的 PAL 电视上看到的内容)是通过画布上下文呈现的。这台机器有固定的调色板,有 256 个元素,在内部它只需要一个字节来定义一个像素的颜色。

2个回答

您可以使用纹理作为调色板,使用不同的纹理作为图像。然后您从图像纹理中获取一个值,并使用它从调色板纹理中查找颜色。

调色板纹理是 256x1 RGBA 像素。您的图像纹理是您想要的任何尺寸,但只是一个单通道 ALPHA 纹理。然后您可以从图像中查找值

    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);

还有你的形象。这是一个只有 alpha 的图像,所以只有 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);

这是一个工作示例:

要制作动画,只需更新图像,然后将其重新上传到纹理中

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

例子:

当然,假设您的目标是通过操作像素在 CPU 上制作动画。否则你可以使用任何普通的 webgl 技术来操纵纹理坐标或其他任何东西。

您还可以为调色板动画更新类似的调色板。只需修改调色板并重新上传即可

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

例子:

稍微相关的是这个瓷砖着色器示例 http://blog.tojicode.com/2012/07/sprite-tile-maps-on-gpu.html

好吧,稍微调整一下颜色表后,我得到了预期的结果。谢谢。但是,如果我将第二个参数的值保持为 0.0,它将正确呈现图像,更改它会更改颜色。'gl_FragColor = texture2D(u_palette, vec2(index, 0.0));',
2021-04-17 01:41:07
恭喜!是的,如果纹理是 1 行高,则第二个值可以是任何值。可以说,对纹素的中心进行采样比对边缘进行采样更正确。请参阅链接到第一个值(而不是第二个)中的情况的帖子
2021-04-18 01:41:07
另外,加起来,<br/>画布的逻辑imgData[idx++] = window.colorBufView[position+2]; // red imgData[idx++] = window.colorBufView[position+1]; // green imgData[idx++] = window.colorBufView[position]; // blue imgData[idx++] = 255; // alpha可以写在片段着色器中吗(见下面的评论)
2021-04-19 01:41:07
'void main(void) {','float index = texture2D(u_image, v_texcoord).a;', 'gl_FragColor.r = texture2D(u_palette, vec2(index , 0.5)).r;', 'gl_FragColor.g = texture2D(u_palette, vec2(index + 0.001, 0.5)).g;', 'gl_FragColor.b = texture2D(u_palette, vec2(index + 0.002, 0.5)).b;', 'gl_FragColor.a = 1.0;', '}'
2021-04-19 01:41:07
在最初的例子中,u_palette 已经指向一个 RGBA 纹理,所以一个 texture2D 命令拉出 RGBA。无需单独将它们拉出。你的 1.0、2.0、3.0 作为 texcoord 也不做任何事情。在夹紧或重复时,它们是相同的坐标,因为纹理坐标仅从 0.0 到 1.0。PS:我更新了上面的片段着色器。请参阅此帖子以了解原因其他想法,您需要确保所有纹理都使用 gl.NEAREST 进行过滤,因为它们是调色板的索引,而过滤会破坏它。
2021-05-12 01:41:07

据推测,您正在构建一个大约 512 x 512(PAL 大小)的 javascript 数组......

WebGL 片段着色器绝对可以很好地完成您的调色板转换。食谱会是这样的:

  1. 使用跨越视口的两个三角形的“几何”设置 WebGL。(GL 都是三角形。)这是最大的麻烦,如果您还不是 GL 流利的。但也没有那么糟糕。http://learningwebgl.com/blog/?page_id=1217 共度美好时光但它将是大约 100 行的东西。入场价格。
  2. 将内存中的帧缓冲区构建大 4 倍。(我认为纹理总是必须是 RGBA?)并用您的像素值填充每四个字节,即 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 整体指南的推荐。

您可以制作一个通道纹理。gl.ALPHAgl.LUMINANCE至于在 256x1 纹理中查找,如果您将纹理过滤设置为,gl.NEAREST那么您应该能够直接使用类似的内容进行查找。float index = texture2D(u_pixels, v_texcoord).r; gl_FragColor = texture2D(u_palette, vec2(index, 0.5));
2021-04-20 01:41:07
“2.将您的内存帧缓冲区构建大 4 倍。” 实际上,片段着色器可以索引 4 个颜色分量 r、g、b、&a,因此您可以将纹理打包得更紧……但我会在第一次通过时使用基本。:)
2021-05-07 01:41:07