오늘 공부해볼 내용은 Point Cloud를 어떻게 실시간 렌더링 할 것인지, 이 점들을 어떻게 3D물체, 즉 메쉬로 재구성을 할 것인지에 대해 알아볼 것이다. 우리의 AI가 2D 이미지를 통해 충분히 맵에 대해 학습했으니 이제는 2D에서 3D로 직접 맵을 구성하고 파악할 수 있게끔 초능력을 부여할 차례이다.
하지만 학습한 내용을 그대로 렌더링하게 되면 중대한 문제가 발생하는데 그 문제가 바로 매 시각마다 달라지는 결과로 인한 노이즈 문제이다. 이 노이즈 문제는 구현한 3D 메쉬를 흔들리게 만들고 제대로된 결과를 만들지 못하게 한다. 고로 이 문제 또한 해결할 방법을 찾아보자.
[시각화] Point Cloud 실시간 렌더링
[재구성] Point Cloud → 3D Mesh
1. 방법 A: 실시간 메시 생성 (Procedural Mesh Component)
이대로 진행하면 나올 문제: 지터링(Jittering) 현상
2. 방법 B: TSDF / Voxel Fusion (고급 기법)
TSDF를 위해 선행으로 알아야 하는 변환좌표 사용법: Visual Odometry (VO)
수만~수십만 개의 3D 포인트를 매 프레임 효율적으로 렌더링하는 것이 목표다. CPU 기반의 AActor 스폰은 프레임 드랍을 유발하므로 반드시 GPU 가속을 활용해야 한다. 왜 매 프레임을 렌더링해야할까?
‘소프트웨어 중심의 언리얼 프로젝트' 관점으로
요약하자면, 이 프로젝트에서 3D 포인트를 매 프레임 렌더링 하는 이유는 '맵을 생성(Generate)'하는 것이라기보다는, 'AI의 시각(2D 이미지)만으로 3D 공간을 실시간으로 이해하고 복제(Reconstruct)'하는 능력 그 자체를 구현하는 것이다.
Niagara에 포인트 클라우드 데이터를 전달하는 가장 효율적인 방법은 Niagara Data Interface를 활용하는 것이다.
A. C++ (데이터 전송) Python으로부터 TArray<FVector> (월드 좌표계로 변환된 포인트 클라우드) 데이터를 수신했다고 가정한다.
C++
// YourActor.h
#include "NiagaraComponent.h"#include "NiagaraDataInterfaceArrayFunctionLibrary.h"#include "NiagaraSystem.h"
UPROPERTY(EditAnywhere)
UNiagaraComponent* NiagaraComponent;
// ...
// YourActor.cpp
// Python에서 데이터를 수신했을 때 호출되는 함수 (예시)
void AMyActor::OnPointCloudReceived(const TArray<FVector>& PointCloudData)
{
if (NiagaraComponent)
{
// 1. Niagara 시스템에 "PointCloud"라는 이름의 FVector 배열 변수가 있다고 가정한다.
// 2. 이 함수를 호출하여 Niagara 시스템의 GPU 메모리에 데이터를 직접 복사한다.
UNiagaraDataInterfaceArrayFunctionLibrary::SetNiagaraArrayVector(
NiagaraComponent,
FName("PointCloud"), // Niagara 시스템 내부의 변수 이름
PointCloudData
);
// (선택적) 파티클 수를 포인트 수에 맞게 동적으로 설정
NiagaraComponent->SetIntParameter(FName("NumParticles"), PointCloudData.Num());
}
}
B. Niagara System (데이터 수신 및 스폰)
이 파이프라인이 완성되면, C++에서 SetNiagaraArrayVector를 호출할 때마다 Niagara 시스템이 즉시 GPU에서 해당 위치에 파티클을 렌더링한다.
점을 넘어 '면'을 만드는 단계. 이것이 진정한 3D Scene Reconstruction이다.
Procedural Mesh Component (PMC)는 런타임에 정점(Vertices)과 삼각형(Triangles) 인덱스 데이터를 받아 메시를 동적으로 생성하는 컴포넌트다.
구현해보자:
// Vertices: 2D 그리드를 순회하며 생성한 TArray<FVector> (Step 2의 결과)
// Triangles: TArray<int32>
for (int y = 0; y < Height - 1; y++)
{
for (int x = 0; x < Width - 1; x++)
{
int v0 = (y * Width) + x; // (x, y)
int v1 = (y * Width) + (x + 1); // (x+1, y)
int v2 = ((y + 1) * Width) + x; // (x, y+1)
int v3 = ((y + 1) * Width) + (x + 1); // (x+1, y+1)
// Triangle 1
Triangles.Add(v0);
Triangles.Add(v1);
Triangles.Add(v2);
// Triangle 2
Triangles.Add(v2);
Triangles.Add(v1);
Triangles.Add(v3);
}
}
// PMC에 메시 생성 요청
ProceduralMeshComponent->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, Colors, Tangents, true);
Monocular Depth Estimation 모델은 완벽하지 않다. 특히 비디오를 실시간으로 처리할 때, 매 프레임 추정되는 깊이 값이 미세하게(혹은 심하게) 떨리게 된다.
이 결과물의 문제를 해결하기 위해 다음과 같은 방법을 사용해보자
TSDF란 "세상을 안정적으로 쌓아 올리는 방법" 으로 실시간 재구성을 위한 업계 표준 방식이다. (예: Kinect Fusion)
TSDF의 작동 원리
TSDF는 "한 프레임만 믿지 말고, 수백 프레임의 정보를 누적 평균내자"는 아이디어다.
융합 (Fusion) 과정 (핵심!)
새로운 뎁스 맵(포인트 클라우드)이 들어올 때마다 (VO를 통해 월드에 정렬된 상태로),
VO는 연속된 이미지 프레임(Frame 1, Frame 2) 사이의 공통 특징점(Feature)들을 추적한다.
VO가 이 변화량을 매 프레임 정확하게 제공해야만, 모든 뎁스 맵을 동일한 월드 좌표계에 정렬(Align)시킬 수 있다.
// 최종 통합 파이프라인
| Unreal Engine 좌표변환 이해하기(2) (0) | 2025.10.20 |
|---|---|
| Unreal Engine 좌표변환 이해하기(1) (0) | 2025.10.20 |
| 완전히 새로 정리된 Unreal + ONNX 통합 코드 (객체 감지 + 지형 파악) Part1 (0) | 2025.09.20 |
| Unreal Engine에서 학습한 ONNX 모델 불러와 실시간 추론하기 (0) | 2025.09.13 |
| Unreal Engine에서 CUDA와 OpenCV 사용 시행착오 정리하기 (5) | 2025.08.30 |