Has anyone actually got USD to work at runtime on a packaged build!

Please, I really need help on this for a crucial project!

I am trying to get a USD animation to run from a packaged project, but I’ve spent more than a couple of weeks with no real success! Has anyone actually got USD to load and play correctly without crashes or errors? I have experimented with Unreal engine 5.3 and 5.4.

On Unreal 5.3 and 5.4, this setup runs in testing, but unfortunately does not work in runtime build.
There has been years of development into supporting USD in Unreal Engine, but I am surprised that it does not actually work in practice when the project is built at Runtime!! By default, USD seems to be disabled for runtime (packaged game build). From reading available docs and forum posts, it seems that the following convoluted route is needed – but it still ends up with fatal errors on runtime in 5.3 and totally not working in 5.4.

Unreal 5.4 - Does not complete packaging build. Many errors in the build output log.
Unreal 5.3 – The USD is visible at runtime momentarily, but a fatal crash occurs instantly.
Unreal 5.2 - Unknown (still working on making the build from sourcecode)

I referred to these forum posts and help pages:

This is my process:

  • Build Unreal engine from source (clone repository from Github, build in Visual studio 2022, make new c++ game project with the newly built windows binary, load project from within Visual studio, add USD plugin, close project.
  • In Visual Studio, add new line of code to the project’s Project.Target.cs :
  
GlobalDefinitions.Add("FORCE_ANSI_ALLOCATOR=1");
  • Run the Unreal project, and add the USD plugin.

  • Create USD stage, and populate it with a USD.

  • In the level blueprint, on BeginPlay, use ‘Set Root Layer’ node (includes the USD’s local path, manually copied into the field) and connect in the USD stage actor into the ‘Set Root Layer’ node.
    (link to Epic help page)

  • Run a packaging build for windows.

Any help or advice on this would be appreciated!

Hi,
I also didn´t manage to get it working in a packaged project yet.

But if you check the Build.cs file of the USDWrapper here:
https://github.com/EpicGames/UnrealEngine/blob/release/Engine/Plugins/Importers/USDImporter/Source/UnrealUSDWrapper/UnrealUSDWrapper.Build.cs

at the very bottom, a comment describes that you need these two lines in your project.target.cs

GlobalDefinitions.Add(“FORCE_ANSI_ALLOCATOR=1”);
GlobalDefinitions.Add(“UE_USE_MALLOC_FILL_BYTES=0”);

Hi @BMWDominic ,
Thanks for your message.

Yes the two lines are definitely needed to be added to the project.target.cs for the USD packaging.

This is interesting - Unreal Engine 5.3 and 5.4 still crashes when the packaged game is run (with a fatal error). But it works in compiled versions of the latest UE5-main build (from github) which is essentially an early 5.5. This is very promising!

If anyone understands why there’s a fatal error on packaged 5.3 and 5.4 runtime exe’s that contain USD, please get in touch!

Hi there,

I have the same issue, but for another plugin.

Essentially I also have to add the two lines to my Project.Target.cs:

GlobalDefinitions.Add(“FORCE_ANSI_ALLOCATOR=1”);
GlobalDefinitions.Add(“UE_USE_MALLOC_FILL_BYTES=0”);

In my case, when I add those lines I can’t package shipping builds. It works with ‘Development’ or ‘DebugGame’ builds (specifically for iOS) without those lines. The error that pops-up, when those lines are added looks like this on my end:


UATHelper: Packaging (IOS): ld: warning: Could not find or use auto-linked framework 'CoreAudioTypes'
PackagingResults: Warning: Could not find or use auto-linked framework 'CoreAudioTypes'
UATHelper: Packaging (IOS): Undefined symbols for architecture arm64:
UATHelper: Packaging (IOS): "StdRealloc(void*, unsigned long, unsigned long)", referenced from:
UATHelper: Packaging (IOS): void Eigen::internal::minimum_degree_ordering<float, int>(Eigen::SparseMatrix<float, 0, int>&, Eigen::PermutationMatrix<-1, -1, int>&) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): void Eigen::internal::minimum_degree_ordering<double, int>(Eigen::SparseMatrix<double, 0, int>&, Eigen::PermutationMatrix<-1, -1, int>&) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): Eigen::internal::conservative_resize_like_impl<Eigen::Matrix<double, -1, 1, 0, -1, 1>, Eigen::Matrix<double, -1, 1, 0, -1, 1>, false>::run(Eigen::DenseBase<Eigen::Matrix<double, -1, 1, 0, -1, 1>>&, long, long) in Module.DynamicMesh.3.cpp.o
UATHelper: Packaging (IOS): "StdFree(void*)", referenced from:
UATHelper: Packaging (IOS): Chaos::TBlockSparseSymmetricLinearSystem<float, 3>::TBlockSparseSymmetricLinearSystem() in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): Chaos::TBlockSparseSymmetricLinearSystem<float, 3>::FPimpl::Reset(int) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): bool Chaos::TBlockSparseSymmetricLinearSystem<float, 3>::FPimpl::Solve<Eigen::DiagonalPreconditioner<float>>(TArrayView<Chaos::TVector<float, 3> const, int> const&, TArrayView<Chaos::TVector<float, 3>, int> const&, int, float, bool, int*, float*) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): void UE::Core::Private::PimplPtr::DeleterFunc<Chaos::TBlockSparseSymmetricLinearSystem<float, 3>::FPimpl>(void*) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): Eigen::ConjugateGradient<Eigen::SparseMatrix<float, 1, int>, 3, Eigen::DiagonalPreconditioner<float>>& Eigen::IterativeSolverBase<Eigen::ConjugateGradient<Eigen::SparseMatrix<float, 1, int>, 3, Eigen::DiagonalPreconditioner<float>>>::compute<Eigen::SparseMatrix<float, 1, int>>(Eigen::EigenBase<Eigen::SparseMatrix<float, 1, int>> const&) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS):       Eigen::IterativeSolverBase<Eigen::ConjugateGradient<Eigen::SparseMatrix<float, 1, int>, 3, Eigen::DiagonalPreconditioner<float>>>::IterativeSolverBase() in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): Eigen::DiagonalPreconditioner<float>& Eigen::DiagonalPreconditioner<float>::factorize<Eigen::Ref<Eigen::SparseMatrix<float, 1, int> const, 0, Eigen::OuterStride<-1>>>(Eigen::Ref<Eigen::SparseMatrix<float, 1, int> const, 0, Eigen::OuterStride<-1>> const&) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): ...
UATHelper: Packaging (IOS):  "StdMalloc(unsigned long, unsigned long)", referenced from:
UATHelper: Packaging (IOS): Chaos::TBlockSparseSymmetricLinearSystem<float, 3>::TBlockSparseSymmetricLinearSystem() in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS):  Chaos::TBlockSparseSymmetricLinearSystem<float, 3>::FPimpl::Reset(int) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS):       Eigen::IterativeSolverBase<Eigen::ConjugateGradient<Eigen::SparseMatrix<float, 1, int>, 3, Eigen::DiagonalPreconditioner<float>>>::IterativeSolverBase() in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): Eigen::DiagonalPreconditioner<float>& Eigen::DiagonalPreconditioner<float>::factorize<Eigen::Ref<Eigen::SparseMatrix<float, 1, int> const, 0, Eigen::OuterStride<-1>>>(Eigen::Ref<Eigen::SparseMatrix<float, 1, int> const, 0, Eigen::OuterStride<-1>> const&) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): void Eigen::internal::conjugate_gradient<Eigen::Ref<Eigen::SparseMatrix<float, 1, int> const, 0, Eigen::OuterStride<-1>>, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -1, 1> const, 0, Eigen::Stride<0, 0>>, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0>>, Eigen::DiagonalPreconditioner<float>>(Eigen::Ref<Eigen::SparseMatrix<float, 1, int> const, 0, Eigen::OuterStride<-1>> const&, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -1, 1> const, 0, Eigen::Stride<0, 0>> const&, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0>>&
, Eigen::DiagonalPreconditioner<float> const&, long&, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0>>::RealScalar&) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): void Eigen::internal::assignment_from_xpr_op_product<Eigen::Matrix<float, -1, 1, 0, -1, 1>, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -1, 1> const, 0, Eigen::Stride<0, 0>>, Eigen::Product<Eigen::Ref<Eigen::SparseMatrix<float, 1, int> const, 0, Eigen::OuterStride<-1>>, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0>>, 0>, Eigen::internal::assign_op<float, float>, Eigen::internal::sub_assign_op<float, float>>::run<Eigen::CwiseBinaryOp<Eigen::internal::scalar_difference_op<float, float>, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -1, 1> co
nst, 0, Eigen::Stride<0, 0>> const, Eigen::Product<Eigen::Ref<Eigen::SparseMatrix<float, 1, int> const, 0, Eigen::OuterStride<-1>>, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0>>, 0> const>, Eigen::internal::assign_op<float, float>>(Eigen::Matrix<float, -1, 1, 0, -1, 1>&, Eigen::CwiseBinaryOp<Eigen::internal::scalar_difference_op<float, float>, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -1, 1> const, 0, Eigen::Stride<0, 0>> const, Eigen::Product<Eigen::Ref<Eigen::SparseMatrix<float, 1, int> const, 0, Eigen::OuterStride<-1>>, Eigen::Map<Eigen::Matrix<float, -1, 1, 0, -
1, 1>, 0, Eigen::Stride<0, 0>>, 0> const> const&, Eigen::internal::assign_op<float, float> const&) in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): void Eigen::DiagonalPreconditioner<float>::_solve_impl<Eigen::Matrix<float, -1, 1, 0, -1, 1>, Eigen::Matrix<float, -1, 1, 0, -1, 1>>(Eigen::Matrix<float, -1, 1, 0, -1, 1> const&, Eigen::Matrix<float, -1, 1, 0, -1, 1>&) const in Module.Chaos.11.cpp.o
UATHelper: Packaging (IOS): ...
UATHelper: Packaging (IOS): ld: symbol(s) not found for architecture arm64
UATHelper: Packaging (IOS): clang: error: linker command failed with exit code 1 (use -v to see invocation)

Based on an answer I received on Unreal Slackers Discord there seems to be a fix available if you have UDN access, or it will probably ship with 5.5:

Do you have P4 access? I believe this fixed by P4 CL 31807575 (JIRA UE-207636), which will be available in UE5.5.

Hi @Thoeme,

Thanks for your reply!

Generally, my latest update on this is that all versions of UE 5.3 and 5.4 crash in one way or another with USD at packaging time. In 5.4 I had the USD show up briefly in the packaged build (if it didn’t crash during packaging), but I still repeatedly get the fatal error when the game starts playing.

Building UE 5.5 (ue5-main) from the source code is generally working, but there is the significant problem that the UsdActor stage keeps on unloading everytime the game is simulated (makes development testing very difficult). I just hope the developers fix this before the UE 5.5 release!

At the moment, we have paused development with USD because it unfortunately doesn’t seem to be properly supported yet for packaging (we can’t rely on it for live projects for clients).

2 Likes

Do you by chance have any updates on the use of USD? I’m trying to decide if USD is sufficiently mature in UE that I can move my methods to that. My target applications span the range of editor, runtime, and packaged (windows and android/quest).

Is 5.5+ working now or do the issues brought up in this discussion remain?

Any news? Is building the engine from source required? I am using UE 5.6.1 (official)

In Unreal Engine 5.5 and 5.6.0 the USD packaging worked, but it seems to be broken in any version after 5.6.1 (inclusive). Getting USD’s to package was hard, but I had it working (changing the target.cs file and set root layer in the level blueprint).

Now in versions 5.6.1 and 5.7, I cannot get USD packaging to work. I’ve heard that the USD packaging code has changed, but I cannot find any details about it. Apparently USD packaging export is better supported in versions 5.6.1 onwards, but it seems to now be fully broken. Can anyone help?

I suspect it might be a setting somewhere in the project settings, but I cannot find anything that actually works.

I have tried adding the USD folder to be exported, but this does not work.

Any advice would be appreciated!

@christianstamati On Unreal Engine 5.5 to 5.6.0, the building of UE from source is essential unfortunately!

Btw, ~ @christianstamati On Unreal Engine 5.5 to 5.6.0, the building of UE from source is essential unfortunately! But I suspect that 5.6.1 and later versions have broken USD export. Best to try 5.6!! I’ve read that the newest versions of UE can export USD without building from source, but I haven’t been able to get this to work, after wasting many days!

I hope this time is real! Did you try this in UE 5.7 Preview? Btw you can add me on Discord if you want christianstamati#1497

Hey !

Is it still broken in 5.6.1 and do we still need UE from source or the official releases in the launcher are good ?

I need to to import and export at runtime in shipping build.

The USD plugin does work at runtime with Unreal 5.7, just the same issue that it cannot be packaged and shipped with the plugin.

I wonder if Epic is interested in adapting the plugin to make it packageable in newer releases? Or a way to push development on this?

Looks like there is quite a lot of interest in this feature and huge potential for runtime apps using USD importing and editing.

Hey everyone!
I managed to fix the runtime USD static mesh loading issue! Here’s the solution (generated by Claude.ai because I was too lazy to write it all out myself :sweat_smile:, but I’ve reviewed everything and it accurately describes how I got it working on my setup):

USD Static Mesh Runtime Build Fix for Unreal Engine 5.6.0

Problem Description

When loading USD stages at runtime in Unreal Engine 5.6.0 (source build), static meshes fail to display with the error:

LogUsd: Warning: Discarding StaticMesh generated for prim '/path/to/mesh' as it didn't produce any valid RenderData (likely all triangles were degenerate)
LogStaticMesh: Verbose: ShouldCreateRenderState returned false for InstancedStaticMeshComponent (StaticMesh is null)

Diagnostic logs showed:

  • Valid geometry data (e.g., 1810 vertices, 1114 triangles)
  • Valid bounding boxes (non-zero, non-NaN)
  • LODResources correctly populated
  • GetCurrentFirstLODIdx(0) returns -1 instead of 0

Root Cause

At runtime (!WITH_EDITOR), the BuildFromMeshDescription() function populates vertex and index buffers but does NOT initialize the BuffersSize property.

The GetFirstValidLODIdx() function validates LODs using this check:

while (LODIndex < LODCount && 
       (LODResources[LODIndex].GetNumVertices() == 0 || 
        LODResources[LODIndex].BuffersSize == 0))  // ⬅️ FAILS HERE
{
    ++LODIndex;
}

When BuffersSize == 0, the LOD is considered invalid, causing GetCurrentFirstLODIdx() to return INDEX_NONE (-1), which leads to mesh rejection.

Solution

File to modify:

Engine/Plugins/Importers/USDImporter/Source/USDSchemas/Private/USDGeomMeshTranslator.cpp

Method:

bool UsdGeomMeshTranslatorImpl::BuildStaticMesh(...)

Location: In the runtime build path (#else block / !WITH_EDITOR), in the Post-LOD Processing section (after the LOD processing loop).

Code to add:

// === Post-LOD Processing ===
UE_LOG(LogUsd, Warning, TEXT("=== Post-LOD Processing ==="));
FStaticMeshRenderData* RenderData = StaticMesh.GetRenderData();

if (!RenderData)
{
    UE_LOG(LogUsd, Error, TEXT("ERROR: RenderData is NULL!"));
}
else
{
    UE_LOG(LogUsd, Warning, TEXT("RenderData exists, LODResources.Num: %d"), RenderData->LODResources.Num());
    
    if (RenderData->LODResources.Num() > 0)
    {
        // Calculate BuffersSize manually for each LOD
        for (int32 LODIndex = 0; LODIndex < RenderData->LODResources.Num(); ++LODIndex)
        {
            FStaticMeshLODResources& LODRes = RenderData->LODResources[LODIndex];
            
            if (LODRes.BuffersSize == 0 && LODRes.GetNumVertices() > 0)
            {
                LODRes.BuffersSize = 0;
                
                // Position buffer (X,Y,Z coordinates)
                LODRes.BuffersSize += LODRes.VertexBuffers.PositionVertexBuffer.GetNumVertices() 
                                    * LODRes.VertexBuffers.PositionVertexBuffer.GetStride();
                
                // Static mesh vertex buffer (normals, tangents, UVs)
                LODRes.BuffersSize += LODRes.VertexBuffers.StaticMeshVertexBuffer.GetResourceSize();
                
                // Color buffer (if present)
                if (LODRes.VertexBuffers.ColorVertexBuffer.GetNumVertices() > 0)
                {
                    LODRes.BuffersSize += LODRes.VertexBuffers.ColorVertexBuffer.GetAllocatedSize();
                }
                
                // Index buffer (triangles)
                LODRes.BuffersSize += LODRes.IndexBuffer.GetAllocatedSize();
                
                UE_LOG(LogUsd, Warning, TEXT("  - LOD%d BuffersSize calculated: %d bytes"), LODIndex, LODRes.BuffersSize);
            }
        }
        
        // Force CurrentFirstLODIdx to 0
        RenderData->CurrentFirstLODIdx = 0;
        
        UE_LOG(LogUsd, Warning, TEXT("GetCurrentFirstLODIdx(0) after fix: %d"), RenderData->GetCurrentFirstLODIdx(0));
    }
}
UE_LOG(LogUsd, Warning, TEXT("=== End Post-LOD Processing ==="));

In editor builds, StaticMesh.Build() calculates BuffersSize automatically. At runtime, BuildFromMeshDescription() populates the buffers but leaves BuffersSize = 0, causing validation to fail.

If you need, here is the complete method BuildStaticMesh:

	bool BuildStaticMesh(UStaticMesh& StaticMesh, const FStaticFeatureLevel& FeatureLevel, TArray<FMeshDescription>& LODIndexToMeshDescription)
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(UsdGeomMeshTranslatorImpl::BuildStaticMesh);

		if (LODIndexToMeshDescription.Num() == 0)
		{
			return false;
		}
		
		UE_LOG(LogUsd, Warning, TEXT("BuildStaticMesh: Building mesh with %d LODs"), LODIndexToMeshDescription.Num());

#if WITH_EDITOR
		UE_LOG(LogUsd, Warning, TEXT("BuildStaticMesh: Editor build path"));
		
		ITargetPlatformManagerModule& TargetPlatformManager = GetTargetPlatformManagerRef();
		ITargetPlatform* RunningPlatform = TargetPlatformManager.GetRunningTargetPlatform();
		check(RunningPlatform);

		const FStaticMeshLODSettings& LODSettings = RunningPlatform->GetStaticMeshLODSettings();
		StaticMesh.GetRenderData()->Cache(RunningPlatform, &StaticMesh, LODSettings);
#else
		UE_LOG(LogUsd, Warning, TEXT("BuildStaticMesh: Runtime build path"));
		
		StaticMesh.GetRenderData()->AllocateLODResources(LODIndexToMeshDescription.Num());

		// Build render data from each mesh description
		for (int32 LODIndex = 0; LODIndex < LODIndexToMeshDescription.Num(); ++LODIndex)
		{
			UE_LOG(LogUsd, Warning, TEXT("=== START Processing LOD%d ==="), LODIndex);
			
			FStaticMeshLODResources& LODResources = StaticMesh.GetRenderData()->LODResources[LODIndex];

			FMeshDescription& MeshDescription = LODIndexToMeshDescription[LODIndex];
			
			UE_LOG(LogUsd, Warning, TEXT("BuildStaticMesh LOD%d: %d vertices, %d triangles"), 
				LODIndex, 
				MeshDescription.Vertices().Num(), 
				MeshDescription.Triangles().Num()
			);
			
			TVertexInstanceAttributesConstRef<FVector4f>
				MeshDescriptionColors = MeshDescription.VertexInstanceAttributes().GetAttributesRef<FVector4f>(MeshAttribute::VertexInstance::Color);

			// Compute normals here if necessary because they're not going to be computed via the regular static mesh build pipeline at runtime
			// (i.e. StaticMeshBuilder is not available at runtime)
			// We need polygon info because ComputeTangentsAndNormals uses it to repair the invalid vertex normals/tangents
			// Can't calculate just the required polygons as ComputeTangentsAndNormals is parallel and we can't guarantee thread-safe access patterns
			UE_LOG(LogUsd, Warning, TEXT("  - Computing normals and tangents..."));
			FStaticMeshOperations::ComputeTriangleTangentsAndNormals(MeshDescription);
			FStaticMeshOperations::ComputeTangentsAndNormals(MeshDescription, EComputeNTBsFlags::UseMikkTSpace);
			UE_LOG(LogUsd, Warning, TEXT("  - Normals and tangents computed"));

			// Manually set this as it seems the UStaticMesh only sets this whenever the mesh is serialized, which we won't do
			LODResources.bHasColorVertexData = MeshDescriptionColors.GetNumElements() > 0;
			UE_LOG(LogUsd, Warning, TEXT("  - bHasColorVertexData: %d"), LODResources.bHasColorVertexData);

			UE_LOG(LogUsd, Warning, TEXT("  - Calling BuildFromMeshDescription..."));
			StaticMesh.BuildFromMeshDescription(MeshDescription, LODResources);
			UE_LOG(LogUsd, Warning, TEXT("  - BuildFromMeshDescription completed"));
			
			UE_LOG(LogUsd, Error, TEXT("After BuildFromMeshDescription LOD%d:"), LODIndex);
			UE_LOG(LogUsd, Error, TEXT("  - NumVertices: %d"), LODResources.GetNumVertices());
			UE_LOG(LogUsd, Error, TEXT("  - NumTriangles: %d"), LODResources.GetNumTriangles());
			UE_LOG(LogUsd, Error, TEXT("  - IndexBuffer.Num: %d"), LODResources.IndexBuffer.GetNumIndices());
			
			if (LODResources.GetNumVertices() > 0)
			{
				UE_LOG(LogUsd, Warning, TEXT("  - Computing bounding box manually..."));
				FBox BoundingBox(ForceInit);
				const FPositionVertexBuffer& PositionVertexBuffer = LODResources.VertexBuffers.PositionVertexBuffer;
	
				for (uint32 VertexIndex = 0; VertexIndex < PositionVertexBuffer.GetNumVertices(); ++VertexIndex)
				{
					BoundingBox += FVector(PositionVertexBuffer.VertexPosition(VertexIndex));
				}
	
				if (BoundingBox.IsValid)
				{
					StaticMesh.GetRenderData()->Bounds = FBoxSphereBounds(BoundingBox);
					UE_LOG(LogUsd, Warning, TEXT("  - Manually computed bounds: %s"), *BoundingBox.ToString());
					UE_LOG(LogUsd, Warning, TEXT("  - Bounds center: %s"), *BoundingBox.GetCenter().ToString());
					UE_LOG(LogUsd, Warning, TEXT("  - Bounds extent: %s"), *BoundingBox.GetExtent().ToString());
				}
				else
				{
					UE_LOG(LogUsd, Error, TEXT("  - ERROR: Computed bounding box is INVALID!"));
				}
			}
			else
			{
				UE_LOG(LogUsd, Error, TEXT("  - ERROR: LODResources has 0 vertices, skipping bounds computation"));
			}
			
			UE_LOG(LogUsd, Warning, TEXT("=== END Processing LOD%d ==="), LODIndex);
		}
		
		UE_LOG(LogUsd, Warning, TEXT("=== Post-LOD Processing ==="));
		FStaticMeshRenderData* RenderData = StaticMesh.GetRenderData();

		if (!RenderData)
		{
		    UE_LOG(LogUsd, Error, TEXT("ERROR: RenderData is NULL!"));
		}
		else
		{
		    UE_LOG(LogUsd, Warning, TEXT("RenderData exists, LODResources.Num: %d"), RenderData->LODResources.Num());
		    UE_LOG(LogUsd, Warning, TEXT("CurrentFirstLODIdx BEFORE fix: %d"), RenderData->CurrentFirstLODIdx);
		    UE_LOG(LogUsd, Warning, TEXT("Bounds BEFORE fix: %s"), *RenderData->Bounds.ToString());
		    
		    if (RenderData->LODResources.Num() > 0)
		    {
		        // === FIX CRITIQUE : Calculer BuffersSize ===
		        for (int32 LODIndex = 0; LODIndex < RenderData->LODResources.Num(); ++LODIndex)
		        {
		            FStaticMeshLODResources& LODRes = RenderData->LODResources[LODIndex];
		            
		            UE_LOG(LogUsd, Warning, TEXT("  - LOD%d BuffersSize BEFORE: %d"), LODIndex, LODRes.BuffersSize);
		            
		            if (LODRes.BuffersSize == 0 && LODRes.GetNumVertices() > 0)
		            {
		                // Calculer la taille des buffers manuellement
		                LODRes.BuffersSize = 0;
		                
		                // Position buffer
		                LODRes.BuffersSize += LODRes.VertexBuffers.PositionVertexBuffer.GetNumVertices() * LODRes.VertexBuffers.PositionVertexBuffer.GetStride();
		                
		                // Static mesh vertex buffer
		                LODRes.BuffersSize += LODRes.VertexBuffers.StaticMeshVertexBuffer.GetResourceSize();
		                
		                // Color buffer (si présent)
		                if (LODRes.VertexBuffers.ColorVertexBuffer.GetNumVertices() > 0)
		                {
		                    LODRes.BuffersSize += LODRes.VertexBuffers.ColorVertexBuffer.GetAllocatedSize();
		                }
		                
		                // Index buffer
		                LODRes.BuffersSize += LODRes.IndexBuffer.GetAllocatedSize();
		                
		                UE_LOG(LogUsd, Warning, TEXT("  - LOD%d BuffersSize AFTER calculation: %d"), LODIndex, LODRes.BuffersSize);
		            }
		        }
		        
		        // Force CurrentFirstLODIdx à 0
		        RenderData->CurrentFirstLODIdx = 0;
		        
		        UE_LOG(LogUsd, Warning, TEXT("CurrentFirstLODIdx AFTER fix: %d"), RenderData->CurrentFirstLODIdx);
		        UE_LOG(LogUsd, Warning, TEXT("GetCurrentFirstLODIdx(0) AFTER fix: %d"), RenderData->GetCurrentFirstLODIdx(0));
		        UE_LOG(LogUsd, Warning, TEXT("BuildStaticMesh: Fixed CurrentFirstLODIdx and BuffersSize"));
		    }
		}
		UE_LOG(LogUsd, Warning, TEXT("=== End Post-LOD Processing ==="));

#if RHI_RAYTRACING
		if (IsRayTracingAllowed() && StaticMesh.bSupportRayTracing)
		{
			StaticMesh.GetRenderData()->InitializeRayTracingRepresentationFromRenderingLODs();
		}
#endif	  // RHI_RAYTRACING
#endif	  // WITH_EDITOR

		return true;
	}

Testing Status

  • Tested on: Unreal Engine 5.6.0 (source build)
  • Not yet tested on: Other UE versions (5.5, 5.4, etc… from source build or launcher)
  • Result: All USD prototype meshes now render correctly at runtime
  • Verified: GetCurrentFirstLODIdx(0) now returns 0 instead of -1

Verification Logs

After applying the fix, you should see:

LogUsd: Warning: === Post-LOD Processing ===
LogUsd: Warning: RenderData exists, LODResources.Num: 1
LogUsd: Warning:   - LOD0 BuffersSize calculated: 245760 bytes
LogUsd: Warning: GetCurrentFirstLODIdx(0) after fix: 0
LogUsd: Warning: === End Post-LOD Processing ===
LogUsd: Stage loaded [stage_name] in [X min Y s]

Instead of the previous errors showing GetCurrentFirstLODIdx(0): -1 and discarded meshes.

Additional Notes

  • This fix only affects the runtime code path (!WITH_EDITOR)
  • The editor path continues to use the standard StaticMesh.Build() workflow
  • No impact on cooked assets or editor-imported USD stages
  • This resolves the issue for procedurally loaded USD stages at runtime

Environment:

  • Unreal Engine: 5.6.0 (Source Build)
  • USD Plugin: Built-in USDImporter
  • Platform: Windows (likely affects all platforms)

Feel free to test this fix and report back if it works on other UE versions!