2018년 1월 16일 화요일

Shader in C/C++ Code (GLSL)

Shader in C/C++ Code (GLSL)

앞서 우리는 3D Geometry Graphics 개념과 Rendering 에 대한 많은 기술과 이론을 이야기 하여 왔다. 물론 아직 가야할 길이 멀다.

struct VERTEX{float X, Y, Z;};

VERTEX vertices[] =
{
{0.0f, 0.5f, 0.0f},
{0.45f, -0.5f, 0.0f},
{-0.45f, -0.5f, 0.0f}
};

위의 vertices 배열는 3개의 VERTEX 가 정의 되어 있다. 이들은 삼각형의 각 정점 좌표이다.
즉 3개의 정점에 의하여 하나의 삼각형을 이루게 된다.
결국 위의 vertices 배열은 한개의 삼각형 정보를 담고 있다.

다음과 같은 코드를 통하여 위의 정점들은 비디오 메모리의 버퍼 영역에 들어가게 되며 이들은 하나의 VBO 즉 “버텍스 버퍼 오브젝트” 라는 것으로 관리 될 것이다.

GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

이제 이러한 버퍼 정보를 실제로 렌더링 하기 위하여 쉐이더를 만들고 이들을 파이프라인으로 연결해야 한다.

다음과 같은 쉐이더 소스를 준비한다.
이러한 소스 코드는 외부 파일에 만들어 이 파일을 string 으로 읽어서 사용할 수도 있고 그냥 C/C++ 소스코드에 char * 로 넣어놓고 사용해도 된다.

// vertex shader source
const GLchar* vertexSource = R"glsl(
#version 150 core
in vec3 position;
void main()
{
gl_Position = vec4(position.x,position.y, position.z, 1.0);
}
)glsl";


// fragment shader source
const GLchar* fragmentSource = R"glsl(
#version 150 core
out vec4 out_color;
void main()
{
out_color = vec4(1.0,1.0,1.0,1.0);
}
)glsl";


이제 버텍스 쉐이더와 프레그먼트 쉐이더를 만들고 위의 쉐이더 소스코드를 컴파일 하는 것을 작성해 보자.

버텍스 쉐이더 컴파일
// Create and compile the vertex shader
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexSource, NULL);
glCompileShader(vertexShader);

프레그먼트 쉐이더 컴파일
// Create and compile the fragment shader
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
glCompileShader(fragmentShader);

OpenGL 의 glCreateShader는 2가지의 Shader를 만들 수 있다.
GL_VERTEX_SHADER
GL_FRAGMENT_SHADER

Geometry Shader는 이 함수로 만들지 않는다.  
이는 OpenGL ARB 의 확장형으로 다루기 때문에 여기서는 이야기 하지 않겠다.
ARB 는 Architecture  Review Board” 의 약자로 OpenGL의 Extensions 에 대한 내용을 담당하고 있다고 보면 된다.


OpenGL 은 위와 같이 만들어진 Shader들을 하나의 Program 으로 연결하여 작동 하도록 하고 있다.




// Link the vertex and fragment shader into a shader program
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);


이렇게 되면 두개의 쉐이더가 각각 만들어지고 (버텍스 쉐이더, 프레그먼트 쉐이더) 하나의 프로그램(shaderProgram) 에 연결 된 것이다.

여기서 프로그램이라는 것은 쉐이더 프로그램이다. 어플리케이션 프로그램이 아니다.
즉 GPU가 실행하는 프로그램이라고 생각하면 된다.

프로그램은 link 를 하고 use 를 해야 GPU가 실행할 수 있다. 다음 함수에 의하여 이러한 과정이 수행될 것이다.

glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);


아래 그림은 위의 과정을 도식한 그림이다.




이제 버텍스 버퍼(vbo) 에 있는 데이터의 구조를 GL에 알려 주어야 하는 일이 남아 있다.
GL 은 버텍스 버퍼에 정점 자료들을 갖고는 있으나 이러한 자료가 어떻게 구성 되었는지를 아직 모른다.
이러한 자료 구조는 결국 쉐이더 프로그램에 연결되는 자료 구조가 된다.
이러한 작업은 Vertex Attribute 라는 것으로 이루어진다.

일단 쉐이더프로그램에서 입력으로 사용할 변수의 위치를 알아내야 한다.
이러한 과정을 layout 이라 부르고 있다.

// Specify the layout of the vertex data
GLint posAttrib = glGetAttribLocation(shaderProgram, "position");

위의 코드는 shaderProgram 에서 “position” 이라는 변수 값이 어느 위치에 있는지를 알아내는 기능을 수행한다.

바로 버텍스 쉐이더 소스의
in vec3 position;
의 위치를 알아 내는 것이다.

이러한 위치는 이렇게 이름으로 알아내는 방법도 있고 직접 쉐이더 코드를 작성할때 layout 을 지정하여 직접 할당할 수도 있다.


다음 코드는 앞에서 알아낸 position 을 활성화 하고 버텍스 버퍼의 위치를 알려주는 역할을 수행하게 된다.
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, sizeof(VERTEX), (void *)0);

즉 아래 그림과 같이 연결해준다고 보면 된다.


자세한 내용은 glVertexAttribPointer 함수의 설명을 참조하기 바란다.

void glVertexAttribPointer( GLuint index,
GLint size,
GLenum type,
GLboolean normalized,
GLsizei stride,
const GLvoid * pointer);

간단히 인수 설명을 하겠다.

GLuint index
vertex attribute index 이다. glGetAttribLocation 함수로 찾은 레이아웃 위치를 말한다.

GLint size
정점 하나에 있는 구성요소의 갯수를 뜻한다. 이 값은 1 부터 4까지의 숫자중 하나가 된다.
즉 버퍼에서 쉐이더로 연결된 attribute 는 하나의 attribute당 최대 4개의 구성요소가 있다는 것이다.

좀더 쉽게 말하자면 3차원 좌표를 보낸다고 생각하면
{x,y,z} 가 vec3 로 연결되는데 이 경우 3개의 구성요소가 있다는 것이다.

만약 {x,y,z,w} 를 vec4로 연결할 경우는 그 값이 4가 될 것이다.

GLenum type
GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT,  GL_UNSIGNED_INT
GL_HALF_FLOAT, GL_FLOAT, GL_DOUBLE, GL_FIXED, GL_INT_2_10_10_10_REV, GL_UNSIGNED_INT_2_10_10_10_REV GL_UNSIGNED_INT_10F_11F_11F_REV
중 하나가 사용된다.
즉 연결된 정점의 구성요소의 자료 크기를 말한다. 위의 경우 float 를 사용하고 있기 때문에 GL_FLOAT를 입력해 주면 된다.

GLboolean normalized
입력된 fixed-point data 값을 normalized (GL_TRUE) 하느냐 아니면 그냥 그값을 사용하느냐(GL_FALSE)를 결정한다.
여기서는 GL_FLASE를 입력한다.

GLsizei stride
버텍스 하나의 바이트 크기를 말한다.
sizeof(VERTEX)

const GLvoid *pointer
해당 정보의 시작 offset를 지정한다. 0 이면 버퍼의 가장 처음을 뜻한다.
만약 이 값이 10 이면 버퍼의 시작에서 10byte 뒤에 데이터가 있다고 하는 것이다.

정점 정보로 좌표에 추가적으로 다른 정보 (예를 들면 색상 정보) 들을 추가 할 수 있다. 이러한 내용은 다음에 설명하도록 하겠다.


댓글 없음:

댓글 쓰기

Vertex , 정점 정보의 확장

Vertex , 정점 정보의 확장 그동안의 설명에 있어서 우리는 VERTEX 정보에 대하여 3 차원의 위치 정보만 사용하였다. struct VERTEX {float x,y,z;}; 이제 이를 확장하여 색상을...