※ OpenGL을 공부하면서 중요하다고 생각되는 부분이나 개인적으로 느끼는 점을 적는 공간이다
그래픽스를 공부하려면 그래픽스 파이프라인에 대해서 잘 알아야한다. 이전 포스팅에서 vertex shader 및 fragment shader를 이용하여 화면에 점을 찍어보았다.
Vertex shader
vertex shader 실행 전에 vertex fetching 혹은 vertex pulling으로 불리는 고정 함수 스테이지가 실행되어 자동으로 vertex shader에 입력을 제공한다.
Vertex attribute
GLSL에서 쉐이더로 in과 out 지시어를 통해 주고받고 할 수 있다.
vertex shader로 in 입력할 때, 저장 지시어를 통해 자동적으로 vertex fetch 스테이지에 의해 채워지는 변수를 vertex attribute라고 한다.
.. 글로만 보면 모르겠으니 코드를 봐야할 것 같다.
// eg3-1.cpp
#version 450 core
// 'offset' is an input vertex attribute
layout(location = 0) in vec4 offset;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4(0.25, 0.25, 0.5, 1.0));
// Add 'offset' to our hard-coded vertex position
gl_Position = vertices[gl_VertexID] + offset;
}
위 코드에서 layout으로 되어있는 부분이 입력 vertex attribute라고 한다.
이 내용들은 glVertexAttrib로 시작하는 이름의 함수로 내용을 채운다.
void glVertexAttrib4fv(GLuint index, const GLfloat * v
대표적으로 glVertexAttrib4fv의 형식은 위와 같다.
index는 인자의 속성을 참조. 즉 위 코드에서 layout으로 설정한 값을 칭한다. 이것이 layout 지시어로 vertex attribute의 location을 0으로 설정한다.
v는 attribute에 넣을 데이터이다.
위 glVertexAttrib 함수를 통해 vertex shader에 전달하는 attribute값을 갱신하기에 애니메이션을 만들 수 도 있다고 한다.
// eg3-2.cpp
#include <sb6.h>
#include <math.h>
class simpleclear_app : public sb6::application
{
public:
void init()
{
static const char title[] = "OpenGL SuperBible - moving triangle";
sb6::application::init();
memcpy(info.title, title, sizeof(title));
}
void startup()
{
static const char* vs_source[] =
{
"#version 410 core \n"
" \n"
"layout (location = 0) in vec4 offset; \n"
" \n"
"void main(void) \n"
"{ \n"
" const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0), \n"
" vec4(-0.25, -0.25, 0.5, 1.0), \n"
" vec4( 0.25, 0.25, 0.5, 1.0)); \n"
" \n"
" // Add 'offset' to our hard-coded vertex position \n"
" gl_Position = vertices[gl_VertexID] + offset; \n"
"} \n"
};
static const char* fs_source[] =
{
"#version 410 core \n"
" \n"
"out vec4 color; \n"
" \n"
"void main(void) \n"
"{ \n"
" color = vec4(0.0, 0.8, 1.0, 1.0); \n"
"} \n"
};
program = glCreateProgram();
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, fs_source, NULL);
glCompileShader(fs);
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, vs_source, NULL);
glCompileShader(vs);
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glGenVertexArrays(1, &vertex_array_object);
glBindVertexArray(vertex_array_object);
}
// Our rendering function
virtual void render(double currentTime)
{
const GLfloat color[] = { (float)sin(currentTime) * 0.5f + 0.5f,
(float)cos(currentTime) * 0.5f + 0.5f,
0.0f, 1.0f };
glClearBufferfv(GL_COLOR, 0, color);
// Use the program object we created earlier for rendering
glUseProgram(program);
GLfloat attrib[] = { (float)sin(currentTime) * 0.5f,
(float)cos(currentTime) * 0.6f,
0.0f, 0.0f };
// Update the value of input attribute 0
glVertexAttrib4fv(0, attrib);
// Draw one triangle
glDrawArrays(GL_TRIANGLES, 0, 3);
}
void shutdown()
{
glDeleteVertexArrays(1, &vertex_array_object);
glDeleteProgram(program);
}
private:
GLuint vertex_shader;
GLuint fragment_shader;
GLuint program;
GLuint vertex_array_object;
};
DECLARE_MAIN(simpleclear_app);
이전에 다루었던 삼각형 rendering 소스에서 정말 layout 지시어와 offset부분만을 변경하니 삼각형이 빙글빙글 돈다.
GLchar는 typedef로 정의한 GL에서 사용할 char이기 때문에 위에서 사용한 것처럼 그냥 char로 대체하여로 된다.
아마 호환성을 위해 라이브러리 type으로 분리한 것이 아닌가 싶다.
(github 소스코드에서 해당 예제는 movingtri project이다)
Passing Data from Stage to Stage
다른 shader 스테이지로 in / out 키워드를 이용해서 데이터를 보낼 수 있다.
// eg3-3.cpp
#version 450 core
// 'offset' and 'color' are input vertex attributes
layout(location = 0) in vec4 offset;
layout(location = 1) in vec4 color;
// 'vs_color' is an output that will be sent to the next shader stage
out vec4 vs_color;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4(0.25, 0.25, 0.5, 1.0));
// Add 'offset' to our hard-coded vertex position
gl_Position = vertices[gl_VertexID] + offset;
// Output a fixed value for vs_color
vs_color = color;
}
위 코드에서는 vertex shader코드이다.
layout location이 1인 color라는 입력을 선언하였다. 이를 vs_color라는 output 변수에 저장한 것이다.
다음으로 오는 코드의 fragment shader에서 이 값을 받아 사용할 것이다.
// eg3-4.cpp
#version 450 core
// Input from the vertex shader
in vec4 vs_color;
// Output to the framebuffer
out vec4 color;
void main(void)
{
// Simply assign the color we were given by the vertex shader to our
output
color = vs_color;
}
vertex에서 위 코드인 fragment shader로 color 데이터를 전달한 것이다.
...
사실 in / out 키워드에 대해서 헷갈렸는데, in 키워드를 사용하기 전까지는 fragment shader 코드의 main함수 위에 out vec4만으로 선언되어있는 것을 볼 수 있다.
그래픽스 파이프라인의 순서가 vertex shader이후 일련의 과정을 거치고 fragment shader로 전달되기 때문에, in으로 전달받는 것은 vertex로부터 fragment shader로의 전달만이 가능하다.
...
Interface Blocks
인터페이스 블록이 무엇이냐? shader stage간의 데이터 전송을 위한 도구이다. 다양한 종류의 데이터가 하나의 interface block으로 그룹화되어 전달하는데, 이는 마치 struct와 비슷한 느낌이다.
또한 in /out 키워드를 이용해서 입출력을 구분해 선언한다.
// eg3-5.cpp
#version 450 core
// 'offset' is an input vertex attribute
layout(location = 0) in vec4 offset;
layout(location = 1) in vec4 color;
// Declare VS_OUT as an output interface block
out VS_OUT
{
vec4 color; // Send color to the next stage
} vs_out;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4(0.25, 0.25, 0.5, 1.0));
// Add 'offset' to our hard-coded vertex position
gl_Position = vertices[gl_VertexID] + offset;
// Output a fixed value for vs_color
vs_out.color = color;
}
VS_OUT은 interface block이고, vs_out은 interface block의 인스턴스이다.
// eg3-6.cpp
#version 450 core
// Declare VS_OUT as an input interface block
in VS_OUT
{
vec4 color; // Send color to the next stage
} fs_in;
// Output to the framebuffer
out vec4 color;
void main(void)
{
// Simply assign the color we were given by the vertex shader to
our output
color = fs_in.color;
}
in으로 interface block을 선언하고, 인스턴스 이름을 fs_in으로 하였다.
interface block이름으로 shader간 block을 매칭시키고, 인스턴스 이름은 각 shader마다 다를 수 있다.
Tessellation
조각화라는 의미를 가진 tessellation은 primitive를 더 작고 단순하고 렌더링 가능하게 분할하는 작업이라고 한다.
그런데 primitive가 뭔지 싶어 보니 그래픽의 입출력에서 더 분할할 수 없는 기본이 되는 그래픽 쪼가리라고 한다.
Tessellation.. 분할할 수 없는 것을 분할하는 작업...
그래픽 파이프라인에서 테셀레이션은 vertex shader와 fragment shader의 중간즈음에 있었다.
이러한 테셀레이션은 세 단계인 테셀레이션 컨트롤 쉐이더, 고정 함수 테셀레이션 엔진, 테셀레이션 이벨류에이션 쉐이더 로 구분된다.
Tessellation Control Shaders
줄여서 TCS라고도 부르는데, vertex shader로부터 입력을 받고 테셀레이션 엔진에 보낼 레벨을 결정하고, 테셀레이션 수행 다음 테셀레이션 이벨류에이션 쉐이더에 보낼 데이터를 생성한다.
테셀레이션은 patches라고 하는 것을 점, 선, 삼각형 등으로 분할하는데, patch는 여러 control point로 구성된다.
void glPatchParameteri(GLenum pname, GLint value);
glPatchParameteri()함수의 pname인자를 GL_PATCH_VERTICES로 하고, value를 control point개수로 호출하면 control point를 변경할 수 있다.
기본적으로는 control point는 3개이다. point별로 vertex shader는 한 번씩 수행되고, tessellation control shader는 control point의 그룹에 대해 배치로 수행된다고한다.
control shader가 만드는 control point의 개수는 control shader 코드에서 다음과 같은 출력 layout지시어로 정한다.
layout (vertices = N) out;
N은 patch당 control point개수이다.
출력 테셀레이션 인자는 gl_TessLevelInner와 gl_TessLevelOuter 내장 출력 변수로 저장된다고 한다. 그외 파이프라인으로 내려가는 다른 데이터는 일반적인 경우처럼 out이나 gl_out 등의 사용자 지정 출력변수로 저장된다.
// eg3-7
#version 450 core
layout (vertices = 3) out;
void main(void)
{
// Only if I am invocation 0 ...
if (gl_InvocationID == 0)
{
gl_TessLevelInner[0] = 5.0;
gl_TessLevelOuter[0] = 5.0;
gl_TessLevelOuter[1] = 5.0;
gl_TessLevelOuter[2] = 5.0;
}
// Everybody copies their input to their output
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}
테셀레이션 컨트롤 쉐이더의 예제 코드이다.
layout(vertices = 3) out;으로 출력 control point를 default와 같은 3으로 설정하였고,
gl_out, gl_in으로 입력을 출력으로 복사한다.
gl_TessLevel을 통해 내부/외부 테셀레이션 레벨을 5로 설정한다.
gl_InvocationID는 gl_in, gl_out 배열의 인덱스로 사용되는데, 이는 테셀레이션 컨트롤 쉐이더의 현재 호출에 의해 처리되는 patch 내 control point에 대한 0기반 인덱스다.
...
라고는 하는데 무슨 말인지 잘 모르겠다.
...
The Tessellation Engine
테셀레이션 엔진이 OpenGL 파이프라인의 고정 함수이고, patch로 표현되는 고차원 surfaces를 작은 primitive(점, 선, 삼각형 등)로 분할하는 역할을 한다. 테셀레이션 엔진이 patches를 받기 전에 테셀레이션 컨트롤 쉐이더가 입력된 control point들을 처리하고, patch 분할을 위한 테셀레이션 인자를 설정한다.
테셀레이션 엔진이 출력 primitive를 생성한 뒤, 해당 vertex들은 테셀레이션 이벨류에이션 쉐이더로 전달된다. 테셀레이션 엔진은 테셀레이션 이벨류에이션 쉐이더의 호출에 사용될 인자를 생성하는 역할을 수행한다. 테셀레이션 이벨류에이션 쉐이더는 primitive를 변환하여 래스터라이제이션 가능한 상태로 만든다.
Tessellation Evaluation Shaders
고정 함수인 테셀레이션 엔진이 수행되면 primitive에 대한 출력 vertex가 생성된다. 이 vertex들은 테셀레이션 이벨류에이션 쉐이더로 전달된다. Tessellation Evaluation Shaders(TES)는 생성된 각 vertex에 대해 한 번씩 호출을 수행한다. 테셀레이션 레벨이 높으면 Tessellation Evaluation Shader가 많이 수행되므로, 복잡한 Tessellation Evaluation Shader 및 높은 Tessellation 레벨을 사용하려면 성능이 상당히 필요하다.
// eg3-8
#version 450 core
layout(triangles, equal_spacing, cw) in;
void main(void)
{
gl_Position = (gl_TessCoord.x * gl_in[0].gl_Position +
gl_TessCoord.y * gl_in[1].gl_Position +
gl_TessCoord.z * gl_in[2].gl_Position);
}
위 코드는 eg3-7의 control shader를 실행한 결과로 테셀레이터에 의해서 생성된 입력 vertex들을 받아 사용하는 테셀레이션 이벨류에이션 쉐이더 내용이다.
layout 지시어를 통해서 테셀레이션 모드를 설정한다. 위 코드에서는 삼각형을 사용하는 모드를 선택한다.
equal_spacing과 cw는 테셀레이트된 폴리곤 가장자리를 따라 '동일한 간격'으로 버텍스들이 생성되도록 하는 옵션과 시계 방향 버텍스 감기 순서(clock wise)로 삼각형을 생성하도록 하는 옵션이다. 이외 옵션들도 있지만 현재는 여기까지만 다루도록 한다.
main함수 안에서는 vertex shader와 같이 gl_Position에 값을 저장한다. gl_TessCoord는 tessellater가 생성한 vertex의 뭉게중심 좌표이고, gl_in[] 구조체 배열의 멤버 gl_Position을 사용하였다.
gl_in은 위에서 다루었던 Tessellation Control Shaders의 gl_out 구조체와 매칭되는 값이다.
위 shader는 통과 테셀레이션이기 때문에 출력 patch는 원본 입력 삼각형 patch와 동일한 모양이다.
void glPolygonMode(GLenum face, GLenum mode);
테셀레이션의 결과를 보기 위해서는 glPolygonMode()를 호출하여 OpenGL이 결과의 가장자리만 그리도록 한다.
face인자는 어떤 타입의 폴리곤인지를 결정한다. 모두 적용하려면 GL_FRONT_AND_BACK으로 설정하면 된다.
mode는 폴리곤이 그려지는 방법이다. wireframe 모드로 그리기 위해서는 GL_LINE으로 설정하면 된다.
#include <sb6.h>
class tessellatedtri_app : public sb6::application
{
void init()
{
static const char title[] = "OpenGL SuperBible - Tessellated Triangle";
sb6::application::init();
memcpy(info.title, title, sizeof(title));
}
virtual void startup()
{
static const char * vs_source[] =
{
"#version 410 core \n"
" \n"
"void main(void) \n"
"{ \n"
" const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0), \n"
" vec4(-0.25, -0.25, 0.5, 1.0), \n"
" vec4( 0.25, 0.25, 0.5, 1.0)); \n"
" \n"
" gl_Position = vertices[gl_VertexID]; \n"
"} \n"
};
static const char * tcs_source[] =
{
"#version 410 core \n"
" \n"
"layout (vertices = 3) out; \n"
" \n"
"void main(void) \n"
"{ \n"
" if (gl_InvocationID == 0) \n"
" { \n"
" gl_TessLevelInner[0] = 5.0; \n"
" gl_TessLevelOuter[0] = 5.0; \n"
" gl_TessLevelOuter[1] = 5.0; \n"
" gl_TessLevelOuter[2] = 5.0; \n"
" } \n"
" gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position; \n"
"} \n"
};
static const char * tes_source[] =
{
"#version 410 core \n"
" \n"
"layout (triangles, equal_spacing, cw) in; \n"
" \n"
"void main(void) \n"
"{ \n"
" gl_Position = (gl_TessCoord.x * gl_in[0].gl_Position) + \n"
" (gl_TessCoord.y * gl_in[1].gl_Position) + \n"
" (gl_TessCoord.z * gl_in[2].gl_Position); \n"
"} \n"
};
static const char * fs_source[] =
{
"#version 410 core \n"
" \n"
"out vec4 color; \n"
" \n"
"void main(void) \n"
"{ \n"
" color = vec4(0.0, 0.8, 1.0, 1.0); \n"
"} \n"
};
program = glCreateProgram();
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, vs_source, NULL);
glCompileShader(vs);
GLuint tcs = glCreateShader(GL_TESS_CONTROL_SHADER);
glShaderSource(tcs, 1, tcs_source, NULL);
glCompileShader(tcs);
GLuint tes = glCreateShader(GL_TESS_EVALUATION_SHADER);
glShaderSource(tes, 1, tes_source, NULL);
glCompileShader(tes);
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, fs_source, NULL);
glCompileShader(fs);
glAttachShader(program, vs);
glAttachShader(program, tcs);
glAttachShader(program, tes);
glAttachShader(program, fs);
glLinkProgram(program);
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
}
virtual void render(double currentTime)
{
static const GLfloat green[] = { 0.0f, 0.25f, 0.0f, 1.0f };
glClearBufferfv(GL_COLOR, 0, green);
glUseProgram(program);
glDrawArrays(GL_PATCHES, 0, 3);
}
virtual void shutdown()
{
glDeleteVertexArrays(1, &vao);
glDeleteProgram(program);
}
private:
GLuint program;
GLuint vao;
};
DECLARE_MAIN(tessellatedtri_app)
테셀레이션 쉐이더 오브젝트생성 및 attach작업이 필요하고,
glDrawArrays로 VAO를 파이프라인에 보낼 때, mode를 GL_PATCHES로 하여 patches를 그리도록 한다.
(github 소스코드에서 해당 예제는 tessellatedtri project이다)
'개발 · 컴퓨터공학' 카테고리의 다른 글
Learning Unreal 4 언리얼 Tip - 오브젝트 복사, 오브젝트 시점으로 움직이기 (0) | 2022.02.23 |
---|---|
OpenGL 공부일지 - OpenGL Super Bible 그래픽스에서 중요한 파이프라인 2 (0) | 2022.02.22 |
Learning Unreal 4 언리얼 공부일지 - FOV(Filed Of View) 에 대해 (0) | 2022.02.20 |
Learning Unreal 4 언리얼 공부일지 - 레벨을 꾸미기 위한 기능 [Post Process Volume] (0) | 2022.02.19 |
Learning Unreal 4 언리얼 공부일지 - 레벨을 꾸미기 위한 기능 [Lighting, Exponential Height Fog] (0) | 2022.02.18 |