지난 포스팅에서는 최적의 길찾기
라는 새로운 주제를 선정하게 된 내용을 다뤘습니다. 이번 포스팅에서는 주제를 달성하기 위해 각 Voxel에 데이터를 추가하게된 과정과 결과에 대해서 적어보겠습니다.
CPathFinding 플러그인에서는 특정 복셀의 Volume상에서의 x그리드 개수 y그리드 개수 z그리드 개수 만큼의 uint32
배열로 복셀들을 저장,관리합니다.
예를 들어서 Volume안에서의 상대 x,y,z좌표가 (2,3,4)인 복셀의 경우
uint32* Voxels;//복셀저장 배열(동적 생성)
//복셀 좌표
int x = 2;
int y = 3;
int z = 4;
//복셀참조
uint32 voxel = Voxels[ConvertIndex(x,y,z)];
bool IsOverlapped = IsOverlapped(voxel);
bool IsFree = IsOverlapped(voxel);
위 코드같은 방식으로 복셀을 저장하고 Overlap정보와 Free정보를 취합니다. 배열의 Index자체가 Position정보를 나타내게 되고 uint32
값은 Overlapped,Free같은 복셀의 상태정보를 저장합니다. 상태정보는 bit 연산으로 취할 수 있습니다.
bit flag구성 예시(uint32)
0000 0000 0000 0000 0000 0000 0000 0001 => Overlapped상태
0000 0000 0000 0000 0000 0000 0000 0000 => Free상태
제가 이전 포스팅에서 제시했던 벽타기와 글라이딩을 고려한 최적 경로
기능 구현을 위해서는 A* 알고리즘에 사용되는 Free Voxel에 벽과 땅에 인접해 있는지 떨어져 있는지에 대한 정보가 필요합니다. 이렇게 저장된 정보를 기반으로 A* 알고리즘을 계산할 때 예외처리를 구현할 수 있기 때문입니다.
가장 먼저 시도한 방법은 Collision은 동작하지만 렌더링은 되지 않는 투명한 Mesh로 벽과 땅을 인식하게 하는 것이었습니다. 다음은 코드 일부입니다.
bool ACustomVoxelVolume::RecheckOctreeAtDepth(CPathOctree* OctreeRef , FVector TreeLocation , uint32 Depth)
{
bool IsFree = true;
for (const auto& Shape : TraceShapesByDepth[Depth])
{
if (GetWorld()->OverlapAnyTestByChannel(TreeLocation, FQuat(FRotator(0)), TraceChannel, Shape))
{//아무 액터나 충돌시
TArray<FOverlapResult> OverlapResults;
if (GetWorld()->OverlapMultiByChannel(OverlapResults, TreeLocation, FQuat(FRotator(0)),TraceChannel, Shape))
{
IsFree = false;
for (const auto& OverlapResult : OverlapResults )
{
if ( UStaticMeshComponent* MeshComponent = Cast<UStaticMeshComponent>(OverlapResult.GetComponent()) )
{
if ( MeshComponent->GetStaticMesh()->GetFName() == WallMesh->GetFName() )
{//설정한 WallMesh일시 Wall데이터 부여(플러그인 제작자 방식대로 Data비트연산 사용)
uint32 IsWall = true;
OctreeRef->Data &= 0xFFFFFFFD;//11111111111111111111111111111101(2비트 값 날리기)
OctreeRef->Data |= (IsWall << 1);//0~~~00000010(2비트값 1로 설정
break;
}
}
}
break;
}
}
}
OctreeRef->SetIsFree(IsFree);
return IsFree;
}
위 코드에서는 설정한 StaticMesh와 충돌할때 Voxel에 벽정보를 저장해주고 있습니다.
(파란색 복셀: 벽으로 인식된 부분)
이 방법은 잘 동작했지만 한계가 존재했습니다. 바로, 일일이 모든 벽과 땅에 해당 투명 Mesh를 잘 배치해야만 동작한다는 것이었습니다. 저는 그래서 이런 번거로운 작업없이 한번에 잘 동작하는 방식은 없는지 더 고민하게 되었습니다.
고민결과 LineTrace함수를 이용할 수 있다는 것을 깨달았습니다. 플러그인 코드에서는 모든 복셀별로 Box영역의 충돌체크를 하는 부분이 있었는데, 해당 코드를 상속받아서 voxel과 인접한 6방향으로 LineTrace를 체크하는 코드를 추가했습니다.
bool ACPathVolumeHermes::RecheckOctreeAtDepth(CPathOctree* OctreeRef , FVector TreeLocation , uint32 Depth)
{
bool IsFree = Super::RecheckOctreeAtDepth(OctreeRef, TreeLocation, Depth);
if (IsFree)
{
float TraceAmount = VoxelSize * 1.49f;//VoxelSize: 최소복셀 크기
{//벽 판단
bool IsWall1 = GetWorld()->LineTraceTestByChannel(TreeLocation , FVector(TreeLocation.X , TreeLocation.Y - TraceAmount , TreeLocation.Z) , TraceChannel);
bool IsWall2 = GetWorld()->LineTraceTestByChannel(TreeLocation , FVector(TreeLocation.X , TreeLocation.Y + TraceAmount , TreeLocation.Z) , TraceChannel);
bool IsWall3 = GetWorld()->LineTraceTestByChannel(TreeLocation , FVector(TreeLocation.X - TraceAmount , TreeLocation.Y , TreeLocation.Z) , TraceChannel);
bool IsWall4 = GetWorld()->LineTraceTestByChannel(TreeLocation , FVector(TreeLocation.X + TraceAmount , TreeLocation.Y , TreeLocation.Z) , TraceChannel);
bool IsWall = IsWall1 || IsWall2 || IsWall3 || IsWall4;
OctreeRef->SetIsWall(IsWall);
if ( !IsWall )
{//벽은 아니지만 대각선 아래방향에 벽이 존재하는 경우 -> 벽으로 취급
bool IsExtraWall1 = GetWorld()->LineTraceTestByChannel(TreeLocation , FVector(TreeLocation.X , TreeLocation.Y - TraceAmount , TreeLocation.Z-TraceAmount) , TraceChannel);
bool IsExtraWall2 = GetWorld()->LineTraceTestByChannel(TreeLocation , FVector(TreeLocation.X , TreeLocation.Y + TraceAmount , TreeLocation.Z-TraceAmount) , TraceChannel);
bool IsExtraWall3 = GetWorld()->LineTraceTestByChannel(TreeLocation , FVector(TreeLocation.X - TraceAmount , TreeLocation.Y , TreeLocation.Z-TraceAmount) , TraceChannel);
bool IsExtraWall4 = GetWorld()->LineTraceTestByChannel(TreeLocation , FVector(TreeLocation.X + TraceAmount , TreeLocation.Y , TreeLocation.Z-TraceAmount) , TraceChannel);
bool IsGround = GetWorld()->LineTraceTestByChannel(TreeLocation, FVector(TreeLocation.X, TreeLocation.Y, TreeLocation.Z- TraceAmount), TraceChannel);
bool IsExtraWall = (IsExtraWall1 || IsExtraWall2 || IsExtraWall3 || IsExtraWall4) && !IsGround;
OctreeRef->SetIsWall(IsExtraWall);
}
}
//땅 판단
bool IsGround = GetWorld()->LineTraceTestByChannel(TreeLocation, FVector(TreeLocation.X, TreeLocation.Y, TreeLocation.Z- TraceAmount), TraceChannel);
OctreeRef->SetIsGround(IsGround);
}
return IsFree;
}
이 코드는 잘 동작했으며, 별도의 Mesh를 레벨에 추가하지 않고도 벽과 땅 데이터를 Voxel에 추가할 수 있었습니다.
위 스크린샷과 같이 벽(하늘색),땅(자주색),벽+땅(노란색) 영역에 대한 정보를 Voxel에 추가할 수 있었습니다.
이번 포스팅에서는 복셀에 벽과 땅 정보를 추가해야하는 필요성과 개발과정에 관한 이야기를 다뤘습니다. 다음 포스팅에서는 추가한 정보기반으로 최적의 경로
를 계산하는 코드를 작성하는 내용을 다뤄보도록 하겠습니다.
와, UNSEEN 프로젝트 글 정말 잘 봤어요! 저도 참가하고 싶었는데, 이렇게 보게 되니 반갑네요. Voxel 데이터 추가 부분이 흥미롭군요. 덕분에 많이 배워갑니다! 앞으로도 유익한 정보 많이 공유해 주세요. 덕분에 언리얼 엔진에 대한 열정이 더 커졌어요. 감사합니다!