diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 6fb1a4e..29917ca 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -218,6 +218,7 @@ void BreezyDesktopEffect::recenter() { void BreezyDesktopEffect::setLookingAtScreenIndex(int index) { m_lookingAtScreenIndex = index; + if (m_smoothFollowEnabled) updateDriverSmoothFollowSettings(); } void BreezyDesktopEffect::reconfigure(ReconfigureFlags) diff --git a/kwin/src/qml/BreezyDesktop.qml b/kwin/src/qml/BreezyDesktop.qml index 3f3fc3b..c660827 100644 --- a/kwin/src/qml/BreezyDesktop.qml +++ b/kwin/src/qml/BreezyDesktop.qml @@ -55,20 +55,20 @@ Node { let targetProgress; if (smoothFollowEnabled && focusedIndex !== -1) { focusedDisplay = breezyDesktop.displayAtIndex(focusedIndex); - if (focusedDisplay !== null) { + if (focusedDisplay) { targetDisplay = focusedDisplay; targetProgress = 1.0; startSmoothFollowFocusAnimation = true; } } else if (!smoothFollowEnabled && breezyDesktop.focusedMonitorIndex !== -1) { unfocusedDisplay = breezyDesktop.displayAtIndex(breezyDesktop.focusedMonitorIndex); - if (unfocusedDisplay !== null) { + if (unfocusedDisplay) { targetDisplay = unfocusedDisplay; targetProgress = 0.0; } } - if (targetDisplay !== null) { + if (targetDisplay) { smoothFollowTransitionAnimation.stop(); smoothFollowTransitionAnimation.target = targetDisplay; smoothFollowTransitionAnimation.from = targetDisplay.smoothFollowTransitionProgress; @@ -80,13 +80,16 @@ Node { if (focusedIndex !== breezyDesktop.focusedMonitorIndex) { const unfocusedIndex = breezyDesktop.focusedMonitorIndex; if (!focusedDisplay) focusedDisplay = focusedIndex !== -1 ? breezyDesktop.displayAtIndex(focusedIndex) : null; - if (focusedDisplay === null) { + if (!focusedDisplay) { if (!unfocusedDisplay) unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex); - zoomOutAnimation.target = unfocusedDisplay; - zoomOutAnimation.target.targetDistance = effect.allDisplaysDistance; - zoomOutAnimation.start(); - } else { - if (unfocusedIndex === -1) { + if (unfocusedDisplay) { + zoomOutAnimation.target = unfocusedDisplay; + zoomOutAnimation.target.targetDistance = effect.allDisplaysDistance; + zoomOutAnimation.start(); + } + } else { + if (!unfocusedDisplay) unfocusedDisplay = unfocusedIndex !== -1 ? breezyDesktop.displayAtIndex(unfocusedIndex) : null; + if (!unfocusedDisplay) { zoomInAnimation.target = focusedDisplay; focusedDisplay.targetDistance = effect.focusedDisplayDistance; zoomInAnimation.start(); @@ -94,9 +97,8 @@ Node { zoomInSeqAnimation.target = focusedDisplay; focusedDisplay.targetDistance = effect.focusedDisplayDistance; - if (!unfocusedDisplay) unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex); zoomOutSeqAnimation.target = unfocusedDisplay; - zoomOutSeqAnimation.target.targetDistance = effect.allDisplaysDistance; + unfocusedDisplay.targetDistance = effect.allDisplaysDistance; zoomOnFocusSequence.start(); } diff --git a/kwin/src/qml/CameraController.qml b/kwin/src/qml/CameraController.qml index 2d69ebc..af00f15 100644 --- a/kwin/src/qml/CameraController.qml +++ b/kwin/src/qml/CameraController.qml @@ -7,27 +7,56 @@ Item { required property Camera camera required property var fovDetails - property var displayResolution: effect.displayResolution - property real diagonalFOV: effect.diagonalFOV + Displays { + id: displays + } + + property real aspectRatio: effect.displayResolution[0] / effect.displayResolution[1] property real lensDistanceRatio: effect.lensDistanceRatio property bool sbsEnabled: effect.sbsEnabled property bool customBannerEnabled: effect.customBannerEnabled 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, // continue to use the origin data for the duration of the Timer property bool smoothFollowDisabling: false - Displays { - id: displays + property real clipNear: 10.0 + 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( - rotations[0], - rotations[1], - effect.imuTimeElapsedMs, - lookAheadMS(effect.imuTimestamp, effect.lookAheadConfig, effect.lookAheadOverride) + rates, + lookAheadMS( + effect.imuTimestamp, + effect.lookAheadConfig, + effect.lookAheadOverride + ) ); camera.position = rotations[0].times(Qt.vector3d(0, 0, -fovDetails.lensDistancePixels)); } @@ -42,43 +71,88 @@ Item { return (override === -1 ? lookAheadConstant : override) + dataAge; } - function applyLookAhead(quatT0, quatT1, elapsedTimeMs, 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; - + function applyLookAhead(rates, lookAheadMs) { return Qt.vector3d( - eulerT0.x + deltaX * timeConstant, - eulerT0.y + deltaY * timeConstant, - eulerT0.z + deltaZ * timeConstant, + rates.eulerEnd.x + rates.pitchDegrees * lookAheadMs, + rates.eulerEnd.y + rates.yawDegrees * lookAheadMs, + rates.eulerEnd.z + rates.rollDegrees * lookAheadMs, ); } - function updateFOV() { - const aspectRatio = displayResolution[0] / displayResolution[1]; - camera.fieldOfView = displays.radianToDegree(displays.diagonalToCrossFOVs( - displays.degreeToRadian(cameraController.diagonalFOV), - aspectRatio - ).vertical); + function updateProjection() { + camera.projection = buildPerspectiveMatrix(); } - onDisplayResolutionChanged: updateFOV(); - onDiagonalFOVChanged: updateFOV(); + function buildPerspectiveMatrix() { + 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 { running: true onTriggered: { const rotations = (effect.smoothFollowEnabled || smoothFollowDisabling) ? effect.smoothFollowOrigin : effect.imuRotations; if (rotations && rotations.length > 0) { - updateCamera(rotations); + const rates = ratesOfChange(rotations); + updateCamera(rotations, rates); + applyRollingShutterShear(rates); } } } diff --git a/kwin/src/qml/Displays.qml b/kwin/src/qml/Displays.qml index 982845c..be2a3d6 100644 --- a/kwin/src/qml/Displays.qml +++ b/kwin/src/qml/Displays.qml @@ -25,13 +25,15 @@ QtObject { // Converts diagonal FOV in radians and aspect ratio to horizontal and vertical FOVs function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) { - var flatDiagonalFOV = 2 * Math.tan(diagonalFOVRadians / 2); - var flatVerticalFOV = flatDiagonalFOV / Math.sqrt(1 + aspectRatio * aspectRatio); - var flatHorizontalFOV = flatVerticalFOV * aspectRatio; + var diagonalTangent = Math.tan(diagonalFOVRadians / 2); + var verticalTangent = diagonalTangent / Math.sqrt(1 + aspectRatio * aspectRatio); + var horizontalTangent = verticalTangent * aspectRatio; return { diagonal: diagonalFOVRadians, - horizontal: 2 * Math.atan(flatHorizontalFOV / 2), - vertical: 2 * Math.atan(flatVerticalFOV / 2) + horizontal: 2 * Math.atan(horizontalTangent), + horizontalTangent: horizontalTangent, + vertical: 2 * Math.atan(verticalTangent), + verticalTangent: verticalTangent } } @@ -50,16 +52,16 @@ QtObject { function buildFovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistance, wrappingChoice) { const aspect = viewportWidth / viewportHeight; - const fovRadians = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect); - const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / defaultDisplayDistance); - const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / defaultDisplayDistance); + const crossFovs = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect); + const defaultDistanceVerticalRadians = 2 * Math.atan(crossFovs.verticalTangent / defaultDisplayDistance); + const defaultDistanceHorizontalRadians = 2 * Math.atan(crossFovs.horizontalTangent / defaultDisplayDistance); // 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; // 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; let monitorWrappingScheme = actualWrapScheme(screens, viewportWidth, viewportHeight); diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml index 0350329..9448606 100644 --- a/kwin/src/qml/main.qml +++ b/kwin/src/qml/main.qml @@ -110,7 +110,7 @@ Item { root.effect.antialiasingQuality === 2 ? SceneEnvironment.High : SceneEnvironment.VeryHigh)) } - PerspectiveCamera { + CustomCamera { id: camera }