개발/OpenGL / / 2022. 3. 13. 13:57

OpenGL 공부일지 - OpenGL Super Bible 텍스처 - 3

반응형

Controlling How Texture Data Is Read

opengl 텍스처에서 데이터를 읽어서 쉐이더에 전달하는 방식이 유연하다. 텍스처 좌표는 보통 정규화되어 0.0에서 1.0사이가 되는데, opengl에서 범위를 벗어나는 텍스처 좌표에 대해 처리방법을 제어할 수 있다. 이를 래핑 모드라고 한다.

샘플의 중간값 계산방법을 정하는 법도 있는데 이를 필터링 모드라고 한다. 래핑 또는 필터링 모드를 제어하는 인자들은 샘플러 객체에 저장된다.

void glCreateSamplers(GLsizei n, GLuint * samplers);

위 함수는 샘플러 객체를 생성한다.

void glSamplerParameteri(GLuint sampler,
    GLenum pname,
    GLint param);

void glSamplerParameterf(GLuint sampler,
    GLenum pname,
    GLfloat param);

위 함수들로 샘플러 객체의 인자를 설정할 수 있다. 둘 다 타깃에 바인딩하지 않고 샘플러 객체를 수정할 수 있으며, 사용을 위해 바인딩 할 때는 텍스처 유닛에 바인딩 한다.

void glBindSampler(GLuint unit, GLuint sampler);

위 함수는 샘플러 객체를 텍스처 유닛에 바인딩한다.

텍스터 유닛에 바인딩된 샘플러 객체와 텍스처 객체는 쉐이더에서 사용할 텍셀에 관한 인자와 데이터를 구성하기 위해 함께 사용한다. 

텍스처 데이터에서 텍스처 샘플러 인자를 분리하면 다음과 같은 기능을 제공한다.

  • 텍스처 개수가 많아도 각 텍스처마다 인자 지정없이 동일한 샘플링 인자를 사용 가능하다.
  • 텍스처 유닛에 바인딩된 텍스처 변경 시, 샘플러 인자 변경이 필요없다.
  • 동일한 텍스처를 다른 샘플러 인자를 사용해서 동시에 읽을 수 있다.

온전히 이해하기에는 시간이 걸리겠지만, 위와 같은 기능들이 있다.

실제로는 자신만의 샘플러 객체를 사용하지만, 텍스처 각각은 샘플러 객체가 해당 텍스처 유닛에 바인딩되지 않는 경우에 대비하여 텍스처에서 사용할 내장 샘플러 객체를 포함한다. 이 개념을 텍스처의 기본 샘플링 인자라고 한다.

void glTextureParameterf(GLuint texture,
    GLenum pname,
    GLfloat param);

or

void glTextureParameteri(GLuint texture,
    GLenum pname,
    GLint param);

위 함수들은 텍스처 객체 안에 샘플러 객체를 사용하기 위해 사용한다. 타깃에 바인딩 한 후 호출해야한다.

Using Multiple Textures

하나의 쉐이더에서 여러 텍스처를 사용하려면 여러 샘플러 유니폼을 만들어서 각기 다른 텍스처 유닛을 참조하게 설정하면 가능하다. 텍스처들은 한꺼번에 콘텍스트에 바인딩할 것을 주의한다. 그래서 opengl은 다중 텍스처 유닛을 제공한다.

GLint units;
glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &units);

위 함수를 이용해서 지원되는 유닛 개수를 얻을 수 있다. 즉 모든 쉐이더 스테이지에서 사용 가능한 최대 동시 텍스처 유닛 개수를 얻을 수 있다.

glActiveTexture(GL_TEXTURE0 + 5);

위 함수를 통해 유닛 선택자를 변경함으로써 특정 텍스처 유닛에 바인딩할 수 있다. 

GLuint textures[3];
// Create three 2D textures
glCreateTextures(3, GL_TEXTURE_2D, &textures);
// Bind the three textures to the first three texture units
glBindTextureUnit(0, textures[0]);
glBindTextureUnit(1, textures[1]);
glBindTextureUnit(2, textures[2]);

위 코드는 세 텍스처를 바인딩하는 과정이다.

바인딩 후 쉐이더 안에서 샘플러 유니폼이 각 텍스처 유닛을 참조하도록 해야한다. 

두 가지 방법이 있는데,

첫 번째는 glUniform1i 로 샘플러 유니폼의 값을 설정하거나, 쉐이더 코드에서 glGetUniformLocation으로 위치를 찾아 수정하는 방법이 있다.

void glBindTextureUnit(GLuint unit, GLuint texture);

6판의 glBindTexture의 역할이 7판에서는 위 함수로 바뀌었다.

 

layout (binding = 0) uniform sampler2D foo;
layout (binding = 1) uniform sampler2D bar;
layout (binding = 2) uniform sampler2D baz;

두 번째는 binding 레이아웃 지시어를 사용해서, 쉐이더 컴파일 시점에 값을 설정할 수 있다.

 

Texture Filtering

텍스처 맵의 텍셀과 화면의 픽셀이 일대일 상관관계가 성립되는 경우는 거의 없다. 신경써서 텍스처링을 해야 가능하다.

텍스처 이미지는 항상 늘어나거나 줄어들어 지오메트리 서피스에 적용된다. 지오메트리의 방향에 따라 객체의 서비스는 늘어나고 줄어들고 한다.

texelFetch()라는 함수로 텍스처에서 특정 정수 좌표를 사용해서 텍셀을 읽어올 수 있다. 근데 프래그먼트 : 텍셀 비율이 정수가 아니면 잘 쓰지 않는다. 더 융통성있는 texture() 함수가 있다.

vec4 texture(sampler1D s, float P);
vec4 texture(sampler2D s, vec2 P);
ivec4 texture(isampler2D s, vec2 P);
uvec4 texture(usampler3D s, vec3 P);

위처럼 여러 가지 overriding이 있다. texelFetch와 다른 점이 있다면, 부동소수점 텍스처 좌표를 인자로 받는다는 것이다. 이것이 어떤 장점을 야기하는지는 나중에 알 수 있을 것이다.

 

늘어나거나 줄어든 텍스처 맵에서 컬러 프래그먼트를 계산하는 작업은 텍스처 필터링이다. 

텍스처를 늘리면 확대, 줄이면 축소. 샘플러 인자 함수로 opengl에서 이 두 작업들이 가능하다.

함수에서 필터를 선택하는 과정에서는, 최단 인접 필터링과 선형 필터링 중 하나를 선택해야한다. 

 

 최단 인접 필터링은 단순하고 빠르다. 텍스처 좌표가 텍스처 텍셀에 대해 계산되고, 좌표가 해당하는 텍셀에 따라 컬러 가 프래그먼트 텍스처 컬러에 적용된다.

glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

최단 인접 필터링은 위 코드처럼 확대 축소 필터 모두에 사용 가능하다.

 

 선형 필터링은 최단 인접보다 많은 작업이 필요하나, 그럴만한 이유가 있다. 최근의 하드웨어는 선형 필터링의 추가비용이 없다. 선형 필터링은 텍스처 좌표에 가장 가까운 텍셀을 구하면서 텍스처 좌표 주변 텍셀의 가중치 평균(선형 보간)을 계산한다.

 텍스처가 늘어날 때 흐려지는 효과가 보여진다. 이 효과는 모자이크처럼 사각형 조각이 보이는데 최단 인접 필터링의 경우 그렇지만, 선형 필터링은 그렇지 않다.

glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

위 함수들은 선형 필터링 함수들이다.

 

Mipmaps

밉맵은 렌더링 성능과 비주얼 퀄리티를 모두 향상시킬 수 있는 강력한 텍스처 기법이다. 표준 텍스처 매핑에는 문제가 두 가지 유명한 것이 있는데, 첫 번째는 반짝거리는 scintillation 현상이다. 이게 뭐냐면 텍스처의 크기보다 너무 작은 크기로 화면에 렌더링되는 객체 서피스의 경우에 생긴다. 즉 텍스처 맵의 샘플링 영역이 화면 크기에 비례해서 변하지 않는 경우 생긴다. 카메라나 객체가 움직일 때 주로 생긴다.

 두 번째는 scintillation과 동일한 환경에서 텍스처를 담으려 너무 많은 메모리가 사용되는 경우, 화면상의 인접 프래그먼트가 실제 텍스처 공간상에서 멀리 떨어진 텍셀과 근접하는 경우가 생긴다. 텍스처 크기는 큰에 액세스를 드문 빈도로 하는 경우 이 텍스처링 성능이 낮아진다.

 

위 두 문제는 더 작은 텍스처 맵을 사용해서 해결 가능하다. 근데 작은 텍스처 맵을 사용하면 객체가 가까운 경우 크게 렌더링되지 못하고 늘어나서 뭉개지거나 블록 현상이 발생한다. 이때 비로소 밉맵으로 해결가능하다.

밉맵은 본래 라틴어에서 출발했는데, '작은 공간에 많은 것을'이라는 의미로 텍스처 객체 하나의 이미지만 로딩하는게 아니라 크기에 따라 여러 이미지를 사용하여 하나의 밉맵 텍스처를 로딩한다. opengl은 필터모드로 지오메트리에 가장 적합한 텍스처를 선택한다.

 

밉맵 텍스처는 여러 텍스처 이미지로 구성되고, 각 이미지는 이전 단계에 비해서 가로세로가 절반으로 픽셀 수는 1/4로 줄어드는 식이다. 한 픽셀이 1*1이 될 때까지 반복하는데, 만약 비율이 달라서 한쪽이 먼저 1이 되면, 다른 차원만 나뉜다. 2D 텍스처는 밉맵을 사용하는 것으로 메모리를 1/3 추가로 잡아먹는다.

 

glTexSubImage2D()는 밉맵 레벨을 로딩하는 함수이다. glTexStorage2D()로 텍스터를 할당하고, 해당 텍스처에 밉맵을 사용할 수 있다.

glTextureParameteri(texture, GL_TEXTURE_BASE_LEVEL, 0);
glTextureParameteri(texture, GL_TEXTURE_MAX_LEVEL, 4);

위와 같은 방법으로 기본레벨 혹은 최대레벨을 지정해서 밉맵 레벨 개수를 제한 가능하다.

Mipmap Filtering

위에서 본 최단 인접 필터링, 선형 필터링 외에 4개의 밉맵이 더 있다.

glTexStorage2D()로 맵 레벨을 로딩한다. 근데 밉맵을 사용하기 위해서는 밉맵 필터를 선택해야하는데, 위 표에서 가장 위의 NEAREST, LINEAR은 기본 텍스처 레벨만 수행되기 때문에, 밑의 4개인 밉맵 필터를 이용한다. 

이 필터 선택에 따라 적합한 애플리케이션과 성능 요구사항이 다르다. 대표적으로 GL_NEAREST_MIPMAP_NEAREST는 성능이 좋고 에일리어싱(반짝거리는 현상)이 적다. 무조건적인 것은 아니고 게임의 경우 GL_LINEAR_MIPMAP_NEAREST가 사용되는데, 다른 크기의 밉 레벨간 밉맵선택이 빠르다. 

 밉맵을 nearest로 선택하면 시각적 결함 가능성이 있다. 객체의 서피스에 밉 레벨간 전이 부분이 확인 되거나, 다른 레벨로 이동하는 부분에 일그러진 선이나 전이 구간이 보인다. 이러한 문제를 해결하기 위해 이미지 필터링을 잘 선택해야한다.

Generating Mip Levels

2d 텍스터 밉맵은 기본 텍스처 이미지보다 1/3 추가로 메모리가 필요하다고 한 적 있다. 더 작은 버전들이 필요할 수 있고 미리 계산되었다면 좋겠지만, 일반적으로 opengl에게 생성요청을 보낸다. 

void glGenerateTextureMipmap(GLenum target);

레벨 0 텍스처를 로딩하였을 때, 위 함수를 사용하면 나머지 모든 밉 레벨을 생성한다.

opengl에 텍스처를 만들어주긴 하지만, 밉맵을 실시간으로 생성하는 것은 이미 생성된 것을 불러오는 것보다 느리다. 상황에 따라 적절하게 대응해야할 것이다.

Mipmaps in Action

#include <sb6.h>
#include <sb6ktx.h>
#include <vmath.h>

class tunnel_app : public sb6::application
{
    void init()
    {
        static const char title[] = "OpenGL SuperBible - Tunnel";

        sb6::application::init();

        memcpy(info.title, title, sizeof(title));
    }

    void startup()
    {
        GLuint  vs, fs;

        static const char * vs_source[] =
        {
            "#version 420 core                                                      \n"
            "                                                                       \n"
            "out VS_OUT                                                             \n"
            "{                                                                      \n"
            "    vec2 tc;                                                           \n"
            "} vs_out;                                                              \n"
            "                                                                       \n"
            "uniform mat4 mvp;                                                      \n"
            "uniform float offset;                                                  \n"
            "                                                                       \n"
            "void main(void)                                                        \n"
            "{                                                                      \n"
            "    const vec2[4] position = vec2[4](vec2(-0.5, -0.5),                 \n"
            "                                     vec2( 0.5, -0.5),                 \n"
            "                                     vec2(-0.5,  0.5),                 \n"
            "                                     vec2( 0.5,  0.5));                \n"
            "    vs_out.tc = (position[gl_VertexID].xy + vec2(offset, 0.5)) * vec2(30.0, 1.0);                  \n"
            "    gl_Position = mvp * vec4(position[gl_VertexID], 0.0, 1.0);       \n"
            "}                                                                      \n"
        };

        static const char * fs_source[] =
        {
            "#version 420 core                                                      \n"
            "                                                                       \n"
            "layout (location = 0) out vec4 color;                                  \n"
            "                                                                       \n"
            "in VS_OUT                                                              \n"
            "{                                                                      \n"
            "    vec2 tc;                                                           \n"
            "} fs_in;                                                               \n"
            "                                                                       \n"
            "layout (binding = 0) uniform sampler2D tex;                            \n"
            "                                                                       \n"
            "void main(void)                                                        \n"
            "{                                                                      \n"
            "    color = texture(tex, fs_in.tc);                                    \n"
            "}                                                                      \n"
        };

        char buffer[1024];

        vs = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vs, 1, vs_source, NULL);
        glCompileShader(vs);

        glGetShaderInfoLog(vs, 1024, NULL, buffer);

        fs = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fs, 1, fs_source, NULL);
        glCompileShader(fs);

        glGetShaderInfoLog(vs, 1024, NULL, buffer);

        render_prog = glCreateProgram();
        glAttachShader(render_prog, vs);
        glAttachShader(render_prog, fs);
        glLinkProgram(render_prog);

        glDeleteShader(vs);
        glDeleteShader(fs);

        glGetProgramInfoLog(render_prog, 1024, NULL, buffer);

        uniforms.mvp = glGetUniformLocation(render_prog, "mvp");
        uniforms.offset = glGetUniformLocation(render_prog, "offset");

        glGenVertexArrays(1, &render_vao);
        glBindVertexArray(render_vao);

        tex_wall = sb6::ktx::file::load("media/textures/brick.ktx");
        tex_ceiling = sb6::ktx::file::load("media/textures/ceiling.ktx");
        tex_floor = sb6::ktx::file::load("media/textures/floor.ktx");

        int i;
        GLuint textures[] = { tex_floor, tex_wall, tex_ceiling };

        for (i = 0; i < 3; i++)
        {
            glBindTexture(GL_TEXTURE_2D, textures[i]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        }

        glBindVertexArray(render_vao);
    }

    void render(double currentTime)
    {
        static const GLfloat black[] = { 0.0f, 0.0f, 0.0f, 0.0f };
        float t = (float)currentTime;

        glViewport(0, 0, info.windowWidth, info.windowHeight);
        glClearBufferfv(GL_COLOR, 0, black);

        glUseProgram(render_prog);

        vmath::mat4 proj_matrix = vmath::perspective(60.0f,
                                                     (float)info.windowWidth / (float)info.windowHeight,
                                                     0.1f, 100.0f);

        glUniform1f(uniforms.offset, t * 0.003f);

        int i;
        GLuint textures[] = { tex_wall, tex_floor, tex_wall, tex_ceiling };
        for (i = 0; i < 4; i++)
        {
            vmath::mat4 mv_matrix = vmath::rotate(90.0f * (float)i, vmath::vec3(0.0f, 0.0f, 1.0f)) *
                                    vmath::translate(-0.5f, 0.0f, -10.0f) *
                                    vmath::rotate(90.0f, 0.0f, 1.0f, 0.0f) *
                                    vmath::scale(30.0f, 1.0f, 1.0f);
            vmath::mat4 mvp = proj_matrix * mv_matrix;

            glUniformMatrix4fv(uniforms.mvp, 1, GL_FALSE, mvp);

            glBindTexture(GL_TEXTURE_2D, textures[i]);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        }
    }

protected:
    GLuint          render_prog;
    GLuint          render_vao;
    struct
    {
        GLint       mvp;
        GLint       offset;
    } uniforms;

    GLuint          tex_wall;
    GLuint          tex_ceiling;
    GLuint          tex_floor;
};

DECLARE_MAIN(tunnel_app)

텍스처를 로딩하고 터널을 렌더링할 때 각각을 전환하여 보여준다. 필터링된 이미지가 텍스처 데이터와 .ktx에 함께 저장되어있다. 

(github 소스코드에서 해당 예제는 tunnel project이다)

아쉽게도 .ktx 파일들을 찾을 수 없어 예제를 실행할 수 없다.

 

Texture Wrap

텍스처 좌표는 텍스처 맵 안 텍셀을 참조하기 위해 0.0~1.0 사이로 지정한다. 이 범위를 좌표가 벗어나면 opengl이 텍스처 래핑 모드에 따라 처리한다. glSamplerParameteri()함수로 래핑 모드를 지정할 수 있다. 

 GL_REPEAT 래핑 모드는 작은 타일 텍스처를 큰 지오메트리 서피스에 적용시 유용하다. 또한 객체 주의를 감싸 반대편까지 감싸는 텍스처에서 잘 동작한다. 다양한 옵션설정으로 텍스처 경계를 처리할 수 있어 원하는 텍셀을 얻을 수 있다.

#include <sb6.h>
#include <sb6ktx.h>

static const char * vs_source[] =
{
    "#version 410 core                                                              \n"
    "                                                                               \n"
    "uniform vec2 offset;                                                           \n"
    "                                                                               \n"
    "out vec2 tex_coord;                                                            \n"
    "                                                                               \n"
    "void main(void)                                                                \n"
    "{                                                                              \n"
    "    const vec4 vertices[] = vec4[](vec4(-0.45, -0.45, 0.5, 1.0),               \n"
    "                                   vec4( 0.45, -0.45, 0.5, 1.0),               \n"
    "                                   vec4(-0.45,  0.45, 0.5, 1.0),               \n"
    "                                   vec4( 0.45,  0.45, 0.5, 1.0));              \n"
    "                                                                               \n"
    "    gl_Position = vertices[gl_VertexID] + vec4(offset, 0.0, 0.0);              \n"
    "    tex_coord = vertices[gl_VertexID].xy * 3.0 + vec2(0.45 * 3);                    \n"
    "}                                                                              \n"
};

static const char * fs_source[] =
{
    "#version 410 core                                                              \n"
    "                                                                               \n"
    "uniform sampler2D s;                                                           \n"
    "                                                                               \n"
    "out vec4 color;                                                                \n"
    "                                                                               \n"
    "in vec2 tex_coord;                                                             \n"
    "                                                                               \n"
    "void main(void)                                                                \n"
    "{                                                                              \n"
    "    color = texture(s, tex_coord);                                             \n"
    "}                                                                              \n"
};

class wrapmodes_app : public sb6::application
{
public:
    void init()
    {
        static const char title[] = "OpenGL SuperBible - Texture Wrap Modes";

        sb6::application::init();

        memcpy(info.title, title, sizeof(title));
    }

    void startup(void)
    {
        // Generate a name for the texture
        glGenTextures(1, &texture);

        // Load texture from file
        sb6::ktx::file::load("media/textures/rightarrows.ktx", texture);

        // Now bind it to the context using the GL_TEXTURE_2D binding point
        glBindTexture(GL_TEXTURE_2D, texture);

        program = glCreateProgram();
        GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fs, 1, fs_source, NULL);
        glCompileShader(fs);

        GLuint vs = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vs, 1, vs_source, NULL);
        glCompileShader(vs);

        glAttachShader(program, vs);
        glAttachShader(program, fs);

        glLinkProgram(program);

        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
    }

    void shutdown(void)
    {
        glDeleteProgram(program);
        glDeleteVertexArrays(1, &vao);
        glDeleteTextures(1, &texture);
    }

    void render(double t)
    {
        static const GLfloat green[] = { 0.0f, 0.1f, 0.0f, 1.0f };
        static const GLfloat yellow[] = { 0.4f, 0.4f, 0.0f, 1.0f };
        glClearBufferfv(GL_COLOR, 0, green);

        static const GLenum wrapmodes[] = { GL_CLAMP_TO_EDGE, GL_REPEAT, GL_CLAMP_TO_BORDER, GL_MIRRORED_REPEAT };
        static const float offsets[] = { -0.5f, -0.5f,
                                          0.5f, -0.5f,
                                         -0.5f,  0.5f,
                                          0.5f,  0.5f };

        glUseProgram(program);
        glViewport(0, 0, info.windowWidth, info.windowHeight);

        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, yellow);

        for (int i = 0; i < 4; i++)
        {
            glUniform2fv(0, 1, &offsets[i * 2]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapmodes[i]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapmodes[i]);

            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        }
    }

private:
    GLuint      texture;
    GLuint      program;
    GLuint      vao;
};

DECLARE_MAIN(wrapmodes_app);

 

(github 소스코드에서 해당 예제는 wrapmodes project이다)

아쉽게도 .ktx파일이 없어 실행은 하지 못한다.

하지만 설명하자면, 여러 텍스터 래핑 모드를 적용한 것이다. 왼쪽 아래는 배경이 왼쪽 오른쪽으로 연장되도록 GL_CLAMP_TO_EDGE를 적용하였다.

 오른쪽 아래는 GL_REPEAT를 사용하여 텍스처가 반복하여 래핑된다.

 오른쪽 위는 GL_MIRRORED_REPEAT의 경우 반대방향으로 반복하여 래핑된다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유