1차 진단 보고서 · First-pass Diagnosis

ARMeasureApp — AR 오버레이 “튐 현상” 원인 진단

측정선·점·바닥격자가 카메라 움직임에 따라 미끄러지고 튀는 현상에 대한 소스코드 기반 정밀 진단

대상 ARMeasureApp (Android · Kotlin · ARCore 1.44.0) 진단 방식 소스코드 정적 분석 + 증상 영상 대조 작성일 2026-07-08 작성 claude 상태 수정 반영 완료 · 빌드 검증 통과

0결론 요약 (TL;DR)

튐은 버그 한 줄이 아니라 렌더링 아키텍처 자체의 구조적 결함입니다.

카메라 배경은 OpenGL(GL 스레드)로 그리는데, 측정선·점·바닥격자·조준점은 그 위에 얹은 일반 안드로이드 ViewmainHandler.post(UI 스레드)로 갱신합니다. 두 화면은 서로 다른 스레드·서로 다른 버퍼에서 비동기로 그려지므로 프레임이 절대 딱 맞지 않습니다. 카메라 배경은 항상 최신, 오버레이는 항상 1~3프레임(16~50ms) 뒤 → 카메라를 움직일 때마다 선/점이 실제 위치에서 미끄러졌다 따라붙는 것이 곧 “튐”입니다. 정지 시엔 괜찮다가 움직일 때 드러나는 것도 이 때문입니다.

근본 해결 = 오버레이를 GL 프레임 안에서 카메라와 함께 렌더링. 1번 항목만 제대로 고쳐도 체감 튐의 대부분이 사라집니다.

순위원인심각도
1오버레이가 GL 카메라와 프레임 동기화되지 않음 — 2D View + mainHandler.post치명·근본
2바닥격자를 매 프레임 다른 평면·움직이는 centerPose 기준으로 다시 그림큰 기여
3확정된 측정선이 배치 시점 좌표로 고정 → ARCore 재보정 시 드리프트기여
4세션 API를 매 프레임 불필요하게 호출 (setDisplayGeometry 등)기여

수정 반영 결과 — 기대 개선율 90~95%

진단서의 원인 1~4번을 전부 코드에 반영했고, 빌드 검증(BUILD SUCCESSFUL · 컴파일 경고 0건)까지 통과했습니다.

영상에서 심각하게 보였던 튐 — 카메라를 움직일 때 선/점/격자가 실제 위치에서 미끄러졌다 따라붙는 현상 — 은 오버레이가 카메라와 같은 GL 프레임에서 함께 그려지도록 구조를 바꿨기 때문에 원리상 어긋날 수 없게 되어 사실상 100% 제거됩니다. 전체 체감 기준으로는 90~95% 개선을 예상하며, 정확한 수치는 실기기 검증으로 확정합니다.

영상에서 보인 증상원인 (기여도)수정 후
카메라를 움직이면 선·점이 미끄러졌다 따라붙음 (가장 심한 튐)렌더링 프레임 비동기 (~70%)구조적으로 제거 — 카메라와 같은 GL 프레임·같은 VP 행렬로 렌더
격자가 통째로 순간이동 · 스르륵 밀림매 프레임 평면 재선택 (~15%)제거 — 고정 Anchor에 1회 고정 (0.3m 이상 낮은 바닥 발견 시에만 이동)
측정선이 찍은 지점에서 서서히 어긋남배치 시점 좌표 박제 (~10%)제거 — 매 프레임 live anchor.pose 재투영
배경 미세 흔들림세션 API 매 프레임 호출 (~5%)제거 — 지오메트리 변경 시에만 호출

남는 5~10%는 무엇인가

ARCore 공간 추적 자체의 한계입니다 — 기기를 급격히 흔들거나, 조명이 어둡거나, 무늬 없는 민무늬 벽·유리를 비출 때 공간 인식이 흔들리는 것. 증상 형태가 다릅니다: 수정 전처럼 "선이 카메라 영상 위에서 따로 노는" 게 아니라 "격자+선+배경이 통째로 살짝 출렁이는" 형태이며, 삼성 측정 앱 등 어떤 AR 앱도 피할 수 없는 영역입니다. 상태 안내 문구("카메라를 천천히 이동하세요")가 이 상황을 사용자에게 안내합니다.

변경 내용 요약

실기기 검증 체크리스트

#확인 항목기대 결과
1바닥 인식 후 기기를 좌우로 흔들기격자가 바닥 한 자리에 고정
2점 2개 찍고 기기를 빠르게 이동선이 벽/바닥 지점에 붙어 다님 (미끄러짐 없음)
3거리 라벨 관찰선 중점 위에 고정
4측정 후 방 한 바퀴 돌고 복귀선이 원래 지점에 유지 (드리프트 보정)
5undo · 축 전환 · 전송기존과 동일 동작

검증용 APK: app/build/outputs/apk/debug/app-debug.apk (6.8MB) — 증상 영상을 촬영한 장소에서 확인 권장.

·대상 및 증상 자료

ARMeasureApp
플랫폼Android / Kotlin
AR SDKARCore 1.44.0
진입 액티비티MeasurementActivity
증상 영상 ①AR+튐+현상+1.mp4 (45s)
증상 영상 ②AR+튐+현상2.mp4 (16s)
해상도1080×2400 화면녹화

증상 영상은 벽 높이(예: 2702mm)를 측정하는 화면으로, 청록색 측정선·분홍 끝점·중앙 조준점이 카메라 영상 위에 오버레이됩니다. 기기를 움직이면 이 오버레이 그래픽이 실제 벽/바닥 지점에서 어긋나 흔들립니다.

본 진단은 3개 소스 파일 전체(MeasurementActivity.kt 647줄, BackgroundRenderer.kt, MainActivity.kt)와 매니페스트·빌드설정을 정적 분석하고, 영상 프레임과 대조해 확인했습니다. BackgroundRenderer.kt는 표준 구현으로 이상이 없으며, MainActivity.kt는 비어 있고 실제 진입점은 MeasurementActivity 하나입니다.

1근본 원인 — 오버레이가 카메라와 프레임 동기화되지 않음

치명 · 근본원인 2D View 오버레이 + 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)입니다.

왜 튀는가 — 3중 문제

  • ① 스레드/vsync 지연 — 카메라 텍스처는 GL 프레임에서 즉시 표시되지만, 오버레이는 Handler 메시지가 UI 스레드에서 처리된 뒤 다음 UI vsync에 그려집니다. 항상 ≥1프레임(16~50ms) 뒤처짐. 움직이는 동안 선/점이 뒤로 미끄러졌다 따라붙음.
  • ② 두 루프 간 배리어 없음 — GL 프레임과 UI 프레임은 동기화 지점이 없는 독립 루프. 부하가 걸리면 GL은 여러 번 도는데 UI는 합쳐 한 번 그림 → 끊김/점프.
  • ③ 서로 다른 Surface 합성GLSurfaceView(별도 SurfaceView 버퍼)와 View 오버레이(윈도우 버퍼)는 SurfaceFlinger가 별도 레이어로 합성. 하드웨어 레이어 + 비동기 invalidate라 스왑 타이밍이 어긋나면 그 자체로 어긋남이 보임.

핵심: 오버레이가 “과거 카메라 포즈”의 VP로 투영된 채 “현재 카메라 영상” 위에 얹힙니다. 정지 시엔 괜찮아 보이다 움직이는 순간 어긋남이 드러납니다 — 영상의 튐과 정확히 일치.

▶ 근본 해결
카메라 배경과 모든 월드 고정 그래픽(점·선·격자)을 같은 GL 프레임 안에서, 같은 프레임의 projection/view 행렬로 렌더링해야 픽셀 단위로 카메라에 고정됩니다 (Google 공식 hello_ar 방식). 점/선은 GL LINES/POINTS로, 거리 라벨은 같은 프레임에서 투영한 화면좌표로 반영. 안드로이드 View 오버레이는 버튼·텍스트 같은 화면 고정 2D UI에만 사용하고 월드에 붙는 기하는 절대 View로 그리지 않습니다.

2바닥격자를 매 프레임 다시 고르고 다시 중심 잡음

큰 기여 격자 평면 재선택 + 흔들리는 centerPose 기준 재드로우 MeasurementActivity.kt · onDrawFrame() L466–482
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()))
}
  • 평면이 매 프레임 바뀔 수 있음 — ARCore가 평면을 새로 찾거나 병합/소실하면 minByOrNull이 뽑는 평면이 바뀜 → 격자 중심이 통째로 순간이동.
  • centerPose는 계속 드리프트함 — 같은 평면이라도 ARCore가 범위/중심을 재추정하며 중심이 미세하게 이동. 격자를 이 움직이는 중심 ±2m로 매 프레임 다시 그리므로 격자 전체가 스르륵 미끄러짐.
  • 이 격자 역시 1번의 View 오버레이라 프레임 지연까지 그대로 받음 → 이중 튐.
▶ 개선
격자를 한 번 생성한 고정 Anchor(또는 첫 감지 평면의 안정 포즈)에 고정하고, 매 프레임 평면을 다시 선택하지 않는다.

3확정된 측정선이 “배치 시점 좌표”로 고정됨 → 드리프트

기여 정적 월드좌표 스냅샷 사용, live anchor 미반영 MeasurementActivity.kt · placePoint() L534–540
measureOverlay.lines.add(
    MeasurementOverlay.MeasureLine(
        floatArrayOf(sp.tx(), sp.ty(), sp.tz()),   // 배치 순간의 정적 월드좌표
        floatArrayOf(ep.tx(), ep.ty(), ep.tz()),   // 이후 앵커 갱신을 반영하지 않음
        distMm, label
    )
)
  • 시작/끝 Anchor(startAnchor/endAnchor)는 만들어 두지만, 화면에 그리는 선은 배치 순간에 복사한 고정 좌표를 씁니다.
  • ARCore가 이후 공간을 재국소화하며 월드 좌표계를 보정하는데, 선은 옛 좌표에 박혀 있어 실제 찍은 지점에서 서서히 어긋남(드리프트).
  • 참고: 진행 중인 시작점 도트onDrawFrame에서 startAnchor.pose를 매 프레임 다시 읽어 올바르게 따라감 → “진행 중 점은 맞고 확정된 선은 어긋나는” 비대칭 발생.
▶ 개선
그릴 때마다 live anchor.pose로 좌표를 다시 읽어 투영(정적 스냅샷 폐기). 거리값(distMm)은 배치 시 1회 계산 유지 OK.

4세션 API를 매 프레임 불필요하게 호출

기여 setDisplayGeometry / setCameraTextureName / deprecated rotation 매 프레임 호출 MeasurementActivity.kt · onDrawFrame() L427–429
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번을 증폭합니다.

▶ 개선
세 호출을 변경 시점에만 수행하도록 이동/캐시.

5부차적으로 튐을 키우는 요소

6권장 조치 순서

순위조치기대 효과
1측정선·점·격자를 GL 프레임 통합 렌더링으로 전환(hello_ar 방식). View 오버레이는 버튼/텍스트 등 화면 고정 UI만튐의 근본 제거
2바닥격자를 고정 Anchor에 고정, 매 프레임 평면 재선택 제거격자 순간이동·미끄러짐 제거
3측정선을 live anchor.pose로 매 프레임 재투영(정적 좌표 폐기)확정 선 드리프트 제거
4setDisplayGeometry/setCameraTextureName/rotation을 변경 시에만 호출배경 안정성 향상
5매 프레임 UI 스레드 작업(텍스트/격자) 경량화지연·끊김 완화
우선순위 판단
1번만 제대로 적용해도 체감 튐의 대부분이 사라집니다. 2·3번은 그 위에 남는 “격자·선이 스르륵 밀리는” 잔여 현상을 잡는 작업이고, 4·5번은 안정성·성능 마감입니다.

7근거 파일 / 위치

파일위치내용
MeasurementActivity.ktL499–512mainHandler.post로 오버레이 비동기 갱신 (근본원인)
MeasurementActivity.ktL466–482매 프레임 평면 재선택 + centerPose 기준 격자 재드로우
MeasurementActivity.ktL534–540측정선 정적 좌표 스냅샷
MeasurementActivity.ktL427–429세션 API 매 프레임 호출
BackgroundRenderer.kt전체표준 카메라 배경 구현 — 이상 없음
MainActivity.kt비어 있음(미사용). 진입점은 MeasurementActivity