mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 06:00:25 +02:00
Fixed "Mirror Mode" Gran Prix
This commit is contained in:
1 parent
6467c6390c
commit
850d157481
5 files changed
+188
-3
No files matched your search
@@ -698,6 +698,12 @@ gfx::StereoReplayFrame make_stereo_replay_frame(const AuroraStereoFrame& input,
|
||||
view.viewFromScene = sceneAnchor.active
|
||||
? gfx::stereo_replay::compose_affine(view.viewFromCenter, anchorFromScene)
|
||||
: view.viewFromCenter;
|
||||
// A mirror-mode draw takes the same route from the mirrored eye delta, so the
|
||||
// reflection its projection carries is taken in the anchored camera's space.
|
||||
const auto mirroredFromCenter = gfx::stereo_replay::mirror_view_delta_x(view.viewFromCenter);
|
||||
view.viewFromSceneMirrored = sceneAnchor.active
|
||||
? gfx::stereo_replay::compose_affine(mirroredFromCenter, anchorFromScene)
|
||||
: mirroredFromCenter;
|
||||
}
|
||||
return replay;
|
||||
}
|
||||
|
||||
@@ -1302,7 +1302,16 @@ static void write_stereo_uniform(std::span<uint8_t> uniform, const gx::UniformRe
|
||||
const Viewport& drawViewport, ClipRect displayRegion,
|
||||
const stereo_replay::HudScreen& hudScreen) noexcept {
|
||||
if (layout.perspective) {
|
||||
const auto projection = stereo_replay::compose_projection(eye.projection, gameProjection);
|
||||
// A projection that flips X (mirror mode) keeps its flip: the eye frustum
|
||||
// replaces the X scale's magnitude, and the reflection moves onto the eye
|
||||
// transform's half of the composition, where it reverses the winding the
|
||||
// draw's own cull mode already expects.
|
||||
const bool mirrored = stereo_replay::projection_mirrors_x(gameProjection);
|
||||
auto projection = stereo_replay::compose_projection(eye.projection, gameProjection);
|
||||
if (mirrored) {
|
||||
projection = stereo_replay::mirror_projection_x(projection);
|
||||
}
|
||||
const auto& viewFromScene = mirrored ? eye.viewFromSceneMirrored : eye.viewFromScene;
|
||||
std::memcpy(uniform.data() + layout.projectionOffset, &projection, sizeof(projection));
|
||||
|
||||
for (uint32_t matrix = 0; matrix < layout.positionMatrixCount; ++matrix) {
|
||||
@@ -1312,14 +1321,14 @@ static void write_stereo_uniform(std::span<uint8_t> uniform, const gx::UniformRe
|
||||
const size_t offset = layout.positionOffset + matrix * sizeof(Mat3x4<float>);
|
||||
Mat3x4<float> source;
|
||||
std::memcpy(&source, uniform.data() + offset, sizeof(source));
|
||||
const auto transformed = stereo_replay::compose_affine(eye.viewFromScene, source);
|
||||
const auto transformed = stereo_replay::compose_affine(viewFromScene, source);
|
||||
std::memcpy(uniform.data() + offset, &transformed, sizeof(transformed));
|
||||
}
|
||||
for (uint32_t matrix = 0; matrix < layout.normalMatrixCount; ++matrix) {
|
||||
const size_t offset = layout.normalOffset + matrix * sizeof(Mat3x4<float>);
|
||||
Mat3x4<float> source;
|
||||
std::memcpy(&source, uniform.data() + offset, sizeof(source));
|
||||
const auto transformed = stereo_replay::compose_normal(eye.viewFromScene, source);
|
||||
const auto transformed = stereo_replay::compose_normal(viewFromScene, source);
|
||||
std::memcpy(uniform.data() + offset, &transformed, sizeof(transformed));
|
||||
}
|
||||
} else {
|
||||
@@ -1484,6 +1493,13 @@ static bool prepare_stereo_replay_uniforms(const StereoReplayFrame& stereoFrame,
|
||||
if (!layout.perspective && !stereo_replay::is_orthographic_projection(gameProjection)) {
|
||||
continue;
|
||||
}
|
||||
// Producer-side and one-shot, so a mirror-mode race can be confirmed from
|
||||
// the log without instrumenting a build.
|
||||
static bool mirroredProjectionLogged = false;
|
||||
if (layout.perspective && !mirroredProjectionLogged && stereo_replay::projection_mirrors_x(gameProjection)) {
|
||||
mirroredProjectionLogged = true;
|
||||
Log.info("Immersive replay: perspective draws flip X (mirror mode); mirroring the eye transform to match");
|
||||
}
|
||||
LateStereoUniform* saved = nullptr;
|
||||
if (history != nullptr) {
|
||||
saved = &history->uniforms.emplace_back();
|
||||
|
||||
@@ -304,6 +304,11 @@ struct StereoReplayEye {
|
||||
// camera in their position matrices and therefore need this one. It equals
|
||||
// viewFromCenter whenever the anchor is identity.
|
||||
Mat3x4<float> viewFromScene;
|
||||
// The same transform for a draw whose projection mirrors X (Mario Kart Wii's
|
||||
// mirror mode), built from the mirrored eye delta so the reflection lands in
|
||||
// the anchored camera's space rather than in each eye's own. Pairs with
|
||||
// stereo_replay::mirror_projection_x; see the comment on those helpers.
|
||||
Mat3x4<float> viewFromSceneMirrored;
|
||||
};
|
||||
|
||||
struct StereoReplayFrame {
|
||||
|
||||
@@ -19,6 +19,58 @@ inline Mat4x4<float> compose_projection(const Mat4x4<float>& eyeFrustum, const M
|
||||
return out;
|
||||
}
|
||||
|
||||
// Mario Kart Wii's mirror mode negates the X scale of its projection matrix and
|
||||
// reverses its cull mode to match the winding that flip produces. compose_projection
|
||||
// replaces that coefficient with the headset frustum's always-positive X scale, so
|
||||
// the eye would draw normal winding against a reversed cull mode: every surface
|
||||
// inside out.
|
||||
//
|
||||
// The flip has to survive, but it cannot simply be re-applied to the eye's clip
|
||||
// position. The eyes are placed by the per-eye view delta, so a reflection taken
|
||||
// after it mirrors each eye about its own axis and swaps the stereo pair. The
|
||||
// reflection S = diag(-1, 1, 1) belongs between the delta and the game camera, i.e.
|
||||
// in the anchored camera's space, which the two helpers below reach by splitting it
|
||||
// in half around the delta V:
|
||||
//
|
||||
// clip = (P . S) . (S . V . S) . A . p = P . V . S . A . p
|
||||
//
|
||||
// mirror_projection_x supplies (P . S), mirror_view_delta_x supplies (S . V . S), and
|
||||
// S . S cancels. Keeping the reflection out of the staged position and normal
|
||||
// matrices leaves lighting in the game's own unmirrored view space, which is the
|
||||
// space its light positions are already expressed in.
|
||||
inline bool projection_mirrors_x(const Mat4x4<float>& gameProjection) noexcept {
|
||||
return gameProjection.m0[0] < 0.0f;
|
||||
}
|
||||
|
||||
// P . S: post-multiplying by the reflection negates the matrix's X column, which is
|
||||
// every coefficient the clip position picks up from the vertex's X.
|
||||
inline Mat4x4<float> mirror_projection_x(const Mat4x4<float>& projection) noexcept {
|
||||
Mat4x4<float> out = projection;
|
||||
out.m0[0] = -out.m0[0];
|
||||
out.m1[0] = -out.m1[0];
|
||||
out.m2[0] = -out.m2[0];
|
||||
out.m3[0] = -out.m3[0];
|
||||
return out;
|
||||
}
|
||||
|
||||
// S . V . S: the mirror image of the headset's eye delta, i.e. the pose the eye
|
||||
// would have if it were reflected along with the world. Conjugating by a reflection
|
||||
// negates exactly the entries with one X index: the X offset (half the IPD, plus any
|
||||
// head translation) and the yaw and roll terms that couple X to the other axes, while
|
||||
// pitch and the Y/Z offsets are left alone. Rendering an eye from this reflected pose
|
||||
// and flipping the result horizontally - which is what mirror_projection_x does - is
|
||||
// what that eye should see of the mirrored world, with the stereo pair the right way
|
||||
// round and head tracking still unmirrored.
|
||||
inline Mat3x4<float> mirror_view_delta_x(const Mat3x4<float>& viewFromCenter) noexcept {
|
||||
Mat3x4<float> out = viewFromCenter;
|
||||
out.m0[1] = -out.m0[1];
|
||||
out.m0[2] = -out.m0[2];
|
||||
out.m0[3] = -out.m0[3];
|
||||
out.m1[0] = -out.m1[0];
|
||||
out.m2[0] = -out.m2[0];
|
||||
return out;
|
||||
}
|
||||
|
||||
// Aurora stores the GX 3x4 matrices row-major. The vertex shader consumes
|
||||
// them as vec4 * mat3x4, which is equivalent to the original column-vector
|
||||
// affine transform. Applying an eye-space delta therefore composes delta *
|
||||
|
||||
@@ -258,5 +258,111 @@ TEST(StereoReplayTest, VirtualScreenStaysAheadOfTheAnchoredCamera) {
|
||||
EXPECT_NEAR(placed.m2[3], centreZ, 1e-3f);
|
||||
}
|
||||
|
||||
// A GX perspective projection with a positive X scale, plus the asymmetric
|
||||
// frustum offset an OpenXR eye contributes.
|
||||
Mat4x4<float> eye_frustum(float offsetX) {
|
||||
Mat4x4<float> m{};
|
||||
m.m0 = {1.3f, 0.0f, offsetX, 0.0f};
|
||||
m.m1 = {0.0f, 1.7f, 0.04f, 0.0f};
|
||||
m.m2 = {0.0f, 0.0f, -1.0001f, -0.2f};
|
||||
m.m3 = {0.0f, 0.0f, -1.0f, 0.0f};
|
||||
return m;
|
||||
}
|
||||
|
||||
Vec4<float> clip_of(const Mat4x4<float>& projection, const Mat3x4<float>& viewFromScene,
|
||||
const Mat3x4<float>& objectToCenter, const Vec4<float>& object) {
|
||||
const auto placed = compose_affine(viewFromScene, objectToCenter);
|
||||
const Vec4<float> view{dot4(placed.m0, object), dot4(placed.m1, object), dot4(placed.m2, object), 1.0f};
|
||||
return {dot4(projection.m0, view), dot4(projection.m1, view), dot4(projection.m2, view), dot4(projection.m3, view)};
|
||||
}
|
||||
|
||||
TEST(StereoReplayTest, MirrorModeIsRecognizedByANegativeProjectionXScale) {
|
||||
const auto game = eye_frustum(0.0f);
|
||||
EXPECT_FALSE(projection_mirrors_x(game));
|
||||
|
||||
auto mirrored = game;
|
||||
mirrored.m0[0] = -mirrored.m0[0];
|
||||
EXPECT_TRUE(projection_mirrors_x(mirrored));
|
||||
}
|
||||
|
||||
TEST(StereoReplayTest, MirroredHalvesComposeIntoOneReflectionOfTheScene) {
|
||||
// The pair must reproduce exactly P . V . S . A: a world reflected about the
|
||||
// anchored camera's X plane, with the eyes placed in the reflected world.
|
||||
const std::array<float, 3> a{40.0f, -12.0f, -260.0f};
|
||||
auto anchor = identity3x4();
|
||||
anchor.m0[3] = -a[0];
|
||||
anchor.m1[3] = -a[1];
|
||||
anchor.m2[3] = -a[2];
|
||||
const auto viewFromCenter = head_tracking_delta();
|
||||
const auto viewFromScene = compose_affine(viewFromCenter, anchor);
|
||||
const auto viewFromSceneMirrored = compose_affine(mirror_view_delta_x(viewFromCenter), anchor);
|
||||
|
||||
auto game = eye_frustum(0.0f);
|
||||
game.m0[0] = -game.m0[0]; // Mirror mode's flip, as the game submits it.
|
||||
const auto eye = eye_frustum(0.11f);
|
||||
const auto projection = mirror_projection_x(compose_projection(eye, game));
|
||||
// compose_projection takes the X scale from the eye, so this is the ordinary
|
||||
// unmirrored eye projection P.
|
||||
const auto reference = compose_projection(eye, game);
|
||||
|
||||
// The reference route: reflect in the anchored camera's space by folding S into
|
||||
// the anchor, then compose the eye delta over it exactly as an unmirrored draw
|
||||
// would. This is the ordering the fix has to reproduce - S sits between the eye
|
||||
// delta and the anchor, not between the anchor and the world.
|
||||
auto reflectedAnchor = anchor;
|
||||
reflectedAnchor.m0[0] = -reflectedAnchor.m0[0];
|
||||
reflectedAnchor.m0[1] = -reflectedAnchor.m0[1];
|
||||
reflectedAnchor.m0[2] = -reflectedAnchor.m0[2];
|
||||
reflectedAnchor.m0[3] = -reflectedAnchor.m0[3];
|
||||
const auto viewFromSceneReference = compose_affine(viewFromCenter, reflectedAnchor);
|
||||
// Anchor ordering has to matter, or the test would pass either way.
|
||||
EXPECT_NE(viewFromSceneReference, viewFromScene);
|
||||
|
||||
Mat3x4<float> objectToCenter{};
|
||||
objectToCenter.m0 = {1.0f, 0.0f, 0.0f, 130.0f};
|
||||
objectToCenter.m1 = {0.0f, 1.0f, 0.0f, 55.0f};
|
||||
objectToCenter.m2 = {0.0f, 0.0f, 1.0f, -900.0f};
|
||||
|
||||
for (const auto& v : kVertices) {
|
||||
const auto mirroredClip = clip_of(projection, viewFromSceneMirrored, objectToCenter, v);
|
||||
const auto expected = clip_of(reference, viewFromSceneReference, objectToCenter, v);
|
||||
for (size_t component = 0; component < 4; ++component) {
|
||||
EXPECT_NEAR(mirroredClip[component], expected[component], 1e-3f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(StereoReplayTest, MirroringKeepsEachEyeOnItsOwnSide) {
|
||||
// The v6 failure this guards against: reflecting the finished clip position
|
||||
// mirrors every eye about its own axis, which swaps the stereo pair. With the
|
||||
// reflection taken before the eye delta, an object straight ahead must still
|
||||
// sit right of centre for the left eye and left of centre for the right.
|
||||
const float ipd = 3.2f; // Half-IPD in game units.
|
||||
const auto eyeDelta = [&](float sign) {
|
||||
auto m = identity3x4();
|
||||
m.m0[3] = -sign * ipd; // The eye moves by +sign*ipd, so the world moves back.
|
||||
return m;
|
||||
};
|
||||
auto game = eye_frustum(0.0f);
|
||||
game.m0[0] = -game.m0[0];
|
||||
|
||||
Mat3x4<float> objectToCenter = identity3x4();
|
||||
objectToCenter.m2[3] = -500.0f; // Straight ahead of the camera.
|
||||
const Vec4<float> object{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
|
||||
std::array<float, 2> ndcX{};
|
||||
for (size_t eyeIndex = 0; eyeIndex < 2; ++eyeIndex) {
|
||||
const float sign = eyeIndex == 0 ? -1.0f : 1.0f;
|
||||
const auto eye = eye_frustum(0.0f);
|
||||
const auto projection = mirror_projection_x(compose_projection(eye, game));
|
||||
const auto view = mirror_view_delta_x(eyeDelta(sign));
|
||||
const auto clip = clip_of(projection, view, objectToCenter, object);
|
||||
ASSERT_GT(clip[3], 0.0f);
|
||||
ndcX[eyeIndex] = clip[0] / clip[3];
|
||||
}
|
||||
EXPECT_GT(ndcX[0], 0.0f);
|
||||
EXPECT_LT(ndcX[1], 0.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace aurora::gfx::stereo_replay
|
||||
Reference in new issue
Block a user