9 분 소요

렌더링 파이프라인

  • 3차원 모델을 2차원 화면에 투영하는 렌더링 과정을 말한다.
    image

  • 벌칸 엔진의 렌더링 파이프라인 과정
    image

Application

image

좌표계 정하기

  • 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

  • 프리미티브를 통해서 프래그먼트를 생성하고 프래그 먼트를 채우는 픽셀들을 찾는다.
  • 각 필셀마다 정점 데이터들을 보간하여 할당한다.
    image

Vertex Shader, Pixel Processing

image

  • 정점 정보를 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()

202113110_전진성

재질과 텍스처

재질

객체 표면의 속성을 정의해 주는 요소

  • 색상 (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)로 한다.

댓글남기기