NxrProfile.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. // Copyright 2016 Nibiru. All rights reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. using System;
  15. using UnityEngine;
  16. // AR parameters of each mobile phone
  17. /// @cond
  18. /// Measurements of a particular phone in a particular AR viewer.
  19. namespace Nxr.Internal
  20. {
  21. [System.Serializable]
  22. public class NxrProfile
  23. {
  24. public NxrProfile Clone()
  25. {
  26. return new NxrProfile
  27. {
  28. screen = this.screen,
  29. viewer = this.viewer
  30. };
  31. }
  32. /// Information about the screen. All distances are in meters, measured relative to how
  33. /// the phone is expected to be seated in the viewer, i.e. landscape orientation.
  34. [System.Serializable]
  35. public struct Screen
  36. {
  37. public float width; // The long edge of the phone.
  38. public float height; // The short edge of the phone.
  39. public float border; // Distance from bottom of the phone to the bottom edge of screen.
  40. }
  41. /// Information about the lens placement in the viewer. All distances are in meters.
  42. [System.Serializable]
  43. public struct Lenses
  44. {
  45. public float separation; // Center to center.
  46. public float offset; // Offset of lens center from top or bottom of viewer.
  47. public float screenDistance; // Distance from lens center to the phone screen.
  48. public int alignment; // Determines whether lenses are placed relative to top, bottom or
  49. // center. It is actually a signum (-1, 0, +1) relating the scale of
  50. // the offset's coordinates to the device coordinates.
  51. public const int AlignTop = -1; // Offset is measured down from top of device.
  52. public const int AlignCenter = 0; // Center alignment ignores offset, hence scale is zero.
  53. public const int AlignBottom = 1; // Offset is measured up from bottom of device.
  54. }
  55. /// Information about the viewing angles through the lenses. All angles in degrees, measured
  56. /// away from the optical axis, i.e. angles are all positive. It is assumed that left and right
  57. /// eye FOVs are mirror images, so that both have the same inner and outer angles. Angles do not
  58. /// need to account for the limits due to screen size.
  59. [System.Serializable]
  60. public struct MaxFOV
  61. {
  62. public float outer; // Towards the side of the screen.
  63. public float inner; // Towards the center line of the screen.
  64. public float upper; // Towards the top of the screen.
  65. public float lower; // Towards the bottom of the screen.
  66. }
  67. /// Information on how the lens distorts light rays. Also used for the (approximate) inverse
  68. /// distortion. Assumes a radially symmetric pincushion/barrel distortion model.
  69. [System.Serializable]
  70. public struct Distortion
  71. {
  72. private float[] coef;
  73. public float[] Coef
  74. {
  75. get
  76. {
  77. return coef;
  78. }
  79. set
  80. {
  81. if (value != null)
  82. {
  83. coef = (float[])value.Clone();
  84. }
  85. else
  86. {
  87. coef = null;
  88. }
  89. }
  90. }
  91. public float distort(float r)
  92. {
  93. float r2 = r * r;
  94. float ret = 0;
  95. for (int j = coef.Length - 1; j >= 0; j--)
  96. {
  97. ret = r2 * (ret + coef[j]);
  98. }
  99. return (ret + 1) * r;
  100. }
  101. public float distortInv(float radius)
  102. {
  103. // Secant method.
  104. float r0 = 0;
  105. float r1 = 1;
  106. float dr0 = radius - distort(r0);
  107. while (Mathf.Abs(r1 - r0) > 0.0001f)
  108. {
  109. float dr1 = radius - distort(r1);
  110. float r2 = r1 - dr1 * ((r1 - r0) / (dr1 - dr0));
  111. r0 = r1;
  112. r1 = r2;
  113. dr0 = dr1;
  114. }
  115. return r1;
  116. }
  117. }
  118. /// Information about a particular device, including specfications on its lenses, FOV,
  119. /// and distortion and inverse distortion coefficients.
  120. [System.Serializable]
  121. public struct Viewer
  122. {
  123. public Lenses lenses;
  124. public MaxFOV maxFOV;
  125. public Distortion distortion;
  126. public Distortion inverse;
  127. }
  128. /// Screen parameters of a Cardboard device.
  129. public Screen screen;
  130. /// Viewer parameters of a Cardboard device.
  131. public Viewer viewer;
  132. /// The vertical offset of the lens centers from the screen center.
  133. public float VerticalLensOffset
  134. {
  135. get
  136. {
  137. return (viewer.lenses.offset - screen.border - screen.height / 2) * viewer.lenses.alignment;
  138. }
  139. }
  140. /// Some known screen profiles.
  141. public enum ScreenSizes
  142. {
  143. Nexus5,
  144. Nexus6,
  145. GalaxyS6,
  146. GalaxyNote4,
  147. LGG3
  148. };
  149. /// Parameters for a Nexus 5 device.
  150. public static readonly Screen Nexus5 = new Screen
  151. {
  152. width = 0.110f,
  153. height = 0.062f,
  154. border = 0.004f
  155. };
  156. /// Parameters for a Nexus 6 device.
  157. public static readonly Screen Nexus6 = new Screen
  158. {
  159. width = 0.133f,
  160. height = 0.074f,
  161. border = 0.004f
  162. };
  163. /// Parameters for a Galaxy S6 device.
  164. public static readonly Screen GalaxyS6 = new Screen
  165. {
  166. width = 0.114f,
  167. height = 0.0635f,
  168. border = 0.0035f
  169. };
  170. /// Parameters for a Galaxy Note4 device.
  171. public static readonly Screen GalaxyNote4 = new Screen
  172. {
  173. width = 0.125f,
  174. height = 0.0705f,
  175. border = 0.0045f
  176. };
  177. /// Parameters for a LG G3 device.
  178. public static readonly Screen LGG3 = new Screen
  179. {
  180. width = 0.121f,
  181. height = 0.068f,
  182. border = 0.003f
  183. };
  184. /// Some known Cardboard device profiles.
  185. public enum ViewerTypes
  186. {
  187. CardboardJun2014,
  188. CardboardMay2015,
  189. GoggleTechC1Glass,
  190. };
  191. /// Parameters for a Cardboard v1.
  192. public static readonly Viewer CardboardJun2014 = new Viewer
  193. {
  194. lenses = {
  195. separation = 0.060f,
  196. offset = 0.035f,
  197. screenDistance = 0.042f,
  198. alignment = Lenses.AlignBottom,
  199. },
  200. maxFOV = {
  201. outer = 40.0f,
  202. inner = 40.0f,
  203. upper = 40.0f,
  204. lower = 40.0f
  205. },
  206. distortion = {
  207. Coef = new [] { 0.441f, 0.156f },
  208. },
  209. inverse = ApproximateInverse(new[] { 0.441f, 0.156f })
  210. };
  211. /// Parameters for a Cardboard v2.
  212. public static readonly Viewer CardboardMay2015 = new Viewer
  213. {
  214. lenses = {
  215. separation = 0.064f,
  216. offset = 0.035f,
  217. screenDistance = 0.039f,
  218. alignment = Lenses.AlignBottom,
  219. },
  220. maxFOV = {
  221. outer = 60.0f,
  222. inner = 60.0f,
  223. upper = 60.0f,
  224. lower = 60.0f
  225. },
  226. distortion = {
  227. Coef = new [] { 0.34f, 0.55f },
  228. },
  229. inverse = ApproximateInverse(new[] { 0.34f, 0.55f })
  230. };
  231. /// Parameters for a Go4D C1-Glass.
  232. public static readonly Viewer GoggleTechC1Glass = new Viewer
  233. {
  234. lenses = {
  235. separation = 0.065f,
  236. offset = 0.036f,
  237. screenDistance = 0.058f,
  238. alignment = Lenses.AlignBottom,
  239. },
  240. maxFOV = {
  241. outer = 50.0f,
  242. inner = 50.0f,
  243. upper = 50.0f,
  244. lower = 50.0f
  245. },
  246. distortion = {
  247. Coef = new [] { 0.3f, 0 },
  248. },
  249. inverse = ApproximateInverse(new[] { 0.3f, 0 })
  250. };
  251. /// Nexus 5 in a Cardboard v1.
  252. public static readonly NxrProfile Default = new NxrProfile
  253. {
  254. screen = Nexus5,
  255. viewer = CardboardJun2014
  256. };
  257. /// Returns a profile with the given parameters.
  258. public static NxrProfile GetKnownProfile(ScreenSizes screenSize, ViewerTypes deviceType)
  259. {
  260. Screen screen;
  261. switch (screenSize)
  262. {
  263. case ScreenSizes.Nexus6:
  264. screen = Nexus6;
  265. break;
  266. case ScreenSizes.GalaxyS6:
  267. screen = GalaxyS6;
  268. break;
  269. case ScreenSizes.GalaxyNote4:
  270. screen = GalaxyNote4;
  271. break;
  272. case ScreenSizes.LGG3:
  273. screen = LGG3;
  274. break;
  275. default:
  276. screen = Nexus5;
  277. break;
  278. }
  279. Viewer device;
  280. switch (deviceType)
  281. {
  282. case ViewerTypes.CardboardMay2015:
  283. device = CardboardMay2015;
  284. break;
  285. case ViewerTypes.GoggleTechC1Glass:
  286. device = GoggleTechC1Glass;
  287. break;
  288. default:
  289. device = CardboardJun2014;
  290. break;
  291. }
  292. return new NxrProfile { screen = screen, viewer = device };
  293. }
  294. /// Calculates the tan-angles from the maximum FOV for the left eye for the
  295. /// current device and screen parameters.
  296. public void GetLeftEyeVisibleTanAngles(float[] result)
  297. {
  298. // Tan-angles from the max FOV.
  299. float fovLeft = Mathf.Tan(-viewer.maxFOV.outer * Mathf.Deg2Rad);
  300. float fovTop = Mathf.Tan(viewer.maxFOV.upper * Mathf.Deg2Rad);
  301. float fovRight = Mathf.Tan(viewer.maxFOV.inner * Mathf.Deg2Rad);
  302. float fovBottom = Mathf.Tan(-viewer.maxFOV.lower * Mathf.Deg2Rad);
  303. // Viewport size.
  304. float halfWidth = screen.width / 4;
  305. float halfHeight = screen.height / 2;
  306. // Viewport center, measured from left lens position.
  307. float centerX = viewer.lenses.separation / 2 - halfWidth;
  308. float centerY = -VerticalLensOffset;
  309. float centerZ = viewer.lenses.screenDistance;
  310. // Tan-angles of the viewport edges, as seen through the lens.
  311. float screenLeft = viewer.distortion.distort((centerX - halfWidth) / centerZ);
  312. float screenTop = viewer.distortion.distort((centerY + halfHeight) / centerZ);
  313. float screenRight = viewer.distortion.distort((centerX + halfWidth) / centerZ);
  314. float screenBottom = viewer.distortion.distort((centerY - halfHeight) / centerZ);
  315. // Compare the two sets of tan-angles and take the value closer to zero on each side.
  316. result[0] = Math.Max(fovLeft, screenLeft);
  317. result[1] = Math.Min(fovTop, screenTop);
  318. result[2] = Math.Min(fovRight, screenRight);
  319. result[3] = Math.Max(fovBottom, screenBottom);
  320. }
  321. /// Calculates the tan-angles from the maximum FOV for the left eye for the
  322. /// current device and screen parameters, assuming no lenses.
  323. public void GetLeftEyeNoLensTanAngles(float[] result)
  324. {
  325. // Tan-angles from the max FOV.
  326. float fovLeft = viewer.distortion.distortInv(Mathf.Tan(-viewer.maxFOV.outer * Mathf.Deg2Rad));
  327. float fovTop = viewer.distortion.distortInv(Mathf.Tan(viewer.maxFOV.upper * Mathf.Deg2Rad));
  328. float fovRight = viewer.distortion.distortInv(Mathf.Tan(viewer.maxFOV.inner * Mathf.Deg2Rad));
  329. float fovBottom = viewer.distortion.distortInv(Mathf.Tan(-viewer.maxFOV.lower * Mathf.Deg2Rad));
  330. // Viewport size.
  331. float halfWidth = screen.width / 4;
  332. float halfHeight = screen.height / 2;
  333. // Viewport center, measured from left lens position.
  334. float centerX = viewer.lenses.separation / 2 - halfWidth;
  335. float centerY = -VerticalLensOffset;
  336. float centerZ = viewer.lenses.screenDistance;
  337. // Tan-angles of the viewport edges, as seen through the lens.
  338. float screenLeft = (centerX - halfWidth) / centerZ;
  339. float screenTop = (centerY + halfHeight) / centerZ;
  340. float screenRight = (centerX + halfWidth) / centerZ;
  341. float screenBottom = (centerY - halfHeight) / centerZ;
  342. // Compare the two sets of tan-angles and take the value closer to zero on each side.
  343. result[0] = Math.Max(fovLeft, screenLeft);
  344. result[1] = Math.Min(fovTop, screenTop);
  345. result[2] = Math.Min(fovRight, screenRight);
  346. result[3] = Math.Max(fovBottom, screenBottom);
  347. }
  348. /// Calculates the screen rectangle visible from the left eye for the
  349. /// current device and screen parameters.
  350. public Rect GetLeftEyeVisibleScreenRect(float[] undistortedFrustum)
  351. {
  352. float dist = viewer.lenses.screenDistance;
  353. float eyeX = (screen.width - viewer.lenses.separation) / 2;
  354. float eyeY = VerticalLensOffset + screen.height / 2;
  355. float left = (undistortedFrustum[0] * dist + eyeX) / screen.width;
  356. float top = (undistortedFrustum[1] * dist + eyeY) / screen.height;
  357. float right = (undistortedFrustum[2] * dist + eyeX) / screen.width;
  358. float bottom = (undistortedFrustum[3] * dist + eyeY) / screen.height;
  359. return new Rect(left, bottom, right - left, top - bottom);
  360. }
  361. public static float GetMaxRadius(float[] tanAngleRect)
  362. {
  363. float x = Mathf.Max(Mathf.Abs(tanAngleRect[0]), Mathf.Abs(tanAngleRect[2]));
  364. float y = Mathf.Max(Mathf.Abs(tanAngleRect[1]), Mathf.Abs(tanAngleRect[3]));
  365. return Mathf.Sqrt(x * x + y * y);
  366. }
  367. // Solves a small linear equation via destructive gaussian
  368. // elimination and back substitution. This isn't generic numeric
  369. // code, it's just a quick hack to work with the generally
  370. // well-behaved symmetric matrices for least-squares fitting.
  371. // Not intended for reuse.
  372. //
  373. // @param a Input positive definite symmetrical matrix. Destroyed
  374. // during calculation.
  375. // @param y Input right-hand-side values. Destroyed during calculation.
  376. // @return Resulting x value vector.
  377. //
  378. private static double[] solveLinear(double[,] a, double[] y)
  379. {
  380. int n = a.GetLength(0);
  381. // Gaussian elimination (no row exchange) to triangular matrix.
  382. // The input matrix is a A^T A product which should be a positive
  383. // definite symmetrical matrix, and if I remember my linear
  384. // algebra right this implies that the pivots will be nonzero and
  385. // calculations sufficiently accurate without needing row
  386. // exchange.
  387. for (int j = 0; j < n - 1; ++j)
  388. {
  389. for (int k = j + 1; k < n; ++k)
  390. {
  391. double p = a[k, j] / a[j, j];
  392. for (int i = j + 1; i < n; ++i)
  393. {
  394. a[k, i] -= p * a[j, i];
  395. }
  396. y[k] -= p * y[j];
  397. }
  398. }
  399. // From this point on, only the matrix elements a[j][i] with i>=j are
  400. // valid. The elimination doesn't fill in eliminated 0 values.
  401. double[] x = new double[n];
  402. // Back substitution.
  403. for (int j = n - 1; j >= 0; --j)
  404. {
  405. double v = y[j];
  406. for (int i = j + 1; i < n; ++i)
  407. {
  408. v -= a[j, i] * x[i];
  409. }
  410. x[j] = v / a[j, j];
  411. }
  412. return x;
  413. }
  414. // Solves a least-squares matrix equation. Given the equation A * x = y, calculate the
  415. // least-square fit x = inverse(A * transpose(A)) * transpose(A) * y. The way this works
  416. // is that, while A is typically not a square matrix (and hence not invertible), A * transpose(A)
  417. // is always square. That is:
  418. // A * x = y
  419. // transpose(A) * (A * x) = transpose(A) * y <- multiply both sides by transpose(A)
  420. // (transpose(A) * A) * x = transpose(A) * y <- associativity
  421. // x = inverse(transpose(A) * A) * transpose(A) * y <- solve for x
  422. // Matrix A's row count (first index) must match y's value count. A's column count (second index)
  423. // determines the length of the result vector x.
  424. private static double[] solveLeastSquares(double[,] matA, double[] vecY)
  425. {
  426. int numSamples = matA.GetLength(0);
  427. int numCoefficients = matA.GetLength(1);
  428. if (numSamples != vecY.Length)
  429. {
  430. Debug.LogError("Matrix / vector dimension mismatch");
  431. return null;
  432. }
  433. // Calculate transpose(A) * A
  434. double[,] matATA = new double[numCoefficients, numCoefficients];
  435. for (int k = 0; k < numCoefficients; ++k)
  436. {
  437. for (int j = 0; j < numCoefficients; ++j)
  438. {
  439. double sum = 0.0;
  440. for (int i = 0; i < numSamples; ++i)
  441. {
  442. sum += matA[i, j] * matA[i, k];
  443. }
  444. matATA[j, k] = sum;
  445. }
  446. }
  447. // Calculate transpose(A) * y
  448. double[] vecATY = new double[numCoefficients];
  449. for (int j = 0; j < numCoefficients; ++j)
  450. {
  451. double sum = 0.0;
  452. for (int i = 0; i < numSamples; ++i)
  453. {
  454. sum += matA[i, j] * vecY[i];
  455. }
  456. vecATY[j] = sum;
  457. }
  458. // Now solve (A * transpose(A)) * x = transpose(A) * y.
  459. return solveLinear(matATA, vecATY);
  460. }
  461. /// Calculates an approximate inverse to the given radial distortion parameters.
  462. public static Distortion ApproximateInverse(float[] coef, float maxRadius = 1,
  463. int numSamples = 100)
  464. {
  465. return ApproximateInverse(new Distortion { Coef = coef }, maxRadius, numSamples);
  466. }
  467. /// Calculates an approximate inverse to the given radial distortion parameters.
  468. public static Distortion ApproximateInverse(Distortion distort, float maxRadius = 1,
  469. int numSamples = 100)
  470. {
  471. const int numCoefficients = 6;
  472. // R + K1*R^3 + K2*R^5 = r, with R = rp = distort(r)
  473. // Repeating for numSamples:
  474. // [ R0^3, R0^5 ] * [ K1 ] = [ r0 - R0 ]
  475. // [ R1^3, R1^5 ] [ K2 ] [ r1 - R1 ]
  476. // [ R2^3, R2^5 ] [ r2 - R2 ]
  477. // [ etc... ] [ etc... ]
  478. // That is:
  479. // matA * [K1, K2] = y
  480. // Solve:
  481. // [K1, K2] = inverse(transpose(matA) * matA) * transpose(matA) * y
  482. double[,] matA = new double[numSamples, numCoefficients];
  483. double[] vecY = new double[numSamples];
  484. for (int i = 0; i < numSamples; ++i)
  485. {
  486. float r = maxRadius * (i + 1) / (float)numSamples;
  487. double rp = distort.distort(r);
  488. double v = rp;
  489. for (int j = 0; j < numCoefficients; ++j)
  490. {
  491. v *= rp * rp;
  492. matA[i, j] = v;
  493. }
  494. vecY[i] = r - rp;
  495. }
  496. double[] vecK = solveLeastSquares(matA, vecY);
  497. // Convert to float for use in a fresh Distortion object.
  498. float[] coefficients = new float[vecK.Length];
  499. for (int i = 0; i < vecK.Length; ++i)
  500. {
  501. coefficients[i] = (float)vecK[i];
  502. }
  503. return new Distortion { Coef = coefficients };
  504. }
  505. }
  506. /// @endcond
  507. }