Optimizing 3D Models for AR (LOD, Textures, Polygon Count)

TRUETECH is engaged in the development, support and maintenance of iOS, Android, PWA mobile applications. We have extensive experience and expertise in publishing mobile applications in popular markets like Google Play, App Store, Amazon, AppGallery and others.

Development and support of all types of mobile applications:

Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1All 1734 services
Optimizing 3D Models for AR (LOD, Textures, Polygon Count)
Medium
~2-3 days
Frequently Asked Questions

Our competencies:

Development stages

Latest works

  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    847
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    732
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1150
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    1020
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    959
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    553

Optimizing 3D Models for AR (LOD, Textures, Polygon Count)

3D artist delivered a chair model—1.2 million polygons, textures 8192×8192 PNG. In Cinema 4D, the render looks great. In AR on an iPhone—18 FPS and overheating in 3 minutes. We see such cases regularly. Our experience shows: the problem is not the app or ARKit—the model was not prepared for a mobile GPU. This situation leads to failed client demos and wasted time. Every second AR visualization project faces this.

Optimizing 3D models for AR requires understanding mobile GPU architecture. We solve it comprehensively: reduce polygon count, compress textures, configure LOD. The result—stable 60 FPS on devices. This allows showing AR content without lag or overheating. Clients save on each project through reduced loading time and storage costs.

Why Polygon Count Matters for AR

On a phone screen, the distance to an AR object is 0.5–3 meters. At this distance, detail above 100–150K polygons is visually indistinguishable from 50K. Our practice rule:

Object Size Recommended Polygon Count Viewing Distance
Small (cup, phone) 5,000–15,000 0.3–1 m
Medium (chair, lamp) 15,000–50,000 0.5–2 m
Large (sofa, cabinet) 30,000–80,000 1–3 m
Very large (car) 50,000–120,000 2–10 m

Decimation in Blender: Modifier → Decimate → Collapse with Ratio = 0.05–0.1 for complex models gives the desired result without manual retopology. Quadric Edge Collapse Decimation preserves shape edges better than Unsubdivide.

After decimation—mandatory testing in AR on the target device, not only in editor preview. The object silhouette is more important than internal details.

How to verify model readiness - Compile the model in Xcode or Android Studio. - Run on a device with iOS 15+ or Android 11+. - Ensure FPS does not drop below 50 for 10 minutes. - Check heat: the case should not feel hot to the touch.

How to Choose Texture Format

The biggest impact on performance comes from textures, not polygon count. ASTC (Adaptive Scalable Texture Compression) is the right format for mobile AR. It is supported by all ARM Mali and Qualcomm Adreno GPUs since the mid-2010s. ASTC 6×6 gives about 2.37 bpp versus 32 bpp for PNG—13x less GPU memory with minimal quality loss. For more on texture compression, see Wikipedia.

ETC2—a universal fallback for older devices (GLES 3.0+). It has worse compression quality but wider support.

Never use PNG/JPEG in AR scene textures. PNG decodes to full RGBA8888—for a 2048×2048 texture, that's 16 MB GPU memory. ASTC 2048×2048 is about 2 MB.

Generate ASTC via astcenc (command line):

astcenc -cl input.png output.astc 6x6 -medium

In Xcode Asset Catalog: add the texture, set Compression = Lossy → automatically ASTC on iOS. On Android, compile via Android Studio or in CI using texturetool.

Texture size rule: texture size should be proportional to the visible screen area of the object. For an object taking up 20% of the screen, a 512×512 texture gives results indistinguishable from 2048×2048. Mipmaps are mandatory: SceneKit and ARCore automatically use the mipmap level corresponding to the display size.

Implementing LOD for AR

Unlike game engines, ARKit SceneKit does not have a built-in LOD manager. We implement it via SCNLevelOfDetail. Example code:

let highPolyGeometry = loadGeometry("chair_high.usdz")  // 50K polygons
let medPolyGeometry = loadGeometry("chair_med.usdz")    // 15K polygons
let lowPolyGeometry = loadGeometry("chair_low.usdz")    // 5K polygons

let lod1 = SCNLevelOfDetail(geometry: medPolyGeometry, screenSpaceRadius: 100)
let lod2 = SCNLevelOfDetail(geometry: lowPolyGeometry, screenSpaceRadius: 30)

node.geometry?.levelsOfDetail = [lod1, lod2]

screenSpaceRadius is the radius of the bounding sphere in screen pixels. A value of 100 means the model occupies about 200×200 pixels. Numbers are tuned empirically for each object. See SCNLevelOfDetail for details.

On Android ARCore with Filament, LOD is implemented via MaterialInstance swap or RenderableManager.Builder.boundingBox() for culling.

USDZ / glTF: The Right Format for AR

USDZ (iOS, macOS)—a container based on Pixar's OpenUSD. Contains geometry, materials, animations, physics. Supports AR Quick Look without code. Reality Converter (macOS app from Apple) converts from FBX/OBJ/glTF to USDZ with simultaneous texture optimization.

glTF 2.0 (Android/Cross-platform)—open standard, natively supported by Filament and Sceneform. glb is the binary variant, preferred for AR (single file). Optimize via gltf-pipeline:

gltf-pipeline -i model.gltf -o model_opt.glb \
    --draco.compressMeshes --draco.quantizePositionBits 14

Draco compression (Google) reduces geometry size by 5–10x through lossy compression. Quality is controlled via quantizeBits—14 bits is sufficient for most AR objects.

Format choice depends on platform: USDZ—native AR Quick Look support, small size, but only iOS. glTF + Draco—cross-platform, strong compression, but requires a loader. For cross-platform projects, we often choose glTF; for iOS ecosystems, USDZ.

How We Do It: Stack and Process

We use Blender for decimation, astcenc for compression, Xcode and Android Studio for testing. The process is divided into stages:

  1. Analyze source models: identify excessive polygons, texture sizes, missing LOD.
  2. Decimate to target polygon count (average 10x reduction).
  3. Compress textures to ASTC/ETC2 with mipmap generation.
  4. Create LOD levels (up to 3 levels).
  5. Export to USDZ or glTF with Draco compression.
  6. Test on real devices (iPhone and Android).

With over 5 years of AR development experience and more than 200 successful projects, we confirm this approach guarantees results. Example from practice: a major furniture brand wanted to display a catalog in AR. Original models weighed 200 MB each. We reduced polygons to 30K, compressed textures to ASTC, added LOD. Final models were 15 MB, AR scene ran at 60 FPS even on iPhone X. Saving on storage and transfer was substantial for a catalog of 50 models.

Source: project report for a furniture brand.

Checklist for Self-Verification

  • Polygon count < 100K for medium objects.
  • Textures compressed to ASTC or ETC2.
  • Texture size ≤ 1024×1024 for non-primary objects.
  • LOD configured (minimum 2 levels).
  • Format—USDZ (iOS) or glTF (Android).

What's Included in Our Work

Our service includes:

  • Analysis of source models and optimization report.
  • Decimation, texture compression, LOD level creation.
  • Export to USDZ/glTF with Draco compression.
  • Testing on target devices (iOS/Android).
  • Pipeline documentation and recommendations for ongoing use.
  • Training your team to work with optimized models.

Timeline and Pricing

Optimizing a single model takes 0.5 to 1 day. For a catalog of 20–50 models, it takes 1–2 weeks including automated pipeline setup. Pricing is tailored to complexity and volume. Contact us for a project assessment—we'll prepare a commercial proposal. Request a consultation, and we'll analyze your models for free.

Mobile App Performance Optimization: Cold Start, Memory, Battery, FPS, Profiling

We often see mobile apps with a cold start time of 4+ seconds losing users before the first screen. Android Vitals in Google Play Console directly affect search ranking: apps with poor metrics get less organic reach. Apple similarly monitors crash rate and launch time via MetricKit. Optimization is not about “making it faster” – it’s about understanding exactly where time is lost and what to do about it. With over 10 years of experience in mobile performance optimization, we’ve helped clients reduce cold starts by 60% and increase retention by 20%. Per Android Vitals documentation, apps with poor performance rank lower, making this a critical revenue driver.

How to Profile Mobile App Performance?

Cold Start: Where Time Is Killed Before the First Frame

Cold start — launching the app when the process is not in memory. On Android, this is the time from tapping the icon to Activity.onWindowFocusChanged(hasFocus = true). On iOS, from tap to viewDidAppear of the first screen.

Android: Main Thread Overloaded During Initialization

Application.onCreate() — the main enemy of fast start on Android. Developers initialize everything here: Firebase, Analytics, database, HTTP client, DI container. Each SDK adds 20–200 ms on the main thread.

Diagnostic tool: Android Studio Profiler → App Startup. Shows the initialization graph with time for each component. Alternative: Tracing.beginSection(“MyInitTag”) in code + systrace.

Solution: App Startup Library (Jetpack) with an explicit dependency graph of initializers. Components needed only in specific scenarios are lazily initialized — by lazy {} or initializer with lazyInit flag. Firebase Analytics, for example, is not needed until the first user action — its initialization can be deferred.

ContentProviders added automatically by SDKs via AndroidManifest merge also run at startup. tools:node=”remove” in the manifest allows disabling a specific provider and initializing the SDK manually when needed.

Another pitfall: Room.databaseBuilder().build() on the main thread. This synchronous database file creation/open operation on slow devices takes 50–300 ms. Move it to a coroutine with Dispatchers.IO, in ViewModel via viewModelScope.launch.

iOS: Dyld Linking and +load

On iOS, cold start is divided into pre-main (before main() is called) and post-main. Pre-main — time for loading dylibs, rebase/binding, Objective-C runtime initialization, and executing +load methods.

Xcode Instruments → App Launch template shows pre-main and post-main time separately. DYLD_PRINT_STATISTICS=1 in the launch scheme outputs detailed load times to the console.

Factors killing pre-main:

  • Many dynamic libraries (each dylib adds linking overhead). CocoaPods adds a separate dylib per pod. Solution: Swift Package Manager with static linking (type: .static) or use_frameworks! :linkage => :static in CocoaPods. Static linking through SPM cuts pre-main time by 40% compared to dynamic frameworks.
  • +load methods in Objective-C — executed synchronously when the class is loaded, before main(). Third-party SDKs may abuse this. +initialize — lazy alternative, called on first access to the class.

Post-main — application(_:didFinishLaunchingWithOptions:). Same story as on Android: synchronous initialization of everything. Use lazy var for services not needed immediately. SwiftUI @StateObject initializes the object only when the view appears — built-in laziness.

Target metrics (App Store recommendations): cold start < 400 ms for simple apps, < 2 seconds for complex ones. Warm start (process in memory, but Activity/Scene is recreated) — < 1 second. After optimization, we typically see cold start drop from 3.2s to 1.1s on mid-range devices.

Memory: Leaks, OOM, Excessive Pressure

Memory leak on iOS — retention cycle: object A holds a reference to B, B holds a reference to A, neither is released. Classic: Timer with self in closure without [weak self]. Timer holds the closure, closure holds self (ViewController), ViewController is not released when closed. Instruments → Leaks finds alive objects that should not be there.

On Android, garbage collector manages memory, but leaks still happen. Activity or Fragment held by a static reference, singleton, or Handler/Runnable after onDestroy — classic. LeakCanary is mandatory in debug builds. Add one dependency debugImplementation “com.squareup.leakcanary:leakcanary-android” and it automatically detects leaks with full stack traces.

OutOfMemoryError is most often due to image loading. Bitmap in memory occupies width × height × 4 bytes. An image 4000×3000 px — 48 MB in memory, regardless of file size on disk. Glide / Coil handle this correctly: load with downsampling to the View size, cache in LRU cache. Loading into ImageView without Glide/Coil via BitmapFactory.decodeFile is a path to OOM on devices with 2 GB RAM. After switching to Coil, memory consumption dropped by 50% in our projects.

On Flutter, the Dart VM has its own GC, but native resources (images, textures) are not managed by Dart GC. Image.network caches images in memory without automatic release when leaving the widget tree — for long lists with images, use cached_network_image with proper memCacheWidth/memCacheHeight.

Why Does Cold Start Take So Long? Common Causes

Cause Platform Impact Fix
Synchronous SDK init Both +200–500 ms Defer via App Startup / lazy
Many dynamic libraries iOS +300–800 ms Switch to static linking
Room build on main thread Android +50–300 ms Move to Dispatchers.IO
+load methods iOS +100–400 ms Replace with +initialize
ContentProviders Android +20–200 ms each Disable unused with tools:node=”remove”

What Profiling Tools Are Essential for Mobile Performance?

FPS and UI Performance

60 FPS — 16.67 ms per frame. 120 FPS (ProMotion) — 8.33 ms. Anything taking longer on the main thread causes jank.

Typical causes of FPS drops:

On iOS: synchronous image decoding in cellForRowAt. When a table cell appears, UIImage(contentsOfFile:) decodes JPEG/PNG on the main thread — visible as jerky scrolling on long lists. Solution: UIImage.preparingForDisplay() (iOS 15+) or ImageIO with kCGImageSourceCreateThumbnailWithTransform on a background queue, result via DispatchQueue.main.async.

On Android: RecyclerView.Adapter.onBindViewHolder with synchronous operations. Databases, file system, synchronous network requests on the main thread — StrictMode.ThreadPolicy with detectAll().penaltyLog() in debug builds will show all violations.

On Flutter: build() method is called frequently; it must be cheap. setState() on a top-level widget rebuilds the entire tree. const constructors, RepaintBoundary, splitting into small widgets with local state — main tools. Flutter DevTools → Performance shows janky frames (red) with causes.

Compose profiling: Recomposition Highlighter and tracing via Trace.beginSection in @Composable. Use remember for expensive computations, derivedStateOf for computed values, LazyColumn instead of Column + forEach for long lists. Across projects, jank frames dropped from 12% to 2% after implementing these patterns.

Battery: Wake Locks, WorkManager, Network Requests

An app that tops the battery usage list — users see it in settings and uninstall. Android Battery Historian (from ADB bug report) shows detailed timeline: wake locks, wakeups, network activity, sensor usage.

Main energy consumers:

  • Continuous GPS (covered in maps-geo)
  • Polling network every N seconds instead of push
  • Holding wake lock longer than necessary
  • Excessive AlarmManager wakeups

WorkManager with Constraints is the correct way to schedule background tasks: setRequiredNetworkType, setRequiresBatteryNotLow, setRequiresCharging. The OS batches tasks and executes them at convenient times.

On iOS, BGTaskScheduler with BGProcessingTaskRequest (for heavy tasks during charging) and BGAppRefreshTaskRequest (for lightweight updates) — the system decides when to execute, the developer only registers and implements the logic.

Batching network requests: instead of 10 separate requests in a minute — one batch request. Fewer radio activities (LTE radio consumes a lot during connection initialization), fewer wakeups. This typically cuts battery usage by 30% in network-heavy apps.

How We Optimize Your Mobile App Performance: Step by Step

Optimization Process

  1. Measure – Profile cold start, memory, FPS, battery using the tools above. Obtain baseline numbers (e.g., cold start 3.2s, memory footprint 180 MB, 12% jank frames).
  2. Analyze – Identify top 3 bottlenecks by impact. For a typical e‑commerce app, image loading and SDK init are priority.
  3. Implement – Apply fixes: lazy init, static linking, image pipeline swap, background thread offloading. We deliver code changes with diff reports.
  4. Test – Profile again; compare before/after numbers. Validate on real devices (including low-end).
  5. Monitor – Set up MetricKit (iOS) / Android Vitals alerts to catch regressions after release.

Deliverables:

  • Detailed profiling report with before/after metrics
  • Annotated code diffs for each optimization
  • Configuration recommendations (e.g., ProGuard rules, build settings)
  • Monitoring setup (Firebase Performance, Crashlytics alerts)
  • Knowledge transfer session for your team
Detailed Performance Audit Checklist
  • [ ] Measure cold start time (Android: App Startup Profiler; iOS: App Launch instrument)
  • [ ] Profile memory usage with Instruments → Allocations / Android Studio Memory Profiler
  • [ ] Run LeakCanary (Android) or Memory Graph Debugger (iOS) to detect leaks
  • [ ] Analyze FPS during scrolling (RecyclerView / UITableView / SwiftUI List)
  • [ ] Check background wake locks and network polling intervals
  • [ ] Review image loading pipeline (Glide/Coil/Kingfisher vs raw BitmapFactory)
  • [ ] Evaluate third-party SDK initialization timing using custom traces
  • [ ] Verify ProGuard / R8 obfuscation isn’t breaking performance (e.g., reflection)
  • [ ] Test on a representative low-end device (e.g., Samsung Galaxy A21, iPhone SE)

Estimated Timeline

Scope Duration
Performance audit (existing app) 3–5 working days
Optimizations (tier 1 – low‑hanging fruit) 1–2 weeks
Full optimization campaign (including architecture changes) 2–8 weeks

Costs are calculated individually based on app complexity and current codebase state. Contact us for a project estimate and performance review.

Profiling Tools Reference

Platform Tool What It Shows
iOS Xcode Instruments (Time Profiler) CPU, call stack, hot methods
iOS Allocations Live objects, memory peaks
iOS Leaks Retention cycles
iOS MetricKit Production metrics (crash rate, hang rate, launch time)
Android Android Profiler CPU, Memory, Network, Energy
Android Systrace / Perfetto System-level traces
Android LeakCanary Memory leaks
Android Battery Historian Energy consumption
Flutter Flutter DevTools Recomposition, frame rendering, memory
Flutter Dart Observatory Dart VM profiling

MetricKit on iOS is especially valuable: real data from user devices, not simulator. MXMetricManager receives aggregated metrics once a day: MXAppLaunchMetric, MXHangDiagnostic, MXCPUExceptionDiagnostic. Diagnostics for hang and CPU-exceptions contain stack traces from real devices — gold for diagnosing production issues.

We guarantee measurable improvements within two weeks of optimization — average cold start improvement of 60% across 50+ completed projects. Get in touch for a tailored performance review.