Fix rendering issues caused by curved displays at closer distances

This commit is contained in:
wheaney 2025-12-22 15:33:06 -08:00
parent dc4f8634e1
commit 31d11307d0
3 changed files with 84 additions and 52 deletions

View File

@ -4,18 +4,23 @@ export function degreeToRadian(degree) {
// FOV in radians is spherical, so doesn't follow Pythagoras' theorem // FOV in radians is spherical, so doesn't follow Pythagoras' theorem
export function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) { export function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) {
// first convert from a spherical FOV to a diagonal FOV on a flat plane at a generic distance of 1.0 // first convert from a spherical FOV to a diagonal FOV on a flat plane at a unit distance of 1.0
const flatDiagonalFOV = 2 * Math.tan(diagonalFOVRadians / 2); const diagonalLengthUnitDistance = 2 * Math.tan(diagonalFOVRadians / 2);
// then convert to flat plane horizontal and vertical FOVs // then convert to flat plane horizontal and vertical FOVs
const flatVerticalFOV = flatDiagonalFOV / Math.sqrt(1 + aspectRatio * aspectRatio); const heightUnitDistance = diagonalLengthUnitDistance / Math.sqrt(1 + aspectRatio * aspectRatio);
const flatHorizontalFOV = flatVerticalFOV * aspectRatio; const widthUnitDistance = heightUnitDistance * aspectRatio;
// then convert back to spherical FOV
return { return {
diagonal: diagonalFOVRadians, // then convert back to spherical FOV
horizontal: 2 * Math.atan(flatHorizontalFOV / 2), diagonalRadians: diagonalFOVRadians,
vertical: 2 * Math.atan(flatVerticalFOV / 2) horizontalRadians: 2 * Math.atan(widthUnitDistance / 2),
verticalRadians: 2 * Math.atan(heightUnitDistance / 2),
// flat values are relative to a unit distance of 1.0
diagonalLengthUnitDistance,
widthUnitDistance,
heightUnitDistance
} }
} }
@ -31,7 +36,10 @@ export const fovConversionFns = {
fovEdgeToScreenCenterDistance: (edgeDistance, screenLength) => Math.sqrt(Math.pow(edgeDistance, 2) - Math.pow(screenLength / 2, 2)), fovEdgeToScreenCenterDistance: (edgeDistance, screenLength) => Math.sqrt(Math.pow(edgeDistance, 2) - Math.pow(screenLength / 2, 2)),
lengthToRadians: (fovRadians, fovLength, screenEdgeDistance, toLength) => Math.asin(toLength / 2 / screenEdgeDistance) * 2, lengthToRadians: (fovRadians, fovLength, screenEdgeDistance, toLength) => Math.asin(toLength / 2 / screenEdgeDistance) * 2,
angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => { angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => {
return toAngleOpposite / toAngleAdjacent * screenDistance return toAngleOpposite / toAngleAdjacent * screenDistance;
},
fovRadiansAtDistance: (fovRadians, unitLength, newScreenDistance) => {
return 2 * Math.atan(unitLength / 2 / newScreenDistance);
}, },
radiansToSegments: (screenRadians) => 1 radiansToSegments: (screenRadians) => 1
}, },
@ -42,6 +50,7 @@ export const fovConversionFns = {
fovEdgeToScreenCenterDistance: (edgeDistance, screenLength) => edgeDistance, fovEdgeToScreenCenterDistance: (edgeDistance, screenLength) => edgeDistance,
lengthToRadians: (fovRadians, fovLength, screenEdgeDistance, toLength) => fovRadians / fovLength * toLength, lengthToRadians: (fovRadians, fovLength, screenEdgeDistance, toLength) => fovRadians / fovLength * toLength,
angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => fovLength / fovRadians * Math.atan2(toAngleOpposite, toAngleAdjacent), angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => fovLength / fovRadians * Math.atan2(toAngleOpposite, toAngleAdjacent),
fovRadiansAtDistance: (fovRadians, unitLength, newScreenDistance) => fovRadians / newScreenDistance,
radiansToSegments: (screenRadians) => Math.ceil(screenRadians * segmentsPerRadian) radiansToSegments: (screenRadians) => Math.ceil(screenRadians * segmentsPerRadian)
} }
} }

View File

@ -28,33 +28,34 @@ function lookAheadMS(imuDateMs, lookAheadCfg, override) {
// Create a mesh of vertices in a pattern suitable for TRIANGLE_STRIP // Create a mesh of vertices in a pattern suitable for TRIANGLE_STRIP
function createVertexMesh(fovDetails, monitorDetails, positionVectorNWU) { function createVertexMesh(fovDetails, monitorDetails, positionVectorNWU) {
let fovConversions = fovDetails.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat; let horizontalWrap = fovDetails.monitorWrappingScheme === 'horizontal';
const sideEdgeDistancePixels = fovConversions.centerToFovEdgeDistance( const horizontalConversions = fovDetails.curvedDisplay && horizontalWrap ? fovConversionFns.curved : fovConversionFns.flat;
const sideEdgeDistancePixels = horizontalConversions.centerToFovEdgeDistance(
fovDetails.completeScreenDistancePixels, fovDetails.completeScreenDistancePixels,
fovDetails.sizeAdjustedWidthPixels fovDetails.sizeAdjustedWidthPixels
); );
const horizontalRadians = fovConversions.lengthToRadians( const horizontalRadians = horizontalConversions.lengthToRadians(
fovDetails.defaultDistanceHorizontalRadians, fovDetails.defaultDistanceHorizontalRadians,
fovDetails.widthPixels, fovDetails.widthPixels,
sideEdgeDistancePixels, sideEdgeDistancePixels,
monitorDetails.width monitorDetails.width
); );
const topEdgeDistancePixels = fovConversions.centerToFovEdgeDistance( let verticalWrap = fovDetails.monitorWrappingScheme === 'vertical';
const verticalConversions = fovDetails.curvedDisplay && verticalWrap ? fovConversionFns.curved : fovConversionFns.flat;
const topEdgeDistancePixels = verticalConversions.centerToFovEdgeDistance(
fovDetails.completeScreenDistancePixels, fovDetails.completeScreenDistancePixels,
fovDetails.sizeAdjustedHeightPixels fovDetails.sizeAdjustedHeightPixels
); );
const verticalRadians = fovConversions.lengthToRadians( const verticalRadians = verticalConversions.lengthToRadians(
fovDetails.defaultDistanceVerticalRadians, fovDetails.defaultDistanceVerticalRadians,
fovDetails.heightPixels, fovDetails.heightPixels,
topEdgeDistancePixels, topEdgeDistancePixels,
monitorDetails.height monitorDetails.height
); );
let horizontalWrap = fovDetails.monitorWrappingScheme === 'horizontal'; const xSegments = horizontalConversions.radiansToSegments(horizontalRadians);
let verticalWrap = fovDetails.monitorWrappingScheme === 'vertical'; const ySegments = verticalConversions.radiansToSegments(verticalRadians);
const xSegments = horizontalWrap ? fovConversions.radiansToSegments(horizontalRadians) : 1;
const ySegments = verticalWrap ? fovConversions.radiansToSegments(verticalRadians) : 1;
const texXLeft = 0; const texXLeft = 0;
const texYTop = 0; const texYTop = 0;
@ -419,8 +420,8 @@ export const VirtualDisplayEffect = GObject.registerClass({
this.set_uniform_float(this.get_uniform_location("u_show_banner"), 1, [this.show_banner ? 1.0 : 0.0]); this.set_uniform_float(this.get_uniform_location("u_show_banner"), 1, [this.show_banner ? 1.0 : 0.0]);
} }
perspective(fovHorizontalRadians, aspect, near, far) { perspective(widthUnitDistance, aspect, near, far) {
const f = 1.0 / Math.tan(fovHorizontalRadians / 2.0); const f = 2.0 / widthUnitDistance;
const range = far - near; const range = far - near;
return [ return [
@ -562,15 +563,15 @@ export const VirtualDisplayEffect = GObject.registerClass({
this._initialized = true; this._initialized = true;
const aspect = this.target_monitor.width / this.target_monitor.height; const aspect = this.target_monitor.width / this.target_monitor.height;
const fovRadians = diagonalToCrossFOVs(degreeToRadian(Globals.data_stream.device_data.displayFov), aspect); const fovLengths = diagonalToCrossFOVs(degreeToRadian(Globals.data_stream.device_data.displayFov), aspect);
const projection_matrix = this.perspective( const projection_matrix = this.perspective(
fovRadians.horizontal, fovLengths.widthUnitDistance,
aspect, aspect,
1.0, 1.0,
10000.0 10000.0
); );
this.set_uniform_matrix(this.get_uniform_location("u_projection_matrix"), false, 4, projection_matrix); this.set_uniform_matrix(this.get_uniform_location("u_projection_matrix"), false, 4, projection_matrix);
this.set_uniform_float(this.get_uniform_location("u_fov_vertical_radians"), 1, [fovRadians.vertical]); this.set_uniform_float(this.get_uniform_location("u_fov_vertical_radians"), 1, [fovLengths.verticalRadians]);
this.set_uniform_float(this.get_uniform_location("u_display_resolution"), 2, [this.target_monitor.width, this.target_monitor.height]); this.set_uniform_float(this.get_uniform_location("u_display_resolution"), 2, [this.target_monitor.width, this.target_monitor.height]);
this.set_uniform_float(this.get_uniform_location("u_look_ahead_cfg"), 4, Globals.data_stream.device_data.lookAheadCfg); this.set_uniform_float(this.get_uniform_location("u_look_ahead_cfg"), 4, Globals.data_stream.device_data.lookAheadCfg);
this.set_uniform_float(this.get_uniform_location("u_actor_to_display_ratios"), 2, this.actor_to_display_ratios); this.set_uniform_float(this.get_uniform_location("u_actor_to_display_ratios"), 2, this.actor_to_display_ratios);

View File

@ -7,7 +7,7 @@ import Shell from 'gi://Shell';
import St from 'gi://St'; import St from 'gi://St';
import { VirtualDisplayEffect, SMOOTH_FOLLOW_SLERP_TIMELINE_MS } from './virtualdisplayeffect.js'; import { VirtualDisplayEffect, SMOOTH_FOLLOW_SLERP_TIMELINE_MS } from './virtualdisplayeffect.js';
import { applyQuaternionToVector, degreeToRadian, diagonalToCrossFOVs, fovConversionFns, normalizeVector, vectorMagnitude } from './math.js'; import { applyQuaternionToVector, degreeToRadian, diagonalToCrossFOVs, fovConversionFns, vectorMagnitude } from './math.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js';
@ -21,8 +21,11 @@ const UNFOCUS_THRESHOLD = 1.1 / 2.0;
// returns how far the look vector is from the center of the monitor, as a percentage of the monitor's dimensions // returns how far the look vector is from the center of the monitor, as a percentage of the monitor's dimensions
function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVector, monitorDetails, upAngleToLength, westAngleToLength) { function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVector, monitorDetails, upAngleToLength, westAngleToLength) {
// since the monitor vector has been modified to be relative to the lens position, we need to calculate its distance from the lens
// we need to adjust all angle-based lengths based on new vector distance
const monitorDistance = vectorMagnitude(monitorVector); const monitorDistance = vectorMagnitude(monitorVector);
const distanceAdjustment = monitorDistance / fovDetails.completeScreenDistancePixels; const distanceAdjustment = monitorDistance / fovDetails.completeScreenDistancePixels;
const vectorUpPixels = upAngleToLength( const vectorUpPixels = upAngleToLength(
fovDetails.defaultDistanceVerticalRadians, fovDetails.defaultDistanceVerticalRadians,
fovDetails.heightPixels, fovDetails.heightPixels,
@ -664,10 +667,10 @@ export const VirtualDisplaysActor = GObject.registerClass({
this._property_connections.push(this.connect(`notify::${property}`, fn.bind(this))); this._property_connections.push(this.connect(`notify::${property}`, fn.bind(this)));
}).bind(this); }).bind(this);
notifyToFunction('toggle-display-distance-start', this._handle_display_distance_properties_change); notifyToFunction('toggle-display-distance-start', this._handle_display_size_distance_change);
notifyToFunction('toggle-display-distance-end', this._handle_display_distance_properties_change); notifyToFunction('toggle-display-distance-end', this._handle_display_size_distance_change);
notifyToFunction('display-distance', this._handle_display_distance_properties_change); notifyToFunction('display-distance', this._handle_display_size_distance_change);
notifyToFunction('display-size', this._handle_display_size_change); notifyToFunction('display-size', this._handle_display_size_distance_change);
notifyToFunction('monitor-wrapping-scheme', this._update_monitor_placements); notifyToFunction('monitor-wrapping-scheme', this._update_monitor_placements);
notifyToFunction('monitor-spacing', this._update_monitor_placements); notifyToFunction('monitor-spacing', this._update_monitor_placements);
notifyToFunction('headset-display-as-viewport-center', this._update_monitor_placements); notifyToFunction('headset-display-as-viewport-center', this._update_monitor_placements);
@ -678,8 +681,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
notifyToFunction('custom-banner-enabled', this._handle_banner_update); notifyToFunction('custom-banner-enabled', this._handle_banner_update);
notifyToFunction('framerate-cap', this._handle_frame_rate_cap_change); notifyToFunction('framerate-cap', this._handle_frame_rate_cap_change);
notifyToFunction('smooth-follow-enabled', this._handle_smooth_follow_enabled_change); notifyToFunction('smooth-follow-enabled', this._handle_smooth_follow_enabled_change);
this._handle_display_size_change(false); this._handle_display_size_distance_change();
this._handle_display_distance_properties_change();
this._handle_frame_rate_cap_change(); this._handle_frame_rate_cap_change();
const actorToDisplayRatios = [ const actorToDisplayRatios = [
@ -860,33 +862,58 @@ export const VirtualDisplaysActor = GObject.registerClass({
_fov_details() { _fov_details() {
const aspect = this.target_monitor.width / this.target_monitor.height; const aspect = this.target_monitor.width / this.target_monitor.height;
const fovRadians = diagonalToCrossFOVs(degreeToRadian(Globals.data_stream.device_data.displayFov), aspect); const fovLengths = diagonalToCrossFOVs(degreeToRadian(Globals.data_stream.device_data.displayFov), aspect);
const monitorWrappingScheme = this._actual_wrap_scheme();
const defaultDistance = this._display_distance_default();
const horizontalConversions = this.curved_display && monitorWrappingScheme === 'horizontal' ? fovConversionFns.curved : fovConversionFns.flat;
const verticalConversions = this.curved_display && monitorWrappingScheme === 'vertical' ? fovConversionFns.curved : fovConversionFns.flat;
// adjusted angles based on how far away the screens are e.g. a closer screen takes up a larger slice of our FOV // adjusted angles based on how far away the screens are e.g. a closer screen takes up a larger slice of our FOV
const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / this._display_distance_default()); const defaultDistanceVerticalRadians = verticalConversions.fovRadiansAtDistance(
const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / this._display_distance_default()); fovLengths.verticalRadians,
fovLengths.heightUnitDistance,
defaultDistance
);
const defaultDistanceHorizontalRadians = horizontalConversions.fovRadiansAtDistance(
fovLengths.horizontalRadians,
fovLengths.widthUnitDistance,
defaultDistance
);
// 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 fullScreenDistancePixels = this.target_monitor.height / 2 / Math.tan(fovRadians.vertical / 2); const fullScreenDistancePixels = this.target_monitor.width / fovLengths.widthUnitDistance;
const lensDistancePixels = fullScreenDistancePixels / (1.0 - Globals.data_stream.device_data.lensDistanceRatio) - fullScreenDistancePixels;
// TODO - fix usage of full/complete distance values, so we can factor lens distance back in
const lensDistancePixels = 0.0; // fullScreenDistancePixels / (1.0 - Globals.data_stream.device_data.lensDistanceRatio) - fullScreenDistancePixels;
// 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 = this.target_monitor.height / 2 / Math.tan(defaultDistanceVerticalRadians / 2); const lensToScreenDistancePixels = fullScreenDistancePixels * defaultDistance;
const completeScreenDistancePixels = lensToScreenDistance + lensDistancePixels; const completeScreenDistancePixels = lensToScreenDistancePixels + lensDistancePixels;
const sizeAdjustedWidthPixels = this.target_monitor.width * this._distance_adjusted_size; Globals.logger.log_debug(`\t\t\tFOV Details: ${JSON.stringify({
const sizeAdjustedHeightPixels = this.target_monitor.height * this._distance_adjusted_size;
return {
widthPixels: this.target_monitor.width, widthPixels: this.target_monitor.width,
sizeAdjustedWidthPixels, sizeAdjustedWidthPixels: this.target_monitor.width * this._distance_adjusted_size,
heightPixels: this.target_monitor.height, heightPixels: this.target_monitor.height,
sizeAdjustedHeightPixels, sizeAdjustedHeightPixels: this.target_monitor.height * this._distance_adjusted_size,
defaultDistanceVerticalRadians, defaultDistanceVerticalRadians,
defaultDistanceHorizontalRadians, defaultDistanceHorizontalRadians,
lensDistancePixels, lensDistancePixels,
fullScreenDistancePixels, fullScreenDistancePixels,
completeScreenDistancePixels, completeScreenDistancePixels,
monitorWrappingScheme: this._actual_wrap_scheme(), monitorWrappingScheme,
curvedDisplay: this.curved_display
})}`);
return {
widthPixels: this.target_monitor.width,
sizeAdjustedWidthPixels: this.target_monitor.width * this._distance_adjusted_size,
heightPixels: this.target_monitor.height,
sizeAdjustedHeightPixels: this.target_monitor.height * this._distance_adjusted_size,
defaultDistanceVerticalRadians,
defaultDistanceHorizontalRadians,
lensDistancePixels,
fullScreenDistancePixels,
completeScreenDistancePixels,
monitorWrappingScheme,
curvedDisplay: this.curved_display curvedDisplay: this.curved_display
}; };
} }
@ -944,17 +971,12 @@ export const VirtualDisplaysActor = GObject.registerClass({
} }
} }
_handle_display_distance_properties_change() { _handle_display_size_distance_change() {
this._distance_adjusted_size = this._display_distance_default() * this.display_size; this._distance_adjusted_size = this._display_distance_default() * this.display_size;
const distance_from_end = Math.abs(this.display_distance - this.toggle_display_distance_end); const distance_from_end = Math.abs(this.display_distance - this.toggle_display_distance_end);
const distance_from_start = Math.abs(this.display_distance - this.toggle_display_distance_start); const distance_from_start = Math.abs(this.display_distance - this.toggle_display_distance_start);
this._is_display_distance_at_end = distance_from_end < distance_from_start; this._is_display_distance_at_end = distance_from_end < distance_from_start;
this._update_monitor_placements();
}
_handle_display_size_change(update_placements = true) {
this._distance_adjusted_size = this._display_distance_default() * this.display_size;
const sizeComplement = (1.0 - this._distance_adjusted_size) / 2.0; const sizeComplement = (1.0 - this._distance_adjusted_size) / 2.0;
const sizeViewportOffsetX = sizeComplement * this.target_monitor.width; const sizeViewportOffsetX = sizeComplement * this.target_monitor.width;
@ -965,7 +987,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
width: monitor.width * this._distance_adjusted_size, width: monitor.width * this._distance_adjusted_size,
height: monitor.height * this._distance_adjusted_size height: monitor.height * this._distance_adjusted_size
})); }));
if (update_placements) this._update_monitor_placements(); this._update_monitor_placements();
} }
_handle_banner_update() { _handle_banner_update() {