Skip to content
Kavin MohanadasStart a projectTalk to Kavin AIKavin AI
All work3D · 02 of 10

Prototype · public demo, still in development

AeroElite Configurator

3D steering-wheel configurator prototype, still in development.

Role
Designer & developer
AeroElite configurator: 3D BMW M steering wheel viewer with the build options panel and CAD price total

In short

  1. 01

    Two wheels (BMW M G8x, Audi B9) share 11 option groups priced in CAD

  2. 02

    One GLSL grip shader zones the single-mesh rim by angle around the hub

  3. 03

    ~592k-triangle GLB with a ~150k lite variant; builds share via ?build= link

Built with Next.js 16 · React 19 · TypeScript · React Three Fiber · three.js · @react-three/drei · camera-controls · three-mesh-bvh · zustand · Tailwind CSS 4 · GLSL · Blender · glTF-Transform

Problem

The AeroElite configurator lets a user build a custom BMW M (G8x) or Audi B9 steering wheel, see the CAD price update as options change, and save or share the result. The catalogue is modelled on a full aftermarket configurator, from side grips and carbon finish through paddles, an LED display and two priced add-ons.

My constraint for the BMW wheel was to use the third-party BMW M steering-wheel geometry exactly as authored in the source .blend and only swap materials on its existing meshes.

Approach

The BMW model is exported from Blender with modifiers applied and compressed with glTF-Transform (dedup, prune, WebP, quantize, meshopt): a full asset at subdivision level 2 (~592k triangles) and a lite one at level 1 (~150k) used below 820 px or on devices with four or fewer cores.

Blender object names map to parts in src/config/parts.ts; an entry matches a mesh by its own name or any ancestor's, so one name covers a multi-material group, and AFFECTED_MATERIALS limits which original material slots a part may replace. src/config/wheels.ts is a registry: the Audi B9 wheel, generated by a Blender script, reuses the same options with its own mesh map, grip zones, badge position and tiling. A ?debug=true panel shows FPS, draw calls and the scene tree and lets me override the mesh mapping at runtime.

Architecture

URL (?wheel ?build ?model ?debug)
  │
  ▼
Configurator ──────────► zustand store: build, price, section, camera, loading
  ├─ ConfigPanel ── setOption ─┘        │
  ├─ CartSheet ── encodeBuild → JSON download / share link
  ├─ LoadingOverlay ◄── byte-level download progress
  └─ Scene (React Three Fiber canvas)
       ├─ Studio: Lightformer IBL, key and rim lights, ACES tone mapping
       ├─ Wheel: useModelFile → useGLTF → prepareModel → applyBuild
       ├─ Addons: addons.glb (four paddle styles, LED housing)
       └─ CameraRig: camera-controls presets, section poses, idle sway

prepareModel clones the scene, gives each mesh its own material instance, fits the orientation, splits the paddle object and centres the root. applyBuild assigns materials per part: one GripMaterial for the grip ring, a cached MeshPhysicalMaterial per carbon finish for the spoke trim, a leather or Alcantara cover for the airbag, a StitchMaterial for the stitch meshes, and a canvas-drawn DecalGeometry badge raycast onto the 6 o'clock spoke.

Technical decisions

  • One shader for the whole rim → the grip ring is a single mesh, so GripMaterial selects side grips, top/bottom and the centre stripe by angle around the hub instead of cutting geometry.
  • PCA orientation fit at load → the file bakes a ~22° steering-column tilt into the vertices, so a plane fitted through the ring rotates the wheel to face +Z.
  • Split the paddle object along world X → both blades are one object in the source; triangles are regrouped, not altered, so each side maps separately.
  • Per-mesh material instances → highlights and swaps never bleed between parts.
  • Reuse the model's own leather normal map → it was authored for this UV layout; the file's roughness texture reads too glossy raw, so a tiled roughness map replaces it.
  • Azimuth clamped to ±150° → rear three-quarter views reach the paddles; full spins are not allowed.

Challenges

  • The stitching came from a Blender node group driven by a mask atlas, which does not survive export. StitchMaterial rebuilds it from the mask's green channel and a normal map derived from the atlas height, with the BMW M tricolour split by angle.
  • Texture tiling was measured against the model's UV islands with a checker texture (one UV unit ≈ 220 mm around the tube) to reach a ~3.5 mm perforation pitch.
  • Loader SVG tick coordinates are rounded so server and client render identical attribute strings.

Results

The prototype is still in development. It is publicly accessible at aero-elite-configurator.replit.app with both wheels, the "Ignition sequence" loader and 11 option sections; the default BMW build totals $849 CAD. The cart sheet collects vehicle model, year and comments, downloads the configuration as JSON and copies a ?build= link that restores the build on load. The app is about 3.5k lines of TypeScript, and nothing is sold through it.

Kavin AI — an AI version of Kavin

Ask about AeroElite Configurator.

Kavin AI, the AI version of me, answers from the same approved write-up you just read.

Next project 03 / 10 · Web

Court Finder (6ixCourts)

Live · 6ixcourts.com

726 Toronto basketball courts on one dark clustered map.