Enhance native wheel handling with topology management

This commit is contained in:
iChris4 committed 2026-09-25 02:21:10 +02:00
1 parent 1d7f2549bd
commit 3d48514182
5 files changed
+484 -44

No files matched your search

+152 -5
View File
@@ -20,14 +20,14 @@ using namespace mkw::vr;
int g_failures = 0;
void Check(bool condition, const char* what) {
void Check(bool condition, const char *what) {
if (!condition) {
++g_failures;
std::cerr << "FAILED: " << what << '\n';
}
}
void CheckNear(float actual, float expected, const char* what, float tolerance = 1.0e-3f) {
void CheckNear(float actual, float expected, const char *what, float tolerance = 1.0e-3f) {
if (!(std::fabs(actual - expected) <= tolerance)) {
++g_failures;
std::cerr << "FAILED: " << what << " (expected " << expected << ", got " << actual << ")\n";
@@ -181,7 +181,10 @@ void TestNativeWheelVertices() {
points.push_back({100.0f, 0.0f, 0.0f});
points.push_back({0.0f, 50.0f, 200.0f});
const auto original = points;
const unsigned changed = RotateNativeWheelVertices(points, {0, 50, 60}, 20.0f, 0.5f);
NativeWheelTopology topology(points.size());
for (uint32_t i = 2; i < 64; ++i)
topology.Triangle(0, i - 1, i);
const unsigned changed = RotateNativeWheelVertices(points, topology, {0, 50, 60}, 20.0f, 0.5f);
Check(changed == 64, "every disc vertex turns");
CheckNear(points[64].x, original[64].x, "chassis vertex untouched");
CheckNear(points[65].z, original[65].z, "vertex off the disc plane untouched");
@@ -190,8 +193,150 @@ void TestNativeWheelVertices() {
CheckNear(std::hypot(points[i].x, points[i].y - 50.0f), 20.0f, "disc vertex stays on the rim", 1e-2f);
}
auto sparse = std::vector<detail::Vec3>(points.begin(), points.begin() + 4);
Check(RotateNativeWheelVertices(sparse, {0, 50, 60}, 20.0f, 0.5f) == 0, "too few candidates leaves the mesh");
Check(RotateNativeWheelVertices(points, {0, 50, 60}, 2.0f, 0.5f) == 0, "an implausible radius leaves the mesh");
NativeWheelTopology sparseTopology(sparse.size());
Check(RotateNativeWheelVertices(sparse, sparseTopology, {0, 50, 60}, 20.0f, 0.5f) == 0,
"too few candidates leaves the mesh");
Check(RotateNativeWheelVertices(points, topology, {0, 50, 60}, 2.0f, 0.5f) == 0,
"an implausible radius leaves the mesh");
}
void TestNativeWheelWithRaisedGrips() {
// The same tilted wheel is gripped near its centre by one driver and near
// its upper rim by another. Uneven spoke density must not move the pivot.
constexpr float radius = 17.0f, slope = 0.3f;
const float inv = 1.0f / std::sqrt(1.0f + slope * slope);
const detail::Vec3 center{0, 28, -9};
std::vector<detail::Vec3> original;
for (int i = 0; i < 64; ++i) {
const float a = float(i) * 6.2831853f / 64.0f;
const float y = radius * std::sin(a);
original.push_back({radius * std::cos(a), center.y + inv * y, center.z + slope * inv * y});
}
for (int i = 0; i < 12; ++i) {
original.push_back({float(i % 3) - 1.0f, center.y + 5.0f, center.z + slope * 5.0f});
}
const auto wheelCount = original.size();
// Inside the broad search box, but off the wheel plane: the chassis must
// neither bias the fit nor be pulled along with the wheel.
original.push_back({-18.0f, 8.0f, 6.0f});
original.push_back(center); // A chassis triangle crosses the wheel volume.
original.push_back({18.0f, 9.0f, 6.0f});
NativeWheelTopology topology(original.size());
for (uint32_t i = 2; i < 64; ++i)
topology.Triangle(0, i - 1, i);
for (uint32_t i = 66; i < wheelCount; ++i)
topology.Triangle(64, i - 1, i);
topology.Triangle(wheelCount, wheelCount + 1, wheelCount + 2);
for (float angle : {-0.7f, 0.7f}) {
auto lowerGrip = original, raisedGrip = original;
Check(RotateNativeWheelVertices(lowerGrip, topology, {0, 27, -5}, 13.0f, angle) == wheelCount,
"all wheel vertices turn with lower grips");
Check(RotateNativeWheelVertices(raisedGrip, topology, {0, 36.7f, -5.9f}, 13.0f, angle) == wheelCount,
"raised grips still turn the entire lower rim");
for (size_t i = 0; i < wheelCount; ++i) {
CheckNear(raisedGrip[i].x, lowerGrip[i].x, "driver hand height does not change wheel rotation X");
CheckNear(raisedGrip[i].y, lowerGrip[i].y, "driver hand height does not change wheel rotation Y");
CheckNear(raisedGrip[i].z, lowerGrip[i].z, "driver hand height does not change wheel rotation Z");
const float x = original[i].x, y = (original[i].y - center.y) / inv;
const float rx = std::cos(angle) * x - std::sin(angle) * y;
const float ry = std::sin(angle) * x + std::cos(angle) * y;
CheckNear(raisedGrip[i].x, rx, "wheel rotates rigidly about its geometric centre X");
CheckNear(raisedGrip[i].y, center.y + inv * ry, "wheel rotates rigidly about its geometric centre Y");
CheckNear(raisedGrip[i].z, center.z + slope * inv * ry, "wheel rotates rigidly in its tilted plane");
}
for (size_t i = wheelCount; i < original.size(); ++i) {
CheckNear(raisedGrip[i].x, original[i].x, "nearby chassis X untouched");
CheckNear(raisedGrip[i].y, original[i].y, "nearby chassis Y untouched");
CheckNear(raisedGrip[i].z, original[i].z, "nearby chassis Z untouched");
}
}
auto corrected = original;
const auto correction = Translation(2, 3, 4);
Check(RotateNativeWheelVertices(corrected, topology, {0, 36.7f, -5.9f}, 13.0f, 0.0f, &correction) == wheelCount,
"the complete wheel also receives cockpit stabilization");
for (size_t i = 0; i < wheelCount; ++i) {
CheckNear(corrected[i].y, original[i].y + 3.0f, "lower rim receives body correction");
}
for (size_t i = wheelCount; i < original.size(); ++i) {
CheckNear(corrected[i].y, original[i].y, "chassis does not receive wheel stabilization");
}
auto narrowGrip = original;
Check(RotateNativeWheelVertices(narrowGrip, topology, {0, 36.7f, -5.9f}, 9.0f, 0.7f) == wheelCount,
"hands inside a wide rim still select the entire wheel");
// Baby Booster's root exchanges the authored lateral/vertical axes.
const Mtx34 bodyFromVertices{0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0};
Mtx34 verticesFromBody;
Check(InvertMtx(bodyFromVertices, verticesFromBody), "authored body basis is invertible");
auto authored = original, expected = original;
for (auto &p : authored)
p = detail::TransformPoint(verticesFromBody, p.x, p.y, p.z);
Check(RotateNativeWheelVertices(authored, topology, {0, 36.7f, -5.9f}, 13.0f, 0.7f, &correction,
bodyFromVertices) == wheelCount,
"a rotated root bone does not hide the wheel");
RotateNativeWheelVertices(expected, topology, {0, 36.7f, -5.9f}, 13.0f, 0.7f, &correction);
for (size_t i = 0; i < original.size(); ++i) {
const auto p = detail::TransformPoint(bodyFromVertices, authored[i].x, authored[i].y, authored[i].z);
CheckNear(p.x, expected[i].x, "authored basis preserves rotation and stabilization X");
CheckNear(p.y, expected[i].y, "authored basis preserves rotation and stabilization Y");
CheckNear(p.z, expected[i].z, "authored basis preserves rotation and stabilization Z");
}
auto domed = original;
for (size_t i = 64; i < wheelCount; ++i) {
domed[i].y -= slope * inv * radius * 0.37f;
domed[i].z += inv * radius * 0.37f;
}
Check(RotateNativeWheelVertices(domed, topology, {0, 36.7f, -5.9f}, 13.0f, 0.7f) == wheelCount,
"a domed hub turns with the rim");
topology.rootOwned[0] = false;
auto foreignJoint = original;
Check(RotateNativeWheelVertices(foreignJoint, topology, {0, 36.7f, -5.9f}, 13.0f, 0.7f) == 0,
"geometry on another animated joint cannot be mistaken for the wheel");
}
void TestNativeWheelTopology() {
const uint8_t strip[]{0x98, 0, 8, 0, 1, 2, 2, 3, 3, 4, 5};
NativeWheelTopology topology(6);
Check(topology.AddPrimitives(strip, sizeof(strip), 2u << 9, 0), "decode an indexed strip");
Check(topology.Root(0) == topology.Root(2) && topology.Root(3) == topology.Root(5),
"strip triangles connect their positions");
Check(topology.Root(0) != topology.Root(3), "degenerate strip connectors do not join pieces");
Check(!topology.AddPrimitives(strip, sizeof(strip) - 1, 2u << 9, 0), "truncated primitive rejected");
Check(!topology.AddPrimitives(strip, sizeof(strip), 1u << 9, 0), "unsupported direct positions rejected");
NativeWheelTopology tooSmall(5);
Check(!tooSmall.AddPrimitives(strip, sizeof(strip), 2u << 9, 0), "out-of-range position rejected");
const uint8_t quads[]{0x80, 0, 4, 0, 0, 0, 1, 0, 2, 0, 3};
NativeWheelTopology quad(4);
Check(quad.AddPrimitives(quads, sizeof(quads), 3u << 9, 0) && quad.Root(0) == quad.Root(3),
"16-bit quad positions connect both triangles");
const uint8_t indexed[]{0x20, 0, 0, 0xb0, 0, 0x20, 0, 1, 0xb0, 12, 0x90, 0, 3, 0, 0, 0, 1, 3, 2};
NativeWheelTopology joints(3);
Check(joints.AddPrimitives(indexed, sizeof(indexed), (2u << 9) | 1u, 0), "decode indexed bone ownership");
Check(joints.rootOwned[0] && joints.rootOwned[1] && !joints.rootOwned[2],
"matrix loads distinguish the body from an animated child joint");
// Minimal MDL0 exercising shape offsets, array IDs and bounds without game assets.
std::vector<uint8_t> mdl(320, 0);
const auto put32 = [&](size_t at, uint32_t value) {
for (unsigned i = 0; i < 4; ++i)
mdl[at + i] = uint8_t(value >> ((3 - i) * 8));
};
put32(0, 0x4d444c30);
put32(4, uint32_t(mdl.size()));
put32(8, 11);
put32(0x38, 64);
put32(68, 1);
put32(100, 40);
constexpr size_t shape = 104;
put32(shape + 0x0c, 3u << 9);
put32(shape + 0x28, sizeof(quads));
put32(shape + 0x2c, 256 - (shape + 0x24));
std::copy(std::begin(quads), std::end(quads), mdl.begin() + 256);
NativeWheelTopology model(4);
Check(ReadNativeWheelTopology(mdl.data(), mdl.size(), 0, model), "MDL0 shape topology decoded");
Check(!ReadNativeWheelTopology(mdl.data(), mdl.size(), 1, model), "unrelated position array ignored");
put32(shape + 0x2c, UINT32_MAX);
Check(!ReadNativeWheelTopology(mdl.data(), mdl.size(), 0, model), "escaping primitive offset rejected");
}
} // namespace
@@ -203,6 +348,8 @@ int main() {
TestWheelGeometry();
TestStabilizer();
TestNativeWheelVertices();
TestNativeWheelWithRaisedGrips();
TestNativeWheelTopology();
if (g_failures != 0) {
std::cerr << g_failures << " check(s) failed\n";
return 1;