From 3507c531c8b17ebabebef6f5ad28f0ddd9555075 Mon Sep 17 00:00:00 2001 From: Jared Fisher Date: Wed, 20 May 2026 18:34:18 -0700 Subject: [PATCH 1/5] UC2 PC static mesh extraction support - Widen UC2 game detection to ArVer 140-151 (was only 151) to cover PC packages - Split Xbox (ArLicenseeVer=1) and PC (ArLicenseeVer=0) serialization paths in FStaticMeshVertexStream, FRawColorStream, and FStaticMeshUVStream: Xbox streams are guarded by a Flag field, PC streams store data directly - On PC, FStaticMeshVertexStream deserializes vertices one at a time to handle the packed int[3] format used by FUC2Vector/FUC2Normal - LoadExternalUC2Data() is now Xbox-only; PC packages store all geometry inline - Fix FStaticMeshSection::NumFaces on PC: the field holds the last index value, not a face count, so divide by 3 after load - Remove the assert in LoadExternalUC2Data that fired when IndexStream1 was already populated (valid on PC) --- Unreal/GameDatabase.cpp | 2 +- Unreal/UnrealMesh/UnMesh2.cpp | 16 +++++++---- Unreal/UnrealMesh/UnMesh2.h | 53 +++++++++++++++++++++++++++-------- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/Unreal/GameDatabase.cpp b/Unreal/GameDatabase.cpp index 4dbdeca1..955d4970 100644 --- a/Unreal/GameDatabase.cpp +++ b/Unreal/GameDatabase.cpp @@ -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 diff --git a/Unreal/UnrealMesh/UnMesh2.cpp b/Unreal/UnrealMesh/UnMesh2.cpp index d1886d19..3216efc7 100644 --- a/Unreal/UnrealMesh/UnMesh2.cpp +++ b/Unreal/UnrealMesh/UnMesh2.cpp @@ -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; @@ -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++) { diff --git a/Unreal/UnrealMesh/UnMesh2.h b/Unreal/UnrealMesh/UnMesh2.h index ea41c350..bb27b9e5 100644 --- a/Unreal/UnrealMesh/UnMesh2.h +++ b/Unreal/UnrealMesh/UnMesh2.h @@ -1577,13 +1577,26 @@ struct FStaticMeshVertexStream if (Ar.Engine() == GAME_UE2X) { TArray UC2Verts; - int Flag; - Ar << Flag; - if (!Flag) + if (Ar.ArLicenseeVer == 1) { - Ar << UC2Verts; - CopyArray(S.Vert, UC2Verts); + // Xbox: a Flag precedes the array; skip the array when Flag is non-zero + int Flag; + Ar << Flag; + if (!Flag) Ar << UC2Verts; } + else + { + // PC: no Flag; vertices are serialized one at a time because FUC2Vector/FUC2Normal + // use a packed int[3] format that requires per-element operator<< rather than a + // bulk TArray read + int Count; + Ar << Count; + UC2Verts.Empty(Count); + UC2Verts.AddZeroed(Count); + for (int i = 0; i < Count; i++) + Ar << UC2Verts[i]; + } + CopyArray(S.Vert, UC2Verts); return Ar << S.Revision; } #endif // UC2 @@ -1621,9 +1634,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 @@ -1661,9 +1683,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 From 63b05c3c1c7b9fbcea29a7a1abf495ef984e1b3e Mon Sep 17 00:00:00 2001 From: Jared Fisher Date: Fri, 22 May 2026 11:30:27 -0700 Subject: [PATCH 2/5] UC2 PC: fix vertex stream to read floats, not int16 On PC (ArLicenseeVer==0) the FPackedPosition serializer writes vertex positions as FVector (3 floats, 12 bytes each) rather than the Xbox int16 triplet (6 bytes). Normals are stored as 3 ints (12 bytes). The previous fix iterated over FStaticMeshVertexUC2 elements one by one which advanced the archive by the right byte count but left V.X/V.Y/V.Z at zero (local read vars were never stored), causing all vertices to collapse to a single deduplicated point in the PSK output. The new PC path bypasses FStaticMeshVertexUC2 entirely and reads directly into FStaticMeshVertex: FVector for position (world-space floats, no GUC2VectorScale/Base transform needed) and int[3] for the normal (consumed but not yet decoded). Co-Authored-By: Claude Sonnet 4.6 --- Unreal/UnrealMesh/UnMesh2.h | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/Unreal/UnrealMesh/UnMesh2.h b/Unreal/UnrealMesh/UnMesh2.h index bb27b9e5..c94be60c 100644 --- a/Unreal/UnrealMesh/UnMesh2.h +++ b/Unreal/UnrealMesh/UnMesh2.h @@ -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; } }; @@ -1576,27 +1578,32 @@ struct FStaticMeshVertexStream #if UC2 if (Ar.Engine() == GAME_UE2X) { - TArray UC2Verts; if (Ar.ArLicenseeVer == 1) { // Xbox: a Flag precedes the array; skip the array when Flag is non-zero + TArray UC2Verts; int Flag; Ar << Flag; if (!Flag) Ar << UC2Verts; + CopyArray(S.Vert, UC2Verts); } else { - // PC: no Flag; vertices are serialized one at a time because FUC2Vector/FUC2Normal - // use a packed int[3] format that requires per-element operator<< rather than a - // bulk TArray read + // 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; Ar << Count; - UC2Verts.Empty(Count); - UC2Verts.AddZeroed(Count); + S.Vert.Empty(Count); + S.Vert.AddZeroed(Count); for (int i = 0; i < Count; i++) - Ar << UC2Verts[i]; + { + Ar << S.Vert[i].Pos; // FVector: X, Y, Z as floats + int nx, ny, nz; + Ar << nx << ny << nz; // normal: 3 ints, decoding TODO + S.Vert[i].Normal.Set(0, 0, 0); + } } - CopyArray(S.Vert, UC2Verts); return Ar << S.Revision; } #endif // UC2 From 007ae3f53811aae34bba5eca7eff39a6f2b47fe3 Mon Sep 17 00:00:00 2001 From: Jared Fisher Date: Fri, 22 May 2026 12:34:09 -0700 Subject: [PATCH 3/5] UC2 PC: fix export table and vertex count reading for ArVer=140 packages ArVer=140 packages (6 out of 45 UC2 PC static mesh packages) use compact AR_INDEX encoding for export table fields that later versions store as full int32. This matches the FName serialization version threshold already in UnPackage.cpp (ArVer >= 145 uses int32). - UnPackage2.cpp: Serialize2X() now branches on ArVer<145: use AR_INDEX for ClassIndex, SuperIndex, SerialSize, SerialOffset (ArVer>=145 path unchanged - still uses full int32 with int16 truncation) - UnMesh2.h: UC2 PC vertex stream count also stored as AR_INDEX for ArVer<145; use AR_INDEX(Count) when reading vertex count in that path Before: 6 packages failed with "wrong name index" errors because misaligned export table reads produced OOB name indices. After: all 45 UC2 PC static mesh packages export cleanly (2,095 files). Co-Authored-By: Claude Sonnet 4.6 --- Exporters/ExportPsk.cpp | 6 +++++ Unreal/UnrealMesh/UnMesh2.h | 5 +++- Unreal/UnrealPackage/UnPackage2.cpp | 39 +++++++++++++++++++---------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/Exporters/ExportPsk.cpp b/Exporters/ExportPsk.cpp index 6db8e510..de6607c6 100644 --- a/Exporters/ExportPsk.cpp +++ b/Exporters/ExportPsk.cpp @@ -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; diff --git a/Unreal/UnrealMesh/UnMesh2.h b/Unreal/UnrealMesh/UnMesh2.h index c94be60c..5e65a9c0 100644 --- a/Unreal/UnrealMesh/UnMesh2.h +++ b/Unreal/UnrealMesh/UnMesh2.h @@ -1593,7 +1593,10 @@ struct FStaticMeshVertexStream // int[3] (12 bytes each). Bypasses FStaticMeshVertexUC2/FUC2Vector entirely // since those assume int16 Xbox encoding; floats need no scale/offset transform. int Count; - Ar << 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++) diff --git a/Unreal/UnrealPackage/UnPackage2.cpp b/Unreal/UnrealPackage/UnPackage2.cpp index 3a67bc3a..eea2e22d 100644 --- a/Unreal/UnrealPackage/UnPackage2.cpp +++ b/Unreal/UnrealPackage/UnPackage2.cpp @@ -177,22 +177,35 @@ void FObjectExport::Serialize2X(FArchive &Ar) uint32 ObjectFlags; #endif - Ar << ClassIndex << SuperIndex; - if (Ar.ArVer >= 150) + if (Ar.ArVer < 145) { - int16 idx = PackageIndex; - Ar << idx; - PackageIndex = idx; + // ArVer=140 PC packages: compact AR_INDEX encoding for most fields + Ar << AR_INDEX(ClassIndex) << AR_INDEX(SuperIndex); + Ar << PackageIndex; + Ar << ObjectName << ObjectFlags; + Ar << AR_INDEX(SerialSize); + if (SerialSize) + Ar << AR_INDEX(SerialOffset); } else - Ar << PackageIndex; - Ar << ObjectName << ObjectFlags << SerialSize; - if (SerialSize) - Ar << SerialOffset; - // UC2 has strange thing here: indices are serialized as 4-byte int (instead of AR_INDEX), - // but stored into 2-byte shorts - ClassIndex = int16(ClassIndex); - SuperIndex = int16(SuperIndex); + { + // ArVer 145-151: full int32 for ClassIndex/SuperIndex/SerialSize/SerialOffset + Ar << ClassIndex << SuperIndex; + if (Ar.ArVer >= 150) + { + int16 idx = PackageIndex; + Ar << idx; + PackageIndex = idx; + } + else + Ar << PackageIndex; + Ar << ObjectName << ObjectFlags << SerialSize; + if (SerialSize) + Ar << SerialOffset; + // UC2 ArVer 145-151: indices are full 4-byte int, but values fit in int16 + ClassIndex = int16(ClassIndex); + SuperIndex = int16(SuperIndex); + } unguard; } From c031a303004099157e13d576ebfa5adbcd06577d Mon Sep 17 00:00:00 2001 From: Jared Fisher Date: Fri, 22 May 2026 16:14:17 -0700 Subject: [PATCH 4/5] Add OBJ export for static meshes (-obj flag) Adds ExportStaticMeshOBJ() producing .obj + .mtl per mesh LOD, selected via -obj on the command line or the Settings dialog. Per-section material groups are named from UUnrealMaterial names. Targeting UT2004 mod import (raw UE coordinate space, no axis transform). Co-Authored-By: Claude Sonnet 4.6 --- Exporters/ExportObj.cpp | 145 ++++++++++++++++++++++++++++++++++ Exporters/Exporters.h | 2 + UmodelTool/Main.cpp | 10 ++- UmodelTool/SettingsDialog.cpp | 1 + UmodelTool/UmodelSettings.h | 1 + 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 Exporters/ExportObj.cpp diff --git a/Exporters/ExportObj.cpp b/Exporters/ExportObj.cpp new file mode 100644 index 00000000..446ac716 --- /dev/null +++ b/Exporters/ExportObj.cpp @@ -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; +} diff --git a/Exporters/Exporters.h b/Exporters/Exporters.h index 2272240c..3dec5625 100644 --- a/Exporters/Exporters.h +++ b/Exporters/Exporters.h @@ -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 diff --git a/UmodelTool/Main.cpp b/UmodelTool/Main.cpp index 5294499a..148c484f 100644 --- a/UmodelTool/Main.cpp +++ b/UmodelTool/Main.cpp @@ -270,6 +270,9 @@ static void CallExportStaticMesh(const CStaticMesh* Mesh) case EExportMeshFormat::gltf: ExportStaticMeshGLTF(Mesh); break; + case EExportMeshFormat::obj: + ExportStaticMeshOBJ(Mesh); + break; } } @@ -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" @@ -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" @@ -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 diff --git a/UmodelTool/SettingsDialog.cpp b/UmodelTool/SettingsDialog.cpp index fb1acf6e..8ef486f0 100644 --- a/UmodelTool/SettingsDialog.cpp +++ b/UmodelTool/SettingsDialog.cpp @@ -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) ] diff --git a/UmodelTool/UmodelSettings.h b/UmodelTool/UmodelSettings.h index 6af56647..85b95cbb 100644 --- a/UmodelTool/UmodelSettings.h +++ b/UmodelTool/UmodelSettings.h @@ -58,6 +58,7 @@ enum class EExportMeshFormat : int psk, md5, gltf, + obj, }; enum class ETextureExportFormat : int From 3c8cd0558fba98dc544616108810a164776d3d90 Mon Sep 17 00:00:00 2001 From: Jared Fisher Date: Fri, 22 May 2026 17:00:09 -0700 Subject: [PATCH 5/5] UC2 PC: decode vertex normals as IEEE 754 floats The 3 ints following each vertex position are float bits, not integer normals. Reinterpret via bitcast. Previously set to (0,0,0). Co-Authored-By: Claude Sonnet 4.6 --- Unreal/UnrealMesh/UnMesh2.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Unreal/UnrealMesh/UnMesh2.h b/Unreal/UnrealMesh/UnMesh2.h index 5e65a9c0..60a7bb4c 100644 --- a/Unreal/UnrealMesh/UnMesh2.h +++ b/Unreal/UnrealMesh/UnMesh2.h @@ -1603,8 +1603,8 @@ struct FStaticMeshVertexStream { Ar << S.Vert[i].Pos; // FVector: X, Y, Z as floats int nx, ny, nz; - Ar << nx << ny << nz; // normal: 3 ints, decoding TODO - S.Vert[i].Normal.Set(0, 0, 0); + 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;