Canvas / WebGL 2D tilemap网格工件

时间:2018-11-24 22:00:19

标签: html5-canvas 2d webgl

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

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

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

如果有关系,我所有的图块都为1号,并按比例缩放至所需的大小,它们的所有坐标均为整数,并且我使用的是纹理图集。

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

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

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

1 个答案:

答案 0 :(得分:2)

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

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

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

请注意,与传统的tilemap不同,每个tile的id是tile纹理中tile的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>