Initialising
Skip to content
AR Developer

Build Your Ride AR

An Android AR car configurator built in Unity: point your phone at the floor and a full-scale 3D car appears, scaled to its real length — a Porsche is genuinely 4.6 m long. The car is parented to a plane-locked ARCore anchor rather than placed in free space, so it holds its physical spot as ARCore revises its map of the room. Real-time light estimation, a contact shadow, and depth-based occlusion on supported phones ground it in the space instead of looking pasted on.

5 cars, real-world scale57 MB APK (was 114 MB)60 fps on Galaxy S23

5 cars at true real-world scale, anchor-locked placement, live light estimation + contact shadow + depth occlusion — 60 fps on a Galaxy S23 at 57 MB (down from a 114 MB first pass).

2024AR DeveloperCompleted

Point your phone at the floor and tap. A full-scale 3D car appears in the room with you — a Porsche is genuinely 4.6 m long — and you can walk around it and change it live: paint, wheels, callipers, spoiler.

What it is

An AR configurator that holds its spot in the room.

Built in Unity with AR Foundation and ARCore. The car isn't dropped where you pointed — it's anchored to the detected floor plane and parented to that anchor, so as ARCore keeps revising its estimate of where the room actually is, the car follows the correction instead of drifting away from it. The first version of this project skipped the anchor. That's why it drifted.

Porsche 911 GT3 RS in the in-editor test harness, with the colour tray open showing Body and Callipers paint groups.
The colour tray, captured in the no-phone-needed editor test harness — see "Under the hood."
The cars

Five cars, one generic pipeline.

Including a literal Batmobile — built-in rear fins, callipers-only paint — sitting next to a Porsche 911 GT3 RS with a toggleable rear wing and full body/wheel/calliper paint.

CarLengthPaintSpoiler
Porsche 911 GT3 RS (992)4.60 mBody · Wheels · CallipersBuilt-in rear wing, toggles
Maserati Quattroporte5.26 mBody · Wheels · CallipersAdd-on
Generic Sport Coupe4.50 mBody · Wheels · CallipersAdd-on
Batmobile4.60 mCallipers onlyBuilt-in rear fins
1936 American Sedan5.00 mDeliberately untouched

The 1936 Sedan renders exactly as its original artist modelled it — a reference point standing next to the modern cars, deliberately left out of the paint pipeline.

How placement works

The anchor is the whole trick.

ARCore constantly revises where it thinks the floor really is. Because the car is a child of the anchor rather than a model dropped in free world space, it inherits those corrections and stays on the same physical spot as you walk around it.

1
Camera sees the room
2
ARCore finds the floor
3
You tap to place
4
Anchor on that plane
5
Car parented to it
Placing it
Tap a plane, get a real anchor

One finger drags to move, two fingers twist to rotate and pinch to resize — all while the anchor stays locked to the plane it was created on.

Measuring drift honestly
A healthy correction looks like a failure — unless you check

divergence = |carMovement − anchorMovement|. Near zero while both move means ARCore is correctly re-estimating and the car is following. A growing gap means the car came loose from its anchor — a parenting bug, not a tracking problem. Without this distinction, a healthy correction reads as broken tracking.

What grounds it in the room

Not pasted on — present.

Light estimation
Lit by the room, not a fixed rig

ARCore measures the room's actual brightness, colour and direction of light and drives the scene light with it — the single biggest reason the car reads as physically present rather than composited on top of the camera feed.

Contact shadow
An invisible surface, doing real work

A quad under the car draws nothing except the shadow that falls on it, blended over the camera feed. Removing it is the fastest way to see how much it's doing — the car looks like a sticker without it.

Occlusion
Real objects can hide the car

On phones with the ARCore Depth API, furniture and walls correctly draw in front of the car instead of the car drawing over everything. Conditional on hardware — see "Honest about it."

Gestures
Drag, twist, pinch

One-finger drag moves the car along the floor, two-finger twist rotates it, and pinch resizes it — all live, with the customization tray staying in sync with whichever car is out.

Batmobile placed in the test harness, callipers-only paint group, built-in rear fins, contact shadow underneath.
The Batmobile — callipers-only paint, built-in rear fins, contact shadow doing the grounding work.
The import pipeline

Adding a car is config, not code.

Clicking through hundreds of material slots by hand doesn't scale to five cars, let alone more. One generic pipeline turns any downloaded FBX into a finished, paintable car.

FBX file
measure it, scale to real length
detect Z-up export, stand it on its wheels
sort meshes into Body / Wheels / Callipers
build a colour palette per group
find the boot, mount a spoiler
save as a prefab

Sorting the meshes is the interesting part. A downloaded model has no idea what a "body" is — it just carries material names like EXT_caliper or TwiXeR_992_gt3rs_carbon_Wing. The mapper matches those against keyword lists, with exclusion rules so taillights and window glass don't get repainted along with the bodywork. Adding a new car is a few lines of config — a filename, a real length in metres, an optional orientation override — not a new importer.

Three bugs worth reading about

The interesting failures, not just the feature list.

Tap-to-place worked on the phone, but drag, twist and pinch did nothing — nothing crashed and nothing appeared in the log, which is what made it so hard to find. A script file had been deleted and recreated during development. Unity identifies scripts by a GUID in a sidecar file, and recreating the file generated a new one — the saved scene still pointed at the old GUID, so the gesture component was an empty "Missing Script" placeholder, present in the scene, doing nothing. Both safety nets had holes: a GetComponent fallback could never work, because a missing-script component isn't the type you're asking for — and the repair tool only ever wired up the component if it found one, it never added it, and its null check swallowed the failure without a word.

LessonA null-guarded "wire it up" step is not a repair. if (x != null) wire(x) does nothing in exactly the case you wrote it for.

Performance

Holding 60 fps next to camera, tracking and depth.

An AR app has to hold frame rate while also running camera capture, plane tracking and depth sensing — every millisecond spent elsewhere is a millisecond stolen from that budget.

Texture compression + streaming
~200 car textures were fully loaded from launch whichever car you picked. Now compressed and streamed on demand — 114 MB → 57 MB.
Deferred saving
Colour taps no longer block on disk writes.
Cached bounds
Hit-testing your finger against the car used to walk 209 meshes on every touch. Now measured once.
Frame-rate independent smoothing
Lerp(a, b, rate * deltaTime) moves at different speeds at 30 and 60 fps. Replaced with an exponential-decay form that behaves identically on both.
Shadow range 150 m → 30 m
A leftover Unity default was rendering cascades across six times more distance than a room-scale app uses.
60 fps
Galaxy S23
57 MB
APK, was 114 MB
When something looks wrong on device

Read the log before guessing.

"I placed a car and see nothing" has four completely different causes that look identical to a user: off-screen, the camera is inside the model, the near plane is clipping it, or it should be visible and something else is wrong. A full run log — every touch, every raycast result, an explicit visibility verdict — usually identifies which one without needing to reproduce it.

Live diagnostics overlay: frame rate, AR session state, environment (light estimation, occlusion), and the current build's colour/spoiler/scale choices.
The built-in diagnostics overlay — frame rate, AR session state, environment, and the current build.
Honest about it

Not solved yet.

Isn't
The car doesn't remember its place in the world

Colours, size and angle survive a restart; the physical spot doesn't, because every AR session starts a fresh origin. Doing it properly needs cloud anchors.

Isn't
Occlusion needs a Depth-capable phone

Without the ARCore Depth API, the car draws over everything in front of it rather than being correctly hidden.

Isn't
Draw calls are high on the detailed cars

209 on the Maserati, 116 on the Porsche. LOD meshes and texture atlasing are the next real win, not yet shipped.

Isn't
Portrait only, in practice

Landscape isn't a supported layout today.

This began as a university 3D Vision project — one car, a plain colour button, and a model that drifted away as you walked. That version's own conclusion named the two things wrong with it: the anchoring drifted, and the car looked pasted onto the screen rather than present in the room. This rebuild targets exactly those two — plane-attached anchoring for the first, real light estimation, contact shadows and depth occlusion for the second.

Under the hood

Skip-able. Everything above stands on its own — this is here for anyone who wants the engine version, the file layout, and how it's tested without a phone.

Coming advancements

Shipped, and still being sharpened.

  • Persistent cloud anchors, so a placed car's physical spot survives closing the app (colours, size and angle already do)
  • LOD meshes + texture atlasing to cut draw calls on the detailed cars (209 on the Maserati, 116 on the Porsche)
  • Occlusion coverage beyond ARCore Depth API-capable phones
  • Landscape support — portrait-only in practice today
Next project
Bionic Arm via Flex Sensor