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

게임그래픽프로그래밍 14주차 - 재질과 텍스처

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

재질

그래픽스에서 객체 표면의 속성을 정의해 주는 요소

 

빛의 반사와 굴절, 텍스처, 투명도, 색깔, 기타 재질 요소 등을 기반으로 객체의 시각적 특성들을 가시화

게임 엔진에서는 재질은 쉐이더에 의해서 속성값들의 차이 존재

 

- 색상 (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문제

728x90
반응형