[Graphics] 렌더링 파이프라인 정리
렌더링 파이프라인
-
3차원 모델을 2차원 화면에 투영하는 렌더링 과정을 말한다.
-
벌칸 엔진의 렌더링 파이프라인 과정
Application
좌표계 정하기
- 2차원은 왼손 좌표계
- 3차원은 오른손 좌표계를 이용한다.
Geometry Processing
- 정점 정보를 3D 공간으로 변환하는 단계
- 모델, 뷰행렬
- 투영, 클립핑 등등
- 화면에 표시할 기하학적 요소들을 화면상에 투영시키는 과정
Resterization
- 정점 정보를 픽셀 데이터로 변환하는 과정
- 프로그래밍이 불가능한 단계이다.
- 첫번째 단계인 정점 정보를 입력으로 받는다.
- 클립 공간으로 변환된 정점들을 통해서 정점, 선분, 삼각형 단위로 처리한다.
- 주로 하는 일은 다음과 같다.
- Clipping
- Perspective Division
- Back-face Culling
- Viewport Transformation
- Scan Transformation
Clipping
- 절두체 외부 폴리곤은 버려졌지만 내부에 걸친 폴리곤을 잘라내는 작업이다.
Perpective Division
- 원근감을 구현하는 작업
- 클립 공간 요소들을 W로 나눈다.
- 이렇게 원근 분할을 마친 좌표계를 NDC(Normalized Device Coordinates)라고 한다.
NDC
- X,Y 좌표가 모두 -1~1, Z좌표는 0~1인 좌표계
- 스크린 좌표로 변환할 수 있도록 하기위한 마지막 좌표계다.
Back-face Culling
- 보이지 않는 물체의 뒷부분을 제거한다.
Viewport Transformation
- NDC 공간 좌표를 2D 스크린좌표로 변환한다.
Scan Transformation
- 프리미티브를 통해서 프래그먼트를 생성하고 프래그 먼트를 채우는 픽셀들을 찾는다.
- 각 필셀마다 정점 데이터들을 보간하여 할당한다.
Vertex Shader, Pixel Processing
- 정점 정보를 3D 공간으로 변환하는 단계
Local Space
- 3D 물체의 본인이 중심인 좌표계
Model Matrix
- Local 좌표계를 World 좌표계로 변환하기 위한 행렬
- 모든 물체를 화면상에 보이기 위해선 모든 물체의 중심을 월드의 한 점으로 지정해야한다.
- 그러기 위해선 Local 좌표계의 정점 정보를 World의 한 점을 기준으로 잡아서 변환 해주어야 한다.
- 이동, 회전, 크기 행렬을 통해 변환해준다. 이 행렬을 변환행렬이라고 한다.
이동 행렬 (Translate Matrix)
## 이동 행렬
moveX = -500
moveY = 0
moveZ = 0
moveMatrix = np.array([
[1, 0, 0, moveX],
[0, 1, 0, moveY],
[0, 0, 1, moveZ],
[0, 0, 0, 1]
])
회전 행렬 (Rotate Matrix)
## 회전 행렬
### yaw
rotateX = 0
rxMathSin = math.sin(math.radians(rotateX))
rxMathCos = math.cos(math.radians(rotateX))
rightRotationMatrix = np.array([
[rxMathCos, 0, rxMathSin],
[0, 1, 0],
[-rxMathSin, 0, rxMathCos]
])
### pitch
rotateY = 0
ryMathSin = math.sin(math.radians(rotateY))
ryMathCos = math.cos(math.radians(rotateY))
upRotationMatrix = np.array([
[1, 0, 0],
[0, ryMathCos, -ryMathSin],
[0, ryMathSin, ryMathCos]
])
### roll
rotateZ = 0
rzMathSin = math.sin(math.radians(rotateZ))
rzMathCos = math.cos(math.radians(rotateZ))
forwardRotationMatrix = np.array([
[rzMathCos, -rzMathSin, 0],
[rzMathSin, rzMathCos, 0],
[0, 0, 1]
])
### Yaw Pitch Roll 순으로 계산한다.
eulerRotationMatrix = np.matmul(upRotationMatrix, rightRotationMatrix)
eulerRotationMatrix = np.matmul(forwardRotationMatrix, eulerRotationMatrix)
e = eulerRotationMatrix
### 4차원으로 변환
eulerRotationMatrix = np.array([
[e[0][0],e[0][1],e[0][2],0],
[e[1][0],e[1][1],e[1][2],0],
[e[2][0],e[2][1],e[2][2],0],
[0, 0, 0, 1],
])
크기 행렬 (Scale Matrix)
## 크기 행렬
scale = 1
scaleMatrix = np.array([
[scale, 0, 0, 0],
[0, scale, 0, 0],
[0, 0, scale, 0],
[0, 0, 0, 1]
])
- 해당 행렬들을 곱할 떄는 순서를 주의해야한다.
- 순서가 바뀌면 제대로 적용이 되지 않는다.
- 열 기준 행렬 - 이동 회전 크기
- 행 기준 행렬 - 크기 회전 이동
srMatrix = np.matmul(scaleMatrix, eulerRotationMatrix)
srmMatrix = np.matmul(moveMatrix, srMatrix)
World Space
- 특정 점을 기준으로 좌표들이 위치한 공간
View Matrix
- 월드 공간에 있는 객체를 보기 위해선 카메라 관점으로 바라봐야한다.
- 그러기 위해 카메라 위치를 기준으로 객체들 좌표를 맞춰줘야한다.
# 뷰 변환 행렬 cameraPosition = [0, 0, -500] viewMatrix = np.array([ [1, 0, 0, -cameraPosition[0]], [0, 1, 0, -cameraPosition[1]], [0, 0, 1, -cameraPosition[2]], [0, 0, 0, 1] ])
View Space
- 카메라 위치를 기준으로 객체들이 정렬된 공간
Projection Matrix
- 공간상에 추상적으로 정의되어 있는 정점들을 직관적으로 2D 스크린상에 투영시켜야한다.
- 후에 스크린 비율에 맞게 변환하는 작업도 필요해서 NDC도 해야한다.
- 객체들을 평면에 투영해서 스크린에 표현 할 수 있는 2D 좌표로 만든다.
NDC (Normalized device coordinate)
- Projection후 정규화하는 작업
- Why?
- 후에 View port 변환시 스크린 해상도에 맞게 화면을 늘리기 때문
- 기기마다 해상도가 달라서 맞춰줘야 한다.
- 카메라의 FOV와 해상도는 다르다.
- FOV는 화면에 보이는 물체들의 범위를 늘리는 것
- 약간 사람과 잠자리의 시야 범위 느낌
- 해상도는 기기의 화면 넓이다.
- 유튜브에서 사람의 사야랑 잠자리의 시야를 똑같은 해상도의 기기에서 볼 수 있는 것과 같다.
# 투영 행렬 정의
# fov를 구하는 이유
# 1. 투영 평면과 카메라 사이거리 d, 2. 투영 평면의 높이
fov = 90
near = 0.1
far = 1000.0
# 투영 행렬, 원근투영
# 투영 평면과의 거리
d = 1 / math.tan(math.radians(fov)/ 2)
# 스크린 종횡비 k
reverseAspect = 1 / aspect
# 카메라에서 near 까지의 거리 n
n = near
# 카메라에서 far 까지의 거리 f
f = far
# NDC까지 적용한 원근투영 행렬
# NDC는 스크린의 종횡비를 변화하려는 축의 값만 변경하면 된다 d/-p1z(p1x/k, p1y)
# 그러나 이걸 최종 투영 + NDC를 적용한 최종행렬에 적용하려면 행렬식 내부에 -P1z가 들어가서
# 매 행렬계산마다 행렬을 새로 만들어주어야 한다.
# 이를 방지하기 위해 P1z를 3X3 행렬로 만들고 p1z를 연산벡터(정점 좌표값 벡터)에 넘겨준다.
# 그후 나온 결과 행렬의 z값(-P1z)을 나눠주면 NDC구현 완료다.
projectionMatrix = np.array([
[d * reverseAspect, 0, 0, 0],
[0, d, 0, 0],
[0, 0, (n + f) / (n- f), (2 * n * f) / (n - f)],
[0, 0, -1, 0]
])
Resterization
Clip Space
- 뷰 공간에서 화면에 투영하기 위해 사용되는 공간
- 좌표 값은 정규화되어 있어서 -1,1 범위이다.
Viewport Tansform
- 디스플레이에 2D 결과 화면을 가시화하는 단계
- depth buffer를 통해 보일 객체를 선택한다.
전체 코드
import pygame
import math
import numpy as np
# 화면 설정
screenWidth = 1280
screenHeight = 720
aspect = screenWidth / screenHeight
halfWidth = screenWidth / 2
halfHeight = screenHeight / 2
# 정육면체 설정
rectWidth = 100
rectHeight = 100
# 함수 정의
def TransMatrix(matrixs):
return np.array([matrixs[0] * halfWidth + halfWidth, -matrixs[1] * halfHeight + halfHeight])
def DrawLine(surface, x1, y1, x2, y2, color):
pygame.draw.line(surface, color, (x1, y1), (x2, y2))
def DrawCube(surface, vertices, edges, color):
for edge in edges:
p1 = vertices[edge[0]]
p2 = vertices[edge[1]]
DrawLine(surface, p1[0], p1[1], p2[0], p2[1], color)
# Pygame 초기화
pygame.init()
BACKGROUND_COLOR = (245, 245, 245)
LINE_COLOR = (20, 20, 20)
screen = pygame.display.set_mode((screenWidth, screenHeight))
pygame.display.set_caption("Graphics Programming")
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill(BACKGROUND_COLOR)
# 정육면체 정점 및 간선 정의
cubeVertices = np.array([
[-rectWidth / 2, -rectHeight / 2, -rectWidth / 2, 1],
[rectWidth / 2, -rectHeight / 2, -rectWidth / 2, 1],
[rectWidth / 2, rectHeight / 2, -rectWidth / 2, 1],
[-rectWidth / 2, rectHeight / 2, -rectWidth / 2, 1],
[-rectWidth / 2, -rectHeight / 2, rectWidth / 2, 1],
[rectWidth / 2, -rectHeight / 2, rectWidth / 2, 1],
[rectWidth / 2, rectHeight / 2, rectWidth / 2, 1],
[-rectWidth / 2, rectHeight / 2, rectWidth / 2, 1]
])
cubeEdges = [
(0, 1), (1, 2), (2, 3), (3, 0),
(4, 5), (5, 6), (6, 7), (7, 4),
(0, 4), (1, 5), (2, 6), (3, 7)
]
# 변환 행렬 정의
## 회전 행렬
### yaw
rotateX = 0
rxMathSin = math.sin(math.radians(rotateX))
rxMathCos = math.cos(math.radians(rotateX))
rightRotationMatrix = np.array([
[rxMathCos, 0, rxMathSin],
[0, 1, 0],
[-rxMathSin, 0, rxMathCos]
])
### pitch
rotateY = 0
ryMathSin = math.sin(math.radians(rotateY))
ryMathCos = math.cos(math.radians(rotateY))
upRotationMatrix = np.array([
[1, 0, 0],
[0, ryMathCos, -ryMathSin],
[0, ryMathSin, ryMathCos]
])
### roll
rotateZ = 0
rzMathSin = math.sin(math.radians(rotateZ))
rzMathCos = math.cos(math.radians(rotateZ))
forwardRotationMatrix = np.array([
[rzMathCos, -rzMathSin, 0],
[rzMathSin, rzMathCos, 0],
[0, 0, 1]
])
### Yaw Pitch Roll 순으로 계산한다.
eulerRotationMatrix = np.matmul(upRotationMatrix, rightRotationMatrix)
eulerRotationMatrix = np.matmul(forwardRotationMatrix, eulerRotationMatrix)
e = eulerRotationMatrix
### 4차원으로 변환
eulerRotationMatrix = np.array([
[e[0][0],e[0][1],e[0][2],0],
[e[1][0],e[1][1],e[1][2],0],
[e[2][0],e[2][1],e[2][2],0],
[0, 0, 0, 1],
])
## 크기 행렬
scale = 1
scaleMatrix = np.array([
[scale, 0, 0, 0],
[0, scale, 0, 0],
[0, 0, scale, 0],
[0, 0, 0, 1]
])
## 이동 행렬
moveX = 0
moveY = 0
moveZ = 0
moveMatrix = np.array([
[1, 0, 0, moveX],
[0, 1, 0, moveY],
[0, 0, 1, moveZ],
[0, 0, 0, 1]
])
srMatrix = np.matmul(scaleMatrix, eulerRotationMatrix)
srmMatrix = np.matmul(moveMatrix, srMatrix)
# 뷰 변환 행렬 정의
cameraPosition = [0, 0, -500]
viewMatrix = np.array([
[1, 0, 0, -cameraPosition[0]],
[0, 1, 0, -cameraPosition[1]],
[0, 0, 1, -cameraPosition[2]],
[0, 0, 0, 1]
])
srmvMatrix = np.matmul(viewMatrix, srmMatrix)
# Clip 변환
# 뷰 공간에서 화면에 투영하기 위해 사용되는 공간
# 카메라 기준으로 정렬된 좌표 값들을 화면상에 보여주기 위해서 -1,1 범위에 정규화해서
# 모든 해상도에 대해서 대비를 하게되는 공간
# 주로 해상도 비율 맞추는 일을 한다.
# Clip 변환에는 두가지 연산이 들어간다.
# 하나는 Projection, 다른 하나는 NDC
# Projection에선 좌표값들을 평면상에 나타내게 만들고
# NDC는 화면 해상도 비율에 따라 어긋난 좌표 값들을 동일하게 맞춰주는 작업이다.
# 투영 행렬 정의
# fov를 구하는 이유
# 1. 투영 평면과 카메라 사이거리 d, 2. 투영 평면의 높이
fov = 90
near = 0.1
far = 1000.0
# GPT의 답변
# 뭔가 많이 다르다
# 인터넷에서 뭔갈 긁어온것 같은데 별로인 것 같다.
# top = near * math.tan(math.radians(fov) / 2)
# bottom = -top
# right = top * aspect
# left = -right
# projectionMatrix = np.array([
# [(2 * near) / (right - left), 0, 0, 0],
# [0, (2 * near) / (top - bottom), 0, 0],
# [(right + left) / (right - left), (top + bottom) / (top - bottom), -(far + near) / (far - near), -1],
# [0, 0, -(2 * far * near) / (far - near), 0]
# ])
# 내가 쓴 것
# 투영 행렬, 원근투영
# 투영 평면과의 거리
d = 1 / math.tan(math.radians(fov)/ 2)
# 스크린 종횡비 k
reverseAspect = 1 / aspect
# 카메라에서 near 까지의 거리 n
n = near
# 카메라에서 far 까지의 거리 f
f = far
# NDC까지 적용한 원근투영 행렬
# NDC는 스크린의 종횡비를 변화하려는 축의 값만 변경하면 된다 d/-p1z(p1x/k, p1y)
# 그러나 이걸 최종 투영 + NDC를 적용한 최종행렬에 적용하려면 행렬식 내부에 -P1z가 들어가서
# 매 행렬계산마다 행렬을 새로 만들어주어야 한다.
# 이를 방지하기 위해 P1z를 3X3 행렬로 만들고 p1z를 연산벡터(정점 좌표값 벡터)에 넘겨준다.
# 그후 나온 결과 행렬의 z값(-P1z)을 나눠주면 NDC구현 완료다.
projectionMatrix = np.array([
[d * reverseAspect, 0, 0, 0],
[0, d, 0, 0],
[0, 0, (n + f) / (n- f), (2 * n * f) / (n - f)],
[0, 0, -1, 0]
])
# 투영 후 NDC를 적용하는 건줄 알았는데 사실 투영, NDC 모두 한번에 하고 있던 것이였다....
# NDC는 마지막 결과 행렬의 Z값을 행렬에 나누는 걸로 완성하는 것
# ndcMatrix = np.array([
# [d * reverseAspect, 0, 0, 0],
# [0, d, 0, 0],
# [0, 0, -1, 0],
# [0, 0, -0, -1]
# ])
# View port 변환
# 최종적으로 디스플레이에 2D 결과화면을 나타내는 단계
# depth를 통해 보일 객체를 선별한다.
# View Space == 카메라 기준 공간
# View port 변환 == Depth를 통한 클리핑 작업
viewportX = 0
viewportY = 0
viewportMatrix = np.array([
[screenWidth / 2, 0, 0, (screenWidth / 2) + viewportX],
[0, -screenHeight / 2, 0, (screenHeight / 2) + viewportY],
[0, 0, 1/2, 1/2],
[0, 0, 0, 1]
])
# 정육면체 변환 및 투영
result_screen_points = []
for point in cubeVertices:
# 로컬 > 월드 > 뷰 까지 적용하기
transViewMatrix = np.matmul(srmvMatrix, point)
# 투영NDC 행렬 적용하여 ClipSpace 만들기
transProjectionMatrix = np.matmul(projectionMatrix, transViewMatrix)
# NDC 적용하여 ClipSpace 완성하기
transProjectionMatrix = transProjectionMatrix / transProjectionMatrix[3]
tpViewportMatrix = np.matmul(viewportMatrix, transProjectionMatrix);
# 원래는 각 행렬요소에 스크린 길이 만큼 더해야했다.
# 하지만 뷰포트 행렬을 적용해서 더는 할 필요 없어졌다
screen_point = tpViewportMatrix[:2];
# screen_point = TransMatrix(tpViewportMatrix)
result_screen_points.append(screen_point[:2])
# 정육면체 그리기
DrawCube(screen, result_screen_points, cubeEdges, LINE_COLOR)
pygame.display.update()
pygame.quit()
재질과 텍스처
재질
객체 표면의 속성을 정의해 주는 요소
- 색상 (Color)
- 투명도(Transparency)
- 텍스처(Texture)
- 표면 속성들
- Specular
- Roughness
- Metalness
텍스처
2D 이미지로써 객체 표면의 색상을 표현해주는 요소. Map이라고도 한다.
- Diffuse Texture : 가장 기본이 되는 텍스처.
- Specular Texture : 빛의 반사에 대한 처리를 위한 덱스처
- Normal Texture : 입체감, 질감 표현 텍스처
텍스처 맵핑
- 2D 텍스처를 폴리곤에 입히는 과정
- 폴리곤의 위치와 텍스처의 위치를 지정 필요하다.
- 3D 모델의 정점에 2D텍스처의 위치를 UV값을 통해 지정
텍스처 로딩
- Texture는 일반적으로 좌상단이 0,0으로 색상 정보가 저장되어 있다.
- UV는 보통 좌하단
![[UV and Texture.excalidraw|300]]UV좌표 계산하기
무게중심좌표계
- 삼각형안 x,y 지점 바탕으로 uv 값을 계산해야한다.
- 삼각형의 좌표값 가중치 정의 : A1 + A2 + A3 = 1
- 해당 꼭짓점이 특정 점 P에 얼마나 기여한지 나타낸다.
- 저 A들을 람다라고한다.
- 점, 특정 지점 식
- P1 = A1 X V1 + A2 X V2 + A3 X V3
정리
- 이미지 파일 가져오기
- 텍스처의 원점은 좌상단
- 삼각형 위치에 맞는 UV값 가져오기
- 무게 중심 좌표계 이용
- FinalUV = (1- λ1 –λ2)𝐕𝟏𝒖𝒗 + λ𝟏𝑽𝟐𝒖𝒗 + λ𝟐𝑽𝟑𝒖𝒗
- 텍스처 맵핑하기
- UV와 텍스처 사이즈의 규격 맞추기
- UV좌표 값 * 텍스처 좌표값
- V는 위아래를 바꿔줘야 하기에 1-(V X 텍스처Y)로 한다.
- UV와 텍스처 사이즈의 규격 맞추기
댓글남기기