Получение вращения Y ARKit pointOfView
Я пытаюсь получить реальный угол обзора точки зрения в сцене ARKit (0 - 360 градусов). Я использую углы Эйлера из SCNNode из pointOfView.
print("\(pointOfView.eulerAngles.y.radiansToDegrees)")
Проблема в том, что при взгляде на север я получаю 0, а при взгляде на юг я также получаю 0. При взгляде на NE я получаю -45 градусов, а при взгляде на SE - -45 градусов. Похоже, SCNNode не может определить между Севером и Югом, только между Западом и Востоком. Любой совет?
Как правило, мне нужно реализовать радар в моей реальной сцене ARKit. Ожидаемое поведение: север: 0, восток: 90, юг: 180, запад: 270.
Заранее спасибо!
1 ответ
Я только что работал над похожей ситуацией. То, что вы после того, как я называю "заголовок", который не так просто определить, как вы думаете.
Краткая справка: к вашему сведению, есть два вида вращения, "Эйлера", которые относятся к пространству реального мира, но которые страдают от того, что они называют Gimbal Lock в крайних точках. И затем есть углы поворота относительно оси устройства, удерживаемые в свойстве преобразования ARCamera
,
Чтобы проиллюстрировать разницу euler.y
всегда означает, как устройство обращено (кроме случаев, когда оно плоское, в этом случае карданный замок портит его, следовательно, наша проблема), тогда как преобразование y
всегда означает вращение вокруг вертикальной оси через телефон (который, чтобы еще больше сбить с толку, основан на ландшафте устройства в ARKit).
(Примечание: если вы привыкли к CoreMotion, вы можете заметить, что в ARKit Gimbal Lock возникает, когда устройство удерживается на ровном месте, тогда как в CM оно находится в вертикальном положении).
Итак, как мы можем получить "заголовок", который работает, является ли устройство плоским или вертикальным? Решение ниже (извините, это цель-c!) Делает следующее:
Возьмите два нормальных вектора, один вдоль оси Z телефона (прямо из экрана) и один, который торчит в нижней части телефона, который я называю осью -Y (хотя на самом деле это ось +X, когда удерживается пейзаж).
Поверните вектор с помощью преобразования устройства (не Эйлера), спроецируйте на плоскость XZ и получите угол проецируемых векторов относительно оси Z.
Когда телефон находится в вертикальном положении, Z Normal будет идеальным курсом, но когда телефон плоский, то Y normal - тот, который нужно использовать. В промежутке между этими событиями мы будем использовать "плавное затухание" на основе "наклона" телефона, т.е.
euler.x
,Одна небольшая проблема заключается в том, что, когда пользователь слегка опускает телефон за пределы плоскости, заголовок, заданный Z Normal, переворачивается. Мы действительно не хотим этого (больше с точки зрения UX, чем с математической точки зрения), поэтому давайте обнаружим этот "наклон вниз" и перевернем
zHeading
180˚, когда это произойдет.
Конечный результат является последовательным и гладким heading
независимо от ориентации устройства. Он даже работает, когда устройство переключается между портретным и ландшафтным режимом... хазз!
// Create a Quaternion representing the devices curent rotation (NOT the same as the euler angles!)
GLKMatrix3 deviceRotM = GLKMatrix4GetMatrix3(SCNMatrix4ToGLKMatrix4(SCNMatrix4FromMat4(camera.transform)));
GLKQuaternion Q = GLKQuaternionMakeWithMatrix3(deviceRotM);
// We want to use the phone's Z normal (in the phone's reference frame) projected onto XZ to get the angle when the phone is upright BUT the Y normal when it's horizontal. We'll crossfade between the two based on the phone tilt (euler x)...
GLKVector3 phoneZNormal = GLKQuaternionRotateVector3(Q, GLKVector3Make(0, 0, 1));
GLKVector3 phoneYNormal = GLKQuaternionRotateVector3(Q, GLKVector3Make(1, 0, 0)); // why 1,0,0? Rotation=(0,0,0) is when the phone is landscape and upright. We want the vector that will point to +Z when the phone is portrait and flat
float zHeading = atan2f(phoneZNormal.x, phoneZNormal.z);
float yHeading = atan2f(phoneYNormal.x, phoneYNormal.z);
// Flip the zHeading if phone is tilting down, ie. the normal pointing down the device suddenly has a +y component
BOOL isDownTilt = phoneYNormal.y > 0;
if (isDownTilt) {
zHeading = zHeading + M_PI;
if (zHeading > M_PI) {
zHeading -= 2 * M_PI;
}
}
float a = fabs(camera.eulerAngles.x / M_PI_2);
float heading = a * yHeading + (1 - a) * zHeading;
NSLog(@"euler: %3.1f˚ %3.1f˚ %3.1f˚ zHeading=%3.1f˚ yHeading=%3.1f˚ heading=%3.1f˚ a=%.2f status:%li:%li zNorm=(%3.2f, %3.2f, %3.2f) yNorm=(%3.2f, %3.2f, %3.2f)", GLKMathRadiansToDegrees(camera.eulerAngles.x), GLKMathRadiansToDegrees(camera.eulerAngles.y), GLKMathRadiansToDegrees(camera.eulerAngles.z), GLKMathRadiansToDegrees(zHeading), GLKMathRadiansToDegrees(yHeading), GLKMathRadiansToDegrees(heading), a, camera.trackingState, camera.trackingStateReason, phoneZNormal.x, phoneZNormal.y, phoneZNormal.z, phoneYNormal.x, phoneYNormal.y, phoneYNormal.z);