절두체 컬링(Frustum Culing)이란?

카메라의 시야(절두체)에서 들어오지 않은 객체를 제외하는 것,

즉, 그 객체들은 렌더링에서 제외된다. 하지만 제외작업을 수행하기 위한 연산작업은 필요로 한다.

 

World Space에 배치된 Mesh(객체)들을 <View 변환> 하는 과정

절두체 컬링을 하기 위해서는 World Space에 있는 절두체 영역을(a그림) 구해야된다.

최종 절두체 영역(c그림)의 8개 정점에서 역으로 계산을 해야한다. 

 

Vworld = VLocal * TM(Transfrom Matrix)  :  Vertex data를 월드변환하는 과정의 식

최종Vertex = Vworld * ViewMatrix * ProjectMatix : 월드로 배치된 Vertex를 View(카메라)행렬과 투영행렬을 곱해 카메라를 원점으로 Vertex가 배치되는 식이다.

 

최종Vertex = Vworld * ViewMatrix * ProjectMatirx 식 양변에 카메라 * 투영 역행렬를 곱해주면

최종 Vertex * (카메라 * 투영 역행렬) = Vworld가 나오게 된다. 

( ∵  카메라 * 투영 역행렬 : (ViewMatrix * ProjectMatix)^-1 )

이러면 WorldSpace에 있는 절두체의 영역(우리가 보고있는 카메라 화면)을 찾을수있게된다. 


필요한 구성요소는

절두체 컬링을 하기위해 절두체 class가 필요함 -> Frustum class 를 생성

View행렬과 Matrix행렬을 가져오기 위한 Camera -> Camera 포인터 변수 필요

절두체 면(Plane) 과 벡터의 수직상 거리를 비교할때 필요한 오차범위 -> planeEpsilon;

절두체 면을 관리할 vector -> frustumPlane[6] //밑에 그림의 숫자와 index와 일치

 

8개의 veretx로 6개의 plane을 만들어 절두체 영역을 만들것, 코드영역에서도 이렇게 표현될 예정

Init함수 , 최종Vertex를 구성하는 함수, Frustum 내부에 객체가 있는지 check하는 함수 등등으로 구성하면 될것이다.

 

<Frusutum class> :

벡터(객체의 원점)를 인자값으로 가져와 Frustum 영역의 내부,외부를 체크하여 외부에 있다고 판단시 절두체컬링을 하게하는 class다.

더보기
//Frustum.h

#pragma once
#include "Components/DXCamera.h"

class Frustum
{
private:
	DXCamera* camera; //view,Projection 행렬을 가져오기 위한 변수 

	float planeEpsilon; // 평면테스트에서 사용되는 부동 소수점 오차범위를 나타냄 
	XMVECTOR frustumPlane[6]; //Plane : 평면 -> 절두체 평면 

public:
	bool Initialize(DXCamera* camera, float planeEpsilon = FLT_EPSILON);
	void ConstructFrustum();

	//IsInFrustumBounds~ 함수 설명
    //물체의 기준으로 히트박스(Bounds뒤에 들어가는 도형,도형은 자유) 만들어서 그 안에있으면 있따고 처리 체크 

	bool IsInFrustum(XMFLOAT3 v); //Frustum 안에 물체 확인 bool 변수
	bool IsInFrustum(XMVECTOR v); //Frustum 안에 물체 확인 bool 변수
	bool IsInFrustumExceptUpDown(XMFLOAT3 v); //Frustum의 윗면과 아랫면은 무조건 있는걸로 취급하고 좌우앞뒤만 체크하는 변수

	bool IsInFrustumBoundsSphere(XMFLOAT3 v,float radius); //Frustum 안에 물체의 기준으로 구 모형 안에 있는지 물체 확인 bool 변수
	bool IsInFrustumBoundsSphere(XMVECTOR v, float radius); //Frustum 안에 물체의 기준으로 구 모형 안에 있는지 물체 확인 bool 변수
	bool IsInFrustumBoundsSphereExceptUpDown(XMFLOAT3 v, float radius); //Frustum안에 물체의 기준으로 구 모혀 위와 아래는무조건 있는걸로 취급하고 좌우앞뒤만 체크하는 변수

	bool IsInFrustumBoundsCube(XMFLOAT3 v, float radius); //Frustum 안에 육면체 모양 안에 물체 확인 bool 변수
	bool IsInFrustumBoundsCube(XMVECTOR v, float radius); //Frustum 안에 육면체 모양 안에 물체 확인 bool 변수

	//Rectangle : AABB/OBB -> 2D게임 만들때거나 3D게임의 UI 만들때 사용
	bool IsInFrustumBoundsRectangle(XMFLOAT3 v, XMFLOAT3 r); //Frustum 안에 2D사각형 안에 물체 확인 bool 변수
	bool IsInFrustumBoundsRectangle(XMVECTOR v, XMVECTOR r); //Frustum 안에 2D사각형 안에 물체 확인 bool 변수

private:
	bool IsInBoundsCube(XMVECTOR p, XMFLOAT3 v, XMFLOAT3 r);
};

//====================================================================

#include "Frustum.h"
bool Frustum::Initialize(DXCamera* camera, float planeEpsilon)
{
	if (!camera) return false; // 매개변수 camera 가 존재하지 않으면 false 반환

	//Frustum data 초기화
	this->camera = camera;
	this->planeEpsilon = planeEpsilon;
	for (int i = 0; 6 > i; i++) frustumPlane[i] = XMVectorSet(0, 0, 0, 0);
	return true;
}

void Frustum::ConstructFrustum()
{
	//최종 vertex는 (-1,-1,0) ~ (1,1,1) 사이의 값으로 바뀐다.
	//최종Vertex = Vworld * ViewMatrix * ProjectMatirx해서 나온 Frusutm 영역의 8개 vertex data임.
    XMFLOAT3 frustumVertices[8] =
	{
		{-1.0f, -1.0f, 0.0f }, // near left bottom v0
		{1.0f, -1.0f, 0.0f }, // near right bottom v1
		{1.0f, -1.0f, 1.0f }, // far right bottom v2
		{-1.0f, -1.0f, 1.0f }, // far left bottom v3
		{-1.0f, 1.0f, 0.0f }, // near left top v4
		{1.0f, 1.0f, 0.0f }, // near right top v5
		{1.0f, 1.0f, 1.0f }, // far right top v6
		{-1.0f, 1.0f, 1.0f }, // far left top v7
	};

	//view * proj의 역행렬 계산.
	XMMATRIX invViewProjectionMatrix = XMMatrixInverse(NULL, camera->GetViewMatrix() * camera->GetProjectionMatrix());
	
	XMVECTOR frustumVectors[8]; //최종vertex * (viewprojectionMatrix)T를 계산한 vertex들의 vector
	for (int i = 0; 8 > i; i++)
	{
		// XMVector3TransformCoord : 주어진 행렬을 사용하여 벡터로 변형하는 함수
		//최종vertex * (viewprojectionMatrix)T 계산,
		frustumVectors[i] = XMVector3TransformCoord(XMVectorSet(frustumVertices[i].x, frustumVertices[i].y, frustumVertices[i].z, 1.0f),
			invViewProjectionMatrix);
	}
	
    //계산된 Vworld(월드상의 vertex)들을 3개씩 사용하여 절두체의 평면을 구성 
    //인덱스 순서에 따라 절두체면(폴리곤)의 윗면이 결정되어 윗면이 법선벡터의 방향이다.(중요)
	frustumPlane[0] = XMPlaneFromPoints(frustumVectors[4], frustumVectors[7], frustumVectors[6]); // 상 평면(top)
	frustumPlane[1] = XMPlaneFromPoints(frustumVectors[0], frustumVectors[1], frustumVectors[2]); // 하 평면(bottom)
	frustumPlane[2] = XMPlaneFromPoints(frustumVectors[0], frustumVectors[4], frustumVectors[5]); // 근 평면(near)
	frustumPlane[3] = XMPlaneFromPoints(frustumVectors[2], frustumVectors[6], frustumVectors[7]); // 원 평면(far)
	frustumPlane[4] = XMPlaneFromPoints(frustumVectors[0], frustumVectors[3], frustumVectors[7]); // 좌 평면(left)
	frustumPlane[5] = XMPlaneFromPoints(frustumVectors[1], frustumVectors[5], frustumVectors[6]); // 우 평면(right)
}


bool Frustum::IsInFrustum(XMFLOAT3 v)
{
	return IsInFrustum(XMVectorSet(v.x, v.y, v.z, 1.0f));
}
bool Frustum::IsInFrustum(XMVECTOR v)
{
	return IsInFrustumBoundsSphere(v, 0.0f);
}
bool Frustum::IsInFrustumExceptUpDown(XMFLOAT3 v)
{
	return IsInFrustumBoundsSphereExceptUpDown(v, 0.0f);
}
bool Frustum::IsInFrustumBoundsSphere(XMFLOAT3 v, float radius)
{
	return IsInFrustumBoundsSphere(XMVectorSet(v.x, v.y, v.z, 1.f), radius);
}

//절두체 안에 경계구가 있는지 체크하는 함수( 작동방식 밑에 서술)
bool Frustum::IsInFrustumBoundsSphere(XMVECTOR v, float radius)
{
	for (int i = 0; 6 > i; i++) // 왜 6인가? 절두체의 면이 6개임
	{
		//XMPlaneDotCoord의 함수 반환값은 XMVECTOR인데, XMVECTOR의 모든값을 내적크기로 반환한다.
		//VectorGetX,Y,Z 함수 아무거나 사용해도 괜찮다. 전부 내적의 값이기 떄문이다.
		if (XMVectorGetX(XMPlaneDotCoord(frustumPlane[i], v)) > (radius + planeEpsilon))
		{
			// 벡터의 내적(물체와 절두체 면의 수직상의 거리) > 물체의 경계(히트박스)거리 가 참인경우
			return false; //frustum 영역 외부에 있음
		}
	}

	return true;//frustum 영역 내부에 있음
}

bool Frustum:: IsInFrustumBoundsSphereExceptUpDown(XMFLOAT3 v, float radius)
{
	XMVECTOR vector = XMVectorSet(v.x, v.y, v.z, 1.0f);

	for (int i = 2; 6 > i ; i++) // 0,1 인덱스의 vertex는 윗면과 ,아랫면의 vertex이므로 제외
	{
		//XMPlaneDotCoord (평면(plane) , 벡터(vertex) ) : 평면과 벡터사이의 내적값을 반환.
		if (XMVectorGetX(XMPlaneDotCoord(frustumPlane[i], vector)) > (radius + planeEpsilon)) return false;
	}

	return true;
}

//절두체를 경계박스로 컬링하는 함수 
bool Frustum::IsInFrustumBoundsCube(XMFLOAT3 v, float radius)
{
	for (int i = 0; 6 > i; i++)
	{
		if (IsInBoundsCube(frustumPlane[i], v, XMFLOAT3(radius, radius, radius))) continue;
		return false;
	}
	return true;
}

bool Frustum::IsInFrustumBoundsCube(XMVECTOR v, float radius)
{
	XMFLOAT3 dest;
	XMStoreFloat3(&dest, v);
	return IsInFrustumBoundsCube(dest, radius);
}

//절두체를 2D사각형으로 컬링하는 함수(2D게임에서 사용하게됨)
bool Frustum::IsInFrustumBoundsRectangle(XMFLOAT3 v, XMFLOAT3 r)
{
	for (int i = 0; 6 > i; i++)
	{
    	//
		if (IsInBoundsCube(frustumPlane[i], v, r)) continue;
		return false;
	}
	return true;
}

bool Frustum::IsInFrustumBoundsRectangle(XMVECTOR v, XMVECTOR r)
{
	XMFLOAT3 destV, destR;
	XMStoreFloat3(&destV, v);
	XMStoreFloat3(&destR, r);
	return IsInFrustumBoundsRectangle(destV, destR);
}

//경계박스 ,및 경계사각형(2D)의 함수 작동방식 
bool Frustum::IsInBoundsCube(XMVECTOR p, XMFLOAT3 v, XMFLOAT3 r)
{
	return
		(
        	//절두체 면(p) ,벡터v,r연산
            //모든 벡터의 내적이 오차범위 보다 작으면 있면 frustum 안에 존재 
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x - r.x, v.y - r.y, v.z - r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x - r.x, v.y - r.y, v.z + r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x - r.x, v.y + r.y, v.z - r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x - r.x, v.y + r.y, v.z + r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x + r.x, v.y - r.y, v.z - r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x + r.x, v.y - r.y, v.z + r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x + r.x, v.y + r.y, v.z - r.z, 1.0f))) <= planeEpsilon) ||
			(XMVectorGetX(XMPlaneDotCoord(p, XMVectorSet(v.x + r.x, v.y + r.y, v.z + r.z, 1.0f))) <= planeEpsilon)
		);
}

 

최종 Vertex의 값 범위가 (-1,-1,0) ~ ( 1,1,1) 사이의 값으로 바뀐다 라는 주석이 있는데

이 값은 D3D에서는 절대 고정이며 view 변환시 카메라 시점을 원점으로 바꾸고 투영행렬을 계산하여 이미지화 (3D->2D)화 시키기 때문에 그런것이다.

//최종 vertex는 (-1,-1,0) ~ (1,1,1) 사이의 값으로 바뀐다.
	//최종Vertex = Vworld * ViewMatrix * ProjectMatirx해서 나온 Frusutm 영역의 8개 vertex data임.
    XMFLOAT3 frustumVertices[8] =
	{
		{-1.0f, -1.0f, 0.0f }, // near left bottom v0
		{1.0f, -1.0f, 0.0f }, // near right bottom v1
		{1.0f, -1.0f, 1.0f }, // far right bottom v2
		{-1.0f, -1.0f, 1.0f }, // far left bottom v3
		{-1.0f, 1.0f, 0.0f }, // near left top v4
		{1.0f, 1.0f, 0.0f }, // near right top v5
		{1.0f, 1.0f, 1.0f }, // far right top v6
		{-1.0f, 1.0f, 1.0f }, // far left top v7
	};

 

 

 

 

절두체 각 면의 법선벡터 방향을 정하는 것은

그래픽스 파이프라인 폴리곤을 그리는 방식에 따라 정해진다.

이 블로그의 그래픽스 파이프라인 - 기본용어 정리 - Vertex를 이용하여 삼각형을 그릴때 필요한 개념 내용을 참고하면 좋다.

https://01149.tistory.com/4

 

기본용어 정리

글 작성자가 공부하고 배운것을 간략하게 정리하는 페이지 입니다. 필요한 정보가 있으시면 확인하시고 만약 잘못된 내용이 있거나 이런 내용이 추가됬으면 좋겠다라면 댓글 부탁드립니다. 감

01149.tistory.com

 

절두체 컬링의 경계 구 방식의 작동방식을 설명 할 것이다.

bool Frustum::IsInFrustumBoundsSphere(XMVECTOR v, float radius)
{
	for (int i = 0; 6 > i; i++) // 왜 6인가? 절두체의 면이 6개임
	{
		//내적은 frustumPlane에서 v까지의 수직상 거리와 같다.
		//벡터의 내적의 크기(벡터 * 법선벡터 = 해당면으로 수직의 벡터크기(길이) 
		// {벡터의 내적 < 오브젝트의 구 범위(반지름이 더 크면)} frustum안에 있다는 뜻이다.
		// XMPlaneDotCoord의 함수 반환값은 XMVECTOR인데, XMVECTOR의 모든값을 내적크기로 반환하기 떄문이다.
		// VectorGetX,Y,Z 함수 아무거나 사용해도 괜찮다. 전부 내적의 값이기 떄문이다.
		if (XMVectorGetX(XMPlaneDotCoord(frustumPlane[i], v)) > (radius + planeEpsilon))
		{
			// 벡터의 내적(물체와 절두체 면의 수직상의 거리) > 물체의 경계(히트박스)거리 가 참인경우
			return false; //frustum 영역 외부에 있음
		}
	}

	return true;//frustum 영역 내부에 있음
}
절두체 컬링 - 경계구 방식의 작동 방식

위의 그림 과정을 절두체 면이 6개니 총 6번 반복하여 동작을한다.

if (XMVectorGetX(XMPlaneDotCoord(frustumPlane[i], v)) > (radius + planeEpsilon))
{
    return false;
}

반복문 동작중 절두체 6면 중 한 면이라도,

벡터의 내적(수직상의 거리) 이 radius + planeEpsilon (경계 구 반지름 + 오차범위)보다 길면
frustun 외부에 존재하는 것이기 때문에 절두체 컬링(화면에서 렌더링을 제외하는)을 작업하게 된다.

<DirectX11Graphics> : DriectX 11 기반으로 Scene을 렌더링하기 위한 그래픽스 객체

더보기
// DirectX11Graphics.h
#include "Frustum.h"
class DirectX11Graphics final : public Graphics
{
Private:

	// Mesh의 이미지 관련 함수들을 담당하고 있음
    std::unique_ptr <PrimitiveModel> primitive; 
    // 컴포넌트 구조의 Mesh Obj : 컴포턴트 추가 및 Update관련(Mesh의 크기,회전,이동 함수등) 
	DXGameObject primitiveObj; 

	Frustum frustum; //절두체컬링을 위한 절두체 객체 추가
};

//========================================================

// DirectX11Graphics.cpp

void DirectX11Graphics::RenderFrame() //그래픽스 객체의 Draw 함수(프레임마다 렌더링함수)
{
	//렌더링 할때마다 , 카메라시점 기준(뷰행렬,투영행렬)으로 절두체 영역 생성
	frustum.ConstructFrustum();

    // GameObject Primitive Mesh Draw
    //절두체 컬링 - 경계구 판정을 사용하여 Mesh의 위치vector와 크기Vector를 인자값으로 가져감
    //크기vector는 경계구의 반지름으로 사용, 해당 객체가 카메라영역에서 사라지고 경계구까지 영역에서 벗어나게되면 절두체 컬링진행
   	//(이 자리에 경계 구의 반지름 값을 조정하여 넣을 수있음 )
    if (frustum.IsInFrustumBoundsSphere(primitiveObj.GetTransform()->GetPosition(), XMVectorGetX(primitiveObj.GetTransform()->GetScale())/*bounds*/))
    {
    	//절두체 6면, 영역 내에 Mesh(객체)가 존재 함으로 Draw를 한다.
    	primitiveObj.Draw(cameraObj.GetComponent<DXCamera>()->GetViewProjectionMatrix());
	}
}


bool DirectX11Graphics::InitializeScene() // 그래픽스 객체의 Scene(장면) Init
{
	//상자 mesh
    Primitive = std::make_unique<PrimitiveCube>(); // 상자 모양 mesh
    Primitive->Initialize(device.Get(), deviceContext.Get(), constantMatricesBuffer);
    Primitive->MakePrimitive("Textures\\box.jpg"); // mesh에 텍스쳐 이미지 추가
    PrimitiveObj.AddComponent<DXMeshRenderer>()->SetModel(Primitive.get());
    
    //절두체 객체 Init , 카메라의 뷰행렬,투영행렬이 필요하기 때문 -> 역행렬을 구해야함
	frustum.Initialize(cameraObj.GetComponent<DXCamera>());

}

 

+ Recent posts