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

+116 -33
View File
@@ -2,44 +2,127 @@
// Ported from heurazy's mario-kart-wii-VR-port (GPL-3.0-or-later).
#pragma once
#include "vr/mkw_vr_first_person.h"
#include "vr/native_wheel_topology.h"
#include <vector>
namespace mkw::vr {
// A number of MKW karts bake the steering wheel into their single body bone.
// Find its thin disc around the authored hand targets, including the hub and
// spokes, and rotate only that disc. Work on a render copy, never guest assets.
inline unsigned RotateNativeWheelVertices(std::vector<detail::Vec3>& points,
detail::Vec3 center,float radius,float angle,const Mtx34* bodyCorrection=nullptr) {
if (!(radius>4 && radius<100) || !detail::IsFiniteFloat(&angle)) return 0;
if(bodyCorrection && !detail::IsFiniteMtx34(*bodyCorrection)) return 0;
float meanY=0,meanZ=0; unsigned count=0;
const auto candidate=[&](const detail::Vec3& p) {
return std::abs(p.x-center.x)<radius*1.5f && std::abs(p.y-center.y)<radius*1.5f &&
std::abs(p.z-center.z)<radius*0.9f;
// Hand targets locate the wheel, but are not its centre/radius: Daisy holds
// the same Standard Kart higher than Mario, and Baby Mario grips inside the
// rim. Identify a complete rim component and fit its own plane and bounds.
// Rotate whole connected pieces only, so neither a rim nor a chassis triangle
// can stretch across the selection boundary. Work on a render copy.
inline unsigned RotateNativeWheelVertices(std::vector<detail::Vec3> &points, NativeWheelTopology &topology,
detail::Vec3 gripCenter, float gripRadius, float angle,
const Mtx34 *bodyCorrection = nullptr,
const Mtx34 &bodyFromVertices = kIdentityMtx34) {
if (!(gripRadius > 4 && gripRadius < 100) || !detail::IsFiniteFloat(&angle) ||
topology.parents.size() != points.size() || topology.used.size() != points.size() ||
topology.rootOwned.size() != points.size())
return 0;
if (bodyCorrection && !detail::IsFiniteMtx34(*bodyCorrection))
return 0;
// Some karts (Baby Booster) author the body in rotated bone coordinates.
// Fit/turn in the kart frame, then convert only selected vertices back.
Mtx34 verticesFromBody;
if (!detail::IsFiniteMtx34(bodyFromVertices) || !InvertMtx(bodyFromVertices, verticesFromBody))
return 0;
auto bodyPoints = points;
for (auto &p : bodyPoints)
p = detail::TransformPoint(bodyFromVertices, p.x, p.y, p.z);
struct Piece {
detail::Vec3 min{INFINITY, INFINITY, INFINITY}, max{-INFINITY, -INFINITY, -INFINITY};
float sumY = 0, sumZ = 0;
unsigned count = 0;
bool selected = true;
};
for(const auto& p:points) if(candidate(p)) { meanY+=p.y; meanZ+=p.z; ++count; }
if(count<8) return 0;
meanY/=count; meanZ/=count;
float yy=0,yz=0;
for(const auto& p:points) if(candidate(p)) { yy+=(p.y-meanY)*(p.y-meanY); yz+=(p.y-meanY)*(p.z-meanZ); }
if(yy<radius*radius) return 0;
const float slope=std::clamp(yz/yy,-1.0f,1.0f);
center.z=meanZ+slope*(center.y-meanY);
const float inv=1/std::sqrt(1+slope*slope);
const detail::Vec3 up{0,inv,slope*inv},normal{0,-slope*inv,inv};
const float c=std::cos(angle),s=std::sin(angle);
unsigned changed=0;
for(auto& p:points) {
const detail::Vec3 delta{p.x-center.x,p.y-center.y,p.z-center.z};
const float x=delta.x,y=detail::Dot(delta,up),z=detail::Dot(delta,normal);
if(x*x+y*y>radius*radius*2.25f || std::abs(z)>radius*0.30f) continue;
const float rx=c*x-s*y,ry=s*x+c*y;
p={center.x+rx,center.y+up.y*ry+normal.y*z,center.z+up.z*ry+normal.z*z};
// The body may spin during tricks/damage while the seated reference
// stays level. Compensate only the wheel, leaving chassis animation intact.
if(bodyCorrection) p=detail::TransformPoint(*bodyCorrection,p.x,p.y,p.z);
++changed;
std::vector<Piece> pieces(points.size());
for (uint32_t i = 0; i < points.size(); ++i)
if (topology.used[i]) {
const auto &p = bodyPoints[i];
auto &piece = pieces[topology.Root(i)];
piece.min = {std::min(piece.min.x, p.x), std::min(piece.min.y, p.y), std::min(piece.min.z, p.z)};
piece.max = {std::max(piece.max.x, p.x), std::max(piece.max.y, p.y), std::max(piece.max.z, p.z)};
piece.sumY += p.y;
piece.sumZ += p.z;
++piece.count;
if (!topology.rootOwned[i])
piece.selected = false;
}
uint32_t rim = uint32_t(points.size());
float bestScore = INFINITY, rimSlope = 0, rimRadius = 0;
detail::Vec3 center{};
for (uint32_t component = 0; component < pieces.size(); ++component) {
const auto &piece = pieces[component];
const float radius = (piece.max.x - piece.min.x) * 0.5f;
const detail::Vec3 mid{(piece.min.x + piece.max.x) * 0.5f, (piece.min.y + piece.max.y) * 0.5f, 0};
if (!piece.selected || piece.count < 8 || radius < gripRadius * 0.65f || radius > gripRadius * 2.2f ||
std::abs(mid.x - gripCenter.x) > gripRadius * 0.35f || std::abs(mid.y - gripCenter.y) > gripRadius * 1.5f)
continue;
const float meanY = piece.sumY / piece.count, meanZ = piece.sumZ / piece.count;
float yy = 0, yz = 0;
for (uint32_t i = 0; i < points.size(); ++i)
if (topology.used[i] && topology.Root(i) == component) {
yy += (bodyPoints[i].y - meanY) * (bodyPoints[i].y - meanY);
yz += (bodyPoints[i].y - meanY) * (bodyPoints[i].z - meanZ);
}
if (yy < radius * radius)
continue;
const float slope = yz / yy;
if (std::abs(slope) > 1.0f)
continue;
const float inv = 1.0f / std::sqrt(1.0f + slope * slope);
const float height = (piece.max.y - piece.min.y) / inv;
const float z = meanZ + slope * (mid.y - meanY);
if (height < radius * 1.3f || height > radius * 2.6f || std::abs(z - gripCenter.z) > gripRadius)
continue;
bool planar = true;
for (uint32_t i = 0; i < points.size(); ++i)
if (topology.used[i] && topology.Root(i) == component) {
if (std::abs((bodyPoints[i].z - meanZ - slope * (bodyPoints[i].y - meanY)) * inv) > radius * 0.3f)
planar = false;
}
if (!planar)
continue;
// Prefer the enclosing rim over the smaller spoke assembly.
const float score = -radius;
if (score >= bestScore)
continue;
bestScore = score;
rim = component;
rimSlope = slope;
rimRadius = std::max(radius, height * 0.5f);
center = {mid.x, mid.y, z};
}
if (rim == points.size())
return 0;
const float inv = 1.0f / std::sqrt(1.0f + rimSlope * rimSlope);
const detail::Vec3 up{0, inv, rimSlope * inv}, normal{0, -rimSlope * inv, inv};
for (uint32_t i = 0; i < points.size(); ++i)
if (topology.used[i]) {
const auto &p = bodyPoints[i];
const detail::Vec3 delta{p.x - center.x, p.y - center.y, p.z - center.z};
const float y = detail::Dot(delta, up), z = detail::Dot(delta, normal);
// Domed hubs (Royal Racer) protrude further than the rim's thin slab.
if (delta.x * delta.x + y * y > rimRadius * rimRadius * 1.21f || std::abs(z) > rimRadius * 0.45f)
pieces[topology.Root(i)].selected = false;
}
// The entire rim is selected even if its polygonal corners exceed a circle.
pieces[rim].selected = true;
const float c = std::cos(angle), s = std::sin(angle);
unsigned changed = 0;
for (uint32_t i = 0; i < points.size(); ++i)
if (topology.used[i] && pieces[topology.Root(i)].selected) {
auto p = bodyPoints[i];
const detail::Vec3 delta{p.x - center.x, p.y - center.y, p.z - center.z};
const float y = detail::Dot(delta, up), z = detail::Dot(delta, normal);
const float rx = c * delta.x - s * y, ry = s * delta.x + c * y;
p = {center.x + rx, center.y + up.y * ry + normal.y * z, center.z + up.z * ry + normal.z * z};
if (bodyCorrection)
p = detail::TransformPoint(*bodyCorrection, p.x, p.y, p.z);
points[i] = detail::TransformPoint(verticesFromBody, p.x, p.y, p.z);
++changed;
}
return changed;
}
} // namespace mkw::vr
+174
View File
@@ -0,0 +1,174 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <numeric>
#include <vector>
namespace mkw::vr {
// Connected position indices in an MDL0 array. Material/normal/UV seams do not
// split a component; disconnected rim, spokes, column and chassis pieces do.
class NativeWheelTopology {
public:
explicit NativeWheelTopology(size_t count) : parents(count), used(count, false), rootOwned(count, true) {
std::iota(parents.begin(), parents.end(), 0u);
}
uint32_t Root(uint32_t i) {
while (parents[i] != i) {
parents[i] = parents[parents[i]];
i = parents[i];
}
return i;
}
bool Triangle(uint32_t a, uint32_t b, uint32_t c) {
if (a >= parents.size() || b >= parents.size() || c >= parents.size())
return false;
// Degenerate strip connectors must not join disconnected pieces.
if (a == b || b == c || a == c)
return true;
used[a] = used[b] = used[c] = true;
const auto root = Root(a);
parents[Root(b)] = root;
parents[Root(c)] = root;
return true;
}
// MDL0 shape primitive data uses the shape's CP VCD. Unsupported direct
// attributes/commands fail closed instead of guessing a vertex stride.
bool AddPrimitives(const uint8_t *data, size_t size, uint32_t vcdLo, uint32_t vcdHi, uint32_t fixedNode = 0) {
std::array<uint32_t, 10> nodes;
nodes.fill(UINT32_MAX);
uint32_t positionOffset = 0;
for (unsigned bit = 0; bit < 9; ++bit)
positionOffset += (vcdLo >> bit) & 1u;
const uint32_t positionType = (vcdLo >> 9) & 3u;
if (positionType < 2)
return false;
uint32_t stride = positionOffset;
for (unsigned attr = 0; attr < 12; ++attr) {
const uint32_t type = attr < 4 ? (vcdLo >> (9 + attr * 2)) & 3u : (vcdHi >> ((attr - 4) * 2)) & 3u;
if (type == 1)
return false;
if (type)
stride += type - 1;
}
const auto read16 = [](const uint8_t *p) { return (uint32_t(p[0]) << 8) | p[1]; };
size_t at = 0;
while (at < size) {
const uint8_t command = data[at++];
if (!command)
continue;
if (command == 0x20 || command == 0x28 || command == 0x30 || command == 0x38) {
if (size - at < 4)
return false;
if (command == 0x20) {
const auto address = read16(data + at + 2) & 0xfffu;
if (address % 12 || address / 12 >= nodes.size())
return false;
nodes[address / 12] = read16(data + at);
}
at += 4;
continue;
}
const auto primitive = command & 0xf8;
if (primitive != 0x80 && primitive != 0x90 && primitive != 0x98 && primitive != 0xa0)
return false;
if (size - at < 2)
return false;
const uint32_t count = read16(data + at);
at += 2;
if (count > (size - at) / stride || count < 3 || (primitive == 0x80 && count % 4) ||
(primitive == 0x90 && count % 3))
return false;
const auto index = [&](uint32_t i) {
const auto *p = data + at + size_t(i) * stride + positionOffset;
return positionType == 2 ? uint32_t(*p) : read16(p);
};
for (uint32_t i = 0; i < count; ++i) {
if (index(i) >= parents.size())
return false;
uint32_t node = fixedNode;
if (vcdLo & 1u) {
const uint32_t selector = data[at + size_t(i) * stride];
if (selector % 3 || selector / 3 >= nodes.size())
return false;
node = nodes[selector / 3];
}
if (node != 0)
rootOwned[index(i)] = false;
}
if (primitive == 0x80) {
for (uint32_t i = 0; i < count; i += 4) {
if (!Triangle(index(i), index(i + 1), index(i + 2)) ||
!Triangle(index(i), index(i + 2), index(i + 3)))
return false;
}
} else if (primitive == 0x90) {
for (uint32_t i = 0; i < count; i += 3)
if (!Triangle(index(i), index(i + 1), index(i + 2)))
return false;
} else {
for (uint32_t i = 2; i < count; ++i)
if (!Triangle(index(primitive == 0xa0 ? 0 : i - 2), index(i - 1), index(i)))
return false;
}
at += size_t(count) * stride;
}
return true;
}
std::vector<uint32_t> parents;
std::vector<bool> used;
std::vector<bool> rootOwned;
};
// MDL0 v8/9 have the shape dictionary at 0x30; v10/11 insert two fur
// dictionaries before it. All offsets below are checked within the MDL0.
inline bool ReadNativeWheelTopology(const uint8_t *mdl, size_t size, uint32_t arrayId, NativeWheelTopology &topology) {
if (!mdl || size < 0x40)
return false;
const auto read32 = [&](size_t at) {
return (uint32_t(mdl[at]) << 24) | (uint32_t(mdl[at + 1]) << 16) | (uint32_t(mdl[at + 2]) << 8) | mdl[at + 3];
};
const auto contains = [&](size_t at, size_t length) { return at <= size && length <= size - at; };
const auto version = read32(8);
if (read32(0) != 0x4d444c30 || version < 8 || version > 11 || read32(4) != size)
return false;
const size_t dictionary = read32(version >= 10 ? 0x38 : 0x30);
if (!dictionary || !contains(dictionary, 8))
return false;
const auto count = read32(dictionary + 4);
if (count > 4096 || !contains(dictionary + 8, size_t(count + 1) * 16))
return false;
bool found = false;
for (uint32_t entry = 1; entry <= count; ++entry) {
const size_t offset = read32(dictionary + 8 + entry * 16 + 12);
if (offset > size - dictionary)
return false;
const size_t shape = dictionary + offset;
if (!contains(shape, 0x60))
return false;
const auto positionId = (uint32_t(mdl[shape + 0x48]) << 8) | mdl[shape + 0x49];
if (positionId != arrayId)
continue;
// NBT triplets can carry three normal indices; do not use the ordinary
// one-index stride for them. Kart body shapes use XYZ normals.
if (((read32(shape + 0x14) >> 2) & 3u) > 1)
return false;
const size_t group = shape + 0x24, dataOffset = read32(group + 8), length = read32(group + 4);
if (dataOffset > size - group || !contains(group + dataOffset, length) || length > 0x400000)
return false;
if (!topology.AddPrimitives(mdl + group + dataOffset, length, read32(shape + 0x0c), read32(shape + 0x10),
read32(shape + 8)))
return false;
found = true;
}
return found;
}
} // namespace mkw::vr