절두체 컬링(Frustum Culing)이란?
카메라의 시야(절두체)에서 들어오지 않은 객체를 제외하는 것,
즉, 그 객체들은 렌더링에서 제외된다. 하지만 제외작업을 수행하기 위한 연산작업은 필요로 한다.
절두체 컬링을 하기 위해서는 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와 일치
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를 이용하여 삼각형을 그릴때 필요한 개념 내용을 참고하면 좋다.
절두체 컬링의 경계 구 방식의 작동방식을 설명 할 것이다.
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>());
}
'컴퓨터 그래픽스' 카테고리의 다른 글
[DirectX11] D3D - Sprite2D ( 투영(Projection) 간단한 내용) (0) | 2023.06.17 |
---|---|
[DirectX11] 계층구조(Hierarchy) - D3D 태양계 만들기 (0) | 2023.06.13 |
그래픽스 파이프라인 간단 내용 정리 (0) | 2023.06.08 |
기본용어 정리 (0) | 2023.06.03 |