jrouwe / JoltPhysics

A multi core friendly rigid body physics and collision detection library. Written in C++. Suitable for games and VR applications. Used by Horizon Forbidden West.
MIT License
6.42k stars 414 forks source link

Problem serializing/deserializing BodyCreationSettings with sRestoreWithChildren #1088

Closed wirepair closed 4 months ago

wirepair commented 4 months ago

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

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

include "ThirdParty/ufbx/ufbx.h"

include <Jolt/Core/StreamWrapper.h>

include

include

include

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);

}

4. To confirm it's not a problem with the mesh, uncomment the lines to visualize it loading properly:
```cpp
//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 commented 4 months ago

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 commented 4 months ago

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!