* Computer Graphics Programming in OpenGL with C++ 책을 참고하였습니다.
* 책을 번역한 것이 아닌, 제가 독학 후 책을 참고하여 설명하는 게시물입니다. 따라서 책에 없는 부연 설명이 있기도 하며, 의역 또는 오역, 오개념이 있을 수 있습니다. 피드백은 댓글을 남겨주세요.
앞서 작성한 프로그램에서는 그저 한 가지 색으로 color 버퍼를 채웠습니다. 무언가를 실제로 그리기 위해서는 vertex shader (정점 쉐이더), fragment shader (pixel shader, 조각 쉐이더, 필셀 쉐이더) 가 필요합니다. OpenGL은 사실 선, 점, 삼각형과 같이 간단한 것들밖에 그리지 못합니다. 이러한 기본 요소들을 primitive라고 부릅니다. 보통 3D 모델이나 렌더러를 보신 분이라면, 모델들이 삼각형 또는 사각형으로 뒤덮여 있는 것을 보셨을 겁니다. 바로 그 삼각형 또는 사각형 하나하나가 primitive입니다.
primitive들은 정점(vertices)으로 이루어집니다. 예를 들어, 삼각형은 3개의 정점이 있습니다. 이러한 정점들은 C++/OpenGL 응용 프로그램에 의해 파일에서 읽어져 버퍼로 로드되기도 하고, C++ 코드로 하드코딩되거나 GLSL에 있기도 합니다. 이런 일들을 하기 위해서, C++/OpenGL 응용 프로그램은 적합한 vertex 쉐이더 프로그램과 fragment 쉐이더 프로그램을 컴파일하고, 링크(link) 한 후, 파이프라인으로 프로그램들을 로드해야 합니다. C++/OpenGL 응용 프로그램은 OpenGL이 삼각형들을 생성하도록 합니다. 바로 이 함수: glDrawArrays(GLenum mode, Glint first, GLsizei count); 를 이용해서 말이죠. 여기서 mode는 primative의 타입을 가리킵니다.GL_POINTS, GL_TRIANGLES 와 같은 값이 들어갑니다. first는 시작하는 정점을 나타냅니다. 보통은 0번 정점에서 시작합니다. 그리고 count는 그려질 정점의 총개수를 나타냅니다. glDrawArrays()가 호출되면, 파이프라인에 있는 GLSL 코드가 실행되기 시작합니다.
+ 다른 방식으로 primative 들을 그리는 방법은, 아래 링크의 포스트에서 실습과 함께 설명하였습니다.
[그래픽스 실습] 세가지 방법으로 오각형 그리기, OpenGL에서 primitive 그리기, glVertex, glDrawArrays, glDr
이번 학기 컴퓨터 그래픽스 과목 첫 번째 실습은 바로, '세 가지 방법으로 오각형 그리기'이다. OpenGL 애플리케이션에서 primitave를 그리는 방식은 세 가지가 있는데, glBegin - glVertex - glEnd를 사용하�
goeden.tistory.com
GPU에서 실행되는 쉐이더 코드는 CPU 프로그램처럼 step-by-step 실행이 아닌, 모든 정점 또는 픽셀 거쳐 vertex-by-vertex, 또는 pixel-by-pixel 단위로 실행됩니다.
(What is Shader?)
GPU에서 연산되는 쉐이더 프로그램은 모든 정점, 모든 픽셀에 같은 코드를 적용하여 그림을 그리는 프로그램임을 염두에 두시기 바랍니다. 그렇다면 정점의 ID나 픽셀의 좌표에 따라 다른 값을 가질 수 있도록 코드를 작성하게 되겠죠? 더 깊은 이해는 프로그램을 작성하며 하도록 합시다.
[Program 2.2 Shaders, Drawing a POINT]
// main.cpp
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#define numVAOs 1
GLuint renderingProgram;
GLuint vao[numVAOs];
using namespace std;
GLuint createShaderProgram() {
const char* vshaderSource =
"#version 410 \n"
"void main(void) \n"
"{gl_Position = vec4(0.0, 0.0, 0.0, 1.0);}";
const char* fshaderSource =
"#version 410 \n"
"out vec4 color; \n"
"void main(void) \n"
"{color = vec4(0.0, 0.0, 1.0, 1.0);}";
GLuint vShader = glCreateShader(GL_VERTEX_SHADER);
GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(vShader, 1, &vshaderSource, nullptr);
glShaderSource(fShader, 1, &fshaderSource, nullptr);
glCompileShader(vShader);
glCompileShader(fShader);
GLuint vfProgram = glCreateProgram();
glAttachShader(vfProgram, vShader);
glAttachShader(vfProgram, fShader);
glLinkProgram(vfProgram);
return vfProgram;
}
void init (GLFWwindow* window) {
renderingProgram = createShaderProgram();
glGenVertexArrays(numVAOs, vao);
glBindVertexArray(vao[0]);
}
void display(GLFWwindow* window, double currentTime) {
glUseProgram(renderingProgram);
// glPointSize(30.0f);
glDrawArrays(GL_POINTS, 0, 1);
}
int main(void) {
if (!glfwInit()) {exit(EXIT_FAILURE);}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter2 - program2", nullptr, nullptr);
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK) {exit(EXIT_FAILURE);}
glfwSwapInterval(1);
init(window);
while (!glfwWindowShouldClose(window)) {
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
우선, 처음 볼 법한 자료형이 있습니다.
// main.cpp
GLuint renderingProgram;
GLuint는 GL unsigned int를 의미합니다. 자주 쓰이는 자료형이니 기억하도록 합시다.
init() 함수가 더 이상 비어있지 않네요. createShaderProgram() 함수를 호출하고 있습니다. 해당 함수는 앞쪽에 있습니다.
// main.cpp
GLuint createShaderProgram() {
...
}
void init (GLFWwindow* window) {
renderingProgram = createShaderProgram();
...
}
createShaderProgram()은 vshaderSource와 fshaderSource라는 두 개의 문자열을 정의하며 시작합니다. 이 두 개의 문자열이 쉐이더 코드입니다.
// main.cpp createShaderProgram()
const char* vshaderSource =
"#version 410 \n"
"void main(void) \n"
"{gl_Position = vec4(0.0, 0.0, 0.0, 1.0);}";
const char* fshaderSource =
"#version 410 \n"
"out vec4 color; \n"
"void main(void) \n"
"{color = vec4(0.0, 0.0, 1.0, 1.0);}";
그 후 glCreateShader()를 두 번 호출합니다. 이 함수는 GL_VERTEX_SHADER와 GL_FRAGMENT_SHADER 타입의 쉐이더를 두 개 생성합니다. OpenGL에서 정의된 vertex 쉐이더 프로그램과 fragment 쉐이더 프로그램의 자료형인 듯합니다.glCreateShader()는 처음에는 비어 있는 쉐이더 개체를 생성합니다. 쉐이더 개체 각각의 인덱스(index)에 대한 정수 ID를 반환하죠. 결과적으로 vShader와 fShader에는 빈 쉐이더 개체 두 개의 ID가 대입됩니다. 하나는 GL_VERTEX_SHADER를 위한 쉐이더 개체고, 하나는 GL_FRAGMENT_SHADER를 위한 개체입니다.
// main.cpp createShaderProgram()
GLuint vShader = glCreateShader(GL_VERTEX_SHADER);
GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);
그다음 호출되는 glShaderSource()가 GLSL 코드를 빈 쉐이더 개체에 로드합니다. glShaderSource()는 네 개의 파라미터를 가집니다. 쉐이더를 저장할 쉐이더 개체, 쉐이더 소스 코드의 수, 소스코드를 포함한 문자열의 포인터 배열, 아직은 사용하지 않는 추가적인 파라미터입니다. 쉐이더 소스 코드의 수는 현재 '1'입니다. 그 이유는 나중에 알아보도록 합시다. glShaderSource()가 실행된 후 glCompileShader()로 두 쉐이더 개체를 컴파일합니다.
// main.cpp createShaderProgram()
glShaderSource(vShader, 1, &vshaderSource, nullptr);
glShaderSource(fShader, 1, &fshaderSource, nullptr);
glCompileShader(vShader);
glCompileShader(fShader);
그다음으로는 glCreateProgram()이 실행됩니다. 이 함수는 program 개체를 만듭니다. 그리고 그 개체의 포인터를 반환합니다. 따라서 vfProgram은 새로 만들어진 program 개체의 포인터가 대입됩니다. 그 뒤의 코드를 보면, glAttachShader()가 실행됩니다. 이름에서 짐작 가능하듯, vfProgram, 즉 방금 생성된 program 개체에 앞서 컴파일되었던vShader와 fShader 쉐이더 코드가 덧붙여집니다. 그에 이어 실행되는 glLinkProgram()은 'GLSL 컴파일러에게 링크*가 되었는지 즉 쉐이더 코드와 program 개체가 잘 연결되어 호환되는지를 확인합니다. 지난 코드를 잠시 떠올려 봅시다. vShader와 fShader는 GL_VERTEX_SHADER와 GL_FRAGMENT_SHADER라는 명확한 정체를 가진 개체입니다. 이들을 하나의 프로그램 개체에 연결하게 됩니다.
// main.cpp createShaderProgram()
GLuint vfProgram = glCreateProgram();
glAttachShader(vfProgram, vShader);
glAttachShader(vfProgram, fShader);
glLinkProgram(vfProgram);
결국 이렇게 링크된 vfProgram이 반환됩니다.
init()에서 createShaderProgram()을 실행했었죠? 그 부분으로 돌아갑시다. renderingProgram이라는 변수에 반환 값이 대입됩니다. renderingProgram은 쉐이더 개체가 링크된 프로그램 개체의 포인터를 갖게 되는 것입니다. 그 밑 glGenVertexArrays()와 glBindVertexArray()함수는 뒤 챕터에서 그 역할을 알아보도록 합시다.
void init (GLFWwindow* window) {
renderingProgram = createShaderProgram();
glGenVertexArrays(numVAOs, vao);
glBindVertexArray(vao[0]);
}
저번 프로그램과 main() 함수 부분이 다르지 않기 때문에, init() 함수 뒤에 display() 함수가 실행되는 구조도 같습니다. display() 함수는 glUseProgram()으로 시작합니다. 이 함수가 두 개의 컴파일된 쉐이더가 포함된 program을 OpenGL 파이프라인 단계로 내려보냅니다. 즉 GPU로 보내는 것이지요. glUseProgram()은 쉐이더를 실행하는 것이 아니라, 하드웨어로 로드할 뿐입니다.
// main.cpp display()
glUseProgram(renderingProgram);
뒤에서 배우게 되겠지만, 원래는 딱 이 시점에서 C++/OpenGL 프로그램은 그려질 모델의 정점을 준비합니다. (예를 들면 모델의 정점들의 좌표, 또는 더해질 변환 등 쉐이더로 보낼 uniform 값을 준비합니다.) 하지만 이번 프로그램에서는, 점 하나만 찍는 게 목표기 때문에 display() 함수는 바로 다음 명령을 실행합니다. 앞서 설명한 glDrawArrays()입니다. primitive의 타입은 GL_POINTS입니다.
// main.cpp display()
glDrawArrays(GL_POINTS, 0, 1);
vertex 쉐이더 코드를 살펴봅시다. 문자열 표시를 빼고 적어볼게요 :
// main.cpp createShaderProgram() vShaderSource " "
#version 410
void main(void)
{ gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
첫 번째 줄은 OpenGL 버전을 의미합니다. 이 숫자가 달라도 컴파일이 안되니 주의하세요. vertex 쉐이더의 주목적은 정점을 파이프라인으로 내려보내는 것입니다. 모든 정점을 같은 코드로 내려보냅니다. 빌트인 변수 gl_Position은 3D 공간에서의 정점 좌표를 설정하기 위해 사용됩니다. 그리고 이 변수는 파이프라인의 다음 단계로 넘겨집니다.
GLSL의 자료형 vec4는 네 개의 값을 가집니다. 따라서 x, y, z 좌표와 기타 값 한 개를 갖기에 적합합니다.
정점들은 파이프라인을 따라 rasterizer로 이동합니다. 이곳에서 픽셀로 변합니다. 사실 정확하게는 fragment(조각)으로 변합니다. 이 차이는 추후에 명확하게 설명하도록 하겠습니다. 결국 이 fragment들은 fragment 쉐이더에 도달합니다.
fagment 쉐이더는 앞서 언급한 제 블로그 다른 게시물들에서 살펴본 바로 그 쉐이더입니다. fragment 쉐이더의 목적은 display될 픽셀의 RGB 색깔을 설정하는 것입니다. 이 프로그램에서는 파란색으로 설정되어 있습니다. 'out' 태그를 유심히 보시길 바랍니다. color 라는 vec4 변수를 쉐이더 밖으로 보낼 것임을 명시합니다. (위 vertex 쉐이더의 gl_Position은 원래 output으로 설정되어 있는 변수기 때문에 out 키워드가 필요 없었습니다. 헷갈리죠?)
// main.cpp createShaderProgram() fShaderSource " "
#version 410
out vec4 color;
void main(void)
{ color = vec4(0.0, 0.0, 1.0, 1.0); }
위의 프로그램을 실행하면 1픽셀짜리 작은 점 하나가 보일 것입니다 :
프로그램에선 주석 처리했던 코드를 같이 실행하면, 포인트 사이즈가 30.0 픽셀로 설정되어 네모 모양의 큰 점이 출력됩니다.
// main.cpp display()
glPointSize(30.0f);
프로그램 실행 결과입니다 :
* 링크 : 어떤 쉐이더 개체든지, 프로그램 개체에 연결되어 있으면, 해당 쉐이더 개체는 프로그래밍 가능한 프로세서에서 실행될 실행파일을 만드는 데 사용됩니다. 따라서, 실행파일을 만드는 프로그램 개체에 쉐이더 개체를 연결시켜 프로그램의 일부임을 컴퓨터에게 알려주는 것입니다.
If any shader objects are attached to program, they will be used to create an executable that will run on the programmable processor.
(출처: https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glLinkProgram.xhtml)
'College Study > OpenGL' 카테고리의 다른 글
[그래픽스] 파일에서 GLSL 소스코드 읽기 (0) | 2020.06.08 |
---|---|
[그래픽스] OpenGL과 GLSL의 오류 검출 (0) | 2020.06.08 |
[그래픽스] OpenGL 프로그램 개요 (1) | 2020.06.01 |
[선형대수학] 벡터 (Vector), 그래픽스 기초 (0) | 2020.06.01 |
[그래픽스] 컴퓨터 그래픽스 프로그래밍 개발 환경 구축 (Windows, MacOS) (3) | 2020.05.22 |
댓글