Implement Bullet Bill model visibility and cockpit recentering improvements

- Added support for hiding model arrays in the rendering pipeline to optimize performance during VR gameplay.
- Introduced new functions in `gx_model_visibility` to manage hidden model arrays and their visibility based on game state.
- Enhanced cockpit recentering logic to ensure accurate seat measurements and eye positioning during gameplay.
- Updated `FirstPersonState` to track cockpit height and forward direction, improving VR experience.
- Added tests for Bullet Bill model visibility and cockpit height adjustments to ensure functionality and stability.
- Refactored existing code to accommodate new features and improve overall code organization.
This commit is contained in:
iChris4 committed 2026-09-28 23:21:01 +02:00
1 parent 8c5a7126da
commit 3c20f677eb
17 files changed
+592 -38

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace mkw::vr {
struct BulletBillBodyArray {
uint32_t offset = 0;
uint32_t size = 0;
};
// kart_killer's body, eye and exhaust shapes are rigid on matrix 0. Its arms
// use other matrices. Select only position arrays used exclusively by rigid
// body shapes, so even a mod sharing positions with an arm keeps those draws.
// Offsets are relative to the MDL0; no names or game assets are embedded here.
inline std::vector<BulletBillBodyArray> ReadBulletBillBodyArrays(const uint8_t* mdl, size_t size) {
if (!mdl || size < 0x40) return {};
const auto contains = [size](size_t at, size_t length) { return at <= size && length <= size - at; };
const auto read32 = [mdl](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 read16 = [mdl](size_t at) { return (uint32_t(mdl[at]) << 8) | mdl[at + 1]; };
const uint32_t version = read32(8);
if (read32(0) != 0x4d444c30 || read32(4) != size || version < 8 || version > 11) return {};
const size_t positions = read32(0x18), shapes = read32(version >= 10 ? 0x38 : 0x30);
if (!positions || !shapes || !contains(positions, 8) || !contains(shapes, 8)) return {};
const uint32_t positionCount = read32(positions + 4), shapeCount = read32(shapes + 4);
if (positionCount > 64 || shapeCount > 4096 ||
!contains(positions + 8, size_t(positionCount + 1) * 16) ||
!contains(shapes + 8, size_t(shapeCount + 1) * 16)) return {};
std::vector<BulletBillBodyArray> result;
for (uint32_t i = 1; i <= positionCount; ++i) {
const size_t array = positions + size_t(read32(positions + 8 + i * 16 + 12));
if (!contains(array, 0x38)) return {};
const uint32_t id = read32(array + 0x10);
bool body = false, other = false;
for (uint32_t j = 1; j <= shapeCount; ++j) {
const size_t shape = shapes + size_t(read32(shapes + 8 + j * 16 + 12));
if (!contains(shape, 0x60)) return {};
if (read16(shape + 0x48) != id) continue;
const bool rigidBody = read32(shape + 8) == 0 && (read32(shape + 0xc) & 1u) == 0;
body |= rigidBody;
other |= !rigidBody;
}
if (!body || other) continue;
const size_t data = array + size_t(read32(array + 8));
const uint32_t bytes = uint32_t(mdl[array + 0x1d]) * read16(array + 0x1e);
if (!bytes || !contains(data, bytes)) return {};
result.push_back({static_cast<uint32_t>(data), bytes});
}
return result;
}
} // namespace mkw::vr
+62 -17
View File
@@ -206,6 +206,17 @@ inline float EyeBehindControls(float eyeForward, float controlsForward, float un
return std::min(eyeForward, controlsForward - clearance);
}
// Once the eye is pulled behind the controls, a long neck or snout must not
// leave them down at the player's knees. Keep the measured character scale,
// but cap the seated eye at 40 cm above the neutral hand targets.
inline float EyeAboveControls(float eyeHeight, float controlsHeight, float units) noexcept {
if (!detail::IsFiniteFloat(&controlsHeight) || !detail::IsFiniteFloat(&units) || units <= 0.0f) {
return eyeHeight;
}
const float limit = controlsHeight + 0.40f * units;
return limit >= 5.0f ? std::min(eyeHeight, limit) : eyeHeight;
}
// Tall characters sit higher; normalise them to a comfortable perceived cockpit
// height by growing the world scale with the measured eye height.
inline float CharacterCockpitScale(float eyeHeight) noexcept {
@@ -237,6 +248,15 @@ inline bool NeutralPlayerScale(const std::array<float, 3>& scale) noexcept {
return true;
}
inline bool ValidSeatedEye(const std::array<float, 3>& eye) noexcept {
for (const auto& value : eye) {
if (!detail::IsFiniteFloat(&value) || std::abs(value) > 500.0f) {
return false;
}
}
return eye[1] >= 5.0f;
}
// Eye position resources are in the face bone's local coordinates, whose axes
// differ between characters. Transform their centre through the complete bind
// matrix before applying the vehicle-specific driver placement.
@@ -258,12 +278,7 @@ inline bool ComputeDriverEyeFromBounds(const Mtx34& face, const Mtx34& placement
(minimum.z + maximum.z) * 0.5f);
const auto seat = detail::TransformPoint(placement, model.x, model.y, model.z);
const std::array<float, 3> result{seat.x, seat.y, seat.z};
for (const auto& value : result) {
if (!detail::IsFiniteFloat(&value) || std::abs(value) > 500.0f) {
return false;
}
}
if (seat.y < 5.0f) {
if (!ValidSeatedEye(result)) {
return false;
}
eye = result;
@@ -288,29 +303,50 @@ inline bool ComputeSeatedEye(const Mtx34& faceWorld, const Mtx34& bodyWorld, det
const detail::Vec3 delta{world.x - bodyWorld[3], world.y - bodyWorld[7], world.z - bodyWorld[11]};
const std::array<float, 3> result{detail::Dot(bc, delta) / det, detail::Dot(ca, delta) / det,
detail::Dot(ab, delta) / det};
for (const auto& value : result) {
if (!detail::IsFiniteFloat(&value) || std::abs(value) > 500.0f) {
return false;
}
}
if (result[1] < 5.0f) {
if (!ValidSeatedEye(result)) {
return false;
}
eye = result;
return true;
}
// Mods may have no separately named eye geometry. Estimate just above/ahead
// of the evaluated head, in vehicle axes, not the head bone's rotated axes.
// The animated pose includes the character animation's scale and placement;
// adding the driver's placement again would put some mods below the vehicle.
inline bool ComputeDriverEyeFromHead(const Mtx34& headWorld, const Mtx34& bodyWorld,
std::array<float, 3>& eye) noexcept {
std::array<float, 3> head{};
if (!ComputeSeatedEye(headWorld, bodyWorld, {0, 0, 0}, head)) {
return false;
}
head[1] += 8.0f;
head[2] += 8.0f;
if (!ValidSeatedEye(head)) {
return false;
}
eye = head;
return true;
}
// The neutral seated eye, accepted once eight consecutive safe samples agree
// within two units, then frozen until the driver or the race changes.
// within two units of the first sample, then frozen until the driver or the
// race changes, or a recenter asks for another calibration. Keep the previous
// seat while waiting for a safe replacement.
struct SeatedEyeReference {
std::array<float, 3> value{}, candidate{};
unsigned stable = 0;
bool valid = false;
bool recalibrating = false;
void Recalibrate() noexcept {
stable = 0;
recalibrating = true;
}
void Observe(const std::array<float, 3>& sample, bool safe, bool freeze) {
if (freeze && valid) {
if (freeze && valid && !recalibrating) {
return;
}
if (!safe) {
if (!safe || !ValidSeatedEye(sample)) {
stable = 0;
return;
}
@@ -318,11 +354,16 @@ struct SeatedEyeReference {
for (int i = 0; i < 3; ++i) {
delta = std::max(delta, std::abs(sample[i] - candidate[i]));
}
stable = stable && delta < 2.0f ? stable + 1 : 1;
candidate = sample;
if (stable && delta < 2.0f) {
++stable;
} else {
stable = 1;
candidate = sample;
}
if (stable >= 8) {
value = sample;
valid = true;
recalibrating = false;
stable = 8;
}
}
@@ -521,6 +562,10 @@ void MkwVRFirstPersonCommit() noexcept;
// Drops every captured pointer and the held anchor. Call on race entry/exit.
void MkwVRFirstPersonReset() noexcept;
// Thread-safe request; the guest thread remeasures the cockpit on subsequent
// neutral frames. Keeps the current seat until a replacement is ready.
void MkwVRFirstPersonRecenter() noexcept;
// Producer-side read. Thread-safe. A valid anchor is also what marks the mode
// as engaged, and so what selects the first-person world scale: it is invalid
// whenever the mode is off, the race has not produced a usable anchor, or the