
Frustum Culling
frustum은 한국어로 절도체라고 하는데, 피라미드에서 윗부분이 잘린 도형을 말한다. frustum이 곧 카메라가 비추는 부분인데, 게임에서 Frustum Culling을 이용해서 Frustum 안쪽의 물체만 출력한다. 이것을 이용하면 CPU에서 GPU로 넘기는 단계에서 걸러버리기 때문에 큰 최적화 효과를 볼 수 있다.

평면의 방정식
평면을 나타내는 방정식은 ax+by+cz+d=0 인데 이 방정식의 몇가지 특징이 있다.
① x,y,z의 계수 (a,b,c)는 평면의 법선벡터이다.
평면위 임의의 점 두 개를 잇는 벡터와 (a,b,c)를 내적하면 0이 나온다.
② -d는 원점에서 평면까지 거리이다.
벡터의 내적은 각 x,y,z좌표를 곱해서 더한것과 같기 때문에 ax+by+cz 는 원점으로부터 (x,y,z)에 있는 평면 위 점으로 향하는 벡터 OM과 법선벡터(a,b,c)를 내적한 것과 같다. 그 값은 원점에서 평면까지 거리와도 같다. (OM*N*cos) N은 1이므로 OM Cos 는 원점에서 평면까지 거리이기 때문이다. ax+by+cz=-d 이므로 -d가 원점~평면 거리이다.
③ax+by+cz+d<0이면 원점과 평면사이의 좌표, >0이면 원점 너머 평면 너머에 좌표이다.
ax+by+cz가 OM과 법선벡터n의 내적값이기 때문에 0보다 작으면 d보다 내적값이 작은 것이기 때문에 원점과 평면 사이이고, 0보다 크면 d보다 내적값이 크기 때문에 평면 너머에 좌표이다.
그래서 Frustum의 6개 면의 평면의 방정식을 통해 물체가 Frustum 내부에 있는지 아닌지 확인한다.
void Frustum::FinalUpdate()
{
Matrix matViewInv = Camera::S_MatView.Invert();
Matrix matProjectionInv = Camera::S_MatProjection.Invert();
Matrix matInv = matProjectionInv * matViewInv;
vector<Vec3> worldPos =
{
::XMVector3TransformCoord(Vec3(-1.f, 1.f, 0.f), matInv),
::XMVector3TransformCoord(Vec3(1.f, 1.f, 0.f), matInv),
::XMVector3TransformCoord(Vec3(1.f, -1.f, 0.f), matInv),
::XMVector3TransformCoord(Vec3(-1.f, -1.f, 0.f), matInv),
::XMVector3TransformCoord(Vec3(-1.f, 1.f, 1.f), matInv),
::XMVector3TransformCoord(Vec3(1.f, 1.f, 1.f), matInv),
::XMVector3TransformCoord(Vec3(1.f, -1.f, 1.f), matInv),
::XMVector3TransformCoord(Vec3(-1.f, -1.f, 1.f), matInv)
};
_planes[PLANE_FRONT] = ::XMPlaneFromPoints(worldPos[0], worldPos[1], worldPos[2]); // CW
_planes[PLANE_BACK] = ::XMPlaneFromPoints(worldPos[4], worldPos[7], worldPos[5]); // CCW
_planes[PLANE_UP] = ::XMPlaneFromPoints(worldPos[4], worldPos[5], worldPos[1]); // CW
_planes[PLANE_DOWN] = ::XMPlaneFromPoints(worldPos[7], worldPos[3], worldPos[6]); // CCW
_planes[PLANE_LEFT] = ::XMPlaneFromPoints(worldPos[4], worldPos[0], worldPos[7]); // CW
_planes[PLANE_RIGHT] = ::XMPlaneFromPoints(worldPos[5], worldPos[6], worldPos[1]); // CCW
}
Frustum을 World 좌표에서 계산하기 위해 matView 역행렬, matProjection 역행렬, 둘을 곱한 matInv를 구한다.
Frustum 각 평면의 정점 순서도 중요한데 평면의 normal 벡터가 Frustum의 바깥쪽을 향하도록 해야 한다. CW는 시계방향, CCW는 반시계 방향으로 세팅했다. XMPlaneFromPoints는 점 3개로 평면을 구하는 함수이다.
inline XMVECTOR XM_CALLCONV XMPlaneFromPoints
(
FXMVECTOR Point1,
FXMVECTOR Point2,
FXMVECTOR Point3
) noexcept
{
XMVECTOR V21 = XMVectorSubtract(Point1, Point2);
XMVECTOR V31 = XMVectorSubtract(Point1, Point3);
XMVECTOR N = XMVector3Cross(V21, V31);
N = XMVector3Normalize(N);
XMVECTOR D = XMPlaneDotNormal(N, Point1);
D = XMVectorNegate(D);
XMVECTOR Result = XMVectorSelect(D, N, g_XMSelect1110.v);
return Result;
}
점3개중 2개를 골라 서로 다른 벡터 2개를 구하고 외적해 Normal Vector를 구한다. 그리고 Normal Vector와 점 하나를 내적한 값이 -d와 같다는 것을 이용한다. 그리고 XMVectorSelect함수에서 N벡터로 a,b,c D벡터로 d를 구한다.
bool Frustum::ContainsSphere(const Vec3& pos, float radius)
{
for (const Vec4& plane : _planes)
{
// n = (a, b, c)
Vec3 normal = Vec3(plane.x, plane.y, plane.z);
// ax + by + cz + d > radius
if (normal.Dot(pos) + plane.w > radius)
return false;
}
return true;
}
물체는 점이 아니라 부피를 가지고 있기 때문에 구가 있다고 가정하고 Frustum Culling 계산을 해보겠다. normal.Dot(pos) = ax + by + cz + d 이고 plane.w = d 이므로 둘을 합친 것이 0보다 큰 것이 아닌 radius 보다 클 경우 출력하지 않게 했다.
for (auto& gameObject : gameObjects)
{
if (gameObject->GetMeshRenderer() == nullptr)
continue;
if (gameObject->GetCheckFrustum())
{
if (_frustum.ContainsSphere(
gameObject->GetTransform()->GetWorldPosition(),
gameObject->GetTransform()->GetBoundingSphereRadius()) == false)
{
continue;
}
}
gameObject->GetMeshRenderer()->Render();
}
Camera::Render에서 gameObject 별로 ContainsSphere이 false일 경우 Render을 하지 않고 continue로 넘겨버린다.
GetBoundingSphereRadius는 물체의 범위를 표현하기 위해 임시적으로 만든 함수로 localscale.x, y, z 중 가장 큰 값을 반지름으로 하는 구를 반환한다.
'그래픽스' 카테고리의 다른 글
| [DX12] Root Signature (0) | 2022.07.22 |
|---|---|
| [DX12] Constant Buffer (0) | 2022.07.22 |
| [DX12] Normal Mapping (0) | 2022.02.28 |
| [DX12] Projection, Screen 변환 행렬 (0) | 2022.02.26 |
| [DX12] World, View 변환 행렬 (0) | 2022.02.26 |