시작에 앞서 지금까지 구축한 파이프라인을 한 번 살펴보자.
정점이 처리되며 거쳐가는 공간은 위와 같다. homogeneous coordinates와 ndc space로의 변환은 지난 글에서 다루었던 내용이다.
파이프라인의 세부적인 프로세스들을 살펴보면 다음과 같다.
정점을 나눠 vertex shader를 거치고 triangle assembly로 삼각형들로 만든 후, 카메라 방향에 의한 culling을 처리하고, geometry shader를 거치고 나서 persepctive division 및 screen transform을 처리하고, triangle rasterization, z-buffering을 처리한 후 pixel shader를 처리하고 최종적으로 스크린에 렌더링한다.
여기서 잠시 한 가지 문제를 짚고 넘어가면, 우리의 파이프라인에서는 Back-face culling이 Triangle assembly 직후 처리되는데, 이렇게 할 경우 만약 Geometry shader에서 삼각형의 위치를 변경시켜 특정 폴리곤이 카메라를 바라보도록 바꿔버리면 이미 Backface culling이 처리된 이후이므로 올바른 렌더링 결과를 얻지 못할 수도 있다.
때문에 Backface culling 과정을 Geometry shader 이후로 옮겨주는 게 맞다. (본 학습의 목적 상 이 부분을 굳이 수정하지는 않겠다)
현재 우리의 프레임워크에는 앞서 말한 문제 외의 한 가지 중대한 문제가 있다.
바로 어떤 정점이 화면 밖으로 벗어날 경우 크래시가 발생하는 문제이다. 이번 시간에는 이 문제를 해결해보자. (Clipping을 구현해보자)
Clipping의 구현법으로는 Raster clipping과 Geometric clipping이 있다.
Raster clipping은 단순히 어떤 폴리곤을 rasterize할 때 scanline을 통해 스캔해나가는 과정에서 scanline의 시작 지점을 스크린의 경계에 맞춰 처리해주는 형태로 clipping을 처리한다.
즉 for loop의 범위를 clamp해주는 것으로 딱 화면 내에 들어오는 점들만 그리는 형태이다.
Geometric clipping은 폴리곤 자체를 변형시켜 화면에 맞는 새 폴리곤을 생성해주는 방식으로 clipping을 처리한다.
조금 더 디테일하게 살펴보자.
우리는 앞선 내용들을 통해 화면상에 그릴 물체들은 오직 NDC Space 내에 포함되있는 물체들이라는 것을 배웠다. (달리 말해 NDC space는 화면상에 그려질 물체들을 나타내는 공간이라는 것을 배웠다)
즉 우리는 만약 어떤 폴리곤이 NDC Space 바깥에 있을 경우 그 부분은 화면 밖에 있다는 것을 알 수 있고, 이를 이용해 폴리곤을 자르거나 무시해버리는 것으로 clipping을 구현할 수 있다.
실제 예시로 살펴보자.
위 상황은 NDC space를 보기 편하게 단면도로 나타낸 그림이다. 화면의 녹색 폴리곤 일부가 NDC space 밖으로 삐져나와있는 것을 볼 수 있는데, 이를 어떻게 잘라낼 수 있는지(정확히는 이 부분을 제외한 필요한 부분들로만 구성된 새 폴리곤을 어떻게 생성할 수 있는지) 살펴보자.
첫번째 예시의 경우, 삼각형에서 필요없는 부분이 얼마나 되는 지를 알기 위해서 불필요한 변의 길이에 전체 변의 길이를 나누어 interpolation에 필요한 alpha값을 얻는다.
(위 예시 상황에서는 삼각형의 두 변이 NDC 바깥과 겹치므로 두 alpha 값이 필요하다)
그 후 이 alpha 값을 이용해 폴리곤의 모든 관련 요소들(텍스쳐, 알파 등)을 interpolate해주는 것으로 필요한 부분의 관련 요소들만 추출해낼 수 있다.
다른 예시 상황도 살펴보자. 이 경우는 접근방식이 달라지는데, 이러한 상황에서 Geometric clipping 방식으로 clipping하기 위해서는 폴리곤의 갯수를 늘려야 함을 알 수 있다.
위 상황의 경우, 폴리곤의 갯수가 더 많이 필요함을 알 수 있다.
이렇게 폴리곤의 갯수를 늘리는 Geometric clipping을 도대체 왜 사용해야 할까?
그냥 Raster clipping으로 전부 해결하면 안될까?
3D 환경에서는 아쉽게도 이게 불가능하다.
우리가 어떤 폴리곤을 rasterize할 때 우리는 폴리곤의 x,y만을 for loop을 통해 scan하며 화면상에 픽셀을 찍었다. 이게 가능한 이유는 우리가 보는 컴퓨터 스크린이 2D 공간이기 때문이다.
그러나 Clipping을 할 때, 우리는 컴퓨터 내의 (가상의) 3차원 공간에 존재하는 3차원 물체를 3차원 범위 내로 clipping 시켜주어야 하기에 x,y만을 고려할 수가 없게 된다. (물체가 z축 방향으로 카메라 시야 공간을 벗어나게 될 수도 있으니 말이다)
"굳이 z축까지 clipping을 해주어야 할 필요가 있나? 어차피 스크린에 렌더링되는 건 x,y축뿐인데?"
라고 생각할 수도 있다. 그러나 여기에는 큰 문제가 있다.
z축 clipping을 안했을 때 발생하는 문제 한 가지를 살펴보자.
만약 vertex 하나가 카메라 position에 위치하고 있는 어떤 폴리곤을 그린다고 해보자.
이 폴리곤의 z는 NDC space 기준으로 near plane(노란면) 보다 뒤에 있으니 음수값(-베타로 표기)이 될 것이지만, w값은 0이 될 것이다. (w값을 구할 때는 NDC space 좌표를 사용하는 게 아니라 월드좌표에서 z값의 차를 사용하고, 이 경우 카메라와 폴리곤 정점의 z pos가 동일하므로 그 값이 0이 된다)
이러한 폴리곤을 만약 clipping없이 파이프라인에서 처리한다면 perspective divide를 처리하는 과정에서 division by zero 에러가 발생할 것이다.
때문에 near plane으로(=z축 기준으로) 폴리곤을 clipping하는 과정은 필수적이다.
far plane으로 clipping하는 것의 경우, 하는 게 좋지만 사실 하지 않는다고 near plane처럼 크래시가 나거나 하지는 않는다. 다만 clipping하지 않는다는 건, NDC space 내의 폴리곤들중 일부는 z값이 1이 넘는(즉 NDC Space 밖으로 살짝 삐져나오는) 좌표를 가질 수도 있다는 것을 의미한다. (그림 상 우측 빨간 삼각형) 이는 언젠가 혼란을 야기할 수도 있다.
또 정점이 하나라도 NDC Space 내에 있다면 거리에 관계없이 모든 폴리곤을 다 그려버리기 때문에 다소 부하가 있을 수 있다는 단점도 있다.
어쨌든 far plane clipping은 optional하고, 반면 near plane clipping을 필수적이라는 것을 알아두자.
코드를 살펴보기 전에, clipping 구현과정에서의 중요한 점 한가지를 짚고 넘어가자.
Clipping을 하기 위해서는 어떤 정점이 NDC의 면 안에 있는지 밖에 있는지를 비교한다.
x축을 기준으로 표현하면, 이는 |Xndc| < 1로 표현할 수 있다.
이떄 우리는 앞선 글에서 어떤 NDC 좌표는 homogeneous coordinate에 w를 나눠준 것으로 표현가능하다는 것을 배웠다. 즉 같은 조건을 다음과 같이 변형 가능하다.
우변으로 wndc를 옮기면 최종적으로 다음과 같은 조건식 하나로 X가 NDC 내에 있는지 밖에 있는지 확인할 수 있다.
이 과정이 왜 중요한가?
우리는 이 과정을 통해 Xndc를 구하기 전부터, 즉 Xhomo에 w를 나눠주기 전의 시점에서도 Clipping을 처리할 수 있게 되었다. 이는 즉 Perspective division이 처리되기 전의 시점에 Clipping을 처리할 수 있음을 의미하고, 이는 성능 상에 큰 도움이 된다.
Clipping을 통해 굳이 연산할 필요 없는 폴리곤들을 걸러내고 나서 필요한 폴리곤들에만 대해서 Perspective division을 실행하면 연산의 횟수를 줄임으로써 불필요한 연산을 크게 줄일 수 있다. (게다가 w로 나눠주는 나눗셈 연산은 부하가 큰 연산이다. 연산의 횟수를 줄이는 것의 의미가 크다.)
더 나아가 이 clipping 과정에서 w=0인 값들이 걸러지기 때문에 perspective division을 처리하기 전에 perspective division에서 발생할 수 있는 division by zero 문제를 봉쇄한다.
(주석: 내 추측으로는 기존 프레임워크에서 오브젝트가 화면 밖으로 나가면 크래시가 발생했던 것도 이 division by zero 때문이었다고 생각된다)
수정된 파이프라인을 대략적으로 살펴보자.
Clipper unit이 Geometry shader 다음에 추가되었다.
Clipper unit은 하나의 폴리곤을 인풋으로 받아 0개 이상(즉 2개가 넘을 수도 있다. Geometric clipping에 의해 여러 개의 폴리곤으로 늘어날 수도 있기 때문이다) 의 폴리곤을 반환한다.
또 Triangle Rasterizer에서 이제 raster clipping이 처리된다. 이는 for loop의 범위를 clamp해주는 간단한 방식으로 구현된다.
코드로 살펴보자.
ClipCullTriangle이라는 함수가 추가되었다.
이 함수는 어떤 폴리곤이 NDC Space에서 완전히 벗어난 경우 이 폴리곤을 무시해버리는 역할을 해준다(cull)
달리 말해, 정점 하나라도 NDC Space에 포함된다면 그 폴리곤은 살려둔다.
ClipCullTriangle의 로직은 각 정점의 값들을 w값과 비교하는 것으로 이루어진다. (NDC의 각 surface들과 위치를 비교하는 과정이다)
z의 경우 혼자 비교 범위가 약간 다른데, 이는 NDC Space의 z범위가 x,y와 달리 0~1이기 때문이다.
Geometric clipping을 코드로 살펴보자.
// Pipeline.h
// Clipping에 사용할 함수 두 개를 정의한다.
// Clip1은 필요없는 부분을 잘라내고 남은 부분이 사각형꼴이라
// 이를 삼각형 하나를 두 개로 쪼개는 경우이고,
// Clip2는 필요없는 부분을 잘라내면 바로 삼각형 하나만 남는 경우이다.
// clipping routines
const auto Clip1 = [this]( GSOut& v0,GSOut& v1,GSOut& v2 )
{
// calculate alpha values for getting adjusted vertices
const float alphaA = (-v0.pos.z) / (v1.pos.z - v0.pos.z);
const float alphaB = (-v0.pos.z) / (v2.pos.z - v0.pos.z);
// interpolate to get v0a and v0b
const auto v0a = interpolate( v0,v1,alphaA );
const auto v0b = interpolate( v0,v2,alphaB );
// draw triangles
PostProcessTriangleVertices( Triangle<GSOut>{ v0a,v1,v2 } );
PostProcessTriangleVertices( Triangle<GSOut>{ v0b,v0a,v2 } );
};
const auto Clip2 = [this]( GSOut& v0,GSOut& v1,GSOut& v2 )
{
// calculate alpha values for getting adjusted vertices
const float alpha0 = (-v0.pos.z) / (v2.pos.z - v0.pos.z);
const float alpha1 = (-v1.pos.z) / (v2.pos.z - v1.pos.z);
// interpolate to get v0a and v0b
v0 = interpolate( v0,v2,alpha0 );
v1 = interpolate( v1,v2,alpha1 );
// draw triangles
PostProcessTriangleVertices( Triangle<GSOut>{ v0,v1,v2 } );
};
// 어떤 상황에서 어떤 함수를 써야하는지 판정하고 실제로 함수를 호출하는 부분
// near clipping tests
if( t.v0.pos.z < 0.0f )
{
if( t.v1.pos.z < 0.0f )
{
Clip2( t.v0,t.v1,t.v2 );
}
else if( t.v2.pos.z < 0.0f )
{
Clip2( t.v0,t.v2,t.v1 );
}
else
{
Clip1( t.v0,t.v1,t.v2 );
}
}
else if( t.v1.pos.z < 0.0f )
{
if( t.v2.pos.z < 0.0f )
{
Clip2( t.v1,t.v2,t.v0 );
}
else
{
Clip1( t.v1,t.v0,t.v2 );
}
}
else if( t.v2.pos.z < 0.0f )
{
Clip1( t.v2,t.v0,t.v1 );
}
else // no near clipping necessary
{
PostProcessTriangleVertices( t );
}
Raster clipping 구현도 살펴보자.
사실 크게 달라지진 않았고, rasterization을 처리하는 for loop의 범위를 clamp시켜줌으로써 화면 바깥으로 나가는 정점들을 렌더링 시 무시해버리는 형태로 구현한다.
코드를 실행해보면 잘 작동함을 알 수 있다.
모델이 스크린 밖으로 벗어나도 크래시가 나지 않는다.
near plane clipping이 잘 작동됨을 위 사진에서 볼 수 있다. (우측 하단 삼각형 모서리가 잘림)
또 이제 모델 "내부"로도 들어갈 수 있게 되었다.
near plane clipping이 있기에 가능하다.
현재 파이프라인에는 far plane clipping이 구현되어 있지 않음에도 물체가 충분히 멀어지면 물체가 조금씩 사라지는 것을 볼 수 있는데, 이는 물체를 구성하는 어떤 폴리곤의 정점 전체가 NDC space 밖으로 나가버리게 되면 그 폴리곤이 무시됨에 따라 발생하는 현상이다. (즉 폴리곤 단위로 사라진다)
만약 보다 부드럽게, 정확히 far plane을 벗어나는 지점들만 사라지기를 원한다면 near plane clipping과 마찬가지의 원리로 far plane clipping을 구현하면 된다.
'computer graphics > 3D Graphics' 카테고리의 다른 글
[3DGraphics] 22. Camera view (0) | 2022.10.28 |
---|---|
[3DGraphics] 20. Projection matrix (1) | 2022.10.11 |
[3DGraphics] 19. Implementing Vec4, Mat4 (2) | 2022.10.11 |
[3DGraphics] 18. Specular highlights (1) | 2022.10.11 |
[3DGraphics] 17. Phong shading & Per-pixel lighting (0) | 2022.09.09 |