mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 08:00:25 +02:00
Enhance native wheel handling with topology management
This commit is contained in:
1 parent
1d7f2549bd
commit
3d48514182
5 files changed
+484
-44
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user