- 목차
개요
Unity Arcore를 이용한 Depth lab 프로젝트는 아래 링크에서 제공한다.
master 브랜치의 경우 Unity Arcore의 최신버전인 AR Foundation을 이용하며, 이번 포스트에서 다루려고 하는 Arcore SDK 버전은 브랜치가 arcore_unity_sdk인 프로젝트를 다운 받아야한다.
https://github.com/googlesamples/arcore-depth-lab/tree/arcore_unity_sdk
프로젝트 초기 설정에 대해서는 아래 포스트에서 따로 정리했다.
https://likefeb16220.blogspot.com/2024/03/unity-arcore-sdk-setting.html
Project structure
Common
각 샘플에서 공통적으로 사용되는 script, prefab 등 애셋들
-
MotionStereoDepthDataSource (Script)
Depth data에 low-level로 접근 가능한 클래스 -
DepthSource (Script)
싱글톤 클래스. CheckAttachedToScene 메서드 내부에서 자동으로 인스턴스를 생성함. MotionStereoDepthDataSource 클래스를 이용하여 작업.- CameraIntrinsics : DepthSource.FocalLength, DepthSource.PrincipalPoint, DepthSource.ImageDimensions 값 들이 정의된 부분을 따라가다 보면, MotionStereoDepthDataSource 클래스 내부에 InitializeCameraIntrinsics 함수로 넘어가게된다.
/// <summary>
/// Queries and correctly scales camera intrinsics for depth to vertex reprojection.
/// </summary>
private void InitializeCameraIntrinsics()
{
// Gets the camera parameters to create the required number of vertices.
_depthCameraIntrinsics = Frame.CameraImage.TextureIntrinsics;
// Scales camera intrinsics to the depth map size.
Vector2 intrinsicsScale;
intrinsicsScale.x = _depthWidth / (float) _depthCameraIntrinsics.ImageDimensions.x;
intrinsicsScale.y = _depthHeight / (float) _depthCameraIntrinsics.ImageDimensions.y;
_depthCameraIntrinsics.FocalLength = Utilities.MultiplyVector2(
_depthCameraIntrinsics.FocalLength, intrinsicsScale);
_depthCameraIntrinsics.PrincipalPoint = Utilities.MultiplyVector2(
_depthCameraIntrinsics.PrincipalPoint, intrinsicsScale);
_depthCameraIntrinsics.ImageDimensions =
new Vector2Int(_depthWidth, _depthHeight);
_initialized = true;
}
-
DepthTarget (Script)
DepthSource 클래스에서 자동으로 업데이트 해준다. Scene에 적어도 하나의 DepthTarget이 있어야 DepthSource에서 Depth Data를 제공해준다. Depth Texture는 DepthTarget 스크립트가 MeshRenderer 컴포넌트가 있는 GameObject에 있는 경우, MeshRenderer의 Material로 설정된다. -
DepthARComponents (Prefab)
(Prefab hierarchy)
DepthARComponents - DestroyInDemoScene.cs, DepthSource.cs
|-- Depth ARCore Device - InstantiatePrefabHere.cs
| |-- First Person Camera
| |-- Shadow workaround
|-- Environmental Light
| |-- Directional light
|
|-- ShadowReceiverMesh
|
|-- EventSystem
각 Scene에는 공통적으로 있지만, (스크립트 DestoryInDemoScene에 의해) 활성화된 Scene이 아니면 전부 Destroy하기 때문에 사실상 하나만 활성화되어 있는 Object
- PositionFilter.cs
Scene
DemoCarousel
Carousel은 “회전목마”, "회전식 원형 컨베이어"를 뜻하는 영단어이다. 앱 실행시 상단 부분에 각 기능을 선택해서 볼 수 있는 버튼을 제공하며, 좌우로 슬라이드를 하여 다른 버튼들도 볼 수 있다. 버튼 선택시, 선택한 버튼에 해당하는 Scene을 로드하게 된다.
프로젝트에서 각각의 Scene 단독으로도 실행될 수 있도록 설계되어 있지만, DemoCarousel Scene을 실행하게 될 경우, DemoCarousel Scene은 Scene 전환 UI 및 공통 AR Components 역할을 담당하게 된다.
OrientedReticle
"3D Cursor"를 선택할 때 호출되는 Scene. Depth hit 테스트를 사용하여 Raycast된 3D 위치와 해당 위치의 표면 법선 얻은 후, 원형 커서 아이콘을 표시해준다. 다른 Scene들은 OrientedReticle Scene에 추가 기능이 구현된 것이라고 생각하면 된다.
DepthEffects
"Depth Map"을 선택하면 호출
Material Wrap
"Material Wrap"을 선택하면 호출. 왼쪽 하단에 있는 버튼에 해당하는 Material로 화면을 씌운다. Depth Mesh 이용
OrientedSplat
"Color Balloon"을 선택하면 호출된다. Reticle이 가르키는 지점에 물풍선을 던진다고 생각하면 된다.
Collider
"Collider"를 선택하면 호출된다. OrientedSplat Scene에서 물풍선이 아니라 다른 도형을 던진다고 생각하면된다.
Projectile.cs
"Projectile"은 "발사체"를 뜻하는 영단어이다. Anchor를 생성한 후, 자신의 부모로 설정한다.
DepthMeshCollider.cs
DepthProcessingCompute.compute
CollisionAwareObjectPlacement
"Object Placement"를 선택하면 호출된다. 토스터 모델을 Reticle 커서가 가리키는 곳에 배치한다. 스크립트 코드들은 “CollisionDetection” 폴더 내부에 있다.
CheckReticleOrientation.cs
Reticle 이 기울어졌는지 확인
ObjectCollisionEvent.cs
충돌에 따라 객체의 색상을 바꾸고, “Place Object” 버튼의 활성화 상태를 바꾼다.
ObjectCollisionController.cs
해당 Scene에서 오브젝트 충돌 관련 기능 관리
ObjectCollisionAwarePlacementManager.cs
ScreenSpaceDepthMesh
앱 상단에서 "Mesh"를 선택할 때 호출된다. Material Wrap와 기능이 유사하다.
AvaterLocomotion
앱 상단에서 "Avater"를 선택할 때 호출된다. 적당한 지점에서 가운데 아래 버튼(“Place avater”,“Drop marker”)을 누르면, 로봇 3D 모델이 이동을 하게 된다.
AvaterPathController.cs
AvaterController.cs
GoogleARCore 관련 코드
ARBackground.shader
ARCore에서 받아오는 rgb frame texture 크기는 1920 x 1080이다. Android 단말기에 출력되는 화면은 아래 shader 코드를 거쳐서 나온 texture라고 생각하면 된다.
코드 중간에 TransitionIcon과 관련된 변수를 볼 수 있는데 아래에 있는 아이콘이라고 보면 된다. Shader 코드 커스터마이징시 없애도된다. (AR Foundation에서는 사라진듯 하지만…)
TransitionIcon (파일 이름 : ViewInARIcon.png) |
---|
|
Shader "ARCore/ARBackground"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" {}
_UvTopLeftRight ("UV of top corners", Vector) = (0, 1, 1, 1)
_UvBottomLeftRight ("UV of bottom corners", Vector) = (0 , 0, 1, 0)
}
// For GLES3 or GLES2 on device
SubShader
{
Pass
{
ZWrite Off
Cull Off
GLSLPROGRAM
#pragma only_renderers gles3 gles
// #ifdef SHADER_API_GLES3 cannot take effect because
// #extension is processed before any Unity defined symbols.
// Use "enable" instead of "require" here, so it only gives a
// warning but not compile error when the implementation does not
// support the extension.
#extension GL_OES_EGL_image_external_essl3 : enable
#extension GL_OES_EGL_image_external : enable
uniform vec4 _UvTopLeftRight;
uniform vec4 _UvBottomLeftRight;
// Use the same method in UnityCG.cginc to convert from gamma to
// linear space in glsl.
vec3 GammaToLinearSpace(vec3 color)
{
return color *
(color * (color * 0.305306011 + 0.682171111) + 0.012522878);
}
#ifdef VERTEX
varying vec2 textureCoord;
varying vec2 uvCoord;
void main()
{
vec2 uvTop = mix(_UvTopLeftRight.xy,
_UvTopLeftRight.zw,
gl_MultiTexCoord0.x);
vec2 uvBottom = mix(_UvBottomLeftRight.xy,
_UvBottomLeftRight.zw,
gl_MultiTexCoord0.x);
textureCoord = mix(uvTop, uvBottom, gl_MultiTexCoord0.y);
uvCoord = vec2(gl_MultiTexCoord0.x, gl_MultiTexCoord0.y);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
varying vec2 textureCoord;
varying vec2 uvCoord;
uniform samplerExternalOES _MainTex;
uniform sampler2D _TransitionIconTex; // 사실 필요 없음
uniform vec4 _TransitionIconTexTransform;// 사실 필요 없음
uniform float _Brightness;
void main()
{
vec3 mainTexColor;
#ifdef SHADER_API_GLES3
mainTexColor = texture(_MainTex, textureCoord).rgb;
#else
mainTexColor = textureExternal(_MainTex, textureCoord).rgb;
#endif
if (_Brightness < 1.0)
{
//왜 인지 모르지만, 출력 값에 알파(투명도)가 적용되지 않는다.
//RGB 값에 상수 값을 곱하여 밝기 조절로 대신한다.
mainTexColor = mainTexColor * _Brightness;
//하단 코드는 아이콘 관련 코드인데,
//RGB 색상 밝기 조절(상수 값 곱셈)을 하면 아이콘이 보이게된다.
//없어도 되는 부분이다.
if (_TransitionIconTexTransform.x > 0.0 &&
_TransitionIconTexTransform.z > 0.0)
{
vec2 uvCoordTex = vec2(uvCoord.x *
_TransitionIconTexTransform.x +
_TransitionIconTexTransform.y,
uvCoord.y *
_TransitionIconTexTransform.z +
_TransitionIconTexTransform.w);
vec4 transitionColor = vec4(0.0);
if (uvCoordTex.x >= 0.0 &&
uvCoordTex.x <= 1.0 &&
uvCoordTex.y >= 0.0 &&
uvCoordTex.y <= 1.0)
{
transitionColor = texture2D(_TransitionIconTex,
uvCoordTex);
}
if (transitionColor.a > 0.0)
{
mainTexColor = mix(transitionColor.rgb,
mainTexColor,
_Brightness);
}
}
}
#ifndef UNITY_COLORSPACE_GAMMA
mainTexColor = GammaToLinearSpace(mainTexColor);
#endif
//알파(투명도) 값이 적용되지 않는 이유를....
gl_FragColor = vec4(mainTexColor, 1.0);
}
#endif
ENDGLSL
}
}
// For Instant Preview 생략... 번거로워서 삭제함.
FallBack Off
}
ARCoreDepth.cginc
※ 여기서 cginc 파일 이란???
Unity에서 Shader에서 사용되는 헬퍼코드. Unity Shader에서 사용되는 헤더라고 생각하면 편하다.
GoogleARCore\SDK\Materials\ARCoreDepth.cginc
/*
대부분의 ARCore 관련 Shader 파일에서 include 되어 사용된다.
*/
#define ARCORE_DEPTH_SCALE 0.001 // mm to m
#define ARCORE_FLOAT_TO_SHORT 0xFFFF // (0.0, 1.0) -> (0, 65535)
#define ARCORE_FLOAT_TO_5BITS 31 // (0.0, 1.0) -> (0, 31)
#define ARCORE_FLOAT_TO_6BITS 63 // (0.0, 1.0) -> (0, 63)
#define ARCORE_RGB565_RED_SHIFT 2048 // left shift 11 bits
#define ARCORE_RGB565_GREEN_SHIFT 32 // left shift 5 bits
#define ARCORE_BLEND_FADE_RANGE 0.01
sampler2D _CurrentDepthTexture;
uniform float4 _CurrentDepthTexture_TexelSize;
uniform float4 _UvTopLeftRight;
uniform float4 _UvBottomLeftRight;
uniform float _OcclusionBlendingScale;
uniform float _OcclusionOffsetMeters;
// Calculates depth texture UV given screen-space UV. Uses _UvTopLeftRight and _UvBottomLeftRight.
inline float2 ArCoreDepth_GetUv(float2 uv)
{
float2 uvTop = lerp(_UvTopLeftRight.xy, _UvTopLeftRight.zw, uv.x);
float2 uvBottom = lerp(_UvBottomLeftRight.xy, _UvBottomLeftRight.zw, uv.x);
return lerp(uvTop, uvBottom, uv.y);
}
// Returns depth value in meters for a given depth texture UV. Uses _CurrentDepthTexture.
inline float ArCoreDepth_GetMeters(float2 uv)
{
// Depth texture는 RGB565 포맷 (2byte)를 사용한다.
// 이 때문에 DepthSource 클래스의 Depth 데이터 리턴 값이 short array로 가져온다.
// The depth texture uses TextureFormat.RGB565.
float4 rawDepth = tex2Dlod(_CurrentDepthTexture, float4(uv, 0, 0));
// 여기까지가 Raw Depth 값 계산이고.
float depth = (rawDepth.r * ARCORE_FLOAT_TO_5BITS * ARCORE_RGB565_RED_SHIFT)
+ (rawDepth.g * ARCORE_FLOAT_TO_6BITS * ARCORE_RGB565_GREEN_SHIFT)
+ (rawDepth.b * ARCORE_FLOAT_TO_5BITS);
//여기까지가 rgb를 적용한 것.
depth *= ARCORE_DEPTH_SCALE; //mm -> m 로 단위 환산
return depth;
}
inline float _ArCoreDepth_GetSampleAlpha(float2 uv, float3 virtualDepth)
{
float realDepth = ArCoreDepth_GetMeters(uv);
float signedDiffMeters = realDepth - virtualDepth;
return saturate(signedDiffMeters / ARCORE_BLEND_FADE_RANGE);
}
inline float _ArCoreDepth_GetBlendedAlpha(float2 uv, float3 virtualDepth, float2 stride)
{
// Hammersley low-discrepancy sampling, with triangular weighting.
//
// The 2D Hammersley point set of size N is usually defined as:
// H(i,N) = {i/N, phi2(i)}, with i=[0,N-1]
// Where phi2 is the base-2 van der Corput sequence.
//
// See: https://en.wikipedia.org/wiki/Low-discrepancy_sequence
// See: https://en.wikipedia.org/wiki/Van_der_Corput_sequence
//
// With N=2^n points in the 2D Hammersley set of size N lie on an NxN grid.
// We choose N=8. To center these points around 0 we need to subctract 7/16.
//
// For the triangular weighting, we take weights from the matrix:
//
// +-- --+
// | 1 2 3 4 4 3 2 1 |
// | 2 4 6 8 8 6 4 2 |
// | 3 6 9 12 12 9 6 3 |
// | 4 8 12 16 16 12 8 4 |
// | 4 8 12 16 16 12 8 4 |
// | 3 6 9 12 12 9 6 3 |
// | 2 4 6 8 8 6 4 2 |
// | 1 2 3 4 4 3 2 1 |
// +-- --+
const float2 center_bias = float2(7.0/16.0, 7.0/16.0);
float s;
s = _ArCoreDepth_GetSampleAlpha(uv + (float2(0.0/8.0, 0.0/1.0) - center_bias) *
stride, virtualDepth) * 1.0/52.0;
s += _ArCoreDepth_GetSampleAlpha(uv + (float2(1.0/8.0, 1.0/2.0) - center_bias) *
stride, virtualDepth) * 8.0/52.0;
s += _ArCoreDepth_GetSampleAlpha(uv + (float2(2.0/8.0, 1.0/4.0) - center_bias) *
stride, virtualDepth) * 9.0/52.0;
s += _ArCoreDepth_GetSampleAlpha(uv + (float2(3.0/8.0, 3.0/4.0) - center_bias) *
stride, virtualDepth) * 8.0/52.0;
s += _ArCoreDepth_GetSampleAlpha(uv + (float2(4.0/8.0, 1.0/8.0) - center_bias) *
stride, virtualDepth) * 8.0/52.0;
s += _ArCoreDepth_GetSampleAlpha(uv + (float2(5.0/8.0, 5.0/8.0) - center_bias) *
stride, virtualDepth) * 9.0/52.0;
s += _ArCoreDepth_GetSampleAlpha(uv + (float2(6.0/8.0, 3.0/8.0) - center_bias) *
stride, virtualDepth) * 8.0/52.0;
s += _ArCoreDepth_GetSampleAlpha(uv + (float2(7.0/8.0, 7.0/8.0) - center_bias) *
stride, virtualDepth) * 1.0/52.0;
return s;
}
// Returns an alpha to apply to occluded geometry that blends depth samples from nearby texels.
// Uses _OcclusionOffsetMeters, _CurrentDepthTexture_TexelSize, _OcclusionBlendingScale and
// _CurrentDepthTexture.
inline float ArCoreDepth_GetVisibility(float2 uv, float3 viewPos)
{
float virtualDepth = -viewPos.z - _OcclusionOffsetMeters;
float2 stride = _CurrentDepthTexture_TexelSize * _OcclusionBlendingScale;
return _ArCoreDepth_GetBlendedAlpha(uv, virtualDepth, stride);
}
CameraIntrinsics
GoogleARCore\SDK\Scripts\CameraIntrinsics.cs
namespace GoogleARCore
{
using UnityEngine;
public struct CameraIntrinsics
{
public Vector2 FocalLength;
public Vector2 PrincipalPoint;
public Vector2Int ImageDimensions;
internal CameraIntrinsics(
Vector2 focalLength, Vector2 principalPoint, Vector2Int imageDimensions)
{
FocalLength = focalLength;
PrincipalPoint = principalPoint;
ImageDimensions = imageDimensions;
}
}
}
NativeSession 내부의 CameraApi에서 계산되어 (값이 채워져서) 리턴
이슈
Unable to load DLL “arcore_camera_utility” The specified module could not be found.
Assets\GoogleARCore\Examples\ComputerVision\Plugins
libarcore_camera_utility.so라는 파일이 기본적으로 제공되지만, 다른 플랫폼 타겟 (예: il2cpp ARM64)인 경우 libarcore_camera_utility.so 파일을 해당 플랫폼에 맞게 다시 생성 해야된다. 하단 링크에 잘 설명되어 있다. (NDK 빌드)
ARCore 카메라 이미지 (텍스처) 관련
3D 오브젝트나 UI가 없는 ARCore의 Background에서 나오는 것만 가져오기 위해서는 ARCore에서 제공하는 함수를 이용해야한다.
ARCore 내부에 Frame.CameraImage.Texture와 Frame.CameraImage.AcquireCameraImageBytes 함수 제공해주기 때문에 이를 사용하면 되지 않을… 안타깝게도 두 함수에는 이슈가 있다.
일단 두 녀석들에 의해 나온 이미지는 YUV-420-888 포맷를 가진다. GPU(Shader)영역 에서는 RGB 색상이 잘 나와주지만, CPU영역에서는 Gray Scale로 나온다. (Background는 YUV-420-888, Depths는 RGB565 … 잘한다 잘해… , 설상가상으로 Frame.CameraImage.Texture 함수는 제대로 작동하지 않는다는…)
ARCore 이미지를 가져오는 방법은 GoggleARCore Example 코드의 ComputerVision 샘플에 있는 TextureReader.cs를 활용하는 것이다. 해당 예제 코드가 있는 경로는 아래와 같다.
방법 1 : AcquireCameraImageBytes 함수 이용
해당 함수에서 리턴되는 GoogleARCore.CameraImageBytes 값을 직접 사용하는 방법이다. 해당 함수에서 반환되는 이미지는 YUV-420-888 포맷을 가진다. 그리고 단말기 회전에 따른 텍스처 회전도 같이 처리해줘야한다. 다만 리턴 값을 확인할 필요가 있다. R값만 또는 GrayScale로 반환되는 경우가 있다.
방법 2 : TextureReader 클래스 (권장)
TextureReader 클래스에 있는 OnImageAvailablebackFunc 콜백에서 반환되는 정보를 이용하면 된다. 가장 좋은 방법은 새로운 MonoBahaviour를 생성하여 TextureReader를 component로 가지도록한 다음, OnImageAvailablebackFunc 콜백 값을 이용하면 된다. (콜백에서 보내는 pixelBuffer 값은 동일한 주소 값이 전송된다.)
여기서 콜백 값을 이용하는 시점은 상단의 AcquireCameraImageBytes에서 리턴되는 GoogleARCore.CameraImageBytes 가 IsAvailable 일 때 사용하면된다. (단순히 IsAvailable의 값만 체크하는 용도)
<출처>
https://qiita.com/nshinya/items/e5d0cf3c4baf90452ef5
Github issue : Why i can not get texture by API “Frame.CameraImage.Texture”?
Stackoverflow (1) : Save AcquireCameraImageBytes() from Unity ARCore to storage as an image
Stackoverflow (2) : ARCore Save Camera Image (Unity C#) on Button click
LoadRawTextureData 오류
같은 데이터에 대해 LoadRawTextureData 너무 빠르게 호출하면 “not enough data provided (will result in overread).” 에러가 호출된다.
댓글
댓글 쓰기