Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem serializing/deserializing BodyCreationSettings with sRestoreWithChildren #1088

Closed
wirepair opened this issue May 2, 2024 · 2 comments

Comments

@wirepair
Copy link

wirepair commented May 2, 2024

Problem

Hello, I'm trying to debug a strange error when I go to load the serialized BodyCreationSettings data from calling SaveWithChildren. It is very possible I'm doing something wrong here!

What I'm trying to do is load an FBX that contains multiple meshes in a loop and serializing it. Then deserializing them in a loop by calling Result.Get() from the returned object of sRestoreWithChildren(...). Unfortunately, I get random parts of the mesh failing to load, returning an Error reading body creation settings error message. What's super strange is it fails at different times everytime I reload the test.

I did confirm the saved output and loaded output is the same so I must be doing something wrong here!

If it helps I attached the FBX and the serialized output, but I imagine you'll spot what I'm doing wrong in my code :>.

Reproduction steps

  1. Use the attached FBX or Generate a Landscape in UE5, select all the Landscape_Proxy_X_X and Edit -> Export Selected. Export as FBX with Source Mesh.
  2. Add to the JoltPhysics Samples the ufbx project. Drop in the .h and .c file (rename the .c to .cpp and add to the Samples cmake file)
  3. Replace the BoxShapeTest with the following code:
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT

#include <TestFramework.h>

#include <Tests/Shapes/BoxShapeTest.h>
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/CompoundShape.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Layers.h>
#include "ThirdParty/ufbx/ufbx.h"
#include <Jolt/Core/StreamWrapper.h>

#include <fstream>
#include <iostream>
#include <string>

JPH_IMPLEMENT_RTTI_VIRTUAL(BoxShapeTest)
{
	JPH_ADD_BASE_CLASS(BoxShapeTest, Test)
}

void PrintVec3(ofstream &Out, ufbx_vec3 &Vec)
{
	Out << "\tx: " << Vec.x << "\ty: " << Vec.y << "\tz: " << Vec.z;
}
void BoxShapeTest::Initialize()
{
	std::string File = "D:\\Temp\\out.log";
	auto Out = std::ofstream(File, std::ios::out | std::ios::trunc);

	ufbx_load_opts opts = { 0 }; // Optional, pass NULL for defaults
	ufbx_error error; // Optional, pass NULL if you don't care about errors
	ufbx_scene *scene = ufbx_load_file("D:\\TestLevel1.fbx", &opts, &error);
	if (!scene) 
	{
		fprintf(stderr, "Failed to load: %s\n", error.description.data);
		return;
	}

	bool Save = true;
	std::stringstream SaveData;
	std::stringstream LoadData;
	if (Save)
	{
		// Use and inspect `scene`, it's just plain data!
		JPH::VertexList VertexList;
		JPH::IndexedTriangleList IdxTriangleList;
		JPH::TriangleList Triangles;
		// Serialize
		JPH::StreamOutWrapper OutStream(SaveData);
		// Docs say to re-use these across saves.
		JPH::BodyCreationSettings::ShapeToIDMap ShapeToId;
		JPH::BodyCreationSettings::MaterialToIDMap MaterialToId;
		JPH::BodyCreationSettings::GroupFilterToIDMap GroupToId;

		JPH::MeshShape FloorShape;
		// Let's just list all objects within the scene for example:
		for (size_t MeshIdx = 0; MeshIdx < scene->nodes.count; MeshIdx++) 
		{
			ufbx_node *node = scene->nodes.data[MeshIdx];
			if (node->is_root) continue;

			auto Mesh = node->mesh;
			if (!Mesh) 
			{
				continue;
			}
			
			JPH::VertexList VertList;
			JPH::IndexedTriangleList TriList;
			for (auto& Vertex : Mesh->vertices) 
			{
				auto N2W = ufbx_transform_position(&node->node_to_world, Vertex);
				
				auto LocalScale = node->local_transform.scale;
				
				auto Scaled = JPH::Float3(N2W.x / LocalScale.x, N2W.y / LocalScale.y, N2W.z / LocalScale.z);
				VertList.push_back(Scaled);
			}

			for (size_t i = 0; i < Mesh->num_indices; i+=3)
			{
				auto idx0 = Mesh->vertex_indices[i];
				auto idx1 = Mesh->vertex_indices[i + 1];
				auto idx2 = Mesh->vertex_indices[i + 2];

				JPH::IndexedTriangle triangle(idx0, idx1, idx2);

				TriList.push_back(triangle);
			}

			auto Rot = JPH::Quat::sRotation(Vec3::sAxisX(), -.5F * JPH_PI);
			MeshShapeSettings MeshSettings(std::move(VertList), std::move(TriList));

			MeshSettings.SetEmbedded();
			BodyCreationSettings FloorSettings(&MeshSettings,RVec3(Vec3(0.0f, 0.0f, 0.0f)), Rot, EMotionType::Static, Layers::NON_MOVING);
			
			FloorSettings.SaveWithChildren(OutStream, &ShapeToId, &MaterialToId, &GroupToId);

			// Uncomment this to confirm the floor data is properly loaded and visible 
			//Body &floor = *mBodyInterface->CreateBody(FloorSettings);
			//mBodyInterface->AddBody(floor.GetID(), EActivation::DontActivate);
		}
		
		std::ofstream OutBin{"D:\\Out.bin", ofstream::out | ofstream::trunc | ofstream::binary};
		SaveData.flush();
		OutBin << SaveData.rdbuf();
		OutBin.flush();
		OutBin.close();
	}

	ufbx_free_scene(scene);


	ifstream InFile{"D:\\Out.bin", ifstream::in | ofstream::binary};
	LoadData << InFile.rdbuf();
	
	InFile.close();
        // Note this never returns false, so the data is correctly loading.
	if (!(SaveData.str() == LoadData.str()))
	{
		Out << "Data was not equal!!!\n";
		Out << "save data: " << SaveData.str().length() << " load data: " << LoadData.str().length() << "\n"; 
		Out.close();
		return;
	}

	JPH::StreamInWrapper StreamIn(LoadData);
	JPH::BodyCreationSettings::IDToShapeMap IdToShape;
	JPH::BodyCreationSettings::IDToMaterialMap IdToMaterial;
	JPH::BodyCreationSettings::IDToGroupFilterMap IdToGroup;
	JPH::BodyCreationSettings::BCSResult Result = JPH::BodyCreationSettings::sRestoreWithChildren(StreamIn, IdToShape, IdToMaterial, IdToGroup);

	while (Result.IsValid())
	{
		Out << "Loaded Mesh\n";
		BodyCreationSettings RestoredBodySettings = Result.Get();
		Body &floor = *mBodyInterface->CreateBody(RestoredBodySettings);
		
		mBodyInterface->AddBody(floor.GetID(), EActivation::DontActivate);
	
		// Decode next one
		Result = JPH::BodyCreationSettings::sRestoreWithChildren(StreamIn, IdToShape, IdToMaterial, IdToGroup);
		if (Result.HasError())
		{
			Out << "Had Error:\n";
			Out << Result.GetError().c_str() << ". Trying again...\n";
		}
	}

	Out.close();

	// Different sized boxes
	Body &body1 = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(20, 1, 1)), RVec3(0, 10, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
	mBodyInterface->AddBody(body1.GetID(), EActivation::Activate);

	Body &body2 = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(2, 3, 4)), RVec3(0, 10, 10), Quat::sRotation(Vec3::sAxisZ(), 0.25f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
	mBodyInterface->AddBody(body2.GetID(), EActivation::Activate);

	Body &body3 = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(0.5f, 0.75f, 1.0f)), RVec3(0, 10, 20), Quat::sRotation(Vec3::sAxisX(), 0.25f * JPH_PI) * Quat::sRotation(Vec3::sAxisZ(), 0.25f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
	mBodyInterface->AddBody(body3.GetID(), EActivation::Activate);
}
  1. To confirm it's not a problem with the mesh, uncomment the lines to visualize it loading properly:
//Body &floor = *mBodyInterface->CreateBody(FloorSettings);
//mBodyInterface->AddBody(floor.GetID(), EActivation::DontActivate);
  1. Run the samples app -> Select Test -> Shapes -> Box Shape
  2. Note the missing meshes, hitting "R" to reload, will cause different meshes to not load, seemingly randomly due to a result.SetError("Error reading body creation settings"); being returned

Evidence

Here's what it looks like if we don't serialize/deserialize
image

Here's a random attempt at loading:
image

Here's hitting "R" to reload:
image

Here's the FBX I'm trying to load, or the already serialized data if you want to load directly:
FBX_Out_Data.zip

Thanks!

@jrouwe
Copy link
Owner

jrouwe commented May 2, 2024

The problem is that SaveWithChildren will build a ShapeToIDMap which maps Shape* to an ID. In your loop, the created Shape is deleted at the end of the iteration (when BodyCreationSettings/MeshShapeSettings is destructed). There is a certain chance that in the next iteration, a new Shape will be allocated at the exact same location in memory. This means it will see it as the same shape and not write it to the stream. You need to keep your Shapes, PhysicsMaterials and GroupFilters in memory until everything has been written (or if you're sure there is no sharing possible, you need to clear those maps after every shape on both save and load).

@wirepair
Copy link
Author

wirepair commented May 3, 2024

Excellent! Thank you for confirming I was indeed using it wrong :>. I assume simply using Jolt's overriden 'new' when creating the MeshSettings & FloorSettings and stuffing them into a vector until the data is written would be sufficient here, that way they are allocated from unique locations from the heap.

Closing anyways as this fixed my issue, thanks again!

@wirepair wirepair closed this as completed May 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants