【问题标题】:webgl tilemap rendering incorrect UV calculationwebgl tilemap渲染不正确的UV计算
【发布时间】:2020-11-27 03:52:31
【问题描述】:

我正在尝试将平铺地图渲染到单个四边形上。

我的方法使用“平铺贴图”纹理,其中每个像素存储平铺集内平铺的 X 和 Y 索引。

在渲染片段时,想法是:

  1. 使用顶点纹理坐标对“平铺贴图”纹理进行采样
  2. 从纹理的 R 和 G 通道检索 X 和 Y 索引
  3. 计算所选图块的 UV
  4. 使用 UV 对纹理图集进行采样

我在让#3 工作时遇到问题。

这是我试图用来渲染它的着色器代码:

顶点

#version 300 es
precision mediump float;

uniform mat4 uVIEW;
uniform mat4 uPROJECTION;

uniform mat3 uMODEL;

layout(location = 0) in vec2 aPOSITION;
layout(location = 1) in vec2 aTEXCOORD;

out vec2 vTEXCOORD;

void main()
{
    // flip uv and pass it to fragment shader
    vTEXCOORD = vec2(aTEXCOORD.x, 1.0f - aTEXCOORD.y);
    // transform vertex position
    vec3 transformed = uMODEL * vec3(aPOSITION, 1.0);
    gl_Position = uPROJECTION * uVIEW * vec4(transformed.xy, 0.0, 1.0);
}

片段

#version 300 es
precision mediump float;
precision mediump usampler2D;

uniform usampler2D uMAP;
uniform sampler2D uATLAS;
uniform vec2 uATLAS_SIZE;

in vec2 vTEXCOORD;

out vec4 oFRAG;

void main()
{
    // sample "tile map" texture
    vec4 data = vec4(texture(uMAP, vTEXCOORD));
    // calculate UV
    vec2 uv = (data.xy * 32.0 / uATLAS_SIZE) + (vTEXCOORD * 32.0 / uATLAS_SIZE);
    // sample the tileset
    oFRAG = texture(uATLAS, uv);
}

我相信这是罪魁祸首:

vec2 uv = (data.xy * 32.0 / uATLAS_SIZE) + (vTEXCOORD * 32.0 / uATLAS_SIZE);

这里的公式是uv = (tile_xy_indices * tile_size) + (texcoord * tile_size),其中:

  • texcoord是顶点uv(标准[0, 1], [0, 0], [1, 0], [1, 1])
  • tile_xy_indices是tileset中tile的X,Y坐标
  • tile_size 是瓦片集中一个瓦片的归一化大小

所以如果我有值texcoord = (0, 0)tile_xy_indices = (7, 7)tile_size = (32 / 1024, 32 / 1024),那么这个片段的 UV 应该是(0.21875, 0.21875),如果是texcoord = (1, 1),那么它应该是(0.25, 0.25)。这些值对我来说似乎是正确的,为什么它们会产生错误的结果,我该如何解决?

这里有一些额外的上下文:

  • 平铺贴图纹理(夸张的颜色):

  • 预期结果(减去网格线):

  • 实际结果:

【问题讨论】:

  • 仅供参考:没有理由通过uATLAS_SIZE。只需调用vec2 ATLAS_SIZE = vec2(textureSize(uATLAS, 0)) 即可获取纹理的大小
  • 另外仅供参考:this answer 中有一个正在工作的 tiler

标签: javascript webgl


【解决方案1】:

代码将这两行中的三件事混为一谈

// sample "tile map" texture
vec4 data = vec4(texture(uMAP, vTEXCOORD));
// calculate UV
vec2 uv = (data.xy * 32.0 / uATLAS_SIZE) + (vTEXCOORD * 32.0 / uATLAS_SIZE);
// sample the tileset

第一个,对于您正在绘制的整个四边形,覆盖您的整个瓷砖地图。这可能不是你想要的。通常使用 tilemap 的应用想要显示其中的一部分,而不是全部。

第二个问题是第二行需要知道一个图块将覆盖多少像素,而不是一个图块有多少像素。换句话说,如果您有一个 32x32 的图块并且您以 4x4 像素绘制它,那么您的纹理坐标需要在该图块中以 4 像素而不是 32 像素从 0.0 变为 1.0。

第三个问题是除以 32 不会穿过 32 像素的图块,除非纹理上有 32 个图块。想象一下,您有一个 32x32 像素的图块,但图块集中有 8x4 个图块。您需要在 1/8 和 1/4 之间从 0 到 1,而不是在 1/32 之间

this answer implements a tilemap

实际上它使用了 2 个矩阵。绘制四边形的方法是可以旋转、缩放、投影 3D 等,但我们可以说正常的瓷砖地图是只绘制一个覆盖画布的四边形。

第二个是纹理矩阵(或瓦片矩阵),其中每个单元是 1 个瓦片。因此,给定一个 0 到 1 的四边形,您可以计算一个矩阵以将该四边形展开并旋转到上面的四边形上。

假设您不旋转,您仍然需要决定在四边形上绘制多少块瓷砖。如果您希望四边形有 4 个图块并向下 3 个图块,那么您可以将比例设置为 x=4 和 y=3。

这样,每个图块会自动在自己的空间中从 0 变为 1。或者换一种说法,图块 2x7 从 U 中的 2.03.0 和 V 中的 7.08.0 开始。然后我们可以从地图图块 2,7 向上查找并使用 fract 覆盖该图块瓦片在四边形中占据的空间。

const vs = `#version 300 es
precision mediump float;

uniform mat4 uVIEW;
uniform mat4 uPROJECTION;
uniform mat4 uMODEL;

uniform mat4 uTEXMATRIX;

layout(location = 0) in vec4 aPOSITION;
layout(location = 1) in vec4 aTEXCOORD;

out vec2 vTEXCOORD;

void main()
{
    vTEXCOORD = (uTEXMATRIX * aTEXCOORD).xy;
    gl_Position = uPROJECTION * uVIEW * uMODEL * aPOSITION;
}
`;

const fs = `#version 300 es
precision mediump float;
precision mediump usampler2D;

uniform usampler2D uMAP;
uniform sampler2D uATLAS;
uniform vec2 uTILESET_SIZE; // how many tiles across and down the tileset

in vec2 vTEXCOORD;

out vec4 oFRAG;

void main()
{
    // the integer portion of vTEXCOORD is the tilemap coord
    ivec2 mapCoord = ivec2(vTEXCOORD);
    uvec4 data = texelFetch(uMAP, mapCoord, 0);
    
    // the fractional portion of vTEXCOORD is the UV across the tile
    vec2 texcoord = fract(vTEXCOORD);

    vec2 uv = (vec2(data.xy) + texcoord) / uTILESET_SIZE;
    
    // sample the tileset
    oFRAG = texture(uATLAS, uv);
}
`;

const tileWidth = 32;
const tileHeight = 32;
const tilesAcross = 8;
const tilesDown = 4;

const m4 = twgl.m4;
const gl = document.querySelector('canvas').getContext('webgl2');
if (!gl) alert('need WebGL2');

// compile shaders, link, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// gl.createBuffer, bindBuffer, bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
  aPOSITION: {
    numComponents: 2,
    data: [
      0, 0,
      1, 0,
      0, 1,
      
      0, 1,
      1, 0,
      1, 1,
    ],
  },
  aTEXCOORD: {
    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(4) < 1 
      ? (r(totalTiles) | 0)
      : 9;
  tilemapU8[off + 0] = tileId % tilesAcross;
  tilemapU8[off + 1] = tileId / tilesAcross | 0;
}

const mapTexture = twgl.createTexture(gl, {
  internalFormat: gl.RGBA8UI,
  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);  

  // these mats affects where the quad is drawn
  const projection = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
  const view = m4.identity();
  const model =
  m4.scaling([gl.canvas.width, gl.canvas.height, 1]);
 
  const tilesAcrossQuad = 10;//easeLerp(.5, 2, time * 1.1);
  const tilesDownQuad = 5;//easeLerp(.5, 2, time * 1.1);
  
  // scroll position in tiles
  // set this to 0,0 and the top left corner of the quad
  // will be the start of the map.
  const scrollX = time % mapWidth;
  const scrollY = 0;//time % (mapHeight * tileHeight);
  
  const tmat = m4.identity();
  // sets where in the map to look at in tile coordinates
  // so 3,4 means start drawing 3 tiles over, 4 tiles down
  m4.translate(tmat, [scrollX, scrollY, 0], tmat);
  // sets how many tiles to display
  m4.scale(tmat, [tilesAcrossQuad, tilesDownQuad, 1], tmat);

  twgl.setUniforms(programInfo, {
    uPROJECTION: projection,
    uVIEW: view,
    uMODEL: model,
    uTEXMATRIX: tmat,
    uMAP: mapTexture,
    uATLAS: tileTexture,
    uTILESET_SIZE: [tilesAcross, tilesDown],
  });
  
  gl.drawArrays(gl.TRIANGLES, 0, 6);
  
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>

下次发布一个有效的 sn-p 对回答者友好得多。

【讨论】:

  • for the entire quad you're drawing, goes over your entire tilemap. That's probably not what you want. - 这就是我想要的。我遇到了你的答案,但我无法将它应用到我正在做的事情上。
  • 我正在绘制的四边形缩放为[columns*tilesize, rows*tilesize]。我有一个图块集(纹理图集),其中包含独特的图块。然后是瓦片地图,它只是瓦片索引的列表,我想使用索引对瓦片集进行采样。我想一次渲染整个瓷砖地图,然后根据需要平移/缩放/旋转四边形。
  • 在OP中可以看到渲染的tilemap纹理。我想要做的就是对 tilemap 中的每个图块,在图块集中的 X 和 Y 处对图块进行采样。我在您链接的答案中看到,您正在对纹理坐标进行某种转换,但我不知道它做了什么。没有注释,变量名含糊其辞...
  • 我更新了答案。纹理矩阵决定要在四边形上显示瓷砖贴图的哪一部分。它位于平铺坐标中,并将标准的 0 到 1 纹理坐标相乘。因此,如果您为 uTEXMATRIX 传递身份,您将获得地图中的第一个图块,该图块绘制在整个四边形上。如果你传入一个 (x=4, y=3) 的缩放矩阵,你会得到 4 个方块和 3 个方块。如果传入 x=6,y=4 的平移,则四边形左上角的图块将是地图 6x4 的图块。
猜你喜欢
  • 2022-01-03
  • 1970-01-01
  • 2023-04-02
  • 1970-01-01
  • 2019-07-08
  • 2011-03-19
  • 1970-01-01
  • 2014-03-17
  • 2020-07-29
相关资源
最近更新 更多