Post Lists

2018년 6월 24일 일요일

Getting Started - Shaders (6) 번역

위의 자료를 번역한 내용입니다.
--------------------------------------

Shaders
Hello Triangle tutorial에서 언급했듯이, shaders들은 GPU에 있는 작은 프로그램들이다. 이러한 프로그램들은 그래픽스 파이프라인의 각각 특정한 구간을 위해 구동되어진다. 기본적인 의미로, 쉐이더들은 inputs을 outputs으로 변형시키는 프로그램일 뿐이다. 쉐이더들은 또한 서로 communicate하는 것이 허용되어있지 않다는 점에서 매우 고립된 프로그램들이다. 그들이 할 수 있는 유일한 커뮤니케이션은 그들의 inputs과 outpus이다.

GLSL
Shaders들은 C와 같은 언어 GLSL로 쓰인다. GLSL은 그래픽스 사용을 위해 맞춰졌고, vector와 matrix 조작에 특별히 목적을 맞춘 유용한 특징들을 포함한다.

쉐이더들은 항상 version 선언과 시작한다. 그리고 input과 output 변수들, uniforms(변수) 그리고 그것의 main 함수들이 나온다. 각 쉐이더의 시작 점은 그것의 main function에서 시작한다. 거기에서 우리는 어떤 input 변수를 처리하고, 그것의 output 변수들에서 결과를 output시킨다. uniforms들이 무엇인지 모른다면 걱정하지말아라. 우리는 그것에 대해 곧 설명할 것이다.

쉐이더는 전형적으로 다음의 구조를 갖는다.

#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

void main()
{
`// process input(s) and do some weird graphics stuff
...
// output processed stuff to output variable
out_variable_name = weird_stuff_we_processed;
}

우리가 vertex shader에 대해 특정하게 말할 때, 각각의 input 변수는 또한 vertex attribute로 알려져있다. 하드웨어에 의핸 제한된 우리가 선언하도록 허용되는 vertex attributes들의 최대 숫자가 있다. OpenGL은 항상 적어도 16개의 4-component vertex attributes가 이용가능하지만, 몇 몇 하드웨어는 너가 GL_MAX_VERTEX_ATTRIBS를 쿼리하여 너가 받을 수 있는 것에 대해서 고려할지도 모른다. :

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << '\n';

이것은 종종 대부분의 목적을 위해 충분한 것 이상의 최소 16의 값을 반환한다.

Types
GLSL은 우리가 무슨 종류의 변수와 작업하고 싶은 것들을 명시하기 위해 다른 프로그래밍 언어처럼 데이터 타입들을 가지고 있다. GLSL은 C같은 언어들로부터 우리가 아는 default 기본 타입들의 대부분을 가진다. : int, float, double, uint and bool. GLSL은 또한 튜토리얼 전반에서 많이 사용할 두 개의 컨테이너 타입들의 특징을 가지고 있다. 이름으로는 vectors들과 matrices들. 우리는 나중 튜토리얼에서 matrice들에 대해 이야기 할 것이다.

Vectors
GLSL에서 vector는 방금 언급된 기본 타입들에 대해 1,2,3 또는 4개의 component container이다. 그것들은 다음의 형태를 취할 수 있다. (n은 components의 개수를 나타낸다.)

* vecn : n개의 floats의 기본 vector
* bvecn : n개의 booleans의 vector
* ivecn : n개의 integers의 vector
* uvecn : n개의 unsigned integers의 vector
* dvecn : n개의 double components들의 vector

대부분, 우리는 vecn을 사용할 것이다. floats들이 대부분의 우리 목적에 충분하기 때문이다.

한 vector의 구성요소들은 vec.x로 접근되어질 수 있다. 거기에서 x는 vector의 첫 번째 요소이다. 너는 첫 번째, 두, 셋 네 번째 컴포넌트에 개별적으로 접근하기위해  .x, .y, .z 그리고 .w를 사용할 수 있다. GLSL은 또한 colors를 위해 rgba 또는 texture 좌표를 위해 stpq를 사용하는 것을 허용한다. 이것은 같은 구성요소에 접근하는 것이다.

vector datatype은 swizzling이라 불려지는 몇 몇 흥미롭고 유연한 component selection을 제공한다. Swizzling은 다음 syntax를 제공한다. :

(swizzle은 혼합주 칵테일의 뜻을 가지고 있다. 여기에서 vec의 원소를 마구잡이로 섞어서 사용가능하는 것을 의미하는 것 같다.)
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

너는 원래의 vector가 그러한 컴포넌트들을 가지는 한, (같은 유형의) 새로운 vector를 만들기 위해서, 4개까지의 문자들로된 어떤 조합이든지 사용할 수 있다. 예를들어, vec2의 .z 컴포넌트에 접근하는 것은 허용되지 않는다. 우리는 또한 다른 vector 생성자 call에 vectors들을 인자로서 넘길 수 있다. 이것은 요구되는 인자들의 수를 줄인다.

vec2 vect = vec2(0.5,0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

Vectors들은 따라서 모든 종류의 input과 output을 위해 우리가 사용할 수 있는 유연한 data type이다. 튜토리얼 전반에서, 너는 어떻게 우리가 창의적으로 vector들을 관리하는지에 대한 많은 예제들을 볼 것 이다.

In and outs
쉐이더들은 그들 스스로 돌아가는 좋고 작은 프로그램이지만, 그것들은 전체의 일부이다. 그 이유 때문에, 우리는 각각의 쉐이더들에 대해 inputs과 outputs들을 가지길 원한다. 이것은 우리가 여러 것들을 옮길 수 있기 위해서이다. GLSL은 그 목적을 위해서 특정하게 in과 out 키워드를 정의했다. 각각의 쉐이더는 그러한 키워드들을 사용하요 inputs과 outputs들을 명시할 수 있다. output 변수가 다음 쉐이더 단계의 input variable과 어디에서 매치되던 간에, 그것들은 넘겨진다. 그런데, The vertex and fragment shader는 조금 다르다.

vertex shader는 몇몇의 input 형태를 받아야만 한다. 그렇지 않다면, 그것은 꽤 비효과적이다. vertex shader는 그것의 input에서 다르다. 그것이 그것의 input을 vertex data로부터 직접 받는다는 점에서. vertex data가 어떻게 구성되어있는지를 정의하기 위해서, 우리는 input 변수를 location metadata와함께 명시한다. 그래서 우리는 CPU에 vertex attributes를 설정할 수 있다. 우리는 이전 튜토리얼에서 ,layout (location = 0)이라는 것을 보았다. 그 vertex shader는 따라서 그것의 inputs에 대한 extra layout specification을 요구한다. 그래서 우리는 그것을 vertex data와 연결할 수 있다.

Green Box
layout (location = 0)이라는 명시자를 빼고, glGetAttribLocation을 통해서 너의 OpenGL 코드에서 attribute location들을 query하는 것이 가능하다. 그러나 나는 그것들을 vertex shader에 설정하는 것을 선호한다. 이해하기에 더 쉽고, 너에게 (그리고 OpenGL)에게 몇 가지 작업을 줄인다.

다른 예외는 fragment shader는 vec4 color output 변수를 요구한다는 것이다. 왜냐하면 fragment shader들은 최종 output color를 만들 필요가 있기 때문이다. 만약 너의 fragment shader에서 output color를 명시하는데 실패한다면, OpenGL은 너의 객체를 검정색 (또는 하얀색)으로 렌더링할 것이다.

그래서, 만약 너가 한 쉐이더로부터 다른 것으로 데이터를 보내길 원한다면, 우리는 보내는 shader에서 output을 선언해야만하고, 받는 shader에서 비슷한 input을 선언해야 한다. 타입과, 이름이 두 sides에서 동일할 때, OpenGL은 그러한 변수들을 함께 연결할 것이고, 그리고나서 쉐이더간에 데이터를 보내는 것이 가능하다. (이것은 프로그램 객체를 link할 때 된다.) 너에게 어떻게 이것이 실제로 작동하는지를 보여주기 위해서, 우리는 vertex shader가 fragment shader를 위한 color를 결정하도록 하기 위해, 이전 튜토리얼에서 쉐이더를 변경할 것이다.

* Vertex shader
#version 330 core
layout (location = 0) in vec3 aPos; // the poisition variable has attribute position 0
out vec4 vertexColor; // specify a color output o the fragment shader

void main()
{
gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark red color
}

* Fragment shader
#version 330 core
out vec4 FragColor;

in vec4 Vertex Color; // the input variable from the vertex shader (same name and same type)

void main()
{
FragColor = vertexColor;
}

너는 우리가 vertex shader에서 설정한 vertexColor 변수를 vec4 output으로서 선언한 것을 볼 수 있다. 그리고 우리는 비슷한 vertexColor input을 fragment shader에서 선언했다. 그것들이 둘다 같은 type과 name을 갖기 때문에, fragment shader에 있는 vertexColor는 vertex shader에 있는 vertexColor로 연결된다. 우리는 vertex shader에서 색을 dark-red color로 설정했기 때문에, 그 결과 fragments들은 또한 drak-red가 되어야 한다. 다음 이미지는 그 결과를 보여준다. :

 

해냈다! 우리는 vertex shader에서 fragment shader로 값을 마침내 보냈다. 그것을 좀 더 요리해보자, 그리고 우리가 우리의 프로그램에서 fragment shader로 값을 보낼 수 있는지를 봐보자.

Uniforms
Uniforms는 CPU에 있는 우리의 프로그램에서 GPU에 있는 쉐이더들에게 데이터를 전송하는 또 다른 방법이다. 그러나 uniforms는 다소 vertex attributes와 비교하자면 다르다. 첫 째로, uniforms은 global이다. Global은 uniform 변수가 쉐이더 프로그램 객체마다 한 개의 uniform 변수가 유일하다는 것을 의미하고, 쉐이더 프로그램에서 어떤 단계에 있는 어떤 쉐이더로부터 접근되어질 수 있다. 둘 째로, 너가 uniform value를 무엇으로 설정하든, uniforms는 그것들이 reset되거나 또는 업데이트될 때 까지 그들의 값을 보존할 것이다.

GLSL에서 uniform을 선언하기 위해서, 우리는 간단히 type과 이름과함께 한 쉐이더에 uniform keyword를 추가한다.

#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // we set this variable in the OpenGL code.

void main()
{
FragColor = ourColor;
}

우리는 한 uniform vec4 ourColor를 fragment shader에서 선언했다. 그리고 그 fragment의 output color를 이 uniform value의 content로 설정했다. uniforms들은 전역 변수들이기 때문에, 우리는 우리가 하고싶은 어느 쉐이더에서 든지 그것들을 정의할 수 있다. 그래서 fragment shader에 대해 어떤 것을 얻기위해 다시 vertex shader를 거칠 필요가 없다. 우리는 vertex shader에서 이 uniform을 사용하지 않을 것이다. 그래서 그것을 거기에 정의할 필요가 없다.

Red Box
만약 너가 너의 GLSL 코드 어디에도 사용되지 않는 uniform을 선언한다면, 그 컴파일러는 조용히 컴파일된 버전에서 그 변수를 제거할 것이다. 이것은 몇 가지 좌절적인 에러들의 원인이다. 명심해라.

이 uniform은 현제 비어있다; 우리는 그 uniform에 아직 어떤 데이터도 추가하지 않았다. 그래서 시도해보자. 우리는 처음에 우리의 shader에서 uniform attribute의 index/location을 찾을 필요가 있다. 일단 우리가 uniform의 index/location을 가지고 있다면, 우리는 그것의 값을 업데이트할 수 있다. fragment shader에 단일의 색을 전달하는 것 대신에, 점차적으로 시간에 따라 색을 바꿔서 요리해보자.

float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

첫 번째로, 우리는 glfwGetTime()으로 구동 시간을 초로 받는다. 그리고나서 우리는 sin 함수를 사용하여 0.0 ~ 1.0 사이의 범위의 색을 변형시키고, 그 결과를 greenValue에 저장한다.

그런후에, 우리는 glGetUniformLocation을 사용하여 ourColor uniform의 위치를 쿼리한다. 우리는 그 query 함수에 shader program과 uniform의 이름을 넣는다. (그 uniform의 이름은 우리가 위치를 받고자 하는 것이다.) 만약 glGetUniformLocation이 -1을 반환한다면, 그것은 location을 발견할 수가 없는 것이다. 마지막으로 우리는 glUniform4f 함수를 이용하여 uniform value를 설정할 수 있다. uniform location을 찾는 것은 너가 shader program을 처음에 사용하도록 하는 것을 요구하지 않지만, uniform을 업데이트하는 것은 너가 처음에 그 프로그램을 사용할 것을(glUseProgram을 사용하여) 요구한다는 것에 주목해라. 왜냐하면 그것은 현재 활성화된 쉐이더 프로그램에 uniform을 설정하기 때문이다.

OpenGL이 그것의 core에서 C 라이브러리로 되어있기 때문에, 그것은 type overloading에 대한 native support를 가지지 않는다. 그래서 한 함수가 다른 types들을 가진채 호출되는 곳이 어디든, OpenGL은 요구되는 각 타입에 대해 새로운 함수를 정의한다.; glUniform은 이것의 완벽한 예시이다. 그 함수는 너가 설정하기 원하는 uniform의 type의 특정한 postfix를 요구한다. 몇 가지 가능한 postfixes들은 다음과 같다 :

* f : the function expects a float as its value
* i : the function expects an int as its value
* ui : the function expects an unsigned int as its value
* 3f : the function expects 3 floats as its value
* fv : the function expects a float vector/array as its value

너가 OpenGL의 한 옵션을 설정하길 원할 때 마다, 간단히 너의 type과 일치하는 overloaded 함수를 골라라. 우리의 경우에 우리는 uniform의 4개의 floats들을 개별적으로 설정한다. 그래서 우리는 glUniform4f를 통해서 우리의 데이터를 넘긴다. (우리는 또한 fv version을 사용할 수 있다는 것을 주목해라)

 우리는 uniform 변수들의 값을 어떻게 설정하는지를 알게 됐으니, 렌더링을 위해 그것들을 사용할 수 있다. 만약 우리가 색이 점차 바뀌기를 원한다면, 우리는 이 uniform을 매 game loop iteration마다 업데이트 하길 원한다. (그래서 그것은 프레임 마다바뀐다.) 만약 그렇게 하지 않는다면, 그 삼각형은 한 개의 색으로 유지될 것이다. 만약 우리가 그것을 한 번만 설정한다면, 그래서 우리는 greenValue를 계산하고 그리고 매 render iteration마다 uniform을 업데이트한다.

코드는 상대적으로 이전 코드의 간단한 적용이다. 이 번에, 우리는 삼각형을 그리기전에 매 iteration마다 uniforme value를 업데이트 시킨다. 만약 너가 uniform을 정확히 업데이트 한다면, 너는 너의 삼각형의 색이 점차 초록색에서 검정색으로 다시 초록색으로 돌아가는 것을 볼 수 있을 것이다.

만약 막힌다면, 여기에서 소스코드를 참고해라

너가 보듯이, uniforms들은 렌더링 반복에서 바뀔지도 모르는 attributes들을 설정하기 위해 또는 너의 프로그램과 쉐이더사이의 데이터를 상호 교환하기 위한 유용한 도구이다. 그러나 만약 너가 각 vertex마다  색을 설정하고 싶다면 어떨까? 그러한 경우에, 우리는 우리가 가진 정점만큼의 많은 uinforms들을 선언해야만 한다. 더 좋은 해결책은 우리가 할 것인 vertex attributes들에 좀 더 많은 데이터를 포함시키는 것이다.

More attributes!
우리는 이전 튜토리얼에서 어떻게 VBO를 채우고, vertex attribute pointer들을 설정하고 그것을 VAO안에 모두 저장하는지를 보았다. 이번에, 우리는 또한 vertex data에 color data를 추가하길 원한다. 우리는 color data를 3개의 float으로서 vertices array에 추가할 것이다. 우리는 RGB color를 우리의 삼각형의 마지막에 각각 개별적으로 할당한다.

float vertices[] = 
{
// positions         // colors
0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // bottom right
-0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // bottom left
0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // top 
};

우리는 지금 vertex shader에 보낼 더 많은 데이터를 가지고 있기 때문에, 우리의 color 값을 vertex attribute input으로서 받기 위해 vertex shader를 조정할 필요가 있다. 우리는 aColor의 로케이션을 layout specifier로 1로 설정했다는 것에 주목해라.

#version 330 core
layout (location = 0) in vec3 aPos; // the position variable has attribute position 0
layout (location = 1) in vec3 aColor; // the color variable has attribute position 1

out vec3 ourColor; // output a color to the fragment shader

void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // set ourColor to the input color we got from the vertex data
}

우리는 더 이상 fragment의 컬러에 대해 uniform을 사용하지 않기 때문에, 지금은 ourColor output 변수를 사용하기 떄문에, 우리는 fragment shader 또한 바꿔야만 할 것이다.

#version 330 core
out vec4 FragColor;
in vec3 ourColor;

void main()
{
FragColor = vec4(ourColor, 1.0);
}

우리는 다른 vertex attribute를 추가했고, VBO의 메모리를 업데이트 시켰기 때문에, 우리는 vertex attribute pointer들을 재 설정해야만 한다. VBO의 메모리에 있는 업데이트 된 데이터는 지금 이것같이 보인다:

현재의 layout을 알고나서, 우리는 glVertexAttribPointer로 vertex format을 업데이트 시킬 수 있다:

// position attribute
glVetexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

glVertexAttribPointer의 인자들을 상대적으로 간단하다. 이번에 우리는 vertex attribute를 attribute location에서 1로 설정하고 있다. color 값들은 3 floats들의 사이즈를 가지고 있고, 우리는 그 값들을 표준화 하지 않는다.

우리는 이제 두 개의 vertex attributes들을 가지고 있기 때문에, 우리는 그 stride value를 다시 계산해야만 한다. data array에서 다음 attribute value를 얻기 위해 (예를들어, position vector의 다음 x component) 우리는 오른쪽으로 6개의 floats들을 움직여야만 한다. 3개는 position value들 그리고 3개는 color value들을 위해서. 이것은 우리에게 바이트로 float 크기의 6배를 한 stride 값을 준다. (24 bytes). 또한, 이 번에 우리는 offset을 설정해야만 한다. 각각의 vertex에 대해, position vertex attriute는 처음에 우리가 선언하기로 0의 offset이다. color attribute는 position data가 끝난 후에 시작한다. 그래서 offset은 바이트단위로 3*sizeof(float)이다. (= 12bytes).

프로그램을 작동시키면 다음의 이미지를 만들 것이다.:

 

너가 막혔다면, 소스코드를 여기에서 확인해보아라.

이미지는 정확히 너가 기대한 것이 아닐 것이다. 왜냐하면 우리는 우리가 지금 보고있는 거대한 color 팔레트가 아닌 오직 3개의 색들을 공급했기 떄문이다. 이것은 소위 fragment shader에서 fragment interpolation이라고 불려지는 것의 결과이다. 삼각형을 렌더링할 때, rasterization 단계는 보통 원래 명시된 정점들보다 좀 더 많은 fragments들을 만들어낸다. 그 rasterizer는 그런 후에, 그것들이 triangle shape에서 어디에 있는지를 기준으로 그러한 fragments들의 각각의 위치를 결정한다.
이러한 위치에 기반으로 하여, fragment shader는 모든 fragment shader의 input 변수들을 interpolate한다. 예를들어 우리가 위 점에서 초록색, 아래점에서 파란색인 직선을 가졌다고 해보자. 만약 fragment shader가 그 직선의 70%지점의 위치에 있는 fragment에서 구동된다면, 그것의 결과 color input attribute는 그런후에 green과 blue의 linear combination이 될 것이다. 좀 더 정확히해서 30% 파란색으로 70% 초록색으로.

이것은 정확히 삼각형에서 일어났던 것이다. 우리는 3개의 정점들과 따라서 3개의 값들을 가지고있다. 삼각형들의 픽셀들로부터 판단하여, 그것은 아마도 약 5만개의 fragments들을 포함한다. 그리고 거기에서 fragment shader 그러한 픽셀들 사이에서 값들을 interpolate시켰다. 만약 너가 색들을 잘 본다면, 너는 모든 것들이 이해 될 것이다: 빨강에서 파란색으로 갈 떄 처음에 보라색이되고 그 후에 파란색이 된다. Fragment interpolation은 모든 fragment shader의 input attribute들에 적용된다.

Our own shader class
쉐이더들을 작성하고, 컴파일하고 관리하는 것은 꽤 번거로울 수 있다. 쉐이더 주제에 대한 마지막 터치로서, 우리는 디스크로부터 쉐이더들을 읽어와 그것들을 컴파일하고 링크하고 에러들을 체크하고 사용하기에 쉬운 shader class를 만들어 삶을 좀 더 쉽게 만들 것이다.  이것은 또한 너에게 어떻게 우리가 지금까지 배웠던 몇몇 지식을 유용한 추상적인 객체로 캡슐화할 수 있는지에 대한 아이디어를 준다.

우리는 header file에 전적으로 쉐이더 클래스를 만들 것이다. 주로 배우는 목적과 휴대성을 위해. 요구되는 include와 class 구조를 정의하여 시작해보자.

#ifndef OPENGL_SHADER_H
#define OPENGL_SHADER_H

#include <glad/glad.h> // include glad to get all the required OpenGL headers

#include <string>
#inlcude <fstream>
#include <sstream>
#include <iostream>

class Shader
{
public:
// the porgram ID
unsigned int ID;

// constructor reads and builds the shader
Shader(const GLchar* vertexPath, const GLchar* fragmentPath);

// use/activate the shader
void use();

// utility uniform functions
void setBool(const std::string& name, bool value) const;
void setInt(const std::string& name, int value) const;
void setFloat(const std::string& name, float value) const;
};

#endif


Green Box
우리는 헤더파일의 맨 위에 몇 가지 전처리 지시를 사용했다. 코드의 이러한 작은 라인들을 사용하는 것은 너의 컴파일러가 이 헤더파일을 포함하여 컴파일 할지를 알려준다. 만약 그것이 아직 included 되지 않았따면, 비록 많은 파일들이 그 shader header를 include할 지라도. 이것은 linking conflict들을 방지한다.

shader 클래스는 쉐이더 프로그램의 ID를 가지고 있따. 그것의 생성자는 간단한 텍스트 파일로서 디스크에 저장할 수 있는 vertex, fragment shader의 소스코드의 파일 주소를  개별적으로 요구한다. 더해서, 우리는 또한 몇 가지 우리의 삶을 조금 편하게 하기 위해 몇 가지 utility 함수들을 추가 했다. : use는 쉐이더프로그램을 활성화시키고 모든 set.. 함수들은 uniform location을 쿼리하고 그것의 값을 설정한다.

Reading from file
우리는 C++ 파일스트림들을 파일의 내용들을 몇 가지 string 객체들로 읽어오기 위해 사용할 것이다.

Shader::Shader(const char* vertexPath, const char* fragmentPath)
{
// 1. retrieve the vertex/fragment source code from filePath
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;

// Ensure ifstream objects can throw exceptions:
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);

try
{
// open files
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);

std::stringstream vShaderStream, fShaderStream;

// read file's buffer contents into streams
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();

// close file handlers
vShaderFile.close();
fShaderFile.close();

// convert stream into string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch (std::ifstream::failure e)
{
std::cerr << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ\n";
}

const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
}

다음으로, 우리는 쉐이더들을 컴파일하고 link할 필요가 있다. 우리가 또한 컴파일/링킹이 실패했느지를 보고있고, 만약 그렇다면 디버깅 할 때(너는 결국 error logs들들이 필요하게 될 것이다.) 매우 유용한 compile-time error들을 프린트 하도록 하고 있다는 것을 주목해라.

// 2. compile shaders
unsigned int vertex, fragment;
int success;
char infoLog[512];

// vertex Shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);

// print compile errors if any
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cerr << "ERROR::SHADER::VERTEX::COMPILATION_FAILE\n" << infoLog << '\n';
}

// fragment shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);

// print compile errors if any
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragment, 512, NULL, infoLog);
std::cerr << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILE\n" << infoLog << '\n';
}

// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);

// print linking errors if any
glGetShaderiv(ID, GL_LINK_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(ID, 512, NULL, infoLog);
std::cerr << "ERROR::SHADER::PROGRAM::LINKIN_FAILED\n" << infoLog << '\n';
}

// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);

use 함수는 간단하다:

void use()
{
glUseProgram(ID);
}

유사하게 세터 함수들도 비슷하다.

void Shader::setBool(const std::string& name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}

void Shader::setInt(const std::string& name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}

void Shader::setFloat(const std::string& name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}

그리고 완전한 쉐이더 클래스를 갖추었다. 쉐이더 클래스를 사용하는 것은 꽤 쉽다. 우리는 쉐이더 객체를 한 번 만들고 그 때부터 간단핳게 그것을 사용하기 시작한다.

Shader ourShader("~", ",");
...
while(...)
{
ourShader.use();
ourShader.setfloat("someUniform", 1.0f);
DrawStuff();
}

여기에서 우리는 shader.vs와 shader.fs라고 불려지는 vertex와 fragment shader source code를 두 개의 파일에 저장했다. 자유롭게 너의 쉐이더 파일을의 이름 지을 수 있다. 너가 좋아하는대로; 나는 개인적으로 .vs와 .fs 확장자명이 꽤 직관적이라고 생각한다.

너는 여기에서 우리가 새롭게 만든 shader class를 사용하는 소스코드를 볼 수 있 다. 너는 쉐이더 파일의 주소를 각 쉐이더 소스코드를 찾기위해 클릭할 수 있다는 것을 주목해라.

float greenchange[4] = { 0.f, 0.f, 0.f, 1.f };
// Render Loop
while (!glfwWindowShouldClose(window))
{
// Input
processInput(window);

glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// draw our first triangle
shader1.use();
glBindVertexArray(VAO[0]); 
glDrawArrays(GL_TRIANGLES, 0, 3);

float timeValue = glfwGetTime();
float greenValue = sin(timeValue) / 2.0f + 0.5f;
greenchange[1] = greenValue;
shader2.use();
shader2.setFloat4("ourColor", greenchange);
glBindVertexArray(VAO[1]);
glDrawArrays(GL_TRIANGLES, 0, 3);

// check and call events and swap the buffers.
glfwPollEvents(); 
glfwSwapBuffers(window);
}


Exercises
1. 삼각형이 거꾸로 되도록 vertex shader를 바꾸어라.

 


* upsidedown.vs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 vertexColor;

void main()
{
gl_Position = vec4(aPos.x * -1, aPos.y* -1, aPos.z, 1.0);
vertexColor = aColor;
}

2. uniform을 통해 수평 지점을 명시하고 이 offset value를 사용하여 vertex shader에서 스크린의 오른쪽으로 삼각형을 움직여라

나는 offset을 while문에서 바꾸어 계속 왔다갔다 하게 만들었다.
* rightmove.vs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

uniform float offset;

out vec3 vertexColor;

void main()
{
gl_Position = vec4(aPos.x + offset, aPos.yz, 1.0);
vertexColor = aColor;
}

* render loop
float blueChange[4] = { 0.f,0.f,0.f,1.f };
// Render Loop
while (!glfwWindowShouldClose(window))
{
// Input
processInput(window);

glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// draw our first triangle
float horizon = sin(glfwGetTime()) / 2.f + 0.5f;
shader1.use();
shader1.setFloat("offset", horizon);
glBindVertexArray(VAO[0]); 
glDrawArrays(GL_TRIANGLES, 0, 3);

blueChange[2] = sin(glfwGetTime()) / 2.f + 0.5f;
shader2.use();
shader2.setFloat4("ourColor", blueChange);
glBindVertexArray(VAO[1]);
glDrawArrays(GL_TRIANGLES, 0, 3);

// check and call events and swap the buffers.
glfwPollEvents(); 
glfwSwapBuffers(window);
}
 

3. out keyword를 사용하여 vertex position을 fragment shader로 output시키고, fragment의 값을 이 vertex position과 동일하게 설정하라. (심지어 vertex position 값들도 삼각형 전반에 어떻게 interpolated 되는지 보아라). 너가 이것을 해낸다면; 다음 질문에 답하도록 해봐라: 우리의 삼각형의 왼쪽 하단이 왜 검은색인가?

 
* samevertexfrag.vs // fs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 vertexColor;

void main()
{
gl_Position = vec4(aPos, 1.0);
vertexColor = aPos ;
}

#version 330 core

out vec4 FragColor;
in vec3 vertexColor;

void main()
{
    FragColor = vec4(vertexColor, 1.0);
}

* render loop
// Render Loop
while (!glfwWindowShouldClose(window))
{
// Input
processInput(window);

glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// draw our first triangle
float horizon = sin(glfwGetTime()) / 2.f + 0.5f;
shader1.use();
shader1.setFloat("offset", horizon);
glBindVertexArray(VAO[0]); 
glDrawArrays(GL_TRIANGLES, 0, 3);

shader2.use();
glBindVertexArray(VAO[1]);
glDrawArrays(GL_TRIANGLES, 0, 3);

// check and call events and swap the buffers.
glfwPollEvents(); 
glfwSwapBuffers(window);
}

만약 vertex stream 좌표값이
float secondTri[] = 
{
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};

이렇게 되어있다면 왼쪽 하단의 정점은 color 값이 마이너스가 되어버린다. 그런데 color value는 0.0 ~ 1.0이므로 0 밑으로는 검정색 처리를 하는 것 같다.

따라서 각 vertex에 따라서 color 값은 다음처럼 갖게 된다.

{
0.5f, 0.f, 0.f,
0.f, 0.f, 0.f,
0.f, 0.5f, 0.f
}

그래서 오른쪽 하단은 빨간색의 half, 위쪽은 초록색의 half, 그리고 왼쪽하단은 검정색이 되어서, 그 지점들은 그렇게 색이 지정되고, 나머지 영역은 interpolate되어진다.

댓글 없음:

댓글 쓰기