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 Shader and Pixel Shader (HLSL)

Vertex Shader and Pixel Shader (HLSL)


Direct3D 의 Shader 프로그램은 HLSL (High Level Shading Language) 언어로 작성 된다.
HLSL 은 OpenGL 의 GLSL 과 전체적으로는 유사한 언어구조를 갖는다. 그러나 다른점이 많다.


가정 먼저 나타나는 차이는 바로 main 함수의 존재이다.


GLSL 에서는 Shader의 진입 함수로 main() 함수가 사용되었다. 그러나 HLSL은 이러한 역할을 수행하는 함수가 있지만 그 함수 이름이 main으로 고정되지는 않는다.


즉 진입 함수를 프로그램적으로 정의 할 수 있다는 것이다.


이렇게 진입 함수 이름을 다르게 정할 수 있는데에는 나름대로 장점이 있다.


우리는 Vertex Shader와 Pixel Shader를 모두 작성하게 된다.
GLSL에서는 이 두개의 쉐이더에 각각 main 함수가 있어야 하기 때문에 이 두 파일을 따로 컴파일 하여야 한다. 쉐이더 코드를 각각 준비해야 된다는 것이다.


그러나 이렇게 진입호출 함수를 직접 정할 수 있다면, 하나의 파일로 Vertex Shader 와 Pixel Shader를 관리 할 수 있다는 것이다.


GLSL 과 또다른 차이는 Semantics 가 있고 없고의 차이 이다.
GLSL 에서는 Semantics 이 없다. 그러나 HLSL 에서는 Semantics 가 있어야 한다.


그러면 Semantics가 과연 무엇인지를 알아야 GLSL 의 지식에 기반하여 HLSL 프로그램을 이해할 수 있게 된다. GLSL 은 Semantic 이라는 것이 필요가 없다.


struct VOut
{
   float4 position : SV_POSITION;
};

VOut VShader(float4 position : POSITION)
{
   VOut output;
   output.position = position;
   return output;
}

float4 PShader(float4 position : SV_POSITION) : SV_TARGET
{
   return float4(1.0,1.0,1.0,1.0);
}


위의 코드는 HLSL 로 작성한 버텍스 쉐이어와 픽셀쉐이더이다. 두개의 서로다른 쉐이더를  같은 파일에 작성 할 수 있는 것은 앞서 말했듯이 main가 없고, 직접 쉐이더 프로그램을 C/C++ 코드에서 직접 지정할 수 있기 때문이다.


GLSL 의 코드와 차이가 있음을 알 수 있다.


우선 쉐이더 main 함수가 없고, 직접 이름을 지정하여 호출하게 되는데, 이 함수들이 입력 파라미터를 갖고 있고 또한 return 값도 갖고 있다.


우리가 GLSL 의 main 함수는 void main(); 으로 return 값이 없었다는 것과 입력 파라미터가 없었다는 것을 기억하기 바란다.


또한 변수의 선언에 차이를 보인다.


float4 position : POSITION


와 같이 선언 된것을 볼 수 있다.


HLSL에서 변수의 선언(함수의 선언도 비슷하다) 구조는 다음과 같다.


[Storage_Class] [Type_Modifier] Type Name[Index]
   [: Semantic]
   [: Packoffset]
   [: Register];
   [Annotations]
   [= Initial_Value]


좀 복잡하기도 하다.. 생략 가능한 몇가지를 제거하고 좀 단순화 시켜보자.


Type Name [: Semantic];


Type Name 은 GLSL 과 같은 모습이다. 물론 C 언어와도 같은 모습이다.


여기에 이어서  : (콜론 기호) 를 입력한 후 Semantic 을 추가 할 수 있다.
시멘틱에 대해서는 다음 링크를 읽어보기 바란다.




시멘틱은 해당 변수의 성격을 정해준다. POSITION 이라는 것은 시멘특에서 보면 버텍스 쉐이더에서 사용하는 input 값중 하나로 object 의 버텍스 위치를 의미한다고 되어 있다.


POSITION[n]
Vertex position in object space.


VOut VShader(float4 position : POSITION)
버텍스 쉐이더 이다. 버텍스 버퍼의 위치정보가 position 인수로 전달되어 온다.
VOut 이라는 구조체를 return 하게 된다.


VOut 은 float4 position : SV_POSITION 정보를 갖도록 선언된 구조체이다.


여기서 SV_POSITION 도 시멘틱이다.

float4 PShader(float4 position : SV_POSITION) : SV_TARGET
픽셀쉐이더 이다.

위의 쉐이더에 의하여 출력되는 화면은 다음과 같다.





이또한 픽셀쉐이더에서 색상을 변경할 수 있다.


float4 PShader(float4 position : VPOS) : SV_Target
{
   return float4(1.0,.0,.0,1.0);
}




위의 경우 버텍스 쉐이더에서 픽셀쉐이더로 position이 전달되고 있는데 쉐이더 코드에서는 직접 사용하지 않고 있기는 하다.


Vertex , 정점 정보의 확장

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