Back to top

LearnOpenGL(03) - Hello Triangle

작성날짜 2022/06/25

이전글: LearnOpenGL(02) - Hello Window

다음글: LearnOpenGL(04) - Shaders


Hello triangle


OpenGL에서는 모든 것이 3차원 공간에 있습니다. 하지만 화면은 픽셀들의 2차원 배열입니다. 때문에 3차원 좌표들을 화면에 맞춰서 2차원 픽셀들로 바꾸는 건 OpenGL이 하는 일 중 상당한 부분입니다. 3차원 좌표들을 2차원 픽셀들로 바꾸는 과정은 OpenGL의 그래픽스 파이프라인이 관리합니다. 그래픽스 파이프라인은 크게 두 부분으로 나눌 수 있는데, 하나는 3차원 좌표들을 2차원 좌표들로 변환하는 것이고 나머지 하나는 2차원 좌표들을 실제 색을 가진 픽셀들로 변환하는 것입니다. 이번 챕터에서는 그래픽스 파이프라인이라는 것과 이것을 근사한 픽셀들을 만드는 데에 어떻게 이용할 수 있을지 논해보겠습니다.

그래픽스 파이프라인은 3차원 좌표들을 입력 받아서 픽셀로 변환하여 화면에 출력합니다. 그래픽스 파이프라인은 몇 가지 단계로 구분할 수 있으며 각 단계마다 입력과 출력이 있고, 입력 데이터는 이전 단계의 출력에서 받아옵니다. 모든 단계는 각자 특별한 기능을 수행하며 쉽게 병렬로 실행할 수 있습니다. 이런 병렬 실행 특성 덕분에 최근 그래픽 카드들은 그래픽스 파이프라인 데이터 처리를 빠르게 하기 위해 수천개의 작은 처리 코어를 가지게 되었습니다. GPU 내에서 처리 코어는 파이프라인의 각 단계마다 작은 프로그램을 실행합니다. 이 작은 프로그램을 셰이더라고 부릅니다.

셰이더를 개발자가 직접 작성할 수도 있습니다. 이렇게 개발자가 직접 작성한 셰이더로 기존 셰이더를 대체하는 것으로 파이프라인의 세밀한 조정이 가능해집니다. 셰이더는 GPU에서 동작하므로 귀중한 CPU 타임을 절약할 수 있습니다. 셰이더는 OpenGL Shading Language(GLSL)로 작성되며 이것에 대해선 다음 챕터에 더 깊게 들어가보겠습니다.

아래 그림은 그래픽스 파이프라인의 각 과정을 추상적으로 보여줍니다. 파란색 영역은 직접 작성한 셰이더를 넣을 수 있는 과정입니다.

pipeline.png

보시다시피 그래픽스 파이프라인이 버텍스 데이터를 픽셀로 완전히 렌더링하기까지 특수한 여러 과정을 거쳐야합니다. 당신이 파이프라인의 전체적인 실행 과정을 알 수 있도록 각 과정을 대략적으로 설명하겠습니다.

그래픽스 파이프라인에 데이터를 전달할 때에는 Vertex Data라고 하는 삼각형의 모양을 이루는 3개의 3차원 좌표들로 이루어진 리스트를 전달해야 합니다. 이 버텍스 데이터가 버텍스들의 콜렉션입니다. 버텍스는 3차원 좌표 하나에 해당하는 데이터들의 모음입니다. 버텍스에는 vertex attributes라는 걸 이용해서 우리가 원하는 데이터를 담을 수도 있습니다. 하지만 지금은 3차원 좌표, 그리고 일부 버텍스만 색상 값을 가지고 있다고 가정합시다.

OpenGL이 좌표와 색상 값의 콜렉션으로 무엇을 만들어야 할지 알려주기 위해선 데이터로 어떤 타입의 렌더링을 할지 OpenGL에게 알려줘야 합니다. 우리가 점들을 렌더링 하고 싶어하는지, 삼각형들을 렌더링 하길 원하는지 아님 그냥 하나의 기다란 선을 렌더링 하길 원하는지 OpenGL은 알지 못합니다. 이런 렌더링 타입을 프리미티브(primitives)라고 하며 모든 그리기 명령(drawing commands)마다 OpenGL에 넘겨줘야 합니다. 프리미티브엔 GL_POINTS, GL_TRIANGLES, GL_LINE_STRIP 등이 있습니다.

파이프라인의 가장 첫 단계는 버텍스 셰이더(vertex shader)입니다. 이 단계에서는 하나의 버텍스를 입력으로 받습니다. 버텍스 셰이더의 주 목적은 3차원 좌표를 또다른 3차원 좌표로 변환하고 사용자가 vertex attributes를 이용해 몇가지 기초적인 작업을 하도록 합니다.

버텍스 셰이더의 출력은 옵션에 따라 지오메트리 셰이더(geometry shader)에 전달됩니다. 지오메트리 셰이더는 프로미티프 형식의 버텍스들의 콜렉션을 입력으로 받고 새로운 버텍스를 방출하여 기존과는 다른 형식의 프로미티브로 바꿀수도 있습니다. 이번 예시에선 기존 삼각형 외부에 또 다른 삼각형을 생성합니다.

프리미티브 어셈블리(primitive assembly) 단계에서는 버텍스(또는 지오메트리) 셰이터에서 모든 버텍스들(GL_POINTS 옵션이 선택됐다면 하나의 버텍스)을 입력으로 받습니다. 이 버텍스들은 하나 또는 여러 개의 프로미티브를 이루는데, 주어진 프로미티브 형태에 따라 모든 점들을 조합합니다. 이번 예시에선 두 삼각형을 조합했습니다.

프리미티브 어셈블리의 출력은 레스터라이제이션(rasterization) 단계로 넘겨집니다. 이 단계에선 넘겨받은 프리미티브들을 화면의 픽셀들과 연결시켜, 프레그먼트 셰이더에서 사용할 프레그먼트를 생성합니다. 프레그먼트 셰이더 실행 전에 클립핑이 실행됩니다. 클립핑은 성능 향상을 위해서 시야밖의 프레그먼트들을 버립니다.

OpenGL에서 프레그먼트란 하나의 픽셀을 렌더하기 위해 필요한 모든 데이터를 말합니다.

프레그먼트 셰이더(fragment shader)의 주요 목적은 픽셀의 색을 최종적으로 계산하는 것이며 주로 이 단계에서 고급 OpenGL 이펙트가 적용됩니다. 일반적으로 프레그먼트 셰이더는 최종 픽셀 색상을 계산하기 위한 3D 씬의 데이터(빛, 그림자, 빛의 색 등)를 가지고 있습니다.

모든 색상 값이 결정되면 최종 객체가 알파 테스트(alpha test)블렌딩(blending)이라는 단계를 거칩니다. 이 단계에선 프레그먼트의 깊이(그리고 스텐실)값을 확인하고 그 값으로 어느 객체의 뒤에 있으면 해당 프레그먼트를 적절히 처리합니다. 이 단계에선 알파값(알파 값은 객체의 불투명도를 뜻합니다)도 확인하여 객체들을 적절히 섞어(blend)줍니다. 프레그먼트 셰이더에서 픽셀의 색상이 계산되었더라도 여러 삼각형을 렌더링하는 경우엔 최종 색상이 완전히 달라질 수 있습니다.

지금까지 본 것처럼 그래픽스 파이프라인은 설정이 필요한 많은 부분들로 이루어져 있습니다. 하지만 대부분의 경우 우리는 버텍스 셰이더와 프레그먼트 셰이더만 작업하면 됩니다. 지오메트리 셰이더는 선택적이고 주로 기본 셰이더로 남겨놓습니다. 이 외에도 테셀레이션 단계와 트랜스폼 피드백 루프가 있지만 지금 알아야할 건 아닙니다.

현대 OpenGL에선 버텍스와 프레그먼트 셰이더만큼은 직접 정의해야만 합니다(GPU에 기본 버텍스/프레그먼트 셰이더가 없습니다). 현대 OpenGL를 배울 때 어려운 이유 중 하나가 자신의 첫 삼각형을 렌더할 때 상당한 지식을 요하기 때문입니다. 이 챕터의 마지막에 자신의 삼각형을 렌더할 때 쯤이면 그래픽스 프로그래밍에 대한 많은 것을 알게 될 것입니다.


Vertex Input


뭔가를 그리려면 OpenGL에게 버텍스 데이터를 주는 것부터 시작해야 합니다. OpenGL은 3D 그래픽스 라이브러리입니다. 따라서 우리가 OpenGL에서 지정하는 모든 좌표들은 3차원입니다(x, y와 x 좌표). OpenGL은 모든 3차원 좌표를 2차원 픽셀로 변환해주지 않습니다. OpenGL은 -1.0과 1.0사이의 3개의 축(x, y, z)으로된 특정한 범위의 좌표만 처리합니다. 이 좌표들을 정규 장치 좌표(normalized device coordiantes)라고 하며 이 범위에 포함된 좌표만 화면에 표시됩니다(즉, 범위 밖의 좌표는 포시되지 않습니다).

하나의 삼각형을 렌더하려면 각각 3차원 위치를 가지고 있는 3개의 버텍스를 작성해야 합니다. 다음처럼 float array형태로 정규 장치 좌표 범위에 버텍스들을 정의합니다.

float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f, 0.5f, 0.0f
};

OpenGL은 3차원 공간에서 작업하지만 2차원 삼각형을 렌더하고 싶으므로 각 버텍스는 0.0의 z축 좌표값을 가지고 있습니다. 이 방법은 삼각형의 깊이(depth)를 똑같도록 하여 2차원처럼 보이게 됩니다.

정규 장치 좌표(NDC)
버텍스 셰이더 단계를 거친 버텍스들은 정규 장치 좌표 내에 위치하게 됩니다. 정규 장치 좌표는 x, y, z의 값이 -1.0부터 1.0 사이인 좌표이며 이 범위 밖의 좌표들은 버려지거나 잘리게 되어 화면에 표시되지 않습니다. 아래에서 정규 장치 좌표 내에 작성한 삼각형을 볼 수 있습니다.ndc.png 일반적으로 위쪽 상단이 원점인 데에 반해 OpenGL에서는 양의 y축이 위쪽 방향이고 (0, 0) 좌료가 그래프의 중심입니다. 결과적으로 모든 좌표가 이 좌표 공간에서 끝나야 하며 그렇지 않은 것들은 표시되지 않습니다.NDC의 좌표들은 glViewport에 제공한 데이터를 사용하여 뷰포트 변환(viewport transform)를 거쳐서 화면 공간 좌표(screen space coordinates)로 변환됩니다. 이 화면 공간 좌표들은 프레그먼트 셰이더를 통해 프레그먼트로 변환됩니다.

이제 정의한 버텍스 데이터를 그래픽스 파이프라인의 첫 단계인 버텍스 셰이더에 보내야합니다. 그러려면 GPU에 버텍스 데이터를 저장할 메모리를 만들고 OpneGL이 메모리를 어떻게 해석할지, 그래픽 카드에 데이터를 어떻게 보낼지를 설정해야합니다.  버텍스 셰이더는 설정된 만큼의 메모리를 처리합니다.

vertex buffer objects(VBO)라는 것을 이용해 수많은 버텍스를 GPU의 메모리에 저장하도록 관리할 수 있습니다. 버퍼 오브젝트를 사용하면 메모리가 남아있는만큼 데이터를 한번에 많이 보낼 수 있습니다. CPU에서 그래픽 카드로 데이터를 보내는 것은 상대적으로 느리므로 한 번에 가능한 많이 보내야 합니다. 데이터가 그래픽 카드에 들어가기만 하면 버텍스 셰이더가 매우 빠르게 접근할 수 있습니다.

버텍스 버퍼 오브젝트는 OpneGL 챕터에서 얘기한 것처럼 가장 처음 발생하는 OpneGL 객체입니다. 다른 OpenGL객체들처럼 버퍼는 고유의 ID를 가져야 합니다. 따라서 glGenBuffers 함수에 buffer ID를 넘겨주는 것으로 버퍼를 생성할 수 있습니다.

unsigned int VBO;
glGenBuffers(1, &VBO);




참고: https://learnopengl.com/Getting-started/Hello-Triangle


관련글

LearnOpenGL(01) - 창 만들기LearnOpenGL(02) - Hello Window LearnOpenGL(04) - ShadersLearnOpenGL(05) - 텍스처
LearnOpenGL(03) - Hello Triangle
An unhandled error has occurred. Reload 🗙