Tistory View

3.0까지 texture에 직접 쓸 수 있는 방법이 없었다. FBO로 감싸서 그려야 했고, 그 과정은 코드로는 간단하지만, 실제 GPU에서는 상당한 작업이 들어가는 작업이었다. 이제 ComputeShader는 바로 읽기/쓰기를 지원한다. 하지만 여기에는 제약이 있는 데, 기존에 사용하던 방식이 아닌 다른 함수를 통해 텍스쳐를 만들어야 한다.

 

Immutable-storage에 텍스쳐만들기

말이 좀 해깔리지만(Immutable:바꿀 수 없는) 그냥 ComputeShader는 Immutable-storage의 텍스쳐만 쓸(write) 수 있다고 생각하면 된다. 이렇게 만들 수 있는 함수는 glTexStorage*()함수들이다.

 

텍스쳐 만드는 코드

GLuint texId = 0;
glGenTextures( 1, &texId );
glBindTexture( GL_TEXTURE_2D, texId );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

glTexStorage2D( GL_TEXTURE_2D, 1, GL_RGBA8, w, h );

//glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, 픽셀 데이터);

다른 것은 다 동일하고 단지 glTexImage2D대신 glTexStorage2D함수를 이용하고 있다.

 

void glTexStorage2D( GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height );

여기서 좀 주의 할 것은 levels는 만들어질 개수로 0이 아닌 1을 넣었다. 말 그대로 만들어 질 개수를 넣는 것이다, texture의 Lod Index를 넣는 것이 아니다. 이 것은 할당을 할 뿐 실제 이미지 데이터를 보내는 것은 glTexSubImage2D함수를 그대로 이용하여 필요한 위치에 이미지 데이터를 보낼 수 있다.

 

사용할 수 있는 internalformat은 [이 곳 glTexStoreage2D Manual]을 참고 하면 된다.

위 링크에 [Table 1. Sized Internal Formats]을 보면 Renderable과 Filterable에 값이 있다.

Renderable 이 "Y"가 아닌 경우 이 텍스쳐를 화면에 그려도 그려지지 않을 것이고, Filterable이 "Y"가 아니면 아무리 난리를 쳐도 부드러운 화면을 보지 못 할 것이다.(절때~ 필자가 이 것 때문에 고생했다는 것은 아니다... 아니라구 믿어~)

 

이제 예제로 설명을 계속 하겠다.

 

ColorMatrix using ComputeShader

필자가 이전에 포스트한 Android Java의 ColorMatrix가 있다. 이 것을 ComputeShader로 구현을 해 보도록 하겠다.

 

ColorMatrix Shader

#version 310 es
layout( local_size_x = 8, local_size_y = 8, local_size_z = 1 ) in;
precision highp float;
layout(binding = 0, rgba8 ) readonly  uniform highp restrict image2D uSrc;
layout(binding = 1, rgba8 ) writeonly uniform highp restrict image2D uDst;

layout( location = 0 ) uniform ivec2 dim;
layout( location = 2 ) uniform mat4 matColor;
layout( location = 3 ) uniform vec4 vecColor0;

void main() {
    ivec2 pos = ivec2( gl_WorkGroupID.xy * gl_WorkGroupSize.xy + gl_LocalInvocationID.xy );
    if( all( lessThan( pos, dim ) ) ) {
        imageStore( uDst, pos, matColor * imageLoad( uSrc, pos ) + vecColor0 );
    }
}

local_size를 8x8x1로 지정했다. 이 쓰레드가 텍스쳐내의 어떤 픽셀을 처리할 지 구분하여 pos값을 만들고, 해당 픽셀을 불러와 계산 후 다시 저장하는 단순한 shader다.

 

Dispatch 코드

// srcW, srcH : texture크기


// gray-scale ColorMatrix 
float mat[16] = { 0.2989f, 0.5870f, 0.1140f, 0.0F,
                  0.2989f, 0.5870f, 0.1140f, 0.0F,
                  0.2989f, 0.5870f, 0.1140f, 0.0F,
                  0.0000F, 0.0000F, 0.0000F, 1.0F };


float vec0[4] = { 0.0F, 0.0F, 0.0F, 0.0F };



const int32_t localSizeX = 8; // shader의 local_size_x
const int32_t localSizeY = 8; // shader의 local_size_y

const int32_t groupSize_x = ( srcW + localSizeX - 1 ) / localSizeX;
const int32_t groupSize_y = ( srcH + localSizeY - 1 ) / localSizeY;


glMemoryBarrier( GL_SHADER_IMAGE_ACCESS_BARRIER_BIT );


glUseProgram(programTex);

glUniform2i( 0, srcW, srcH );         // 이미지의 크기를 보내준다.
// transpose[pivot]한다. java에서 matrix순서를 opengl용으로 바꾼다.col <-> row
glUniformMatrix4fv( 2, 1, GL_TRUE, mat ); 
glUniform4fv( 3, 1, vec0 );


glBindImageTexture(0, srcTex, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8 );
glBindImageTexture(1, dstTex, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8 );
glDispatchCompute( groupSize_x, groupSize_y, 1 );


glUseProgram( 0 );

 

goupSize_*은 다음과 같이 계산된다. 예를 들어 70x90의 이미지가 있다면

localsize_x값은 8 이므로 가로pixel 처리를 위한 쓰레드 수는

workgroup_x = 70 / 8 = 8.75번 이 필요하다. 따라서 9번 돌려 줘야 한다.

따라서 가로 픽셀들을 처리하는 Thread수는 9x8 = 72번 이 되는 데,  70번째 Thread가 가로의 가장 마지막 픽셀을 처리하게 되므로 71번째, 72번째 쓰레드는 할 일이 없다.세로도 마찬가지로 계산이 된다.

따라서 다음의 쉐이더코드가 처리 할지 말지를 구분하는 것 뿐이다.

if( all( lessThan( pos, dim ) ) ) {
    // 처리한다.
}

왜 굳이 localsize_x와 y를 8로 정했는지는... 시간나면 쓰겠다.

 

texture를 읽고 쓰는 함수인 imageLoad와 imageStore는 texture내의 어느위치나 읽고 쓸 수 있다. fragment shader와 다르게 쓸 위치를 random하게 할 수 있다는 점이 이 ComputeShader의 장점이다.

texture는 readonly나 writeonly를 꼭 써줘야 한다. 즉 읽거나 쓰거나 하나만 할 수 있다. 다른 상황이 하나 있는 데, 둘 다 쓰는 것도 인정된다. 이 경우 읽을 수도 쓸 수도 없지만, imageSize함수를 통해 texture의 크기만 알아 낼 때 쓰기도 한다.

 

glBindImageTexture함수

// shader code
layout(binding = 0, rgba8 ) readonly  uniform highp restrict image2D uSrc;

// c code
glBindImageTexture(0, srcTex, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8 );

 

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

다른 파라미터는 바로 느낌이 온다. 필요한 것만 이야기 하면,

unit는 shader의 binding 값

format은 shader에서 어떤 형태로 사용할 것인지를 나타낸다.

(텍스처를 만들 때의 값과는 호환이 되어야 한다는데.. 필자는 그냥 동일하게 사용해서)

예제에서 GL_RGBA8을 전달 하기에 shader에서는 rgba8로 받아 서로 맞춰줘야 한다.

각각의 매칭값은 다음과 같다.

C shader type
GL_RGBA32F rgba32f float
GL_RGBA16F rgba16f float
GL_R32F r32f float
GL_RGBA32UI rgba32ui uint
GL_RGBA16UI rgba16ui uint
GL_RGBA8UI rgba8ui uint
GL_R32UI r32ui uint
GL_RGBA32I rgba32i int
GL_RGBA16I rgba16i int
GL_RGBA8I rgba8i int
GL_R32I r32i int
GL_RGBA8 rgba8 float
GL_RGBA8_SNORM rgba8_snorm float

type값은 이미지를 읽거나 쓰는 imageLoad/imageStore에서 사용되는 형태다.

imageLoad를 예로 들면 "리턴값은 GL_RGBA8인 경우 vec4로 받아야 한다. GL_RGBA16I일 경우는 ivec4로 받아야 한다."는 이야기다.

// C++ 
// GL_RGBA8은 float형
glBindImageTexture(0, srcTex, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8 );

// shader
vec4 color = imageLoad( tex, ivec( x,y ) );
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// GL_RGBA8UI은 uint형
glBindImageTexture(0, srcTex, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8UI );

// shader
uvec4 color = imageLoad( tex, ivec( x,y ) );

 

 

 

float 형

 

 

정수형(테스트 안해봄~) iimage2D <- i가 2개

 

Replies
Reply Write