이전 글에서 언급했듯, Face Mask 머티리얼을 변경하는 방식으로 눈과 입 위에 이미지를 띄우려고 하니 입을 벌리는 등의 행동을 하면 이미지가 사용자의 얼굴에 따라 늘어나면서 표정 가이드라인 위치가 실제 눈과 입 위치와 맞지 않는 문제가 발생했다.
이 문제를 해결하기 위해 가이드라인 이미지를 왼쪽 눈, 오른쪽 눈, 입으로 따로 저장하고 사용자의 쪽 눈, 오른쪽 눈, 입 위치에 올려보는 방식으로 시도해보았다.
빈 Game Object에 Sprite Renderer 컴포넌트를 추가하고 머티리얼을 넣어주었다.
이렇게 왼쪽 눈, 오른쪽 눈, 입 머티리얼이 들어간 Game Object를 3개 만들고 해당 오브젝트들을 모두 프리팹으로 만들었다.
이 프리팹들은 Material 배열에 들어간다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
public enum EEmotion
{
Happy,
Sad,
Angry,
Surprise
}
public class ARFaceImageOverlay : MonoBehaviour
{
private ARFaceManager arFaceManager;
public EEmotion CurEmotion = EEmotion.Happy;
private ARFace trackedFace;
// 표정 가이드라인 Material 배열
// 왼쪽 눈, 오른쪽, 입 순서대로 값 설정할 것!
public GameObject[] HappyPrefabs;
//public GameObject[] SadPrefabs;
//public GameObject[] AngryPrefabs;
//public GameObject[] SurprisePrefabs;
private GameObject LeftEyeSprite;
private GameObject RightEyeSprite;
private GameObject MouthSprite;
private void Awake()
{
arFaceManager = GetComponent<ARFaceManager>();
}
void Update()
{
// 감지된 얼굴이 없으면
if (arFaceManager.trackables.count == 0)
{
SetPrefabVisibility(false);
return;
}
// 처음으로 얼굴이 감지될 때 프리팹 생성
if (LeftEyeSprite == null || RightEyeSprite == null || MouthSprite == null)
{
InstantiateEmotionPrefabs();
}
ApplyFaceMaterial();
}
void ApplyFaceMaterial()
{
foreach (ARFace face in arFaceManager.trackables)
{
// ARFace의 렌더러를 비활성화 (페이스 마스크 숨기기)
MeshRenderer faceRenderer = face.GetComponent<MeshRenderer>();
if (faceRenderer != null)
{
faceRenderer.enabled = false;
}
// 얼굴 추적이 안 되면
if (face.trackingState != TrackingState.Tracking)
{
SetPrefabVisibility(false);
return;
}
Vector3 LeftEyePos = face.transform.TransformPoint(face.vertices[133]);
Vector3 RightEyePos = face.transform.TransformPoint(face.vertices[362]);
Vector3 MouthPos = face.transform.TransformPoint(face.vertices[13]);
// 눈 위치 보정
LeftEyePos.x -= 0.02f;
RightEyePos.x += 0.02f;
Quaternion faceRotation = face.transform.rotation;
if (LeftEyeSprite != null)
{
LeftEyeSprite.transform.position = LeftEyePos;
// 얼굴 회전에 맞춰 회전
LeftEyeSprite.transform.rotation = faceRotation;
}
if (RightEyeSprite != null)
{
RightEyeSprite.transform.position = RightEyePos;
RightEyeSprite.transform.rotation = faceRotation;
}
if (MouthSprite != null)
{
MouthSprite.transform.position = MouthPos;
MouthSprite.transform.rotation = faceRotation;
}
SetPrefabVisibility(true);
}
}
void InstantiateEmotionPrefabs()
{
GameObject[] CurPrefabs = null;
switch (CurEmotion)
{
case EEmotion.Happy:
CurPrefabs = HappyPrefabs;
break;
case EEmotion.Sad:
//CurPrefabs = SadPrefabs;
break;
case EEmotion.Angry:
//CurPrefabs = AngryPrefabs;
break;
case EEmotion.Surprise:
//CurPrefabs = SurprisePrefabs;
break;
default:
Debug.LogWarning("Invalid Emotion!");
break;
}
if(CurPrefabs != null)
{
LeftEyeSprite = Instantiate(CurPrefabs[0], Vector3.zero, Quaternion.identity);
RightEyeSprite = Instantiate(CurPrefabs[1], Vector3.zero, Quaternion.identity);
MouthSprite = Instantiate(CurPrefabs[2], Vector3.zero, Quaternion.identity);
LeftEyeSprite.transform.SetParent(transform);
RightEyeSprite.transform.SetParent(transform);
MouthSprite.transform.SetParent(transform);
}
}
void SetPrefabVisibility(bool isVisible)
{
if (LeftEyeSprite != null)
LeftEyeSprite.SetActive(isVisible);
if (RightEyeSprite != null)
RightEyeSprite.SetActive(isVisible);
if (MouthSprite != null)
MouthSprite.SetActive(isVisible);
}
}
얼굴의 랜드마크 위치를 구한 후, Material 배열에 들어있는 프리팹들을 생성하여 해당 위치에 띄워주었다.
프리팹은 처음으로 얼굴이 감지될 때에만 생성하고, 그 뒤에는 얼굴이 감지되지 않으면 생성된 프리팹의 Visibility를 끄고 감지되면 켜지는 방식으로 구현했다.
매번 프리팹을 생성 / 제거하는 것보다 훨씬 효율적이다.
문제는 사용자가 고개를 꺾거나 핸드폰 카메라 회전 시 표정 가이드라인 위치와 회전 각도가 맞지 않는 문제가 생겼다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
public enum EEmotion
{
Happy,
Sad,
Angry,
Surprise
}
public class ARFaceImageOverlay : MonoBehaviour
{
private ARFaceManager arFaceManager;
public EEmotion CurEmotion = EEmotion.Happy;
private ARFace trackedFace;
// 표정 가이드라인 Material 배열
// 왼쪽 눈, 오른쪽, 입 순서대로 값 설정할 것!
public GameObject[] HappyPrefabs;
//public GameObject[] SadPrefabs;
//public GameObject[] AngryPrefabs;
//public GameObject[] SurprisePrefabs;
private GameObject LeftEyeSprite;
private GameObject RightEyeSprite;
private GameObject MouthSprite;
private void Awake()
{
arFaceManager = GetComponent<ARFaceManager>();
}
void Update()
{
// 감지된 얼굴이 없으면
if (arFaceManager.trackables.count == 0)
{
SetPrefabVisibility(false);
return;
}
// 처음으로 얼굴이 감지될 때 프리팹 생성
if (LeftEyeSprite == null || RightEyeSprite == null || MouthSprite == null)
{
InstantiateEmotionPrefabs();
}
ApplyFaceMaterial();
}
void ApplyFaceMaterial()
{
foreach (ARFace face in arFaceManager.trackables)
{
// ARFace의 렌더러를 비활성화 (페이스 마스크 숨기기)
MeshRenderer faceRenderer = face.GetComponent<MeshRenderer>();
if (faceRenderer != null)
{
faceRenderer.enabled = false;
}
// 얼굴 추적이 안 되면
if (face.trackingState != TrackingState.Tracking)
{
SetPrefabVisibility(false);
return;
}
// 얼굴의 로컬 정점 위치 가져오기
Vector3 LeftEyeLocal = face.vertices[133]; // 왼쪽 눈
Vector3 RightEyeLocal = face.vertices[362]; // 오른쪽 눈
Vector3 MouthLocal = GetMouthCenterLocal(face); // 입
// 눈 위치 보정
LeftEyeLocal.x -= 0.015f;
RightEyeLocal.x += 0.015f;
Vector3 LeftEyeWorldPos = face.transform.TransformPoint(LeftEyeLocal);
Vector3 RightEyeWorldPos = face.transform.TransformPoint(RightEyeLocal);
Vector3 MouthWorldPos = face.transform.TransformPoint(MouthLocal);
Quaternion faceRotation = face.transform.rotation;
// 얼굴이 바라보는 방향
Vector3 faceForward = face.transform.forward;
// 얼굴이 바라보는 방향
Vector3 faceUp = face.transform.up;
// 카메라 회전 보정 (카메라가 회전해도 스프라이트가 이상하지 않도록)
Quaternion inverseCameraRotation = Quaternion.Inverse(Camera.main.transform.rotation);
Quaternion adjustedRotation = inverseCameraRotation * faceRotation;
if (LeftEyeSprite != null)
{
LeftEyeSprite.transform.position = LeftEyeWorldPos;
LeftEyeSprite.transform.rotation = adjustedRotation;
}
if (RightEyeSprite != null)
{
RightEyeSprite.transform.position = RightEyeWorldPos;
RightEyeSprite.transform.rotation = adjustedRotation;
}
if (MouthSprite != null)
{
MouthSprite.transform.position = MouthWorldPos;
MouthSprite.transform.rotation = adjustedRotation;
}
SetPrefabVisibility(true);
}
}
Vector3 GetMouthCenterLocal(ARFace face)
{
const int UpperLipIndex = 13;
const int LowerLipIndex = 14;
if (face.vertices.Length > LowerLipIndex)
{
Vector3 UpperLip = face.vertices[UpperLipIndex];
Vector3 LowerLip = face.vertices[LowerLipIndex];
return (UpperLip + LowerLip) / 2;
}
Debug.LogWarning("Can't Find Mouth Center!");
return Vector3.zero;
}
void InstantiateEmotionPrefabs()
{
GameObject[] CurPrefabs = null;
switch (CurEmotion)
{
case EEmotion.Happy:
CurPrefabs = HappyPrefabs;
break;
case EEmotion.Sad:
//CurPrefabs = SadPrefabs;
break;
case EEmotion.Angry:
//CurPrefabs = AngryPrefabs;
break;
case EEmotion.Surprise:
//CurPrefabs = SurprisePrefabs;
break;
default:
Debug.LogWarning("Invalid Emotion!");
break;
}
if(CurPrefabs != null)
{
LeftEyeSprite = Instantiate(CurPrefabs[0], Vector3.zero, Quaternion.identity);
RightEyeSprite = Instantiate(CurPrefabs[1], Vector3.zero, Quaternion.identity);
MouthSprite = Instantiate(CurPrefabs[2], Vector3.zero, Quaternion.identity);
LeftEyeSprite.transform.SetParent(transform);
RightEyeSprite.transform.SetParent(transform);
MouthSprite.transform.SetParent(transform);
}
}
void SetPrefabVisibility(bool isVisible)
{
if (LeftEyeSprite != null)
LeftEyeSprite.SetActive(isVisible);
if (RightEyeSprite != null)
RightEyeSprite.SetActive(isVisible);
if (MouthSprite != null)
MouthSprite.SetActive(isVisible);
}
}
Vector3 LeftEyeWorldPos = face.transform.TransformPoint(LeftEyeLocal);
Vector3 RightEyeWorldPos = face.transform.TransformPoint(RightEyeLocal);
Vector3 MouthWorldPos = face.transform.TransformPoint(MouthLocal);
face.vertices는 얼굴 메쉬의 로컬 좌표를 반환한다.
즉, 얼굴 자체를 기준으로 한 상대적인 위치이다.
face.vertices 값을 그대로 사용하면 ARFace 오브젝트의 로컬 공간에서의 위치를 가져오므로, 스프라이트가 얼굴과 관계없이 엉뚱한 곳에 배치될 가능성이 크다.
로컬 좌표만 사용하면 올바른 위치에 따라가지 않으므로 월드 좌표로 변환이 필요하다.
Quaternion inverseCameraRotation = Quaternion.Inverse(Camera.main.transform.rotation);
Quaternion adjustedRotation = inverseCameraRotation * face.transform.rotation;
AR 환경에서는 카메라가 움직이면 월드 좌표도 같이 움직이면서 스프라이트가 엉뚱한 방향을 볼 수 있다.
따라서 카메라 회전을 반대로 적용하고 이 값을 얼굴의 회전에 곱해서 스프라이트가 항상 얼굴 방향을 유지하도록 한다.
입 가이드라인 스프라이트를 입 중앙에 위치시키고자 하였으나, 기존에 face.vertices[13]은 윗입술 위치를 반환하여 원하는 위치에서 약간 벗어나는 문제가 있었다.
Vector3 GetMouthCenterLocal(ARFace face)
{
const int UpperLipIndex = 13;
const int LowerLipIndex = 14;
if (face.vertices.Length > LowerLipIndex)
{
Vector3 UpperLip = face.vertices[UpperLipIndex];
Vector3 LowerLip = face.vertices[LowerLipIndex];
return (UpperLip + LowerLip) / 2;
}
Debug.LogWarning("Can't Find Mouth Center!");
return Vector3.zero;
}
따라서 윗입술과 아랫입술 위치를 구하여 그 둘의 중간 위치를 반환하는 GetMouthCenterLocal 함수를 추가하여 해당 함수가 반환하는 위치에 가이드라인이 올라가도록 수정했다.