mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 04:04:18 +02:00
Add the cockpit, steering-wheel and hand-steering building blocks
The pure pieces of the first-person cockpit and hand steering, ported from
heurazy's mario-kart-wii-VR-port: the SteeringWheel grab/turn model, the
native wheel vertex rotation, the level seat stabiliser, the seated-eye and
wheel/handlebar geometry, and the XR_FB_hand_tracking_mesh loader. Adds
openxr_driving.h, the OpenXR-free snapshot the pacing thread will publish for
the guest thread, with the hand-off rule (a held wheel replaces the left
stick's X and releases that hand's grip for the game) and the wheel's
displayed angle.
New [vr] keys: first_person_seat (cockpit), cockpit_units_per_meter (100),
steering_wheel (true), native_steering_wheel (true), hand_steering (false)
and the seven wheel_* tuning keys. Nothing reads them yet.
The fresh-config template now writes the first-person defaults the
constants hold (50 / 1.5 / 0); d86dcb0 updated the constants but not the
template.
Tests: mkw_steering_wheel_tests (the fork's), mkw_vr_cockpit_tests,
mkw_vr_hand_steering_tests.
This commit is contained in:
1 parent
21b8d209dd
commit
50dc354123
12 files changed
+1547
-8
No files matched your search
@@ -2,6 +2,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "vr/steering_wheel.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -40,12 +43,35 @@ enum class FirstPersonRotation : uint8_t {
|
||||
Full,
|
||||
};
|
||||
|
||||
// Where the first-person head is placed.
|
||||
enum class FirstPersonSeat : uint8_t {
|
||||
// At the driver's own eyes, measured from the character's model and kept
|
||||
// behind the steering wheel, at a life-size cockpit scale. The wheel or
|
||||
// handlebar is then within reach of the player's hands.
|
||||
Cockpit,
|
||||
// The free first_person_head_*_meters offsets at first_person_units_per_meter.
|
||||
Custom,
|
||||
};
|
||||
|
||||
// The camera relocation published to Aurora for one guest frame: a transform
|
||||
// from the game's recorded view space into the space the headset renders from.
|
||||
struct FirstPersonAnchor {
|
||||
Mtx34 anchor_from_scene = kIdentityMtx34;
|
||||
bool valid = false;
|
||||
uint64_t guest_frame_index = 0;
|
||||
// The rest describes the cockpit seat and is left empty by the custom seat.
|
||||
bool cockpit = false;
|
||||
// World units per metre the anchor was built with (character and player
|
||||
// scale included).
|
||||
float units_per_meter = 0.0f;
|
||||
// The vehicle's steering wheel or handlebar, in metres in the seated frame
|
||||
// (+X right, +Y up, -Z forward, origin at the head).
|
||||
WheelGeometry native_wheel{};
|
||||
bool bike = false;
|
||||
// The vehicle's own wheel is being animated in the scene this frame.
|
||||
bool native_mesh_prepared = false;
|
||||
// Changes whenever the player's vehicle object does.
|
||||
uint64_t vehicle_identity = 0;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -137,6 +163,241 @@ inline Vec3 TransformPoint(const Mtx34& matrix, float x, float y, float z) noexc
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cockpit seat and steering-wheel geometry. Ported from heurazy's
|
||||
// mario-kart-wii-VR-port (GPL-3.0-or-later).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
inline Mtx34 ComposeMtx(const Mtx34& a, const Mtx34& b) noexcept {
|
||||
Mtx34 out{};
|
||||
for (int row = 0; row < 3; ++row) {
|
||||
for (int col = 0; col < 4; ++col) {
|
||||
out[row * 4 + col] = col == 3 ? a[row * 4 + 3] : 0.0f;
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
out[row * 4 + col] += a[row * 4 + k] * b[k * 4 + col];
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline bool InvertMtx(const Mtx34& m, Mtx34& out) noexcept {
|
||||
if (!detail::IsFiniteMtx34(m)) {
|
||||
return false;
|
||||
}
|
||||
const detail::Vec3 a{m[0], m[4], m[8]}, b{m[1], m[5], m[9]}, c{m[2], m[6], m[10]};
|
||||
const auto x = detail::Cross(b, c), y = detail::Cross(c, a), z = detail::Cross(a, b);
|
||||
const float det = detail::Dot(a, x);
|
||||
if (!detail::IsFiniteFloat(&det) || std::abs(det) < 1e-6f) {
|
||||
return false;
|
||||
}
|
||||
out = {x.x / det, x.y / det, x.z / det, 0, y.x / det, y.y / det, y.z / det, 0, z.x / det, z.y / det, z.z / det, 0};
|
||||
for (int row = 0; row < 3; ++row) {
|
||||
out[row * 4 + 3] = -(out[row * 4] * m[3] + out[row * 4 + 1] * m[7] + out[row * 4 + 2] * m[11]);
|
||||
}
|
||||
return detail::IsFiniteMtx34(out);
|
||||
}
|
||||
|
||||
// Keeps the eye behind the steering wheel or handlebar even when a long face or
|
||||
// a leaned-forward riding animation puts the character's eyes over it. Units
|
||||
// are the vehicle's; `radius` is the control's half width.
|
||||
inline float EyeBehindControls(float eyeForward, float controlsForward, float units, float radius) noexcept {
|
||||
const float clearance = std::clamp(0.40f + radius / units * 0.3f, 0.45f, 0.65f) * units;
|
||||
return std::min(eyeForward, controlsForward - clearance);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if (!detail::IsFiniteFloat(&eyeHeight)) {
|
||||
return 1.0f;
|
||||
}
|
||||
return std::clamp(eyeHeight / 100.0f, 1.0f, 2.5f);
|
||||
}
|
||||
|
||||
inline float ValidPlayerScale(float scale) noexcept {
|
||||
return detail::IsFiniteFloat(&scale) && scale >= 0.1f && scale <= 4.0f ? scale : 1.0f;
|
||||
}
|
||||
|
||||
inline Mtx34 ScaleModelBasis(Mtx34 pose, const std::array<float, 3>& scale) noexcept {
|
||||
for (int row = 0; row < 3; ++row) {
|
||||
for (int col = 0; col < 3; ++col) {
|
||||
pose[row * 4 + col] *= scale[col];
|
||||
}
|
||||
}
|
||||
return pose;
|
||||
}
|
||||
|
||||
inline bool NeutralPlayerScale(const std::array<float, 3>& scale) noexcept {
|
||||
for (float value : scale) {
|
||||
if (!detail::IsFiniteFloat(&value) || std::abs(value - 1.0f) > 0.001f) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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.
|
||||
inline bool ComputeDriverEyeFromBounds(const Mtx34& face, const Mtx34& placement, detail::Vec3 minimum,
|
||||
detail::Vec3 maximum, std::array<float, 3>& eye) noexcept {
|
||||
if (!detail::IsFiniteMtx34(face) || !detail::IsFiniteMtx34(placement)) {
|
||||
return false;
|
||||
}
|
||||
const std::array<float, 6> bounds{minimum.x, minimum.y, minimum.z, maximum.x, maximum.y, maximum.z};
|
||||
for (const auto& value : bounds) {
|
||||
if (!detail::IsFiniteFloat(&value) || std::abs(value) > 500.0f) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (minimum.x > maximum.x || minimum.y > maximum.y || minimum.z > maximum.z) {
|
||||
return false;
|
||||
}
|
||||
const auto model = detail::TransformPoint(face, (minimum.x + maximum.x) * 0.5f, (minimum.y + maximum.y) * 0.5f,
|
||||
(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) {
|
||||
return false;
|
||||
}
|
||||
eye = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Removes the visible vehicle's world transform from the evaluated head pose.
|
||||
// This retains the riding posture, but never imports kart motion into the seat.
|
||||
inline bool ComputeSeatedEye(const Mtx34& faceWorld, const Mtx34& bodyWorld, detail::Vec3 eyeLocal,
|
||||
std::array<float, 3>& eye) noexcept {
|
||||
if (!detail::IsFiniteMtx34(faceWorld) || !detail::IsFiniteMtx34(bodyWorld)) {
|
||||
return false;
|
||||
}
|
||||
const detail::Vec3 a{bodyWorld[0], bodyWorld[4], bodyWorld[8]}, b{bodyWorld[1], bodyWorld[5], bodyWorld[9]},
|
||||
c{bodyWorld[2], bodyWorld[6], bodyWorld[10]};
|
||||
const auto bc = detail::Cross(b, c), ca = detail::Cross(c, a), ab = detail::Cross(a, b);
|
||||
const float det = detail::Dot(a, bc);
|
||||
if (!detail::IsFiniteFloat(&det) || std::abs(det) < 1e-6f) {
|
||||
return false;
|
||||
}
|
||||
const auto world = detail::TransformPoint(faceWorld, eyeLocal.x, eyeLocal.y, eyeLocal.z);
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
eye = result;
|
||||
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.
|
||||
struct SeatedEyeReference {
|
||||
std::array<float, 3> value{}, candidate{};
|
||||
unsigned stable = 0;
|
||||
bool valid = false;
|
||||
void Observe(const std::array<float, 3>& sample, bool safe, bool freeze) {
|
||||
if (freeze && valid) {
|
||||
return;
|
||||
}
|
||||
if (!safe) {
|
||||
stable = 0;
|
||||
return;
|
||||
}
|
||||
float delta = 0.0f;
|
||||
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 >= 8) {
|
||||
value = sample;
|
||||
valid = true;
|
||||
stable = 8;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Neutral authored hand targets, transformed by the stabilised cockpit body. Do
|
||||
// not use the animated hand IK targets: feeding their steering rotation back
|
||||
// into the controller angle would make the input chase its own animation.
|
||||
// `seat_from_body` maps vehicle-local units into the seated frame in units;
|
||||
// the result is in metres.
|
||||
inline WheelGeometry ComputeNativeWheelGeometry(const Mtx34& seat_from_body, detail::Vec3 left, detail::Vec3 right,
|
||||
float units) noexcept {
|
||||
WheelGeometry out{};
|
||||
if (!detail::IsFiniteMtx34(seat_from_body) || !detail::IsFiniteFloat(&units) || units <= 0.0f) {
|
||||
return out;
|
||||
}
|
||||
if (left.x > right.x) {
|
||||
std::swap(left, right);
|
||||
}
|
||||
const auto a = detail::TransformPoint(seat_from_body, left.x, left.y, left.z);
|
||||
const auto b = detail::TransformPoint(seat_from_body, right.x, right.y, right.z);
|
||||
detail::Vec3 x{b.x - a.x, b.y - a.y, b.z - a.z};
|
||||
const float radius = std::sqrt(detail::Dot(x, x)) / (2.0f * units);
|
||||
if (!detail::IsFiniteFloat(&radius) || radius < 0.04f || radius > 1.0f || !detail::Normalize(x)) {
|
||||
return out;
|
||||
}
|
||||
// Kart +X points left when looking along its +Z driving direction.
|
||||
// WheelHand uses headset +X (right), so reverse the authored lateral axis.
|
||||
x = {-x.x, -x.y, -x.z};
|
||||
detail::Vec3 y{seat_from_body[1], seat_from_body[5], seat_from_body[9]};
|
||||
const float projection = detail::Dot(x, y);
|
||||
y = {y.x - x.x * projection, y.y - x.y * projection, y.z - x.z * projection};
|
||||
if (!detail::Normalize(y)) {
|
||||
return out;
|
||||
}
|
||||
const auto z = detail::Cross(x, y);
|
||||
out.center = {(a.x + b.x) / (2.0f * units), (a.y + b.y) / (2.0f * units), (a.z + b.z) / (2.0f * units)};
|
||||
for (const auto& value : out.center) {
|
||||
if (!detail::IsFiniteFloat(&value) || std::abs(value) > 5.0f) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
out.right = {x.x, x.y, x.z};
|
||||
out.up = {y.x, y.y, y.z};
|
||||
out.normal = {z.x, z.y, z.z};
|
||||
out.radius = radius;
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
inline WheelGeometry ComputeNativeHandlebarGeometry(const Mtx34& seatFromHandle, const Mtx34& seatFromBody,
|
||||
detail::Vec3 left, detail::Vec3 right, float units) noexcept {
|
||||
auto out = ComputeNativeWheelGeometry(seatFromHandle, left, right, units);
|
||||
if (!out.valid || !detail::IsFiniteMtx34(seatFromBody)) {
|
||||
return {};
|
||||
}
|
||||
// Use the body's neutral axes, not the already-steered handle's axes.
|
||||
// Otherwise the visual steering feeds back into the next input sample.
|
||||
detail::Vec3 x{-seatFromBody[0], -seatFromBody[4], -seatFromBody[8]},
|
||||
forward{seatFromBody[2], seatFromBody[6], seatFromBody[10]};
|
||||
if (!detail::Normalize(x)) {
|
||||
return {};
|
||||
}
|
||||
const float along = detail::Dot(forward, x);
|
||||
forward = {forward.x - along * x.x, forward.y - along * x.y, forward.z - along * x.z};
|
||||
if (!detail::Normalize(forward)) {
|
||||
return {};
|
||||
}
|
||||
const auto vertical = detail::Cross(x, forward);
|
||||
out.right = {x.x, x.y, x.z};
|
||||
out.up = {forward.x, forward.y, forward.z};
|
||||
out.normal = {vertical.x, vertical.y, vertical.z};
|
||||
return out;
|
||||
}
|
||||
|
||||
// Builds the anchor from the game's view matrix (world -> recorded view space),
|
||||
// the kart's pose (kart-local -> world), and head offsets already converted to
|
||||
// world units.
|
||||
|
||||
Reference in new issue
Block a user