측정선·점·바닥격자가 카메라 움직임에 따라 미끄러지고 튀는 현상에 대한 소스코드 기반 정밀 진단
카메라 배경은 OpenGL(GL 스레드)로 그리는데, 측정선·점·바닥격자·조준점은 그 위에 얹은
일반 안드로이드 View를 mainHandler.post(UI 스레드)로 갱신합니다.
두 화면은 서로 다른 스레드·서로 다른 버퍼에서 비동기로 그려지므로 프레임이 절대 딱 맞지 않습니다.
카메라 배경은 항상 최신, 오버레이는 항상 1~3프레임(16~50ms) 뒤 → 카메라를 움직일 때마다 선/점이 실제 위치에서
미끄러졌다 따라붙는 것이 곧 “튐”입니다. 정지 시엔 괜찮다가 움직일 때 드러나는 것도 이 때문입니다.
근본 해결 = 오버레이를 GL 프레임 안에서 카메라와 함께 렌더링. 1번 항목만 제대로 고쳐도 체감 튐의 대부분이 사라집니다.
| 순위 | 원인 | 심각도 |
|---|---|---|
| 1 | 오버레이가 GL 카메라와 프레임 동기화되지 않음 — 2D View + mainHandler.post | 치명·근본 |
| 2 | 바닥격자를 매 프레임 다른 평면·움직이는 centerPose 기준으로 다시 그림 | 큰 기여 |
| 3 | 확정된 측정선이 배치 시점 좌표로 고정 → ARCore 재보정 시 드리프트 | 기여 |
| 4 | 세션 API를 매 프레임 불필요하게 호출 (setDisplayGeometry 등) | 기여 |
영상에서 심각하게 보였던 튐 — 카메라를 움직일 때 선/점/격자가 실제 위치에서 미끄러졌다 따라붙는 현상 — 은 오버레이가 카메라와 같은 GL 프레임에서 함께 그려지도록 구조를 바꿨기 때문에 원리상 어긋날 수 없게 되어 사실상 100% 제거됩니다. 전체 체감 기준으로는 90~95% 개선을 예상하며, 정확한 수치는 실기기 검증으로 확정합니다.
| 영상에서 보인 증상 | 원인 (기여도) | 수정 후 |
|---|---|---|
| 카메라를 움직이면 선·점이 미끄러졌다 따라붙음 (가장 심한 튐) | 렌더링 프레임 비동기 (~70%) | 구조적으로 제거 — 카메라와 같은 GL 프레임·같은 VP 행렬로 렌더 |
| 격자가 통째로 순간이동 · 스르륵 밀림 | 매 프레임 평면 재선택 (~15%) | 제거 — 고정 Anchor에 1회 고정 (0.3m 이상 낮은 바닥 발견 시에만 이동) |
| 측정선이 찍은 지점에서 서서히 어긋남 | 배치 시점 좌표 박제 (~10%) | 제거 — 매 프레임 live anchor.pose 재투영 |
| 배경 미세 흔들림 | 세션 API 매 프레임 호출 (~5%) | 제거 — 지오메트리 변경 시에만 호출 |
ARCore 공간 추적 자체의 한계입니다 — 기기를 급격히 흔들거나, 조명이 어둡거나, 무늬 없는 민무늬 벽·유리를 비출 때 공간 인식이 흔들리는 것. 증상 형태가 다릅니다: 수정 전처럼 "선이 카메라 영상 위에서 따로 노는" 게 아니라 "격자+선+배경이 통째로 살짝 출렁이는" 형태이며, 삼성 측정 앱 등 어떤 AR 앱도 피할 수 없는 영역입니다. 상태 안내 문구("카메라를 천천히 이동하세요")가 이 상황을 사용자에게 안내합니다.
ArSceneRenderer.kt 신규 — 측정선·점·격자·거리라벨을 GL에서 직접 렌더. 측정선은 화면 공간 굵은 쿼드(기기별 glLineWidth 상한 회피), 점은 셰이더 원형 도트, 라벨은 텍스처 1회 베이크 → 라벨까지 완전 프레임 동기MeasurementActivity.kt — View 오버레이(격자·측정선) 폐지, 격자 고정 Anchor, live pose 렌더링, 버튼 커맨드의 GL 스레드 처리(앵커 경합 제거), 상태 UI는 변경 시에만 갱신BackgroundRenderer.kt — 배경 UV 변환을 hasDisplayGeometryChanged()일 때만 재계산| # | 확인 항목 | 기대 결과 |
|---|---|---|
| 1 | 바닥 인식 후 기기를 좌우로 흔들기 | 격자가 바닥 한 자리에 고정 |
| 2 | 점 2개 찍고 기기를 빠르게 이동 | 선이 벽/바닥 지점에 붙어 다님 (미끄러짐 없음) |
| 3 | 거리 라벨 관찰 | 선 중점 위에 고정 |
| 4 | 측정 후 방 한 바퀴 돌고 복귀 | 선이 원래 지점에 유지 (드리프트 보정) |
| 5 | undo · 축 전환 · 전송 | 기존과 동일 동작 |
검증용 APK: app/build/outputs/apk/debug/app-debug.apk (6.8MB) — 증상 영상을 촬영한 장소에서 확인 권장.
증상 영상은 벽 높이(예: 2702mm)를 측정하는 화면으로, 청록색 측정선·분홍 끝점·중앙 조준점이 카메라 영상 위에 오버레이됩니다. 기기를 움직이면 이 오버레이 그래픽이 실제 벽/바닥 지점에서 어긋나 흔들립니다.
본 진단은 3개 소스 파일 전체(MeasurementActivity.kt 647줄, BackgroundRenderer.kt, MainActivity.kt)와
매니페스트·빌드설정을 정적 분석하고, 영상 프레임과 대조해 확인했습니다. BackgroundRenderer.kt는 표준 구현으로 이상이 없으며,
MainActivity.kt는 비어 있고 실제 진입점은 MeasurementActivity 하나입니다.
mainHandler.post 렌더링
MeasurementActivity.kt · onDrawFrame() L423–515 (post L499–512)
onDrawFrame()은 GLSurfaceView 렌더 스레드(RENDERMODE_CONTINUOUSLY)에서 돌며, 카메라 영상은 GL로 즉시 그리지만
실제 AR 그래픽은 UI 스레드로 던져서 그립니다.
// onDrawFrame() 내부 — GL 렌더 스레드
backgroundRenderer.draw(frame, cameraTextureId) // ① 카메라 영상: GL에서 즉시 렌더 (최신 프레임)
...
val vp = /* 이 프레임의 projection × view 행렬 */
mainHandler.post { // ② UI 스레드로 비동기 전달 — 지연 발생
measureOverlay.startWorld = finalStart
measureOverlay.refresh(finalVp) // invalidate() → "다음 UI vsync"에 그려짐
}
MeasurementOverlay·FloorGridOverlay·CrosshairView는 전부 FrameLayout에 얹은 일반 View(Canvas onDraw)입니다.
GLSurfaceView(별도 SurfaceView 버퍼)와 View 오버레이(윈도우 버퍼)는 SurfaceFlinger가 별도 레이어로 합성. 하드웨어 레이어 + 비동기 invalidate라 스왑 타이밍이 어긋나면 그 자체로 어긋남이 보임.핵심: 오버레이가 “과거 카메라 포즈”의 VP로 투영된 채 “현재 카메라 영상” 위에 얹힙니다. 정지 시엔 괜찮아 보이다 움직이는 순간 어긋남이 드러납니다 — 영상의 튐과 정확히 일치.
hello_ar 방식). 점/선은 GL LINES/POINTS로, 거리 라벨은 같은 프레임에서 투영한 화면좌표로 반영. 안드로이드 View 오버레이는 버튼·텍스트 같은 화면 고정 2D UI에만 사용하고 월드에 붙는 기하는 절대 View로 그리지 않습니다.
val floorPlane = planes
.filter { it.trackingState == TRACKING && it.type == HORIZONTAL_UPWARD_FACING }
.minByOrNull { it.centerPose.ty() } // ← 매 프레임 "가장 낮은 평면"을 다시 선택
?: planes.firstOrNull { it.trackingState == TRACKING }
floorPlane?.let { plane ->
val cp = plane.centerPose // ← 계속 미세하게 흔들리는 값
floorGridOverlay.updateFromGl(vp, floatArrayOf(cp.tx(), cp.ty(), cp.tz()))
}
minByOrNull이 뽑는 평면이 바뀜 → 격자 중심이 통째로 순간이동.measureOverlay.lines.add(
MeasurementOverlay.MeasureLine(
floatArrayOf(sp.tx(), sp.ty(), sp.tz()), // 배치 순간의 정적 월드좌표
floatArrayOf(ep.tx(), ep.ty(), ep.tz()), // 이후 앵커 갱신을 반영하지 않음
distMm, label
)
)
startAnchor/endAnchor)는 만들어 두지만, 화면에 그리는 선은 배치 순간에 복사한 고정 좌표를 씁니다.onDrawFrame에서 startAnchor.pose를 매 프레임 다시 읽어 올바르게 따라감 → “진행 중 점은 맞고 확정된 선은 어긋나는” 비대칭 발생.anchor.pose로 좌표를 다시 읽어 투영(정적 스냅샷 폐기). 거리값(distMm)은 배치 시 1회 계산 유지 OK.val rotation = windowManager.defaultDisplay.rotation // deprecated, 매 프레임 디스플레이 상태 조회
session.setDisplayGeometry(rotation, viewportWidth, viewportHeight) // 매 프레임
session.setCameraTextureName(cameraTextureId) // 매 프레임 (이미 onSurfaceCreated/onResume에서 설정)
setDisplayGeometry는 지오메트리가 바뀔 때만(onSurfaceChanged/회전 변경) 호출해야 함. 매 프레임 호출 시 ARCore가 UV 변환을 매번 재계산 → 배경 변환이 미세하게 흔들릴 여지.windowManager.defaultDisplay.rotation은 deprecated이고 매 프레임 조회 불필요 → 캐시.setCameraTextureName은 텍스처가 안 바뀌면 1회면 충분.단독으로 큰 튐을 만들진 않지만 배경 변환 안정성을 떨어뜨려 1~3번을 증폭합니다.
mainHandler.post 블록이 매 프레임 statusText.text 문자열 조합·버튼 alpha·오버레이 invalidate 수행. UI 프레임이 밀리면 1번의 지연이 더 커짐.FloorGridOverlay.onDraw가 offset −2~+2, step 0.1로 ~41×2회 투영/drawLine을 UI 스레드에서 매 invalidate마다 실행 → UI 프레임 지연 가중.| 순위 | 조치 | 기대 효과 |
|---|---|---|
| 1 | 측정선·점·격자를 GL 프레임 통합 렌더링으로 전환(hello_ar 방식). View 오버레이는 버튼/텍스트 등 화면 고정 UI만 | 튐의 근본 제거 |
| 2 | 바닥격자를 고정 Anchor에 고정, 매 프레임 평면 재선택 제거 | 격자 순간이동·미끄러짐 제거 |
| 3 | 측정선을 live anchor.pose로 매 프레임 재투영(정적 좌표 폐기) | 확정 선 드리프트 제거 |
| 4 | setDisplayGeometry/setCameraTextureName/rotation을 변경 시에만 호출 | 배경 안정성 향상 |
| 5 | 매 프레임 UI 스레드 작업(텍스트/격자) 경량화 | 지연·끊김 완화 |
| 파일 | 위치 | 내용 |
|---|---|---|
MeasurementActivity.kt | L499–512 | mainHandler.post로 오버레이 비동기 갱신 (근본원인) |
MeasurementActivity.kt | L466–482 | 매 프레임 평면 재선택 + centerPose 기준 격자 재드로우 |
MeasurementActivity.kt | L534–540 | 측정선 정적 좌표 스냅샷 |
MeasurementActivity.kt | L427–429 | 세션 API 매 프레임 호출 |
BackgroundRenderer.kt | 전체 | 표준 카메라 배경 구현 — 이상 없음 |
MainActivity.kt | — | 비어 있음(미사용). 진입점은 MeasurementActivity |