[DirectX11 3D] Frustum Culling

한창희·2024년 6월 10일

DirecX3D 엔진 만들기

목록 보기
12/14

📕Frustum Culling(절두체 컬링)

: 3D 공간에서 카메라 시야범위 안에 있는 물체만 렌더링하고 나머지는 렌더링하지 않는 기법으로 드로우콜을 줄여 속도가 빨라진다.

📌절두체

: 투영행렬을 계산할 때 범위의 모양으로 삼각뿔을 자른 모양

  • 절두체는 6개의 평면의 내부라고 정의할 수 있음-> 절두체 안에있는지 확인하기 위해 평면의 방정식을 이용한다.

📌평면의 방정식

ax + by + cz + d =0

  • a,b,c 는 평면의 법선 벡터, d는 평면과 원점사이의 거리를 나타내는 값
  • 수식을 만족하는 점 a,b,c는 평면 위에 있다.
  • ax+by+cz+d > 0 라면, 원점을 기준으로 점이 평면 뒤에 있다.
  • ax+by+cz+d < 0 라면, 원점을 기준으로 점이 평면 앞에 있다.

위와 같은 평면의 방정식의 성질을 이용하여 절두체의 여섯면에 대해 한 점이 모두 안에 있다면 절두체 안에있다고 판정할 수 있고, 이를 이용해 Culling을 한다.


📌 Frustum

//"Frustum.h"

#pragma once
#include "CEntity.h"

enum class FACE
{
    F_NEAR,
    F_FAR,
    F_TOP,
    F_BOT,
    F_LEFT,
    F_RIGHT,
    END,
};


class CCamera;

class CFrustum :
    public CEntity
{
private:
    CCamera*    m_Owner;
    Vec3        m_ProjPos[8];
    Vec4        m_arrFace[(UINT)FACE::END];    



public:
    bool FrustumCheck(Vec3 _Center);
    bool FrustumCheck(Vec3 _Center, float _Radius);

public:
    void finaltick();

private:
    void SetOwner(CCamera* _Cam) { m_Owner = _Cam; }

public:
    CLONE(CFrustum);
    CFrustum();
    ~CFrustum();

    friend class CCamera;
};
  • Frustum은 카메라가 들고 자신의 범위를 Frustum을 통해 확인하여 컬링한다.
  • 평면의 방정식에 대한 정보는 Vec4로 저장한다.(a,b,c,d)
#include "pch.h"
#include "CFrustum.h"

#include "CCamera.h"

CFrustum::CFrustum()
	: m_Owner(nullptr)
	, m_ProjPos{}
	, m_arrFace{}
{
	// 투영좌표계 초기 좌표 설정
	//     4 -- 5
	//   / |   /|
	//  /  7 -/ 6
	// 0 -- 1  /
	// | /  | /
	// 3 -- 2
	m_ProjPos[0] = Vec3(-1.f, 1.f, 0.f);
	m_ProjPos[1] = Vec3(1.f, 1.f, 0.f);
	m_ProjPos[2] = Vec3(1.f, -1.f, 0.f);
	m_ProjPos[3] = Vec3(-1.f, -1.f, 0.f);

	m_ProjPos[4] = Vec3(-1.f, 1.f, 1.f);
	m_ProjPos[5] = Vec3(1.f, 1.f, 1.f);
	m_ProjPos[6] = Vec3(1.f, -1.f, 1.f);
	m_ProjPos[7] = Vec3(-1.f, -1.f, 1.f);
}

CFrustum::~CFrustum()
{
}

void CFrustum::finaltick()
{
	Matrix matInv = m_Owner->GetProjInvMat() * m_Owner->GetViewInvMat();

	Vec3 vWorld[8] = {};
	for (int i = 0; i < 8; ++i)
	{
		vWorld[i] = XMVector3TransformCoord(m_ProjPos[i], matInv);
	}

	// WorldSpace 상에서 카메라의 시야를 표현하는 6 개의 평면 제작
	//     4 -- 5
	//   / |   /|
	//  /  7 -/ 6
	// 0 -- 1  /
	// | /  | /
	// 3 -- 2
	m_arrFace[(UINT)FACE::F_NEAR] = XMPlaneFromPoints(vWorld[0], vWorld[1], vWorld[2]);
	m_arrFace[(UINT)FACE::F_FAR] = XMPlaneFromPoints(vWorld[5], vWorld[4], vWorld[7]);
	m_arrFace[(UINT)FACE::F_TOP] = XMPlaneFromPoints(vWorld[0], vWorld[4], vWorld[5]);
	m_arrFace[(UINT)FACE::F_BOT] = XMPlaneFromPoints(vWorld[2], vWorld[6], vWorld[7]);
	m_arrFace[(UINT)FACE::F_LEFT] = XMPlaneFromPoints(vWorld[0], vWorld[7], vWorld[4]);
	m_arrFace[(UINT)FACE::F_RIGHT] = XMPlaneFromPoints(vWorld[1], vWorld[5], vWorld[6]);
}

bool CFrustum::FrustumCheck(Vec3 _vWorldPos)
{
	// ->
	// N dot P + D > 0
	for (int i = 0; i < 6; ++i)
	{
		float dot = _vWorldPos.Dot(Vec3(m_arrFace[i].x, m_arrFace[i].y, m_arrFace[i].z));

		if ( dot + m_arrFace[i].w > 0 )
		{
			return false;
		}
	}

	return true;
}

bool CFrustum::FrustumCheck(Vec3 _Center, float _Radius)
{
	// ->
	// N dot P + D > 0
	for (int i = 0; i < 6; ++i)
	{
		float dot = _Center.Dot(Vec3(m_arrFace[i].x, m_arrFace[i].y, m_arrFace[i].z));

		if (dot + m_arrFace[i].w > _Radius)
		{
			return false;
		}
	}

	return true;
}

평면의 방정식을 구하기 위해서는 평면위의 점 세개가 필요하다.
이를 쉽게 구하기 위해 World Pos가 카메라의 View, Projection 을 곱해 ndc 좌표계로 이동하는 것을 거꾸로 하여, ndc공간에서의 좌표를 기본 좌표로 잡고, 카메라의 ProjInv, ViewInv를 곱하여 평면의 world좌표를 구한다.

구한 점 3개와 XMPlaneFromPoints함수를 통해 평면의 방정식을 알아낸다.

평면의 방정식의 성질을 이용해서 물체의 WorldPos가 평면의 안쪽에 있는지 바깥쪽에 있는지 구분하여 만약 평면의 바깥쪽에있다면 렌더링하지 않는다.


📌문제점

  • 물체의 중앙이 Frustum안에 들어와있지않아 컬링되지만, 물체의 일부가 카메라 시야범위 내에있어 렌더링 해야하는경우 -> 물체에 바운딩 박스를 줘서 바운딩 박스의 일부가 카메라 시야범위 안에 들어온다면, 컬링하지 않도록 수정해야 한다.
  • SkyBox처럼 World Pos와는 상관 없이 항상 렌더링되어야 하는 물체가 컬링된다. -> 물체가 컬링되지 않도록 옵션값을 주어 Frustum Check를 받을지 말지를 결정한다.

0개의 댓글