본문 바로가기
[Unity]

[Unity] 안드로이드(Android) 카메라 연동하기

by 김기승 2023. 8. 5.

안드로이드의 카메라 화면을 실시간으로 가져와보자.


Canvas 설정

먼저, RawImage 컴포넌트를 Canvas 상에 배치해야한다. 이 컴포넌트에 카메라 화면을 담을 것이다.

Canvas를 생성하고, 자식으로 RawImage를 생성했다.

이때, Pivot은 따로 건드리지 말자. 나중에 스크립트에서 Width와 Height를 직접 조절할 것이기 때문이다.

※ CanvasScaler의 UI Scale Mode는 Constant Pixel Size로 지정해준다.


Script 작성

[Header("Setting")]
public Vector2 requestedRatio; //설정하고자 하는 카메라 비율
public int requestedFPS; //설정하고자 하는 프레임

[Header("Component")]
public RawImage webCamRawImage; //Canvas에 올려둔 RawImage

[Header("Data")]
public WebCamTexture webCamTexture; //Camera 화면을 지닌 Texture

일단, 필요한 변수부터 생성해준다. 접근 지정자를 public으로 두어 inspector에서 설정할 수 있다.

여기서 핵심은 WebCamTexture인데, 원하는 카메라의 화면을 지속해서 갱신하여 지니게 된다.

 

private void Start()
{
    SetWebCam();
}
private void SetWebCam() //WebCam을 지정하는 함수
{
    if (Permission.HasUserAuthorizedPermission(Permission.Camera)) //이미 카메라 권한을 획득한 경우에는
        CreateWebCamTexture(); //WebCamTexture를 생성하는 함수 바로 실행
    else //카메라 권한이 없는 경우에는
    {
        PermissionCallbacks permissionCallbacks = new();
        permissionCallbacks.PermissionGranted += CreateWebCamTexture; //권한 획득 후 실행될 함수로 WebCamTexture를 생성하는 함수 추가
        Permission.RequestUserPermission(Permission.Camera, permissionCallbacks); //카메라 권한을 요청
    }
}

Start문에서 WebCam을 지정할 수 있도록 함수를 구현한다.

만약, 카메라 권한이 없을 경우에는 권한을 획득한 후, CreateWebCamTexture 함수를 실행할 수 있도록 한다.

 

※ CreateWebCamTexture 함수는 아래에서 구현한다.

 

private void CreateWebCamTexture(string permissionName = null) //WebCamTexture를 생성하는 함수
{
    if (webCamTexture) //이미 WebCamTexture가 존재하는 경우에는
    {
        Destroy(webCamTexture); //삭제한다 (메모리 관리)
        webCamTexture = null;
    }

    WebCamDevice[] webCamDevices = WebCamTexture.devices; //현재 접근할 수 있는 카메라 기기들을 모두 가져온다
    if (webCamDevices.Length == 0) return; //만약, 카메라가 없다면 종료

    int backCamIndex = -1; //후방 카메라를 저장하기 위한 변수
    for (int i = 0, l = webCamDevices.Length; i < l; ++i) //카메라를 탐색하면서
    {
        if (!webCamDevices[i].isFrontFacing) //후방 카메라를 발견하면
        {
            backCamIndex = i; //인덱스 저장
            break; //반복문 빠져나오기
        }
    }

    if (backCamIndex != -1) //후방 카메라를 발견했으면
    {
        int requestedWidth = Screen.width; //설정하고자 하는 가로 픽셀 변수 선언 (현재 화면의 가로 픽셀을 기본값으로 지정)
        int requestedHeight = Screen.height; //설정하고자 하는 세로 픽셀 변수 선언 (현재 화면의 세로 픽셀을 기본값으로 지정)
        for (int i = 0, l = webCamDevices[backCamIndex].availableResolutions.Length; i < l; ++i) //현재 선택된 후방 카메라가 활용할 수 있는 해상도를 탐색하면서
        {
            Resolution resolution = webCamDevices[backCamIndex].availableResolutions[i];
            if (GetAspectRatio((int)requestedRatio.x, (int)requestedRatio.y).Equals(GetAspectRatio(resolution.width, resolution.height))) //설정하고자 하는 비율과 일치하는 해상도를 발견하면
            {
                requestedWidth = resolution.width; //설정하고자 하는 가로 픽셀로 지정
                requestedHeight = resolution.height; //설정하고자 하는 세로 픽셀로 지정
                break; //반복문 빠져나오기
            }
        }

        webCamTexture = new WebCamTexture(webCamDevices[backCamIndex].name, requestedWidth, requestedHeight, requestedFPS); //카메라 이름으로 WebCamTexture 생성
        webCamTexture.filterMode = FilterMode.Trilinear;
        webCamTexture.Play(); //카메라 재생

        webCamRawImage.texture = webCamTexture; //RawImage에 할당하기
    }
}
private string GetAspectRatio(int width, int height, bool allowPortrait = false) //비율을 반환하는 함수
{
    if (!allowPortrait && width < height) Swap(ref width, ref height); //세로가 허용되지 않는데, (가로 < 세로)이면 변수값 교환
    float r = (float)width / height; //비율 저장
    return r.ToString("F2"); //소수점 둘째까지 잘라서 문자열로 반환
}
private void Swap<T>(ref T a, ref T b) //두 변수값을 교환하는 함수
{
    T tmp = a;
    a = b;
    b = tmp;
}

이 부분은 조금 길지만, 별로 어렵지는 않다.

사용할 수 있는 카메라를 탐색하면서, 조건에 맞는 비율의 해상도로 WebCamTexture를 생성하는 것이다.

 

※ 요즘 안드로이드 스마트폰의 카메라 수(WebCamTexture.devices)는 지원하는 배율 개수에 따라 달라진다.

(x1, x0.5, x2, 셀카, ...)

※ 카메라를 탐색하면서 최초로 조건에 일치하는 후방 카메라의 배율은 대부분 x1이다.

 

private void Update()
{
    UpdateWebCamRawImage();
}
private void UpdateWebCamRawImage() //RawImage를 관리하는 함수
{
    if (!webCamTexture) return; //WebCamTexture가 존재하지 않으면 종료

    int videoRotAngle = webCamTexture.videoRotationAngle;
    webCamRawImage.transform.localEulerAngles = new Vector3(0, 0, -videoRotAngle); //카메라 회전 각도를 반영

    int width, height;
    if (Screen.orientation == ScreenOrientation.Portrait || Screen.orientation == ScreenOrientation.PortraitUpsideDown) //세로 화면이면
    {
        width = Screen.width; //가로를 고정하고
        height = Screen.width * webCamTexture.width / webCamTexture.height; //WebCamTexture의 비율에 따라 세로를 조절
    }
    else //가로 화면이면
    {
        height = Screen.height; //세로를 고정하고
        width = Screen.height * webCamTexture.width / webCamTexture.height; //WebCamTexture의 비율에 따라 가로를 조절
    }

    if (Mathf.Abs(videoRotAngle) % 180 != 0f) Swap(ref width, ref height); //WebCamTexture 자체가 회전되어있는 경우 가로/세로 값을 교환
    webCamRawImage.rectTransform.sizeDelta = new Vector2(width, height); //RawImage의 size로 지정
}

마지막으로 Update문이다.

RawImage의 사이즈와 회전을 처리하기 위한 함수를 작성한다. 특히, AutoRotation인 경우에도 반응형으로 처리할 수 있도록 가로/세로로 분기를 나누었다.

 

※ WebCamTexture 내부적으로 회전이 일어나는 경우가 있기 때문에 Swap 처리를 따로 했다.

※ webCamTexture.videoRotationAngle은 WebCamTexture 생성과 동시에 할당되지 않으므로, Update에서 처리하는 것이 적절하다.

 

스크립트 풀버전은 접어서 아래에 두었다.

더보기

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Android;

public class WebCamController : MonoBehaviour
{
    [Header("Setting")]
    public Vector2 requestedRatio; //설정하고자 하는 카메라 비율
    public int requestedFPS; //설정하고자 하는 프레임

    [Header("Component")]
    public RawImage webCamRawImage; //Canvas에 올려둔 RawImage

    [Header("Data")]
    public WebCamTexture webCamTexture; //Camera 화면을 지닌 Texture

    private void Start()
    {
        SetWebCam();
    }
    private void SetWebCam() //WebCam을 지정하는 함수
    {
        if (Permission.HasUserAuthorizedPermission(Permission.Camera)) //이미 카메라 권한을 획득한 경우에는
            CreateWebCamTexture(); //바로 WebCamTexture를 생성하는 함수 실행
        else //카메라 권한이 없는 경우에는
        {
            PermissionCallbacks permissionCallbacks = new();
            permissionCallbacks.PermissionGranted += CreateWebCamTexture; //권한 획득 후 실행될 함수로 WebCamTexture를 생성하는 함수 추가
            Permission.RequestUserPermission(Permission.Camera, permissionCallbacks); //카메라 권한을 요청
        }
    }

    private void CreateWebCamTexture(string permissionName = null) //WebCamTexture를 생성하는 함수
    {
        if (webCamTexture) //이미 WebCamTexture가 존재하는 경우에는
        {
            Destroy(webCamTexture); //삭제한다 (메모리 관리)
            webCamTexture = null;
        }

        WebCamDevice[] webCamDevices = WebCamTexture.devices; //현재 접근할 수 있는 카메라 기기들을 모두 가져온다
        if (webCamDevices.Length == 0) return; //만약, 카메라가 없다면 종료

        int backCamIndex = -1; //후방 카메라를 저장하기 위한 변수
        for (int i = 0, l = webCamDevices.Length; i < l; ++i) //카메라를 탐색하면서
        {
            if (!webCamDevices[i].isFrontFacing) //후방 카메라를 발견하면
            {
                backCamIndex = i; //인덱스 저장
                break; //반복문 빠져나오기
            }
        }

        if (backCamIndex != -1) //후방 카메라를 발견했으면
        {
            int requestedWidth = Screen.width; //설정하고자 하는 가로 픽셀 변수 선언 (현재 화면의 가로 픽셀을 기본값으로 지정)
            int requestedHeight = Screen.height; //설정하고자 하는 세로 픽셀 변수 선언 (현재 화면의 세로 픽셀을 기본값으로 지정)
            for (int i = 0, l = webCamDevices[backCamIndex].availableResolutions.Length; i < l; ++i) //현재 선택된 후방 카메라가 활용할 수 있는 해상도를 탐색하면서
            {
                Resolution resolution = webCamDevices[backCamIndex].availableResolutions[i];
                if (GetAspectRatio((int)requestedRatio.x, (int)requestedRatio.y).Equals(GetAspectRatio(resolution.width, resolution.height))) //설정하고자 하는 비율과 일치하는 해상도를 발견하면
                {
                    requestedWidth = resolution.width; //설정하고자 하는 가로 픽셀로 지정
                    requestedHeight = resolution.height; //설정하고자 하는 세로 픽셀로 지정
                    break; //반복문 빠져나오기
                }
            }

            webCamTexture = new WebCamTexture(webCamDevices[backCamIndex].name, requestedWidth, requestedHeight, requestedFPS); //카메라 이름으로 WebCamTexture 생성
            webCamTexture.filterMode = FilterMode.Trilinear;
            webCamTexture.Play(); //카메라 재생

            webCamRawImage.texture = webCamTexture; //RawImage에 할당하기
        }
    }
    private string GetAspectRatio(int width, int height, bool allowPortrait = false) //비율을 반환하는 함수
    {
        if (!allowPortrait && width < height) Swap(ref width, ref height); //세로가 허용되지 않는데, (가로 < 세로)이면 변수값 교환
        float r = (float)width / height; //비율 저장
        return r.ToString("F2"); //소수점 둘째까지 잘라서 문자열로 반환
    }
    private void Swap<T>(ref T a, ref T b) //두 변수값을 교환하는 함수
    {
        T tmp = a;
        a = b;
        b = tmp;
    }

    private void Update()
    {
        UpdateWebCamRawImage();
    }
    private void UpdateWebCamRawImage() //RawImage를 관리하는 함수
    {
        if (!webCamTexture) return; //WebCamTexture가 존재하지 않으면 종료

        int videoRotAngle = webCamTexture.videoRotationAngle;
        webCamRawImage.transform.localEulerAngles = new Vector3(0, 0, -videoRotAngle); //카메라 회전 각도를 반영

        int width, height;
        if (Screen.orientation == ScreenOrientation.Portrait || Screen.orientation == ScreenOrientation.PortraitUpsideDown) //세로 화면이면
        {
            width = Screen.width; //가로를 고정하고
            height = Screen.width * webCamTexture.width / webCamTexture.height; //WebCamTexture의 비율에 따라 세로를 조절
        }
        else //가로 화면이면
        {
            height = Screen.height; //세로를 고정하고
            width = Screen.height * webCamTexture.width / webCamTexture.height; //WebCamTexture의 비율에 따라 가로를 조절
        }

        if (Mathf.Abs(videoRotAngle) % 180 != 0f) Swap(ref width, ref height); //WebCamTexture 자체가 회전되어있는 경우 가로/세로 값을 교환
        webCamRawImage.rectTransform.sizeDelta = new Vector2(width, height); //RawImage의 size로 지정
    }
}


Script 추가

이제 작성한 스크립트를 Scene에 올려주고, Camera도 생성해서 올려준다.

비율은 4:3, 프레임은 60으로 설정했다.

RawImage 컴포넌트도 드래그 앱 드롭해준다.

 

※ 비율은 4:3이나 3:4나 동일하도록 내부적으로 처리되어있다(GetAspectRatio 함수 참조).

※ Camera는 여백이 깜빡이는 경우도 있기 때문에 추가해주었다.


▼ Portrait.gif

▼ Landscape.gif

▼ AutoRotation.gif


잘 작동하는 것을 볼 수 있다.

화질이 떨어지는 점은 양해바란다. 폰을 바꿀 때가 됐다.

댓글