Приложение концептуально очень простое и следует стандартному конвейеру ARCore:
- Рендеринг фона камеры
- Визуализация характерных точек
- Обнаружение плоскости
- Нажмите, чтобы разместить две точки и измерить расстояние
Однако я столкнулся с серьезной проблемой трудно диагностируемая нестабильность отслеживания, которую мне не удалось устранить, несмотря на несколько дней отладки.
При запуске приложения:
Инициализация сеанса ARCore
Отслеживание переходов из режима ПАУЗА → ОТСЛЕЖИВАНИЕ
Появляются функциональные точки
Плоскости обнаруживаются последовательно
Фактическое поведение (проблема)
Поведение случайное и недетерминированное:
- При холодном запуске приложение часто зависает в
TrackingState.PAUSED на неопределенный срок - Самолеты не обнаружены
- Функциональные точки минимальны или отсутствуют
- Иногда приложение отправляется в фоновый режим, а затем разблокируется на переднем плане отслеживание
- Отслеживание переходов в режим ОТСЛЕЖИВАНИЕ
- Появляются характерные точки и плоскости
- Даже когда оно разблокируется, отслеживание нестабильно:
- Быстрое мигание между ОТСЛЕЖИВАНИЕМ и ПАУЗОЙ
- Эта нестабильность препятствует надежному обнаружению плоскости
- Измерения AR становятся непригодными
- Журналы отображаются быстро чередование:
ОТСЛЕЖИВАНИЕ → ПАУЗА → ОТСЛЕЖИВАНИЕ → ПАУЗА
Жизненный цикл ARCore
- Session.configure() вызывается до session.resume()
- session.resume() и session.pause() в потоке пользовательского интерфейса
- session.update() вызывается из потока GL/render
- Нет одновременных вызовов резюме() / пауза()
- Текстура камеры (setCameraTextureName) устанавливается один раз, после создания контекста GL.
- Нет повторных вызовов для каждого кадра.
- Больше нет проблем с черным экраном.
- Проверено. оба:
- Config.UpdateMode.BLOCKING
- Config.UpdateMode.LATEST_CAMERA_IMAGE
- Нет изменений в поведении
- Проверки попадания отключены, пока отслеживание ПРИОСТАНОВЛЕНО
- Логика рендеринга не требует принудительного обновления во время ПАУЗЫ
- Мерцание не вызвано отрисовкой условий
- Похожие приложения (например, AR Ruler) работают хорошо
- Происходит при нескольких запусках на одном устройстве
- Одинаковое освещение и среда
- ARCore установлен и запущен на сегодняшний день
- Разрешения камеры предоставлены правильно
На данный момент удобство использования приложения скомпрометировано случайным отслеживанием сбоев и нестабильности запуска, и у меня заканчиваются гипотезы.
Любая информация от разработчиков с глубоким опытом работы с ARCore будет чрезвычайно признательна.
ЗДЕСЬ мой КОД (в этой версии) Я только инициализировал AR)
класс MainActivity : AppCompatActivity(), GLSurfaceView.Renderer {
Код: Выделить всё
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)
// UI
surfaceView = findViewById(R.id.surfaceView)
trackingStateText = findViewById(R.id.trackingStateText)
// Display rotation helper
displayRotationHelper = DisplayRotationHelper(this)
// GLSurfaceView setup
//surfaceView.holder.setFixedSize(480, 960) // RENDERING SCALING: 540x960 -> 1080x2185 (HW Scaler)
surfaceView.preserveEGLContextOnPause = true
surfaceView.setEGLContextClientVersion(2)
surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0)
surfaceView.setRenderer(this)
surfaceView.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
// Renderers (init dopo, su GL thread)
backgroundRenderer = BackgroundRenderer()
pointCloudRenderer = PointCloudRenderer()
Log.d(TAG, "onCreate completed")
}
override fun onResume() {
super.onResume()
// Check camera permission
if (!CameraPermissionHelper.hasCameraPermission(this)) {
CameraPermissionHelper.requestCameraPermission(this)
return
}
// Check ARCore availability
try {
when (ArCoreApk.getInstance().requestInstall(this, !installRequested)) {
ArCoreApk.InstallStatus.INSTALL_REQUESTED -> {
installRequested = true
return
}
ArCoreApk.InstallStatus.INSTALLED -> {
// Continue
}
else -> {
// Handle other cases
}
}
} catch (e: UnavailableException) {
Log.e(TAG, "ARCore not available", e)
Toast.makeText(this, "ARCore not available: ${e.message}", Toast.LENGTH_LONG).show()
finish()
return
}
// 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)")
}
// Resume helpers
displayRotationHelper.onResume()
surfaceView.onResume()
surfaceView.visibility = View.VISIBLE
}
override fun onPause() {
super.onPause()
// Pause SOLO se init completato
if (isGLInitCompleted) {
displayRotationHelper.onPause()
surfaceView.visibility = View.GONE
surfaceView.onPause()
session?.pause()
Log.d(TAG, "Session paused")
}
}
override fun onDestroy() {
super.onDestroy()
if (isGLInitCompleted) {
session?.close()
session = null
Log.d(TAG, "Session closed")
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
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)
val config = Config(s).apply {
focusMode = Config.FocusMode.AUTO
// PRIMA: updateMode = Config.UpdateMode.BLOCKING
// DOPO:
updateMode = Config.UpdateMode.LATEST_CAMERA_IMAGE
// PRIMA: planeFindingMode = Config.PlaneFindingMode.DISABLED
// DOPO:
planeFindingMode = Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL
// 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)
}
}
// ========== GLSurfaceView.Renderer ==========
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f)
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 onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
displayRotationHelper.onSurfaceChanged(width, height)
Log.d(TAG, "Surface changed: ${width}x${height}")
}
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
}
// 3. Update MVP matrices
camera.getProjectionMatrix(projectionMatrix, 0, 0.1f, 100.0f)
camera.getViewMatrix(viewMatrix, 0)
android.opengl.Matrix.multiplyMM(
viewProjectionMatrix, 0,
projectionMatrix, 0,
viewMatrix, 0
)
// 4. Draw point cloud
pointCloudRenderer.draw(viewProjectionMatrix)
} catch (e: Exception) {
Log.e(TAG, "Error in onDrawFrame", e)
}
}
/**
* 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)
}
}
Подробнее здесь: https://stackoverflow.com/questions/798 ... background