Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions Exporters/ExportObj.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#include "Core.h"
#include "UnCore.h"
#include "UnObject.h"
#include "UnrealMaterial/UnMaterial.h"
#include "Mesh/StaticMesh.h"
#include "Exporters.h"


void ExportStaticMeshOBJ(const CStaticMesh* Mesh)
{
guard(ExportStaticMeshOBJ);

UObject* OriginalMesh = Mesh->OriginalMesh;
if (!Mesh->Lods.Num())
{
appNotify("Mesh %s has 0 lods", OriginalMesh->Name);
return;
}

int MaxLod = GExportLods ? Mesh->Lods.Num() : 1;
for (int Lod = 0; Lod < MaxLod; Lod++)
{
guard(Lod);

const CStaticMeshLod& MeshLod = Mesh->Lods[Lod];
if (MeshLod.NumVerts == 0)
{
appPrintf("WARNING: skipping LOD %d with 0 vertices\n", Lod);
continue;
}
if (MeshLod.Sections.Num() == 0)
{
appNotify("Mesh %s Lod %d has no sections", OriginalMesh->Name, Lod);
continue;
}

char objName[512];
char mtlName[512];
if (Lod == 0)
{
appSprintf(ARRAY_ARG(objName), "%s.obj", OriginalMesh->Name);
appSprintf(ARRAY_ARG(mtlName), "%s.mtl", OriginalMesh->Name);
}
else
{
appSprintf(ARRAY_ARG(objName), "%s_Lod%d.obj", OriginalMesh->Name, Lod);
appSprintf(ARRAY_ARG(mtlName), "%s_Lod%d.mtl", OriginalMesh->Name, Lod);
}

FArchive* Ar = CreateExportArchive(OriginalMesh, EFileArchiveOptions::TextFile, "%s", objName);
if (!Ar)
{
if (Lod == 0) return;
continue;
}

// Header
Ar->Printf("# Exported by UEViewer\nmtllib %s\no %s\n\n", mtlName, OriginalMesh->Name);

// Vertex positions (raw UE coordinate space)
for (int i = 0; i < MeshLod.NumVerts; i++)
{
const CVecT& P = MeshLod.Verts[i].Position;
Ar->Printf("v %g %g %g\n", P[0], P[1], P[2]);
}
Ar->Printf("\n");

// UV coordinates
for (int i = 0; i < MeshLod.NumVerts; i++)
{
const CMeshUVFloat& UV = MeshLod.Verts[i].UV;
Ar->Printf("vt %g %g\n", UV.U, UV.V);
}
Ar->Printf("\n");

// Normals
for (int i = 0; i < MeshLod.NumVerts; i++)
{
CVec3 N;
Unpack(N, MeshLod.Verts[i].Normal);
Ar->Printf("vn %g %g %g\n", N[0], N[1], N[2]);
}
Ar->Printf("\n");

// Faces grouped by section (material)
CIndexBuffer::IndexAccessor_t GetIndex = MeshLod.Indices.GetAccessor();
for (int SecIdx = 0; SecIdx < MeshLod.Sections.Num(); SecIdx++)
{
const CMeshSection& S = MeshLod.Sections[SecIdx];

char matBuf[256];
const char* MatName;
if (S.Material)
MatName = S.Material->Name;
else
{
appSprintf(ARRAY_ARG(matBuf), "Material_%d", SecIdx);
MatName = matBuf;
}

Ar->Printf("g %s\nusemtl %s\n", MatName, MatName);

for (int f = 0; f < S.NumFaces; f++)
{
int base = S.FirstIndex + f * 3;
// OBJ indices are 1-based; vertex/uv/normal share the same index
int i0 = GetIndex(base + 0) + 1;
int i1 = GetIndex(base + 1) + 1;
int i2 = GetIndex(base + 2) + 1;
Ar->Printf("f %d/%d/%d %d/%d/%d %d/%d/%d\n",
i0, i0, i0,
i1, i1, i1,
i2, i2, i2);
}
Ar->Printf("\n");
}

delete Ar;

// MTL file — one newmtl entry per section
FArchive* MtlAr = CreateExportArchive(OriginalMesh, EFileArchiveOptions::TextFile, "%s", mtlName);
if (MtlAr)
{
for (int SecIdx = 0; SecIdx < MeshLod.Sections.Num(); SecIdx++)
{
const CMeshSection& S = MeshLod.Sections[SecIdx];
char matBuf[256];
const char* MatName;
if (S.Material)
MatName = S.Material->Name;
else
{
appSprintf(ARRAY_ARG(matBuf), "Material_%d", SecIdx);
MatName = matBuf;
}
MtlAr->Printf("newmtl %s\n\n", MatName);
}
delete MtlAr;
}

unguardf("%d", Lod);
}

unguard;
}
6 changes: 6 additions & 0 deletions Exporters/ExportPsk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,12 @@ static void ExportStaticMeshLod(const CStaticMeshLod &Lod, FArchive &Ar)
{
guard(ExportStaticMeshLod);

if (Lod.NumVerts == 0)
{
appPrintf("WARNING: skipping LOD with 0 vertices\n");
return;
}

// using 'static' here to avoid zero-filling unused fields
static VChunkHeader BoneHdr, InfHdr;

Expand Down
2 changes: 2 additions & 0 deletions Exporters/Exporters.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ void ExportMd5Anim(const CAnimSet* Anim);
// glTF
void ExportSkeletalMeshGLTF(const CSkeletalMesh* Mesh);
void ExportStaticMeshGLTF(const CStaticMesh* Mesh);
// OBJ
void ExportStaticMeshOBJ(const CStaticMesh* Mesh);
// 3D
void Export3D(const UVertMesh* Mesh);
// TGA, DDS, PNG
Expand Down
10 changes: 9 additions & 1 deletion UmodelTool/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ static void CallExportStaticMesh(const CStaticMesh* Mesh)
case EExportMeshFormat::gltf:
ExportStaticMeshGLTF(Mesh);
break;
case EExportMeshFormat::obj:
ExportStaticMeshOBJ(Mesh);
break;
}
}

Expand Down Expand Up @@ -440,6 +443,7 @@ static void PrintUsage()
" -psk use ActorX format for meshes (default)\n"
" -md5 use md5mesh/md5anim format for skeletal mesh\n"
" -gltf use glTF 2.0 format for mesh\n"
" -obj use OBJ format for static mesh (UT2004 import compatible)\n"
" -lods export all available mesh LOD levels\n"
" -dds export textures in DDS format whenever possible\n"
" -png export textures in PNG format instead of TGA\n"
Expand All @@ -451,7 +455,7 @@ static void PrintUsage()
" SkeletalMesh exported as ActorX psk file, MD5Mesh or glTF\n"
" MeshAnimation exported as ActorX psa file or MD5Anim\n"
" VertMesh exported as Unreal 3d file\n"
" StaticMesh exported as psk file with no skeleton (pskx) or glTF\n"
" StaticMesh exported as psk file with no skeleton (pskx), glTF, or OBJ\n"
" Texture exported in tga or dds format\n"
" Sounds file extension depends on object contents\n"
" ScaleForm gfx\n"
Expand Down Expand Up @@ -874,6 +878,10 @@ int main(int argc, const char **argv)
{
GSettings.Export.SkeletalMeshFormat = GSettings.Export.StaticMeshFormat = EExportMeshFormat::gltf;
}
else if (!stricmp(opt, "obj"))
{
GSettings.Export.StaticMeshFormat = EExportMeshFormat::obj;
}
else if (!stricmp(opt, "all") && mainCmd == CMD_Dump)
{
// -all should be used only with -dump
Expand Down
1 change: 1 addition & 0 deletions UmodelTool/SettingsDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ UIElement& UISettingsDialog::MakeExportOptions()
.SetWidth(100)
.AddItem("ActorX (pskx)", EExportMeshFormat::psk)
.AddItem("glTF 2.0", EExportMeshFormat::gltf)
.AddItem("OBJ", EExportMeshFormat::obj)
]
+ NewControl(UICheckbox, "Export LODs", &Opt.Export.ExportMeshLods)
]
Expand Down
1 change: 1 addition & 0 deletions UmodelTool/UmodelSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ enum class EExportMeshFormat : int
psk,
md5,
gltf,
obj,
};

enum class ETextureExportFormat : int
Expand Down
2 changes: 1 addition & 1 deletion Unreal/GameDatabase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ void FArchive::DetectGame()
SET(GAME_UC1);
#endif
#if UC2
if (ArVer == 151 && (ArLicenseeVer == 0 || ArLicenseeVer == 1))
if ((ArVer >= 140 && ArVer <= 151) && (ArLicenseeVer == 0 || ArLicenseeVer == 1))
SET(GAME_UC2);
#endif

Expand Down
16 changes: 11 additions & 5 deletions Unreal/UnrealMesh/UnMesh2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1621,13 +1621,20 @@ void UStaticMesh::Serialize(FArchive &Ar)
GUC2VectorScale = VectorScale;
GUC2VectorBase = VectorBase;
Ar << VertexStream << ColorStream << AlphaStream << UVStream << IndexStream1;
// Xbox bundles IndexStream2 together with vertex data in XPR files, so it is loaded
// separately by LoadExternalUC2Data(); PC packages store it inline like every other stream
if (Ar.ArLicenseeVer != 1) Ar << IndexStream2;
//!!!!!
// appPrintf("v:%d c1:%d c2:%d uv:%d idx1:%d\n", VertexStream.Vert.Num(), ColorStream.Color.Num(), AlphaStream.Color.Num(),
// UVStream.Num() ? UVStream[0].Data.Num() : -1, IndexStream1.Indices.Num());
Ar << f108;

LoadExternalUC2Data();
// Xbox geometry (vertices + indices) is stored in external XPR files, not the package
if (Ar.ArLicenseeVer == 1)
LoadExternalUC2Data();

// On PC, FStaticMeshSection::NumFaces holds the last index value rather than a face count;
// divide by 3 to get the actual number of triangles
if (Ar.ArLicenseeVer == 0)
for (int s = 0; s < Sections.Num(); s++)
Sections[s].NumFaces = Sections[s].NumFaces / 3;

// skip collision information
goto skip_remaining;
Expand Down Expand Up @@ -1704,7 +1711,6 @@ void UStaticMesh::LoadExternalUC2Data()
//?? S.NumFaces is used as NumIndices, but it is not a multiply of 3
//?? (sometimes is is N*3, sometimes = N*3+1, sometimes - N*3+2 ...)
//?? May be UC2 uses triangle strips instead of triangles?
assert(IndexStream1.Indices.Num() == 0); //???
/* int NumIndices = 0;
for (i = 0; i < Sections.Num(); i++)
{
Expand Down
69 changes: 55 additions & 14 deletions Unreal/UnrealMesh/UnMesh2.h
Original file line number Diff line number Diff line change
Expand Up @@ -857,9 +857,11 @@ struct FUC2Vector
friend FArchive& operator<<(FArchive &Ar, FUC2Vector &V)
{
if (Ar.ArLicenseeVer == 1)
return Ar << V.X << V.Y << V.Z;
// LicenseeVer == 0 -- serialize as int[3] and rescale, using some global scale
int X, Y, Z;
return Ar << V.X << V.Y << V.Z; // Xbox: 3 int16s
// PC: stored as FVector (3 floats). FStaticMeshVertexStream bypasses this
// for the vertex stream and reads floats directly; this path exists for
// any other call sites and must consume exactly 12 bytes.
float X, Y, Z;
return Ar << X << Y << Z;
}
};
Expand Down Expand Up @@ -1576,14 +1578,35 @@ struct FStaticMeshVertexStream
#if UC2
if (Ar.Engine() == GAME_UE2X)
{
TArray<FStaticMeshVertexUC2> UC2Verts;
int Flag;
Ar << Flag;
if (!Flag)
if (Ar.ArLicenseeVer == 1)
{
Ar << UC2Verts;
// Xbox: a Flag precedes the array; skip the array when Flag is non-zero
TArray<FStaticMeshVertexUC2> UC2Verts;
int Flag;
Ar << Flag;
if (!Flag) Ar << UC2Verts;
CopyArray(S.Vert, UC2Verts);
}
else
{
// PC: positions are stored as FVector (3 floats, 12 bytes each), normals as
// int[3] (12 bytes each). Bypasses FStaticMeshVertexUC2/FUC2Vector entirely
// since those assume int16 Xbox encoding; floats need no scale/offset transform.
int Count;
if (Ar.ArVer < 145)
Ar << AR_INDEX(Count);
else
Ar << Count;
S.Vert.Empty(Count);
S.Vert.AddZeroed(Count);
for (int i = 0; i < Count; i++)
{
Ar << S.Vert[i].Pos; // FVector: X, Y, Z as floats
int nx, ny, nz;
Ar << nx << ny << nz; // stored as IEEE 754 float bits
S.Vert[i].Normal.Set(*(float*)&nx, *(float*)&ny, *(float*)&nz);
}
}
return Ar << S.Revision;
}
#endif // UC2
Expand Down Expand Up @@ -1621,9 +1644,18 @@ struct FRawColorStream
#if UC2
if (Ar.Engine() == GAME_UE2X)
{
int Flag;
Ar << Flag;
if (!Flag) Ar << S.Color;
if (Ar.ArLicenseeVer == 1)
{
// Xbox: a Flag precedes the color array; skip it when Flag is non-zero
int Flag;
Ar << Flag;
if (!Flag) Ar << S.Color;
}
else
{
// PC: color array is stored directly with no Flag guard
Ar << S.Color;
}
return Ar << S.Revision;
}
#endif // UC2
Expand Down Expand Up @@ -1661,9 +1693,18 @@ struct FStaticMeshUVStream
#if UC2
if (Ar.Engine() == GAME_UE2X)
{
int Flag;
Ar << Flag;
if (!Flag) Ar << S.Data;
if (Ar.ArLicenseeVer == 1)
{
// Xbox: a Flag precedes the UV array; skip it when Flag is non-zero
int Flag;
Ar << Flag;
if (!Flag) Ar << S.Data;
}
else
{
// PC: UV array is stored directly with no Flag guard
Ar << S.Data;
}
return Ar << S.f10 << S.f1C;
}
#endif // UC2
Expand Down
Loading