Plane Equation

한 평면 위의 임의의 Vector와 평면의 법선 벡터(Normal Vector)는 서로 수직 한다.
즉, $ V_{평면 위의 임의의 Vec} \cdot N_{평면의 법선 벡터} = 0$ 를 만족한다.


Vertex Position의 변환 행렬[M]은 평면 위의 임의의 점을 변환시켜주는 변환 행렬이다.

변환 전 : $N \cdot V = 0$
변환 후 : $N' \cdot V' = 0$

여기서 법 선 벡터[N]와 평면 위의 임의의 Vector 사이에 $M^{-1} M$를 삽입한다.($M^{-1} M = I$)
$NM^{-1} MV = 0 \rightarrow \  NM^{-1} V' = 0 = N'V' \rightarrow \ NM^{-1} = N'$
$(M^{-1})^{T} N = N'$


결론

법선 벡터의 변환 행렬은 Vertex Position의 변환 행렬의 역행렬을 전치시킨 행렬이다.
[$(M^{-1})^{T}$]

 

'OpenGL' 카테고리의 다른 글

Texture Coordinate  (0) 2022.11.14
OpenGL[Lighting]  (0) 2022.10.18
OpenGL[Coordinate System]  (0) 2022.10.18
OpenGL[02](First Triangle)  (1) 2022.10.12
OpenGL[01](Shader)  (1) 2022.10.12

배우는 이유?

 

우리가 사용하는 어떤 앱을 만들기 위해 서버, 데이터 베이스 등 몇 개의 앱을 사용하는데,
이런 것들을 한꺼번에 할 수 없을까?

이런 상황을 위해 각각의 실행하되, 격리된 환경에서 실행되게 하면 되지 않을까?
그렇게 격리된 환경에서 네트워크로 연결해서 필요한 정보를 주고받으면 되겠구나.
격리된 환경에는 운영체제가 존재하진 않지만 앱을 실행시키기 위한 것들만 존재하게 하면 많은 것들을 아낄 수 있다.

이렇게 하나의 컴퓨터에 여러 개의 격리된 환경을 만들 수 있게 하는 것이 Docker다.

하나의 컴퓨터는 Host, 격리된 각각의 환경을 Container라고 칭한다.
그렇다면 Host와 Container의 운영체제는 같아야 하는가? 아니다.

Docker는 기본적으로 Linux에서 돌아간다.
그래서 Mac, Windows에서 Docker를 설치하면 경량화된 Linux가 가상화되어 그 위에서 Docker가 실행되는 것이다.

그렇다면, VM과 다를 것이 없을 텐데 왜 사용하는가? 아니다.
VM과는 명백하게 다른 점이 있다.

  1. VM은 운영체제를 완전히 설치해야 하기 때문에 어떤 운영체제도 받을 수 있다.
    • Docker는 Linux의 위에서만 돌아가기 때문에 Linux계열의 운영체제만 지원된다.
  2. VM은 Host와 완전히 분리되어 있다.
    • VM은 논리적 가상화로 인해 운영체제를 완전히 설치해서 사용한다.
    • Docker는 Kernel의 공유로 Host의 운영체제에서 부족한 부분의 운영체제를 설치해서 사용한다.
      • 즉, Docker에서의 명령은 Host를 통해 실행된다.
  3. VM은 해킹당해도 문제없지만, Docker는 해킹당하면 Host가 위험하다.
    • VM은 Host와 분리되어 있기에 영향이 없지만, Docker는 Host와 연결되어있기에 다른 컨테이너 또한 위험하다.

 

결과적으로, 관리만 잘하면 자원의 낭비 없이 효율적으로 사용하는 유용한 도구가 Docker인 것이다.

 


네트워크

 

내 컴퓨터에서 어떤 웹페이지를 띄운다면 Host의 Port를 통해 접근해서 어떤 html 파일을 열게 된다.
근데 이런 과정을 Docker를 이용해서 한다면,
Host의 Port를 통해 들어가면 해당 Port와 연결된 Container의 Port로 들어가서 Container에 저장된 어떤 html을 열게 된다.

이런 Host와 Container의 Port를 연결하는 과정을 Port-Forwarding이라 한다.

ps. 호스트와 컨테이너의 파일 시스템을 연결시킬 수 있는 방법도 있다.


Docker의 Image 만드는 법

 

Image는 Container를 만들기 위한 설계도로 모든 파일과 설정값을 갖고 있다.
또한, Image는 변하지 않고, 여러 개의 컨테이너를 생성할 수 있다.
이런 Image는 Container에서 commit을 이용해서 생성할 수 있고, Dockerfile를 build 하여 생성할 수 있다.

Container에서의 commit은 현재 상태를 백업하는 용도로 Image를 생성하는 느낌이다.

Dockerfile은 Image의 초기 설정값, 실행할 명령어 등을 문서상으로 작성해놓은 것.


Dockerfile 작성법

 

FROM ubuntu:20.04

Docker의 Image 환경을 지정하는 방식이다.

- Image의 환경을 Ubuntu의 20.04 Version을 사용하는 것이다.

RUN apt update && apt install -y python3

Image의 환경에서 실행되는 명령어인데, 실행할 때마다 Layer가 생성되기 때문에 한 줄에 여러 개의 명령어를 작성하는 것이 좋다.
Build 되는 시점에 실행되는 명령어 -> Image에 반영

- 현재 apt의 상태를 최신으로 해준 뒤 python3을 설치해주는데, Yes/No의 대답에 항상 Yes를 해주겠다는 내용이다.

WORKDIR /var/www/html

Shell에서 "mkdir -p /var/www/html"과 같은 명령어이다.

- 해당하는 폴더가 없다면 만들어주고, 해당 폴더로 현재 경로를 옮겨준다.

COPY ["index.html", "."]

- Host의 현재 경로에서 첫 번째 변수를 Image의 현재 경로에 복사한다.

CMD ["python3", "-u", "-m", "http.server"]

Container를 만들 때 실행되는 Default 명령어 -> Container에 반영
즉, Docker Run 하면서 명령어를 지정하면 실행되지 않는다.

- python3의 기본 Web Server를 실행시킨다.(8000번 Port로 연결된다.)

Dockerfile 실행 방법

 

docker build -t <Image의 이름> <Dockerfile의 경로>
// 경로의 Dockerfile을 찾아 실행시켜 Image를 생성한다.

docker rm --force <Container의 이름>
// 만약 생성할 Container가 이미 있는 이름이라면 제거한다.

docker run -p <Host의 Port>:<Container의 Port> --name <Container의 이름> <Image의 이름> (<명령어>)
// Container를 생성하는데, Port-Forwarding해준 뒤, 이름 지정, 명령어를 실행시켜 준다.(명령어는 생략 가능)

Docker Compose를 이용한 Docker Container 제어하기

 

실제 Docker Container는 하나의 프로그램을 실행하는데, 하나의 프로그램으로 서비스가 된다면 Docker는 필요하지 않았을 것 같다.
예시로 WordPress를 컨테이너로 만들게 되면, SQL이 필요하여 또 하나의 컨테이너가 필요하게 된다.
그렇게 두 개의 컨테이너가 만들기 위해 Dockerfile을 실행시킬거나 Image를 가져올 때 몇 개의 명령어를 위해 Docker-compose.yml파일을 만들게 된다.

  • 컨테이너의 이름 설정
  • 컨테이너 생성 순서 설정(의존성)
  • Port-Forwarding(Host와 Container 간의 통신이 필요한 경우)
  • Volume 공유
  • 컨테이너 환경 변수 설정
  • 네트워크 생성, 설정

 


왜 Inception에서 MariaDB, Nginx, Wordpress를 사용할까?

 

  • Nginx는 웹 서버를 위한 도구
  • Wordpress는 손쉽게 웹 사이트를 만들 수 있는 도구
  • MariaDB는 관계형 데이터 베이스 관리를 위한 도구
    • 데이터 베이스 : 컴퓨터 시스템에서 전자적으로 저장되는 구조화된 정보 또는 데이터의 조직화된 모음

Wordpress의 모든 정보는 데이터 베이스에 저장, 저장된 데이터는 웹 사이트를 만든다.
웹 사이트를 구성하는 대부분이 데이터 베이스에 저장되어 있다.
예를 들면, 사용자 이름, 비밀번호, 포스트, 페이지, 댓글, Wordpress 구성 설정 등

작동방식

사용자가 웹 사이트를 방문하면 해당 브라우저가 Nginx에게 Request를 전달하고,
Nginx는 받은 Request를 WordPress에게 전달해준다.
그리고 WordPress는 Request를 받아 MariaDB에서 필요한 데이터를 추출한다.
추출된 데이터에 따라 웹 사이트를 만들어 Nginx로 보내진 뒤 사용자에게 보여준다.

 

'42일기' 카테고리의 다른 글

kqueue의 사용법  (0) 2022.07.23
I/O Multiplexing  (0) 2022.07.23
ft_containers[Red-Black Tree]  (1) 2022.04.29
ft_containers[Map]  (0) 2022.04.26
CPP [템플릿]  (0) 2022.03.02

이번 내용은 LearnOpenGL에서의 내용과, 이미지를 사용하여 적어보겠습니다.

실생활의 조명은 매우 복잡하지만, OpenGL에서의 조명은 처리되기 쉽고 실제 사물과 비슷하게 보이는 모델을 사용하여 실제에 대한 근사치를 기반으로 이뤄져 있다. 사실 빛이 반사되는 모든 경우를 계산해서 정점의 색을 결정한다면 표현할 수 있겠지만, 그럴 수 없기 때문이다.

Flat Shading

  • 기본적인 Shading으로 하나의 Primitive를 하나의 색으로 채우는 방식이다.
  • Primitive마다의 색이 다르기 때문에 색이 있는 폴리곤을 보는 느낌이 들 수 있다.

Gouraud Shading

  • Primitive의 정점에 대해 색상을 부여하여 색상을 보간하여 채우는 방식이다.
  • Flat Shading보다는 자연스럽게 표현되지만 어딘지 모르게 부자연스러운 느낌이다.

Phong Shading

  • Primitive의 채워지는 Pixel에 대해 색상을 계산하여 채운다.
  • Pixel마다의 빛을 고려했기 때문에 자연스럽게 계산된다.


나는 Phong Lighting Model에 대해 적어보려 한다.(앞으로 이야기할 색의 범위는 [0~1]이다.)

Phong Lighting Model

  1. Ambient Lighting(주변광)
  2. Diffuse Lighting(분산광)
  3. Specular Lighting(반사광)

Ambient Lighting

아무리 어두운 곳에 있더라도 어디선가는 빛이 있다.
빛은 여러 방향으로 산란하여 퍼지기 때문에 어디 있을지 모르는 물체에 간접적인 영향을 준다.
하지만, 얼마나 영향을 받을지는 모르기에 사용자가 정한다.

결과적으로, Ambient 상수, 빛의 색, 물체의 색을 곱한 값이 된다.


Diffuse Lighting

빛과 물체의 관계에 따른 물체의 기본적인 색을 결정하는 데 사용된다.

광원에서 정점까지의 빛을 법선 벡터와 정점의 평면벡터로 나누게 되면 해당 값이 나오는데 이를 내적으로 계산한다.
즉, 빛의 세기가 1이라고 했을 때 법선 벡터 방향의 빛의 세기라고 볼 수 있다.

광원에서 정점까지의 벡터를 L, 정점의 법선 벡터라 했을 때, 내적 $(-L) \cdot N = |L||N|cos\theta = cos\theta$
여기서 만약 내적 값이 0보다 작다면 앞면이 아니기 때문에 그리지 않아도 된다.


Specular Lighting

빛과 물체 그리고 카메라의 관계에 따른 빛에 의해서 강조되는 부분을 표현하는 데 사용한다.

광원에서 정점까지의 빛을 정말 반사했을 때의 벡터, 카메라의 위치에서 정점까지의 벡터를 내적 한 값을 n제곱을 하면 된다.

위에서 Diffuse에서처럼 내적 값이 0보다 작다면 앞면이 아니기 때문에 그리지 않아도 된다. (0 처리하면 됨)

여기서 n제곱을 하는 마지막 과정이 있는데, 이는 우리가 반사된 빛을 얼마나 똑바로 보고 있는가를 판단하는데,
해당 값의 범위가 [0~1] 사이인데, 이 값을 n 제곱하면서 1과 가까운 값만 유효하게 쓸 수 있도록 해준다.
여기서 n의 값이 크면 클수록 유효한 범위가 작아지는 것을 알 수 있다.

'OpenGL' 카테고리의 다른 글

Texture Coordinate  (0) 2022.11.14
Normal Vector의 변환 행렬  (0) 2022.11.14
OpenGL[Coordinate System]  (0) 2022.10.18
OpenGL[02](First Triangle)  (1) 2022.10.12
OpenGL[01](Shader)  (1) 2022.10.12

Local Space

OpenGL에서 우리가 물체를 그리기 위해 정점의 위치를 정할 때 기준을 정하여 기준에 맞춘 위치를 명시한다.
예를 들면 육면체는 육면체의 중심, 원기둥은 아래 뚜껑의 중앙, 원뿔은 바닥의 중앙 등의 기준을 갖는다.
이런 기준을 Local Space에서의 원점이고 말 그대로 물체를 기준으로 본 좌표계에서의 원점이다.

하지만, 우리가 그릴 물체는 여러 개일 수도 있고, 움직임이 존재할 수 도 있고, 축소/확대될 수도 있다.
그러기 위해 우리는 Local Space에 존재하는 물체를 모든 물체에 기준이 되는 좌표계에 그려주어야 한다.
이를 World Space라고 한다.


World Space

World Space에서의 원점은 우리가 그릴 모든 물체에 기준이 되는 좌표계이다.
즉, 물체의 위치, 기울임, 크기 등을 조절해서 여러 개의 물체들을 World Space에 동시에 존재하도록 할 수 있다.
우리는 OpenGL에서 glm을 이용하여 Local Space에서 World Space로 변환 행렬을 만들고,
해당하는 정점의 위치, 법선 벡터에 적용시키게 된다.

이때, 정점의 위치는 단순한 위치를 표현하지만 법선 벡터는 방향을 갖기 때문에 변환 행렬을 수정해야 한다.

더보기

법선 변환 행렬의 증명과정은 아래와 같다.

 

T : 법선에 수직하고, 해당하는 정점과 임의의 한 점까지의 벡터
N : 법선
T' : 변환된 T
N' : 변환된 N
M : 정점의 변환 행렬
G : 법선의 변환 행렬

T와 N은 수직 하기 때문에 내적 했을 때 0이 나온다.
$N \cdot T = 0,\ N' \cdot T' = 0$

$N'\cdot T' = (GN)\cdot(MT)=0$
$N'\cdot T' = (GN)^{T}\cdot(MT)=0$
$N'\cdot T' = N^{T} G^{T} M T)=0$

위 상황에서 우리는 $N^{T} T = 0$인 것을 알고 있다.
그렇다면 $G^{T} M = I$가 되어 $G = (M^{-1})^{T}를 만족하게 된다.

법선 변환 행렬은 변환 행렬의 역행렬의 전치 행렬이다. => $(M^{-1})^{T}$이다.
이렇게 정점의 정보들을 World Space로 변환하면 사전작업이 완료됐다 볼 수 있다.

여기서 우리가 그려야 할 장면은 어떤 위치에서 어떤 방향으로 존재하며, 어떻게 해야 할지는 다음 과정에서 알 수 있다.


View Space

우리가 보는 장면, 카메라가 보는 장면이라 해서 Camera Space, Eye Space라고 불리기도 한다.
조금 어렵게 보일 수 있지만, 장면을 그리기 위해서 World Space의 앞/뒤, 위/아래, 위치를 변환하는 과정이다.

이 과정에서 카메라의 위치, 카메라가 보는 방향, 그리고 카메라의 정수리 방향이 어딘지 정하고,
회전, 이동 행렬들을 조합한 변환 행렬을 만들어 기존의 World Space 정보에 적용시키는 과정이다.

이렇게 적용되었을 때, 우리가 정한 너비, 크기에 해당하게 물체를 그릴지 판단하는 공간이 Clip Space이다.


Clip Space

우리가 보는 시야는 제한되어있기 때문에, 인간은 기계도 인간과 같이 표현하길 원한다.
표현한다면 기계는 수치적 연산을 통해 뒤까지 볼 수 있는 잠자리의 시야각을 표현할 수 있지만, 인간은 불편하다 느낀다.
그렇기에 인간이 느끼는 것처럼 제한된 시야를 보게 하기 위해 x, y의 좌표를 [-1~1]까지로 제한하여 정규화시킨다.
이러한 작업을 Clip이라 표현하여 그리지 않을 정보를 취급하지 않게 한다.

이렇게 만들어진 Viewing Box는 절두체(Frustum)이라고 불리고 절두체 내부의 물체들은 사용자의 화면에 나타난다.
이런 절두체를 화면에 표시하는 방법은 Orthographic Projection, Perspective Projection으로 2가지 정도 있다.

Orthographic Projection

이 방법은 너비, 높이를 그대로 찍는 방식으로 시야각을 고려하지 않고 주물을 찍어내듯 찍는 방식이다.

Orthographic Projection

 

Perspective Projection

원근감을 표현하기 위해 멀어질수록 작게 보이게 하기 위한 방법이다.

Prospective Projection

 

둘의 가장 큰 차이점은 w값을 조작하는가?이다.
우리가 가져온 물체들은 3차원의 정보들이었지만, 우리는 4차원의 정보를 다뤄야 원근감을 표현할 수 있다.|
이 4차원의 좌표계는 Homogeneous Coordinate라 한다.

 

Homogeneous Coordinate

기본적으로 우리는 3차원의 정보를 가져와 $vec4(Position, 1.0)$로 표현한다.
즉, $(2.0, 3.0, 4.0) -> (2.0, 3.0, 4.0, 1.0)$로 만들어주는데,
도대체 왜? 이게 원근법과 관련이 있나?라는 의문이 계속될 것이다.

계속해서 $(x, y, z) -> (x, y, z, 1.0)$으로 변환의 역변환은 $(x, y, z, 1.0) -> (x/1.0, y/1.0, z/1.0)$이다.
즉, 1.0으로 넣은 이유는 결국 x, y, z에 1.0을 곱한 변환인 것이다.
다시 말해서 $(x, y, z) -> (wx, wy, wz, w)$으로 변환의 역변환은 $(wx, wy, wz, w) -> (x, y, z))$이다.

위에서 Prospective Projection에서만 w값을 조작하는 이유가 여기 있다.
만일 $w = z$라고 한다면? $(x, y, z, z) -> (x/z, y/z, 1)$로 간단하게 확인할 수 있다.
또한, 여기서 Depth Value의 그래프가 $\frac {1}{z}$인 이유를 알 수 있다.

그리고 w값이 0이 된다면 Zero Division Error가 발생할 테니 z의 값은 0보다는 큰 값이 될 것이다.
즉, z의 범위는 [0~1]까지가 될 것이다.


정리

우리가 가져온 정보는 Local Space에 존재하기에 물체의 정보에 따른 변환 행렬을 통해 World Space로 변환하는 과정이 필요하다.
World Space에서는 우리가 그리고 싶은 장면을 표현하기 위해 View Space로의 변환이 필요한데,
이 과정에서 앞/뒤, 위/아래, 원점의 변환이 필요하다.
View Space에서 필요한 정보만 얻기 위해 Clip Space로 변환하고, Projection 하여 우리가 보는 장면이 탄생한다.

여기까지 OpenGL의 좌표계 변환 과정이다.

'OpenGL' 카테고리의 다른 글

Normal Vector의 변환 행렬  (0) 2022.11.14
OpenGL[Lighting]  (0) 2022.10.18
OpenGL[02](First Triangle)  (1) 2022.10.12
OpenGL[01](Shader)  (1) 2022.10.12
OpenGL[00](First Window)  (0) 2022.10.12

우리는 삼각형을 그릴 텐데 Shader를 불러오고, VAO, VBO, EBO를 연결해준 뒤 무한루프에서 그릴 수 있게 해 주면 된다.

// shader.vs

#version 460 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
}

// shader.fs

#version 460 core
out vec4 FragColor;

in vec3 ourColor;

void main()
{
    FragColor = vec4(ourColor, 1.0f);
}

GLSL로 작성된 Vertex Shader, Fragment Shader를 읽어와서 컴파일한 후, 프로그램에 붙여주어야 한다.

GLuint	vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vertexShaderCode, NULL);
glCompileShader(vertex);

GLuint	fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fragmentShaderCode, NULL);
glCompileShader(fragment);
  1. 해당하는 Shader Type에 따라 생성해주어야 한다.
  2. ShaderCode는 해당 파일에서 const char*로 읽어온다.
  3. 해당하는 Shader에 코드를 넣어준다.
  4. 컴파일해준다.

프로그램도 쉐이더를 불러오는 방식과 비슷하다.

GLuint	ID = glCreateProgram();

glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);

glDeleteShader(vertex);
glDeleteShader(fragment);
  1. 프로그램 생성
  2. Vertex Shader, Fragment Shader를 프로그램에 붙여준다.
  3. 프로그램을 링크해준다.
  4. 사용한 vertex, Fragment는 지워준다.

이제 쉐이더 끝!


해당하는 정점과, 정점의 인덱스를 선언하여 VAO를 바인드 하여 VBO, EBO에 정보를 전달해준다.

float vertices[] = {
    0.5f, 0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
    -0.5f, 0.5f, 0.0f
};
uint32_t indices[] = {
    0, 1, 3,
    1, 2, 3
};

해당 정점, 순서를 지정한다.

GLuint	VAO, VBO, EBO;

glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
  1. VAO 생성해서 바인드 한다.
  2. VBO 생성해서 바인드 하면 GL_ARRAY_BUFFER를 타깃으로 하는 모든 버퍼는 VBO를 사용하게 된다.
  3. glVertexAttribPointer()로 Vertex Shader에서 사용할 정점 속성에 대해 알려준다.
    • 첫 번째 파라미터가 Vertex Shader에서의 layout의 위치다.
    • 두 번째 파라미터는 해당 정점의 속성 값의 크기이다.
    • 세 번째 파라미터는 해당하는 데이터의 타입이다.
    • 네 번째 파라미터는 데이터를 정규화할 것인지 정하는 것인데, GL_TRUE로 설정하면 [0~1], 부호가 있다면 [-1~1]의 범위를 갖게 한다.
    • 다섯 번째 파라미터는 데이터가 시작하는 위치의 offset이다.
  4. glEnableertexAttribArray()로 해당하는 Vertex Shader에서의 layout의 위치를 사용할 수 있게 한다.
  5. EBO 또한 VBO와 같은 방식이지만, GL_ELEMENT_ARRAY_BUFFER를 바인딩한다.

이후 무한 루프에서 그려주면 된다.

while (!glfwWindowShouldClose(window))
{
	glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);
	glUseProgram(m_program);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
	glfwSwapBuffers(window);
	glfwPollEvents();
}

해당 프로그램을 사용하도록 해준다.

glDrawElements()를 이용해서 그릴 프리미티브의 특성을 정해준다.

  • 첫 번째 파라미터는 프리미티브의 유형을 정해준다.
  • 두 번째 파라미터는 몇 개의 정점을 그릴지에 대해 정한다.
  • 세 번째 파라미터는 정점 인덱스의 자료형을 정한다.
  • 네 번째 파라미터는 정점 인덱스의 오프셋이다.

 

'OpenGL' 카테고리의 다른 글

OpenGL[Lighting]  (0) 2022.10.18
OpenGL[Coordinate System]  (0) 2022.10.18
OpenGL[01](Shader)  (1) 2022.10.12
OpenGL[00](First Window)  (0) 2022.10.12
vcpkg를 이용한 GLFW, GLM, ASSIMP, GLAD setup  (0) 2022.10.06

Shader

물체의 Shading을 할 때 GPU에서 돌리는 프로그램이다.
Shading은 가상의 3D 공간에서 물체의 표면, 색, 빛, 카메라, 재질 등을 표현하기 위해 설정하는 다양한 설정값들을 바탕으로 표현하는 과정을 의미한다.


OpenGL Shader Language(GLSL)

C언어를 기반으로 하며 OpenGL에서 사용하는 Shading 언어다.
Types는 기본적인 것들은 대부분 갖고 있으며 Vector, Matrices가 있다.

실제로 OpenGL을 이용해서 무언가를 그리기 위해선 Vertex Shader, Fragment Shader가 필요하다.
그리고 OpenGL은 실제로 점, 선, 삼각형밖에 그리질 못하고 이것을 우리는 프리미티브(Primitive)라고 한다.

3D 모델링을 위해 폴리곤이라는 것을 검색해보면 많은 수의 삼각형 또는 사각형 등으로 조각조각되어있다.
우리는 그 삼각형 또는 사각형 조각 하나를 프리미티브라고 부른다.
즉, 3D 모델은 수많은 프리미티브를 조합한 것이다.

프리미티브는 실제로 주어진 정점에 따라 도형을 그린다는 것에 초점을 둔다.
정점이 1개면 점, 2개면 선, 3개면 삼각형인 것이다.
그렇다면 우리는 정점들의 정보를 갖고 다양한 색을 표현하여 모델을 그린다는 것을 알 수 있다.


Vertex Shader

OpenGL은 Vertex Shader로 해당하는 정점의 정보를 어떤 정보인지 확인해서 넘겨준다. [layout]
하지만 정점 정보만 받는 것이 아니라, 받은 정점의 정보에 변동점(공간 데이터)을 주는 정보도 받는다. [Uniform]
이렇게 받은 두 종류의 정보를 이용하여 Vertex Shader는 정점의 정보를 건네준다. [out]

Rasterization

Vertex Shader에서 건네주는 정보들은 OpenGL에서 일련의 과정에서 프리미티브가 생성되어 픽셀당의 데이터로 표현한다.
이렇게 픽셀당 데이터로 표현하기 위해서 지정한 개수의 정점들을 보간하여 픽셀의 데이터를 정한다.

Fragment Shader

그렇게 프리미티브는 픽셀의 정보들로 변환되어 Fragment Shader에 보내진다.
그리고 픽셀들의 정보들과 OpenGL로부터 받은 빛, 텍스쳐의 정보를 이용하여 색을 적용시킨다.
결과적으로 픽셀의 값들을 Frame Buffer로 전달된다.

이후는 여러 가지 테스트들이 실행된다. 사실 위 세 가지 과정 사이에도 몇 가지 과정들이 숨어있는데 이후에 정리해야겠다.


VAO & VBO & EBO

우리가 Vertex Shader에 정점의 정보를 전달할 때, OpenGL에서는 VAO(Vertex Array Object), VBO(Vertex Buffer Object), EBO(Element Buffer Object)를 이용해서 전달한다.

VBO는 정점의 정보를 담고 있는 배열의 객체이다. 해당 배열에는 정점의 정보를 넣는 순서대로 들어간다.
VAO는 여러 개의 VBO를 바인드 받아서 저장하고 있는 객체이다.
그리고 VBO에 지정된 순서대로 Vertex Shader로 들어간다.

여기서 EBO가 있는 이유는 중복 데이터 제거의 용도인데, 이는 예시를 들어 이해하는 것이 빠르다.

VBO

위의 같은 경우엔 6개의 정점을 선언했지만 따지고 보면 1,5와 2,3의 정점은 똑같은 정점이 된다.
즉, 선언은 6개, 사용은 4개인 것으로 2개의 정점의 자원이 낭비된다는 것이다.

 

EBO

해당 VBO는 4개의 정점을 선언하고, EBO에 정점의 순서를 지정한 것이 된다.
이렇게 진행했을 경우엔 낭비되는 자원이 없다.

'OpenGL' 카테고리의 다른 글

OpenGL[Lighting]  (0) 2022.10.18
OpenGL[Coordinate System]  (0) 2022.10.18
OpenGL[02](First Triangle)  (1) 2022.10.12
OpenGL[00](First Window)  (0) 2022.10.12
vcpkg를 이용한 GLFW, GLM, ASSIMP, GLAD setup  (0) 2022.10.06

앞으로 LearnOpenGL을 참고하여 OpenGL을 공부할 것이다.
OpenGL을 공부하는 이유는 컴퓨터 그래픽스의 파이프라인을 이해하는 것이다.

시작!


기본적인 세팅

// GLFW 초기 설정
    if (!glfwInit())
	{
		const char*	description = nullptr;
		glfwGetError(&description);
		return (putError("Failed to initialize glfw: " + std::string(description)));
	}
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

GLFW는 glfwinit()을 이용해 초기화된다.
기본적으로 하드웨어에서 호환되는 버전을 명시하여 사용해야 하고, OPENGL_PROFILE에 Core-profile을 사용하도록 명시한다.

glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

Mac OS에서는 위의 코드를 추가해야 성공적으로 작동한다.

// GLFW를 이용한 Window생성
	GLFWwindow*	window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_NAME, nullptr, nullptr);
	if (!window)
	{
		glfwTerminate();
		return (putError("Failed to create glfw window"));
	}
	glfwMakeContextCurrent(window);
	
// GLAD 초기 설정
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		glfwTerminate();
		return (putError("Failed to initialize glad"));
	}

glfwCreateWindow()를 이용하여 현재 윈도우의 가로, 세로를 픽셀로 나타내고 윈도우의 프로그램 제목을 나타낸다.
glfwMakeContextCurrent()를 이용하여 내가 현재 만든 윈도우를 현재 창으로 설정하는 것이다.

void	OnFramebufferSizeChange(GLFWwindow* window, int width, int height)
{
	std::cout << "FrameBuffer size changed: (" << width << ", " << height << ")" << std::endl;
	glViewport(0, 0, width, height);
};

void	OnKeyEvent(GLFWwindow* window, int key, int scancode, int action, int mods)
{
	std::cout << "key: " << key << "\tscancode: " << scancode;
	std::cout << "\taction: " <<	(action == GLFW_PRESS ? "Pressed" : 
								(action == GLFW_RELEASE ? "RELEASE" :
								(action == GLFW_REPEAT ? "REPEAT" : "UNKNOWN")))
			<< "\tmods: " << (mods & GLFW_MOD_CONTROL ? "C" :
							(mods & GLFW_MOD_SHIFT ? "S" :
							(mods & GLFW_MOD_ALT ? "A" : "---"))) << std::endl;
	if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);
};

우리는 위의 함수 두 개로 키가 눌리고 창의 크기가 변경되는 것을 콜백 함수로 둘 것이다.

	glfwSetFramebufferSizeCallback(window, OnFramebufferSizeChange);
	glfwSetKeyCallback(window, OnKeyEvent);

glfwSet~Callback() 함수들은 함수 포인터를 가져가서 콜백 함수로 등록해주는 역할이다.

	while (!glfwWindowShouldClose(window))
	{
		glfwSwapBuffers(window);
		glClearColor(0.0f, 0.1f, 0.2f, 0.0f);
		glClear(GL_COLOR_BUFFER_BIT);
		glfwPollEvents();
	}
	glfwDestroyWindow(window);
	glfwTerminate();

glfwWindowShouldClose()는 해당 윈도우를 종료해야 하는가?를 물어보는 함수이다.
즉, 종료하기 전까지는 FALSE의 반환 값으로 실행된다.
GLFW의 윈도우는 기본적으로 이중 버퍼인데, 이전에 그렸던 것과 현재 그렸던 것을 바꾸는 과정을 glfwSwapBuffers()로 처리한다.
glfwPollEvents()는 윈도우와 관련된 다른 이벤트를 다루는데 예를 들면 마우스, 키보드의 입력을 말한다.
glClearColor()glClear() 직전에 호출되고 해당 윈도우의 Color Buffer가 지워질 때 해당 윈도우의 Color Buffer에 넣어질 값을 설정한다.
윈도우가 꺼지게 되면 무한 루프를 탈출하여 glfwDestroyWindow()와 glfwTerminate()로 모든 자원들이 정리될 수 있다.

'OpenGL' 카테고리의 다른 글

OpenGL[Lighting]  (0) 2022.10.18
OpenGL[Coordinate System]  (0) 2022.10.18
OpenGL[02](First Triangle)  (1) 2022.10.12
OpenGL[01](Shader)  (1) 2022.10.12
vcpkg를 이용한 GLFW, GLM, ASSIMP, GLAD setup  (0) 2022.10.06

내가 OpenGL을 위해 사용하는 CMakeList.txt를 예시로 적어보자.

# CMake 최소버전 표기
cmake_minimum_required(VERSION 3.24)

# 프로젝트 이름 지정
set(PROJECT_NAME OpenGL_example)

# 사용할 C++ 버전 표기
set(CMAKE_CXX_STANDARD 17)

# 프로젝트 지정
project(${PROJECT_NAME})

# 빌드 대상 소스파일
add_executable(${PROJECT_NAME} src/main.cpp)

# 외부 라이브러리를 불러오면서 저장소 분리를 위한 Dependency 명령어 추가
include(ExternalProject)

# Dependency 관련 저장소 분리
set(DEP_INSTALL_DIR ${PROJECT_BINARY_DIR}/install)
set(DEP_INCLUDE_DIR ${DEP_INSTALL_DIR}/include)
set(DEP_LIB_DIR ${DEP_INSTALL_DIR}/lib)

# 외부 라이브러리 GIT으로 가져옴
ExternalProject_Add(
    dep_glfw			# 이름
    GIT_REPOSITORY  "https://github.com/glfw/glfw.git"	# git주소
    GIT_TAG         "3.3.8"	# git의 tag
    GIT_SHALLOW     1		# 아래 설명참고
    UPDATE_COMMAND  ""		# 업데이트 단계 재정의
    PATCH_COMMAND   ""		# 패치 단계 재정의
    TEST_COMMAND    ""		# 테스트 단계 재정의
    CMAKE_ARGS      -DCMAKE_INSTALL_PREFIX=${DEP_INSTALL_DIR}
                    -DGLFW_BUILD_EXAMPLES=OFF
                    -DGLFW_BUILD_TESTS=OFF
                    -DGLFW_BUILD_DOCS=OFF
	# 해당하는 라이브러리에 CMake를 열어보면 우측의 Flag, 변수 등이 있는데 자유롭게 조절가능
)

# Dependency 리스트 및 라이브러리 파일 리스트 추가
set(DEP_LIST ${DEP_LIST} dep_glfw)
set(DEP_LIBS ${DEP_LIBS} glfw3)

ExternalProject_Add(
    dep_glm
    GIT_REPOSITORY      "https://github.com/g-truc/glm.git"
    GIT_TAG             "0.9.9.8"
    GIT_SHALLOW         1
    UPDATE_COMMAND      ""
    PATCH_COMMAND       ""
    CONFIGURE_COMMAND   ""
    BUILD_COMMAND       ""
    TEST_COMMAND        ""
    INSTALL_COMMAND     ${CMAKE_COMMAND} -E copy_directory
        ${PROJECT_BINARY_DIR}/dep_glm-prefix/src/dep_glm/glm
        ${DEP_INSTALL_DIR}/include/glm
)

# 위의 과정과 조금 다른 부분은 LIB를 설정하지 않았다는 것인데, 없어서 그렇다.
set(DEP_LIST ${DEP_LIST} dep_glm)

# ImGui는 직접 가져가서 사용하는 것을 권장하여 개발환경에 맞춰서
# 직접 복사해서 가져온다.
add_library(imgui
    imgui/imgui_draw.cpp
    imgui/imgui_tables.cpp
    imgui/imgui_widgets.cpp
    imgui/imgui.cpp
    imgui/imgui_impl_glfw.cpp
    imgui/imgui_impl_opengl3.cpp
    )
# ImGui에서는 개발환경에 따라 의존성을 요구하기에 현재까지 있었던 의존성을 엮어준다.
target_include_directories(imgui PRIVATE ${DEP_INCLUDE_DIR})

# 링크, 의존성등은 비슷하게 진행된다.
add_dependencies(imgui ${DEP_LIST})
set(DEP_INCLUDE_DIR ${DEP_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/imgui)
set(DEP_LIST ${DEP_LIST} imgui)
set(DEP_LIBS ${DEP_LIBS} imgui)

# include / lib 관련 옵션 추가 {의존성, 링크 관련하여 설정할 내용}
target_include_directories(${PROJECT_NAME} PUBLIC
    ${DEP_INCLUDE_DIR}
)
target_link_directories(${PROJECT_NAME} PUBLIC
    ${DEP_LIB_DIR}
)
target_link_libraries(${PROJECT_NAME} PUBLIC
    ${DEP_LIBS}
)

target_compile_definitions(${PROJECT_NAME} PUBLIC
    WINDOW_NAME="${PROJECT_NAME}"
)

# Dependency들이 먼저 Build 되도록 설정한다.
add_dependencies(${PROJECT_NAME}
    ${DEP_LIST}
)

 

Shallow Clone

간혹 git에서 매우 큰 저장소를 clone 하게 되면 시간도 오래 걸릴뿐더러 중간에 실패할 수도 있다.
그럴 때 우리는 Shallow clone을 사용하는데, 해당 git에 전체 이력을 가져오는 게 아니라 일부만 가져오는 것이다.
위에서 우리는 1로 지정했는데 이는 1개의 커밋 히스토리를 불러오는 것이다.

'CMake' 카테고리의 다른 글

CMake[01]  (0) 2022.10.12
CMake[00]  (0) 2022.10.12

CMake는 Make를 사용할 적에 흘리듯이 들었던 링크, 의존성 등의 과정에서 문제가 아주 효율적으로 해결된다.
CMake도 Make와 마찬가지로 의존성 검사를 해서 빌드되지만, 큰 차이점은 CMake는 의존성 정보를 스스로 파악한다는 것이다.
Make에서는 빌드시 중간과정의 Object파일의 이름, 의존성 정보들까지 입력해야 하지만, CMake는 그렇지 않다는 것이다.

정말 간단한 예제를 보자.

# CMake의 최소 버전 입력
cmake_minimum_required(VERSION 3.24)

# CMake의 변수 생성 { PROJECT_NAME = "cmake_project_example"}
set(PROJECT_NAME cmake_project_example)
# CMake에서의 C++ 사용버전
set(CMAKE_CXX_STANDARD 17)

# project 생성
project(${PROJECT_NAME})

# project의 소스 파일 추가
add_executable(${PROJECT_NAME} src/main.cpp)

이렇게 우리가 사용할 CMakeList.txt가 생성되었다.
Header파일도 Object파일도 명시되어있진 않지만 CMake는 내부적으로 처리된다.
(혹시, Object파일이 보고 싶다면 Build파일을 잘 살펴보시길 바랍니다.)

'CMake' 카테고리의 다른 글

CMake[02]  (0) 2022.10.12
CMake[00]  (0) 2022.10.12

분명히 나는 컴퓨터 그래픽스의 공부로 OpenGL을 공부하고 싶었으나...
노트북은 맥, 데스크톱은 윈도우로 운영체제가 달랐고...
맥은 심지어 Metal이라는 고유의 그래픽 API를 사용하면서 OpenGL, OpenCL의 지원을 중단해버렸다.

그래서 나는 고민을 해야 했다.
맥에서 윈도우를 돌릴 방법은 여러 가지 있지만, 그래픽스의 작업이기 때문에 자원을 잡아먹는걸 내가 컨트롤할 자신이 없었다.
고로 Xcode, Visual Studio, Make... 등의 빌드 환경에 따라 빌드 파일을 생성하고 실행, 디버깅할 수 있게 해 줄 것이 필요했다.

예전 맥이 없던 시절의 나는 클러스터에 꼭 나와야 했었다.
어찌어찌 주먹구구식으로 배웠던 Makefile 작성법을 사용하여 과제를 해왔으며,
Makefile이 익숙해져 버린 탓에 데스크톱(윈도우)에서의 개발환경을 조성하질 못하고 붕 떠있었다.

그렇게 여러모로 Cmake는 알 수 없었던 내 상황에서의 타개책이 될 수 있었다.

처음 만났던 Cmake는 굉장히 무섭고, 두려운 존재였다.
왜냐? 처음 봤던 CMakeList.txt는 몸집이 굉장했고 범접할 수 없다 생각이 들 수밖에 없는 라인의 수였기 때문이다.
하지만, 돌고 돌고 돌아서 다시 CMake를 접했을 땐, '생각보다 튜토리얼이 많았다.', '이전엔 도대체 뭘 본거야;;' 대충 이런 생각이었다.

이렇게 나는 CMake를 접했고 작성하기 시작했다.

'CMake' 카테고리의 다른 글

CMake[02]  (0) 2022.10.12
CMake[01]  (0) 2022.10.12

+ Recent posts