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

reading and writing external mesh data (starfield), writing untested #41

Merged
merged 4 commits into from
Jun 9, 2024
Merged
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
20 changes: 20 additions & 0 deletions include/BasicTypes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,26 @@ class NiStreamReversible {
if (mode == Mode::Reading)
fl = halfData;
}

void SyncUDEC3(Vector3& vec) {
uint32_t data;

if (mode == Mode::Writing) {
data = (((uint32_t)((vec.z+1.0)*511.5)) & 1023) << 20;
data &= (((uint32_t)((vec.y+1.0)*511.5)) & 1023) << 10;
data &= (((uint32_t)((vec.x+1.0)*511.5)) & 1023);
}

Sync(data);

if (mode == Mode::Reading) {
vec.x = (float)(((data & 1023) / 511.5) - 1.0);
vec.y = (float)((((data >> 10) & 1023) / 511.5) - 1.0);
vec.z = (float)((((data >> 20) & 1023) / 511.5) - 1.0);
}
}



NiOStream* asWrite() { return ostream; }
NiIStream* asRead() { return istream; }
Expand Down
109 changes: 108 additions & 1 deletion include/Geometry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -534,13 +534,89 @@ class BSDynamicTriShape : public NiCloneableStreamable<BSDynamicTriShape, BSTriS
const std::vector<Vector3>* normals = nullptr) override;
};

// BSGeometryMeshData is not a nif block object. In order to be able to use the data as if it were a block
// data object for reading and modifying geometry data, we inherit the NiGeometryData interface, and override
// the Sync function. The stream provided to sync for this object is not the same stream that is working with
// a nif file.
class BSGeometryMeshData: public NiCloneableStreamable<BSGeometryMeshData, NiGeometryData> {

public:

struct boneweight {
uint16_t boneIndex;
uint16_t weight;
};

struct meshlet {
uint32_t vertCount;
uint32_t vertOffset;
uint32_t primCount;
uint32_t primOffset;
};

struct culldata {
Vector4 boundSphere;
ByteColor4 normalCone; // abusing color4 as a 4 byte structure
float apexOffset;
};

uint32_t version;

uint32_t nTriIndices;
std::vector<Triangle> tris;

float scale;
uint32_t nWeightsPerVert;

// Vert count is a full 32 bits, versus the 16 bit count in NiGeometryData
uint32_t nVertices;
std::vector<uint16_t> packedVerts;
// vertices from NIGeometryData

uint32_t nUV1;
//std::vector<Vector2> uvs1;
uint32_t nUV2;
//std::vector<Vector2> uvs2;
// uvSets from NiGeometryData -- read/write interspersed with nUV1, nUV2

uint32_t nColors;
std::vector<ByteColor4> vColors;
// vertexColors from NiGeometryData

uint32_t nNormals;
std::vector<uint32_t> packedNormals;
// normals from NiGeometryData (UDEC3 packed in file)

uint32_t nTangents;
std::vector<uint32_t> packedTangents;
// tangents from NiGeometryData (UDEC3 packed in file)

uint32_t nTotalWeights;
std::vector < std::vector<boneweight> > skinWeights;

uint32_t nLODS;
std::vector< std::vector<Triangle> > lodTris;

uint32_t nMeshlets;
std::vector<meshlet> meshletList;

uint32_t nCullData;
std::vector<culldata> cullDataList;

void Sync(NiStreamReversible& stream);
};

struct BSGeometryMesh {
uint32_t triSize = 0;
uint32_t numVerts = 0;
uint32_t flags = 0; // Often 64
NiString meshName; // Always(?) 41

// in official files, this is 41 characters: hex characters from sha1 of the mesh data split into 2 parts
// with a path separator. The game does not seem to check the digest, so the same name can be used for
// replacement, or probably a human-readable one
NiString meshName;

BSGeometryMeshData meshData;
void Sync(NiStreamReversible& stream);
};

Expand All @@ -555,13 +631,44 @@ class BSGeometry : public NiCloneableStreamable<BSGeometry, NiShape> {

std::vector<BSGeometryMesh> meshes;

// A currently selected BSGeometryMesh in the list of meshes. All get/set data accessors use this to
// address a desired mesh
uint8_t selectedMesh = 0;

public:
static constexpr const char* BlockName = "BSGeometry";
const char* GetBlockName() override { return BlockName; }

void Sync(NiStreamReversible& stream);
void GetChildRefs(std::set<NiRef*>& refs) override;
void GetChildIndices(std::vector<uint32_t>& indices) override;

NiGeometryData* GetGeomData() const override;

bool GetTriangles(std::vector<Triangle>& tris) const override;
void SetTriangles(const std::vector<Triangle>& tris) override;

uint8_t MeshCount() { return (uint8_t) meshes.size(); }

// SelectMesh provides a way to choose which mesh from the BSGeometryMesh list data accesessors will use.
// If this is not called, functions to retrieve vertices, triangles, etc will default to the first mesh.
// Returns a pointer to the mesh data selected.
// TODO: this is not thread safe. A mutex should be set in SelectMesh and released in ReleaseMesh to
// avoid synchronization issues. Alternatively, Get/Set data functions could be changed to take a
// selector option, but that's a significant API change.
BSGeometryMesh* SelectMesh(uint8_t whichMesh) {
if (whichMesh < meshes.size()) {
selectedMesh = whichMesh;
return &meshes[selectedMesh];
}
return nullptr;
}
// ReleaseMesh resets the selected mesh data to default. This is a stand in for a mutex unlock operation
// so should always be called as soon after SelectMesh as possble.
void ReleaseMesh() {
selectedMesh = 0;
return;
}
};

class NiSkinInstance;
Expand Down
12 changes: 12 additions & 0 deletions include/NifFile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,18 @@ class NifFile {
// Used by OB.
NiTexturingProperty* GetTexturingProperty(NiShape* shape) const;

// Returns a mutable gometry data structure for manipulating geometry data. If
// geometry data cannot be found, nullptr is returned
NiGeometryData* GetGeometryData(NiShape* shape) const;

// Returns a list of mesh names useful for locating external mesh data eg data/geometry/<meshname>
std::vector<std::reference_wrapper<std::string>> GetExternalGeometryPathRefs(NiShape* shape) const;

// Loads external shape data from the provided fstream, storing data in the provided shape
bool LoadExternalShapeData(NiShape* shape, std::fstream& stream, uint8_t shapeIndex);
// Saves external shape data from the provided shape, storing data in the provided fstream
bool SaveExternalShapeData(NiShape* shape, std::fstream& outfile, uint8_t shapeIndex);

// Returns references to all texture path strings of the shape
std::vector<std::reference_wrapper<std::string>> GetTexturePathRefs(NiShape* shape) const;

Expand Down
160 changes: 160 additions & 0 deletions src/Geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1556,6 +1556,142 @@ void BSDynamicTriShape::Create(NiVersion& version,
}
}

void BSGeometryMeshData::Sync(NiStreamReversible& stream) {
// verts,normals, vertcolors are always present, though it's possible the counts are 0
hasVertices = true;
hasNormals = true;
hasVertexColors = true;

stream.Sync(version);

stream.Sync(nTriIndices);
tris.resize(nTriIndices/3);
for(uint32_t t=0; t<nTriIndices/3; t++) {
stream.Sync(tris[t]);
}

stream.Sync(scale);
stream.Sync(nWeightsPerVert);

stream.Sync(nVertices);
// maybe not a good idea to do the below, in case some meshes have over 65k verts, however since
// triangles still use 16 bit indices, the total count must still fit under that limit ...
numVertices = (uint16_t)nVertices;
vertices.resize(nVertices);
for(uint32_t v=0; v< nVertices; v++) {
// Traditional scale based on havok to unit transform used in skyrim, fallout, etc. In Starfield mesh files are normalized to metric units,
// this scale makes default vertex positions closely match the older games
float havokScale = 69.969;
// experimentally, the below scale produced very accurate values to SSE mesh sizes (comparing markerxheading.nif)
// float havokScale = 69.9866;
auto unpack = [&](float posScale) -> float {
int16_t val;
stream.Sync(val);
if(val<0) {
return static_cast<float>((val / 32768.0) * scale * posScale);
} else {
return static_cast<float>((val / 32767.0) * scale * posScale);
}
};
auto pack = [&](float component, float posScale) {
uint16_t val;
uint16_t factor = 32767;
if (component<0) {
factor = 32768;
}
val = (uint16_t) ((component / (scale * posScale)) * factor);
stream.Sync(val);
};
if(stream.GetMode() == NiStreamReversible::Mode::Reading) {
vertices[v].x = unpack(havokScale);
vertices[v].y = unpack(havokScale);
vertices[v].z = unpack(havokScale);
} else {
pack(vertices[v].x, havokScale);
pack(vertices[v].y, havokScale);
pack(vertices[v].z, havokScale);
}
}

uvSets.resize(2);
stream.Sync(nUV1);
uvSets[0].resize(nUV1);
for(uint32_t uv=0; uv<nUV1; uv++) {
stream.SyncHalf(uvSets[0][uv].u);
stream.SyncHalf(uvSets[0][uv].v);
}
stream.Sync(nUV2);
uvSets[1].resize(nUV2);
for(uint32_t uv=0; uv<nUV2; uv++) {
stream.SyncHalf(uvSets[1][uv].u);
stream.SyncHalf(uvSets[0][uv].v);
}

stream.Sync(nColors);
vColors.resize(nColors);
for(uint32_t c=0; c<nColors; c++) {
stream.Sync(vColors[c]);
}

stream.Sync(nNormals);
normals.resize(nNormals);
for(uint32_t n=0; n<nNormals; n++) {
stream.SyncUDEC3(normals[n]);
}

stream.Sync(nTangents);
tangents.resize(nTangents);
for(uint32_t t=0; t<nTangents; t++) {
stream.SyncUDEC3(tangents[t]);
// need to calculate tangent basis and bitangents on read?
}

stream.Sync(nTotalWeights);
if (nWeightsPerVert > 0) {
skinWeights.resize(nTotalWeights / nWeightsPerVert);
}
for(auto &vw: skinWeights) {
vw.resize(nWeightsPerVert);
for(uint32_t w=0;w<nWeightsPerVert;w++) {
boneweight bw;
bw = vw[w];
stream.Sync(bw);
vw[w] = bw;
}
}

stream.Sync(nLODS);
lodTris.resize(nLODS);
for(uint32_t lod=0; lod<nLODS; lod++) {
uint32_t nLodTriIndices = (uint32_t)lodTris[lod].size();
stream.Sync(nLodTriIndices);
lodTris[lod].resize(nLodTriIndices);
for(uint32_t t=0; t<nLodTriIndices/3; t++) {
stream.Sync(lodTris[lod][t]);
}
}

stream.Sync(nMeshlets);
meshletList.resize(nMeshlets);
for(uint32_t mi=0; mi<nMeshlets; mi++) {
meshlet m = meshletList[mi];
stream.Sync(m.vertCount);
stream.Sync(m.vertOffset);
stream.Sync(m.primCount);
stream.Sync(m.primOffset);
meshletList[mi] = m;
}

stream.Sync(nCullData);
cullDataList.resize(nCullData);
for(uint32_t ci=0; ci<nCullData; ci++) {
culldata c = cullDataList[ci];
stream.Sync(c.boundSphere);
stream.Sync(c.normalCone);
stream.Sync(c.apexOffset);
cullDataList[ci] = c;
}
}

void BSGeometryMesh::Sync(NiStreamReversible& stream) {
stream.Sync(triSize);
Expand Down Expand Up @@ -1608,6 +1744,30 @@ void BSGeometry::GetChildIndices(std::vector<uint32_t>& indices) {
}


NiGeometryData* BSGeometry::GetGeomData() const {
if (meshes.size() > selectedMesh) {
// Breaking const correctness here to cast to the desired level of the class heirarchy.
// Perhaps NiShape GetGeomData should return a const* or it shouldn't be a const function?
return dynamic_cast<NiGeometryData*>(const_cast<BSGeometryMeshData*>(&meshes[selectedMesh].meshData));
}
return nullptr;
}


bool BSGeometry::GetTriangles(std::vector<Triangle>& tris) const {
if (meshes.size() > selectedMesh) {
tris = meshes[selectedMesh].meshData.tris;
}
return false;
}

void BSGeometry::SetTriangles(const std::vector<Triangle>& tris) {
if (meshes.size() > selectedMesh) {
meshes[selectedMesh].meshData.tris = tris;
}
}


void NiGeometry::Sync(NiStreamReversible& stream) {
dataRef.Sync(stream);
skinInstanceRef.Sync(stream);
Expand Down
Loading
Loading