mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 01:00:04 +02:00
Fade wrist panel by viewer-relative angle
This commit is contained in:
1 parent
33533ba8a2
commit
afb8509408
6 files changed
+170
-2
No files matched your search
@@ -123,6 +123,10 @@ if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING)
|
||||
add_executable(frameyap_mount_test tests/mount_test.cpp)
|
||||
target_link_libraries(frameyap_mount_test PRIVATE frameyap_mount)
|
||||
add_test(NAME frameyap.mount COMMAND frameyap_mount_test)
|
||||
add_executable(frameyap_angle_fade_test tests/angle_fade_test.cpp)
|
||||
target_include_directories(frameyap_angle_fade_test PRIVATE src)
|
||||
target_compile_options(frameyap_angle_fade_test PRIVATE -UNDEBUG)
|
||||
add_test(NAME frameyap.angle_fade COMMAND frameyap_angle_fade_test)
|
||||
add_executable(frameyap_laser_setting_test tests/laser_setting_test.cpp)
|
||||
target_link_libraries(frameyap_laser_setting_test PRIVATE frameyap_mount)
|
||||
target_compile_options(frameyap_laser_setting_test PRIVATE -UNDEBUG)
|
||||
|
||||
@@ -33,6 +33,9 @@ See [third-party notes](docs/third-party.md). No GitHub release is published yet
|
||||
lower-right corner bracket to scale it (world, head or wrist). Both sit in
|
||||
transparent margins, like Steam's window handles. Scaling keeps the upper-left
|
||||
corner anchored. Position/size changes last for this run only.
|
||||
Wrist mounting fades the panel as its full orientation turns away from an
|
||||
upright viewer-facing pose (60°–75°), hiding interaction past that angle;
|
||||
world and head mounting do not fade.
|
||||
**Bindings** requests SteamVR's binding editor directly. Existing SteamVR overrides may supersede defaults. World-space by
|
||||
default; settings offer left wrist, right wrist and head mounting, plus recenter.
|
||||
Dashboard lasers provide clickable controls. Settings → Lasers anytime is an
|
||||
|
||||
+10
-1
@@ -239,7 +239,16 @@ inward with upright, unmirrored text. The controller-relative center is
|
||||
(0, 0.18, 0.089) m, approximating the compact HUD's surface center: its
|
||||
0.12 m wrist lift, 0.09 m bottom anchor and ~0.03 m panel-center correction;
|
||||
Z combines the fallback 0.054 m wrist calibration and 0.035 m finger-back offset. This copies placement geometry, not
|
||||
VR Workspace's avatar-dependent wrist calibration or its head-facing fade.
|
||||
VR Workspace's avatar-dependent wrist calibration. Wrist-mounted panels now use
|
||||
the same *behavior* as kouseki's watch HUD: fully visible while their entire
|
||||
orientation is within 60° of an upright, viewer-facing panel; linear opacity
|
||||
fade from 60° to 75°, then hidden (including laser interaction). Pitch, yaw and
|
||||
roll contribute together; turning the wrist away or moving the head around it
|
||||
changes the angle. OpenVR's overlay alpha changes without rerendering the panel.
|
||||
World and head mounts do not fade. Missing headset tracking hides a wrist panel;
|
||||
a lost wrist still uses the existing world-space fallback. This was implemented
|
||||
independently with no kouseki library or runtime dependency. Headset readability,
|
||||
fade feel and interaction at the threshold still need live acceptance.
|
||||
|
||||
To tune the selected wrist, set `wrist` in `config.json` as in the example above:
|
||||
`x`, `y`, `z` are controller-local meters (each -0.3 to 0.3), `width` is panel
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include "mount.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace frameyap {
|
||||
namespace angle_fade_detail {
|
||||
struct Vec3 { double x, y, z; };
|
||||
inline Vec3 axis(const Matrix34& m, int column) { return {m[0][column], m[1][column], m[2][column]}; }
|
||||
inline Vec3 operator-(Vec3 a, Vec3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
|
||||
inline Vec3 operator*(Vec3 a, double s) { return {a.x * s, a.y * s, a.z * s}; }
|
||||
inline double dot(Vec3 a, Vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
||||
inline Vec3 cross(Vec3 a, Vec3 b) {
|
||||
return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
|
||||
}
|
||||
inline bool normalize(Vec3& v) {
|
||||
const double n = std::sqrt(dot(v, v));
|
||||
if (!std::isfinite(n) || n < 1e-5) return false;
|
||||
v = v * (1. / n);
|
||||
return true;
|
||||
}
|
||||
} // namespace angle_fade_detail
|
||||
|
||||
// Wrist-only visibility: compare the entire panel orientation (including roll)
|
||||
// to an upright panel pointing at the viewer. Kouseki's watch fades linearly
|
||||
// between 60 and 75 degrees; the matrix math here has no engine dependency.
|
||||
// Both poses must be in the same tracking space, with rigid orthonormal axes.
|
||||
// Invalid geometry hides the panel rather than making it clickable at full alpha.
|
||||
inline float wrist_view_opacity(const Matrix34& panel, const Matrix34& head) {
|
||||
using namespace angle_fade_detail;
|
||||
Vec3 front = axis(head, 3) - axis(panel, 3);
|
||||
if (!normalize(front)) return 0.f;
|
||||
Vec3 right = axis(head, 0);
|
||||
right = right - front * dot(right, front);
|
||||
if (!normalize(right)) {
|
||||
right = cross(axis(head, 1), front);
|
||||
if (!normalize(right)) return 0.f;
|
||||
}
|
||||
Vec3 up = cross(front, right);
|
||||
if (!normalize(up)) return 0.f;
|
||||
Vec3 actual_right = axis(panel, 0), actual_up = axis(panel, 1), actual_front = axis(panel, 2);
|
||||
if (!normalize(actual_right) || !normalize(actual_up) || !normalize(actual_front)) return 0.f;
|
||||
// Trace of readable^T * actual is 1 + 2 cos(angle) for rotations.
|
||||
// Unlike a front-normal dot product, this also rejects upside-down text.
|
||||
const double cosine = std::clamp((dot(right, actual_right) + dot(up, actual_up) +
|
||||
dot(front, actual_front) - 1.) * .5, -1., 1.);
|
||||
if (!std::isfinite(cosine)) return 0.f;
|
||||
constexpr double pi = 3.14159265358979323846;
|
||||
const double angle = std::acos(cosine);
|
||||
return static_cast<float>(1. - std::clamp((angle - pi / 3.) / (pi / 12.), 0., 1.));
|
||||
}
|
||||
|
||||
// OpenVR wrist transforms are controller-relative; tracking poses are absolute.
|
||||
// Compose into the headset's tracking space before comparing orientations.
|
||||
inline float wrist_view_opacity_relative(const Matrix34& local_panel, const Matrix34& anchor,
|
||||
const Matrix34& head) {
|
||||
Matrix34 world{};
|
||||
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) {
|
||||
world[r][c] = c == 3 ? anchor[r][3] : 0.f;
|
||||
for (int k = 0; k < 3; ++k) world[r][c] += anchor[r][k] * local_panel[k][c];
|
||||
}
|
||||
return wrist_view_opacity(world, head);
|
||||
}
|
||||
|
||||
} // namespace frameyap
|
||||
+18
-1
@@ -1,5 +1,6 @@
|
||||
#include "overlay.hpp"
|
||||
#include "overlay_texture.hpp"
|
||||
#include "angle_fade.hpp"
|
||||
#include "gestures.hpp"
|
||||
#include "laser_setting.hpp"
|
||||
#include "panel_surface.hpp"
|
||||
@@ -9,6 +10,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
@@ -89,6 +91,7 @@ struct Overlay::Impl {
|
||||
Panel panel;
|
||||
bool save_failed = false, debug_save_failed = false, auto_save_failed = false;
|
||||
bool world_ready = false, placed = false, has_texture = false, shown = false;
|
||||
float published_alpha = -1.f;
|
||||
float size_scale = 1.f;
|
||||
bool size_changed = false;
|
||||
float move_x = 0.f, move_y = 0.f;
|
||||
@@ -208,7 +211,21 @@ struct Overlay::Impl {
|
||||
gpu_texture.reset();
|
||||
}
|
||||
void visibility() {
|
||||
const bool wanted = placed && has_texture;
|
||||
float alpha = 1.f;
|
||||
if (placed && applied_mount && (*applied_mount == Mount::LeftWrist || *applied_mount == Mount::RightWrist)) {
|
||||
alpha = tracked(anchor) && tracked(vr::k_unTrackedDeviceIndex_Hmd)
|
||||
? wrist_view_opacity_relative(canvas_pose, matrix(poses[anchor].mDeviceToAbsoluteTracking),
|
||||
matrix(poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking))
|
||||
: 0.f;
|
||||
}
|
||||
// Keep geometry/texture stationary; opacity is a compositor property,
|
||||
// not an upload. At zero, hide the interactive overlay as well.
|
||||
alpha = std::round(alpha * 255.f) / 255.f;
|
||||
if (alpha != published_alpha) {
|
||||
overlay_check(overlay->SetOverlayAlpha(handle, alpha), overlay, "SetOverlayAlpha");
|
||||
published_alpha = alpha;
|
||||
}
|
||||
const bool wanted = placed && has_texture && alpha > 0.f;
|
||||
if (wanted == shown) return;
|
||||
overlay_check(wanted ? overlay->ShowOverlay(handle) : overlay->HideOverlay(handle), overlay, "Overlay visibility");
|
||||
if (wanted) ++show_calls; else ++hide_calls;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "angle_fade.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
|
||||
using namespace frameyap;
|
||||
#define CHECK(x) do { if (!(x)) { std::cerr << "line " << __LINE__ << ": " #x "\n"; std::exit(1); } } while (0)
|
||||
namespace {
|
||||
constexpr Matrix34 identity{{{{1.f, 0.f, 0.f, 0.f}},
|
||||
{{0.f, 1.f, 0.f, 0.f}},
|
||||
{{0.f, 0.f, 1.f, 0.f}}}};
|
||||
Matrix34 pitch(float degrees) {
|
||||
auto pose = identity;
|
||||
const float a = degrees * 3.14159265358979323846f / 180.f;
|
||||
pose[1][1] = pose[2][2] = std::cos(a);
|
||||
pose[1][2] = -std::sin(a); pose[2][1] = std::sin(a);
|
||||
return pose;
|
||||
}
|
||||
Matrix34 roll(float degrees) {
|
||||
auto pose = identity;
|
||||
const float a = degrees * 3.14159265358979323846f / 180.f;
|
||||
pose[0][0] = pose[1][1] = std::cos(a);
|
||||
pose[0][1] = -std::sin(a); pose[1][0] = std::sin(a);
|
||||
return pose;
|
||||
}
|
||||
bool near(float a, float b) { return std::abs(a - b) < 1e-4f; }
|
||||
} // namespace
|
||||
int main() {
|
||||
auto head = identity;
|
||||
head[2][3] = 1.f; // facing +Z of the panel
|
||||
CHECK(near(wrist_view_opacity(identity, head), 1.f));
|
||||
CHECK(near(wrist_view_opacity(pitch(60.f), head), 1.f));
|
||||
CHECK(near(wrist_view_opacity(pitch(67.5f), head), .5f));
|
||||
CHECK(near(wrist_view_opacity(pitch(75.f), head), 0.f));
|
||||
CHECK(near(wrist_view_opacity(pitch(-67.5f), head), .5f));
|
||||
CHECK(near(wrist_view_opacity(roll(67.5f), head), .5f)); // no front-only shortcut
|
||||
CHECK(near(wrist_view_opacity(roll(180.f), head), 0.f));
|
||||
auto anchor = identity;
|
||||
anchor[0] = {0.f, 0.f, 1.f, 3.f};
|
||||
anchor[2] = {-1.f, 0.f, 0.f, 2.f};
|
||||
auto turned_head = anchor;
|
||||
turned_head[0][3] = 4.f;
|
||||
CHECK(near(wrist_view_opacity_relative(identity, anchor, turned_head), 1.f));
|
||||
CHECK(near(wrist_view_opacity_relative(pitch(67.5f), anchor, turned_head), .5f));
|
||||
const auto tilted = pitch(55.f);
|
||||
auto combined = tilted;
|
||||
// Add a roll about the viewer axis: each axis alone is readable, combined is not.
|
||||
auto r = roll(55.f);
|
||||
for (int row = 0; row < 3; ++row) for (int col = 0; col < 3; ++col) {
|
||||
float value = 0;
|
||||
for (int k = 0; k < 3; ++k) value += r[row][k] * tilted[k][col];
|
||||
combined[row][col] = value;
|
||||
}
|
||||
CHECK(near(wrist_view_opacity(combined, head), 0.f));
|
||||
auto behind = head;
|
||||
behind[2][3] = -1.f;
|
||||
CHECK(near(wrist_view_opacity(identity, behind), 0.f));
|
||||
auto offset = head;
|
||||
offset[0][3] = 2.f; // camera moved aside, not rotated
|
||||
CHECK(wrist_view_opacity(identity, offset) < 1.f);
|
||||
CHECK(near(wrist_view_opacity(identity, identity), 0.f));
|
||||
auto invalid = identity;
|
||||
invalid[0][0] = std::numeric_limits<float>::quiet_NaN();
|
||||
CHECK(near(wrist_view_opacity(invalid, head), 0.f));
|
||||
std::cout << "wrist angle fade checks passed\n";
|
||||
}
|
||||
Reference in new issue
Block a user