Add rolling shutter adjustment

This commit is contained in:
wheaney 2025-09-23 12:40:25 -07:00
parent fab7e2756c
commit 7cab3393e2
5 changed files with 135 additions and 56 deletions

View File

@ -218,6 +218,7 @@ void BreezyDesktopEffect::recenter() {
void BreezyDesktopEffect::setLookingAtScreenIndex(int index) void BreezyDesktopEffect::setLookingAtScreenIndex(int index)
{ {
m_lookingAtScreenIndex = index; m_lookingAtScreenIndex = index;
if (m_smoothFollowEnabled) updateDriverSmoothFollowSettings();
} }
void BreezyDesktopEffect::reconfigure(ReconfigureFlags) void BreezyDesktopEffect::reconfigure(ReconfigureFlags)

View File

@ -55,20 +55,20 @@ Node {
let targetProgress; let targetProgress;
if (smoothFollowEnabled && focusedIndex !== -1) { if (smoothFollowEnabled && focusedIndex !== -1) {
focusedDisplay = breezyDesktop.displayAtIndex(focusedIndex); focusedDisplay = breezyDesktop.displayAtIndex(focusedIndex);
if (focusedDisplay !== null) { if (focusedDisplay) {
targetDisplay = focusedDisplay; targetDisplay = focusedDisplay;
targetProgress = 1.0; targetProgress = 1.0;
startSmoothFollowFocusAnimation = true; startSmoothFollowFocusAnimation = true;
} }
} else if (!smoothFollowEnabled && breezyDesktop.focusedMonitorIndex !== -1) { } else if (!smoothFollowEnabled && breezyDesktop.focusedMonitorIndex !== -1) {
unfocusedDisplay = breezyDesktop.displayAtIndex(breezyDesktop.focusedMonitorIndex); unfocusedDisplay = breezyDesktop.displayAtIndex(breezyDesktop.focusedMonitorIndex);
if (unfocusedDisplay !== null) { if (unfocusedDisplay) {
targetDisplay = unfocusedDisplay; targetDisplay = unfocusedDisplay;
targetProgress = 0.0; targetProgress = 0.0;
} }
} }
if (targetDisplay !== null) { if (targetDisplay) {
smoothFollowTransitionAnimation.stop(); smoothFollowTransitionAnimation.stop();
smoothFollowTransitionAnimation.target = targetDisplay; smoothFollowTransitionAnimation.target = targetDisplay;
smoothFollowTransitionAnimation.from = targetDisplay.smoothFollowTransitionProgress; smoothFollowTransitionAnimation.from = targetDisplay.smoothFollowTransitionProgress;
@ -80,13 +80,16 @@ Node {
if (focusedIndex !== breezyDesktop.focusedMonitorIndex) { if (focusedIndex !== breezyDesktop.focusedMonitorIndex) {
const unfocusedIndex = breezyDesktop.focusedMonitorIndex; const unfocusedIndex = breezyDesktop.focusedMonitorIndex;
if (!focusedDisplay) focusedDisplay = focusedIndex !== -1 ? breezyDesktop.displayAtIndex(focusedIndex) : null; if (!focusedDisplay) focusedDisplay = focusedIndex !== -1 ? breezyDesktop.displayAtIndex(focusedIndex) : null;
if (focusedDisplay === null) { if (!focusedDisplay) {
if (!unfocusedDisplay) unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex); if (!unfocusedDisplay) unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex);
zoomOutAnimation.target = unfocusedDisplay; if (unfocusedDisplay) {
zoomOutAnimation.target.targetDistance = effect.allDisplaysDistance; zoomOutAnimation.target = unfocusedDisplay;
zoomOutAnimation.start(); zoomOutAnimation.target.targetDistance = effect.allDisplaysDistance;
zoomOutAnimation.start();
}
} else { } else {
if (unfocusedIndex === -1) { if (!unfocusedDisplay) unfocusedDisplay = unfocusedIndex !== -1 ? breezyDesktop.displayAtIndex(unfocusedIndex) : null;
if (!unfocusedDisplay) {
zoomInAnimation.target = focusedDisplay; zoomInAnimation.target = focusedDisplay;
focusedDisplay.targetDistance = effect.focusedDisplayDistance; focusedDisplay.targetDistance = effect.focusedDisplayDistance;
zoomInAnimation.start(); zoomInAnimation.start();
@ -94,9 +97,8 @@ Node {
zoomInSeqAnimation.target = focusedDisplay; zoomInSeqAnimation.target = focusedDisplay;
focusedDisplay.targetDistance = effect.focusedDisplayDistance; focusedDisplay.targetDistance = effect.focusedDisplayDistance;
if (!unfocusedDisplay) unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex);
zoomOutSeqAnimation.target = unfocusedDisplay; zoomOutSeqAnimation.target = unfocusedDisplay;
zoomOutSeqAnimation.target.targetDistance = effect.allDisplaysDistance; unfocusedDisplay.targetDistance = effect.allDisplaysDistance;
zoomOnFocusSequence.start(); zoomOnFocusSequence.start();
} }

View File

@ -7,27 +7,56 @@ Item {
required property Camera camera required property Camera camera
required property var fovDetails required property var fovDetails
property var displayResolution: effect.displayResolution Displays {
property real diagonalFOV: effect.diagonalFOV id: displays
}
property real aspectRatio: effect.displayResolution[0] / effect.displayResolution[1]
property real lensDistanceRatio: effect.lensDistanceRatio property real lensDistanceRatio: effect.lensDistanceRatio
property bool sbsEnabled: effect.sbsEnabled property bool sbsEnabled: effect.sbsEnabled
property bool customBannerEnabled: effect.customBannerEnabled property bool customBannerEnabled: effect.customBannerEnabled
property bool smoothFollowEnabled: effect.smoothFollowEnabled property bool smoothFollowEnabled: effect.smoothFollowEnabled
property real lookAheadScanlineMs: effect.lookAheadConfig[2]
property var crossFovs: displays.diagonalToCrossFOVs(
displays.degreeToRadian(effect.diagonalFOV),
aspectRatio
);
// if true, then smoothFollowEnabled just cleared and the IMU data is slerping back, // if true, then smoothFollowEnabled just cleared and the IMU data is slerping back,
// continue to use the origin data for the duration of the Timer // continue to use the origin data for the duration of the Timer
property bool smoothFollowDisabling: false property bool smoothFollowDisabling: false
Displays { property real clipNear: 10.0
id: displays property real clipFar: 10000.0
function ratesOfChange(rotations) {
const e0 = rotations[0].toEulerAngles();
const e1 = rotations[1].toEulerAngles();
const dt = effect.imuTimeElapsedMs;
const yawDegrees = (e0.y - e1.y) / dt;
const pitchDegrees = (e0.x - e1.x) / dt;
const rollDegrees = (e0.z - e1.z) / dt;
return {
eulerEnd: e0,
eulerStart: e1,
yawDegrees: yawDegrees,
yaw: displays.degreeToRadian(yawDegrees),
pitchDegrees: pitchDegrees,
pitch: displays.degreeToRadian(pitchDegrees),
rollDegrees: rollDegrees,
roll: displays.degreeToRadian(rollDegrees)
};
} }
function updateCamera(rotations) { function updateCamera(rotations, rates) {
camera.eulerRotation = applyLookAhead( camera.eulerRotation = applyLookAhead(
rotations[0], rates,
rotations[1], lookAheadMS(
effect.imuTimeElapsedMs, effect.imuTimestamp,
lookAheadMS(effect.imuTimestamp, effect.lookAheadConfig, effect.lookAheadOverride) effect.lookAheadConfig,
effect.lookAheadOverride
)
); );
camera.position = rotations[0].times(Qt.vector3d(0, 0, -fovDetails.lensDistancePixels)); camera.position = rotations[0].times(Qt.vector3d(0, 0, -fovDetails.lensDistancePixels));
} }
@ -42,43 +71,88 @@ Item {
return (override === -1 ? lookAheadConstant : override) + dataAge; return (override === -1 ? lookAheadConstant : override) + dataAge;
} }
function applyLookAhead(quatT0, quatT1, elapsedTimeMs, lookAheadMs) { function applyLookAhead(rates, lookAheadMs) {
// convert both quats to euler angles
const eulerT0 = quatT0.toEulerAngles();
const eulerT1 = quatT1.toEulerAngles();
// compute the rate of change of the angles based on the elapsed time
const deltaX = (eulerT0.x - eulerT1.x);
const deltaY = (eulerT0.y - eulerT1.y);
const deltaZ = (eulerT0.z - eulerT1.z);
// how much of the delta to apply based on the look-ahead time
const timeConstant = lookAheadMs / elapsedTimeMs;
return Qt.vector3d( return Qt.vector3d(
eulerT0.x + deltaX * timeConstant, rates.eulerEnd.x + rates.pitchDegrees * lookAheadMs,
eulerT0.y + deltaY * timeConstant, rates.eulerEnd.y + rates.yawDegrees * lookAheadMs,
eulerT0.z + deltaZ * timeConstant, rates.eulerEnd.z + rates.rollDegrees * lookAheadMs,
); );
} }
function updateFOV() { function updateProjection() {
const aspectRatio = displayResolution[0] / displayResolution[1]; camera.projection = buildPerspectiveMatrix();
camera.fieldOfView = displays.radianToDegree(displays.diagonalToCrossFOVs(
displays.degreeToRadian(cameraController.diagonalFOV),
aspectRatio
).vertical);
} }
onDisplayResolutionChanged: updateFOV(); function buildPerspectiveMatrix() {
onDiagonalFOVChanged: updateFOV(); const f = 1.0 / crossFovs.verticalTangent;
const nf = 1.0 / (clipNear - clipFar);
const m00 = f / aspectRatio;
const m11 = f;
const m22 = (clipFar + clipNear) * nf;
const m23 = (2.0 * clipFar * clipNear) * nf;
// Standard OpenGL-style projection matrix
return Qt.matrix4x4(
m00, 0, 0, 0,
0, m11, 0, 0,
0, 0, m22, m23,
0, 0, -1, 0
);
}
function applyRollingShutterShear(rates) {
// Convert to maximum shift at bottom of frame
const maxDxNdc = (rates.yaw * lookAheadScanlineMs) / crossFovs.horizontalTangent;
const maxDyNdc = -(rates.pitch * lookAheadScanlineMs) / crossFovs.verticalTangent;
let shx = maxDxNdc / 2.0;
let shy = maxDyNdc / 2.0;
const f = 1.0 / crossFovs.verticalTangent;
const nf = 1.0 / (clipNear - clipFar);
const m00 = f / aspectRatio;
const m11 = f;
const m22 = (clipFar + clipNear) * nf;
const m23 = (2.0 * clipFar * clipNear) * nf;
const r0c0 = m00;
const r0c1 = -(shx * m11) / 2.0;
const r0c2 = -(shx) / 2.0;
const r0c3 = 0.0;
const r1c0 = 0.0;
const r1c1 = m11 * (1.0 - shy / 2.0);
const r1c2 = -(shy) / 2.0;
const r1c3 = 0.0;
const r2c0 = 0.0;
const r2c1 = 0.0;
const r2c2 = m22;
const r2c3 = m23;
const r3c0 = 0.0;
const r3c1 = 0.0;
const r3c2 = -1.0;
const r3c3 = 0.0;
camera.projection = Qt.matrix4x4(
r0c0, r0c1, r0c2, r0c3,
r1c0, r1c1, r1c2, r1c3,
r2c0, r2c1, r2c2, r2c3,
r3c0, r3c1, r3c2, r3c3
);
}
Component.onCompleted: updateProjection();
FrameAnimation { FrameAnimation {
running: true running: true
onTriggered: { onTriggered: {
const rotations = (effect.smoothFollowEnabled || smoothFollowDisabling) ? effect.smoothFollowOrigin : effect.imuRotations; const rotations = (effect.smoothFollowEnabled || smoothFollowDisabling) ? effect.smoothFollowOrigin : effect.imuRotations;
if (rotations && rotations.length > 0) { if (rotations && rotations.length > 0) {
updateCamera(rotations); const rates = ratesOfChange(rotations);
updateCamera(rotations, rates);
applyRollingShutterShear(rates);
} }
} }
} }

View File

@ -25,13 +25,15 @@ QtObject {
// Converts diagonal FOV in radians and aspect ratio to horizontal and vertical FOVs // Converts diagonal FOV in radians and aspect ratio to horizontal and vertical FOVs
function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) { function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) {
var flatDiagonalFOV = 2 * Math.tan(diagonalFOVRadians / 2); var diagonalTangent = Math.tan(diagonalFOVRadians / 2);
var flatVerticalFOV = flatDiagonalFOV / Math.sqrt(1 + aspectRatio * aspectRatio); var verticalTangent = diagonalTangent / Math.sqrt(1 + aspectRatio * aspectRatio);
var flatHorizontalFOV = flatVerticalFOV * aspectRatio; var horizontalTangent = verticalTangent * aspectRatio;
return { return {
diagonal: diagonalFOVRadians, diagonal: diagonalFOVRadians,
horizontal: 2 * Math.atan(flatHorizontalFOV / 2), horizontal: 2 * Math.atan(horizontalTangent),
vertical: 2 * Math.atan(flatVerticalFOV / 2) horizontalTangent: horizontalTangent,
vertical: 2 * Math.atan(verticalTangent),
verticalTangent: verticalTangent
} }
} }
@ -50,16 +52,16 @@ QtObject {
function buildFovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistance, wrappingChoice) { function buildFovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistance, wrappingChoice) {
const aspect = viewportWidth / viewportHeight; const aspect = viewportWidth / viewportHeight;
const fovRadians = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect); const crossFovs = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect);
const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / defaultDisplayDistance); const defaultDistanceVerticalRadians = 2 * Math.atan(crossFovs.verticalTangent / defaultDisplayDistance);
const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / defaultDisplayDistance); const defaultDistanceHorizontalRadians = 2 * Math.atan(crossFovs.horizontalTangent / defaultDisplayDistance);
// distance needed for the FOV-sized monitor to fill up the screen // distance needed for the FOV-sized monitor to fill up the screen
const fullScreenDistance = viewportHeight / 2 / Math.tan(fovRadians.vertical / 2); const fullScreenDistance = viewportHeight / (2 * crossFovs.verticalTangent);
const lensDistancePixels = fullScreenDistance / (1.0 - lensDistanceRatio) - fullScreenDistance; const lensDistancePixels = fullScreenDistance / (1.0 - lensDistanceRatio) - fullScreenDistance;
// distance of a display at the default (most zoomed out) distance, plus the lens distance constant // distance of a display at the default (most zoomed out) distance, plus the lens distance constant
const lensToScreenDistance = viewportHeight / 2 / Math.tan(defaultDistanceVerticalRadians / 2); const lensToScreenDistance = viewportHeight / (2 * Math.tan(defaultDistanceVerticalRadians / 2));
const completeScreenDistancePixels = lensToScreenDistance + lensDistancePixels; const completeScreenDistancePixels = lensToScreenDistance + lensDistancePixels;
let monitorWrappingScheme = actualWrapScheme(screens, viewportWidth, viewportHeight); let monitorWrappingScheme = actualWrapScheme(screens, viewportWidth, viewportHeight);

View File

@ -110,7 +110,7 @@ Item {
root.effect.antialiasingQuality === 2 ? SceneEnvironment.High : SceneEnvironment.VeryHigh)) root.effect.antialiasingQuality === 2 ? SceneEnvironment.High : SceneEnvironment.VeryHigh))
} }
PerspectiveCamera { CustomCamera {
id: camera id: camera
} }