재질
그래픽스에서 객체 표면의 속성을 정의해 주는 요소
빛의 반사와 굴절, 텍스처, 투명도, 색깔, 기타 재질 요소 등을 기반으로 객체의 시각적 특성들을 가시화
게임 엔진에서는 재질은 쉐이더에 의해서 속성값들의 차이 존재
- 색상 (Color) : 디폴트로 흰색을 사용한다. 다른 색상에 영향을 주지 않기 위함.
- 투명도 (Transparency) : 물건의 투명도.
- 텍스쳐 (Texture) : 재질. 색 외의 요소.
- 표면 속성들 : Specular, Roughness, Metalness.
텍스처
2D 이미지로써 객체 표면의 색상을 표현 해 주는 요소.
맵(Map)이라고도 하며, 게임 개발 과정에서 중요한 시각적 요소.
Color만으로 사물의 모든 것을 표현 할 수는 없다.
그래픽스에서 사용되는 일반적인 텍스처의 종류
- Diffuse Texture : 가장 기본이 되는 텍스처, 일반적으로 텍스처라고 하면 이를 칭하는 것.
- Specular Texture : 빛의 반사에 대한 처리를 위한 텍스처
- Normal Texture : 표면의 입체감과 질감을 표현하기 위한 텍스처
하이폴리곤이면 하이폴리곤일 수록 디테일하고, 로우폴리곤일 수록 디테일이 떨어진다.
최적화를 위하여 Normal Texture와 같은 텍스처 기법을 통하여 디테일을 처리했다.
2^n으로 이미지를 만드는 것이 좋다. 이와 관련해서 그래픽 디자이너에게 설명 해 줄 것.
텍스처 맵핑 (Texture Mapping)
2D 텍스처를 폴리곤 표면에 입혀서 색상으로 표현하는 과정이다.
텍스처 맵핑시 폴리곤의 위치와 텍스처의 위치를 지정해야 한다.
- 3D 모델의 정점에 2D 텍스처의위치를 uv 값을 통해 지정
- UV 좌표계 사용 (u, v모두 0~1 사이의 값)
폴리곤을 기준으로 텍스처의 UV좌표계를 기준으로 UV좌표를 결정한다.
폴리곤의 모양보다 텍스처의 어느 부분을 텍스처 맵핑할지가 중요하다.
Cube를 전개도 펼칠 경우 각 정점에 해당되는 UV가 다르다.
Cube 생성 시 UV의 내용을 바탕으로 24개의 정점 정보가 구성된다.
24개가 렌더링하기에 편리하겠지만, 최적화 할 수는 있다. 중복된 정점 정보를 줄이는 식.
0~3/ 4~7. 총 4개를 줄일 수 있다. 하지만, 렌더링 할 때는 굳이 이렇게까지 하지 않는다.
배우는 단계에서 하기에는 구현이 복잡해지기 때문.
Python 클래스 정의.
- 파이썬에서 클래스 정의할 때에 class 지시어를 사용
- 생성자는 __init__(self) 가 호출됨, 멤버함수,변수의 self 사용
# 버텍스 클래스 생성
class Vertex:
def __init__(self, position=None, uv=None):
self.position = position if position is not None else np.array([1, 1, 1]
self.uv = uv if uv is not None else np.array([1, 1])
def set_position(self, position) :
self.position = position
def set_uv(self, uv) :
self.uv = uv
# 사용 예
V1 = Vertex()
V2 = Vertex(np.array([1, 1, 1]), np.array([1, 1])
실습 1 진행
- Vertex 클래스를 생성하세요, 멤버변수로 위치값과 UV값이 존재합니다.
- 삼각형을 렌더링할 때 ScanLine알고리즘을 사용해서 렌더링 하세요.
- Cube : Size 100, 색깔 빨간색을 화면에 렌더링 하세요.
- 빨간색 Cube만 나오면 됩니다.
import pygame
import numpy as np
import math
from pygame.locals import *
# 화면 설정
WIDTH, HEIGHT = 1280, 720
BG_COLOR = (105,105,105)
# 초기화
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
class TriVertexExt:
def __init__(self, position=None, uv=None):
self.position = position if position is not None else np.array([ 1, 1, 1])
self.uv = uv if uv is not None else np.array([ 1, 1])
def set_position(self, position) :
self.position = position
def set_uv(self, uv) :
self.uv = uv
class CubeMeshDataExt:
def __init__(self, size):
half_size = size / 2
self.vertex_data = []
# vertex data를 정의
vertex_positions = [
# front
np.array([-1, -1, 1]) * half_size, np.array([ 1, -1, 1]) * half_size, np.array([ 1, 1, 1]) * half_size,
np.array([ 1, 1, 1]) * half_size, np.array([-1, 1, 1]) * half_size, np.array([-1, -1, 1]) * half_size,
# back
np.array([-1, 1, -1]) * half_size, np.array([ 1, 1, -1]) * half_size, np.array([ 1, -1, -1]) * half_size,
np.array([ 1, -1, -1]) * half_size, np.array([-1, -1, -1]) * half_size, np.array([-1, 1, -1]) * half_size,
# right
np.array([ 1, -1, 1]) * half_size, np.array([ 1, -1, -1]) * half_size, np.array([ 1, 1, -1]) * half_size,
np.array([ 1, 1, -1]) * half_size, np.array([ 1, 1, 1]) * half_size, np.array([ 1, -1, 1]) * half_size,
# left
np.array([-1, -1, -1]) * half_size, np.array([-1, -1, 1]) * half_size, np.array([-1, 1, 1]) * half_size,
np.array([-1, 1, 1]) * half_size, np.array([-1, 1, -1]) * half_size, np.array([-1, -1, -1]) * half_size,
# top
np.array([-1, 1, 1]) * half_size, np.array([ 1, 1, 1]) * half_size, np.array([ 1, 1, -1]) * half_size,
np.array([ 1, 1, -1]) * half_size, np.array([-1, 1, -1]) * half_size, np.array([-1, 1, 1]) * half_size,
# bottom
np.array([-1, -1, -1]) * half_size, np.array([ 1, -1, -1]) * half_size, np.array([ 1, -1, 1]) * half_size,
np.array([ 1, -1, 1]) * half_size, np.array([-1, -1, 1]) * half_size, np.array([-1, -1, -1]) * half_size,
]
# uv data를 정의
uv_data = [
np.array([0.0,0.0]), np.array([1.0,0.0]), np.array([1.0,1.0]),
np.array([1.0,1.0]), np.array([0.0,1.0]), np.array([0.0,0.0]),
np.array([1.0,1.0]), np.array([0.0,1.0]), np.array([0.0,0.0]),
np.array([0.0,0.0]), np.array([1.0,0.0]), np.array([1.0,1.0]),
np.array([0.0,0.0]), np.array([1.0,0.0]), np.array([1.0,1.0]),
np.array([1.0,1.0]), np.array([0.0,1.0]), np.array([0.0,0.0]),
np.array([0.0,0.0]), np.array([1.0,0.0]), np.array([1.0,1.0]),
np.array([1.0,1.0]), np.array([0.0,1.0]), np.array([0.0,0.0]),
np.array([0.0,0.0]), np.array([1.0,0.0]), np.array([1.0,1.0]),
np.array([1.0,1.0]), np.array([0.0,1.0]), np.array([0.0,0.0]),
np.array([0.0,0.0]), np.array([1.0,0.0]), np.array([1.0,1.0]),
np.array([1.0,1.0]), np.array([0.0,1.0]), np.array([0.0,0.0]),
]
# vertex 및 uv data를 이용하여 객체 생성 및 vertex_data에 추가
for i in range(len(vertex_positions)):
self.vertex_data.append(TriVertexExt(vertex_positions[i], uv_data[i]))
# vertex buffer
cube_size = 100
mesh_data_ex = CubeMeshDataExt(cube_size)
# model matrix 구성
# numpy 라이브러리를 이용해서 단위행렬을 만들 때에 np.eye(차원) 사용
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]
])
# 크기 변환 행렬
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 혹은 numpy.deg2rad 사용
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]
])
# Rotaiton : Ry.Rx.Rz
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([100, 100, 300]) # 카메라 위치
target_pos = np.array([0, 0, 0]) # 바라보는 위치
up = np.array([0, 1, 0]) # 카메라의 기준이 되는 위쪽 방향
view_z = (target_pos - cam_pos)
view_z = view_z / np.linalg.norm(view_z)
view_x = np.cross(up, view_z)
view_x = view_x / np.linalg.norm(view_x)
view_y = np.cross(view_z, view_x)
view_y = view_y/ np.linalg.norm(view_y)
cam_inv_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]
])
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_inv_model_matrix)
# clip Space : projection matrix
fov = math.radians(80)
# aspectio ration
k = WIDTH / HEIGHT
# near, far clipplane
n = 0.1
f = 1000
# forcal length
d = 1 / np.tan(fov / 2)
projection_matrix = np.array([
[d/k, 0, 0, 0],
[0, d, 0, 0],
[0, 0, (n + f) / (n - f), (2 * n * f ) / ( n - f)],
[0, 0, -1, 0]
])
# screen space : viewport transform
viewport_x = 0
viewport_y = 0
viewport_matrix = np.array([
[WIDTH / 2, 0, 0, WIDTH / 2 + viewport_x],
[0, -HEIGHT / 2, 0, HEIGHT / 2 + viewport_y],
[0, 0, 0.5, 0.5],
[0, 0, 0, 1]
])
def scanline_render_vertex_fill_triangle(screen,vertices,color):
# y좌표를 기준으로 정렬
if vertices[0].position[1] > vertices[1].position[1]:
vertices[0], vertices[1] = vertices[1], vertices[0]
if vertices[0].position[1] > vertices[2].position[1]:
vertices[0], vertices[2] = vertices[2], vertices[0]
if vertices[1].position[1] > vertices[2].position[1]:
vertices[1], vertices[2] = vertices[2], vertices[1]
v1, v2, v3 = vertices
v1_X = int(v1.position[0]); v1_Y = int(v1.position[1])
v2_X = int(v2.position[0]); v2_Y = int(v2.position[1])
v3_X = int(v3.position[0]); v3_Y = int(v3.position[1])
# 각 변의 기울기를 계산합니다.
slope_12 = (v2_X - v1_X) / (v2_Y - v1_Y) if v2_Y != v1_Y else 0
slope_13 = (v3_X - v1_X) / (v3_Y - v1_Y) if v3_Y != v1_Y else 0
slope_23 = (v3_X - v2_X) / (v3_Y - v2_Y) if v3_Y != v2_Y else 0
for y in range(v1_Y, v3_Y + 1):
if y <= v2_Y :
x1 = v1_X + (y - v1_Y ) * slope_13
x2 = v1_X + (y - v1_Y ) * slope_12
else:
x1 = v1_X + (y - v1_Y ) * slope_13
x2 = v2_X + (y - v2_Y) * slope_23
x_left, x_right = int(min(x1, x2)), int(max(x1, x2))
for x in range(x_left, x_right):
screen.set_at((x, y), color)
CUBE_COLOR = (255,0,0)
# 텍스처 로딩
# 게임 루프
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))
render_vertices = []
# 렌더링할 Vertex를 만든다.
for vertex in mesh_data_ex.vertex_data:
# 3D 좌표계에서 2D 좌표계로 변환
temp_point = np.append(vertex.position,1)
p = np.matmul(proj_view_model_matrix, temp_point)
# NDC 좌표 변환
p /= p[3]
# 화면 좌표 변환
p = np.matmul(viewport_matrix, p)
# 렌더링할 버텍스 집합
render_vertex = TriVertexExt()
render_vertex.position = p[:3]
render_vertex.uv = vertex.uv
render_vertices.append(render_vertex)
# 렌더링
screen.fill(BG_COLOR)
render_vertex_num = int(len(render_vertices)/3)
for idx in range(render_vertex_num):
face_render_points = []
face_render_points.append(render_vertices[3*idx])
face_render_points.append(render_vertices[3*idx+1])
face_render_points.append(render_vertices[3*idx+2])
scanline_render_vertex_fill_triangle(screen,face_render_points,CUBE_COLOR)
pygame.display.flip()
pygame.quit()
텍스처를 로딩하기 위해서는 이미지 파일이 필수이다.
Pygame에서 Texture 로딩 함수 pygame, image.load
# texture 로딩
real_texture = pygame.image.load('ga.jpg’)
Pygame에서 Texture의 특정위치의 색상값 얻기 (R, G, B)
Texture의 get_at 함수 이용 : x, y의 위치는 텍스처의 좌상단 원점
# texture의 x,y의 기준으로 얻기
tex_color = real_texture.get_at(x, y)
Texture의 x, y의 범위는 x는 width-1, y는 height-1
Texture는 일반적으로 좌상단이 0,0으로 색상 정보가 저장되어 있다.
우리가 사용하고 있는 UV의 원점은 좌하단이 0,0이다.
y축이 반대로 되거나 엉뚱한 위치에 배치되는 문제가 발생한다.
Texture를 x, y로 접근할 때 일반적으로 x, y은 0부터 시작한다.
u, v는 0~1사이의 값이므로 정확한 위치값을 계산해야 한다.
1024*1024 텍스처를 이용 u,v값과 적용되는 텍스처의 x,y계산식
# uv를 통해서 texture의 x,y의 값 얻기
tex_x = u*(texture.get_width()-1)
tex_y = (1-v)*(texture.get_height()-1)
tex_color = texture.get_at((tex_x, tex_y))
# 화면에 tex_color값 적용해 보기
screen.set_at((x, y), tex_color)
삼각형 안의 x, y지점을 바탕으로 하는 u, v값 계산
무게중심좌표계(Barycentric coordinate) 기반 u, v값 계산 가능
2차원 공간 무게중심좌표계 좌표값은 가중치 람다1, 람다2, 람다3로 표현.
- 람다1 + 람다2 + 람다3 = 1
삼각형 내부의 점 V1, V2, V3일 경우에 삼각형 내부 특정 지점 P
- P = 람다1*V1 + 람다2*V2, 람다3*V3
삼각형의 텍스처 맵핑이나 쉐이딩 처리할 때 많이 사용한다.
람다 3개를 더하면 1이라는 것을 활용하여 FinalUV(0<=FinalUV<=1)값을 구하는 공식의 유도가 가능하다.
텍스처에 적용하기 위해서는 앞에서 언급한 V좌표계의 처리를 해 주어야 한다.
택스처 맵핑 예제
무게중심좌표계를 이용해서 2D 삼각형을 그리고 ga.jpg를 맵핑한다.
import pygame
import numpy as np
# 게임 초기화
pygame.init()
# 스크린 설정
screen = pygame.display.set_mode((800, 600))
# 텍스처 로드
texture = pygame.image.load('ga.jpg')
triangle = np.array([(200, 200), (200, 400), (400, 200)])
triangle_uv = np.array([(0, 1), (0, 0), (1, 1)])
triangle2 = np.array([(400, 200), (200, 400), (400, 400)])
triangle2_uv = np.array([(1, 1), (0, 0), (1, 0)])
무게중심 좌표계를 이용한 점들의 좌표값 확인.
float64를 사용 해 주어야 오차가 적어진다.
def barycentric_coords_ext(triangle, point):
vector_u = triangle[1] - triangle[0]
vector_v = triangle[2] - triangle[0]
vector_w = point - triangle[0]
dot_uv = np.float64(vector_u.dot(vector_v))
dot_vv = np.float64(vector_v.dot(vector_v))
dot_uu = np.float64(vector_u.dot(vector_u))
inv_denom = 1/ (dot_uv * dot_uv - dot_vv * dot_uu)
dot_wu = np.float64(vector_w.dot(vector_u))
dot_wv = np.float64(vector_w.dot(vector_v))
lambda1 = (dot_wv * dot_uv - dot_wu * dot_vv) * inv_denom
lambda2 = (dot_wu * dot_uv - dot_wv * dot_uu) * inv_denom
lambda3 = 1.0 - lambda1 - lambda2
return (lambda3, lambda1, lambda2)
삼각형 Bbox와 UV를 이용한 Texture맵 적용
# 삼각형을 감싸고 있는 바운딩박스 확인
def compute_bounds(triangle):
min_x = min(triangle[:, 0])
max_x = max(triangle[:, 0])
min_y = min(triangle[:, 1])
max_y = max(triangle[:, 1])
return min_x, max_x, min_y, max_y
# UV를 이용한 Texture맵 적용
def texture_map(texture, uv):
u, v = uv
x = int(u * (texture.get_width() - 1))
y = int((1-v) * (texture.get_height() - 1))
return texture.get_at((x, y))
렌더링을 그리는 함수
barycentric은 여러번 반복해서 실행되기 때문에 비교적 비효율적이다.
삼각형을 그릴 때는 scanline이 훨씬 빠르다.
def render_triangle(screen, texture, triangle, triangle_uv, min_x, max_x, min_y, max_y):
for x in range(min_x, max_x+1):
for y in range(min_y, max_y+1):
point = np.array([x, y])
# barycentric 좌표계를 계산
lambda1, lambda2, lambda3 = barycentric_coords_ext(triangle, point)
# 만약 점이 삼각형 내부에 있다면 원래 0이어야 하나 오차 적용을 위해서 : -1.15e-16
if lambda1 >= -1.15e-16 and lambda2>= -1.15e-16 and lambda3 >= -1.15e-16 :
# UV 좌표 보간
uv = lambda1 * triangle_uv[0] + lambda2 * triangle_uv[1] + lambda3 * triangle_uv[2]
# 텍스처 맵핑
color = texture_map(texture, uv)
# 픽셀 색상을 설정
screen.set_at((x, y), color)
# 삼각형 경계를 계산
bounds = compute_bounds(triangle)
bounds2 = compute_bounds(triangle2)
게임 루프 적용
# 이벤트 루프
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill((150,150,150))
# 삼각형 그리기
render_triangle(screen, texture, triangle, triangle_uv, *bounds)
render_triangle(screen, texture, triangle2, triangle2_uv, *bounds2)
# 화면 업데이트
pygame.display.flip()
pygame.quit()
실습2 진행.
- 실습 1과 동일하다.
- Cube Front면만 생성, Size 100, uv를 적용하여 텍스처 맵핑 렌더링
실습3 진행
- 실습 1과 동일하다.
- Cube : Size 100, uv를 적용하여 텍스처 맵핑 렌더링
실습4 진행.
- 실습 3과 동일, Zbuffer값을 계산하여 앞뒤 구분을 해 주자.
- Cube : Size 100, uv를 적용하여 텍스처 맵핑 렌더링
시험 공지 (총 5문제)
- 손코딩 1문제
- 서술형 4문제
'대학생활 > 수업' 카테고리의 다른 글
게임알고리즘 13주차 - Red-Black Tree, B Tree (0) | 2023.06.21 |
---|---|
게임프로그래밍고급 15주차 - 기말 시험 (0) | 2023.06.20 |
게임네트워크프로그래밍 14주차 - 매칭 서버, 아키텍쳐, 운영체제. (4) | 2023.06.19 |
게임레벨디자인 14주차 - 기말 시험 (0) | 2023.06.16 |
게임음악작곡법 13주차 - Melodic Cadences, 3화음 배열법, 그루브 (0) | 2023.06.15 |