2
votes

So to implement a tilemap using Threejs and Brandon Jone's tilemap method (found here) I am using a THREE.Plane geometry for each layer, and painting the face with the following custom shaders:

Vertex Shader:

var tilemapVS = [
    "varying vec2 pixelCoord;",
    "varying vec2 texCoord;",

    "uniform vec2 mapSize;",
    "uniform vec2 inverseTileTextureSize;",
    "uniform float inverseTileSize;",

    "void main(void) {",
    "    pixelCoord = (uv * mapSize);",
    "    texCoord = pixelCoord * inverseTileTextureSize * inverseTileSize;",
    "    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
    "}"
].join("\n");

Fragment Shader:

var tilemapFS = [
    "varying vec2 pixelCoord;",
    "varying vec2 texCoord;",

    "uniform sampler2D tiles;",
    "uniform sampler2D sprites;",

    "uniform vec2 inverseTileTextureSize;",
    "uniform vec2 inverseSpriteTextureSize;",
    "uniform float tileSize;",
    "uniform int repeatTiles;",

    "void main(void) {",
    "    vec4 tile = texture2D(tiles, texCoord);", //load this pixel of the tilemap
    "    if(tile.x == 1.0 && tile.y == 1.0) { discard; }", //discard if R is 255 and G is 255
    "    vec2 spriteOffset = floor(tile.xy * 256.0) * tileSize;", //generate the offset in the tileset this pixel represents
    "    vec2 spriteCoord = mod(pixelCoord, tileSize);",
    "    vec4 texture = texture2D(sprites, (spriteOffset + spriteCoord) * inverseSpriteTextureSize);",
    "    gl_FragColor = texture;",
    "}"
].join("\n");

Each texture is setup like:

//Setup Tilemap
this.tilemap.magFilter = THREE.NearestFilter;
this.tilemap.minFilter = THREE.NearestMipMapNearestFilter;
//tilemap.flipY = false;
if(this.repeat) {
    this.tilemap.wrapS = this.tilemap.wrapT = THREE.RepeatWrapping;
} else {
    this.tilemap.wrapS = this.tilemap.wrapT = THREE.ClampToEdgeWrapping;
}

//Setup Tileset
this.tileset.wrapS = this.tileset.wrapT = THREE.ClampToEdgeWrapping;
this.tileset.flipY = false;
if(this.filtered) {
    this.tileset.magFilter = THREE.LinearFilter;
    this.tileset.minFilter = THREE.LinearMipMapLinearFilter;
} else {
    this.tileset.magFilter = THREE.NearestFilter;
    this.tileset.minFilter = THREE.NearestMipMapNearestFilter;
}

And the uniforms are:

//setup shader uniforms
this._uniforms = {
    mapSize: { type: 'v2', value: new THREE.Vector2(this.tilemap.image.width * this.tileSize, this.tilemap.image.height * this.tileSize) },
    inverseSpriteTextureSize: { type: 'v2', value: new THREE.Vector2(1/this.tileset.image.width, 1/this.tileset.image.height) },
    tileSize: { type: 'f', value: this.tileSize },
    inverseTileSize: { type: 'f', value: 1/this.tileSize },

    tiles: { type: 't', value: this.tilemap },
    sprites: { type: 't', value: this.tileset },

    inverseTileTextureSize: { type: 'v2', value: new THREE.Vector2(1/this.tilemap.image.width, 1/this.tilemap.image.height) },
    repeatTiles: { type: 'i', value: this.repeat ? 1 : 0 }
};

And the actual geometry and mesh:

//create the shader material
this._material = new THREE.ShaderMaterial({
    uniforms: this._uniforms,
    vertexShader: tilemapVS,
    fragmentShader: tilemapFS,
    transparent: false
});

this._plane = new THREE.PlaneGeometry(
    this.tilemap.image.width * this.tileSize * this.tileScale,
    this.tilemap.image.height * this.tileSize * this.tileScale
);

this._mesh = new THREE.Mesh(this._plane, this._material);
this._mesh.z = this.zIndex;

this.tileSize is 16 and this.tileScale is 4. The problem I am having is that around the edges of the 16x16 tiles I get some tearing:

screen tearing

The strange part is that it doesn't happen all the time, only sparatically when moving along the y axis (however on my linux box the issue is much worse and effects the x axis as well).

It is almost like the 16x16 tiles are off by a small amount when placed with my vertex shader; but I am not sure what is causing it. Any help is appreciated, thanks!

Edit

Here is a better image of the tearing, it is more visible on grassy areas:

screen tearing 2

As you can see it is along the 16x16 tile edges (since they are scaled by 4).

2

2 Answers

4
votes

You are using a mipmapped texture and your texture coordinates are discontinous at the edges of the tiles:

"    vec2 spriteOffset = floor(tile.xy * 256.0) * tileSize;", //generate the offset in the tileset this pixel represents
"    vec2 spriteCoord = mod(pixelCoord, tileSize);",
"    vec4 texture = texture2D(sprites, (spriteOffset + spriteCoord) * inverseSpriteTextureSize);",

The floor and mod functions mean that the texture coordinates suddenly jump between two pixels (as you intend, to get a different tile).

However, the texture lookup uses the change in coordinates between pixels to determine what mipmap level to use, so at tile edges it jumps up to use much smaller mipmaps and there is a discontinuity as this is a different mipmap than the rest of the pixels.

The quickest way round this is to not use mipmaps e.g. one of:

texture.minFilter = THREE.LinearFilter;
texture.minFilter = THREE.NearestFilter;

Otherwise if you need mipmaps you'll need to pass the third parameter bias to texture2D. Set this to 0.0 generally and then when you are at the title edges set this to the negative number of the number of mipmaps you want to traverse back up to counteract this effect.

e.g. -11.0 will take you up 11 mipmaps from the lowest single pixel mipmap up to 0 the fullsized texture at 2048x2048. The exact value will need some calculation based on zoom image size textures size etc.

0
votes

I suspect that you are getting these issues due to the camera position causing errors with the sprite uv mapping. Basically, you are showing the neighboring sprites from the sprite sheet on the sprite that you are supposed to be rendering.

This line in particular: `vec2 spriteOffset = floor(tile.xy * 256.0) * tileSize;"

seems like it could be causing the offset issues. You could do a better check by padding out your sprites with a bright color, and seeing if that pops up.