webgl tilemap 渲染不正确的 UV 计算
webgl tilemap rendering incorrect UV calculation
我正在尝试将瓦片地图渲染到单个四边形上。
我的方法使用“瓦片贴图”纹理,其中每个像素存储瓦片集中瓦片的 X 和 Y 索引。
渲染片段时,思路是:
- 使用顶点纹理坐标对“瓦片贴图”纹理进行采样
- 从纹理的 R 和 G 通道检索 X 和 Y 索引
- 计算所选图块的 UV
- 使用 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
是 tileset 中一个 tile 的归一化大小
所以如果我有值 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)
。这些值对我来说似乎是正确的,为什么它们会产生错误的结果,我该如何解决?
这里有一些额外的上下文:
- 平铺贴图纹理(夸张的颜色):
- 预期结果(减去网格线):
- 实际结果:
代码将这两行中的 3 件事混为一谈
// 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 个图块。您需要从 0 到 1,跨越 1/8 和 1/4,而不是跨越 1/32
实际上它使用了 2 个矩阵。一个绘制四边形的四边形可以旋转、缩放、在 3D 中投影等,但我们只是说瓷砖地图的正常情况是只绘制一个覆盖 canvas.[=16= 的四边形]
第二个是纹理矩阵(或瓦片矩阵),每个单元为 1 个瓦片。因此,给定一个 0 到 1 的四边形,您计算一个矩阵以将该四边形扩展并旋转到上面的四边形。
假设您不旋转,您仍然需要决定在四边形上下绘制多少块。如果你想在四边形上有 4 个图块,在下面有 3 个图块,那么你可以将比例设置为 x=4 和 y=3。
这样,每个图块都会自动从 0 变为 1 space。或者换句话说,瓦片 2x7 从 U 中的 2.0<->3.0 到 V 中的 7.0<->8.0。然后我们可以从地图瓦片 2,7 中查找并使用 fract
覆盖它tile in the space that tile occurs in the quad.
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>
下次 post 一个有效的片段 对回答者更友好。
我正在尝试将瓦片地图渲染到单个四边形上。
我的方法使用“瓦片贴图”纹理,其中每个像素存储瓦片集中瓦片的 X 和 Y 索引。
渲染片段时,思路是:
- 使用顶点纹理坐标对“瓦片贴图”纹理进行采样
- 从纹理的 R 和 G 通道检索 X 和 Y 索引
- 计算所选图块的 UV
- 使用 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
是 tileset 中一个 tile 的归一化大小
所以如果我有值 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)
。这些值对我来说似乎是正确的,为什么它们会产生错误的结果,我该如何解决?
这里有一些额外的上下文:
- 平铺贴图纹理(夸张的颜色):
- 预期结果(减去网格线):
- 实际结果:
代码将这两行中的 3 件事混为一谈
// 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 个图块。您需要从 0 到 1,跨越 1/8 和 1/4,而不是跨越 1/32
实际上它使用了 2 个矩阵。一个绘制四边形的四边形可以旋转、缩放、在 3D 中投影等,但我们只是说瓷砖地图的正常情况是只绘制一个覆盖 canvas.[=16= 的四边形]
第二个是纹理矩阵(或瓦片矩阵),每个单元为 1 个瓦片。因此,给定一个 0 到 1 的四边形,您计算一个矩阵以将该四边形扩展并旋转到上面的四边形。
假设您不旋转,您仍然需要决定在四边形上下绘制多少块。如果你想在四边形上有 4 个图块,在下面有 3 个图块,那么你可以将比例设置为 x=4 和 y=3。
这样,每个图块都会自动从 0 变为 1 space。或者换句话说,瓦片 2x7 从 U 中的 2.0<->3.0 到 V 中的 7.0<->8.0。然后我们可以从地图瓦片 2,7 中查找并使用 fract
覆盖它tile in the space that tile occurs in the quad.
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>
下次 post 一个有效的片段 对回答者更友好。