123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548 |
- // Copyright 2016 Nibiru. All rights reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- using System;
- using UnityEngine;
- // AR parameters of each mobile phone
- /// @cond
- /// Measurements of a particular phone in a particular AR viewer.
- namespace Nxr.Internal
- {
- [System.Serializable]
- public class NxrProfile
- {
- public NxrProfile Clone()
- {
- return new NxrProfile
- {
- screen = this.screen,
- viewer = this.viewer
- };
- }
- /// Information about the screen. All distances are in meters, measured relative to how
- /// the phone is expected to be seated in the viewer, i.e. landscape orientation.
- [System.Serializable]
- public struct Screen
- {
- public float width; // The long edge of the phone.
- public float height; // The short edge of the phone.
- public float border; // Distance from bottom of the phone to the bottom edge of screen.
- }
- /// Information about the lens placement in the viewer. All distances are in meters.
- [System.Serializable]
- public struct Lenses
- {
- public float separation; // Center to center.
- public float offset; // Offset of lens center from top or bottom of viewer.
- public float screenDistance; // Distance from lens center to the phone screen.
- public int alignment; // Determines whether lenses are placed relative to top, bottom or
- // center. It is actually a signum (-1, 0, +1) relating the scale of
- // the offset's coordinates to the device coordinates.
- public const int AlignTop = -1; // Offset is measured down from top of device.
- public const int AlignCenter = 0; // Center alignment ignores offset, hence scale is zero.
- public const int AlignBottom = 1; // Offset is measured up from bottom of device.
- }
- /// Information about the viewing angles through the lenses. All angles in degrees, measured
- /// away from the optical axis, i.e. angles are all positive. It is assumed that left and right
- /// eye FOVs are mirror images, so that both have the same inner and outer angles. Angles do not
- /// need to account for the limits due to screen size.
- [System.Serializable]
- public struct MaxFOV
- {
- public float outer; // Towards the side of the screen.
- public float inner; // Towards the center line of the screen.
- public float upper; // Towards the top of the screen.
- public float lower; // Towards the bottom of the screen.
- }
- /// Information on how the lens distorts light rays. Also used for the (approximate) inverse
- /// distortion. Assumes a radially symmetric pincushion/barrel distortion model.
- [System.Serializable]
- public struct Distortion
- {
- private float[] coef;
- public float[] Coef
- {
- get
- {
- return coef;
- }
- set
- {
- if (value != null)
- {
- coef = (float[])value.Clone();
- }
- else
- {
- coef = null;
- }
- }
- }
- public float distort(float r)
- {
- float r2 = r * r;
- float ret = 0;
- for (int j = coef.Length - 1; j >= 0; j--)
- {
- ret = r2 * (ret + coef[j]);
- }
- return (ret + 1) * r;
- }
- public float distortInv(float radius)
- {
- // Secant method.
- float r0 = 0;
- float r1 = 1;
- float dr0 = radius - distort(r0);
- while (Mathf.Abs(r1 - r0) > 0.0001f)
- {
- float dr1 = radius - distort(r1);
- float r2 = r1 - dr1 * ((r1 - r0) / (dr1 - dr0));
- r0 = r1;
- r1 = r2;
- dr0 = dr1;
- }
- return r1;
- }
- }
- /// Information about a particular device, including specfications on its lenses, FOV,
- /// and distortion and inverse distortion coefficients.
- [System.Serializable]
- public struct Viewer
- {
- public Lenses lenses;
- public MaxFOV maxFOV;
- public Distortion distortion;
- public Distortion inverse;
- }
- /// Screen parameters of a Cardboard device.
- public Screen screen;
- /// Viewer parameters of a Cardboard device.
- public Viewer viewer;
- /// The vertical offset of the lens centers from the screen center.
- public float VerticalLensOffset
- {
- get
- {
- return (viewer.lenses.offset - screen.border - screen.height / 2) * viewer.lenses.alignment;
- }
- }
- /// Some known screen profiles.
- public enum ScreenSizes
- {
- Nexus5,
- Nexus6,
- GalaxyS6,
- GalaxyNote4,
- LGG3
- };
- /// Parameters for a Nexus 5 device.
- public static readonly Screen Nexus5 = new Screen
- {
- width = 0.110f,
- height = 0.062f,
- border = 0.004f
- };
- /// Parameters for a Nexus 6 device.
- public static readonly Screen Nexus6 = new Screen
- {
- width = 0.133f,
- height = 0.074f,
- border = 0.004f
- };
- /// Parameters for a Galaxy S6 device.
- public static readonly Screen GalaxyS6 = new Screen
- {
- width = 0.114f,
- height = 0.0635f,
- border = 0.0035f
- };
- /// Parameters for a Galaxy Note4 device.
- public static readonly Screen GalaxyNote4 = new Screen
- {
- width = 0.125f,
- height = 0.0705f,
- border = 0.0045f
- };
- /// Parameters for a LG G3 device.
- public static readonly Screen LGG3 = new Screen
- {
- width = 0.121f,
- height = 0.068f,
- border = 0.003f
- };
- /// Some known Cardboard device profiles.
- public enum ViewerTypes
- {
- CardboardJun2014,
- CardboardMay2015,
- GoggleTechC1Glass,
- };
- /// Parameters for a Cardboard v1.
- public static readonly Viewer CardboardJun2014 = new Viewer
- {
- lenses = {
- separation = 0.060f,
- offset = 0.035f,
- screenDistance = 0.042f,
- alignment = Lenses.AlignBottom,
- },
- maxFOV = {
- outer = 40.0f,
- inner = 40.0f,
- upper = 40.0f,
- lower = 40.0f
- },
- distortion = {
- Coef = new [] { 0.441f, 0.156f },
- },
- inverse = ApproximateInverse(new[] { 0.441f, 0.156f })
- };
- /// Parameters for a Cardboard v2.
- public static readonly Viewer CardboardMay2015 = new Viewer
- {
- lenses = {
- separation = 0.064f,
- offset = 0.035f,
- screenDistance = 0.039f,
- alignment = Lenses.AlignBottom,
- },
- maxFOV = {
- outer = 60.0f,
- inner = 60.0f,
- upper = 60.0f,
- lower = 60.0f
- },
- distortion = {
- Coef = new [] { 0.34f, 0.55f },
- },
- inverse = ApproximateInverse(new[] { 0.34f, 0.55f })
- };
- /// Parameters for a Go4D C1-Glass.
- public static readonly Viewer GoggleTechC1Glass = new Viewer
- {
- lenses = {
- separation = 0.065f,
- offset = 0.036f,
- screenDistance = 0.058f,
- alignment = Lenses.AlignBottom,
- },
- maxFOV = {
- outer = 50.0f,
- inner = 50.0f,
- upper = 50.0f,
- lower = 50.0f
- },
- distortion = {
- Coef = new [] { 0.3f, 0 },
- },
- inverse = ApproximateInverse(new[] { 0.3f, 0 })
- };
- /// Nexus 5 in a Cardboard v1.
- public static readonly NxrProfile Default = new NxrProfile
- {
- screen = Nexus5,
- viewer = CardboardJun2014
- };
- /// Returns a profile with the given parameters.
- public static NxrProfile GetKnownProfile(ScreenSizes screenSize, ViewerTypes deviceType)
- {
- Screen screen;
- switch (screenSize)
- {
- case ScreenSizes.Nexus6:
- screen = Nexus6;
- break;
- case ScreenSizes.GalaxyS6:
- screen = GalaxyS6;
- break;
- case ScreenSizes.GalaxyNote4:
- screen = GalaxyNote4;
- break;
- case ScreenSizes.LGG3:
- screen = LGG3;
- break;
- default:
- screen = Nexus5;
- break;
- }
- Viewer device;
- switch (deviceType)
- {
- case ViewerTypes.CardboardMay2015:
- device = CardboardMay2015;
- break;
- case ViewerTypes.GoggleTechC1Glass:
- device = GoggleTechC1Glass;
- break;
- default:
- device = CardboardJun2014;
- break;
- }
- return new NxrProfile { screen = screen, viewer = device };
- }
- /// Calculates the tan-angles from the maximum FOV for the left eye for the
- /// current device and screen parameters.
- public void GetLeftEyeVisibleTanAngles(float[] result)
- {
- // Tan-angles from the max FOV.
- float fovLeft = Mathf.Tan(-viewer.maxFOV.outer * Mathf.Deg2Rad);
- float fovTop = Mathf.Tan(viewer.maxFOV.upper * Mathf.Deg2Rad);
- float fovRight = Mathf.Tan(viewer.maxFOV.inner * Mathf.Deg2Rad);
- float fovBottom = Mathf.Tan(-viewer.maxFOV.lower * Mathf.Deg2Rad);
- // Viewport size.
- float halfWidth = screen.width / 4;
- float halfHeight = screen.height / 2;
- // Viewport center, measured from left lens position.
- float centerX = viewer.lenses.separation / 2 - halfWidth;
- float centerY = -VerticalLensOffset;
- float centerZ = viewer.lenses.screenDistance;
- // Tan-angles of the viewport edges, as seen through the lens.
- float screenLeft = viewer.distortion.distort((centerX - halfWidth) / centerZ);
- float screenTop = viewer.distortion.distort((centerY + halfHeight) / centerZ);
- float screenRight = viewer.distortion.distort((centerX + halfWidth) / centerZ);
- float screenBottom = viewer.distortion.distort((centerY - halfHeight) / centerZ);
- // Compare the two sets of tan-angles and take the value closer to zero on each side.
- result[0] = Math.Max(fovLeft, screenLeft);
- result[1] = Math.Min(fovTop, screenTop);
- result[2] = Math.Min(fovRight, screenRight);
- result[3] = Math.Max(fovBottom, screenBottom);
- }
- /// Calculates the tan-angles from the maximum FOV for the left eye for the
- /// current device and screen parameters, assuming no lenses.
- public void GetLeftEyeNoLensTanAngles(float[] result)
- {
- // Tan-angles from the max FOV.
- float fovLeft = viewer.distortion.distortInv(Mathf.Tan(-viewer.maxFOV.outer * Mathf.Deg2Rad));
- float fovTop = viewer.distortion.distortInv(Mathf.Tan(viewer.maxFOV.upper * Mathf.Deg2Rad));
- float fovRight = viewer.distortion.distortInv(Mathf.Tan(viewer.maxFOV.inner * Mathf.Deg2Rad));
- float fovBottom = viewer.distortion.distortInv(Mathf.Tan(-viewer.maxFOV.lower * Mathf.Deg2Rad));
- // Viewport size.
- float halfWidth = screen.width / 4;
- float halfHeight = screen.height / 2;
- // Viewport center, measured from left lens position.
- float centerX = viewer.lenses.separation / 2 - halfWidth;
- float centerY = -VerticalLensOffset;
- float centerZ = viewer.lenses.screenDistance;
- // Tan-angles of the viewport edges, as seen through the lens.
- float screenLeft = (centerX - halfWidth) / centerZ;
- float screenTop = (centerY + halfHeight) / centerZ;
- float screenRight = (centerX + halfWidth) / centerZ;
- float screenBottom = (centerY - halfHeight) / centerZ;
- // Compare the two sets of tan-angles and take the value closer to zero on each side.
- result[0] = Math.Max(fovLeft, screenLeft);
- result[1] = Math.Min(fovTop, screenTop);
- result[2] = Math.Min(fovRight, screenRight);
- result[3] = Math.Max(fovBottom, screenBottom);
- }
- /// Calculates the screen rectangle visible from the left eye for the
- /// current device and screen parameters.
- public Rect GetLeftEyeVisibleScreenRect(float[] undistortedFrustum)
- {
- float dist = viewer.lenses.screenDistance;
- float eyeX = (screen.width - viewer.lenses.separation) / 2;
- float eyeY = VerticalLensOffset + screen.height / 2;
- float left = (undistortedFrustum[0] * dist + eyeX) / screen.width;
- float top = (undistortedFrustum[1] * dist + eyeY) / screen.height;
- float right = (undistortedFrustum[2] * dist + eyeX) / screen.width;
- float bottom = (undistortedFrustum[3] * dist + eyeY) / screen.height;
- return new Rect(left, bottom, right - left, top - bottom);
- }
- public static float GetMaxRadius(float[] tanAngleRect)
- {
- float x = Mathf.Max(Mathf.Abs(tanAngleRect[0]), Mathf.Abs(tanAngleRect[2]));
- float y = Mathf.Max(Mathf.Abs(tanAngleRect[1]), Mathf.Abs(tanAngleRect[3]));
- return Mathf.Sqrt(x * x + y * y);
- }
- // Solves a small linear equation via destructive gaussian
- // elimination and back substitution. This isn't generic numeric
- // code, it's just a quick hack to work with the generally
- // well-behaved symmetric matrices for least-squares fitting.
- // Not intended for reuse.
- //
- // @param a Input positive definite symmetrical matrix. Destroyed
- // during calculation.
- // @param y Input right-hand-side values. Destroyed during calculation.
- // @return Resulting x value vector.
- //
- private static double[] solveLinear(double[,] a, double[] y)
- {
- int n = a.GetLength(0);
- // Gaussian elimination (no row exchange) to triangular matrix.
- // The input matrix is a A^T A product which should be a positive
- // definite symmetrical matrix, and if I remember my linear
- // algebra right this implies that the pivots will be nonzero and
- // calculations sufficiently accurate without needing row
- // exchange.
- for (int j = 0; j < n - 1; ++j)
- {
- for (int k = j + 1; k < n; ++k)
- {
- double p = a[k, j] / a[j, j];
- for (int i = j + 1; i < n; ++i)
- {
- a[k, i] -= p * a[j, i];
- }
- y[k] -= p * y[j];
- }
- }
- // From this point on, only the matrix elements a[j][i] with i>=j are
- // valid. The elimination doesn't fill in eliminated 0 values.
- double[] x = new double[n];
- // Back substitution.
- for (int j = n - 1; j >= 0; --j)
- {
- double v = y[j];
- for (int i = j + 1; i < n; ++i)
- {
- v -= a[j, i] * x[i];
- }
- x[j] = v / a[j, j];
- }
- return x;
- }
- // Solves a least-squares matrix equation. Given the equation A * x = y, calculate the
- // least-square fit x = inverse(A * transpose(A)) * transpose(A) * y. The way this works
- // is that, while A is typically not a square matrix (and hence not invertible), A * transpose(A)
- // is always square. That is:
- // A * x = y
- // transpose(A) * (A * x) = transpose(A) * y <- multiply both sides by transpose(A)
- // (transpose(A) * A) * x = transpose(A) * y <- associativity
- // x = inverse(transpose(A) * A) * transpose(A) * y <- solve for x
- // Matrix A's row count (first index) must match y's value count. A's column count (second index)
- // determines the length of the result vector x.
- private static double[] solveLeastSquares(double[,] matA, double[] vecY)
- {
- int numSamples = matA.GetLength(0);
- int numCoefficients = matA.GetLength(1);
- if (numSamples != vecY.Length)
- {
- Debug.LogError("Matrix / vector dimension mismatch");
- return null;
- }
- // Calculate transpose(A) * A
- double[,] matATA = new double[numCoefficients, numCoefficients];
- for (int k = 0; k < numCoefficients; ++k)
- {
- for (int j = 0; j < numCoefficients; ++j)
- {
- double sum = 0.0;
- for (int i = 0; i < numSamples; ++i)
- {
- sum += matA[i, j] * matA[i, k];
- }
- matATA[j, k] = sum;
- }
- }
- // Calculate transpose(A) * y
- double[] vecATY = new double[numCoefficients];
- for (int j = 0; j < numCoefficients; ++j)
- {
- double sum = 0.0;
- for (int i = 0; i < numSamples; ++i)
- {
- sum += matA[i, j] * vecY[i];
- }
- vecATY[j] = sum;
- }
- // Now solve (A * transpose(A)) * x = transpose(A) * y.
- return solveLinear(matATA, vecATY);
- }
- /// Calculates an approximate inverse to the given radial distortion parameters.
- public static Distortion ApproximateInverse(float[] coef, float maxRadius = 1,
- int numSamples = 100)
- {
- return ApproximateInverse(new Distortion { Coef = coef }, maxRadius, numSamples);
- }
- /// Calculates an approximate inverse to the given radial distortion parameters.
- public static Distortion ApproximateInverse(Distortion distort, float maxRadius = 1,
- int numSamples = 100)
- {
- const int numCoefficients = 6;
- // R + K1*R^3 + K2*R^5 = r, with R = rp = distort(r)
- // Repeating for numSamples:
- // [ R0^3, R0^5 ] * [ K1 ] = [ r0 - R0 ]
- // [ R1^3, R1^5 ] [ K2 ] [ r1 - R1 ]
- // [ R2^3, R2^5 ] [ r2 - R2 ]
- // [ etc... ] [ etc... ]
- // That is:
- // matA * [K1, K2] = y
- // Solve:
- // [K1, K2] = inverse(transpose(matA) * matA) * transpose(matA) * y
- double[,] matA = new double[numSamples, numCoefficients];
- double[] vecY = new double[numSamples];
- for (int i = 0; i < numSamples; ++i)
- {
- float r = maxRadius * (i + 1) / (float)numSamples;
- double rp = distort.distort(r);
- double v = rp;
- for (int j = 0; j < numCoefficients; ++j)
- {
- v *= rp * rp;
- matA[i, j] = v;
- }
- vecY[i] = r - rp;
- }
- double[] vecK = solveLeastSquares(matA, vecY);
- // Convert to float for use in a fresh Distortion object.
- float[] coefficients = new float[vecK.Length];
- for (int i = 0; i < vecK.Length; ++i)
- {
- coefficients[i] = (float)vecK[i];
- }
- return new Distortion { Coef = coefficients };
- }
- }
- /// @endcond
- }
|