Files
2025-05-18 13:04:45 +08:00

444 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "OBJMeshUtil.h"
#include "DynamicMesh/DynamicMesh3.h"
#include "DynamicMesh/DynamicMeshAttributeSet.h"
#include <fstream>
#include <string>
#include <sstream>
#include <iomanip>
#include <iostream>
using namespace UE::Geometry;
namespace UE::Private::MeshFileUtilsLocals
{
/**
* Write obj element: vertex, UV, normal or polygonal face (with only vertex indices).
*
* @param Out File stream we are writing into.
* @param Token One of "v", "vn", "vt", "f".
* @param Value Vector type containing the data to write. ith entry will be accessed with the [] operator.
*
* @note Assumes Out is open.
*/
template<int Dim, class VectorType>
void ObjWriteElement(std::ofstream& Out, const char* Token, const VectorType& Value)
{
Out << Token << ' ';
for (int32 Idx = 0; Idx < Dim; ++Idx)
{
Out << Value[Idx];
if (Idx < Dim - 1)
{
Out << ' ';
}
}
Out << '\n';
}
void ObjWriteString(std::ofstream& Out, const FString& Line)
{
Out << TCHAR_TO_ANSI(*Line);
}
} // end namespace MeshFileUtilsLocals
namespace UE::MeshFileUtils
{
ELoadOBJStatus ReadOBJ(std::istream& FileStream, FDynamicMesh3& Mesh, const FLoadOBJSettings& Settings)
{
auto ExtractTripletFromGroup = [](std::stringstream& InLineStream,
bool bInParseNormalsAndTextures,
int& OutVertexID,
int& OutTextureID,
int& OutNormalID)
{
std::string Blob;
OutVertexID = -1;
OutTextureID = -1;
OutNormalID = -1;
if (InLineStream >> Blob)
{
const char* BlobCStr = Blob.c_str();
const char** Token = &BlobCStr;
OutVertexID = FCStringAnsi::Atoi((*Token));
(*Token) += FCStringAnsi::Strcspn((*Token), "/");
if ((*Token)[0] != '/')
{
return true;
}
if (!bInParseNormalsAndTextures)
{
return true;
}
(*Token)++;
// v//n case
if ((*Token)[0] == '/')
{
(*Token)++;
OutNormalID = FCStringAnsi::Atoi((*Token));
(*Token) += FCStringAnsi::Strcspn((*Token), "/");
return true;
}
// v/t/n or v/t case
OutTextureID = FCStringAnsi::Atoi((*Token));
(*Token) += FCStringAnsi::Strcspn((*Token), "/");
if ((*Token)[0] != '/')
{
return true;
}
// v/t/n case
(*Token)++; // skip '/'
OutNormalID = FCStringAnsi::Atoi((*Token));
(*Token) += FCStringAnsi::Strcspn((*Token), "/");
return true;
}
return false;
};
Mesh.Clear();
struct FTriToAdd
{
FIndex3i Tri, UV, Normal;
bool bHasUV = false;
bool bHasNormal = false;
int32 GroupID = 0;
FTriToAdd() = default;
FTriToAdd(FIndex3i Tri, int32 GroupID) : Tri(Tri), GroupID(GroupID)
{
}
void SetUV(FIndex3i InUV)
{
UV = InUV;
bHasUV = true;
}
void SetNormal(FIndex3i InNormal)
{
Normal = InNormal;
bHasNormal = true;
}
};
// buffer the triangles in case the faces precede the vertices they reference in the file
TArray<FTriToAdd> Triangles;
const bool bLoadNormalsOrUVs = Settings.bLoadNormals || Settings.bLoadUVs;
if (bLoadNormalsOrUVs)
{
Mesh.EnableAttributes();
}
FDynamicMeshNormalOverlay* Normals = (Settings.bLoadNormals) ? Mesh.Attributes()->PrimaryNormals() : nullptr;
FDynamicMeshUVOverlay* UVs = (Settings.bLoadUVs) ? Mesh.Attributes()->PrimaryUV() : nullptr;
int32 GroupID = 1;
bool bHasGroups = false;
std::string Line;
while (std::getline(FileStream, Line))
{
if (Line.empty())
{
continue;
}
std::stringstream LineStream(Line);
std::string Command;
if (LineStream >> Command)
{
if (Command.empty() || Command[0] == '#') // skip comment lines
{
continue;
}
if (Command == "g" || Command == "o")
{
bHasGroups = true;
GroupID++;
}
else if (Command == "v") // vertex
{
FVector3d Vert;
LineStream >> Vert.X >> Vert.Y >> Vert.Z;
Mesh.AppendVertex(Vert);
}
else if (Settings.bLoadNormals && Command == "vn")
{
FVector3f Normal;
LineStream >> Normal.X >> Normal.Y >> Normal.Z;
Normalize(Normal);
Normals->AppendElement(Normal);
}
else if (Settings.bLoadUVs && Command == "vt")
{
FVector2f UV = FVector2f::Zero(); // v is optional so default to zero
LineStream >> UV.X >> UV.Y;
UVs->AppendElement(UV);
}
else if (Command == "f")
{
int V0, VN_1, VN; // vertices 0, N-1, and N (the last two are reused to make a triangle fan out of any non-triangle faces)
int T0, TN_1, TN; // UVs
int N0, NN_1, NN; // normals
if (!ExtractTripletFromGroup(LineStream, bLoadNormalsOrUVs, V0, T0, N0))
{
continue;
}
if (!ExtractTripletFromGroup(LineStream, bLoadNormalsOrUVs, VN_1, TN_1, NN_1))
{
continue;
}
while (ExtractTripletFromGroup(LineStream, bLoadNormalsOrUVs, VN, TN, NN))
{
FTriToAdd& ToAdd = Triangles.Emplace_GetRef(FIndex3i(V0 - 1, VN_1 - 1, VN - 1), GroupID);
if (Normals)
{
ToAdd.SetNormal(FIndex3i(N0 - 1, NN_1 - 1, NN - 1));
}
if (UVs)
{
ToAdd.SetUV(FIndex3i(T0 - 1, TN_1 - 1, TN - 1));
}
VN_1 = VN;
TN_1 = TN;
NN_1 = NN;
}
}
}
}
if (bHasGroups)
{
Mesh.EnableTriangleGroups();
}
for (const FTriToAdd& ToAdd : Triangles)
{
int32 TID = Mesh.AppendTriangle(ToAdd.Tri, ToAdd.GroupID);
if (Settings.bAddSeparatedTriForNonManifold && TID == FDynamicMesh3::NonManifoldID)
{
FIndex3i DupeVertsTri;
for (int32 SubIdx = 0; SubIdx < 3; ++SubIdx)
{
DupeVertsTri[SubIdx] = Mesh.AppendVertex(Mesh.GetVertex(ToAdd.Tri[SubIdx]));
}
TID = Mesh.AppendTriangle(DupeVertsTri, ToAdd.GroupID);
}
if (TID >= 0)
{
if (Normals && ToAdd.bHasNormal)
{
Normals->SetTriangle(TID, ToAdd.Normal);
}
if (UVs && ToAdd.bHasUV)
{
UVs->SetTriangle(TID, ToAdd.UV);
}
}
}
if (Settings.bReverseOrientation)
{
Mesh.ReverseOrientation();
}
return ELoadOBJStatus::Success;
}
ELoadOBJStatus LoadOBJ(const char* Path, FDynamicMesh3& Mesh, const FLoadOBJSettings& Settings)
{
std::ifstream FileStream(Path);
if (!FileStream)
{
return ELoadOBJStatus::InvalidPath;
}
ELoadOBJStatus Status = ReadOBJ(FileStream, Mesh, Settings);
FileStream.close();
return Status;
}
FDynamicMesh3 LoadOBJChecked(const char* Path, const FLoadOBJSettings& Settings)
{
FDynamicMesh3 Mesh;
ELoadOBJStatus Status = LoadOBJ(Path, Mesh, Settings);
check(Status == ELoadOBJStatus::Success);
return Mesh;
}
bool WriteOBJ(const char* Path, const FDynamicMesh3& InMesh, const FWriteOBJSettings& Settings)
{
// We compact the mesh to make sure the order that we write the vertex/normal/uv indices into obj is
// consistent with the storage order
FDynamicMesh3 Mesh;
Mesh.CompactCopy(InMesh);
if (Settings.bReverseOrientation)
{
Mesh.ReverseOrientation();
}
std::ofstream FileStream;
FileStream.precision(std::numeric_limits<double>::digits10);
FileStream.open(Path, std::ofstream::out | std::ofstream::trunc);
if (!ensure(FileStream))
{
return false;
}
bool bHasVertexNormals = Settings.bWritePerVertexValues && Mesh.HasVertexNormals();
bool bHasVertexUVs = Settings.bWritePerVertexValues && Mesh.HasVertexUVs();
for (int32 VID = 0; VID < Mesh.VertexCount(); ++VID)
{
check(Mesh.IsVertex(VID)); // mesh is not compact
FVector3d Pos = Mesh.GetVertex(VID);
if (!Settings.bWritePerVertexColors || !Mesh.HasVertexColors())
{
UE::Private::MeshFileUtilsLocals::ObjWriteElement<3>(FileStream, "v", Pos);
}
else
{
FVector3f Color = Mesh.GetVertexColor(VID);
FileStream << "v " << Pos[0] << " " << Pos[1] << " " << Pos[2] << " " << Color[0] << " " << Color[1] << " " << Color[2] << "\n";
}
if (bHasVertexNormals)
{
FVector3f Normal = Mesh.GetVertexNormal(VID);
UE::Private::MeshFileUtilsLocals::ObjWriteElement<3>(FileStream, "vn", Normal);
}
if (bHasVertexUVs)
{
FVector2f UV = Mesh.GetVertexUV(VID);
UE::Private::MeshFileUtilsLocals::ObjWriteElement<2>(FileStream, "vt", UV);
}
}
const FDynamicMeshUVOverlay* UVs = nullptr;
const FDynamicMeshNormalOverlay* Normals = nullptr;
if (Settings.bWritePerVertexValues == false && Mesh.Attributes())
{
UVs = Mesh.Attributes()->PrimaryUV();
if (UVs)
{
for (int32 UI = 0; UI < UVs->ElementCount(); ++UI)
{
check(UVs->IsElement(UI))
FVector2f UV = UVs->GetElement(UI);
UE::Private::MeshFileUtilsLocals::ObjWriteElement<2>(FileStream, "vt", UV);
}
}
Normals = Mesh.Attributes()->PrimaryNormals();
if (Normals)
{
for (int32 NI = 0; NI < Normals->ElementCount(); ++NI)
{
check(Normals->IsElement(NI));
FVector3f Normal = Normals->GetElement(NI);
UE::Private::MeshFileUtilsLocals::ObjWriteElement<3>(FileStream, "vn", Normal);
}
}
}
for (int32 TID = 0; TID < Mesh.TriangleCount(); ++TID)
{
check(Mesh.IsTriangle(TID));
FIndex3i TriVertices = Mesh.GetTriangle(TID);
if (Settings.bWritePerVertexValues)
{
if (bHasVertexNormals == false && bHasVertexUVs == false)
{
UE::Private::MeshFileUtilsLocals::ObjWriteString(FileStream, FString::Printf(TEXT("f %d %d %d\n"), TriVertices.A + 1, TriVertices.B + 1, TriVertices.C + 1));
}
else if (bHasVertexNormals == true && bHasVertexUVs == false)
{
UE::Private::MeshFileUtilsLocals::ObjWriteString(FileStream, FString::Printf(TEXT("f %d//%d %d//%d %d//%d\n"),
TriVertices.A + 1, TriVertices.A + 1,
TriVertices.B + 1, TriVertices.B + 1,
TriVertices.C + 1, TriVertices.C + 1));
}
else if (bHasVertexNormals == false && bHasVertexUVs == true)
{
UE::Private::MeshFileUtilsLocals::ObjWriteString(FileStream, FString::Printf(TEXT("f %d/%d %d/%d %d/%d\n"),
TriVertices.A + 1, TriVertices.A + 1,
TriVertices.B + 1, TriVertices.B + 1,
TriVertices.C + 1, TriVertices.C + 1));
}
else
{
UE::Private::MeshFileUtilsLocals::ObjWriteString(FileStream, FString::Printf(TEXT("f %d/%d/%d %d/%d/%d %d/%d/%d\n"),
TriVertices.A + 1, TriVertices.A + 1, TriVertices.A + 1,
TriVertices.B + 1, TriVertices.B + 1, TriVertices.B + 1,
TriVertices.C + 1, TriVertices.C + 1, TriVertices.C + 1));
}
}
else
{
bool bHaveUV = UVs != nullptr && UVs->IsSetTriangle(TID);
bool bHaveNormal = Normals != nullptr && Normals->IsSetTriangle(TID);
FIndex3i TriUVs = bHaveUV ? UVs->GetTriangle(TID) : FIndex3i::Invalid();
FIndex3i TriNormals = bHaveNormal ? Normals->GetTriangle(TID) : FIndex3i::Invalid();
if (bHaveUV && bHaveNormal)
{
UE::Private::MeshFileUtilsLocals::ObjWriteString(FileStream, FString::Printf(TEXT("f %d/%d/%d %d/%d/%d %d/%d/%d\n"),
TriVertices.A + 1, TriUVs.A + 1, TriNormals.A + 1,
TriVertices.B + 1, TriUVs.B + 1, TriNormals.B + 1,
TriVertices.C + 1, TriUVs.C + 1, TriNormals.C + 1));
}
else if (bHaveUV)
{
UE::Private::MeshFileUtilsLocals::ObjWriteString(FileStream, FString::Printf(TEXT("f %d/%d %d/%d %d/%d\n"),
TriVertices.A + 1, TriUVs.A + 1,
TriVertices.B + 1, TriUVs.B + 1,
TriVertices.C + 1, TriUVs.C + 1));
}
else if (bHaveNormal)
{
UE::Private::MeshFileUtilsLocals::ObjWriteString(FileStream, FString::Printf(TEXT("f %d//%d %d//%d %d//%d\n"),
TriVertices.A + 1, TriNormals.A + 1,
TriVertices.B + 1, TriNormals.B + 1,
TriVertices.C + 1, TriNormals.C + 1));
}
else
{
UE::Private::MeshFileUtilsLocals::ObjWriteString(FileStream, FString::Printf(TEXT("f %d %d %d\n"), TriVertices.A + 1, TriVertices.B + 1, TriVertices.C + 1));
}
}
}
FileStream.close();
return true;
}
}