mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +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
@@ -453,9 +453,13 @@ bike's handlebar turns with your steering: the left stick's deflection at the fu
|
|||||||
(`wheel_kart_degrees` 90, `wheel_bike_degrees` 45), eased so a flicked stick does not snap it round,
|
(`wheel_kart_degrees` 90, `wheel_bike_degrees` 45), eased so a flicked stick does not snap it round,
|
||||||
or the hands' own angle while they hold it. `native_steering_wheel = true` turns the vehicle's own
|
or the hands' own angle while they hold it. `native_steering_wheel = true` turns the vehicle's own
|
||||||
model. Karts bake the wheel into the body, so at the race draw boundary the runtime decodes the
|
model. Karts bake the wheel into the body, so at the race draw boundary the runtime decodes the
|
||||||
body's MDL0 position arrays, turns only the disc around the authored hand grips on a copy, and hands
|
body's MDL0 position arrays and shape connectivity. Hand grips locate the wheel, but its complete
|
||||||
the copy to the GX thread; Aurora substitutes it into the draws that bind that array with the
|
rim determines the rotation centre, radius and tilt: grip height/spacing varies by character.
|
||||||
player's own model-view matrix (`aurora_set_native_wheel_vertices`), checking each changed vertex's
|
Whole rim and spoke components turn together on a copy; a column or chassis component crossing
|
||||||
|
the selection stays intact. The root bone's authored transform is included when locating and
|
||||||
|
turning the wheel (the Baby Booster authors its body with rotated axes). The copy goes to the GX
|
||||||
|
thread; Aurora substitutes it into draws that bind that array with the player's model-view
|
||||||
|
matrix including the root transform (`aurora_set_native_wheel_vertices`), checking each changed vertex's
|
||||||
matrix slot, so an opponent sharing the asset and other joints of the same draw are untouched. The
|
matrix slot, so an opponent sharing the asset and other joints of the same draw are untouched. The
|
||||||
guest's own vertices are never written, and the copies are dropped after the frame's draws. Bikes
|
guest's own vertices are never written, and the copies are dropped after the frame's draws. Bikes
|
||||||
turn their handle part in the game already; its copy is only re-seated on the cockpit frame so the
|
turn their handle part in the game already; its copy is only re-seated on the cockpit frame so the
|
||||||
@@ -465,6 +469,13 @@ the copy (for 30 frames running; the race's opening pan does this) a separate VR
|
|||||||
which is also what `native_steering_wheel = false` draws. The copy keeps being published, so the
|
which is also what `native_steering_wheel = false` draws. The copy keeps being published, so the
|
||||||
vehicle's own wheel returns as soon as draws take it again, and the log notes both switches.
|
vehicle's own wheel returns as soon as draws take it again, and the log notes both switches.
|
||||||
|
|
||||||
|
Validated on the extracted PAL disc's 216 single-player kart/character and Mii combinations
|
||||||
|
(all 18 kart types): each selects the complete 21-position rim and 15-position spoke assembly,
|
||||||
|
with the remaining positions unchanged and connected-piece distances preserved. Regression tests
|
||||||
|
also cover raised/narrow grips, domed hubs, rotated roots, child joints, chassis triangles crossing
|
||||||
|
the wheel volume, degenerate strip connectors and malformed MDL0 data. This asset check does not
|
||||||
|
by itself verify every combination's live draw matching or modded vehicle models.
|
||||||
|
|
||||||
The substitution is decided per draw, and a draw that folds into a neighbour renders through that
|
The substitution is decided per draw, and a draw that folds into a neighbour renders through that
|
||||||
neighbour's array binding, so only draws that reached the same decision may merge. Deciding this
|
neighbour's array binding, so only draws that reached the same decision may merge. Deciding this
|
||||||
per array instead, and so refusing to merge every primitive that binds the vehicle's array, cost 6 ms
|
per array instead, and so refusing to merge every primitive that binds the vehicle's array, cost 6 ms
|
||||||
|
|||||||
@@ -2,44 +2,127 @@
|
|||||||
// Ported from heurazy's mario-kart-wii-VR-port (GPL-3.0-or-later).
|
// Ported from heurazy's mario-kart-wii-VR-port (GPL-3.0-or-later).
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "vr/mkw_vr_first_person.h"
|
#include "vr/mkw_vr_first_person.h"
|
||||||
|
#include "vr/native_wheel_topology.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace mkw::vr {
|
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
|
// Hand targets locate the wheel, but are not its centre/radius: Daisy holds
|
||||||
// spokes, and rotate only that disc. Work on a render copy, never guest assets.
|
// the same Standard Kart higher than Mario, and Baby Mario grips inside the
|
||||||
inline unsigned RotateNativeWheelVertices(std::vector<detail::Vec3>& points,
|
// rim. Identify a complete rim component and fit its own plane and bounds.
|
||||||
detail::Vec3 center,float radius,float angle,const Mtx34* bodyCorrection=nullptr) {
|
// Rotate whole connected pieces only, so neither a rim nor a chassis triangle
|
||||||
if (!(radius>4 && radius<100) || !detail::IsFiniteFloat(&angle)) return 0;
|
// can stretch across the selection boundary. Work on a render copy.
|
||||||
if(bodyCorrection && !detail::IsFiniteMtx34(*bodyCorrection)) return 0;
|
inline unsigned RotateNativeWheelVertices(std::vector<detail::Vec3> &points, NativeWheelTopology &topology,
|
||||||
float meanY=0,meanZ=0; unsigned count=0;
|
detail::Vec3 gripCenter, float gripRadius, float angle,
|
||||||
const auto candidate=[&](const detail::Vec3& p) {
|
const Mtx34 *bodyCorrection = nullptr,
|
||||||
return std::abs(p.x-center.x)<radius*1.5f && std::abs(p.y-center.y)<radius*1.5f &&
|
const Mtx34 &bodyFromVertices = kIdentityMtx34) {
|
||||||
std::abs(p.z-center.z)<radius*0.9f;
|
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; }
|
std::vector<Piece> pieces(points.size());
|
||||||
if(count<8) return 0;
|
for (uint32_t i = 0; i < points.size(); ++i)
|
||||||
meanY/=count; meanZ/=count;
|
if (topology.used[i]) {
|
||||||
float yy=0,yz=0;
|
const auto &p = bodyPoints[i];
|
||||||
for(const auto& p:points) if(candidate(p)) { yy+=(p.y-meanY)*(p.y-meanY); yz+=(p.y-meanY)*(p.z-meanZ); }
|
auto &piece = pieces[topology.Root(i)];
|
||||||
if(yy<radius*radius) return 0;
|
piece.min = {std::min(piece.min.x, p.x), std::min(piece.min.y, p.y), std::min(piece.min.z, p.z)};
|
||||||
const float slope=std::clamp(yz/yy,-1.0f,1.0f);
|
piece.max = {std::max(piece.max.x, p.x), std::max(piece.max.y, p.y), std::max(piece.max.z, p.z)};
|
||||||
center.z=meanZ+slope*(center.y-meanY);
|
piece.sumY += p.y;
|
||||||
const float inv=1/std::sqrt(1+slope*slope);
|
piece.sumZ += p.z;
|
||||||
const detail::Vec3 up{0,inv,slope*inv},normal{0,-slope*inv,inv};
|
++piece.count;
|
||||||
const float c=std::cos(angle),s=std::sin(angle);
|
if (!topology.rootOwned[i])
|
||||||
unsigned changed=0;
|
piece.selected = false;
|
||||||
for(auto& p:points) {
|
}
|
||||||
const detail::Vec3 delta{p.x-center.x,p.y-center.y,p.z-center.z};
|
uint32_t rim = uint32_t(points.size());
|
||||||
const float x=delta.x,y=detail::Dot(delta,up),z=detail::Dot(delta,normal);
|
float bestScore = INFINITY, rimSlope = 0, rimRadius = 0;
|
||||||
if(x*x+y*y>radius*radius*2.25f || std::abs(z)>radius*0.30f) continue;
|
detail::Vec3 center{};
|
||||||
const float rx=c*x-s*y,ry=s*x+c*y;
|
for (uint32_t component = 0; component < pieces.size(); ++component) {
|
||||||
p={center.x+rx,center.y+up.y*ry+normal.y*z,center.z+up.z*ry+normal.z*z};
|
const auto &piece = pieces[component];
|
||||||
// The body may spin during tricks/damage while the seated reference
|
const float radius = (piece.max.x - piece.min.x) * 0.5f;
|
||||||
// stays level. Compensate only the wheel, leaving chassis animation intact.
|
const detail::Vec3 mid{(piece.min.x + piece.max.x) * 0.5f, (piece.min.y + piece.max.y) * 0.5f, 0};
|
||||||
if(bodyCorrection) p=detail::TransformPoint(*bodyCorrection,p.x,p.y,p.z);
|
if (!piece.selected || piece.count < 8 || radius < gripRadius * 0.65f || radius > gripRadius * 2.2f ||
|
||||||
++changed;
|
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;
|
return changed;
|
||||||
}
|
}
|
||||||
} // namespace mkw::vr
|
} // 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
|
||||||
@@ -740,6 +740,24 @@ bool PublishNativeWheelMesh(uint32_t part, const Mtx34& model_view, const Mtx34&
|
|||||||
if (version < 8 || version > 11) {
|
if (version < 8 || version > 11) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
Mtx34 body_from_vertices = kIdentityMtx34;
|
||||||
|
Mtx34 wheel_model_view = model_view;
|
||||||
|
if (!whole_part) {
|
||||||
|
// Body::mtx is the model placement, while the root bone may have
|
||||||
|
// its own authored rotation (notably the Baby Booster). GX draws
|
||||||
|
// its positions through placement * root, so both wheel selection
|
||||||
|
// and Aurora's local-player matrix match must include that root.
|
||||||
|
const uint32_t bone_dic_offset = Memory::Read32(mdl + 0x14);
|
||||||
|
if (!bone_dic_offset || bone_dic_offset > 0x100000) return false;
|
||||||
|
const uint32_t bone_dic = mdl + bone_dic_offset;
|
||||||
|
if (!Memory::Contains(bone_dic, 40) || !Memory::Read32(bone_dic + 4)) return false;
|
||||||
|
const uint32_t bone_offset = Memory::Read32(bone_dic + 36);
|
||||||
|
if (bone_offset > 0x100000) return false;
|
||||||
|
const uint32_t root_bone = bone_dic + bone_offset;
|
||||||
|
if (!Memory::Contains(root_bone, 0xa0) || Memory::Read32(root_bone + 0x10) != 0 ||
|
||||||
|
!ReadGuestMtx34(root_bone + 0x70, body_from_vertices)) return false;
|
||||||
|
wheel_model_view = ComposeMtx(model_view, body_from_vertices);
|
||||||
|
}
|
||||||
const uint32_t dic_offset = Memory::Read32(mdl + 0x18);
|
const uint32_t dic_offset = Memory::Read32(mdl + 0x18);
|
||||||
if (dic_offset == 0 || dic_offset > 0x100000) {
|
if (dic_offset == 0 || dic_offset > 0x100000) {
|
||||||
return false;
|
return false;
|
||||||
@@ -795,8 +813,15 @@ bool PublishNativeWheelMesh(uint32_t part, const Mtx34& model_view, const Mtx34&
|
|||||||
for (auto& point : points) {
|
for (auto& point : points) {
|
||||||
point = detail::TransformPoint(correction, point.x, point.y, point.z);
|
point = detail::TransformPoint(correction, point.x, point.y, point.z);
|
||||||
}
|
}
|
||||||
} else if (RotateNativeWheelVertices(points, center, radius, angle, &correction) < 8) {
|
} else {
|
||||||
continue;
|
const uint32_t mdl_size = Memory::Read32(mdl + 4);
|
||||||
|
const uint8_t* mdl_bytes = mdl_size <= 0x1000000 ? Memory::GetPointer(mdl, mdl_size) : nullptr;
|
||||||
|
NativeWheelTopology topology(num);
|
||||||
|
if (!ReadNativeWheelTopology(mdl_bytes, mdl_size, Memory::Read32(header + 0x10), topology) ||
|
||||||
|
RotateNativeWheelVertices(points, topology, center, radius, angle, &correction,
|
||||||
|
body_from_vertices) < 8) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const uint8_t* source = Memory::GetPointer(data, size);
|
const uint8_t* source = Memory::GetPointer(data, size);
|
||||||
if (source == nullptr) {
|
if (source == nullptr) {
|
||||||
@@ -825,7 +850,7 @@ bool PublishNativeWheelMesh(uint32_t part, const Mtx34& model_view, const Mtx34&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (valid && GxNativeWheel::PostVertices(data, bytes.data(), size, model_view.data())) {
|
if (valid && GxNativeWheel::PostVertices(data, bytes.data(), size, wheel_model_view.data())) {
|
||||||
published = true;
|
published = true;
|
||||||
g_state.wheel_arrays_posted = true;
|
g_state.wheel_arrays_posted = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ using namespace mkw::vr;
|
|||||||
|
|
||||||
int g_failures = 0;
|
int g_failures = 0;
|
||||||
|
|
||||||
void Check(bool condition, const char* what) {
|
void Check(bool condition, const char *what) {
|
||||||
if (!condition) {
|
if (!condition) {
|
||||||
++g_failures;
|
++g_failures;
|
||||||
std::cerr << "FAILED: " << what << '\n';
|
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)) {
|
if (!(std::fabs(actual - expected) <= tolerance)) {
|
||||||
++g_failures;
|
++g_failures;
|
||||||
std::cerr << "FAILED: " << what << " (expected " << expected << ", got " << actual << ")\n";
|
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({100.0f, 0.0f, 0.0f});
|
||||||
points.push_back({0.0f, 50.0f, 200.0f});
|
points.push_back({0.0f, 50.0f, 200.0f});
|
||||||
const auto original = points;
|
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");
|
Check(changed == 64, "every disc vertex turns");
|
||||||
CheckNear(points[64].x, original[64].x, "chassis vertex untouched");
|
CheckNear(points[64].x, original[64].x, "chassis vertex untouched");
|
||||||
CheckNear(points[65].z, original[65].z, "vertex off the disc plane 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);
|
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);
|
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");
|
NativeWheelTopology sparseTopology(sparse.size());
|
||||||
Check(RotateNativeWheelVertices(points, {0, 50, 60}, 2.0f, 0.5f) == 0, "an implausible radius leaves the mesh");
|
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
|
} // namespace
|
||||||
@@ -203,6 +348,8 @@ int main() {
|
|||||||
TestWheelGeometry();
|
TestWheelGeometry();
|
||||||
TestStabilizer();
|
TestStabilizer();
|
||||||
TestNativeWheelVertices();
|
TestNativeWheelVertices();
|
||||||
|
TestNativeWheelWithRaisedGrips();
|
||||||
|
TestNativeWheelTopology();
|
||||||
if (g_failures != 0) {
|
if (g_failures != 0) {
|
||||||
std::cerr << g_failures << " check(s) failed\n";
|
std::cerr << g_failures << " check(s) failed\n";
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
Reference in new issue
Block a user