html5-canvas - Canvas/WebGL 2D tilemap 网格工件
问题描述
我正在创建一个简单的 2D 网页游戏,它适用于您的典型瓷砖地图和精灵。
扭曲是我想要平滑的相机控制,包括平移和缩放(缩放)。
我尝试使用 Canvas 2D API 和 WebGL,在两者中我都无法避免流血的网格线伪影,同时还支持正确缩放。
如果重要的话,我所有的图块的大小都是 1,并缩放到需要的任何大小,它们的所有坐标都是整数,我使用的是纹理图集。
这是使用我的 WebGL 代码的示例图片,其中不需要细红/白线。
我记得几年前使用桌面 GL 编写精灵图块地图,具有讽刺意味的是使用类似的代码(或多或少等同于我可以使用 WebGL 2 所做的),并且它从未遇到任何这些问题。
我正在考虑下一步尝试基于 DOM 的元素,但我担心它不会感觉或看起来不流畅。
解决方案
一种解决方案是在片段着色器中绘制图块
所以你有你的地图,比如说Uint32Array
。将其分解为每个 4 个字节的单元。前 2 个字节是图块 ID,最后一个字节是标志
当您在 tilemap 纹理中查找每个像素的四边形时,您可以使用它来计算 UV 坐标,以从该瓦片的纹理中获取像素。如果您的瓷砖纹理具有 gl.NEAREST 采样集,那么您将永远不会流血
请注意,与传统的瓦片地图不同,每个瓦片的 id 是瓦片纹理中瓦片的 X、Y 坐标。换句话说,如果您的瓦片纹理有 16x8 瓦片,并且您希望地图显示瓦片 7 和 4 瓦片,那么该瓦片的 id 为 7,4(第一个字节 7,第二个字节 4),与传统 CPU 一样基于系统的磁贴 id 可能是 4*16+7 或 71(第 71 个磁贴)。您可以向着色器添加代码以执行更传统的索引,但由于着色器必须将 id 转换为 2d 纹理坐标,因此使用 2d id 似乎更容易。
const vs = `
attribute vec4 position;
//attribute vec4 texcoord; - since position is a unit square just use it for texcoords
uniform mat4 u_matrix;
uniform mat4 u_texMatrix;
varying vec2 v_texcoord;
void main() {
gl_Position = u_matrix * position;
// v_texcoord = (u_texMatrix * texccord).xy;
v_texcoord = (u_texMatrix * position).xy;
}
`;
const fs = `
precision highp float;
uniform sampler2D u_tilemap;
uniform sampler2D u_tiles;
uniform vec2 u_tilemapSize;
uniform vec2 u_tilesetSize;
varying vec2 v_texcoord;
void main() {
vec2 tilemapCoord = floor(v_texcoord);
vec2 texcoord = fract(v_texcoord);
vec2 tileFoo = fract((tilemapCoord + vec2(0.5, 0.5)) / u_tilemapSize);
vec4 tile = floor(texture2D(u_tilemap, tileFoo) * 256.0);
float flags = tile.w;
float xflip = step(128.0, flags);
flags = flags - xflip * 128.0;
float yflip = step(64.0, flags);
flags = flags - yflip * 64.0;
float xySwap = step(32.0, flags);
if (xflip > 0.0) {
texcoord = vec2(1.0 - texcoord.x, texcoord.y);
}
if (yflip > 0.0) {
texcoord = vec2(texcoord.x, 1.0 - texcoord.y);
}
if (xySwap > 0.0) {
texcoord = texcoord.yx;
}
vec2 tileCoord = (tile.xy + texcoord) / u_tilesetSize;
vec4 color = texture2D(u_tiles, tileCoord);
if (color.a <= 0.1) {
discard;
}
gl_FragColor = color;
}
`;
const tileWidth = 32;
const tileHeight = 32;
const tilesAcross = 8;
const tilesDown = 4;
const m4 = twgl.m4;
const gl = document.querySelector('#c').getContext('webgl');
// compile shaders, link, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// gl.createBuffer, bindBuffer, bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
],
},
});
function r(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return min + (max - min) * Math.random();
}
// make some tiles
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = tileWidth * tilesAcross;
ctx.canvas.height = tileHeight * tilesDown;
ctx.font = "bold 24px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const f = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~';
for (let y = 0; y < tilesDown; ++y) {
for (let x = 0; x < tilesAcross; ++x) {
const color = `hsl(${r(360) | 0},${r(50,100)}%,50%)`;
ctx.fillStyle = color;
const tx = x * tileWidth;
const ty = y * tileHeight;
ctx.fillRect(tx, ty, tileWidth, tileHeight);
ctx.fillStyle = "#FFF";
ctx.fillText(f.substr(y * 8 + x, 1), tx + tileWidth * .5, ty + tileHeight * .5);
}
}
document.body.appendChild(ctx.canvas);
const tileTexture = twgl.createTexture(gl, {
src: ctx.canvas,
minMag: gl.NEAREST,
});
// make a tilemap
const mapWidth = 400;
const mapHeight = 300;
const tilemap = new Uint32Array(mapWidth * mapHeight);
const tilemapU8 = new Uint8Array(tilemap.buffer);
const totalTiles = tilesAcross * tilesDown;
for (let i = 0; i < tilemap.length; ++i) {
const off = i * 4;
// mostly tile 9
const tileId = r(10) < 1
? (r(totalTiles) | 0)
: 9;
tilemapU8[off + 0] = tileId % tilesAcross;
tilemapU8[off + 1] = tileId / tilesAcross | 0;
const xFlip = r(2) | 0;
const yFlip = r(2) | 0;
const xySwap = r(2) | 0;
tilemapU8[off + 3] =
(xFlip ? 128 : 0) |
(yFlip ? 64 : 0) |
(xySwap ? 32 : 0) ;
}
const mapTexture = twgl.createTexture(gl, {
src: tilemapU8,
width: mapWidth,
minMag: gl.NEAREST,
});
function ease(t) {
return Math.cos(t) * .5 + .5;
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function easeLerp(a, b, t) {
return lerp(a, b, ease(t));
}
function render(time) {
time *= 0.001; // convert to seconds;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0, 1, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const mat = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
m4.scale(mat, [gl.canvas.width, gl.canvas.height, 1], mat);
const scaleX = easeLerp(.5, 2, time * 1.1);
const scaleY = easeLerp(.5, 2, time * 1.1);
const dispScaleX = 1;
const dispScaleY = 1;
// origin of scale/rotation
const originX = gl.canvas.width * .5;
const originY = gl.canvas.height * .5;
// scroll position in pixels
const scrollX = time % (mapWidth * tileWidth );
const scrollY = time % (mapHeight * tileHeight);
const rotation = time;
const tmat = m4.identity();
m4.translate(tmat, [scrollX, scrollY, 0], tmat);
m4.rotateZ(tmat, rotation, tmat);
m4.scale(tmat, [
gl.canvas.width / tileWidth / scaleX * (dispScaleX),
gl.canvas.height / tileHeight / scaleY * (dispScaleY),
1,
], tmat);
m4.translate(tmat, [
-originX / gl.canvas.width,
-originY / gl.canvas.height,
0,
], tmat);
twgl.setUniforms(programInfo, {
u_matrix: mat,
u_texMatrix: tmat,
u_tilemap: mapTexture,
u_tiles: tileTexture,
u_tilemapSize: [mapWidth, mapHeight],
u_tilesetSize: [tilesAcross, tilesDown],
});
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas { border: 1px solid black; }
<canvas id="c"></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
推荐阅读
- sql - 带有 where 条件的 PL/SQL 更新查询作为带有一些空值的选择查询
- python - 使用 XGB 泊松回归预测计数数据
- angular - 了解顶层
和子模块 - django - 在发送 API 之前解码 JSON 数据
- ios - 如何在 Jenkins groovy 的 ssh 命令中 escpae 美元符号?
- tightvnc - 如何在嵌入 TightVNC 的 TightVNC 中退出全屏模式
- android - 使用jdbc驱动程序的android app sql server 2012连接
- java - 如何在 sdkman 中列出所有可用的 java 版本?
- html - 为什么从视图传递到 Django 模板时,空格后的列表元素会消失?
- rust - rust-analyzer 中的自定义 Cargo 命令“install-ra”从何而来?