개발/OpenGL / / 2022. 3. 14. 20:03

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

반응형

Array Textures

다른 텍스처 유닛을 사용하면 여러 텍스처를 동시에 접근 가능하다. 샘플러 유니폼을 여러 개 선언 시 쉐에더에서 동시에 여러 텍스처 객체에 접근할 수 있으므로 상당히 유용하다. 

배열 텍스처 기능을 사용하면 더한 것도 가능하다. 여러 가지의 1D, 2D, 큐브 맵 이미지를 단일 텍스처 객체에 로딩하는 것이다. 즉 한 텍스처에 하나 이상의 이미지를 갖는다.

 각 밉 레벨은 서로 다른 이미지이지만, 텍스처 배열에서 전체 텍스처 이미지 배열을 하나의 텍스처 객체에 바인딩하고 쉐이더에서 각각에 대한 인덱스를 사용 가능하다. 그러므로 애플리케이션에서 동시에 사용가능한 텍스처 개수가 늘어난다.

대부분 텍스처는 배열 타입이다. 하지만 3D 배열 텍스처는 opengl에서 지원하지 않는다. 큐브 맵 배열 텍스처는 밉맵을 가질 수 있고, 샘플러 유니폼의 배열을 생성하면, 그 배열에 대한 인덱스 사용값은 유니폼이어야 한다. 

텍스처 배열에서는 텍스처 맵에 대한 인덱스가 배열의 다른 요소로부터의 값도 가능하다.

텍스처 배열요소와 배열 텍스처 요소를 구분하기 위해 보통 레이어로 표현한다.

 2D와 3D텍스처는 배열 텍스처 레이어 간 필터링이 적용되지 않는다. 

Loading a 2D Array Texture

2D 배열생성은 새 텍스처 객체를 만들 후 GL_TEXTURE_2D_ARRAY 타깃에 바인딩하고, glTexStorage3D()를 사용해서 스토리지를 할당한 뒤 glTexSubImage3D()로 이미지를 로딩한다. 3D버전 함수를 사용하여 z좌표로 레이어의 역할을 한다.

GLuint tex;
glCreateTextures(GL_TEXTURE_2D_ARRAY1, &tex);
glTextureStorage3D(tex,
        8,
        GL_RGBA8,
        256,
        256,
        100);

for (int i = 0; i < 100; i++)
{
    glTextureSubImage3D(tex,
        0,
        0, 0,
        i,
        256, 256,
        1,
        GL_RGBA,
        GL_UNSIGNED_BYTE,
        image_data[i]);
}

위 코드는 2D 배열 텍스처 로딩 코드이다.

.KTX 파일 형식은 배열 텍스처를 지원하여 직접 읽을 수 있다. sb6::ktx::file::load를 이용한다.

 

#version 450 core
layout (location = 0) in int alien_index;

out VS_OUT
{
	flat int alien;
	vec2 tc;
} vs_out;

struct droplet_t
{
    float x_offset;
    float y_offset;
    float orientation;
    float unused;
};

layout (std140) uniform droplets
{
	droplet_t droplet[256];
};

void main(void)
{
    const vec2[4] position = vec2[4](vec2(-0.5, -0.5),
        vec2(0.5, -0.5),
        vec2(-0.5, 0.5),
        vec2(0.5, 0.5));
    vs_out.tc = position[gl_VertexID].xy + vec2(0.5);
    float co = cos(droplet[alien_index].orientation);
    float so = sin(droplet[alien_index].orientation);
    mat2 rot = mat2(vec2(co, so),
    	vec2(-so, co));
    vec2 pos = 0.25 * rot * position[gl_VertexID];
    gl_Position = vec4(pos.x + droplet[alien_index].x_offset,
    	pos.y + droplet[alien_index].y_offset,
    	0.5, 1.0);
}

위 코드는 alienrain이라는 프로젝트의 버텍스 쉐이더 코드이다.

버텍스 위치와 텍스처 좌표는 하드코딩되고, 회전 행렬 rot를 통해 인스턴스를 회전한다. 텍스처좌표는 프래그먼트 쉐이더에 vs_out.alien을 통해 전달된다.

#version 450 core
layout (location = 0) out vec4 color;

in VS_OUT
{
	flat int alien;
	vec2 tc;
} fs_in;

layout (binding = 0) uniform sampler2DArray tex_aliens;

void main(void)
{
	color = texture(tex_aliens, vec3(fs_in.tc, float(fs_in.alien)));
}

프래그먼트 쉐이더에서는 간단히 입력값을 사용해서 텍스처 샘플을 구하고 출력에 저장한다. 

Accessing Texture Arrays

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);
    
    glBindBufferBase(GL_UNIFORM_BUFFER, 0, rain_buffer);
            vmath::vec4 * droplet =
            (vmath::vec4 *)glMapBufferRange(
            GL_UNIFORM_BUFFER,
            0,
            256 * sizeof(vmath::vec4),
            GL_MAP_WRITE_BIT |
            GL_MAP_INVALIDATE_BUFFER_BIT);
            
    for (int i = 0; i < 256; i++)
    {
        droplet[i][0] = droplet_x_offset[i];
        droplet[i][1] = 2.0f - fmodf((t + float(i)) *
        droplet_fall_speed[i], 4.31f);
        droplet[i][2] = t * droplet_rot_speed[i];
        droplet[i][3] = 0.0f;
    }
    glUnmapBuffer(GL_UNIFORM_BUFFER);
    
    int alien_index;
    for (alien_index = 0; alien_index < 256; alien_index++)
    {
        glVertexAttribI1i(0, alien_index);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    }
}

alienrain 예제 코드의 렌더링 함수에는 간단한 루프 안에 드로잉 커맨드뿐이다. 매 프레임마다 rain_buffer 버퍼 객체의 데이터값을 갱신한다. 

버퍼 객체로 값을 저장하고, glDrawArrays()를 통해 alien들을 그린다. 

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

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

Writing to Textures in Shaders

텍스처 객체는 이미지의 집합으로 밉맵 체인이 포함되면 필터링과 텍스처 좌표 래핑 등을 지원한다. opengl은 읽기 뿐 아니라 쉐이더에서 텍스처를 읽거나 직접 쓸 수도 있다. 

 이미지 변수는 샘플러 유니폼처럼 선언한다. 이미지 변수의 타입은 opengl에서 다양하다.

uniform image2D my_image;

위표와 같은 이미지 변수들을 위 코드처럼 유니폼으로 선언해서 이미지 유닛과 연결시킨다.

vec4 imageLoad(readonly image2D image, ivec2 P);
void imageStore(image2D image, ivec2 P, vec4 data);

위 두함수는 각각 이미지를 읽고 쓰는 함수이다.

load함수로 이미지 데이터를 읽어 쉐이더에 전달하고, store함수로 전달한 값을 가져와 해당 위치의 이미지에 저장한다. 

 

ivec4 imageLoad(readonly iimage2D image, ivec2 P);
void imageStore(iimage2D image, ivec2 P, ivec4 data);
uvec4 imageLoad(readonly uimage2D image, ivec2 P);
void imageStore(uimage2D image, ivec2 P, uvec4 data);

이미지 변수로 부동소수점 데이터를 저장할 수 있어 ivec 형과 uvec이 따로 있다. 

void glBindImageTexture(GLuint unit,
    GLuint texture,
    GLint level,
    GLboolean layered,
    GLint layer,
    GLenum access,
    GLenum format);

로딩과 저장을 위해 위 함수를 사용하여 텍스처를 이미지유닛에 바인딩한다.

 

#version 450 core

// Uniform image variables:
// Input image - note use of format qualifier because of loads
layout (binding = 0, rgba32ui) readonly uniform uimage2D image_in;
// Output image
layout (binding = 1) uniform writeonly uimage2D image_out;

void main(void)
{
    // Use fragment coordinate as image coordinate
    ivec2 P = ivec2(gl_FragCoord.xy);
    
    // Read from input image
    uvec4 data = imageLoad(image_in, P);
    
    // Write inverted data to output image
    imageStore(image_out, P, ~data);
}

위 코드는 이미지 로딩 및 저장 기능으로 한 이미지에서 다른 이미지로 데이터를 모두 논리적 인버트 하는 프래그먼트 쉐이더 예제이다.

이미지 로딩 및 저장은 한 쉐이더에 여러 번 사용가능하고 좌표도 원하는 대로 설정할 수 있다. 

프래그먼트 쉐이더는 프레임버퍼의 지정된 위치에 저장하는게 아니라 이미지의 어느 곳에서나 쓸 수 있고, 여러 이미지 유니폼으로 이미지에 사용할 수 있다. 프래그먼트 쉐이더뿐 아니라 어떤 쉐이더 스테이지에도 이미지에 데이터를 쓸 수 있다. 여기서 단점은 어토믹이 없이는 쉐이더에 덮어써 사라질 수 있다는 것이다.

Atomic Operations on Images

어토믹 연산은 이미지 형태의 데이터에서도 수행 가능하다. 어토믹 연산은 읽기, 수정, 쓰기의 연속으로 결과를 얻기 위해 더 이상 나눌 수 없어야 한다. 또한 GLSL의 내장함수를 사용한다.

 

위 표와 같이 다양한 어토믹 연산 함수들이 있다. 

 

 어토믹 함수는 연산 수행 전 메모리상의 기존값을 리턴한다. 이러한 특성은 리스트에 데이터를 추가할 때 유용하다. 

또 어토믹 함수는 메모리상에서 연결 리스트와 같은 데이터 구조를 구성하는 경우 사용한다.

 쉐이더에서는 연결 리스트를 만들기 위해 세 개의 스토리지가 필요한데, 첫 번째는 리스트 아이템을 저장하고, 두 번째는 아이템 개수를 저장하고, 세 번째는 리스트의 마지막 아이템 인덱스인 '헤드 포인터'를 저장한다. 

즉 쉐이더 스토리지 버퍼로 연결 리스트 아이템을 저장하거나, 어토믹 카운터로 현재 아이템 카운트를 저장하고, 이미지를 사용해서 리스트에 대한 헤드 포인터를 저장할 수 있다. 

정리하면 다음과 같다.

  1. 어토믹 카운터 증가, atomicCounterIncrement가 return하는 이전 값 저장
  2. imageAtomicExchange를 사용하여 갱신된 카운터 값을 현재 헤드 포인터와 교환
  3. 데이터를 데이터 스토어에 저장. 각 요소에는 다음 인덱스가 포함됨. 여기에 저장한 헤드포인터의 이전 값 적용
#version 450 core
// Atomic counter for filled size
layout (binding = 0, offset = 0) uniform atomic_uint fill_counter;

// 2D image to store head pointers
layout (binding = 0) uniform uimage2D head_pointer;

// Shader storage buffer containing appended fragments
struct list_item
{
    vec4 color;
    float depth;
    int facing;
    uint next;
};

layout (binding = 0, std430) buffer list_item_block
{
	list_item item[];
};

// Input from vertex shader
in VS_OUT
{
	vec4 in;
} fs_in;

void main(void)
{
    ivec2 P = ivec2(gl_FragCoord.xy);
    
    uint index = atomicCounterIncrement(fill_counter);
    
    uint old_head = imageAtomicExchange(head_pointer, P, index);
    
    item[index].color = fs_in.color;
    item[index].depth = gl_FragCoord.z;
    item[index].facing = gl_FrontFacing ? 1 : 0;
    item[index].next = old_head;
}

위 쉐이더 코드는 헤드 포인터와 어토믹 카운터를 사용해서 쉐이더 스토리지 버퍼에 저장된 연결 리스트에 프래그먼트를 추가한다.

gl_FrontFacing 값은 프래그먼트 쉐이더에 대한 부울 입력값으로 컬링 스테이지로부터 생성되었다.

쉐이더 실행 전, 리스트 아이템 인덱스로 사용할 수 없는 값으로 헤드 포인터 이미지를 초기화한다. 그리고 어토믹 카운터는 0으로 리셋한다. 처음으로 추가된 아이템은 아이템 0이 되고, 그 값은 헤드 포인터에 저장된다. 

 

 헤드 포인터 이미지의 리셋값이 다음 인덱스에 저장되고, 다음 값이 리스트에 추가되면 인덱스 1에 위치하여, 헤드 포인터에 저장되고 이전 값(0)이 다음 인덱스에 저장된다. 이러한 방식으로 계속 진행된다. 

마지막으로 리스트에 추가된 아이템의 인덱스가 헤드 포인터 이미지의 결과이다.

 

#version 450 core
// 2D image to store head pointers
layout (binding = 0, r32ui) coherent uniform uimage2D head_pointer;

// Shader storage buffer containing appended fragments
struct list_item
{
    vec4 color;
    float depth;
    int facing;
    uint next;
};

layout (binding = 0, std430) buffer list_item_block
{
	list_item item[];
};

layout (location = 0) out vec4 color;

const uint max_fragments = 10;

void main(void)
{
    uint frag_count = 0;
    float depth_accum = 0.0;
    ivec2 P = ivec2(gl_FragCoord.xy);
    
    uint index = imageLoad(head_pointer, P).x;
    
    while (index != 0xFFFFFFFF && frag_count < max_fragments)
    {
        list_item this_item = item[index];
        
        if (this_item.facing != 0)
        {
        	depth_accum -= this_item.depth;
        }
        else
        {
        	depth_accum += this_item.depth;
        }
        
        index = this_item.next;
        frag_count++;
    }
    
    depth_accum *= 3000.0;
    color = vec4(depth_accum, depth_accum, depth_accum, 1.0);
}

리스트 순회를 위해 첫 번째 아이템의 인덱스를 헤드 포인터 이미지에서 로딩하여 쉐이더 스토리지 버퍼로부터 읽는다. 아이템마다 다음 인덱스만 계속 따라가다보면 끝에 다다른다. 위 코드의 경우 프래그먼트의 최대 개수까지만 순회하도록 한다. 

위 코드에서는 쉐이더가 연결 리스트를 순회하여 각 픽셀에 저장된 프래그먼트의 깊이에 대한 총합을 기록한다. 전면 방향 프리미티브의 깊이값은 총합에 더하고, 후면 방향 프리미티브의 깊이값은 총합에서 뺀다. 결과는 컨벡스 객체의 내부에 채워진 깊이의 총합이다. 이 값은 볼륨과 내부가 채워진 공간을 렌더링할 때 사용가능하다고 한다.

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

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

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