본문 바로가기
대학생활/수업

게임그래픽프로그래밍 13주차 - 게임엔진과 구성요소에 대한 이해

by se.jeon 2023. 6. 12.
728x90
반응형

게임엔진

게임을 만들기 위한 저작 도구

 

게임엔진 구성 요소

- 게임 엔진은 렌더링(가장 중요), AI, 물리, 사운드, UI 등 다양한 구성요소로 구성된다.

- 게임엔진의 공간은 게임 개체들을 배치파고, 이벤트를 설정하고, 게임 내부의 가상 공간을 구성한다.

- 게임 엔진에서 씬(Scene)은 게임을 최종적으로 가시화할 때 게임 공간을 구성하고 있는 모든 객체들의 집합

- Scene의 구성 요소 : GameObject, Camera, Light, Material, Texture, UI, Sound

 

Scene Graph

- Scene을 구성하고 있는 객체들과의 관계를 계층적으로 표현한 데이터 구조

- Scene Graph의 하나의 Node는 Scene을 구성하고 있는 객체

- 하나의 Node들은 여러 개의 자식 Node들을 소유 가능하다.

- 매 프레임마다 Scene Graph를 탐색하면서 메인 카메라 기반으로 가시화 가능한 객체들만 화면에 렌더링을 진행한다.

- 카메라는 반드시 1개는 존재해야한다. Camera는 사용자가 보는 시야를 결정하기 때문.

 

Scene Graph의 데이터 구조

씬 그래프의 데이터 구조는 트리 구조를 가지고 있는 경우가 많다.

부모가 움직이면 자식 오브젝트도 같이 움직여야 한다. 이를 수학적으로 어떻게 표현할 수 있는가?

자신의 부모에 대한 변화도 감지하여 변화시킨다. 부모에 있는 world matrix를 가져와서 곱해주는 형태.

 

카메라

- Scene의 모습을 최종적으로 결정하는 역할

- 카메라의 위치 (FPS - 얼굴, 눈/ TPS - 머리 뒤)

구현적 이유, 몰입적 이유

- 카메라의 타겟 위치

- FOV(Field of View) : vertical

- Aspect Ratio : 가로/ 세로

- Near Plane과의 거리 : Clipping, 절두체를 만들기 위함.

- Far Plane과의 거리 : 멀어지면 렌더링 하는 부분이 많아진다. (Culling이 들어감에도.)

 

카메라에서 결정되는 중요 행렬 2개

- View Matrix : 카메라의 위치를 기준으로 카메라 공간으로 이동.

- Projection Matrix (투영 행렬) : 투영을 위해서 투영 공간으로 이동.

2개의 행렬은 카메라의 구성요소들을 바탕으로 결정된다.

 

카메라와 ViewMatrix 결정방법 첫 번째

- 카메라의 위치와 카메라의 yaw, pitch, roll 회전을 통한 처리

- 카메라의 위치와 카메라의 바라보는 지점을 통한 처리

 

카메라와 ViewMatrix 결정방법 두 번째

- 카메라의 위치와 카메라의 바라보는 지점을 통한 처리

- 바라보는 지점을 이동 시 카메라의 위치를 같이 이동 가능하다.

바라보는 위치 - 현재 카메라의 위치를 통해 벡터를 구한다.

구한 벡터를 z축으로 삼고, 정규화한다.

카메라가 월드상의 y축을 기준으로 위를 향하고 있는지, 아래를 향하고 있는지 체크하여 업벡터를 준다.

y축과 z축을 외적해서 x축을 만든다.

z축과 x축을 외적시켜서 y를 구한다.

x = y 외적 z

y = z 외적 x

z = x 외적 y

회전시켜서 x축을 맞춰준다.

이를 반대로 해서 현재 카메라의 위치 - 바라보는 위치를 할 수 있다.

 

현재 오른손 좌표계 기반으로 외적 진행 시 축의 방향 주의

뷰 공간의 z축을 만들기 위한 구성

z_axis = 카메라 바라보는 지점 - 카메라의 위치

z_axis 정규화

 뷰 공간의 x축을 만들기 위한 구성

z_axis = up vector와 z_axis와의 외적

z_axis  정규화

 뷰 공간의 y축을 만들기 위한 구성

y_axis = z_axis와 x_axis와의 외적

y_axis 정규화

 

up vector는 사용자에 따라 마음대로 들어올 수 있다. 그래서 정규화를 한번 더 해 주게 된다.

어떠한 예외사항에 대해서도 처리를 해야하기 때문에 정규화를 다시 진행 해 준다.

 

up vector을 일반적으로 (0, 1, 0)을 이용해서 축을 계산할 때 이용한다.

up vector가 (0, -1, 0)으로 사용 할 수도 있음.

각 축을 구한 후에 해당 내용을 회전행렬에 대입하면 된다.

카메라가 바라보는 방향과 월드 공간의 y축이 평행이면 카메라 공간의 x축 계산은 외적을 통해서 구하지 않고 (1, 0, 0)으로 예외처리 된다 : 내적은 cos, 외적은 sin이기 때문에 예외처리가 필요함.

 

최종 View 행렬 계산

카메라의 이동과 회전행렬을 T, R로 정의한다.

- 카메라의 모델행렬(Mcam)의 역행렬이 뷰행렬이다.

최종 뷰 공간은 y축 기준 π회전 필수이므로 y축 회전행렬을 고려해야 한다.

 

카메라와 Projection Matrix 결정 방법 첫 번째

- 원근 투영을 위해서 사용되는 Projection Matrix

- 카메라의 위치와 카메라의 종횡비, 시야각, 근, 원근 평면의 거리.

 

게임 오브젝트

- Scene을 구성하는 가장 기본 객체

- 객체의 위치, 회전, 크기 변환에 대한 정보 (World Matrix)

- Mesh에 대한 정보

- Material에 대한 정보 : color, texture, material info

- 게임 상의 플레이어, 적, NPC, 사물들을 표현.

- Scenegraph의 하나의 Node로 사용. (부모-자식 관계이다.)

- 부모 자식 관계를 통한 계층 구조로 다양한 객체를 표현 가능하다.

 

게임 오브젝트에서 결정되는 중요한 행렬

- Local Space에서 World Space로 이동하는 Model Matrix

- 이동, 회전, 크기 변환을 하나의 행렬로 표현

- Mmodel = T⋅R⋅S = T⋅Ry⋅Rx⋅Rz⋅S

 

메쉬(Mesh)

- 모델을 표현하기 위해 필요한 기하학적 데이터 구조.

- 일반적으로 삼각형으로 구성되어져 있음.

- 정점 정보. (같은 정점이여도 UV값이 다를 수 있다. 이 경우에는 2개로 나누어져야 한다.)

- 페이스 정보. (삼각형에 대한 Index 정보, 3개씩 묶인다.)

- UV 정보.

- Normal 정보.

- Weight 정보. (Skinning을 위해 옵션)

 

메쉬(Mesh)를 이용한 그리기 방식

- 일반적으로 다수의 삼각형으로 구성

- 정점 정보를 가지고 있는 정점 버퍼

- 삼각형을 이루고 있는 정점의 인덱스에 대한 인덱스 버퍼

- 3D 저작 툴을 통해서 데이터 파일로 추출 (ex : FBX 파일)

- 리소스 로딩을 통해 정점 버퍼와 페이스를 이루는 인덱스 버퍼 구성

- 렌더링 시 인덱스 버퍼의 3개 점을 이용해서 삼각형을 렌더링한다.

 

메쉬(Mesh) 그리기의 최적화

불필요한 뒷면을 그리지않는다 : 백페이스 컬링 (Backface Culling)

뒷면이라는 것은 무엇인가? : 카메라의 바라보는 방향과 삼각형의 법선 방향이 일치하는 경우

뒷면인 경우에는 렌더링을 하지 않게 하여 속도를 최적화 할 수 있다.

- 카메라의 바라보는 방향과 같은 방향

- 카메라의 바라보는 방향과 반대 방향 : 반대로 하기로 했기 때문에, 내적을 했을 때 음수가 나와야 한다.

 

백페이스 컬링 수학적 계산 방법

- 삼각형 세 점 a, b, c가 있을 때 a의 점을 기준으로 ab, ac 벡터를 구성한다.

- 두 개의 벡터를 이용해서 외전을 계산해서 삼각형의 법선 벡터를 계산한다.

- 카메라의 바라보는 벡터와 삼각형 법선 벡터의 내적으로 계산한다.

- 삼각형 노말 벡터 N(Nx, Ny, Nz)

- 삼각형 방향 벡터 N(Nx, Ny, Nz)

D⋅N < 0 이면 삼각형 렌더링

- cos는 -90에서 90가 양수, 나머지는 음수이다.

 

렌더링 파이프라인 구축

렌더링 파이프라인 구축

- Local Space : 객체의 메쉬 데이터 (삼각형)

- World Space : Model Matrix를 통해 구축 (T⋅R⋅S)

- View Space : View Matrix를 통해 구축

- Clip Space : Projection Matrix를 통해 구축, NDC로 추가 변환

- Screen Space : Viewport Matrix를 통해 구축

 

시험 공지 진행.
지필 시험 + 손코딩 진행.

 

실습 진행.

첫 번째 예제를 통해 만든 결과물을 통해 이후 결과물을 만들 예정.

총 여섯개 실습 진행.

 

정육면체 cube

- 점의 개수 : 8개

- 인덱스 : 6*2(삼각형)*3(개수) = 36개

 

import pygame
import numpy as np
import math
from pygame.locals import *


# 화면 설정
WIDTH, HEIGHT = 1280, 720
BG_COLOR = (105,105,105)
CUBE_COLOR = (255, 0, 0)


# 메쉬 데이터 초기화
# 정점 데이터 구성
cube_size = 100
half_size = cube_size / 2

# 동차좌표계를 사용하여 4차원까지 확장
tripoint = np.array([
    [-1*half_size, -1*half_size,  1*half_size, 1],
    [ 1*half_size, -1*half_size,  1*half_size, 1],
    [ 1*half_size,  1*half_size,  1*half_size, 1],
    [-1*half_size,  1*half_size,  1*half_size, 1],
    [-1*half_size, -1*half_size, -1*half_size, 1],
    [ 1*half_size, -1*half_size, -1*half_size, 1],
    [ 1*half_size,  1*half_size, -1*half_size, 1],
    [-1*half_size,  1*half_size, -1*half_size, 1],
])

# 삼각형을 이루는 정점 인덱스 구축
# 모든 면은 오른손 좌표계

# 백페이스 컬링
# 면이 바라보고 있는 벡터를 노말 벡터라고 함.
# 삼각형 노말 벡터와 카메라 방향 벡터의 내적 결과 값이 0보다 크면 삼각형을 렌더링한다.
trifaces = [
    [0, 1, 2], #front
    [2, 3, 0], #front
    [7, 6, 5], #back
    [5, 4, 7], #back
    [4, 5, 1], #bottom
    [1, 0, 4], #bottom
    [3, 2, 6], #top
    [6, 7, 3], #top
    [4, 0, 3], #left
    [3, 7, 4], #left
    [1, 5, 6], #right
    [6, 2, 1], #right
]


# model matrix 구성
# numpy 라이브러리를 이용해서 단위행렬을 만들 때에 np.eye(차원)을 사용한다.
# 4차원 동차좌표계 생성
model_matrix = np.eye(4)

# 이동 변환 행렬
translate_pos = [0, 0, 0]
translate_matrix = np.array([
    [1, 0, 0, translate_pos[0]],
    [0, 1, 0, translate_pos[1]],
    [0, 0, 1, translate_pos[2]],
    [0, 0, 0, 1],
])

# 크기 변환 행렬
# 0면 결과 값이 0이 되어버리기 때문에 초기 값은 1이다.
scale_value = [1, 1, 1]
scale_matrix = np.array([
    [scale_value[0], 0, 0, 0], #열벡터
    [0, scale_value[1], 0, 0], #열벡터
    [0, 0, scale_value[2], 0], #열벡터
    [0, 0, 0, 1]               #동차좌표계를 위함. 결과 값이 벡터가 되게끔.
])

# 회전 변환 행렬
# 회전 변환 행렬의 값은 radian값이므로 주의해야 한다.
# math.radians or numpy.deg2rad를 사용한다. (둘은 똑같은 기능, pygame에서는 radian을 기준으로 함.)
# 각도를 돌릴 때 파이썬에서는 radian을 사용하기 때문.
rotation_value = [0, 0, 0]
rotation_x = np.array([
    [1, 0, 0, 0],
    [0, np.cos(rotation_value[0]), -np.sin(rotation_value[0]), 0],
    [0, np.sin(rotation_value[0]), np.cos(rotation_value[0]), 0],
    [0, 0, 0, 1],
])

rotation_y = np.array([
    [np.cos(rotation_value[1]), 0, np.sin(rotation_value[1]), 0],
    [0, 1, 0, 0],
    [-np.sin(rotation_value[1]), 0, np.cos(rotation_value[1]), 0],
    [0, 0, 0, 1]
])

rotation_z = np.array([
    [np.cos(rotation_value[2]), -np.sin(rotation_value[2]), 0, 0],
    [np.sin(rotation_value[2]), np.cos(rotation_value[2]), 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1],
])

# rotation : Ry·Rx·Rz
# yxz로 하는 것은 편의성 + 약속.
rotation_matrix = np.matmul(rotation_y, np.matmul(rotation_x, rotation_z))

# model matrix : T·R·S
model_matrix = np.matmul(translate_matrix, np.matmul(rotation_matrix, scale_matrix))


# view matrix 구성
# 카메라의 위치
cam_pos = np.array([0, 0, 300])

#바라보는 위치
target_pos = np.array([0, 0, 0])

# 카메라의 위쪽 방향 기준 벡터
# up vector를 의미한다. 어느 면을 기준으로 지반삼아 서 있는가.
up = np.array([0, 1, 0])

# x, y, z축은 모두 정규화 : 벡터의 노름을 반환하는 np.linalg.norm(라인 알고리즘) 이용.
view_z = (target_pos - cam_pos)          # 좌표 형태. 타겟부터 카메라까지의 거리
view_z = view_z / np.linalg.norm(view_z) # 스칼라의 나눗셈. 정규화를 하기 위함.

# 월드의 up vector와 view_z를 외적하면 카메라 기준의 x좌표계가 왼쪽으로 갈지, 오른쪽으로 갈 지 알 수 있다.
view_x = np.cross(up, view_z)            
view_x = view_x / np.linalg.norm(view_x) # 외적한 값을 정규화.

# z와 x를 외적하여 x와 z에 90도인 값을 구한다.
view_y = np.cross(view_z, view_x)
view_y = view_y / np.linalg.norm(view_y) # 외적한 값을 정규화


# 외적이 좌우, 내적이 앞뒤...
# 카메라의 모델행렬의 역행렬
cam_inverse_model_matrix = np.array([
    [view_x[0], view_x[1], view_x[2], -np.dot(view_x, cam_pos)],
    [view_y[0], view_y[1], view_y[2], -np.dot(view_y, cam_pos)],
    [view_z[0], view_z[1], view_z[2], -np.dot(view_z, cam_pos)],
    [0, 0, 0, 1]
])

# 호도법 기준
# -1은 180도 회전을 의미한다.
# y축을 기준으로 회전하므로 x, z값이 바뀐다.
rotation_y_180 = np.array([
    [-1, 0,  0, 0],
    [ 0, 1,  0, 0],
    [ 0, 0, -1, 0],
    [ 0, 0,  0, 1]
])

# y축을 180도 회전 시켜서 x, y축이 수학적인 2차원 좌표계처럼 보이게 수정 (마이너스를 반대로 하면 생략 가능)
view_matrix = np.matmul(rotation_y_180, cam_inverse_model_matrix)


# clip space : projection matrix
fov = math.radians(80)

k = WIDTH / HEIGHT      # aspection ratio
n = 0.1                 # near clip plane
f = 1000                # far clip plane
d = 1 / np.tan(fov / 2) # forcal length, tan값이 1/d인데, 이의 역수를 구해 길이를 구하기 위함.

# 원근투영
projection_matrix = np.array([
    [d/k, 0, 0, 0],                     # 종횡비의 역을 계산 해 준다.
    [0, d, 0, 0],                       
    [0, 0, (n+f)/(n-f), (2*n*f)/(n-f)], # 깊이를 구하는 과정. 원근 투영을 했을 때 z값이 어디에 오는가. depth 값을 구하기 위함.
    [0, 0, -1, 0]                       # 동차좌표계. Near는 좌표가 -1, Far는 1
                                        # 카메라의 정보만 가지면 모든 점에 사용이 가능하기 때문에
                                        # -1로 처리하고 마지막에 한 번에 계산하는 방식을 사용한다. (최적화)
                                        # 카메라를 기준으로 음수.
])


# screen space : viewport transform
viewport_matrix = np.array([
    [WIDTH/2, 0, 0, WIDTH/2],
    [0, -HEIGHT/2, 0, HEIGHT/2],
    [0, 0, 0.5, 0.5],
    [0, 0, 0, 1]
])


# 초기화
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))

# 게임 루프
running = True
while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False

    # Update
    proj_view_model_matrix = np.matmul(projection_matrix, np.matmul(view_matrix, model_matrix))
    transform_points = []

    for vertex in tripoint:
        # 3D 좌표계에서 2D 좌표계로 변환
        v = np.matmul(proj_view_model_matrix, vertex)

        # NDC 좌표 변환
        # 나중에 나눠주는 P1z에 해당한다.
        v /= v[3]

        # 화면 좌표 변환
        v = np.matmul(viewport_matrix, v)
        transform_points.append(v[:3]) # 마지막 동차좌표계는 필요하지 않음.


    # 렌더링
    screen.fill(BG_COLOR)

    # 12회 반복
    for face in trifaces:
        face_points = []

        # 3회 반복
        for j in face:
            # 각 삼각형 점들에 대한.
            # z는 찍어주는 데에 필요하지 않으니까 제외
            face_points.append(transform_points[j][:2])

        # 각 점들을 그려주면서 내부를 채우는 과정
        pygame.draw.polygon(screen, CUBE_COLOR, face_points)

    pygame.display.flip()
pygame.quit()
728x90
반응형