OpenGL Shader – 3

Vertex Processor

버텍스 프로세서는 버텍스 쉐이더를 실행한다. 버텍스 쉐이더의 입력은 버텍스 데이터로써, 주로 버텍스의 위치, 색, 법선벡터 등으로써 OpenGL 어플리케이션이 보내준다.

다음 OpenGL 코드는 버텍스 프로세서에게 각 버텍스에 대한 색상, 위치 값을 전달해준다.

glBegin(...);
    glColor3f(0.2,0.4,0.6);
    glVertex3f(-1.0,1.0,2.0);

    glColor3f(0.2,0.4,0.8);
    glVertex3f(1.0,-1.0,2.0);
glEnd();

버텍스 쉐이더에서 다음과 같은 일을 할 수 있다.

  • 모델뷰 행렬과 프로젝션 행렬을 이용해서 버텍스의 위치를 이동
  • 노말 벡터의 변환, 그리고 필요하다면 노말 벡터의 정규화
  • 텍스쳐 좌표 생성과 변환
  • 버텍스에 대한 빛 또는 픽셀에 대한 빛 계산
  • 색상 계산

위의 모든 연산을 수행할 필요는 없다. 그러나 일단 버텍스 쉐이더를 만들었다면 버텍스 프로세서의 전체 기능을 버텍스 쉐이더가 대신하며, 따라서 법선 변환을 수행할 수 없고 고정 기능이 텍스쳐 좌표 생성을 수행하도록 할 수 없다. 버텍스 쉐이더가 사용될때 파이프 라인의 이 단계의 필요한 모든 기능을 버텍스 쉐이더가 대신하게 된다.

이전에서 설명했던 것처럼, 버텍스 쉐이더는 버텍스간 연결성 고려에 대한 정보를 가지고 있지 않다. 그러므로 위상 데이터를 필요로 하는 연산은 버텍스 쉐이더에서 수행할 수 없다. 예를들어서, 버텍스 쉐이더가 뒷면 제외를 할 수 없는데, 버텍스 쉐이더는 버텍스 단위 연산이지 버텍스가 이루고 있는 면에 대한 연산을 할 수 없기 때문이다. 면을 알려면 버텍스간의 연결 정보를 알아야 한다. 이러한 연결정보가 바로 위상 데이터의 하나이다. 버텍스 프로세서는 개별적인 버텍스 하나 하나를 처리 하는 순간에 그 하나 이외의 나머지 버텍스를 알지 못한다.

버텍스 쉐이더는 최소한 gl_Position 이라는 변수를 변경해야만 한는데, 이 변수는 보통 모델뷰 행렬과 프로젝션 행렬을 이용한 변환이다.

버텍스 프로세서는 OpenGL 상태에 접근할 수 있어서, 예를 들어 빛을 포함하고 재질을 사용하는 연산을 수행할 수 있다. 또한 텍스쳐에 대한 정보도 접근할 수 있는데 이것은 최신 그래픽 카드에서만 허용한다. 하지만 프레임 버퍼에는 접근할 수 없다.

OpenGL Shader – 2

다음 그림은 매우 단순화된 파이프라인 다이어그램이며 데이터가 어떤 식으로 파이프 라인을 타는지를 보여준다. 매우 단순화시켰지만 쉐이더 프로그래밍에 대한 중요한 개념을 제공하고 있다. 이 부분에서는 파이프라인의 고정 기능을 제공한다. 파이프라인은 추상적이며 모든 각 단계에서 어떤 특정한 구현과 만날 필요가 없다는 점을 주의하기 바란다.
Vertex Transformation

여기에서 버텍스는 공간상의 위치, 색상, 노말벡터, 텍스쳐 좌표 등과 같은 속성들의 집합이다. 이 단계에서의 입력값들은 하나 하나의 버텍스 속성들이다. 이 고정 기능에 의해 수행되는 연산은 다음과 같다.

  • 버텍스의 위치 변환
  • 버텍스에 대한 광원 계산
  • 텍스쳐 좌표의 생성 및 변환

Primitive Assembly and Rasterization

이 단계에서의 입력값은 변환된 버텍스와 연결 정보이다. 연결정보라는 것은 버텍스들이 Primitive(삼각형 등과같은 기본 요소)를 이루기 위해 어떻게 연결되느냐이다. 이 단계에서 Primitive가 생성된다. 또한 이 단계는 뒷면제거나 뷰 절두체에 대한 클리핑 연산을 담당할 수 있다.

라스터라이징은 Fragment를 결정하고 Primitive의 픽셀 위치를 결정한다. 이 문맥에서 Fragment는 특정한 위치에서 프레임 버퍼의 픽셀을 변경하기 위해 사용될 데이터의 조각이다. 쉽게 말하자면 Primitive가 화면에 실제 렌더링될때 그려질 Pixel 값이다. Fragment는 색 뿐만이 아니라 수직벡터값과 텍스쳐 좌표 등과 같은 값들인데, 이러한 값들을 이용해서 실제로 화면상에 찍힐 픽셀의 새로운 색상을 계산하는데 쓰인다.

이 단계에서의 출력값은 다음과 같다.

  • 프레임버퍼 안의 Fragment들의 위치
  • 버텍스 변환 단계에서 계산된 속성에 대한 각 Framgment에 대한 보간된 값

버텍스 변환 단계에서 계산된 값은 버텍스 연결정보와 함께 조합되어 Fragment를 위한 알맞은 속성을 계산하는데 사용된다. 예를들어서 각 버텍스는 변환된 위치를 가지고 있다. 버텍스가 Primitive로 만들어지는데 사용될때 Primitive의 Fragment의 위치를 계산하는것이 가능하다. 또 다른 예는 색상의 사용이다. 만약 삼각형을 구성하는 각 버텍스가 각기 다른 색상을 가지고 있다면, 삼각형 안의 Fragment 색상은 각 버텍스의 상대적인 거리에의한 가중치를 받아 색상값들이 보간되어져 얻어진다.

Fragment Texturing and Coloring

이 단계에서의 입력값은 보간되어진 Fragment의 정보이다. 색상은 이미 이전 단계에서 보간을 통해 계산되어졌고, 이 단계에서는 텍셀(Texel, Texture element)값 결합과 같은 연산이 수행된다. 텍스쳐 좌표는 이전단계에서 보건되어진다. 안개도 역시 이 단계에서 적용된다. 고통적인 최종 출력값은 Fragment의 색상값과 깊이 값이다.

Raster Operations

이 단계에서의 입력값은 다음과 같다.

  • 픽셀의 위치
  • Fragment의 색상값과 깊이값

Fragment에 대해 수행되는 파이프라인의 마지막 연속 단계는 주로 다음과 같다.

  • Scissor Test
  • Alpha Test
  • Stencil Test
  • Depth Test

만약 테스트가 성공한다면, Fragment 정보는 현재의 블렌딩 모드에 따라 픽셀의 값을 변경하는데 사용된다.  블렌딩은 이 단계에서만 수행되는데, Fragment 텍스쳐링과 컬링 단계에서는 프레임 버퍼에 접근하지 못하기 때문이다. 프레임 버퍼는 오직 이 단계에서만 접근할 수 있다.

고정 기능에 대한 간단한 그림 설명

다음 그림은 위에서 설명한 각 단계에 대한 설명을 그림으로 다시 풀어높은 것이다.


Replacing Fixed Functionality

현재의 그래픽 카드는 프로그래머가 위에서 기술된 단계 중에 2개를 새롭게 정의할 수 있게 해준다.

  • 버텍스 쉐이더는 Vertex Transformation 단계를 작성하는데 사용된다.
  • 프레그먼트(Fragment) 쉐이더는 프레그먼트 텍스쳐링과 컬러링 단계의 고정 기능을 교체하는데 사용된다.

다음 섹션에서는 프로그래밍 가능한 단계, 즉 버텍스 처리기와 프레그먼트 처리기에 대해 설명할 것이다.

OpenGL Shader – 1

GLSL을 사용하는 쉐이더 프로그래밍을 알아보자. 쉐이더는 최신 기술이며 3D 게임에서 경이로운 효과를 내는데 사용된다. 이 글을 통해 쉐이더의 세계에 발을 들여놓자.

이 글과 앞으로 진행될 내용을 이해하기 위해서는 OpenGL 프로그래밍에 익숙해야 한다. 또한 쉐이더를 위한 스펙에 대해 알고 싶다면, OpenGL 2.0과 GLSL에 대한 공식문서를 반드시 읽어보길 바란다.

GLSL은 OpenGL 쉐이딩 언어를 의미하며 종종 “glslang”으로 불리며 ARB(Architectural Review Board) OpenGL에 의해 정의된다.

OpenGL과 경쟁하는 Cg, 즉 Nvidia가 나름대로 만든 또 다른 쉐이더 언어와 GLSL을 비교하거나 누가 더 우수한지 따지지는 않겠다. 그 이유는 지금 여기서는 Cg가 아닌 GLSL에 대해서만 살펴볼 것이기 때문이다.

어떤 언어로든 쉐이더를 작성하기 전에, 그래픽 파이프라인에 대한 기본을 이해하는 것이 매우 중요하다. 그래픽 파이프라인은 쉐이더가 무엇을 하는지, 쉐이더의 종류에는 뭐가 있는지와 같은 쉐이더의 핵심을 제공한다. 또한 쉐이더로 할 수 없는것이 무엇인지에 대한 힌트도 제공한다.

파이프라인에 대한 소개 다음에 GLSL을 위한 OpenGL의 설정에 대해서 알아보겠다. OpenGL 어플리케이션에서 쉐이더를 사용하데 필요한 단계에 대해 자세하게 살펴볼 것이다. 최종적으로 OpenGL 어플리케이션이 매우 유연하고 강력한 효과를 낼 수 있는 방법에 대해 살펴볼 것이다.

몇가지 기본적인 개념으로 데이터 타입(data types), 변수(variables), 문장(statements), 함수(function)의 정의에 대해서 알아본다.

이 글은 ARB 확장과 OpenGL 2.0 버전에 대해 다룬다. 후자는 이미 표준이지만 전자는 향후 표준으로 자리잡을 가능성이 있는 것이다. 차이는 작지만 함수의 이름이나 상수 이름에 약간의 차이를 가지고 있다. 이것들을 구분을 돕기위해 코드에 색상을 넣었다. ARB는 회색으로 OpenGL 2.0은 오렌지 색으로 말이다.

이 적용범위는 실수하기 쉽다. 그러니 혹시 이런 표기에 문제가 있다면 언제든지 내게 알려주길 바란다.

이 글은 점진적으로 작성될 것이므로 글과 데모 프로그램에서 문제가 있을 수 있는데, 이것에 대해서 좀 참아주길 바란다. 비록 사소할지라도 문제를 발견하면 알려주길 바란다. 또한 많은 제안을 해주길 바란다. 부디 이 글을 즐겁게 읽어주길 희망하며 첫번째 GLSL, OpenGL Shader의 첫글을 마치도록 하겠다.

마우스를 이용한 View 회전

예전에 나이스가이님이 질문한 마우스를 이용해 현재 화면을 회전하면서 화면상의 물체를 살펴보는 방법에 대해 간단하게 설명해 보려한다. 전문용어(?)로는 Arcball 기법이라고 하는데, 이 Arcball을 구현하는 방법으로 직접 회전행렬을 계산해서 OpenGL의 glMultMatrix를 이용해 적용하는 방법 하나와 x와 y축에 대한 회전각도만을 계산해서 glRotatef를 이용해 적용하는 방법이 있다. 첫번째 방법은 NeHe의 강좌에 소개되어져있으나 URL은 http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=48 이다. 하지만 필자는 두번째 방법을 이용해 Arcball를 구현해 보려고 한다. 이유인 즉, 첫번째 방법은 OpenGL에서는 기본적으로 행렬 Type와 행렬간의 연산자 함수를 제공해주지 않으므로, 회전행렬을 직접 구하기 위한 행렬 Type을 개발자가 직접 정의해줘야 하고 행렬연산을 만들어줘야하는 번거로움이 있는 반면, 두번째 방법은 단지 x와 y축에 대한 회전각도 값을 실수형으로도 충분하기 때문이다.

글자만 있으면 썰렁하므로 별 의미도 없을법한 최종 결과 스크린샷은 아래와 같다.


화면상에 OpenGL의 GLUquadricObj을 이용하여 실린더를 6개를 그렸으며, 마우스를 이용하여 실린더 6개를 회전시키면서 살펴볼 수 있다. 보다 자세한 조작법은 마우스 오른쪽 버튼을 누른 상태에서 좌우로 마우스를 이동하면 Y축으로 회전되며 상하로 마우스를 이동하면 X축으로 회전한다. 이러한 사용자 액션과 반응을 염두해 두고 코드를 살펴보면 이해하는데 큰 도움이 될 것이다.

먼저 마우스 조작에 따른 Arcball 기법을 적용하기 이전에 화면상에 6개의 실린더를 그려주는 코드는 다음과 같다.

int DrawGLScene()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	glTranslatef(0.0f, 0.0f, -20.0f);

	bool bToggle = true;
	for(float offset=-5; offset<=5; offset+=2) {
		if(bToggle) glColor3f(0.6f, 0.6f, 1.0f);
		else glColor3f(1.0f, 0.6f, 0.6f);
		bToggle = !bToggle;

		glPushMatrix();
		glTranslatef(offset, 0.0f, 0.0f);
		glTranslatef(0.0f, 0.0f, -8.0f);
		gluCylinder(obj, 1.0f, 1.0f, 16.0f, 38, 4);
		glPopMatrix();
	}

	return TRUE;			
}

코드를 보고 이미 짐작하고 계실 분도 있겠지만, 이 코드는 NeHe의 강좌에서 제공하는 코드를 기본으로 하고 있다. 혹시 필요한 분들을 위해 전체코드를 다운로드 할 수 있도록 하겠다.

이제 위의 코드에서 마우스를 이용해 상하좌우로 회전시키면서 6개의 실린더를 구석구석 관찰하는 코드를 추가해보자.

먼저 마우스의 Drag를 통해 계산될 X축과 Y축에 대한 회전값을 위한 변수는 다음과 같다.

GLfloat xAngle;
GLfloat yAngle;
POINT mouseDownPt;

언급하지 않은 mouseDownPt 변수가 있는데, 이것은 마우스 버튼을 누른 좌표 지점을 저정하기 위한 변수이다. 마우스 버튼을 누른 지점과 마우스 버튼을 누른 상태에서 마우스를 이동하였을때 X축과 Y축으로 얼마만큼 이동되었는지를 계산하고 이 계산된 값을 이용해 xAngle와 yAngle값이 계산된다.

일단 xAngle와 yAngle가 계산되었다고 가정하고 최종적으로 회전을 적용시킨 코드는 가장 앞서 언급한 코드인 DrawGLScene 함수의 수정에 있다.

int DrawGLScene()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();							
	glTranslatef(0.0f, 0.0f, -20.0f);

	glRotatef(xAngle, 1.0f,0.0f,0.0f);
	glRotatef(yAngle, 0.0f,1.0f,0.0f);

	bool bToggle = true;
	for(float offset=-5; offset<=5; offset+=2) {
		.
		.
		.
	}

	return TRUE;			
}

새롭게 추가된 오렌지색 두줄의 코드가 전부이다. X축과 Y축의 회전에 맞게 glRotatef의 함수의 인자가 사용되었음을 주의해서 보기 바란다.

이제 마지막으로 마우스의 액션에 따른 실제 xAngle와 yAngle의 값을 계산하는 방법을 알아보도록하자. 이 계산 코드는 마우스 액션에 대한 Event에 위치하게 되는데, MouseDown, MouseMove, MouseUp 이벤트에 그 코드가 존재한다.

	case WM_LBUTTONDOWN:
	{
		mouseDownPt.x = GET_X_LPARAM(lParam);
		mouseDownPt.y = GET_Y_LPARAM(lParam);

		SetCapture(hWnd);

		return 0;
	}

위의 코드는 간단히 앞에서 정의한 mouseDownPt 변수에 가장 처음 마우스 버튼이 눌려진 위치를 저장하고 있으며 SetCapture  API를 호출해서 마우스가 Window를 벗어나도 지속적으로 Mouse Message를 받을수 있도록 하고 있다.

	case WM_MOUSEMOVE:
	{
		if(GetCapture() == hWnd) {
			int X = GET_X_LPARAM(lParam);
			int Y = GET_Y_LPARAM(lParam);

			xAngle += (Y-mouseDownPt.y) / 3.6;
			yAngle += (X-mouseDownPt.x) / 3.6;

	                InvalidateRect(hWnd, NULL, FALSE);

			mouseDownPt.x = X;
			mouseDownPt.y = Y;
		}

		return 0;
	}

위의 코드는 마우스를 이동했을때 호출되는 코드인데, GetCapture를 이용해서 현재 마우스 버튼이 눌러졌는지를 검사하고, 만약 마우스가 눌려진 상태에서 움직였다면 바로 여기서 xAngle와 yAngle를 계산한다. 계산공식을 눈여겨 보길 바란다. 3.6으로 나눠주고 있는데, 이것은 마우스를 1 픽셀 이동했을 경우 1/3.6 회전, 즉 0.27777778도 회전하게 되는데, 이 회전량이 사용자가 가장 자연스럽게 느껴지므로 3.6으로 나눴다. 좀더 회전정도를 강하게 하고 싶다면 이 값을 줄이면 회전이 팍팍!! 되도록 할 수 있다.

	case WM_LBUTTONUP:
	{
		ReleaseCapture();
		return 0;
	}

끝으로 위의 코드는 마우스 버튼을 눌러 이동하면서 물체를 이리저러 살펴보다가 마우스 버튼을 누름을 해제시킬때 발생하는 코드로써, ReleaseCapture API를 호출한다. ReleaseCapture는 SetCapture를 호출한 윈도우가 반드시 호출해줘야 하는 Couple API로써 SetCapture에 의한 마우스 메세지의 독점처리를 해제하는 함수이다.

이상으로 간단하게 마우스를 이용한 Arcball 기법에 대해 살펴보는 것을 마치도록 하며, 아래에 해당 소스를 링크한다.