
В прошлой статье мы рассмотрели как интегрировать Huawei AR Engine в Unity и создали простейшее приложение, реализующее SLAM (simultaneous localization & mapping) и находящее в окружающем пространстве плоские поверхности, на которых впоследтсвии мы размещали виртуальные объекты.
Сегодня мы попробуем кое-что другое – трекинг лица, отдельных его частей и наложение на лицо трехмерной маски. Вы несомненно могли видеть такое в приложениях для камер, в мессенджерах, в сторис, а теперь мы сделаем это сами.
Чтобы не расписывать заново все шаги по интеграции движка в Unity я отправляю читателя в прошлую статью. Кратко напомню, что нужно импортировать плагин Huawei AR Engine, затем создать контроллер сцены, создать PreviewCamera, родителем которой должен быть этот контроллер, и создать у контроллера два скрипта: Session Component и Tracking Controller. Эта конструкция является некоей базой — она общая для всех AR приложений, использующих AR Engine.
После этого мы уже начинаем развивать приложении в ту сторону, в которую нужно. Первое, что нужно сделать – подобрать файл конфигурации. В прошлый раз мы использовали WorldTrackingConfig, чтобы трекать окружение. Сегодня мы трекаем лицо, поэтому берем FaceTrackingConfig. Для каждого отдельного кейса в плагине присутсвует свой конфигурационный файл. Задаем этот файл в поле Config скрипта Session Component;
Теперь нам нужно немного подправить скрипт Tracking Controller:
namespace Common
{
using UnityEngine;
using System.Collections.Generic;
using HuaweiARUnitySDK;
using System.Collections;
using System;
using Common;
public class TrackingController : MonoBehaviour
{
[Tooltip("plane prefabs")]
public GameObject facePrefabs;
private List<ARFace> newFaces = new List<ARFace>();
public void Update()
{
_DrawFace();
}
private void _DrawFace()
{
newFaces.Clear();
ARFrame.GetTrackables<ARFace>(newFaces, ARTrackableQueryFilter.NEW);
for (int i = 0; i < newPlanes.Count; i++)
{
GameObject faceObject = Instantiate(facePrefabs, Vector3.zero, Quaternion.identity, transform);
faceObject.GetComponent<FaceVisualizer>().Initialize(newFaces[i]);
}
}
}
}
Если вы сравните его с этим же скриптом из предыдущего примера, то увидите, что единственное что поменялось по сути – теперь мы получаем из объекта кадра ARFrame объекты лица ARFace вместо объектов плоскостей ARPlane. Во всем остальном скрипт абсолютно аналогичен предыдущему. Мы также получаем лицо, а потом передаем его скрипту FaceVisualizer, который отвечает за визуализации. Скрипт FaceVisualizer при этом может быть присоединен к любому префабу в сцене.
Скучная часть позади, теперь приступаем к главному – к визуализации.
В скрипте FaceVisualizer первое, что мы делаем – пишем метод Initialize():
public void Initialize(ARFace face)
{
m_face = face;
}
Сначала мы получаем наш объект лица.
Далее нам нужно создать объект camera, который будет повторять позицию и поворот физической камеры. Этот объект нам нужен затем, чтобы впоследствии присоединять к нему некоторые виртуальные объекты в сцене.
camera.transform.position = ARFrame.GetPose().position;
camera.transform.rotation = ARFrame.GetPose().rotation;
После этого мы создаем объект facecenter. В будущем этот объект будет располагаться в геометрическом центре меша лица и повторять его движение. На данном этапе нам нужно перевести этот объект в систему координат камеры:
facecenter.transform.position = camera.transform.position;
facecenter.transform.rotation = camera.transform.rotation;
facecenter.transform.SetParent(camera.transform);
Последнее, что нужно сделать при инциализации – создать объект head, то есть нашу маску, и перевести этот объект в систему координат facecenter. Команда head.transform.Rotate(0,90,0) нужна затем, чтобы на этапе инициализации развернуть маску под правильным углом относительно центра лица.
head.transform.position = facecenter.transform.position;
head.transform.rotation = facecenter.transform.rotation;
head.transform.Rotate(0,90,0);
head.transform.SetParent(facecenter.transform);
Теперь перейдем к методу Update():
public void Update()
{
camera.transform.position = ARFrame.GetPose().position;
camera.transform.rotation = ARFrame.GetPose().rotation;
}
Сначала мы получим новые значения положения и поворота камеры. Затем мы получим положение и поворот лица. В данном случае GetPose() возвращает их в системе координат камеры. После этого мы задаем полученную трансформацию объекту facecenter, причем координату z нужно инвертировать:
pose = m_face.GetPose();
facecenter.transform.position = new Vector3(pose.position.x,pose.position.y,-pose.position.z);
facecenter.transform.rotation = pose.rotation;
С объектом facecenter перемещается и объект маски head, поскольку facecenter является его родителем. Теперь наша маска расположена прямо поверх лица. В принципе на этом можно было бы закончить, но я предлагаю немного усложнить этот проект. Статичная маска – это, конечно, круто, но можно ее анимировать. Сделать так, чтобы маска повторяла движения отдельных частей лица. Например, чтобы когда вы закрываете глаза – маска делала то же самое. Или когда улыбаетесь – на маске тоже появлялась улыбка.
Давайте попробуем реализовать улыбку. AR Engine умеет отслеживать состояния различных частей лица и возвращать эти состояния в виде числа в диапазоне от 0 до 1. Если мы говорим про глаза, то 0 будет означать, что глаз полностью открыт, а 1 – что полностью закрыт. Аналогичная история с улыбкой. Для начала получим состояния интересующих нас частей лица с помощью m_face.GetBlendShapeWithBlendName()[…]. В квадратных скобках нужно указать строку, соответствующую тому мимическому движению, которое вы хотите отследить. В нашем случае – “степень” улыбки левой и правой частей рта. И если эти значения превышают какое-то пороговое значение — мы можем произвести изменения в нашей маске. Например, заменить текстуру лица с нейтральной на улыбающуюся.
if (m_face.GetBlendShapeWithBlendName()["Animoji_Mouth_Smile_Right"] > 0.3 ||
m_face.GetBlendShapeWithBlendName()["Animoji_Mouth_Smile_Left"] > 0.3)
{
nosmile.SetActive(false);
smile.SetActive(true);
}
else
{
smile.SetActive(false);
nosmile.SetActive(true);
}
Полный список отслеживаемых мимических движений можно получить вот так:
StringBuilder sb;
var blendShapes = m_face.GetBlendShapeWithBlendName();
foreach(var bs in blendShapes)
{
sb.Append(bs.Key);
sb.Append(": ");
sb.Append(bs.Value);
sb.Append("\n");
}
А вот, что у нас в итоге получается:
Маска готова и анимирована! Но весь потенциал трекинга лица все еще не раскрыт. m_face.GetBlendShapeWithBlendName()[…] позволяет извлекать мимику, но не позволяет работать с лицом на более глубоком уровне — на уровне чистой геометрии и отдельных полигонов и вершин поверхности лица. Давайте, например, попробуем сделать трекинг глаз. Парадоксально, но отследить степень открытости глаз можно в одну команду, а вот чтобы получить их координаты, придется попотеть. Для начала вернемся в метод Initialize() и добавим объекты для глаз, переводя их в систему координат центра лица:
eye1.transform.position = facecenter.transform.position;
eye1.transform.rotation = facecenter.transform.rotation;
eye1.transform.SetParent(facecenter.transform);
eye2.transform.position = facecenter.transform.position;
eye2.transform.rotation = facecenter.transform.rotation;
eye2.transform.SetParent(facecenter.transform);
Так же во время инициализации нам нужно сделать следующее:
geom = m_face.GetFaceGeometry();
l = geom.TriangleIndices[3985*3];
r = geom.TriangleIndices[125*3];
Мы сначала получаем геометрию лица. Далее, существуют методы geom.Vertices[] и geom.TriangleIndices[]. Первый возвращает одномерный массив вершин, из которых состоит поверхность лица. Второй массив содержит треугольники, из которых состоит поверхность лица, причем структура этого массива такова: он одномерен и состоит из идущих друг за другом троек значений. Каждое значение — это ссылка на массив geom.Vertices[], то есть вершина. Таким образом, нам нужно найти либо вершину либо треугольник, соответствующий той части лица, с которой мы хотим работать. Для этого существует еще один метод – geom.TriangleLabel(), который возвращает список треугольников, каждый из которых подписан одной из строк:
1: lower lip;
2: upper lip;
3: left eye;
4: right eye;
5: left eyebrow;
6: right eyebrow;
7: eyebrow center;
8: nose.
Соответственно пройдя по всему списку можно найти треугольники, соответствующие нужной части лица. После этого необходимо взять индекс этого треугольника и помножить его на 3, а затем использовать полученное значение в geom.TriangleIndices[] и получить ссылку на вершину. А далее дело за малым – в методе Update добавляем следующие строки:
eye1.transform.position = facecenter.transform.position;
eye2.transform.position = facecenter.transform.position;
vertexr = geom.Vertices[r];
vertexl = geom.Vertices[l];
eye1.transform.Translate(vertexr.x,vertexr.y,vertexr.z);
eye2.transform.Translate(vertexl.x,vertexl.y,vertexl.z);
Мы получаем вершины, соответствующие левому и правому глазу, а потом просто перемещаем объекты глаз в эти координаты. Хочу заметить, что координаты вершин представлены в системе координат центра лица. Но для нас это не проблема, потому что ранее мы уже перевели глаза в систему координат центра лица.
Теперь посмотрим на результат:
Применений у данной техники — масса: эластичные маски, повторяющие геометрию лица, примерка аксессуаров и украшений, примерка причесок, макияжа, татуировок. Я же в качестве демонстрации сделал демку, которая имитирует глубину экрана смартфона, что достигается за счет отслеживания глаз с последующим изменением точки перспективы в сцене:
Павел Кочетков, Huawei