Issues using SkeletalMeshMerge to merge meshes inside the editor

Hello everyone,
lately I tried to create a little tool to merge two skeletal meshes inside the editor. Basically I tried to use the skeletal Merge Mesh feature described here ( Working with Modular Characters | Unreal Engine Documentation ) but at editor time, and I tried to add the ability to save the created skeletalMesh as a package.

The code I use is very similar to the one shown in the documentation above. With some addition to make a package and create the skeletal mesh using this package so that it appears in the content browser / could be serialized on disk.

Everything seemed to work perfectly fine. I managed to run this code in the editor and got my skeletal meshes merged correctly. But that’s were the problems begin : The generated SkeletalMesh can’t be loaded by the engine. When I generate the mesh, put it on the map, close the editor and re-open it, it will crash.
The crash happens during the loading/initialization of the new mesh. The engines is complaining because it doesn’t find any LODInfo in the skeletalMesh. From what I see, those LODInfo where present before I close the editor. So I think that the SkeletalMergeMesh feature doesn’t generate all the required info to make the skeletal mesh completely viable.

Here are some screen of the warning and error during the mesh initialization when I re-open the editor :

I also noticed that the generated mesh doesn’t have anything in the Sections/ tab in LOD 0 :

Does anyone has any clue on what’s going on here ? I don’t know if it’s a “bug” or if it’s by design. Could be by design since this skeletal mesh merger is designed to merge meshes at runtime.
Do you know what this “Sections” section related about ? Is it the “TArray<FSkelMeshRenderSection> RenderSections;” in FSkeletalMeshLODRenderData ? If so, I’m pretty sure the Mesh merger is generating those. So I don’t know why they doesn’t appear in the editor.
It’s kind of frustrating to see the mesh working as intented but not being able to serialize/deserialize it properly so it remains “inaccessible” ^^.

Have a great day.

Ok I think I have my answer. The skeletalMesh needs its “ImportedModel” in order to be loaded correctly. And the SkeletalMeshMerger doesn’t generate those data. It only generate the MeshRenderData needed at runtime. In fact, those MeshRenderData are re-générated in the editor when the mesh is loaded. That make sense.

But that make the MeshRenderData useless for what I want to achieve… I need a way to directly generate the ImportMesh data.

I managed to have something which work. I’ll put the code here since it could be useful for others. Use it at your own risk since it has not been thoroughly tested and it’s mostly a lot of fixes and patchworks added on top of the code provided in the documentation.
Basically I tried to create the ImportMesh data back from the generated RenderedData and the input meshes to merge. It seems to work fine for my use case which concerns simple skeletal meshes.

Here is the code :
(Note that the rest of the code is the same as the one in the documentation so I won’t paste it here)



USkeletalMesh* UMeshMergeFunctionLibrary::MergeMeshes(const FSkeletalMeshMergeParams& Params, const FString& PackagePath, const FString& AssetName)
{
    TArray<USkeletalMesh*> MeshesToMergeCopy = Params.MeshesToMerge;
    MeshesToMergeCopy.RemoveAll(](USkeletalMesh* InMesh)
        {
            return InMesh == nullptr;
        });
    if (MeshesToMergeCopy.Num() <= 1)
    {
        UE_LOG(LogTemp, Warning, TEXT("Must provide multiple valid Skeletal Meshes in order to perform a merge."));
        return nullptr;
    }
    EMeshBufferAccess BufferAccess = Params.bNeedsCpuAccess ?
        EMeshBufferAccess::ForceCPUAndGPU :
        EMeshBufferAccess::Default;
    TArray<FSkelMeshMergeSectionMapping> SectionMappings;
    TArray<FSkelMeshMergeUVTransforms> UvTransforms;
    ToMergeParams(Params.MeshSectionMappings, SectionMappings);
    ToMergeParams(Params.UVTransformsPerMesh, UvTransforms);
    bool bRunDuplicateCheck = false;

    IAssetTools& assetToolModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
    FString formatedAssetName;
    FString PackageName = PackagePath + "/" + AssetName;
    assetToolModule.CreateUniqueAssetName(PackageName, "", PackageName, formatedAssetName);
    UPackage* Package = CreatePackage(NULL, *PackageName);
    if (!Package)
        return nullptr;

    Package->FullyLoad();

    USkeletalMesh* BaseMesh = NewObject<USkeletalMesh>(Package, *AssetName, RF_Public | RF_Standalone | RF_MarkAsRootSet);
    if (!BaseMesh)
        return nullptr;

    if (Params.Skeleton && Params.bSkeletonBefore)
    {
        BaseMesh->Skeleton = Params.Skeleton;
        bRunDuplicateCheck = true;
        for (USkeletalMeshSocket* Socket : BaseMesh->GetMeshOnlySocketList())
        {
            if (Socket)
            {
                UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocket: %s"), *(Socket->SocketName.ToString()));
            }
        }
        for (USkeletalMeshSocket* Socket : BaseMesh->Skeleton->Sockets)
        {
            if (Socket)
            {
                UE_LOG(LogTemp, Warning, TEXT("SkelSocket: %s"), *(Socket->SocketName.ToString()));
            }
        }
    }
    FSkeletalMeshMerge Merger(BaseMesh, MeshesToMergeCopy, SectionMappings, Params.StripTopLODS, BufferAccess, UvTransforms.GetData());
    if (!Merger.DoMerge())
    {
        UE_LOG(LogTemp, Warning, TEXT("Merge failed!"));
        return nullptr;
    }
    if (Params.Skeleton && !Params.bSkeletonBefore)
    {
        BaseMesh->Skeleton = Params.Skeleton;
    }
    if (bRunDuplicateCheck)
    {
        TArray<FName> SkelMeshSockets;
        TArray<FName> SkelSockets;
        for (USkeletalMeshSocket* Socket : BaseMesh->GetMeshOnlySocketList())
        {
            if (Socket)
            {
                SkelMeshSockets.Add(Socket->GetFName());
                UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocket: %s"), *(Socket->SocketName.ToString()));
            }
        }
        for (USkeletalMeshSocket* Socket : BaseMesh->Skeleton->Sockets)
        {
            if (Socket)
            {
                SkelSockets.Add(Socket->GetFName());
                UE_LOG(LogTemp, Warning, TEXT("SkelSocket: %s"), *(Socket->SocketName.ToString()));
            }
        }
        TSet<FName> UniqueSkelMeshSockets;
        TSet<FName> UniqueSkelSockets;
        UniqueSkelMeshSockets.Append(SkelMeshSockets);
        UniqueSkelSockets.Append(SkelSockets);
        int32 Total = SkelSockets.Num() + SkelMeshSockets.Num();
        int32 UniqueTotal = UniqueSkelMeshSockets.Num() + UniqueSkelSockets.Num();
        UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocketCount: %d | SkelSocketCount: %d | Combined: %d"), SkelMeshSockets.Num(), SkelSockets.Num(), Total);
        UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocketCount: %d | SkelSocketCount: %d | Combined: %d"), UniqueSkelMeshSockets.Num(), UniqueSkelSockets.Num(), UniqueTotal);
        UE_LOG(LogTemp, Warning, TEXT("Found Duplicates: %s"), *((Total != UniqueTotal) ? FString("True") : FString("False")));
    }

    // Generate the imported model data (or try to...)
    FSkeletalMeshModel* skMeshModel = BaseMesh->GetImportedModel();
    {
        FSkeletalMeshRenderData* renderData = BaseMesh->GetResourceForRendering();
        for (int32 lodIdx = 0; lodIdx < renderData->LODRenderData.Num(); lodIdx++)
        {
            FSkeletalMeshLODModel* skMeshLODModel = new FSkeletalMeshLODModel();
            {
                FSkeletalMeshLODRenderData* mainLODRenderData = &renderData->LODRenderData[lodIdx];

                TArray<uint32> indexBuffer;
                indexBuffer.Reserve(mainLODRenderData->MultiSizeIndexContainer.GetIndexBuffer()->Num());
                for (int32 i = 0; i < mainLODRenderData->MultiSizeIndexContainer.GetIndexBuffer()->Num(); i++)
                {
                    indexBuffer.Add(mainLODRenderData->MultiSizeIndexContainer.GetIndexBuffer()->Get(i));
                }

                TArray<FSkelMeshSection> sections;
                sections.Reserve(mainLODRenderData->RenderSections.Num());
                for (int32 i = 0; i < mainLODRenderData->RenderSections.Num(); i++)
                {
                    const FSkelMeshRenderSection& renderSection = mainLODRenderData->RenderSections*;
                    FSkelMeshSection section;
                    section.BaseIndex = renderSection.BaseIndex;
                    section.BaseVertexIndex = renderSection.BaseVertexIndex;
                    section.bCastShadow = renderSection.bCastShadow;
                    section.bDisabled = renderSection.bDisabled;
                    section.BoneMap = renderSection.BoneMap;
                    section.bRecomputeTangent = renderSection.bRecomputeTangent;
                    section.bSelected = false;
                    section.ClothingData = renderSection.ClothingData;
                    section.ClothMappingData = renderSection.ClothMappingData;
                    section.CorrespondClothAssetIndex = renderSection.CorrespondClothAssetIndex;
                    section.GenerateUpToLodIndex = -1;
                    section.MaterialIndex = renderSection.MaterialIndex;
                    section.MaxBoneInfluences = renderSection.MaxBoneInfluences;
                    section.NumTriangles = renderSection.NumTriangles;
                    section.NumVertices = renderSection.NumVertices;
                    // section.OverlappingVertices;
                    {
                        int32 firstVertexIdx = 0;
                        TArray<FSoftSkinVertex> vertices;
                        for (auto& meshToMerge : MeshesToMergeCopy)
                        {
                            TArray<FSoftSkinVertex> tmpVertices;
                            meshToMerge->GetImportedModel()->LODModels[lodIdx].GetVertices(tmpVertices);

                            int32 curVertexIdx = 0;
                            for (FSoftSkinVertex& tmpVertex : tmpVertices)
                            {
                                const bool hasExtraBoneInfluence = mainLODRenderData->GetSkinWeightVertexBuffer()->HasExtraBoneInfluences();
                                if (hasExtraBoneInfluence)
                                {
                                    const TSkinWeightInfo<true>* SrcSkinWeights = mainLODRenderData->GetSkinWeightVertexBuffer()->GetSkinWeightPtr<true>(curVertexIdx + firstVertexIdx);
                                    // if source doesn't have extra influence, we have to clear the buffer
                                    FMemory::Memzero(tmpVertex.InfluenceBones);
                                    FMemory::Memzero(tmpVertex.InfluenceWeights);
                                    FMemory::Memcpy(tmpVertex.InfluenceBones, SrcSkinWeights->InfluenceBones, sizeof(SrcSkinWeights->InfluenceBones));
                                    FMemory::Memcpy(tmpVertex.InfluenceWeights, SrcSkinWeights->InfluenceWeights, sizeof(SrcSkinWeights->InfluenceWeights));
                                }
                                else
                                {
                                    const TSkinWeightInfo<false>* SrcSkinWeights = mainLODRenderData->GetSkinWeightVertexBuffer()->GetSkinWeightPtr<false>(curVertexIdx + firstVertexIdx);
                                    // if source doesn't have extra influence, we have to clear the buffer
                                    FMemory::Memzero(tmpVertex.InfluenceBones);
                                    FMemory::Memzero(tmpVertex.InfluenceWeights);
                                    FMemory::Memcpy(tmpVertex.InfluenceBones, SrcSkinWeights->InfluenceBones, sizeof(SrcSkinWeights->InfluenceBones));
                                    FMemory::Memcpy(tmpVertex.InfluenceWeights, SrcSkinWeights->InfluenceWeights, sizeof(SrcSkinWeights->InfluenceWeights));
                                }

                                curVertexIdx++;
                            }
                            vertices.Append(tmpVertices);
                            firstVertexIdx += tmpVertices.Num();
                        }
                        section.SoftVertices = vertices;
                    }


                    sections.Add(section);
                }

                //------------------------------------
                skMeshLODModel->ActiveBoneIndices = mainLODRenderData->ActiveBoneIndices;
                skMeshLODModel->IndexBuffer = indexBuffer;
                skMeshLODModel->MaxImportVertex = mainLODRenderData->GetNumVertices();
                //skMeshLODModel->MeshToImportVertexMap = ;
                skMeshLODModel->NumTexCoords = mainLODRenderData->GetNumTexCoords();
                skMeshLODModel->NumVertices = mainLODRenderData->GetNumVertices();
                //skMeshLODModel->RawPointIndices = ;
                //skMeshLODModel->RawSkeletalMeshBulkData = ;
                skMeshLODModel->RequiredBones = mainLODRenderData->RequiredBones;
                skMeshLODModel->Sections = sections;

                TMap<FName, FImportedSkinWeightProfileData> mergedImportedSkinWeightProfile;
                for (auto& meshToMerge : MeshesToMergeCopy)
                {
                    mergedImportedSkinWeightProfile.Append(meshToMerge->GetImportedModel()->LODModels[lodIdx].SkinWeightProfiles);
                }
                for (const FSkinWeightProfileInfo& profile : BaseMesh->GetSkinWeightProfiles())
                {
                    skMeshLODModel->SkinWeightProfiles.Add(profile.Name, *mergedImportedSkinWeightProfile.Find(profile.Name));
                }

            }
            skMeshModel->LODModels.Add(skMeshLODModel);
        }
    }
    //--------------------------------------------------------------

    // register the package
    Package->MarkPackageDirty();
    FAssetRegistryModule::AssetCreated(BaseMesh);
    //--------------------------------------------------------------

    // Really dirty fix
    UProperty* prop = FindFieldChecked<UProperty>(USkeletalMesh::StaticClass(), GET_MEMBER_NAME_CHECKED(USkeletalMesh, Skeleton));
    FPropertyChangedEvent propChange(prop);
    BaseMesh->PostEditChangeProperty(propChange);
    //--------------------------------------------------------------

    return BaseMesh;
}



Where/how do you invoke this function in the editor?

@JSwigart I put them in a Blueprint function library and I use them in the Editor with Blutility.

Perhaps you need to enable bAllowCPUAccess on your meshes? (it’s in the asset details) Otherwise it won’t be kept in RAM after being loaded to the GPU.

@wolflow94 Was looking to do a similar thing - did it end up working ? Going to try tonight - thanks for posting

Hi!

Just wanted to share what we now use for this purpose. It’s based on your code,@wolflow94 (thanks for sharing, btw!), but has some additional changes that we found necessary to get it working for our use case. Also, updated to 4.25, in which the SkinWeightVertexBuffer has been changed.

This code also needs a slight modification to the SkinWeightVertexBuffer in-engine (or duplication of engine code…), because the FSkinWeightVertexBuffer::GetVertexSkinWeights() function is not DLL-exported and therefore the attached code won’t link without adding ENGINE_API to that function’s declaration.

There are some things where I’m not confident if they are really working properly, mostly becaus FSKeletalMeshMerge does some strange things: OverlappingVertices and MaxBoneinfluences. There are comments in the attached files regarding those issues, and tips how to fix OverlappingVertices in-engine if one needs them.

Best Regards!

Just noticed an issue in the code I pasted:
The newly created assets are all marked as RF_MarkAsRootSet, that’s probably not desired behaviour. So keep that in mind when downloading the above code…

hi i wanna use your code for my fskeletalmeshmerge implementation.wanna know what are the pros and cons of this implementation?what can be improve?im planning to use this code for modular character creation similiar to wow and black desert online

if you have discord,please message me on KhaiSaki#1061

als

also im trying to compile the code and i get error on the SkinWeightVertexBuffer…u said its duplicating the code from engine code…how can i fix it?you said i need to put ENGINE_API but do i need to put it in the engine code or in the meshmergelibrary?can show the fix for it?

Has anyone seen issues with materials being scrambled on the model when writing out a merged mesh to disk? If I just use the merged mesh at runtime, there’s no issues.

I have separate head, torso, leg, feet models that I’m mixing and matching in a BP to write out as a single mesh, and the mesh seems to be visually correct, except that the materials seem swapped around ( the material for the feet are on the head, etc ).

Any ideas?

Sorry, I wasn’t following the thread and missed a few questions…

First things first: The GetVertexSkinWeights(uint32 VertexIndex) change was literally writing ENGINE_API in front of the function declaration in SkinWeightVertexBuffer.h:
ENGINE_API FSkinWeightInfo GetVertexSkinWeights(uint32 VertexIndex) const;

The pros and cons…
Pros: It works as it should. You set the merge parameters in-editor, run it, and get a skeletal mesh asset out.
Cons: If you use OverlappingVertices and MaxBoneInlfluences you’ll need to implement support for those first.

As my files seem to have disappeared, so here they are again. This is our latest version of the code, meant to be used with 4.26.2.
MeshMergeSources.7z (4.7 KB)

1 Like

i followed this but GetVertexSkinWeights still has _stdcel error
Even i compiled from source engine and fixed macro