[OpenGL] LearnOpenGL 정리본 4~5

2022. 7. 23. 08:58opengl

728x90

OpenGL에서 모든 것은  3D 공간 안에 있다. 그렇지만, 윈도는 2차원 픽셀 배열이다. 

3D -> 2D 로 바꾸는 작업이 필요함 

이 작업을 그래픽 파이프라인이 관리함.

 

그래픽 파이프라인은 크게 두 부분으로 나뉜다. 

1. 3D 좌표를 2D 좌표로 변환하는 것이다.

2. 2D 좌표를 실제 색이 들어간 픽셀로 변환하는 것이다. 

좌표와 픽셀의 차이 

좌표는 정확하지만 픽셀은 정확하게 보이는 것이다. 

 

Shaders: 파이프라인의 각 단계에서 GPU위에 작은 프로그램들을 병렬로 실행시킴으로써 데이터를 빠르게 처리하기 위해 여러 개의 작은 프로세싱 코어를 가지고 있는데 이 작은 프로세싱 코어(프로그램)들을 뜻함.

 

Shader는 여러게의 종류가 있다. 

OpenGL Shading Language (GLSL)으로 작성되고 나중에 다룰 예정이다.

 

그래픽 파이프라인의 입력으로 삼각형을 구성할 수 있는 정점 데이터 3개의 3D좌표를 전달할 수 있다.

이 정점에 대한 데이터는 정점(vertex) 속성을 사용하여 나타낼 수 있다. 

이 vertex 속성은 모든 데이터를 포함할 수 있다. 

 

그렇지만 간단히 하기 위해 위치와,  컬러 값을 가진다고 생각한다. 

OpenGL이 좌표값과 컬러 값의 집합을 만들기 위해 어떻게 이을지? 알려주는 것을 

primitive라고 한다. (ex.  GL_POINTS, GL_TRIANGLES, GL_LINE_STRIP)

 

 

1. vertex shader

파이프 라인의 첫번째 부분은 하나의 정점을 입력받는 정점 쉐이더(vertex shader)이다. 이 쉐이더는 3D좌표를 primitive에 맞게 변환하는 것이다.

 

2. geometry shader

정점들의 집합을 받는다. 이 정점들의 집합은 primitive를 구성하고 또 다른 primitive를 형성함으로써 다른 도형으로 변환이 가능될 수 있는 정점들이다. 

geomety shader의 출력 값은 rasterization stage로 넘어간다. 

이 출력값과 primitive(s)를 최종 화면의 적절한 픽셀과 매핑한다.  그 결과 다음 shader인  fragment shader에서 사용할  fragment가 나오게 된다. 

*fragment shader이 실행되기 전에 clipping이 수행되는데 clipping은 성능을 증가시킬 기 위해 뷰 밖에 있는 fragment를 폐기한다. 

*fragment는 하나의 픽셀을 렌더링 하기 위해 필요한 모든 데이터다. 

 

3.  칼럼 값들의 마지막

최종 결과물은 그대로 출력되는 것이 아니라.  한 단계를 더 거친다. 

alpha test와  blending 단계라고 불리게 된다. 이 단계에서 fragment의 해당 깊이 값을 체크하고, 최종 fragment가 다른 오브젝트 보다 앞에 있는지 뒤에 있는지 체크하고, 다른 오브젝트보다 뒤에 있는 fragment는 즉시 폐기된다. 또한 이 단계에서 alpha값을 확인하고 그에 맞춰 다른 오브젝트와 섞는다.(blaending) 그래서 fragment shaser에서 픽셀 출력 색이 계산됐어도 최종 픽셀 컬러는 여러 개의 삼각형을 렌더링 할 때 완전히 다른 색이 나올 수 있다

 

Vertex 입력

우리는 OpenGL을 공부하고 있고 이 OpenGL은 3D 그래픽 라이브러리이기 때문에 우리가 명시한 좌표는 모두 3D 공간의 좌표이다. OpengGL은 모든 3D좌표를 화면상의 2D픽셀로 간단히 변환하는 것이 아니다. 모든 좌표는 3개의 축 x, y, z에서 값이 모두 -1.0와  1.0 사이에 있어야만 처리하게 됩니다. 

 

우리는 범위 안에서 삼각형을 렌더링 하기 위해서는 x, y, z의 3개의 변수를 가지는 3개의 점을 찍어야 합니다. (float로 정의)

우리는 z 좌표가 0.0인 정점들을 이용해서 2D 삼각형을 그립니다. 이 방법은 2D처럼 보이도록 삼각형을 만든 것이다. 

=Normalized Device Coordinates (NDC)

NDC좌표는 glViewport 함수에 제공한 데이터를 사용하여 viewport transform을 통해 screen-space coordinates 좌표로 변환됩니다. 최종 screen-space coordinates 좌표는 fragment로 변환되어 fragment shader의 입력값이 됩니다. 

 

정점 데이터가 정의되면 우리는 그 데이터를 그래픽 파이프라인의 첫 번째 단계인 vertex shader에게 전달합니다. GPU 정점 데이터를 저장할 공간의 메모리를 할당하고 OpenGL이 어떻게 메모리를 해석할 것인지 구성하고 데이터를 어떻게 그래픽 카드에 전달할 것인지에 대해 명시하고 vertex shader에서 우리가 말한 정점 메모리에 처리한다. 

 

우리는 버퍼를 통해  vertex buffer objects(VBO)를 통해 메모리를 관리한다.  VBO는 많은 양의 정점들을 GPU 메모리 상에 저장할 수 있다. 이러한 버퍼 객체를 시용하면 한 번에 정점 데이터를 보내지 않아도 대량의 데이터를 한꺼번에 그래픽 카드로 전송할 수 있다. CPU에 하나씩 보내는 것은 속도가 느림 그래서 가능한 많은 데이터를 한 번에 보내야 한다. (GPU)

vertex shader은 즉각적으로 빠르게 정점에 접근이 가능하다. 

 

 

vertex buffer objects(VBO)는 우리가 OpenGL에서 처음으로 사용할 객체이다. 

이 객체는 고유 ID를 가지고 있다. 그래서 함수로써 관리를 해준다. (glGenBuffers)

 

 

OpenGL은 많은 유형의 버퍼 객체를 가지고 있으며 vertex buffer objecet의 버퍼 유형은 GL_ARRAY_BUFFER으로,  OpenGL은 버퍼 유형이 다른 여러 가지 버퍼를 바인딩할 수 있다. 우리는 glBindBuffer함수를 사용하여 GL_ARRAY__BUFFER로 바인딩할 수 있다. 

*바인딩(binding) : 프로그래밍 언어가 해당 언어에 네이티브 하지 않은 라이브러리나 운영 체제 서비스를 사용할 수 있도록 해주는 글루 코드를 제공하는 API이다.

이 시점부터 우리가 호출하는 모든 버퍼는 현제 바인딩된 버퍼(VBO)를 사용하게 된다. 

그런 다음 우리는 glBufferData 함수를 호출할 수 있다. 

glBufferData 함수는 사용자가 정의한 데이터를 현제 바인딩된 버퍼에 복사하는 기능을 수행한다. 

첫 번째 파라미터: 우리가 데이터를 복사해서 집어넣을 버퍼의 유형(VBO 가 바인딩된 GL_ARRAY_BUFFER)

두 번째 파라미터: 버퍼에 저장할 데이터의 크기 sizeof 키워드 추천

세 번째 파라미터: 우리가 실제 보낼 데이터

네 번째 파라미터 : 데이터를 관리하는 방법

Gl_STATIC(거의 안 변함), DYNAMIC(자주 바뀜), STREAM(그릴 때마다)._DRAW

 

 

지금은 삼각형인데 삼각형은 호출할 때마다 바뀌지 않으므로 GL_STATIC이 가장 알맞음.

지금은 정점 데이터를 그래픽 카드의 메모리에 저장했다. 이 메모리는 VBO가 관리한다. 

 

정리 : Vertex 값을 받으면 이 데이터가 shaders를 거쳐서 VBO에 메모리 관리가 된다. (병렬로 GPU에 보냄)

 

Vertex shader 

GLSL(OpenGL Shading Language)를 통해 Vertex shader를 작성하고 컴파일하여 사용 가능하다. 

GLSL를 이용한 예시

vertex shader에 in 키워드를 사용하여 모든 입력 정점 속성을 선언해야 합니다. 현제는 위치 데이터만 사용하므로 하나의 속성만 필요합니다.  vertex는 1~4 개의 실수의 구성됨. 각 정점은 3D 좌표를 가지고 있기 때문에 우리는 aPos라는 이름을 가진 vec3 타입 입력 변수를 생성합니다.  또한 layout (location=0) 코드를 통해 입력 변수의 location을 설정함.

 

vector를 통하여 좌표를 나타낼 수 있다.  4번째 파라미터는 형 변환을 시켜주는데 이건 나중에 설명한단다. 

 

shader 컴파일

OpengGL이 shader를 사용하기 위해서는 런타임 시에 shader소스 코드를 동적으로 컴파일해야 합니다. 

glCreateShader 함수의 파라미터로 우리가 생성할 shader의 유형을 입력합니다. 우리는 vertex shader를 생성하기 때문에 GL_VERTEX_SHADER를 파라미터로 입력한다. 

그 너 다음 Shader의 소스 코드를  shader 객체에 첨부한 후 shader를 컴파일한다.

 

glShaderSource 함수는 shader를 컴파일하기 위해 shader 객체를 첫 번째 파라미터로 받습니다. 

두 번째 파라미터는 소스 코드가 몇 개의 문자열로 되어있는지에 대한 값을 받는다. 

세 번째 파라미터는 vertex shader의 실제 소스코드를 받고

네 번째 파라미터 느 null로 남견 둔다. 

 

glCompileShader 함수는 컴파일이 성공적으로 완료됐는지 확인 가능

 

Fragment shader

 

Fragment shader은 픽셀의 출력 컬러 값을 계산하는 것에 관한 쉐이더.

주황색을 픽셀이 넣어주는 코드

그 후 쉐이더 타입을 설정하기 위해서는 밑에 코드처럼 

GL_FRAGMENT_SHADER상수를 shader 타입으로 설정해주기만 하면 됨

지금 두 개의 shader은 지금 컴파일되었고 마지막 할 일은 shader program으로 두 개의  shader 객체를 서로 연결하는 것이다. shader program은 렌더링 할 때 우리가 사용할 수 있다. 

 

Shader program

Shader program 객체는 여러 shader를 결합한 버전(마지막)

객체를 서로 link 해주어야 한다. 오브젝트를 렌더링 할 때 shader program을 활성화하면 활성화된 shader program안의 shader들은 렌더링 명령이 호출될 때 사용된다. 

shader 출력 -> 다음 shader의 입력값으로 연결 출력과 입력이 일치하지 않으면 오류 발생

glCreateProgram 함수는 program을 생성하고 program의 ID를 리턴. 

이전에 컴파일 한 shader들을 program객체에 첨수 그런 다음 glLinkProgram함수를 사용하여 그들을 연결해야 함.

 

연결해주고 링크가 잘되면 돌아주고 shader가 잘 연결 안 됐으면 flase 

glUseProgram 함수를 호출한 이후 shader와 렌더링 명령은 이 program객체를 사용하게 된다. 

그리고 shader들은 program 객체로 연결하고 나면 shader객체들을 제거해준다. 

 

 

정점 속성 연결(Linking Vertex Attributes)

vertex shader은 우리가 원하는 모든 입력들을 정점 속성의 형식으로 지정할 수 있도록 해준다. 

vertex shader은 유연성이 좋지만,  데이터의 어느 부분이 vertex shader의 어떤 정점과 속성이 맞는지 직접 지정해야 함.

 

glVertexAttribPointer 에 위의 내용도 함께 전달해준다.

glVertexAttribPointer 함수를 사용하면 OpenGL에게 어떻게 vertex속성을 해석해야 하는지 알려줄 수 있다.

 

첫 번째 파라미터 : vertex 속성을 지정해둔다. Vertex shader에서 layout (location = 0) 코드를 사용하여 position vertex 속성의 위치를 지정했었던 것처럼 이는 vertex 속성의 위치(location)를 0으로 설정하고 우리는 데이터를 이 vertex 속성에 전달하고자 하기 때문에 0에 전달한다.

 

두 번째 파라미터: vertex속성의 크기를 지정한다. 이 속성은 vec3타입이므로 3개의 값으로 이루어져 있다. 

 

세 번째 파라미터: 데이터 타입을 지정한다. 여기서는 GL_FLOAT으로 지정한다. 

 

네 번째 파라미터: 데이터 정규화를 지정한다. GL_TURE 설정하면 0(부호를 가진 데이터면 -1) 1 사이에 있지 않은 값들의 데이터들이 그 사이의 값들로 매핑된다. 우리는 GL_FLASE로 놔둔다. 

 

다섯 번째 파라미터: stride라고도 불리면 연속된 vertex속성 세트들 사이의 공백을 알려준다. 다음 포지션 데이터의 세트는 정확히 float타입 3개의 크기 뒤에 떨어져 있다. 우리는 이 값을 stride로 지정한다. 이 배열이 빽빽이 채워져 있다(다음 vertex 속성 값 사이에 공백이 없음)는 것을 알고 있으면 stride를 0으로 지정하여 OpenGL이 stride를 지정하게 할 수 있다. 

** stride에 대한 내용을 추가적으로 공부해주자\

 

여섯 번째 파라미터: void* 타입으로 형 변환이 필요하다. 버퍼에서 데이터가 시작하는 위치의 offset입니다. 위치 데이터가 배열의 시작 부분에 있기 때문에 이 파라미터는 0으로 지정함.

 

정리

각 vertex 속성은 VBO에 의해 관리되는 메모리로부터 데이터를 받습니다. 그리고 데이터를 받을 VBO(하나가 여러 VBO를 가질 수도 있습니다)는 glVertexAttribPointer 함수를 호출할 때 GL_ARRAY_BUFFER에 현재 바인딩된 VBO로 결정됩니다. glVertexAttribPointer 함수가 호출하기 전에 미리 정의된 VBO가 바인딩되어 있으므로 vertex 속성 0이 해당 vertex 정점과 연결된다.

 

 glEnableVertexAttribArray 함수의 파라미터로 vertex 속성 location를 전달하고 호출하여 vertex 속성을 사용할 수 있도록 해야 합니다. Vertex 속성은 기본적으로 사용하지 못하도록 설정되어 있습니다. 모두 설정이 된 후부터 vertex buffer 객체를 사용하여 vertex 데이터를 초기화하였고 vertex shader와 fragment shader를 셋업 했다.

 

 한 OpenGL에게 vertex 데이터가 vertex shader의 vertex 속성에 어떻게 연결되는지 알려주었다. OpenGL의 오브젝트를 그리는 것은 다음과 같이 형식을 취한다.

 

 

  오브젝트를 그려야 할 때마다 우리는 이 과정을 반복해야 한다. 그렇게 많아 보이지 않을 수 있지만 5개 이상의 vertex 속성과 100개의 다른 오브젝트들(흔한 경우입니다)이 있다고 생각해보자,  오브젝트들에 대해 신속히 적절한 buffer 객체를 바인딩하는 것과 모든 vertex 속성들을 구성하는 것은 번거로운 과정이 됩니다. 이 모든 상태 설정을 객체에 저장하고 간단히 이 객체를 바인딩하여 상태를 복원할 수 있는 방법은 다음에 배우겠다.

 

vertex array object (VAO)

 vertex buffer object와 같이 바인딩될 수 있으며 그 이후의 vertex 속성 호출은 VAO 내에 저장된다. 이는 vertex 속성 포인터를 구성할 때 오직 한 번 호출하기만 하면 되고 오브젝트를 그려야 할 때마다 해당 VAO를 바인딩하기만 하면 된다는 장점을 가지고 있습니다. 이는 서로 다른 vertex 데이터와 속성들을 다른 VAO를 바인딩함으로써 손쉽게 교체할 수 있습니다. 설정한 모든 상태가 VAO 내부에 저장된다. 

OpenGL은 VAO를 사용하도록 요구함 VAO는 다음 항목들을 저장한다.

VAO와 VBO 생성과정은 비슷하다.

VAO를 사용하기 위해 해야 할 일은 glBindVertexArray 함수를 사용하여 VAO를 바인딩하는 것이다. 그 후부터 해당 VBO(s)와 속성 포인터를 바인딩/구성하고 VAO를 나중에 사용하기 위해 언바인딩해야 한다. 오브젝트를 그리려면 그전에 간단히 원하는 세팅과 함께 VAO를 바인딩하기만 하면 된다. (추가 삭제에 용이하다)

 

VAO는 vertex 속성 구성을 저장한다. 일반적으로 여러 가지의 오브젝트들을 그리고 싶을 때 먼저 모든 VAO(또한 필요한 VBO와 속성 포인터들)를 생성하고 구성한다. 그리고 그것들을 나중에 사용하기 위해 저장합니다. 오브젝트들 중에 하나를 그리고 싶다면 해당 VAO를 가져와 바인딩하고 오브젝트를 그린 후 VAO를 다시 언바인딩한다.

 

삼각형 그리기

OpenGL은 glDrawArrays 함수를 제공해줍니다. 이 함수는 현재 활성화된 shader, 이전에 정의된 vertex 속성 구성, VBO의 vertex 데이터(VAO를 통해 간접적으로 바인딩된(GL_TRIANGLES))를 사용하여 Primitive 그립니다.

 

glDrawArrays 함수는 첫 번째 파라미터로 우리가 그리려 하는 OpenGL primitive유형을 지정한다. 우리는 삼각형을 그리고자 하기 때문에 GL_TRIANGLES으로 지정한다. 두 번째 파라미터는 vertex 배열의 시작 인덱스를 지정한다. 우리는 0으로 지정합니다. 마지막 파라미터는 몇 개의 vertex를 그리기 원하는지를 지정한다. 우리의 경우에는 3(1개의 삼각형을 그리기 때문에 3개의 정점)이다.

 

element buffer objects

사각형을 그려야 된다고 생각할때, 이렇게 그리면 오버헤드가 발생한다고 생각할 수 있다. 사실 겹치는 점이 두개 있어서 4개의 점으로 만으로도 가능하다. 그렇다면? 그리는 순서를 정해주면됨 ㅇㅇ

 

그후 VBO랑 비슷하게 glBufferData 함수를 사용해서 EBO를 바인딩하고 인덱스들을 버퍼에 복사해준다. 

우리는 이 버퍼를 GL_ELEMENT_ARRAY_BUFFER로 지정한다.

 

그 후 GL_ELEMENT_ARRAY 버퍼를 타겟으로 지정했다고 생각하고, 이제 마지막으로 glDrawArray 함수를 glDrawElements 함수로 대체하는 것이다. 이 함수는 인덱스 버퍼로 부터 삼각형으로 그리겠다고 지시한다.  glDrawElements 함수를 사용할 때 현제 바인딩된 element buffer object의 인덱스들을 사용하여 그리게 된다. 

 

첫 번째 파라미터는 glDrawArrays 함수와 마찬가지로 우리가 그리기 원하는 모드를 지정한다. 두 번째 파라미터는 우리가 그리고 싶은 요소의 갯수를 지정한다. 우리는 최종적으로 6개의 정점을 그려야하기 때문에 6을 지정했다. 세 번째 파라미터는 인덱스의 타입이다. 여기서는 GL_UNSIGNED_INT로 지정합니다. 마지막 파라미터는 EBO에서의 offset을 지정한다. (순서-차례를 정한다)

 

  glDrawElements 함수는 GL_ELEMENT_ARRAY_BUFFER를 타겟으로 현재 바인딩 된 EBO로 부터 인덱스들을 가져온다. 이는 해당 EBO를 렌더링 할 때마다 바인딩해야한다는 것을 의미한다. vertex array object는 또한 element buffer object 바인딩도 저장한다. VAO가 바인딩 되어 있는 동안 element buffer object가 바인딩 되면 VAO의 버퍼 객체로서 저장된다. VAO를 바인딩 하면 자동으로 내부에 있는 EBO도 바인딩 됩니다. (오호~)

 

 VAO는 타겟이 GL_ELEMENT_ARRAY_BUFFER일 때의 glBIndBuffer 함수 호출을 저장한다. 언바인드 호출도 저장하기 때문에 VAO를 언바인드 하기전에 element array buffer를 언바인드 하지 않도록 하자, 그렇지 않으면 EBO를 구성하지 않을 것이다.

 

 

728x90

'opengl' 카테고리의 다른 글

<자료구조> 큐(queue),덱(deque)  (0) 2023.06.01
[OpenGL] LearnOpenGL 정리본 6~7  (0) 2022.07.30
[OpenGL] LearnOpenGL 정리 본 (1~4)  (0) 2022.07.18
그래픽스 스터디  (0) 2022.07.12