简体   繁体   中英

How to map texture coordinates perfectly to a tile by using a tilemap?

I am unable to map a tile perfectly on a quad, by perfect i mean that it will render only the pixels from a specific tile in the texture image, and nothing more.

Edit: (updated code a bit)

I have now made example working code:

/*
Image of the bug: https://i.stack.imgur.com/Drb5U.png
edit: the bug seems to change on different gfx cards, but still visible one way or another!
*/
#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "Glu32.lib")

#include <windows.h>
#include <stdio.h>
#include <gl/glew.h>
#include <gl/gl.h>

HDC hDC = NULL;
HGLRC hRC = NULL;
HWND hWnd = NULL;
HINSTANCE hInstance;

bool active = 1;
int window_width = 640;
int window_height = 480;

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
    switch(uMsg){
        case WM_ACTIVATE: {
            active = !HIWORD(wParam);
            return 0;
        }
        case WM_CLOSE: {
            exit(0);
            return 0;
        }
        case WM_SIZE: {
            window_width = LOWORD(lParam);
            window_height = HIWORD(lParam);
            return 0;
        }
    }
    return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

BOOL create_window(int width, int height){
    GLuint PixelFormat;
    WNDCLASS wc;
    DWORD dwExStyle,dwStyle;
    RECT WindowRect;
    WindowRect.left = (long)0;
    WindowRect.right = (long)width;
    WindowRect.top = (long)0;
    WindowRect.bottom = (long)height;
    hInstance = GetModuleHandle(NULL);
    wc.style = CS_HREDRAW|CS_VREDRAW|CS_OWNDC;
    wc.lpfnWndProc = (WNDPROC)WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = NULL;
    wc.hCursor = NULL;
    wc.hbrBackground = NULL;
    wc.lpszMenuName = NULL;
    wc.lpszClassName = TEXT("test");
    if(!RegisterClass(&wc)) return FALSE;
    dwExStyle = WS_EX_APPWINDOW|WS_EX_WINDOWEDGE;
    dwStyle = WS_OVERLAPPEDWINDOW;
    ShowCursor(TRUE);
    AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);
    if(!(hWnd = CreateWindowEx(dwExStyle,TEXT("test"),TEXT("test"),dwStyle|WS_CLIPSIBLINGS|WS_CLIPCHILDREN,0,0,width,height,NULL,NULL,hInstance,NULL))) return FALSE;
    static PIXELFORMATDESCRIPTOR pfd = {sizeof(PIXELFORMATDESCRIPTOR),1,PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER,PFD_TYPE_RGBA,(BYTE)32,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,PFD_MAIN_PLANE,0,0,0,0};
    if(!(hDC = GetDC(hWnd))) return FALSE;
    if(!(PixelFormat = ChoosePixelFormat(hDC, &pfd))) return FALSE;
    if(!SetPixelFormat(hDC, PixelFormat, &pfd)) return FALSE;
    if(!(hRC = wglCreateContext(hDC))) return FALSE;
    if(!wglMakeCurrent(hDC, hRC)) return FALSE;
    ShowWindow(hWnd, SW_SHOW);
    SetForegroundWindow(hWnd);
    SetFocus(hWnd);
    return TRUE;
}



int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){
    if(!create_window(window_width,window_height)){
        return 1;
    }
    glShadeModel(GL_SMOOTH);
    glDisable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glClearColor(0,0,0,1);
    glColor4f(1,1,1,1);

    // create red/yellow checkers pattern with slightly randomized tile pixels:
    int img_w = 256;
    int img_h = 256;
    int tilesize = 16;
    unsigned int *data = (unsigned int *)malloc(img_w*img_h*4);
    unsigned int colors[] = {0xFF3333, 0xFFFF33};
    for(int y = 0; y < img_h; y+=tilesize){
        for(int x = 0; x < img_w; x+=tilesize){
            unsigned int i = ((x/tilesize)+((y/tilesize)%2))%2;
            for(int yy = 0; yy < tilesize; yy++){
                for(int xx = 0; xx < tilesize; xx++){
                    int r = ((colors[i]>>0)&255)-(rand()%30);
                    int g = ((colors[i]>>8)&255)-(rand()%30);
                    int b = ((colors[i]>>16)&255)-(rand()%30);
                    if(r < 0) r = 0;
                    if(g < 0) g = 0;
                    if(b < 0) b = 0;
                    data[(y+yy)*img_w+(x+xx)] = (b << 16) | (g << 8) | r;
                }
            }
        }
    }
    // take one tile somewhere from middle and make texcoords for it:
    int x = 2*tilesize;
    int y = 4*tilesize;
    float tx1 = (float)x/img_w;
    float ty1 = (float)y/img_h;
    float tx2 = (float)(x+tilesize)/img_w;
    float ty2 = (float)(y+tilesize)/img_h;

    unsigned int texid = 0;
    glGenTextures(1, &texid);
    glBindTexture(GL_TEXTURE_2D, texid);
    glEnable(GL_TEXTURE_2D);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img_w, img_h, 0, GL_BGRA, GL_UNSIGNED_BYTE, data);
    free(data);

    BOOL done = 0;
    static MSG msg;

    float zpos = 600.0f;

    while(!done){
        if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){
            if(msg.message == WM_QUIT){
                done = TRUE;
            }else{
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }else if(active){
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            glViewport(0, 0, window_width, window_height);
            glMatrixMode(GL_PROJECTION);
            glLoadIdentity();
            gluPerspective(45.0f, (GLdouble)window_width/(GLdouble)window_height, 0.1f, 5000.0f);
            glMatrixMode(GL_MODELVIEW);
            glLoadIdentity();

            glRotatef(0, 1,0,0);
            glRotatef(0, 0,1,0);
            glRotatef(0, 0,0,1);
            glTranslatef(0, 0, -zpos);
            zpos /= 1.02f;
            if(zpos < 0.2f) zpos = 0.2f;

            // draw something...
            glDisable(GL_DEPTH_TEST);
            glDisable(GL_BLEND);
            glEnable(GL_TEXTURE_2D);
            glBindTexture(GL_TEXTURE_2D, texid);
            glColor4f(1,1,1,1);
            glBegin(GL_QUADS);
                glTexCoord2f(tx1,ty1); glVertex2f(-10,-10);
                glTexCoord2f(tx2,ty1); glVertex2f(0,-10);
                glTexCoord2f(tx2,ty2); glVertex2f(0,0);
                glTexCoord2f(tx1,ty2); glVertex2f(-10,0);
            glEnd();
            glBegin(GL_QUADS);
                glTexCoord2f(tx1,ty1); glVertex2f(0,0);
                glTexCoord2f(tx2,ty1); glVertex2f(10,0);
                glTexCoord2f(tx2,ty2); glVertex2f(10,10);
                glTexCoord2f(tx1,ty2); glVertex2f(0,10);
            glEnd();

            Sleep(16); // dont run too fast or you break your legs!

            SwapBuffers(hDC);
        }
    }

    return 0;
}

And a code with multiple tiles (to see same bug when less zoom):

/*
Image of the bug: https://i.stack.imgur.com/Drb5U.png
edit: the bug seems to change on different gfx cards, but still visible one way or another!
*/
#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "Glu32.lib")

#include <windows.h>
#include <stdio.h>
#include <gl/glew.h>
#include <gl/gl.h>

HDC hDC = NULL;
HGLRC hRC = NULL;
HWND hWnd = NULL;
HINSTANCE hInstance;

bool active = 1;
int window_width = 1024;
int window_height = 768;

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
    switch(uMsg){
        case WM_ACTIVATE: {
            active = !HIWORD(wParam);
            return 0;
        }
        case WM_CLOSE: {
            exit(0);
            return 0;
        }
        case WM_SIZE: {
            window_width = LOWORD(lParam);
            window_height = HIWORD(lParam);
            return 0;
        }
    }
    return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

BOOL create_window(int width, int height){
    GLuint PixelFormat;
    WNDCLASS wc;
    DWORD dwExStyle,dwStyle;
    RECT WindowRect;
    WindowRect.left = (long)0;
    WindowRect.right = (long)width;
    WindowRect.top = (long)0;
    WindowRect.bottom = (long)height;
    hInstance = GetModuleHandle(NULL);
    wc.style = CS_HREDRAW|CS_VREDRAW|CS_OWNDC;
    wc.lpfnWndProc = (WNDPROC)WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = NULL;
    wc.hCursor = NULL;
    wc.hbrBackground = NULL;
    wc.lpszMenuName = NULL;
    wc.lpszClassName = TEXT("test");
    if(!RegisterClass(&wc)) return FALSE;
    dwExStyle = WS_EX_APPWINDOW|WS_EX_WINDOWEDGE;
    dwStyle = WS_OVERLAPPEDWINDOW;
    ShowCursor(TRUE);
    AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);
    if(!(hWnd = CreateWindowEx(dwExStyle,TEXT("test"),TEXT("test"),dwStyle|WS_CLIPSIBLINGS|WS_CLIPCHILDREN,0,0,width,height,NULL,NULL,hInstance,NULL))) return FALSE;
    static PIXELFORMATDESCRIPTOR pfd = {sizeof(PIXELFORMATDESCRIPTOR),1,PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER,PFD_TYPE_RGBA,(BYTE)32,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,PFD_MAIN_PLANE,0,0,0,0};
    if(!(hDC = GetDC(hWnd))) return FALSE;
    if(!(PixelFormat = ChoosePixelFormat(hDC, &pfd))) return FALSE;
    if(!SetPixelFormat(hDC, PixelFormat, &pfd)) return FALSE;
    if(!(hRC = wglCreateContext(hDC))) return FALSE;
    if(!wglMakeCurrent(hDC, hRC)) return FALSE;
    ShowWindow(hWnd, SW_SHOW);
    SetForegroundWindow(hWnd);
    SetFocus(hWnd);
    return TRUE;
}



int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){
    if(!create_window(window_width,window_height)){
        return 1;
    }
    glShadeModel(GL_SMOOTH);
    glDisable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glClearColor(0,0,0,1);
    glColor4f(1,1,1,1);

    // create red/yellow checkers pattern with slightly randomized tile pixels:
    int img_w = 256;
    int img_h = 256;
    int tilesize = 16;
    unsigned int *data = (unsigned int *)malloc(img_w*img_h*4);
    unsigned int colors[] = {0xFF3333, 0xFFFF33};
    for(int y = 0; y < img_h; y+=tilesize){
        for(int x = 0; x < img_w; x+=tilesize){
            unsigned int i = ((x/tilesize)+((y/tilesize)%2))%2;
            for(int yy = 0; yy < tilesize; yy++){
                for(int xx = 0; xx < tilesize; xx++){
                    int r = ((colors[i]>>0)&255)-(rand()%30);
                    int g = ((colors[i]>>8)&255)-(rand()%30);
                    int b = ((colors[i]>>16)&255)-(rand()%30);
                    if(r < 0) r = 0;
                    if(g < 0) g = 0;
                    if(b < 0) b = 0;
                    data[(y+yy)*img_w+(x+xx)] = (b << 16) | (g << 8) | r;
                }
            }
        }
    }

    unsigned int texid = 0;
    glGenTextures(1, &texid);
    glBindTexture(GL_TEXTURE_2D, texid);
    glEnable(GL_TEXTURE_2D);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img_w, img_h, 0, GL_BGRA, GL_UNSIGNED_BYTE, data);
    free(data);

    BOOL done = 0;
    static MSG msg;

    float zpos = 600.0f;

    while(!done){
        if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){
            if(msg.message == WM_QUIT){
                done = TRUE;
            }else{
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }else if(active){
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            glViewport(0, 0, window_width, window_height);
            glMatrixMode(GL_PROJECTION);
            glLoadIdentity();
            gluPerspective(45.0f, (GLdouble)window_width/(GLdouble)window_height, 0.05f, 5000.0f);
            glMatrixMode(GL_MODELVIEW);
            glLoadIdentity();

            glRotatef(0, 1,0,0);
            glRotatef(0, 0,1,0);
            glRotatef(0, 0,0,1);
            glTranslatef(0, 0, -zpos);
            zpos /= 1.02f;
            if(zpos < 0.1f) zpos = 0.1f;

            // draw map from randomly picked tiles:
            glDisable(GL_DEPTH_TEST);
            glDisable(GL_BLEND);
            glEnable(GL_TEXTURE_2D);
            glBindTexture(GL_TEXTURE_2D, texid);
            glColor4f(1,1,1,1);
            static GLuint my_list = 0;
            static bool list_made = 0;
            if(!list_made){
                int map_w = 100;
                int map_h = 100;
                int tiles_x = img_w/tilesize;
                int tiles_y = img_h/tilesize;
                my_list = glGenLists(1);
                glNewList(my_list, GL_COMPILE);
                glBegin(GL_QUADS);
                for(int y = -map_h; y < map_h; y++){
                    for(int x = -map_w; x < map_w; x++){
                        int xt = (rand()%tiles_x)*tilesize;
                        int yt = (rand()%tiles_y)*tilesize;
                        float tx1 = (float)xt/img_w;
                        float ty1 = (float)yt/img_h;
                        float tx2 = (float)(xt+tilesize)/img_w;
                        float ty2 = (float)(yt+tilesize)/img_h;
                        float x1 = (float)x;
                        float y1 = (float)y;
                        float x2 = (float)(x+1);
                        float y2 = (float)(y+1);
                        glTexCoord2f(tx1,ty1); glVertex2f(x1,y1);
                        glTexCoord2f(tx2,ty1); glVertex2f(x2,y1);
                        glTexCoord2f(tx2,ty2); glVertex2f(x2,y2);
                        glTexCoord2f(tx1,ty2); glVertex2f(x1,y2);
                    }
                }
                glEnd();
                glEndList();
                list_made = 1;
            }
            if(list_made){
                glCallList(my_list);
            }


            Sleep(16); // dont run too fast or you break your legs!

            SwapBuffers(hDC);
        }
    }

    return 0;
}

Here is also what this bug is about:

在此处输入图片说明

In the image above i have created a checkers pattern of red/yellow tiles.

I can fix this by reducing 0.0001f from the tx2 and ty2 texcoords, but it is not acceptable since the tiles wont be right sizes anymore, and it still doesn't fix the problem completely.

Note: the error appears only in one corner (top right in this case).

What I'm looking for is to render a tile and be 100% sure that nothing else will be rendered from that texture except the tile i want.

  • Is it really not possible, is this normal in opengl?
  • Could it be fixed by a shader perhaps?

Only way i know how to fix this is to pad the tiles edges with same color as the tile edges are. Or create 256 separate images of 16x16 size, which isn't acceptable.

Edit: I tested the code with my other computer: it has the same bug too, but different: it only shows the problem at TOP of the quad, and only at the top! and its also ONLY 1 pixel thick for whole time, (and visible from the beginning, while on my other computer its not). in my other computer the error is in top and right side, so i believe its a gfx card/driver problem... I don't have any more computers to test it on, and I believe the error is visible one way or another in any computer. if someone can test the code, and you don't see the error, please tell your gfx card model and maybe driver version.

This is based on the new example provided. Note that the previously stated information is still correct, because sub-pixel precision is a real issue (which is why I'm leaving it there). However, that's not the cause of the random lines that show up well before reaching those sub-pixel limits in the new case.

The lines at lower zoom levels are caused by simple imprecision in the texture coordinate interpolation. What you want to pass is a half-open range: [0/w, 16/w). But if the interpolator produces exactly 16/w, then it will fetch from the 16th texel.

The typical solution for this is to apply a small bias to the texture coordinate:

float tx2 = ((float)(xt+tilesize)/img_w) - (1.0f / 8192.0f);
float ty2 = ((float)(yt+tilesize)/img_h) - (1.0f / 8192.0f);

This will also get rid of the sub-pixel issue, because you're applying a bias to the texture coordinate.

It won't be "pixel perfect", but if it was pixel-perfect the first time, you wouldn't have seen the issue. Pixel-perfection is always relative when dealing with scaling and textures.

It will be pixel-perfect within the bias specified. Which is 1 out of 8192. Given your case of a 256x256, image, that's pixel-perfect within 1/32nd of a pixel. You would have to magnify a tile by 32x before you would be even one pixel off.

If you can tell the difference without taking it into an image editing or processing application, or otherwise by counting pixels, I'd be very surprised).

If you go for bigger images, you may want a smaller bias. Note that the effectiveness of the bias itself is absolute ; the smaller the bias, the greater the chance of seeing the problem again. For example, with a bias of 1/32768, my Radeon HD-3300 shows the issue. So 8192 is pretty close to the largest the denominator can get.

Note that padding the top-left of each tile would also work. Personally, I'd stick with the bias unless you have so many tiles that the bias is affecting the rendering of the image.

Note that shaders provide ways to resolve this issue without modifying the texture coordinate, by simply passing the texture coordinates as pixel-coordinate, and clamping them to just before the edge. This works with any texture resolution. Array textures would work by putting each tile in its own entry in the array.

So those are your options.


Note: I'm running an AMD HD-3300 card.

What appears to be happening when extremely close to the object is that the texture fetch unit seems to be running out of sub-pixel bits of precision. I can tell because if I change the threshold at which the zoom stops to the point where it is just on the cusp of happening, I can expand the window's size and then it appears. So it's the apparent screen size that's inducing the effect.

I was able to rule out texture coordinate interpolation precision issues by increasing the size of the texture to 4096. That changed nothing about when the issue appeared. So it must be in the texture fetch unit hardware itself, not the texture coordinate interpolation unit. That is, the interpolated texture coordinate is fine; it's when the hardware does the math to get the texel to fetch from that things go wrong.

After some experimentation, it looks like the issue appears when the texel is being mapped to a size of about 64-90 screen pixels. So it seems you get about 6 sub-pixel bits of precision before things get messy.

In the image above i have created a checkers pattern of red/yellow tiles.

Could not reproduce the problem on GTX460/WinXP, even with zpos = 0.02f.

Sounds like driver/precision/fixed function problem.

What im looking for is to render a tile and be 100% sure that nothing else will be rendered from that texture except the tile i want.

You could store every tile in separate texture (I know it is a waste) and use GL_CLAMP_TO_EDGE for GL_TEXTURE_WRAP_S / GL_TEXTURE_WRAP_T .

Or you could add border of pixels around every tile - "expand" tile by copying pixels on edges outwards. Ie above top edge of tile, place copy of top edge, to the left of the left edge, same to the right and bottom edges of the tile. This will waste extra texture memory (since width/height of every tile will increase by 2 pixels), but you'll never see adjacent pixel

Could it be fixed by a shader perhaps?

Perhaps. You could adjust texture coordinates in a fragment shader this way (non-GLSL pseudocode):

void adjustCoords(float &s, float &t, int tilex, int tiley){
    float sMin = (0.5f + (float)tilex)/(float)image.width,
        sMax = ((float)(tilex + tilewidth) - 0.5f)/(float)image.width,
        tMin = (0.5f + (float)tiley)/(float)image.height,
        tMax = ((float)(tiley + tileheight) - 0.5f)/(float)image.height;
    s = min(max(s, sMin), sMax);
    t = min(max(t, tMin), tMax);
}

before using them (texture coordinates) to actually read data from texture/sampler. However, you'll need to know tile coordinates and texture size. On other hand, nothing prevents you from encoding this data into vertex attributes/uniforms.

So, to bypass the problem, you can:

  1. Use more textures (wasting performance switching them).
  2. Waste texture memory.
  3. Waste GPU power adjusting texture cooridnates using shader.

Decision is yours.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM