■ 0. 그래픽 파이프라인
Vertex Data[] → Vertex Shader → Geometry Shader → Shape Assembly → Rasterization → Fragment Shader → Tests and Blending
1. 3D 좌표 리스트인 정점(Vertex) 데이터들을 입력으로 받는다.
2. Vertex Shader에서 모델 좌표를 클립 공간의 좌표로 변환하고 버텍스 속성(위치, 색상, 텍스처 좌표) 등을 처리한다.
3. Geometry Shader에서 Primitive(점, 선, 삼각형) 단위로 도형을 형성한다
4. Rasterization Stage에서 결과 Primitive를 최종 화면의 적절한 픽셀과 매핑한다. 그 결과로 fragment(하나의 픽셀을 렌더링하기 위해 필요한 모든 데이터)가 도출된다.
+ 이 때 성능을 증가시키기 위해 뷰 밖에 있는 fragment를 폐기하는 'Clipping'이 수행된다.
5. Fragment Shader에서 픽셀의 최종 컬러를 계산한다.
(Fragment Shader는 3D 씬 데이터를 갖고있으며 이 데이터는 최종 픽셀 컬러를 계산하기 위해 사용된다)
6. Alpha Test와 Blending 단계를 거친다.
최종 Fragment가 다른 오브젝트보다 뒤에 있으면 폐기하고 Alpha 값을 체크하여 다른 오브젝트와 Blending 한다.
현대 OpenGL에서는 Vertex Shader와 Fragment Shader는 스스로 작성한 것을 사용하기를 요구한다. (Geometry Shader는 선택적으로 사용)
■ 1. Vertex 입력하기
1. 하나의 삼각형을 렌더링하기 위해 3D 좌표를 갖고 있는 3개의 정점을 명시한다.
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
* 모든 좌표가 Normalized Device Coordinates(NDC) 범위 안에 있어야 보인다.
* Normalized Device Coordinates (NDC)
- x, y, z 값이 모두 -1.0 ~ 1.0 사이에 있는 공간으로 이 영역 밖의 좌표는 폐기되고 화면에 보이지 않는다.
- NDC 좌표는 glViewport 함수에 제공한 데이터를 사용해 Viewport Transform을 통해 Screen-Space Coordinates 좌표로, 이후 Fragment로 변환되어 Fragment Shader의 입력값이 된다.
2. 정점 데이터가 정의되면 Vertex Shader에 전달한다.
GPU에 정점 데이터를 저장할 공간의 메모리를 할당하고 OpenGL이 어떻게 메모리를 해석할 것인지 구성하고, 데이터를 어떻게 그래픽카드에 전달할 것인지에 대해 명시함으로써 이 작업이 완료된다.
● VBO
Vertex Buffer Object(VBO)라는 것을 통해 이 메모리를 관리한다. 이러한 버퍼 객체를 사용하면 많은 양의 정점들을 GPU 메모리에 저장할 수 있고 한꺼번에 전송할 수 있다.
// 버퍼 ID를 생성한다
unsigned int VBO;
glGenBuffers(1, &VBO);
// 생성된 버퍼를 GL_ARRAY_BUFFER로 바인딩한다
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 정점 데이터를 버퍼의 메모리에 복사한다
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
여기까지 과정을 통해 정점 데이터를 그래픽카드의 메모리에 저장하였다.
■ 2. Vertex Shader 만들기
● Vertex Shader 파일 작성
- 솔루션 디렉터리에 Shaders 폴더 생성, Shaders 폴더 내에 shader.vert 파일을 생성한다.
* 확장자는 속성에 영향이 없지만 Vertex Shader의 확장자는 주로 .vert를 사용하여 구분한다.
- GLSL 버전을 선언하고 in 키워드를 사용하여 모든 입력 정점 속성을 선언한다 (현재는 위치 데이터만 사용하므로 하나의 속성만 필요)
// 버전을 선언한다
#version 330 core
// 0번째 Vertex Attribute(삼각형을 이루는 꼭지점의 위치값)를 가져와 aPos라는 이름의 변수에 담는다.
layout (location = 0) in vec3 aPos;
void main()
{
// gl_Position에 위치 데이터를 할당한다. (크기가 4인 벡터로 형변환)
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
* gl_Position은 미리 정의된 내장 변수로 Vertex Shader의 '출력 변수'이며 이 변수에 저장된 값이 Clip-Space Coordinates로 출력되어 다음 셰이더들에서 사용된다.
● Vertex Shader 컴파일
- OpenGl이 Shader를 사용하기 위해서는 런타임에 Shader 코드를 동적으로 컴파일해야 한다.
// 버텍스 셰이더 소스 코드를 정적으로 저장
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
// Vertex Shader 생성
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEXT_SHADER);
// Shader 객체, 소스 코드가 몇개의 문자열로 되어있는지, 실제 소스코드, NULL 전달받아 Shaer를 컴파일
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
■ 3. Fragment Shader 만들기
- Fragment Shader는 픽셀의 출력 컬러 값을 계산하는 셰이더이다.
● Fragment Shader 파일 작성
- Shaders 폴더 내에 shader.frag 파일을 생성한다.
* 확장자는 속성에 영향이 없지만 Fragment Shader의 확장자는 주로 .frag를 사용하여 구분한다.
// 버전을 선언한다
#version 330 core
// 출력 변수를 선언한다
out vec4 FragColor;
void main()
{
// 출력할 컬러를 지정한다
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
* Fragment Shader는 오직 하나의 출력 변수 FragColor만을 필요로 한다. out 키워드를 사용하여 출력 값을 선언할 수 있다.
● Fragment Shader 컴파일
// 프래그먼트 셰이더 소스 코드를 정적으로 저장
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
// Fragment Shader 생성
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
// Vertex Shader와 비슷한 방식으로 컴파일
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
+ 셰이더의 소스코드들을 문자열로 저장하지 않고 파일로 읽어오기
■ 4. Shader Program
- 작성한 shader.vert와 shader.frag를 컴파일하여 하나의 실행 가능한 Shader Program으로 만들어 렌더링 시 사용한다.
// Shader Program 객체 생성
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
// 작성한 셰이더들을 프로그램에 붙이고 glLinkProgram 함수를 사용하여 연결한다.
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// shaderProgram 객체를 활성화한다. 이후 모든 Shader와 렌더링 명령은 이 Program 객체 (혹은 내부의 Shader)를 사용하게 된다.
glUseProgram(shaderProgram);
// 연결 후 더 이상 필요하지 않은 셰이더 객체들을 제거한다.
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
- 여기까지의 과정을 통해 Vertex 데이터를 GPU로 보냈고 어떻게 처리할 지 Shader들을 통해 지시하였다.
- 이후 OpenGL에게 메모리 상의 정점 데이터들을 어떻게 해석하고, 데이터와 셰이더의 속성들을 어떻게 연결할 지 알려주어야 한다.
■ 5. 정점 속성 연결
// 정점 배열을 OpenGL에서 사용하기 위해 버퍼에 복사
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// vertex 속성 포인터를 설정
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 오브젝트를 그리고 싶을 때 생성한 shader program을 사용
glUseProgram(shaderProgram);
// 오브젝트를 그린다.
someOpenGLFunctionThatDrawsOurTriangle();
* glVertexAttribPointer 함수의 파라미터 설명
1. 설정할 Vertex 속성을 지정 (Vertex Shader에서의 location = 0)
2. Vertex 속성의 크기 (vec3 타입이므로 3)
3. 데이터의 타입 (vec*은 실수형 점으로 이루어짐)
4. 데이터를 정규화 할 것인가 : GL_TRUE로 설정하면 0~1 혹은 부호가 있다면 -1~1 사이에 있지 않는 값들이 그 사이의 값으로 매핑된다.
5. 'Stride'라고도 불리며 다음 데이터셋이 얼마나 떨어져있는지 알려준다.
6. 버퍼에서 데이터가 시작하는 위치의 offset (시작 부분에 있기 때문에 0)
- 오브젝트를 그릴 때 마다 이 과정을 반복해야 한다. 하지만 이 모든 상태 설정을 객체에 저장하고, 해당 객체를 바인딩하여 상태를 복원할 수 있는 방법이 있다.
● VAO
Vertex Array Object(VAO)는 VBO와 같이 바인딩 될 수 있으며 그 이후의 Vertex 속성 호출은 VAO에 저장된다.
→ Vertex 속성 포인터를 구성할 때 한번만 호출하고 오브젝트를 그릴 때 해당 VAO를 바인딩 하기만 하면 된다.
* VAO는 다음 항목들을 저장한다.
1. glEnableVertexAttribArray 또는 glDisableVertexAttribArray 함수의 호출
2. glVertexAttribPointer 함수를 통한 Vertex 속성의 구성
3. glVertexAttribPointer 함수를 통해 Vertex 속성과 연결된 VBO 객체들
// VAO 생성
unsigned int VAO;
glGenVertexArrays(1, &VAO);
// VAO 바인딩
glBindVertexArray(VAO);
// OpenGL이 사용하기 위해 입력된 Vertex를 복사
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Vertex 속성 포인터를 세팅
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 오브젝트를 그린다 (렌더링 루프 내부)
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
* glDrawArrays(GL_TRIANGLES, 0, 3);
→ 그리려는 OpenGL 프리미티브 유형, Vertex의 시작 인덱스, 몇개의 Vertex를 그릴지 지정
■ 소스 코드
'🎨 Graphics > 🔵 OpenGL' 카테고리의 다른 글
| [OpenGL] Shader (2) | 2024.07.24 |
|---|---|
| [OpenGL] EBO (0) | 2024.07.18 |
| [OpenGL] 윈도우 생성 및 렌더링 (0) | 2024.07.17 |
| [OpenGL] 개발 환경 세팅 (0) | 2024.07.11 |
| [OpenGL] 개요 (0) | 2024.07.10 |