The open source OpenXR runtime
1// Copyright 2025, Beyley Cardellio
2// SPDX-License-Identifier: BSL-1.0
3/*!
4 * @file
5 * @brief Handles calculating the distortion profile for Oculus Rift devices.
6 * @author Beyley Cardellio <ep1cm1n10n123@gmail.com>
7 * @ingroup drv_rift
8 */
9
10#include "rift_distortion.h"
11
12#include "math/m_vec2.h"
13
14#define HMD_TRACE(hmd, ...) U_LOG_XDEV_IFL_T(&hmd->base, hmd->log_level, __VA_ARGS__)
15#define HMD_DEBUG(hmd, ...) U_LOG_XDEV_IFL_D(&hmd->base, hmd->log_level, __VA_ARGS__)
16#define HMD_INFO(hmd, ...) U_LOG_XDEV_IFL_I(&hmd->base, hmd->log_level, __VA_ARGS__)
17#define HMD_WARN(hmd, ...) U_LOG_XDEV_IFL_W(&hmd->base, hmd->log_level, __VA_ARGS__)
18#define HMD_ERROR(hmd, ...) U_LOG_XDEV_IFL_E(&hmd->base, hmd->log_level, __VA_ARGS__)
19
20static float
21rift_catmull_rom_spline(struct rift_catmull_rom_distortion_data *catmull, float scaled_value)
22{
23 float scaled_value_floor = floorf(scaled_value);
24 scaled_value_floor = CLAMP(scaled_value_floor, 0, CATMULL_COEFFICIENTS - 1);
25
26 float t = scaled_value - scaled_value_floor;
27 int k = (int)scaled_value_floor;
28
29 float p0, p1, m0, m1;
30 switch (k) {
31 case 0:
32 p0 = 1.0f;
33 m0 = (catmull->k[1] - catmull->k[0]);
34 p1 = catmull->k[1];
35 m1 = 0.5f * (catmull->k[2] - catmull->k[0]);
36 break;
37 default:
38 p0 = catmull->k[k];
39 m0 = 0.5f * (catmull->k[k + 1] - catmull->k[k - 1]);
40 p1 = catmull->k[k + 1];
41 m1 = 0.5f * (catmull->k[k + 2] - catmull->k[k]);
42 break;
43 case CATMULL_COEFFICIENTS - 2:
44 p0 = catmull->k[CATMULL_COEFFICIENTS - 2];
45 m0 = 0.5f * (catmull->k[CATMULL_COEFFICIENTS - 1] - catmull->k[CATMULL_COEFFICIENTS - 2]);
46 p1 = catmull->k[CATMULL_COEFFICIENTS - 1];
47 m1 = catmull->k[CATMULL_COEFFICIENTS - 1] - catmull->k[CATMULL_COEFFICIENTS - 2];
48 break;
49 case CATMULL_COEFFICIENTS - 1:
50 p0 = catmull->k[CATMULL_COEFFICIENTS - 1];
51 m0 = catmull->k[CATMULL_COEFFICIENTS - 1] - catmull->k[CATMULL_COEFFICIENTS - 2];
52 p1 = p0 + m0;
53 m1 = m0;
54 break;
55 }
56
57 float omt = 1.0f - t;
58
59 float res = (p0 * (1.0f + 2.0f * t) + m0 * t) * omt * omt + (p1 * (1.0f + 2.0f * omt) - m1 * omt) * t * t;
60
61 return res;
62}
63
64static float
65rift_distortion_distance_scale_squared(struct rift_lens_distortion *lens_distortion, float distance_squared)
66{
67 float scale = 1.0f;
68
69 switch (lens_distortion->distortion_version) {
70 case RIFT_LENS_DISTORTION_LCSV_CATMULL_ROM_10_VERSION_1: {
71 struct rift_catmull_rom_distortion_data data = lens_distortion->data.lcsv_catmull_rom_10;
72
73 float scaled_distance_squared =
74 (float)(CATMULL_COEFFICIENTS - 1) * distance_squared / (data.max_r * data.max_r);
75
76 return rift_catmull_rom_spline(&data, scaled_distance_squared);
77 }
78 default: return scale;
79 }
80}
81
82struct xrt_vec3
83rift_distortion_distance_scale_squared_split_chroma(struct rift_lens_distortion *lens_distortion,
84 float distance_squared)
85{
86 float scale = rift_distortion_distance_scale_squared(lens_distortion, distance_squared);
87
88 struct xrt_vec3 scale_split;
89 scale_split.x = scale;
90 scale_split.y = scale;
91 scale_split.z = scale;
92
93 switch (lens_distortion->distortion_version) {
94 case RIFT_LENS_DISTORTION_LCSV_CATMULL_ROM_10_VERSION_1: {
95 struct rift_catmull_rom_distortion_data data = lens_distortion->data.lcsv_catmull_rom_10;
96
97 scale_split.x *= 1.0f + data.chromatic_abberation[0] + distance_squared * data.chromatic_abberation[1];
98 scale_split.z *= 1.0f + data.chromatic_abberation[2] + distance_squared * data.chromatic_abberation[3];
99 break;
100 }
101 }
102
103 return scale_split;
104}
105
106struct rift_distortion_render_info
107rift_get_distortion_render_info(struct rift_hmd *hmd, uint32_t view)
108{
109 struct rift_lens_distortion *distortion = &hmd->lens_distortions[hmd->distortion_in_use];
110
111 float display_width_meters = MICROMETERS_TO_METERS(hmd->display_info.display_width);
112 float display_height_meters = MICROMETERS_TO_METERS(hmd->display_info.display_height);
113
114 float lens_separation_meters = MICROMETERS_TO_METERS(hmd->display_info.lens_separation);
115 float center_from_top_meters = MICROMETERS_TO_METERS(hmd->display_info.center_v);
116
117 struct xrt_vec2 pixels_per_meter;
118 pixels_per_meter.x =
119 (float)hmd->display_info.resolution_x / (display_width_meters - hmd->extra_display_info.screen_gap_meters);
120 pixels_per_meter.y = (float)hmd->display_info.resolution_y / display_height_meters;
121
122 struct xrt_vec2 pixels_per_tan_angle_at_center;
123 pixels_per_tan_angle_at_center.x =
124 pixels_per_meter.x * distortion->data.lcsv_catmull_rom_10.meters_per_tan_angle_at_center;
125 pixels_per_tan_angle_at_center.y =
126 pixels_per_meter.y * distortion->data.lcsv_catmull_rom_10.meters_per_tan_angle_at_center;
127
128 struct xrt_vec2 tan_eye_angle_scale;
129 tan_eye_angle_scale.x =
130 0.25f * (display_width_meters / distortion->data.lcsv_catmull_rom_10.meters_per_tan_angle_at_center);
131 tan_eye_angle_scale.y =
132 0.5f * (display_height_meters / distortion->data.lcsv_catmull_rom_10.meters_per_tan_angle_at_center);
133
134 float visible_width_of_one_eye = 0.5f * (display_width_meters - hmd->extra_display_info.screen_gap_meters);
135 float center_from_left_meters = (display_width_meters - lens_separation_meters) * 0.5f;
136
137 struct xrt_vec2 lens_center;
138 lens_center.x = (center_from_left_meters / visible_width_of_one_eye) * 2.0f - 1.0f;
139 lens_center.y = (center_from_top_meters / display_height_meters) * 2.0f - 1.0f;
140
141 // if we're on the right eye, flip the lens center
142 if (view != 0) {
143 lens_center.x *= -1;
144 }
145
146 return (struct rift_distortion_render_info){
147 .distortion = distortion,
148 .lens_center = lens_center,
149 .pixels_per_tan_angle_at_center = pixels_per_tan_angle_at_center,
150 .tan_eye_angle_scale = tan_eye_angle_scale,
151 };
152}
153
154static struct rift_viewport_fov_tan
155rift_calculate_fov_from_eye_position(
156 float eye_relief, float offset_to_right, float offset_downwards, float lens_diameter, float extra_eye_rotation)
157{
158 float half_lens_diameter = lens_diameter * 0.5f;
159
160 struct rift_viewport_fov_tan fov_port;
161 fov_port.up_tan = (half_lens_diameter + offset_downwards) / eye_relief;
162 fov_port.down_tan = (half_lens_diameter - offset_downwards) / eye_relief;
163 fov_port.left_tan = (half_lens_diameter + offset_to_right) / eye_relief;
164 fov_port.right_tan = (half_lens_diameter - offset_to_right) / eye_relief;
165
166 if (extra_eye_rotation > 0.0f) {
167 extra_eye_rotation = CLAMP(extra_eye_rotation, 0, 30.0f);
168
169 float eyeball_center_to_pupil = 0.0135f;
170 float eyeball_lateral_pull = 0.001f * (extra_eye_rotation / DEG_TO_RAD(30.0f));
171 float extra_translation = eyeball_center_to_pupil * sinf(extra_eye_rotation) + eyeball_lateral_pull;
172 float extra_relief = eyeball_center_to_pupil * (1.0f - cosf(extra_eye_rotation));
173
174 fov_port.up_tan = fmaxf(fov_port.up_tan, (half_lens_diameter + offset_downwards + extra_translation) /
175 (eye_relief + extra_relief));
176 fov_port.down_tan =
177 fmaxf(fov_port.down_tan,
178 (half_lens_diameter - offset_downwards + extra_translation) / (eye_relief + extra_relief));
179 fov_port.left_tan =
180 fmaxf(fov_port.left_tan,
181 (half_lens_diameter + offset_to_right + extra_translation) / (eye_relief + extra_relief));
182 fov_port.right_tan =
183 fmaxf(fov_port.right_tan,
184 (half_lens_diameter - offset_to_right + extra_translation) / (eye_relief + extra_relief));
185 }
186
187 return fov_port;
188}
189
190static struct xrt_vec2
191rift_transform_screen_ndc_to_tan_fov_space(struct rift_distortion_render_info *distortion, struct xrt_vec2 screen_ndc)
192{
193 struct xrt_vec2 tan_eye_angle_distorted = {
194 (screen_ndc.x - distortion->lens_center.x) * distortion->tan_eye_angle_scale.x,
195 (screen_ndc.y - distortion->lens_center.y) * distortion->tan_eye_angle_scale.y};
196
197 float distance_squared = (tan_eye_angle_distorted.x * tan_eye_angle_distorted.x) +
198 (tan_eye_angle_distorted.y * tan_eye_angle_distorted.y);
199
200 float distortion_scale = rift_distortion_distance_scale_squared(distortion->distortion, distance_squared);
201
202 return m_vec2_mul_scalar(tan_eye_angle_distorted, distortion_scale);
203}
204
205static struct rift_viewport_fov_tan
206rift_fov_find_range(struct xrt_vec2 from,
207 struct xrt_vec2 to,
208 int num_steps,
209 struct rift_distortion_render_info *distortion)
210{
211 struct rift_viewport_fov_tan fov_port = {0.0f, 0.0f, 0.0f, 0.0f};
212
213 float step_scale = 1.0f / (num_steps - 1);
214 for (int step = 0; step < num_steps; step++) {
215 float lerp_factor = step_scale * (float)step;
216 struct xrt_vec2 sample = m_vec2_add(from, m_vec2_mul_scalar(m_vec2_sub(to, from), lerp_factor));
217 struct xrt_vec2 tan_eye_angle = rift_transform_screen_ndc_to_tan_fov_space(distortion, sample);
218
219 fov_port.left_tan = fmaxf(fov_port.left_tan, -tan_eye_angle.x);
220 fov_port.right_tan = fmaxf(fov_port.right_tan, tan_eye_angle.x);
221 fov_port.up_tan = fmaxf(fov_port.up_tan, -tan_eye_angle.y);
222 fov_port.down_tan = fmaxf(fov_port.down_tan, tan_eye_angle.y);
223 }
224
225 return fov_port;
226}
227
228static struct rift_viewport_fov_tan
229rift_get_physical_screen_fov(struct rift_distortion_render_info *distortion)
230{
231 struct xrt_vec2 lens_center = distortion->lens_center;
232
233 struct rift_viewport_fov_tan left_fov_port =
234 rift_fov_find_range(lens_center, (struct xrt_vec2){-1.0f, lens_center.y}, 10, distortion);
235
236 struct rift_viewport_fov_tan right_fov_port =
237 rift_fov_find_range(lens_center, (struct xrt_vec2){1.0f, lens_center.y}, 10, distortion);
238
239 struct rift_viewport_fov_tan up_fov_port =
240 rift_fov_find_range(lens_center, (struct xrt_vec2){lens_center.x, -1.0f}, 10, distortion);
241
242 struct rift_viewport_fov_tan down_fov_port =
243 rift_fov_find_range(lens_center, (struct xrt_vec2){lens_center.x, 1.0f}, 10, distortion);
244
245 return (struct rift_viewport_fov_tan){.left_tan = left_fov_port.left_tan,
246 .right_tan = right_fov_port.right_tan,
247 .up_tan = up_fov_port.up_tan,
248 .down_tan = down_fov_port.down_tan};
249}
250
251static struct rift_viewport_fov_tan
252rift_clamp_fov_to_physical_screen_fov(struct rift_distortion_render_info *distortion,
253 struct rift_viewport_fov_tan fov_port)
254{
255 struct rift_viewport_fov_tan result_fov_port;
256 struct rift_viewport_fov_tan physical_fov_port = rift_get_physical_screen_fov(distortion);
257
258 result_fov_port.left_tan = fminf(fov_port.left_tan, physical_fov_port.left_tan);
259 result_fov_port.right_tan = fminf(fov_port.right_tan, physical_fov_port.right_tan);
260 result_fov_port.up_tan = fminf(fov_port.up_tan, physical_fov_port.up_tan);
261 result_fov_port.down_tan = fminf(fov_port.down_tan, physical_fov_port.down_tan);
262
263 return result_fov_port;
264}
265
266struct rift_viewport_fov_tan
267rift_calculate_fov_from_hmd(struct rift_hmd *hmd, struct rift_distortion_render_info *distortion, uint32_t view)
268{
269 float eye_relief = distortion->distortion->eye_relief;
270
271 struct rift_viewport_fov_tan fov_port;
272 fov_port = rift_calculate_fov_from_eye_position(eye_relief, 0, 0, hmd->extra_display_info.lens_diameter_meters,
273 DEFAULT_EXTRA_EYE_ROTATION);
274
275 fov_port = rift_clamp_fov_to_physical_screen_fov(distortion, fov_port);
276
277 return fov_port;
278}
279
280struct rift_scale_and_offset
281rift_calculate_ndc_scale_and_offset_from_fov(struct rift_viewport_fov_tan *fov)
282{
283 struct xrt_vec2 proj_scale = {.x = 2.0f / (fov->left_tan + fov->right_tan),
284 .y = 2.0f / (fov->up_tan + fov->down_tan)};
285
286 struct xrt_vec2 proj_offset = {.x = (fov->left_tan - fov->right_tan) * proj_scale.x * 0.5,
287 .y = (fov->up_tan - fov->down_tan) * proj_scale.y * 0.5f};
288
289 return (struct rift_scale_and_offset){.scale = proj_scale, .offset = proj_offset};
290}
291
292struct rift_scale_and_offset
293rift_calculate_uv_scale_and_offset_from_ndc_scale_and_offset(struct rift_scale_and_offset eye_to_source_ndc)
294{
295 struct rift_scale_and_offset result = eye_to_source_ndc;
296 result.scale = m_vec2_mul_scalar(result.scale, 0.5f);
297 result.offset = m_vec2_add_scalar(m_vec2_mul_scalar(result.offset, 0.5f), 0.5f);
298 return result;
299}
300
301static struct xrt_uv_triplet
302rift_transform_screen_ndc_to_tan_fov_space_chroma(struct rift_distortion_render_info *distortion,
303 struct xrt_vec2 screen_ndc)
304{
305 struct xrt_vec2 tan_eye_angle_distorted = {
306 (screen_ndc.x - distortion->lens_center.x) * distortion->tan_eye_angle_scale.x,
307 (screen_ndc.y - distortion->lens_center.y) * distortion->tan_eye_angle_scale.y};
308
309 float distance_squared = (tan_eye_angle_distorted.x * tan_eye_angle_distorted.x) +
310 (tan_eye_angle_distorted.y * tan_eye_angle_distorted.y);
311
312 struct xrt_vec3 distortion_scales =
313 rift_distortion_distance_scale_squared_split_chroma(distortion->distortion, distance_squared);
314
315 return (struct xrt_uv_triplet){
316 .r = {tan_eye_angle_distorted.x * distortion_scales.x, tan_eye_angle_distorted.y * distortion_scales.x},
317 .g = {tan_eye_angle_distorted.x * distortion_scales.y, tan_eye_angle_distorted.y * distortion_scales.y},
318 .b = {tan_eye_angle_distorted.x * distortion_scales.z, tan_eye_angle_distorted.y * distortion_scales.z},
319 };
320}
321
322// unused math functions which may be useful in the future for stuff like calculating FOV.
323// disabled to not give unused warnings
324#if 0
325static float
326rift_distortion(struct rift_lens_distortion *lens_distortion, float distance)
327{
328 return distance * rift_distortion_distance_scale_squared(lens_distortion, distance * distance);
329}
330
331static float
332rift_distortion_distance_inverse(struct rift_lens_distortion *lens_distortion, float distance)
333{
334 assert(distance <= 20.0f);
335
336 float delta = distance * 0.25f;
337
338 float s = distance * 0.25f;
339 float d = fabsf(distance - rift_distortion(lens_distortion, s));
340
341 for (int i = 0; i < 20; i++) {
342 float s_up = s + delta;
343 float s_down = s - delta;
344 float d_up = fabsf(distance - rift_distortion(lens_distortion, s_up));
345 float d_down = fabsf(distance - rift_distortion(lens_distortion, s_down));
346
347 if (d_up < d) {
348 s = s_up;
349 d = d_up;
350 } else if (d_down < d) {
351 s = s_down;
352 d = d_down;
353 } else {
354 delta *= 0.5f;
355 }
356 }
357
358 return s;
359}
360
361static struct xrt_vec2
362rift_transform_tan_fov_space_to_render_target_tex_uv(struct rift_scale_and_offset *eye_to_source_uv,
363 struct xrt_vec2 tan_eye_angle)
364{
365 return m_vec2_add(m_vec2_mul(tan_eye_angle, eye_to_source_uv->scale), eye_to_source_uv->offset);
366}
367
368static struct xrt_vec2
369rift_transform_render_target_ndc_to_tan_fov_space(struct rift_scale_and_offset *eye_to_source_ndc, struct xrt_vec2 ndc)
370{
371 return m_vec2_div(m_vec2_sub(ndc, eye_to_source_ndc->offset), eye_to_source_ndc->scale);
372}
373
374static struct xrt_vec2
375rift_transform_tan_fov_space_to_screen_ndc(struct rift_distortion_render_info *distortion,
376 struct xrt_vec2 tan_eye_angle)
377{
378 float tan_eye_angle_radius = m_vec2_len(tan_eye_angle);
379 float tan_eye_angle_distorted_radius =
380 rift_distortion_distance_inverse(distortion->distortion, tan_eye_angle_radius);
381
382 struct xrt_vec2 tan_eye_angle_distorted = tan_eye_angle;
383 if (tan_eye_angle_radius > 0) {
384 tan_eye_angle_distorted =
385 m_vec2_mul_scalar(tan_eye_angle, tan_eye_angle_distorted_radius / tan_eye_angle_radius);
386 }
387
388 return m_vec2_add(m_vec2_div(tan_eye_angle_distorted, distortion->tan_eye_angle_scale),
389 distortion->lens_center);
390}
391#endif
392
393xrt_result_t
394rift_hmd_compute_distortion(struct xrt_device *dev, uint32_t view, float u, float v, struct xrt_uv_triplet *out_result)
395{
396#define TO_NDC(x) ((x * 2) - 1)
397
398 struct rift_hmd *hmd = rift_hmd(dev);
399
400 struct xrt_vec2 source_ndc = {TO_NDC(u), TO_NDC(v)};
401
402 struct rift_distortion_render_info distortion_render_info = rift_get_distortion_render_info(hmd, 0);
403
404 struct rift_scale_and_offset *eye_to_source_uv = &hmd->extra_display_info.eye_to_source_uv;
405
406 struct xrt_uv_triplet tan_fov_chroma =
407 rift_transform_screen_ndc_to_tan_fov_space_chroma(&distortion_render_info, source_ndc);
408
409#if 0 // no distortion (green channel doesn't have any chromatic aberration correction)
410 struct xrt_uv_triplet sample_tex_coord = {
411 .r = m_vec2_add(m_vec2_mul(tan_fov_chroma.g, eye_to_source_uv->scale), eye_to_source_uv->offset),
412 .g = m_vec2_add(m_vec2_mul(tan_fov_chroma.g, eye_to_source_uv->scale), eye_to_source_uv->offset),
413 .b = m_vec2_add(m_vec2_mul(tan_fov_chroma.g, eye_to_source_uv->scale), eye_to_source_uv->offset)};
414#else
415 struct xrt_uv_triplet sample_tex_coord = {
416 .r = m_vec2_add(m_vec2_mul(tan_fov_chroma.r, eye_to_source_uv->scale), eye_to_source_uv->offset),
417 .g = m_vec2_add(m_vec2_mul(tan_fov_chroma.g, eye_to_source_uv->scale), eye_to_source_uv->offset),
418 .b = m_vec2_add(m_vec2_mul(tan_fov_chroma.b, eye_to_source_uv->scale), eye_to_source_uv->offset)};
419#endif
420
421 *out_result = sample_tex_coord;
422
423 return XRT_SUCCESS;
424
425#undef TO_NDC
426}
427
428void
429rift_fill_in_default_distortions(struct rift_hmd *hmd)
430{
431 hmd->num_lens_distortions = 2;
432 hmd->lens_distortions = calloc(hmd->num_lens_distortions, sizeof(struct rift_lens_distortion));
433
434 uint16_t i = 0;
435
436 struct rift_catmull_rom_distortion_data distortion_data;
437
438 // TODO: dump these from the latest oculus runtime
439 // TODO: select the right distorions based on rift variant, right now this is hard-coded to the DK2
440
441 hmd->lens_distortions[i].distortion_version = RIFT_LENS_DISTORTION_LCSV_CATMULL_ROM_10_VERSION_1;
442 hmd->lens_distortions[i].eye_relief = 0.008f;
443
444 distortion_data.meters_per_tan_angle_at_center = 0.036f;
445 distortion_data.max_r = 1.0f;
446
447 distortion_data.k[0] = 1.003f;
448 distortion_data.k[1] = 1.02f;
449 distortion_data.k[2] = 1.042f;
450 distortion_data.k[3] = 1.066f;
451 distortion_data.k[4] = 1.094f;
452 distortion_data.k[5] = 1.126f;
453 distortion_data.k[6] = 1.162f;
454 distortion_data.k[7] = 1.203f;
455 distortion_data.k[8] = 1.25f;
456 distortion_data.k[9] = 1.31f;
457 distortion_data.k[10] = 1.38f;
458
459 distortion_data.chromatic_abberation[0] = -0.0112f;
460 distortion_data.chromatic_abberation[1] = -0.015f;
461 distortion_data.chromatic_abberation[2] = 0.0187f;
462 distortion_data.chromatic_abberation[3] = 0.015f;
463
464 hmd->lens_distortions[i].data.lcsv_catmull_rom_10 = distortion_data;
465
466 i += 1;
467
468 hmd->lens_distortions[i].distortion_version = RIFT_LENS_DISTORTION_LCSV_CATMULL_ROM_10_VERSION_1;
469 hmd->lens_distortions[i].eye_relief = 0.018f;
470
471 distortion_data.meters_per_tan_angle_at_center = 0.036f;
472 distortion_data.max_r = 1.0f;
473
474 distortion_data.k[0] = 1.003f;
475 distortion_data.k[1] = 1.02f;
476 distortion_data.k[2] = 1.042f;
477 distortion_data.k[3] = 1.066f;
478 distortion_data.k[4] = 1.094f;
479 distortion_data.k[5] = 1.126f;
480 distortion_data.k[6] = 1.162f;
481 distortion_data.k[7] = 1.203f;
482 distortion_data.k[8] = 1.25f;
483 distortion_data.k[9] = 1.31f;
484 distortion_data.k[10] = 1.38f;
485
486 distortion_data.chromatic_abberation[0] = -0.015f;
487 distortion_data.chromatic_abberation[1] = -0.02f;
488 distortion_data.chromatic_abberation[2] = 0.025f;
489 distortion_data.chromatic_abberation[3] = 0.02f;
490
491 hmd->lens_distortions[i].data.lcsv_catmull_rom_10 = distortion_data;
492
493 i += 1;
494
495 // TODO: let the user specify which distortion is in use with an env var,
496 // and interpolate the distortions for the user's specific eye relief setting
497 hmd->distortion_in_use = 1;
498}