Programming

OpenGL에서 GLSL을 GPGPU 목적으로 사용하기

라우드니스 2012. 4. 12. 21:14

CPU라는 제한된 프로세서 만으로는 현대인이 요구하는 빠른 속도를 감당하기에는 약간 힘든 것이 사실입니다. 모든 사용자가 프로그래머가 아니니 알고리즘의 시간복잡도에 따른 속도를 이해하며 프로그램의 수행시간 차이를 기다려 주진 않습니다.


만약 프로그램을 제작하는데 타겟이 되는 하드웨어에서 GPU가 달려있다면, (대부분의 PC와 모바일 디바이스는 GPU가 99.9%로 존재합니다!) GPGPU는 더 향상된 능력을 보여주는데 도움이 될 것입니다.


PC용 프로그램이라면 GPGPU 목적의 Nvidia사의 CUDA Library라던가, OpenCL을 사용할 수 있지만 모바일 디바이스의 경우 그렇지 않습니다. 이런 경우를 생각한다면, Shader Language를 GPGPU목적으로 사용한다는것은 아직도 의미가 있다고 생각합니다.

 

1. 어떻게 GPU와 CPU는 데이터를 주고 받을 수 있는가?

 

GPU를 사용하려면 우선 GPU쪽으로 데이터를 넘겨줘야 하는데, 저는 처음에 이런 함수가 OpenGL 상에 존재할거라 생각했습니다. 그래서 여러가지 Buffer에서 값을 읽어오는 듯한 함수들을 많이 사용해 봤지만, 결과적으로는 그렇지 않다는 것을 관련 문서를 찾고나서 깨닫게 됐습니다. 


GPU와 CPU간의 데이터 교환은 Texture를 이용하여 합니다. 이미지를 다뤄보셨다면 아시겠고, 혹은 그렇지 않으시다면 조금만 생각을 해보면 됩니다. 이미지란 width와 height만큼 pixel을 가지고 있는 데이터 배열입니다. 즉 GPU에게 '어떠한 Texture를 가지고 있어라' 하는 것은 CPU 입장에서는 width*height만큼의 pixel channel bit(8bit, 16bit, 24bit, 32bit) 정보를 저장하는 데이터 배열을 가지고 있는 것과 동일합니다.


2. 계산 범위의 지정은 어떻게 할 것인가?


1번에서 이미지에 대해서 이야기 하면서 눈치채셨겠지만, 이미지의 height와 width의 곱이 곧 배열의 범위가 됩니다. 또 texture가 어떠한 channel의 이미지인지를 사용하여 한 데이터의 크기를 설정하게 됩니다.

 

3. 실제 연산을 하는 코드는 어떻게 만들어야 하나?


그럼 실제 연산을 하는 코드는 어디에서, 어떻게 만들어야 하나? Shader에는 Vertex Shader와 Fragment(Pixel) Shader 두 가지가 존재합니다. 이 중 Fragment Shader에서 하면 됩니다.

 

왜 Fragment Shader에서 하면 되는가? 라는 질문은 그래픽의 처리과정에서 알 수 있습니다. 간단하게 이야기 하자면 Vertex Shader를 처리한 후 그 결과를 가장 마지막에 처리하는 것이 Fragment Shader이기 때문이기도 하고, 이름 그대로 Vertex 내부의 각각의 pixel을 처리하는 것이 Fragment Shader이기 때문입니다. Texture로 따지면 이미지가 어느곳에 위치할지, Width와 Height를 기준으로 어느 정도의 크기를 가지는지에 대한 간단한 사각형을 그리는 것은 Vertex Shader가 하고 이미지 내부의 각각의 pixel을 처리하는 것은 Fragment Shader가 담당하게 됩니다. 


Texture가 우리가 계산할 배열이기 때문에 Fragment Shader에서 각각의 pixel을 계산하다는 것은 곧 그 데이터 위치를 참조했다고 생각할 수 있습니다.

 

s와 t는 그래픽스에서 texture의 x,y를 나타내는데 사용하는 용어입니다.


4. 연산이 끝낸 데이터를 GPU에서 어떻게 CPU로 보내는가?

 

CPU에서 GPU가 처리한 내용을 곧바로 가져올 수 있는 방법은 적어도 OpenGL 상에서는 없습니다. 그러면 어떻게 하면 되는가? 간단히 Texture를 렌더링하면 됩니다. 렌더링을 하려면 우선 결과값이 Frame buffer에 가게 되고, CPU는 Frame Buffer의 값은 참조할 수 있기 때문이죠. 이는 OpenGL 상에서 몇가지 함수가 존재하긴 합니다만, 확실한건 glReadPixels 함수를 사용하면 간단히 얻어올 수 있습니다. 한가지 유의하실 점은 glReadPixels 함수에서는 좌하단의 좌표가 (x,y)의 시작점이기 때문에 좌상단의 지점을 시작점이라고 생각하시면 데이터가 y축으로 반전되어서 받아오는게 됩니다.

 

지금까지의 이야기를 실제 OpenGL 함수로 이야기 해보자면, GPU에게 Texture를 보내주는 glTexImage2D함수는 곧 CPU가 GPU에게 데이터를 넘겨주는 역할을 하게 됩니다. glDraw(Arrays, Elements)함수는 GPU가 그래픽을 렌더링하여 그 결과를 Buffer에 옮기는 역할을 수행하게 되며, 마지막으로 glReadPixels을 이용해 그 내용을 메모리로 읽어오면 되는 것 입니다.


5. Blur 처리를 하는 Vertex, Fragment Shader Code


private
string[] vertexShaderCode = {

"in vec4 vPosition; \n",

"in vec2 textureCoord; \n",

"out vec2 tCoord; \n",

"void main(){ \n",

" tCoord = textureCoord; \n",

" gl_Position.xz = vPosition.xz; \n",

" gl_Position.y = -vPosition.y; \n",

"} \n"};


glReadPixels에 대해 이야기 하였듯, 데이터를 처리할 때 GPU상에서는 이미지를y축으로 반전시켜야 올바른 데이터를 CPU쪽에서 가져올 수 있습니다. 이런 역할을 하는 것이 vertex shader이기 때문에 이 쪽에서 y축 위치를 반전시켜 주면 됩니다.

 

private
string[] BlurShaderCode = {

"precision mediump float; \n",

"uniform sampler2D texture1; // color texture\n",

"in vec4 image; \n",

"in vec2 tCoord; \n",

"void main(){ \n",

" float width = 1/649.0; \n",

" float height = 1/868.0; \n",

" vec4 sample[9]; \n",

" sample[0] = texture2D(texture1, tCoord + vec2(-width, -height)); \n",

" sample[1] = texture2D(texture1, tCoord + vec2(0, -height)); \n",

" sample[2] = texture2D(texture1, tCoord + vec2(width, -height)); \n",

" sample[3] = texture2D(texture1, tCoord + vec2(-width, 0.0)); \n",

" sample[4] = texture2D(texture1, tCoord);\n",

" sample[5] = texture2D(texture1, tCoord + vec2(width, 0.0)); \n",

" sample[6] = texture2D(texture1, tCoord + vec2(-width, height)); \n",

" sample[7] = texture2D(texture1, tCoord + vec2(0, height)); \n",

" sample[8] = texture2D(texture1, tCoord + vec2(width, height)); \n",

" gl_FragColor = (sample[0] + (3.0*sample[1]) + sample[2] + (3.0*sample[3]) + sample[4] + (3.0*sample[5]) + sample[6] + (3.0*sample[7]) + sample[8]) / 13.0;\n",

"} \n"};


Blur를 처리하기 위해서는 자기 자신과 주변의 값을 기준으로 특정 값만큼 처리를 하게 됩니다. Fragment shader에서는 어떠한 처리를 실제로 하는데 어떠한 것을 사용 할 수 있는 지는 GLSL에 대하여 공부하시면 됩니다. 대충 짜느냐 이미지의 width하고 height를 shader안에서 처리하고 위치 참조에 관한 값 처리도 코드 내부에서 해서 조금 더럽습니다만, 좋은 프로그램이라면 이런 부분을 코드 상에서 해서 shader를 일일이 수정하지 않게 해야겠죠 -_-;;;


6. 결과


제 노트북의 CPU가 Pentium B940이라 더 좋은 CPU를 사용하면 충분히 속도차가 더 조금 날 수 있겠지만 CPU의 경우 약 9초가 결렸고 GPU의 경우 약 0.2초가 걸렸습니다. 


약 45배의 속도차이가 났습니다만 해당 연산 자체가 병렬프로그래밍하기에 좋게 되어있기 때문에 GPU의 속도가 월등히 빠를 수 있었겠지요. 아마 실제 연산을 위해 GPU를 사용한다면 이 정도의 극적인 성능차이는 얻기 힘들 것이라 생각됩니다.


그래도 확실한건 GPU라는 별도의 연산 코어가 있는데 이를 사용하지 않는 것은 분명히 자원의 낭비이고, CPU가 해야할 일을 GPU가 해준다는 것은 CPU가 더 많은 일을 할 수 있다는 것을 뜻하므로 아마 전체적인 시스템의 성능 향상이 일어나지 않을까 생각합니다. 물론 추가적인 전기 소비 때문에 모바일 디바이스의 경우 더 빠른 배터리 소모를 볼 수 있을지도 모르겠습니다만.

반응형