본문 바로가기
카테고리 없음

Unreal Skeletal Mesh to Procedural Mesh - 멀티 스레드 사용

by daisy0461 2025. 2. 25.

지금까지 한 방법은 모든 SkeletalMesh의 모든 vertex를 순회한다는 것이다.

for문만 23309번 도는 것을 확인할 수 있다. 지금 3D Model이 1개만 있어서 성능적으로 그게 문제가 없다고 느껴지지만 이후에 더 늘어나면 더 성능에 부담이 될 것이라고 생각이 된다.

그렇기에 BeginPlay에서 미리 Game Thread가 아닌 다른 Thread에서 vertex만 선별해보기로 했다.

 

방법은 간단하다.

void AEnemy::SelectVertices(int32 LODIndex)
{
	if (!GetMesh() || !ProcMeshComponent){
        UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: SkeletalMeshComponent or ProcMeshComp is null."));
        return;
    }

	// SkeletalMesh를 가져온다.
    USkeletalMesh* SkeletalMesh = GetMesh()->GetSkeletalMeshAsset();
    if (!SkeletalMesh){
        UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: SkeletalMesh is null."));
        return;
    }
	//GetResourceForRendering - Skeletal Mesh의 렌더링 데이터를 가져오는 함수
    const FSkeletalMeshRenderData* RenderData = SkeletalMesh->GetResourceForRendering();
    if (!RenderData || !RenderData->LODRenderData.IsValidIndex(LODIndex)){
        UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: LODRenderData[%d] is not valid."), LODIndex);
        return;
    }
	//SkeletalMesh에서 LODRenderData를 가져온다.LODRenderData는 버텍스 데이터, 인덱스 데이터, 섹션 정보 등이 포함
    //FSkeletalMeshLODRenderData란 LOD의 Mesh 데이터를 가지고 있는 구조체이다.
    const FSkeletalMeshLODRenderData& LODRenderData = RenderData->LODRenderData[LODIndex];
	//SkinWeightVertexBuffer를 가져온다. -> vertex가 어떤 Bone에 영향을 받는지 저장하는 데이터이며 Animation에서 사용 예정
	const FSkinWeightVertexBuffer& SkinWeights = LODRenderData.SkinWeightVertexBuffer;

	//위치를 들고온다.
	FTransform MeshTransform = GetMesh()->GetComponentTransform();
	FVector TargetBoneLocation = GetMesh()->GetBoneLocation(TargetBoneName); 

	int32 vertexCounter = 0;
    for (const FSkelMeshRenderSection& Section : LODRenderData.RenderSections){
        //NumVertices - 해당 Section의 Vertex 수, BaseVertexIndex - 해당 Section의 시작 Vertex Index
        const int32 NumSourceVertices = Section.NumVertices;
        const int32 BaseVertexIndex = Section.BaseVertexIndex;

        for (int32 i = 0; i < NumSourceVertices; i++){
            const int32 VertexIndex = i + BaseVertexIndex;

            //vertex의 위치를 가져온다. -> LODRenderData.StaticVertexBuffers.PositionVertexBuffer(현재 LOD의 Vertex 위치 데이터를 저장하는 버퍼)
            //.VertexPosition(VertexIndex)-> VertexIndex의 위치를 가져온다. 반환 타입이 FVector3f이다.
            const FVector3f SkinnedVectorPos = LODRenderData.StaticVertexBuffers.PositionVertexBuffer.VertexPosition(VertexIndex);		//로컬 좌표 반환
			FVector WorldVertexPosition = MeshTransform.TransformPosition(FVector(SkinnedVectorPos)); // FVector3f -> FVector 변환 & 로컬 -> 월드 좌표
			//UE_LOG(LogTemp, Display, TEXT("WorldVertexPosition : %s"), *WorldVertexPosition.ToString());
			float DistanceToBone = FVector::Dist(WorldVertexPosition, TargetBoneLocation);
			//TargetBoneLocation을 기준으로 일정 거리 내에 있는 Vertex만 추가해서 Procedural Mesh 생성
            if (DistanceToBone <= CreateProceduralMeshDistance)
			{
				// 위치 정보 추가
				FVector LocalVertexPosition = FVector(SkinnedVectorPos);
				VertexIndexMap.Add(VertexIndex, vertexCounter);
				//WorldVertexPosition을 사용하면 다른 위치에 Procedural Mesh가 생성된다.
            	FilteredVerticesArray.Add(LocalVertexPosition);
				vertexCounter += 1;
				// 노멀(Normal), 탄젠트(Tangent) 추가
				const FVector3f Normal = LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(VertexIndex);
				const FVector3f TangentX = LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentX(VertexIndex);
				const FVector2f SourceUVs = LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV(VertexIndex, 0);
				Normals.Add(FVector(Normal));
				Tangents.Add(FProcMeshTangent(FVector(TangentX), false));
				UV.Add(FVector2D(SourceUVs));
				//VertexColors.Add(FColor(0, 0, 0, 255));
				VertexColors.Add(FColor(0, 0, 0, 255));
			}
        }
    }

	//UE_LOG(LogTemp, Display, TEXT("VertexIndexMan Count : %d"), VertexIndexMap.Num());
    //LODRenderData.MultiSizeIndexContainer.GetIndexBuffer()는 원래 Skeletal Mesh의 각 vertex가 어떻게 삼각형으로 구성되어있었는지를 들고온다.
    const FRawStaticIndexBuffer16or32Interface* IndexBuffer = LODRenderData.MultiSizeIndexContainer.GetIndexBuffer();
    if (!IndexBuffer){
        UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: Index buffer is null."));
        return;
    }

    //현재 LOD의 총 Index 수를 가져온다.
    const int32 NumIndices = IndexBuffer->Num();
	Indices.SetNum(NumIndices); // 모든 값을 0으로 초기화하며 메모리 공간 확보보
    for (int32 i = 0; i < NumIndices; i+=3){
        //IndexBuffer Get(i) - 현재 처리 중인 삼각형을 구성하는 버텍스 인덱스를 가져옴.
        //VertexIndex : Get(0) = a, Get(1) = b, Get(2) = c로 abc삼각형, Get(3) = c, Get(4) = d, Get(5) = a로 cda삼각형 (여기서 abcd는 FVector위치라고 취급)
        //즉, 첫 BaseVertexIndex에서 그려지는 삼각형부터 순서대로 삼각형이 그려지는 vertex의 Index를 가져온다.
        //결과적으로 Indices를 순환하면 3개씩 묶어서 삼각형을 그릴 수 있다.
        //uint32가 반환되어 int32로 Casting, 데이터 일관성을 위해 Casting한다.
        //Indices[i] = static_cast<int32>(IndexBuffer->Get(i));
		//아래 코드는 3각형을 이루는 3개의 i를 들고오는 것이다.
		int32 OldIndex0 = static_cast<int32>(IndexBuffer->Get(i));
		int32 OldIndex1 = static_cast<int32>(IndexBuffer->Get(i + 1));
		int32 OldIndex2 = static_cast<int32>(IndexBuffer->Get(i + 2));

		int32 NewIndex0 = VertexIndexMap.Contains(OldIndex0) ? VertexIndexMap[OldIndex0] : -1;
		int32 NewIndex1 = VertexIndexMap.Contains(OldIndex1) ? VertexIndexMap[OldIndex1] : -1;
		int32 NewIndex2 = VertexIndexMap.Contains(OldIndex2) ? VertexIndexMap[OldIndex2] : -1;
		// 기존 VertexIndex가 NewVertexIndex에 포함된 경우만 추가 - 실제로 내가 vertex 수집한 곳에 있는 Index인지 확인한다.
		//NewIndex >= FilteredVerticesArray.Num() - VertexIndexMap이 잘못된 값을 반환하거나, Indices 배열에 유효하지 않은 인덱스가 추가될 때 발생
		if (NewIndex0 < 0 || NewIndex0 >= FilteredVerticesArray.Num() || NewIndex1 < 0 || NewIndex1 >= FilteredVerticesArray.Num() || NewIndex2 < 0 || NewIndex2 >= FilteredVerticesArray.Num()){
			//UE_LOG(LogTemp, Warning, TEXT("Skipping triangle due to invalid indices: (%d, %d, %d) → (%d, %d, %d)"),	OldIndex0, OldIndex1, OldIndex2, NewIndex0, NewIndex1, NewIndex2);
		}
		else{
			Indices.Add(NewIndex0);
			Indices.Add(NewIndex1);
			Indices.Add(NewIndex2);
		}
    }

	//
	// for (int32 i = 0; i < FilteredVerticesArray.Num(); i++) {
	// 	// 잘린 부분의 버텍스를 Black(0, 0, 0)으로 설정
	// 	VertexColors.Add(FColor(0, 0, 0, 0));  // 알파 값 포함
	// }
}

이렇게 Vertex를 모으는 함수를 따로 지정해준다.

 

그리로 BeginPlay에서 다음과 같이 작성해준다.

	AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this]()
    {
        this->SelectVertices(0);
    });

이렇게 하면 다른 Thread에서 vertex를 선별하기 때문에 성능적으로 나아진다.

 

그리고 Skeletal Mesh 위치에 Procedural Mesh를 생성하는 코드는 따로 뺀다.

void AEnemy::CopySkeletalMeshToProcedural(int32 LODIndex)
{
    if (!GetMesh() || !ProcMeshComponent){
        UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: SkeletalMeshComponent or ProcMeshComp is null."));
        return; 
    }

    //Skeletal Mesh의 Location과 Rotation을 들고온다.
    FVector MeshLocation = GetMesh()->GetComponentLocation();
    FRotator MeshRotation = GetMesh()->GetComponentRotation();

    //Skeletal Mesh의 Location과 Rotation을 Procedural Mesh에 적용한다.
    ProcMeshComponent->SetWorldLocation(MeshLocation);
    ProcMeshComponent->SetWorldRotation(MeshRotation);

    //Section Index - 어떤 Section부터 시작하는가?, Vertices - 어떤 vertex를 사용하는가?
    //Indices - 어떤 삼각형 구조를 사용하는가?, Normals, UV, Colors, Tangents, bCreateCollision - 충돌 활성화
    ProcMeshComponent->CreateMeshSection(0, FilteredVerticesArray, Indices, Normals, UV, VertexColors, Tangents, true);

    //위에선 LOD Section별로 Vertex를 가져와서 모두 처리했지만 여기서는 GetMaterial(0)로 0번째만 Material을 가져와서 적용함. 즉, 0번째 Material만 적용됨.
    //더 적용하기 위해선 수정 필요.
    UMaterialInterface* SkeletalMeshMaterial = GetMesh()->GetMaterial(0);
    if (SkeletalMeshMaterial){
        ProcMeshComponent->SetMaterial(0, SkeletalMeshMaterial);
        //UE_LOG(LogTemp, Display, TEXT("Applied material from SkeletalMesh to ProceduralMesh."));
    } else UE_LOG(LogTemp, Warning, TEXT("SkeletalMesh has no material assigned."));
}

 

 

결과 확인

수치로 한번 살펴보자 마네킹을 100개로 늘린 상태이다.

그냥 GameThread에서 BeginPlay에 했을 경우다.

왼쪽 아래에 있는 그래프에서 초록색이 Fream이고 붉은색이 GameThread 그래프이다. 

이 결과는 첫 포스트로 적은 글 (Skeletal Mesh 모든 vertex 수집)의 결과이다.

이 결과는 vertex를 선별해서 수집하는 결과이다. 의외로 이게 더 성능적으로 더 걸린다.

Dist 계산과 Index Buffer를 재구성해야 해서 성능적으로 더 먹는다고 판단된다.

 

절단을 사용하기 위해서 vertex를 선별해서 수집할 것이기 때문에 vertex 수집을 다른 스레드로 보냈을 때의 결과이다.

수치상으로 보나 그래프의 최정점 길이를 보나 현재 상태가 더 성능이 낫다는 것을 확인할 수 있다.

 

 

추가적으로 궁금해서 해본 실험이다.

둘다 Game Thread에서 실행하였다.

모든 Vertex로 Procedural Mesh를 만들었을 때의 결과이다.

이건 vertex를 선별해서 만들었을 때의 경우이다.

Procedrual Mesh를 만드는 것 까지 했을 때는 vertex를 선별해서 만들었을 때 성능이 더 낫다는 것을 확인 가능하다.