3
\$\begingroup\$

I'm trying to render a tile map world on a single sprite mesh (instead of one sprite per tile.) This significantly reduces the array vertex bus labor for extremely large worlds:

enter image description here

In order to do this, you pass two textures to the shader: the sprite sheet itself, and a texture containing tile IDs.

Sprite map (I know some tiles repeat but ignore that, this could be any spritesheet):

enter image description here

Below: Entire tile world represented by a 128 x 128 texture map, where each RGB value holds actual Tile ID for that location on the map. There are more efficient ways to represent the actual tile ID. Here, for the sake of simplicity, it is stored in the R component.

enter image description here

Putting it all together:

enter image description here

The right-most square is the screen output. On the image I just repeat texture... but what should actually happen is a tile world rendered from tiles on the sprite sheet.

I wrote the shader that is supposed to do this, but I'm stuck. I can render the sprite sheet itself, and even scale it on the sprite mesh, but I can't seem to get the tile IDs render at the proper location, matching the tile world data.

I do think I am really close and there are just a couple of modification needed to the shader -- so what am I doing wrong?

The following shader is what I have so far:

tilemap.vs

#version 330 core

layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 texCoord;

out vec2 TexCoord;

uniform sampler2D ourTexture;
uniform sampler2D ourTilemapDataTexture;

uniform mat4 Model;
uniform mat4 View;
uniform mat4 Projection;


void main()
{
    gl_Position = Projection * View * Model * vec4(position, 1.0);

    TexCoord = texCoord;
}

tilemap.frag

#version 330 core

in vec2 TexCoord;

uniform sampler2D ourTexture;
uniform sampler2D ourTilemapDataTexture;

void main() {

    // size of the tile world (128 by 128 tiles on one sprite)
    float cols = 128.0f;
    float rows = 128.0f;

    float tilesPerSide = 8.0f;

    // figure out which tile we´re in from the 3D coordinates
    vec2 tile = vec2(floor(TexCoord.x)/cols, floor(TexCoord.y)/rows);

    // get tile´s texture ID from tile ID texture map
    float tex = texture2D(ourTilemapDataTexture, tile).r;

    vec2 UV = vec2(tex + fract(TexCoord.x), fract(TexCoord.y));

    color = texture2D(ourTexture, UV);

    if (color.a == 0) // remove transparent areas
        discard;
}

PS: I looked at every tutorial I could find on Google, and tried to re-implement it in my shader to no effect. This is as far as I got so far.

Can anyone point out what I'm doing wrong?

Update #1 -- I was able to get position sampling to work.

I painted a bit on the world map:

enter image description here

It's not perfect but definitely a step forward:

enter image description here

Adjustments made to the fragment shader:

float cols = 128.0f;
float rows = 128.0f;

float tex = texture2D(ourTilemapDataTexture, TexCoord).r;
float tey = texture2D(ourTilemapDataTexture, TexCoord).g;

vec2 UV = vec2( (TexCoord.x * (cols) / (tex * 32.0f)),
                (TexCoord.y * (rows) / (tey * 32.0f)) );

color = texture2D(ourTexture, UV);

Sure it's not 100% correct but at least map indexing works now :)

\$\endgroup\$
2
\$\begingroup\$

There is no need to have a secondary texture.

Instead, give your quad different texture coordinates.

For the sake of simplicity look at the following image:

enter image description here

Now, we know that our "atlas" texture has 2x2 tiles. So, we give our quad texture coordinates (0,0) to (0.5,0.5), which is, each coordinate axis is 1/number of tiles per direction.

Now when we want to draw a red tile, we send two uniforms (1,1) to the gpu program: The index of the vertical, and horizontal tile. This becomes our offset, and is computed in the vertex shader (normal vertex shader code omitted for brevity):

uniform vec2 offset;

void main()
{
    out.uv = uv += uv * offset; (member by member multiplication and addition)
}

Thus the uv minimum becomes (0.5,0.5) to (1, 1).

in the same way, to draw a yellow tile, your offset uniform would be (0,0), green would be (1,0), and black would be (0,1).

Because you are doing it in the vertex shader, the coordinates can be interpolated for each fragment using hardware, and the texture prefetch optimised for the fragment stage as normal.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Would stripping the texture coordinates from the data make the ID version lighter? The id map can be compressed quite a lot more than 8 floats. \$\endgroup\$ – Louis Garczynski Jan 17 at 16:02
  • \$\begingroup\$ I'm not sure what you mean by that. \$\endgroup\$ – Ian Young Jan 18 at 20:21
  • \$\begingroup\$ UV coordinates consist of 2 floats per vertex, or 8 floats per tile. This can be pretty heavy compared to an id texture which allows the shader to generate its own uvs. However it's only interesting if you actually manage to strip the uvs from the mesh, which I don't know if it is possible. \$\endgroup\$ – Louis Garczynski Jan 19 at 4:43
  • \$\begingroup\$ Well given that all tiles will use the same geometry, but with different id to modify uv coordinates within the fragment shader, stripping would seem unecessary. \$\endgroup\$ – Ian Young Jan 20 at 19:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.