센로그
6. Rasterization 본문
◆ Rasterizer Stage
Clip Space의 vertex데이터(position, color, uv 등)를 Viewport로 변환하고,
정점 데이터를 기반으로 보간된 프래그먼트(픽셀 데이터)를 생성한다
쉽게 말해서 삼각형 내부를 pixel로 채우는 것! 이 단계 이후로 픽셀 각각에 대해 접근가능해짐
Vertex Shader을 거쳐, vertex들의 final position (clip space position)을 구해서 이제 Rasterizer로 넘어감.
Rasterizer는 있어도 되고 없어도 되는데, 어쨌든 미리 만들어진 fixed 형태로 작동한다.
Rasterizer 스테이지의 주 역할은, Vertex들을 기준으로, primitive 내부의 Pixel을 채워주는 것이다.
그전에, 아래와 같은 몇가지 처리들을 한다.
- Primitive Clipping
- Perspective Division
- Viewport Transform
- Back-face Culling
Q. Face culling 같은건 additional(부가적인)한 연산이잖아. 왜 굳이 만들었다 지움? 그냥 하는게 더 빠르지 않음?
A. Pixel 데이터로 바뀌는 순간 연산 오버헤드가 막 늘어남. vertex 기준이던게 pixel로 증식해서, 얘네 칼라값 다 정해서 채워야 하니까. 따라서 Pixel shader로 넘어가기 전에 불필요한 애들을 쳐 놓으면 나중에 효율이 엄청 올라가게 되는 것. |
◆ Rasterizer Stage - Primitive Clipping
clipping volume 밖으로 나가는 primitive들을 잘라내고 처리(새 primitve 만들기)하는 것
primitive clipping은 clip space에서 진행하지만, 직관적인 이해를 돕기 위해 camera space인 것처럼 설명하겟음.
▷ t1은 완전히 frustum 외부에 있으므로, CPU에서 미리 frustum culling됨
▷ t2는 완전히 frustum 내부에 있으므로, 그대로 냅둠
▷ t3는 frustum 경계에 걸쳐 있다! => clipping 됨
이전에 projection transform을 통해서 2x2x1 size의 clipping volume(Clip space)으로 이동되었다.
이제 clipping volume 경계에 걸친 primitive들을 clipping 해주면 됨.
primitive가 어떻게 clip되는지는 primitive type에 따라 다름.
- Point : 포인트가 1 픽셀보다 큰 경우, 이 포인트의 중심이 clipping volume 밖에 있으면 없애고 안에 있으면 살려.
- Line : 선이 부분적으로 clipping volume 밖으로 나가면, 나가는 경계를 자르고 그 지점에 새 vertex 추가해.
- Triangle : triangle이 부분적으로 clipping volume 밖으로 나가면 자르고, 잘린 곳에다 다시 vertex 생성해서 새로 triangle 만들어.
◆ Rasterizer Stage - Perspective Division
3D space를 2D 화면으로 변환하기 위한 원근법을 구현
Clip space -> NDC space
현재 우리가 알고 있는 것은 clipping volume이라는 3차원 공간(Clip space)이다. 그러나 최종적으로 필요한 것은 모니터 화면에 출력될 2차원 영역임! 따라서 3차원 공간을 2차원 공간으로 변환시켜줘야 할 것임. 이 과정에서 우선 원근법을 적용시켜 줘야 함~ (저번에 M_proj 곱해서 처리했던 원근법은 3차원상 원근법이었음.)
M_proj는 마지막 열이 (0, 0, 1, 0)이라서, (x, y, z, 1) 벡터와 곱하면 w축 좌표가 z가 된다.
Vertex Shader은 Rasterizer에게 이런 형태의 좌표들을 넘겨줬을 것임!
따라서 Vertex Shader에게 받은 Vertex들의 Clip space 좌표에서
w에 저장된 값(원래 z값)으로 모든 좌표를 나눠주면 원근법 구현이 완료됨.
Z로 나누어 준다는 것은, 멀리 있는 물체는 더 큰 값으로 나눠주고, 가까이 있는 물체는 더 작은 값으로 나눠준다는 것.
그리고 최종적으로 z값을 0~1사이로 한정할 수 있을 것이다.
이처럼 가까이/멀리를 구분해서 가까이 있는 물체를 더 크게, 멀리 있는 물체를 더 작게 보이도록 만든 것임.
이를 perspective division(원근 나눗셈)이라고 함.
참고)
왼쪽 그림에서 잘린 피라미드 구간이 view frustum이고, 초록색 구간이 Clip space인듯.
오른쪽 그림 네모는 NDC space를 의미.
- projection transform : view frustum → Clip space (in Vertex Shader)
- perspective division : Clip space → NDC space (in Rasterizer)
z값으로 나눠 줌으로써, 원근법을 구현해줌과 동시에
xy 평면으로부터 떨어져있는 Clip space 애들을 xy 평면에 딱 붙여줌.(near plane이 붙음)
이 작업 이후 (x,y,z,w)의 동차 좌표계(Clip space)에서 (x, y, z)의 카르테시안 좌표계로 변한 것을 확인할 수 있음!
이때 이 공간을 NDC(Normalized Device Coordinates) 공간이라고 부름.
(이 좌표의 x, y범위가 둘다 [-1,1]이고 z범위가 [0,1]이기 때문에 Normalized라고 함.)
DirectX기준임. 여담으로 OpenGL에서는 z범위가 [-1,1]이라고 한다.
◆ Rasterizer Stage - Viewport Transform
NDC space → Viewport
이제 원근법 적용까지 했겠다, 컴퓨터 화면 상의 Screen space로 변환시켜줘야 함.
다시말해, NCD space에 있는 물체들을 Viewport로 옮겨와야 함. (-1~1 범위였던 (X, Y) 좌표를 화면 범위로 변환)
Viewport란 2차원 직사각형 공간으로, 3D scene이 여기에 projection되어 보여질 것임. (실제 윈도우 안에서 내가 그림을 그릴 영역. full 화면으로 안하고, 내가 지정할 수도 있음.)
근데 screen space와 viewport는 (2차원일거 같지만) 둘다 3차원임.. 따라서 화면 안쪽으로 파고드는 z축을 하나 더 생각을 해야함(z 범위, 즉 depth도 설정을 해줘야 한다)
ex) D3D의 Viewport 구조
우리는 두가지를 해줘야 한다.
1) 우선 우리의 NDC space를 viewport 크기에 맞게 scaling해줘야 함.
이때, D3D Viewport는 그림처럼 왼쪽 위가 (0,0,0)이므로, 이에 맞춰서 우리 NDC space의 기저들도 왼쪽 위를 원점으로 하도록 변환해줘야 함. (현재는 왼쪽 아래가 (0,0,0)일 것.)
2) 그리고나서, 변환한 NDC의 원점을 viewport의 원점에다가 넣어줘야 함.
좀더 자세히 얘기해보자.
1) 2x2x1짜리 NDC space를 wxhx(MaxDepth-MinDepth)로 scaling해줘야 함.(이 과정에서 TopLeft를 (0,0,0)으로 설정)
2) 이 원점(TopLeft)이 실제로 viewport의 TopLeft에 가도록 translation해줘야 함.
다음과 같이 Scaling 변환과 Translation 변환을 사용해서 옮겨줄 수 있음.
이때 TopLeft를 (0,0,0)으로 설정하기 위해 y-negation(y축 뒤집기)를 해줌. (scaling 과정에서 아래와 같이 S_y에 - 가 붙음.)
이 변환을 마치면, 2x2x1짜리 NDC space가 wxhx(MaxDepth-MinDepth)로 scaling된 것!
이제, viewport 중심점에 위치한 NDC의 TopLeft(0,0,0)을 viewport의 TopLeft로 옮겨줘야함.
직관적으로 Width/2, Height/2씩 옮겨주면 됨.
쪼금만 생각해보면~ 그림에서 NDC 원점이 Viewport 중심에 있던 경우에, 이 Viewport 중심점은 원래 (0,0,0)이었을 텐데,
NDC 원점을 Viewport TopLeft로 옮겨주면, Viewport 중심점은 (+, +, 0)이 될 것임~
이때 z는 MinDepth에 위치하도록 함.
최종적으로 두 변환을 합치면, 다음과 같은 변환 행렬이 나옴.
이런식으로 NDC space에 있는 물체들을 Viewport로 옮겨줄 수 있음.
https://learn.microsoft.com/ko-kr/windows/win32/direct3d9/viewports-and-clipping
◆ Rasterizer Stage - Back-face Culling
NDC space -> Viewport
카메라를 등지고있는 back face(뒷면)의 경우 안보이는 부분이라 렌더링할 필요가 없으므로,
앞으로 있을 비싼 연산들 하기 전에 미리 제거해줌.
(물론 투명한 거 렌더링 하는 경우에는 back face도 해야함)
그림에서 카메라를 등지고 있는 n1은 렌더링할 필요가 없으므로 잘라주면 됨.
카메라의 시선 벡터(projection line)를 c라 하고, face의 법선 벡터를 n이라고 할 때
당연하게도 두 벡터의 내적 값이 0보다 작으면(==두 벡터가 이루는 각이 둔각이면) back face임을 알 수 있음.
그런데..
이런식으로 앞면, 뒷면 판단하기 위해서 하나하나 각각의 시선 벡터랑 법선 벡터 계산해서 너무 힘드니까 꼼수가 있음!
DirectX는 LHS를 사용하므로, 카메라 기준에서 봤을 때 앞면에 보이는 face들은 정점 순서가 CW 방향으로 정렬되어 있을것이다. (안 보이는 면들은 뒤집어져 있기 때문에 CCW 방향으로 보임)
따라서 정점들을 잇는 두 벡터를 외적했을 -z가 나오는 부분을 앞면으로, +z가 나오는 부분을 뒷면으로 간주할 수 있음. (0은 edge-on face).
즉, 그림에서 t2의 경우에는
← 이거 계산해서, - 나오면 front face로, + 나오면 back face로,
0이 나오면 edge-on face로 보면 됨.
이런 식으로 하면 전부 벡터로 만들어서 계산할 필요 없으니까 빠르게 계산할 수 있다. 굿~
※ 참고~
행렬
| a₁ b₁ |
| a₂ b₂ |
이거 외적한 값은 a₁b₂ - a₂b₁ 이다.
※ 참고2~
Back face Culling은 필수 코스는 아님. 옵션으로 바꿀 수 있음
ex) DirectX에서는 default로 꺼져있음. 바꾸려면 CullMode 바꾸면 됨.
◆ Rasterization
이제 필요없는 영역은 다 버렸어! 본격적으로 pixel을 채워보자.
언급했다시피, rasterization stage의 주 역할은 vertex 정보를 raster image로 변환하는 것임.
primitive를 raterizing하는 것은 다음 두 파트로 구성됨
ㆍWindows coordinate에서 primitive가 차지하는 그리드를 결정한다. (edge equation)
ㆍ각 grid에 color 값과 depth 값을 할당한다. (interpolation)
◆ Edge Equation
두 벡터 외적했을 때의 magnitude 반환.
평면을 두 파트로 나누어주는 라인이 있다 하자
이 라인을 기준으로 왼쪽에 있는지 오른쪽에 있는지 반환해주는 함수를 Edge Equation이라고 함
Edge Equation은 pixel이 traingle과 겹치는지(triangle 안에 있는지) 테스트함
어떤 점이 세 개의 엣지 벡터를 기준으로 모두 오른쪽(positive를 반환)에 있다면, p는 삼각형 안에 있다고 할 수 있음.
(DirectX 는 LHS니까 vertex를 잇는 라인 방향은 LHS방향)
Edge Equation는 다음과 같이 정의된다.
가운데 점 기준으로, 왼쪽 점 이은 벡터랑 오른쪽 점 이은 벡터 외적.
(그림에서 v0 기준, P-v0이랑 v1-v0 외적)
이것은 다음 행렬의 determinant 임ㅇㅇ
determinant값이 양수냐, 음수냐에 따라 위치를 확인할 수 있음.
이거 z축 앞으로오는게아니고 뒤로가는그림임. 착시현상이...
Vector a와 b의 Crossproduct의 magnitude로 구해도 동일한 결과 나옴
외적값이 양수이면 오른쪽, 음수이면 왼쪽. 0이면 라인 위.
이렇게 판단한 점 p가 세 개의 엣지 벡터를 기준으로 모두 오른쪽(positive를 반환)에 있다면, p는 삼각형 안에 있다고 할 수 있다!!
기하학적으로 magnitude는 이 마름모의 넓이와 같음!
최종적으로 Edge Equation 결과 E(p, v0, v1)는 이 세 점의 magnitude를 반환함~ 이때 부호는 다음과 같음.(당연^^)
- 0 < θ < 180이면 sinθ > 0 이므로, 양수 반환
- 180 < θ < 360 이면 sinθ < 0 이므로, 음수 반환
이건 뒤에서 vertex별 가중치 구할 때 쓰임.
◆ Assigning Attributes for Fragments
세 정점들의 속성으로부터 삼각형 내부 점의 속성을 보간해야 함.
삼각형 내부의 어떤 점 p의 속성들(color, depth, texture, normal, ...)은 삼각형의 세 vertex들의 속성들을 보간(interpolate)해서 얻을 수 있음
삼각형 내부 어떤 점의 Color 값(p)을 계산한다 할 때, 이렇게 직관적으로 정점별 가중치(λ)와 정점별 Color값(C)을 곱해서 더해주면 됨. (다른 속성값들도 동일한 방식으로 계산)
이때 가중치들의 합은 당연히 1이 될 것~
◆ Barycentric coordinate
무게중심 좌표계. 정점별 가중치 구해서, 내부 점 속성을 보간할 때 쓰임.
그럼 정점별 가중치(λ)는 어떻게 아냐!?
가중치 좌표 (λ0, λ1, λ2)는 각각 마주보는 트라이앵글 (T0, T1, T2)의 영역과 비례한다.
p, v1, v2의 Edge equation의 결과 E(p, v1, v2)는 v1-p 벡터와 v2-p벡터가 이루는 평행사변형의 넓이이므로, 반으로 나누면 트라이앵글 T0 영역이 나옴.
즉, T0, T1, T2의 크기는 다음과 같음
- T0 = 0.5 ∗ E(p, v1, v2)
- T1 = 0.5 ∗ E(p, v2, v0)
- T2 = 0.5 ∗ E(p, v0, v1)
그리고 전체 삼각형의 넓이는 0.5 ∗ E(v2, v0, v1)가 됨.
따라서 가중치는 다음과 같음
◆ Raterization Rule (Top-left Rule)
두 삼각형이 공유하는 pixel에 대하여
한 삼각형의 top 엣지또는 left 엣지에 있는 pixel인 경우, 그 삼각형에 포함하도록 하는 규칙이다~ (top left 우선)
만약 픽셀이 두 삼각형의 엣지에 걸쳐있다면, 어느 트라이앵글에 속할지 결정해야함.
이를 위한 여러가지 규칙이 있는데, 그중에서 Top-left 규칙에 대해 설명해주겠음.
Top-left Rule은 top이랑 left에 우선적으로 포함하도록 하는 규칙.
Direct3D에서 이 규칙을 사용함.
그리드에서 직관적으로 left, right, top, bottom 구분할 수 있음.
- t1: left 2개, right 1개
- t2 : left 1개, right 1개, bottom 1개
- t3 : left 1개, right 1개, top 1개
t1과 t2 사이의 엣지는 t1에게는 left고, t2에게는 right이므로 t1에 포함함
t2와 t3 사이의 엣지는 t2에게는 bottom이고, t3에게는 top이므로 t3에 포함함
그래서 결과적으로 그림처럼 t2에 포함되는 픽셀은 엄청 적어진 것~
◆ Scissor Test
사용자가 정의한 Scissor Rectangle 밖에 있는 픽셀들은 무시함
기본 Scissor Rectangle은 뷰포트 전체화면임.
버튼이 있다든지, 잘 가려지는 부분에 대해, 짜를거면 그냥 렌더링하지마! 하는 것.
Direct3D에서 사용함
'게임 > 그래픽 프로그래밍' 카테고리의 다른 글
8. Lighting (0) | 2023.05.27 |
---|---|
7. Image Texturing (0) | 2023.05.26 |
5. Input Assembler & Vertex Processing(2) (0) | 2023.05.12 |
4. Input Assembler & Vertex Processing(1) (4) | 2023.05.12 |
3. Spaces and Transforms (0) | 2023.04.25 |