일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Tutorial
- vertexarrayobject
- shader
- 자동판결
- git
- vertex sahder
- 시작
- OpenGL
- 파일생성 명령어
- fetching
- 튜링기계
- 하나의 솔루션
- glfw
- Qtspim
- VAO
- 멀티프로세스
- 솔루션에프로젝트추가
- turingmachine
- vertex
- 정의
- fragment
- 수리명제
- multi process
- .sln
- visualstudip
- glDrawArrays
- qtspim stack
- 프로젝트 여러개
- superbible
- interface block
- Today
- Total
공사중
[SB] window에 삼각형 그리기 본문
일단은 본 내용을 눈으로 보면서 이해합니다. 실제로 실행해보는 것은 필요할 때에 언급하겠습니다.
빈 윈도우 띄워보기
#include "sb7.h" class my_application : public sb7::application { public: void render(double currentTime) { static const GLfloat red[] = { 1.0f, 0.0f, 0.0f, 1.0f }; glClearBufferfv(GL_COLOR, 0, red); } }; DECLARE_MAIN(my_application);
먼저 sb7.h를 include 합니다. 이 헤더파일은 sb7라는 namespace를 정의하고 그 안에 applicaion이라는 이름을 가지는 class를 정의합니다. 우리는 이 application 클래스를 상속받는 my_application이라는 클래스는 정의했습니다. 그리고 my_application를 DECALRE_MAIN매크로의 인스턴스에 포함시킵니다. 이 매크로는 application의 main entry point를 정의합니다. 여기서는 my_applicatino의 instance가 생성되고 run()메서드를 호출합니다. main은 run안에 정의되어있습니다.
run()함수 안에는 startup()과 render()가 정의되어있어서 run()을 호출하면 startup과 render()가 호출합니다. 전자는 초기화를 수행하는 메서드, 후자는 랜더링을 담당하는 메서드입니다. 두 메서드는 모두 가상함수로 정의가 되어있고, 파생 클래스(my_application)에서 오버라이드하여 그 안에 렌더링 코드를 작성한 형태입니다. 어플리케이션 프레임워크는 윈도우 생성, 입력 처리, 렌더링된 결과를 사용자에게 표시하는 역할을 맡습니다.
위의 예제는 아래 메서드를 사용해서 모든 윈도우를 빨간 화면으로 채웁니다.
glClearBufferfv(GLenum buffer, GLint drawBuffer, const GLfloat* value);
모든 openGL 함수는 gl로 시작하며, 해당 함수의 일부 인자 타입은 함수 이름 끝에 접미사로 줄여쓰는 등 여러 naming convention을 따릅니다. 이를 통해서 함수 오버로딩을 지워하지 않는 경우에도 제한된 형태로 오버로딩이 가능합니다.. 예를 들어 위 메서드이름에는 fv라는 접미사가 붙어있습니다. 이를 통해 이 함수가 부동소수점(floating point)를 갖는 벡터(vector)를 사용한다는 것을 알 수 있습니다. openGL에서는 배열과 벡터는 동일한 의미로 혼용됩니다.
위 메서드는 첫 번째 인자인 버퍼를 세 번째 인자의 값으로 지우라는 명령을 openGL에게 내립니다. 두 번째 인자는 지울 출력 버퍼가 여럿일 때 사용합니다. 여기서는 하나의 버퍼만 사용하기 때문에 drawBuffer는 0이라는 값을 전달합니다.(0기반 index를 사용하기 때문입니다.) red배열(벡터)는 R,G,B,Alpha에 대응하는 값으로 저장이 됩니다. Alpha는 1일 때 완전 불투명, 0일 때 완전 투명해집니다. 그래서 위 예제는 완전 불투명한 빨간색 배경을 출력합니다.
정적인 윈도우의 색을 시간에 따라 변화를 줘보겠습니다.
#include
const GLfloat color[] = { (float)sin(currentTime) * 0.5f + 0.5f, (float)cos(currentTime) * 0.5f + 0.5f, 0.0f, 1.0f};
red 대신에 시간에 따라 변하는 color라는 벡터를 저장한 것입니다. currentTime은 해당 어플리케이션이 시작한 이후로 경과 시간을 초 단위로 나타낸 값입니다. 이 값을 sin과 cos에 넣어 [-1,+1]의 값으로 변화시키켜 결과적으로 R,G,B,A값을 모두 [-1,+1]사이의 값으로 변환되도록 합니다. R,G,B,A의 값들은 모두 [-1,+1]사이의 값만 입력해야하고 이 과정을 정규화normalizing이라고 부릅니다. 버텍스 좌표가 일단 버텍스 셰이더에서 작업되기 위해서는 각각의 좌표가 normalized device coordinate된 공간에 있어야합니다. [-1,+1]이 범위 밖에 있는 좌표들은 모두 버려지거나 Cliping되어서 화면에 보여지지 않습니다. 일반적으로 사용되는 좌상단 좌표가 (0,0)인 경우와 달리 +y축과 (0,0)이 화면의 정중앙에 있습니다.
우리가 normalizaing 과정을 거친 좌표값(NDC)는 viewport transform의 과정을 통해서 screen-space coordinate로 전환이 됩니다. viewport transform에서 사용되는 데이터는 glViewport(window의 어디까지 rendering그려질지를 정하는 과정, 그림판에서 실제로 그림이 그려지는 부분과 설정 버튼이 위치하는 부분을 구분하는 과정이라고 보면 됨)이를 통해서 주어집니다. 이렇게 전환된 screen-space 좌표는 fragment shader(출력되는 pixel에 어떤 색을 입힐지 결정하는 과정)의 입력으로 주어집니다.
쉐이더 사용하기
이전 포스트에서 언급했듯이 openGL은 shader라고 불리는 작은 프로그램을 고정 함수로 연결시켜서 작동합니다. 화면에 그릴 때 GPU는 작성된 shader를 실행시키고 입려과 출력을 파이프라인으로 이동시켜 최종적으로 픽셀에 그려지게 합니다. 쉐이더는 cpp가 아닌 openGL Shading Language(GLSL)라는 것으로 작성됩니다. 이 언어는 C에 기반을 두어서 어렵지 않을 것입니다. 이 언어의 컴파일러는 openGL에 내장되어있습니다. 우리가 작성한 shader 소스코드는 쉐이더 객체로 바뀌어서 컴파일되고, 여러 쉐이더 객체들이 하나의 프로그램 객체로 link됩니다. 하나의 프로그램 객체는 여러 쉐이더 스테이지의 쉐이더를 포함할 수 있습니다.
먼저 vertex shader와 fragment shader를 작성해보겠습니다.
//vertex shader #version 430 core //GLSL version 420 corresponds to OpenGL version 4.2 for example void main() { gl_Position = vec4(0.0, 0.0, 0.5, 1.0); }
//fragment shader #version 430 core out vec4 color; void main() { color= vec4(0.0, 0.8, 1.0, 1.0); }
vertex shader는 window에 그려질 vertex들의 좌표를 지정하는 역할을 합니다. 그리고 fragment shader는 최종적인 결과값을 window에 전달하여 pixel의 색을 결정하는 역할을 합니다. fragment shdaer 코드의 out 키워드는 frament shader의 결과값을 나타냅니다. color라는 이름을 가진 vec4(4개의 구성요소 R,G,B,A를 가지는 vector type)을 윈도우 화면으로 보낸다는 뜻입니다.
쉐이더 코드를 작성했으니 사용해보겠습니다.
GLuint compile_shaders(void) { GLuint vertex_shader; GLuint fragment_shader; GLuint program; //vertex shader source code static const GLchar* vertex_shader_source[] = { "#version 430 core //GLSL version 420 corresponds to OpenGL version 4.2 for example\n" "void main()\n" "{\n" " gl_Position = vec4(0.0, 0.0, 0.5, 1.0);\n" "}\n" }; static const GLchar* fragment_shader_source[] = { "#version 430 core\n" "out vec4 color;\n" "void main()\n" "{\n" " color = vec4(0.0, 0.8, 1.0, 1.0);\n" "}\n" }; //버텍스 쉐이더를 생성하고 컴파일 vertex_shader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex_shader, 1, vertex_shader_source, NULL); glCompileShader(vertex_shader); //프래그먼트 쉐이더를 생성하고 컴파일 fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment_shader, 1, fragment_shader_source, NULL); glCompileShader(fragment_shader); //프로그램을 생성하고 쉐이더를 attach시키고 link program = glCreateProgram(); glAttachShader(program, vertex_shader); glAttachShader(program, fragment_shader); glLinkProgram(program); //이제 프로그램이 쉐이더를 소유하므로 쉐이더를 삭제한다. glDeleteShader(vertex_shader); glDeleteShader(fragment_shader); return program; }
GLuint은 openGL에서 정의한 unsigned int type을 의미하고, 나머지는 메서드의 이름을 보면 어떤 역할을 대략 하는지 알 수 있습니다. 특이한 점은 shader 소스코드를 main.cpp에서 static const GLchar* type으로 저장하는 방법입니다. 쉐이더를 생성하고 컴파일하는 과정에만 조금 설명을 붙혀보겠습니다.
- glCreateShader() : 빈 쉐이더 객체를 생성합니다. 이제 소스 코드를 받아서 컴파일 하면 됩니다
- glShaderSource() : 쉐이더 소스코드를 쉐이더 객체로 전달해서 복사복은 유지합니다.
- glCompileShader() : 쉐이더 객체에 포함된 소스코드를 컴파일 합니다.
glUseProgram(shaderProgram);
이 코드 이후의 랜더링 call 함수와 glUseProgram은 이제 이 shaderProgram(과 링크된 shader파일)을 사용합니다.
vertex shader 이전의 vertex fetch stage
이전 포스트에서 openGL의 스테이지에 대해서 알아볼 때 아래와 같은 순서를 알아본 적이 있습니다.
- 버텍스 패치
- 버텍스 쉐이더
- 테셀레이션 컨트롤 쉐이더
- 테셀레이션
- 테셀레이션 이벨류에이션 쉐이더
- 지오메트리 쉐이더
- 래스터라이제이션
- 프래그먼트 쉐이더
- 프레임버퍼 동작
vertex fetch 단계에서는 vertex array object(VAO)를 생성합니다. 다시 말해서 vertex fetch stage의 결과를 vertex shader stage의 입력으로 넣기 위해서 사용되는 객체라고 생각하면 됩니다. 지금 작성한 vertex shader는 in 키워드가 없기 때문에 VAO를 만들 때 복잡한 과정을 필요하지 않습니다.
void glGenVertexArrays(GLsizei n, GLuint * arrays); void glBindVertexArray(GLuint array);
void glGenVertexArrays(1, &vertex_array_object); void glBindVertexArray(vertex_array_object);
단순히 Vertex Arrays를 만들고 context에 attach하도록 bind하는 함수를 호출하면 됩니다. context는 window가 생성될 때 같이 생성되는 것으로, openGL 내부적으로 'window와 context가 각각 생성되고 이 둘의 묶음은 하나의 handler를 통해서 다루어진다.'정도로만 우선 알고 있으면 됩니다. 이제 모든 코드르 하나의 파일에 합쳐보겠습니다.
#include "sb7.h" class my_application : public sb7::application { private: GLuint rendering_program; GLuint vertex_array_object; public: GLuint compile_shaders(void) { GLuint vertex_shader; GLuint fragment_shader; GLuint program; //vertex shader source code static const GLchar* vertex_shader_source[] = { "#version 430 core //GLSL version 420 corresponds to OpenGL version 4.2 for example\n" "void main()\n" "{\n" " gl_Position = vec4(0.0, 0.0, 0.5, 1.0);\n" "}\n" }; static const GLchar* fragment_shader_source[] = { "#version 430 core\n" "out vec4 color;\n" "void main()\n" "{\n" " color = vec4(0.0, 0.8, 1.0, 1.0);\n" "}\n" }; //버텍스 쉐이더를 생성하고 컴파일 vertex_shader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex_shader, 1, vertex_shader_source, NULL); glCompileShader(vertex_shader); //프래그먼트 쉐이더를 생성하고 컴파일 fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment_shader, 1, fragment_shader_source, NULL); glCompileShader(fragment_shader); //프로그램을 생성하고 쉐이더를 attach시키고 link program = glCreateProgram(); glAttachShader(program, vertex_shader); glAttachShader(program, fragment_shader); glLinkProgram(program); //이제 프로그램이 쉐이더를 소유하므로 쉐이더를 삭제한다. glDeleteShader(vertex_shader); glDeleteShader(fragment_shader); return program; }//compile_shaders(void) end void startup() {//초기화 작업 : shader program object생성 및 저장, VAO 생성 및 bind rendering_program = compile_shaders(); glGenVertexArrays(1, &vertex_array_object); glBindVertexArray(vertex_array_object); } void render(double currentTime) {//window를 완전 불투명 빨간색으로 칠하기 static const GLfloat red[] = { 1.0f, 0.0f, 0.0f, 1.0f }; glClearBufferfv(GL_COLOR, 0, red); } void shutdown() {//프로그램을 종료할 때 할당된 값을 직접 정리하는 코드 glDeleteVertexArrays(1, &vertex_array_object); glDeleteProgram(rendering_program); glDeleteVertexArrays(1, &vertex_array_object); } }; DECLARE_MAIN(my_application);
위 코드는 작동하긴 하지만 여전히 재미없는 빨간 window만 그립니다. 이제 첫 번째 drawing 명령을 내려보겠습니다. 가장 기초적인 점을 찍어보는 코드입니다.
void glDrawArrays(GLenum mode, GLint first, Glsizei count);
void render(double currentTime) { const GLfloat color[] = { (float)sin(currentTime) * 0.5f + 0.5, (float)cos(currentTime) * 0.5f + 0.5, 0.0f, 1.0f }; glClearBufferfv(GL_COLOR, 0, color); glUseProgram(rendering_program); glDrawArrays(GL_POINTS, 0, 1); }
첫 번째 인자는 어떤 type의 primitive를 사용할 것인지를 나타냅니다. 점, 선, 삼각형의 primitive 중 하나를 선택할 수 있습니다. 두 번째 인자는 몇 개의 점만 찍어볼 것이기 때문에 중요하지 않습니다. 0으로 둡니다. 세 번째 인자는 몇 개의 점을 찍을 것인지를 전달합니다. 소소하게 점의 크기를 키워보고 싶으면 아래의 메서드를 호출하면 됩니다.
void glPointSize(GLfloat size);
void glPointSize(40.0f);
삼각형 그리기
삼각형을 그리기 위해서는 glDrawArrays()의 첫 번째 인자 primitive로 GL_LINES나 GL_TRIANGLES를 전달할 수 있습니다. 하지만 현재의 쉐이더는 둘 이상의 vertex를 동일한 공간에 그리는 shader입니다. 둘 이상의 버텍스 하나의 좌표에 위치하면 면적이 0이 되어서 primitive가 취소됩니다. 즉, 각각의 vertex에 다른 위치를 지정해주어야합니다.
이를 위해서 GL_VertexID라는 특별한 입력을 사용합니다. 이는 해당 시점에 처리될 vertex의 인덱스를 명시합니다. glDrawArrays의 두 번째 인자로 전달된 값부터 세번째 인자로 전달된 수만큼 '그릴 수 있는 벡터(배열) : vertex shader에서 작성한 배열'에서 적절한 index를 가져오기 위해서 사용됩니다. 사용 방법은 아래와 같습니다.
#version 430 core 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)); gl_Position = vertives[gl_VertexID]; }
gl_VertexID를 사용해서 vertices배열에 저장한 세 개의 vertex 좌표에 하나씩 접근을 합니다. 그리고 이 값을 그 때 그때 gl_Position이라는 변수에 저장해서 사용합니다. 계속 말했던 것처럼 vertex shader의 결과값은 다름 stage의 입력으로 사용됩니다. 이런 과정에서 gl_Position에 저장된 값이 내부적으로 사용됩니다. gl_Position과 gl_VertexID 모두 openGL 내장변수이니까요.
vertex shader를 위와 같이 수정하고
render()함수를 아래와 같이 작성하면 됩니다.
void render(double currentTime) { const GLfloat color[] = { 0.0f, 0.2f, 0.0f, 1.0f }; glClearBufferfv(GL_COLOR, 0, color); glUseProgram(rendering_program); //0번째 인덱스부터 3개까지 그리기 : color[0], color[1], color[2] 그리기 glDrawArrays(GL_TRIANGLE, 0, 3); }
'개발 | OpenGL > OpenGL Super Bible' 카테고리의 다른 글
[SB] Tesellation Control Shader (0) | 2019.03.04 |
---|---|
[SB]Vertex fetch, Vertex shader, shader 간 데이터 전달(in/out) (0) | 2019.03.02 |
[SB] 예제 프로젝트 빌드와 실행해보기 (1) | 2019.02.27 |
[SB] 전체적인 흐름 짚어보기 (0) | 2019.02.26 |
[SB] 들어가기 (0) | 2019.02.25 |