mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-22 12:42:49 +00:00
fix(nif): leave out a root node's rotation and translation
The client's render component sets the object's own position and rotation on the root node it loads, over the stored ones, so a root's turn never shows in game (its scale stays). NifFile now reads roots the same way. LU Toolbox's .nif (root turned 90 degrees about X, the NiLODNodes turned back) now stands up in icons and the dashboard's views; every game .nif has an unturned root and reads as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -387,7 +387,14 @@ namespace {
|
|||||||
reader.Skip(static_cast<uint64_t>(effectCount) * 4);
|
reader.Skip(static_cast<uint64_t>(effectCount) * 4);
|
||||||
if (!reader.Ok()) return;
|
if (!reader.Ok()) return;
|
||||||
|
|
||||||
const auto world = parent.Then(av.transform);
|
// The client puts the object's own position and rotation in place of the root node's (the render component
|
||||||
|
// sets them on the loaded root), so a root's stored rotation and translation are never seen; its scale is kept
|
||||||
|
auto local = av.transform;
|
||||||
|
if (depth == 0) {
|
||||||
|
local.r = Transform{}.r;
|
||||||
|
local.t = {};
|
||||||
|
}
|
||||||
|
const auto world = parent.Then(local);
|
||||||
// Recorded even when hidden: attach points often are
|
// Recorded even when hidden: attach points often are
|
||||||
if (!av.name.empty() && !m_Model.nodes.contains(av.name)) {
|
if (!av.name.empty() && !m_Model.nodes.contains(av.name)) {
|
||||||
NifFile::NodeTransform node;
|
NifFile::NodeTransform node;
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
* the NifTools project's format description (nif.xml). Pure (bytes in, meshes out) so it can be unit tested.
|
* the NifTools project's format description (nif.xml). Pure (bytes in, meshes out) so it can be unit tested.
|
||||||
*
|
*
|
||||||
* The client's files are versions 20.2.0.8 and 20.3.0.9 with user version 0. What is drawn: the scene graph
|
* The client's files are versions 20.2.0.8 and 20.3.0.9 with user version 0. What is drawn: the scene graph
|
||||||
* (NiNode, NiLODNode, NiBillboardNode and other nodes) with its transforms baked into the vertices, NiTriShape and
|
* (NiNode, NiLODNode, NiBillboardNode and other nodes) with its transforms baked into the vertices (except a root
|
||||||
|
* node's rotation and translation, which the client replaces with the object's own), NiTriShape and
|
||||||
* NiTriStrips geometry (positions, normals, the first UV set, vertex colors), and the properties Gamebryo passes down
|
* NiTriStrips geometry (positions, normals, the first UV set, vertex colors), and the properties Gamebryo passes down
|
||||||
* the tree: NiMaterialProperty, NiAlphaProperty, NiTexturingProperty's base texture (an external NiSourceTexture),
|
* the tree: NiMaterialProperty, NiAlphaProperty, NiTexturingProperty's base texture (an external NiSourceTexture),
|
||||||
* NiVertexColorProperty and NiStencilProperty's draw mode (double sided), and how fast NiTextureTransformControllers move
|
* NiVertexColorProperty and NiStencilProperty's draw mode (double sided), and how fast NiTextureTransformControllers move
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#include "NifFile.h"
|
#include "NifFile.h"
|
||||||
@@ -152,9 +153,16 @@ TEST(NifFileTests, ReadsATriangleWithItsVertexData) {
|
|||||||
|
|
||||||
TEST(NifFileTests, BakesNodeTransformsIntoVertices) {
|
TEST(NifFileTests, BakesNodeTransformsIntoVertices) {
|
||||||
NifBuilder nif;
|
NifBuilder nif;
|
||||||
// 90 degrees about y (x goes to -z), then scaled by 2 and moved 10 along x
|
// A node under the root: 90 degrees about y (x goes to -z), then scaled by 2 and moved 10 along x
|
||||||
const std::array<float, 9> yaw{ 0, 0, 1, 0, 1, 0, -1, 0, 0 };
|
const std::array<float, 9> yaw{ 0, 0, 1, 0, 1, 0, -1, 0, 0 };
|
||||||
const auto file = OneTriangle(nif, Av(0, { 10, 0, 0 }, yaw, 2.0f, {}));
|
const auto root = nif.Add("NiNode", {});
|
||||||
|
const auto node = nif.Add("NiNode", {});
|
||||||
|
const auto shape = nif.Add("NiTriShape", {});
|
||||||
|
const auto data = nif.Add("NiTriShapeData", TriShapeData());
|
||||||
|
nif.Set(root, Node(Av(0, { 0, 0, 0 }, IDENTITY, 1.0f, {}), { node }));
|
||||||
|
nif.Set(node, Node(Av(0, { 10, 0, 0 }, yaw, 2.0f, {}), { shape }));
|
||||||
|
nif.Set(shape, Geometry(Av(0, { 0, 0, 0 }, IDENTITY, 1.0f, {}), data));
|
||||||
|
const auto file = nif.Build();
|
||||||
std::string error;
|
std::string error;
|
||||||
const auto model = NifFile::Parse(file, 0, error);
|
const auto model = NifFile::Parse(file, 0, error);
|
||||||
ASSERT_TRUE(model) << error;
|
ASSERT_TRUE(model) << error;
|
||||||
@@ -168,6 +176,45 @@ TEST(NifFileTests, BakesNodeTransformsIntoVertices) {
|
|||||||
EXPECT_NEAR(n[2], 0.0f, 1e-5);
|
EXPECT_NEAR(n[2], 0.0f, 1e-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(NifFileTests, LeavesOutTheRootsRotationAndTranslation) {
|
||||||
|
// The client sets the object's own position and rotation on the root node it loads, so the root's stored ones
|
||||||
|
// never show; its scale does
|
||||||
|
NifBuilder nif;
|
||||||
|
const std::array<float, 9> yaw{ 0, 0, 1, 0, 1, 0, -1, 0, 0 };
|
||||||
|
const auto file = OneTriangle(nif, Av(0, { 10, 0, 0 }, yaw, 2.0f, {}));
|
||||||
|
std::string error;
|
||||||
|
const auto model = NifFile::Parse(file, 0, error);
|
||||||
|
ASSERT_TRUE(model) << error;
|
||||||
|
EXPECT_EQ(model->meshes.at(0).positions, (std::vector<float>{ 0, 0, 0, 2, 0, 0, 0, 2, 0 }));
|
||||||
|
EXPECT_EQ(model->meshes[0].normals[2], 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NifFileTests, StandsUpAModelWhoseRootIsTurned) {
|
||||||
|
// Laid out as LU Toolbox's .nif (niftools from Blender, Z up): the root turned 90 degrees about x, the NiLODNode
|
||||||
|
// turned back and the shape turned again, so without the root's turn the vertices stay as stored (Y up)
|
||||||
|
NifBuilder nif;
|
||||||
|
const std::array<float, 9> up{ 1, 0, 0, 0, 0, -1, 0, 1, 0 }; // +90 about x: y goes to z, z to -y
|
||||||
|
const std::array<float, 9> down{ 1, 0, 0, 0, 0, 1, 0, -1, 0 }; // -90 about x
|
||||||
|
const auto root = nif.Add("NiNode", {});
|
||||||
|
const auto lodNode = nif.Add("NiLODNode", {});
|
||||||
|
const auto level = nif.Add("NiNode", {});
|
||||||
|
const auto shape = nif.Add("NiTriShape", {});
|
||||||
|
const auto data = nif.Add("NiTriShapeData", TriShapeData());
|
||||||
|
const auto ranges = nif.Add("NiRangeLODData", Bytes{}.Floats({ 0, 0, 0 }).Put<uint32_t>(1).Floats({ 0, 100 }));
|
||||||
|
nif.Set(root, Node(Av(0, { 0, 0, 0 }, down, 1.0f, {}), { lodNode }));
|
||||||
|
nif.Set(lodNode, Node(Av(0, { 0, 0, 0 }, up, 1.0f, {}), { level }).Put<uint16_t>(3).Put<uint32_t>(0).Put(ranges));
|
||||||
|
nif.Set(level, Node(Av(0, { 0, 0, 0 }, IDENTITY, 1.0f, {}), { shape }));
|
||||||
|
nif.Set(shape, Geometry(Av(0, { 0, 0, 0 }, down, 1.0f, {}), data));
|
||||||
|
std::string error;
|
||||||
|
const auto model = NifFile::Parse(nif.Build(), 0, error);
|
||||||
|
ASSERT_TRUE(model) << error;
|
||||||
|
const auto& p = model->meshes.at(0).positions;
|
||||||
|
// The vertex (0, 1, 0) still points up (with the root's turn it would lie along -z)
|
||||||
|
EXPECT_NEAR(p[6], 0.0f, 1e-6);
|
||||||
|
EXPECT_NEAR(p[7], 1.0f, 1e-6);
|
||||||
|
EXPECT_NEAR(p[8], 0.0f, 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(NifFileTests, SkipsHiddenSubtrees) {
|
TEST(NifFileTests, SkipsHiddenSubtrees) {
|
||||||
NifBuilder nif;
|
NifBuilder nif;
|
||||||
const auto file = OneTriangle(nif, Av(1, { 0, 0, 0 }, IDENTITY, 1.0f, {}));
|
const auto file = OneTriangle(nif, Av(1, { 0, 0, 0 }, IDENTITY, 1.0f, {}));
|
||||||
@@ -551,15 +598,41 @@ TEST(NifFileTests, EncodesEachMeshsLook) {
|
|||||||
|
|
||||||
// The game client's own meshes, when a client is configured (DLU_CLIENT_RES, else client_location in the build's
|
// The game client's own meshes, when a client is configured (DLU_CLIENT_RES, else client_location in the build's
|
||||||
// sharedconfig.ini): the first 300 .nif files under res/mesh/env read, and most have something to draw
|
// sharedconfig.ini): the first 300 .nif files under res/mesh/env read, and most have something to draw
|
||||||
TEST(NifFileTests, ReadsTheClientsMeshes) {
|
namespace {
|
||||||
std::filesystem::path res;
|
// The game client's res folder (DLU_CLIENT_RES, else the build's sharedconfig.ini), empty when there is none
|
||||||
if (const char* env = std::getenv("DLU_CLIENT_RES")) res = env;
|
std::filesystem::path ClientRes() {
|
||||||
else {
|
std::filesystem::path res;
|
||||||
std::ifstream config(std::filesystem::path(DLU_SOURCE_DIR) / "build" / "sharedconfig.ini");
|
if (const char* env = std::getenv("DLU_CLIENT_RES")) res = env;
|
||||||
for (std::string line; std::getline(config, line);) {
|
else {
|
||||||
if (line.starts_with("client_location=")) res = std::filesystem::path(line.substr(16)) / "res";
|
std::ifstream config(std::filesystem::path(DLU_SOURCE_DIR) / "build" / "sharedconfig.ini");
|
||||||
|
for (std::string line; std::getline(config, line);) {
|
||||||
|
if (line.starts_with("client_location=")) res = std::filesystem::path(line.substr(16)) / "res";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NifFileTests, ReadsAClientModelWhereItStands) {
|
||||||
|
// A game model (the pirate raft reward) reads where it stood before roots' turns were left out: upright on y 0
|
||||||
|
const auto path = ClientRes() / "mesh" / "reward" / "rew_pirate-raft.nif";
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file) GTEST_SKIP() << "No game client configured";
|
||||||
|
const std::string data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||||
|
std::string error;
|
||||||
|
const auto model = NifFile::Parse(data, 0, error);
|
||||||
|
ASSERT_TRUE(model) << error;
|
||||||
|
ASSERT_EQ(model->meshes.size(), 1u);
|
||||||
|
EXPECT_NEAR(model->min[0], -1.6f, 1e-4);
|
||||||
|
EXPECT_NEAR(model->min[1], 0.0f, 1e-4);
|
||||||
|
EXPECT_NEAR(model->min[2], -3.32814f, 1e-4);
|
||||||
|
EXPECT_NEAR(model->max[0], 1.6f, 1e-4);
|
||||||
|
EXPECT_NEAR(model->max[1], 5.12f, 1e-4);
|
||||||
|
EXPECT_NEAR(model->max[2], 4.04143f, 1e-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NifFileTests, ReadsTheClientsMeshes) {
|
||||||
|
const auto res = ClientRes();
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
const auto folder = res / "mesh" / "env";
|
const auto folder = res / "mesh" / "env";
|
||||||
if (res.empty() || !std::filesystem::is_directory(folder, ec)) GTEST_SKIP() << "No game client configured";
|
if (res.empty() || !std::filesystem::is_directory(folder, ec)) GTEST_SKIP() << "No game client configured";
|
||||||
|
|||||||
Reference in New Issue
Block a user