Canvas/WebGL 2D tilemap网格工件 [英] Canvas/WebGL 2D tilemap grid artifacts

查看:94
本文介绍了Canvas/WebGL 2D tilemap网格工件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在创建一个简单的2D网络游戏,该游戏可以与您的典型地砖地图和精灵一起使用.

I am creating a simple 2D web game that works with your typical tile map and sprites.

我想说的是,我想要平滑的摄像机控制,包括平移和缩放(缩放).

The twist is that I want smooth camera controls, both translation and scaling (zooming).

我尝试同时使用Canvas 2D API和WebGL,在这两种方法中我都无法避免出现网格线伪影,同时还支持正确缩放.

I tried using both the Canvas 2D API, and WebGL, and in both I simply cannot avoid the bleeding grid line artifacts, while also supporting zooming properly.

如果有关系,我所有的图块都为1号,并缩放到所需的任何大小,它们的所有坐标均为整数,并且我使用的是纹理图集.

If it matters, all of my tiles are of size 1, and scaled to whatever size is needed, all of their coordinates are integers, and I am using a texture atlas.

这是使用我的WebGL代码的示例图片,其中不需要细的红/白线.

Here's an example picture using my WebGL code, where the thin red/white lines are not wanted.

我记得几年前用桌面GL编写精灵图块地图,具有讽刺意味的是,它使用类似的代码(或多或少相当于我对WebGL 2所做的工作),而且从来没有任何这些问题.

I remember writing sprite tile maps years ago with desktop GL, ironically using similar code (more or less equivalent to what I could do with WebGL 2), and it never had any of these issues.

我正在考虑接下来尝试基于DOM的元素,但是我担心它不会感觉或看起来很光滑.

I am considering to try DOM based elements next, but I fear it will not feel or look smooth.

推荐答案

一种解决方案是在片段着色器中绘制图块

One solution is to draw the tiles in the fragment shader

因此,您有了自己的地图,说一个 Uint32Array .将其分解为每个4字节的单位.前2个字节是图块ID,最后一个字节是标志

So you have your map, say a Uint32Array. Break it down into units of 4 bytes each. First 2 bytes are the tile ID, last byte is flags

当您遍历四边形时,会在图块纹理中查找的每个图块都位于该图块中,然后使用该值计算UV坐标,以从该图块纹理中获取该图块中的像素.如果您的瓷砖纹理设置了gl.NEAREST采样设置,那么您将永远不会流血

As you walk across the quad for each pixel you lookup in the tilemap texture which tile it is, then you use that to compute UV coordinates to get pixels from that tile out of the texture of tiles. If your texture of tiles has gl.NEAREST sampling set then you'll never get any bleeding

请注意,与传统的tilemap不同,每个tile的id是tile纹理中该tile的X,Y坐标.换句话说,如果您的图块纹理具有16x8的图块,并且您希望地图将图块显示为7上方和4向下,则该图块的ID为7,4(第一个字节7,第二个字节4),与传统CPU中的ID相同基于系统的图块ID可能是4 * 16 + 7或71(第71个图块).您可以向着色器添加代码以执行更多传统索引,但是由于着色器必须将id转换为2d纹理坐标,因此使用2d id似乎更容易.

Note that unlike traditional tilemaps the ids of each tile is the X,Y coordinate of the tile in the tile texture. In other words if your tile texture has 16x8 tiles across and you want your map to show the tile 7 over and 4 down then the id of that tile is 7,4 (first byte 7, second byte 4) where as in a traditional CPU based system the tile id would probably be 4*16+7 or 71 (the 71st tile). You could add code to the shader to do more traditional indexing but since the shader has to convert the id into 2d texture coords it just seemed easier to use 2d ids.

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>

这篇关于Canvas/WebGL 2D tilemap网格工件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆