Я разрабатываю простое приложение AR-линейки для Android на основе Google ARCore.
Приложение концептуально очень простое и следует стандартному конвейеру ARCore:
Рендеринг фона камеры
Визуализация характерных точек
Обнаружение плоскости
Нажмите, чтобы разместить две точки и измерить расстояние
Однако я столкнулся с серьезной проблемой трудно диагностируемая нестабильность отслеживания, которую мне не удалось устранить, несмотря на несколько дней отладки.
Ожидаемое поведение
При запуске приложения:
Инициализация сеанса ARCore
Отслеживание переходов из режима ПАУЗА → ОТСЛЕЖИВАНИЕ
Появляются функциональные точки
Плоскости обнаруживаются последовательно Фактическое поведение (проблема)
Поведение случайное и недетерминированное:
При холодном запуске приложение часто зависает в
TrackingState.PAUSED на неопределенный срок
Самолеты не обнаружены
Функциональные точки минимальны или отсутствуют
Иногда приложение отправляется в фоновый режим, а затем разблокируется на переднем плане отслеживание
Отслеживание переходов в режим ОТСЛЕЖИВАНИЕ
Появляются характерные точки и плоскости
Даже когда оно разблокируется, отслеживание нестабильно:
Быстрое мигание между ОТСЛЕЖИВАНИЕМ и ПАУЗОЙ
Эта нестабильность препятствует надежному обнаружению плоскости
То, что я уже проверил/исключил. Я тщательно проверил и протестировал следующие моменты:
Жизненный цикл ARCore
Session.configure() вызывается до session.resume()
session.resume() и session.pause() в потоке пользовательского интерфейса
session.update() вызывается из потока GL/render
Нет одновременных вызовов резюме() / пауза()
Текстура камеры и настройка GL.
Текстура камеры (setCameraTextureName) устанавливается один раз, после создания контекста GL.
Нет повторных вызовов для каждого кадра.
- Больше нет проблем с черным экраном.
Режим обновления.
Проверено. оба:
Config.UpdateMode.BLOCKING
Config.UpdateMode.LATEST_CAMERA_IMAGE
Нет изменений в поведении
Проверка попадания и логика рендеринга
Проверки попадания отключены, пока отслеживание ПРИОСТАНОВЛЕНО
Логика рендеринга не требует принудительного обновления во время ПАУЗЫ
Мерцание не вызвано отрисовкой условий
Устройство и среда
Похожие приложения (например, AR Ruler) работают хорошо
Происходит при нескольких запусках на одном устройстве
Одинаковое освещение и среда
ARCore установлен и запущен на сегодняшний день
Разрешения камеры предоставлены правильно
Вывод
На данный момент удобство использования приложения скомпрометировано случайным отслеживанием сбоев и нестабильности запуска, и у меня заканчиваются гипотезы.
Любая информация от разработчиков с глубоким опытом работы с ARCore будет чрезвычайно признательна. ЗДЕСЬ мой КОД (в этой версии) Я только инициализировал AR)
класс MainActivity : AppCompatActivity(), GLSurfaceView.Renderer {
Я разрабатываю простое приложение AR-линейки для Android на основе Google ARCore. Приложение концептуально очень простое и следует стандартному конвейеру ARCore: [list] [*]Рендеринг фона камеры [*]Визуализация характерных точек [*]Обнаружение плоскости [*]Нажмите, чтобы разместить две точки и измерить расстояние Однако я столкнулся с серьезной проблемой трудно диагностируемая нестабильность отслеживания, которую мне не удалось устранить, несмотря на несколько дней отладки. [/list] [b]Ожидаемое поведение[/b] При запуске приложения: Инициализация сеанса ARCore Отслеживание переходов из режима ПАУЗА → ОТСЛЕЖИВАНИЕ Появляются функциональные точки Плоскости обнаруживаются последовательно [b]Фактическое поведение (проблема)[/b] Поведение случайное и недетерминированное: [list] [*]При холодном запуске приложение часто зависает в TrackingState.PAUSED на неопределенный срок [*]Самолеты не обнаружены [*]Функциональные точки минимальны или отсутствуют [*]Иногда приложение отправляется в фоновый режим, а затем разблокируется на переднем плане отслеживание [*]Отслеживание переходов в режим ОТСЛЕЖИВАНИЕ [*]Появляются характерные точки и плоскости [*]Даже когда оно разблокируется, отслеживание нестабильно: [*]Быстрое мигание между ОТСЛЕЖИВАНИЕМ и ПАУЗОЙ [*]Эта нестабильность препятствует надежному обнаружению плоскости [*]Измерения AR становятся непригодными [*]Журналы отображаются быстро чередование: ОТСЛЕЖИВАНИЕ → ПАУЗА → ОТСЛЕЖИВАНИЕ → ПАУЗА [/list] [b]То, что я уже проверил/исключил[/b]. Я тщательно проверил и протестировал следующие моменты: Жизненный цикл ARCore [list] [*]Session.configure() вызывается до session.resume() [*]session.resume() и session.pause() в потоке пользовательского интерфейса [*]session.update() вызывается из потока GL/render [*]Нет одновременных вызовов резюме() / пауза() [/list] ✅ Текстура камеры и настройка GL. [list] [*]Текстура камеры (setCameraTextureName) устанавливается один раз, после создания контекста GL. [*]Нет повторных вызовов для каждого кадра. - Больше нет проблем с черным экраном. [/list] ✅ Режим обновления. [list] [*]Проверено. оба: [*]Config.UpdateMode.BLOCKING [*]Config.UpdateMode.LATEST_CAMERA_IMAGE [*]Нет изменений в поведении [/list] ✅ Проверка попадания и логика рендеринга [list] [*]Проверки попадания отключены, пока отслеживание ПРИОСТАНОВЛЕНО [*]Логика рендеринга не требует принудительного обновления во время ПАУЗЫ [*]Мерцание не вызвано отрисовкой условий [/list] ✅ Устройство и среда [list] [*]Похожие приложения (например, AR Ruler) работают хорошо [*]Происходит при нескольких запусках на одном устройстве [*]Одинаковое освещение и среда [*]ARCore установлен и запущен на сегодняшний день [*]Разрешения камеры предоставлены правильно [/list] [b]Вывод[/b] На данный момент удобство использования приложения скомпрометировано случайным отслеживанием сбоев и нестабильности запуска, и у меня заканчиваются гипотезы. Любая информация от разработчиков с глубоким опытом работы с ARCore будет чрезвычайно признательна. [b]ЗДЕСЬ мой КОД (в этой версии) Я только инициализировал AR)[/b] класс MainActivity : AppCompatActivity(), GLSurfaceView.Renderer { [code]companion object { private const val TAG = "MainActivity" }
// UI private lateinit var surfaceView: GLSurfaceView private lateinit var trackingStateText: TextView
// ARCore private var session: Session? = null private var installRequested = false private var isGLInitCompleted = false private var lastTrackingState: TrackingState? = null
// Helpers private lateinit var displayRotationHelper: DisplayRotationHelper
// Renderers private lateinit var backgroundRenderer: BackgroundRenderer private lateinit var pointCloudRenderer: PointCloudRenderer
// MVP matrices private val projectionMatrix = FloatArray(16) private val viewMatrix = FloatArray(16) private val viewProjectionMatrix = FloatArray(16)
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main)
// Create session se necessario if (session == null) { try { session = Session(this) configureSession() Log.d(TAG, "Session created and configured") val size = session!!.cameraConfig.imageSize Log.e(TAG, "VERSIONE 6") Log.e(TAG, "ACTUAL RUNNING CONFIG: ${size.width}x${size.height}") } catch (e: Exception) { Log.e(TAG, "Failed to create session", e) Toast.makeText(this, "Failed to create ARCore session", Toast.LENGTH_LONG).show() finish() return } }
// CRITICO: Resume session SOLO se GL è inizializzato (texture già settata) // Al primo avvio: NON fare resume qui. Session sarà resumata in onSurfaceCreated // DOPO setCameraTextureName (ordine: texture → resume → update) // Nei resume successivi: GL pronto, texture valida, resume qui if (isGLInitCompleted) { try { session?.setCameraTextureName(backgroundRenderer.getTextureId()) session?.resume() Log.d(TAG, "Session resumed from onResume (GL ready)") } catch (e: CameraNotAvailableException) { Log.e(TAG, "Camera not available", e) Toast.makeText(this, "Camera not available", Toast.LENGTH_LONG).show() session = null finish() return } } else { Log.d(TAG, "Session created but NOT resumed (waiting for GL init)") }
if (!CameraPermissionHelper.hasCameraPermission(this)) { Toast.makeText(this, "Camera permission required", Toast.LENGTH_LONG).show() if (!CameraPermissionHelper.shouldShowRequestPermissionRationale(this)) { CameraPermissionHelper.launchPermissionSettings(this) } finish() } }
/** * Configura ARCore Session. * Li aggiungiamo come TENTATIVO per vedere se migliorano stabilità tracking. */ private fun configureSession() { Log.e(TAG, "CHIAMO CONFIGURE SESSION") val s = session ?: return
// CRITICO: Seleziona camera config a 307k pixel (640x480) // setCameraConfig DEVE essere chiamato PRIMA di session.resume() selectBestCameraConfig(s)
// DepthMode: DISABLED (default). // AUTOMATIC aggiunge carico computazionale inutile per questa fase. }
s.configure(config) }
/** * Seleziona la camera config con risoluzione piu vicina a 307.200 pixel (640x480). * Riduce il carico di processing → tracking piu stabile. */ private fun selectBestCameraConfig(session: Session) { try { val filter = CameraConfigFilter(session) filter.setTargetFps(EnumSet.of(CameraConfig.TargetFps.TARGET_FPS_30))
val configs = session.getSupportedCameraConfigs(filter) if (configs.isEmpty()) { Log.w(TAG, "No supported camera configs found, using default") return }
// Trova la config piu vicina a 307.200 pixel (640x480) val TARGET_PIXELS = 307200.0f var bestConfig: CameraConfig? = null var bestDiff = Float.MAX_VALUE
for (config in configs) { val size = config.imageSize val pixels = (size.width * size.height).toFloat() val diff = Math.abs(TARGET_PIXELS - pixels) if (diff < bestDiff) { bestConfig = config bestDiff = diff } }
if (bestConfig != null) { session.cameraConfig = bestConfig val size = bestConfig.imageSize Log.d(TAG, "Camera config: ${size.width}x${size.height} " + "(${size.width * size.height} pixels, target=$TARGET_PIXELS)") } } catch (e: Exception) { Log.e(TAG, "Failed to select camera config, using default", e) } }
try { // Init renderers su GL thread backgroundRenderer.createOnGlThread(this) pointCloudRenderer.createOnGlThread(this)
// 1. Set texture PRIMA di resume session?.setCameraTextureName(backgroundRenderer.getTextureId())
isGLInitCompleted = true
// 2. Resume session ORA che la texture è settata // Al primo avvio, onResume() NON ha fatto resume (perché isGLInitCompleted era false) // Quindi la session è in stato "paused" e la resumiamo qui session?.resume()
Log.d(TAG, "GL init complete, texture=${backgroundRenderer.getTextureId()}, session resumed") } catch (e: CameraNotAvailableException) { Log.e(TAG, "Camera not available on first resume", e) finish() } catch (e: Exception) { Log.e(TAG, "Failed to initialize GL", e) } }
override fun onDrawFrame(gl: GL10?) { // Clear GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
val session = this.session ?: return
// Skip se GL non pronto if (!isGLInitCompleted) { return }
try { // CRITICO: setDisplayGeometry PRIMA di update() displayRotationHelper.updateSessionIfNeeded(session)
// ===== UPDATE PHASE ===== val frame = session.update() val camera = frame.camera
if (camera.trackingState == TrackingState.PAUSED) { val reason = camera.trackingFailureReason Log.e(TAG, "PAUSED reason: $reason") }
// .use {} garantisce close() in tutti i path (normale + eccezione) frame.acquirePointCloud().use { pointCloud -> pointCloudRenderer.update(pointCloud) }
// Update tracking state su UI thread // FIX PERFORMANCE: Aggiorna la UI solo se lo stato cambia if (camera.trackingState != lastTrackingState) { lastTrackingState = camera.trackingState updateTrackingStateUI(camera.trackingState, camera.trackingFailureReason) }
// ===== DRAW PHASE =====
// 1. Draw camera background SEMPRE (anche se tracking PAUSED) // 2. Se tracking PAUSED, skip overlay (ma background gia disegnato!) if (camera.trackingState == TrackingState.PAUSED) { return }
/** * Aggiorna UI tracking state (chiamato da GL thread). */ private fun updateTrackingStateUI(state: TrackingState, reason: com.google.ar.core.TrackingFailureReason?) { runOnUiThread { val text = when (state) { TrackingState.TRACKING -> "✓ TRACKING" TrackingState.PAUSED -> "⚠ PAUSED - ${reason?.name ?: "unknown"}" TrackingState.STOPPED -> "✗ STOPPED" }
val color = when (state) { TrackingState.TRACKING -> 0xFF00FF00.toInt() // Green TrackingState.PAUSED -> 0xFFFFAA00.toInt() // Orange TrackingState.STOPPED -> 0xFFFF0000.toInt() // Red }
trackingStateText.text = text trackingStateText.setTextColor(color) } } [/code] }