← Alle Artikel

Technik i.dentity

Augmented Reality ohne App: Wie wir 3D-Modelle auf iOS und Android zum Sprechen bringen

Ein sprechender Baum, der im Wald auftaucht – auf iPhone und Android, ohne App, aus einer einzigen Datei. Ein technischer Blick hinter die Kulissen unserer Web-AR.

Vom Drahtgittermodell in Blender bis zum sprechenden Baum, der per Kamera im Wald auftaucht – dasselbe 3D-Modell auf iOS und Android.

Bei i.appear wird im Hintergrund ständig an unserem technischen Erlebnis gefeilt. Wir bauen unsere Anwendungen oft auf unkonventionelle Weise – denn wie das bei Technik so ist: Theoretisch ist fast alles möglich, finanziell aber nicht.

Als kleines Unternehmen ist es deshalb essenziell, Device-Pipelines zu bauen, die kostengünstig und effizient sind, aber trotzdem ohne künstliche Limits für beliebige Endgeräte funktionieren. Um die Einstiegshürde so niedrig wie möglich zu halten, haben wir i.appear vor einem halben Jahr von einer App aus dem App Store zu einer Progressive Web App (PWA) umgebaut.

Die vorherige Anwendung war in Unity umgesetzt, was eine nahtlose Implementierung AR-spezifischer Inhalte direkt in der App erlaubte. Die neue Web-App war anfänglich mit dem Google Model-Viewer für 3D-Modelle und AR geplant und getestet. Allerdings stießen wir relativ schnell an die Grenzen des Viewers: Er erlaubt in AR nur einfache Animationen. Für komplexe AR-Anwendungen mit Audio oder Interaktion ist er nicht ausgelegt. Und da AR den Kern unserer App ausmacht, mussten wir eine neue Lösung erarbeiten.

Konkret ging es bei der ersten Kundenanfrage um die Visualisierung eines 3D-Modells mit audio-synchronisierter Animation. Über den Google-Viewer funktionierte das nur auf Android – mit Animation, aber ohne Audio. Auf iOS wurde nicht einmal die Animation abgespielt. Um zu verstehen, warum, mussten wir zuerst erkennen, dass die Animations-Technik selbst eine Rolle für Apples Quick-Look-Player spielt: Das Modell war ursprünglich in Blender mit Shape Keys animiert – und genau das blockiert Apple. Also wurde das Modell neu geriggt und mit einer Bones-Animation versehen, die der Quick-Look-Player akzeptiert.

Da unsere AR-Projekte aber deutlich tiefer und komplexer gehen, brauchte es ohnehin eine Alternative als neuen AR-Player. Nach umfassender Recherche kam Needle Engine ins Spiel. Needle veröffentlichte 2025 eine Beta seines Blender-Plugins, das 2026 in der stabilen Version 1.2.0 erschien. Das Plugin bringt eine mächtige Ausstattung an Komponenten mit, die es über sogenannte „Everywhere Actions“ und den USDZ-Exporter ermöglichen, ein fixes GLB-Paket zu exportieren. So weit die Theorie! Aber wie das oft so ist, wenn man einen theoretischen Plan hat, kommt einem schnell die Plattform-Realität in die Quere.

Hat Apple etwas gegen AR?

Die Ausgangslage war von einer Plattform-Asymmetrie geprägt: Ein einziges glTF-Binärmodell (GLB) sollte auf zwei technisch verschiedenen Wegen ausgespielt werden – unter iOS über Apples Quick Look, das ein USDZ-Derivat rendert, und unter Android über WebXR, das die Needle Engine direkt im Browser ausführt. Beide Wege mussten Geometrie, Skelett-Animation, Tonspur und die Interaktivität – Starten, Stoppen und erneutes Auslösen von Stimme und Bewegung per Tap – aus derselben Quelldatei beziehen. Während sich Animations- und Audiowiedergabe früh beherrschen ließen, erwies sich die Interaktionssteuerung unter Quick Look als Engpass, da Apples Laufzeit gegenüber WebXR einen stark eingeschränkten und nur teilweise dokumentierten Funktionsumfang besitzt. Das Verhalten zerfiel in zwei unabhängige Teilprobleme. Das erste betraf die Audiowiedergabe: Bei mehrfachem Antippen überlagerten sich auf dem iPhone mehrere Instanzen derselben Tonspur mit zeitlichem Versatz – ein Verhalten, das ausschließlich unter Quick Look auftrat, nicht unter WebXR. Das zweite betraf die Animation: Es war offen, ob sich eine laufende Bewegung per Interaktion aktiv anhalten lässt oder ob Quick Look nur das passive Auslaufenlassen zulässt. Beide wurden getrennt behandelt, da sie unterschiedliche Komponenten ansprechen – Audioquelle und Auslöser einerseits, den Animator mit seinem Zustandsautomaten andererseits.

Zur Eingrenzung der Audio-Überlagerung wurde das eigene Setup einem offiziellen Needle-Referenzbeispiel gegenübergestellt, das den Fehler nicht zeigte. Daraus ergaben sich drei Ursachenfelder: die Herkunft der GLB-Datei (offizielle Needle-Build-Pipeline gegenüber einem Export aus Blender 4.2.3 LTS mit dem Needle-Exporter-Plugin v1.2.0 nebst nachgelagertem Patch-Skript), die Bezugsquelle der Engine sowie die Art der Einbindung. Das zweite Feld stand lange als plausible Erklärung im Raum: Die Engine lässt sich entweder als gebündelte Abhängigkeit in den Build der Web-Anwendung einbinden oder als zur Laufzeit über ein CDN nachgeladenes Skript. In einem früheren Zustand unterschieden sich Referenz und Eigenimplementierung in genau diesem Bezugsweg und potenziell in der Version. Nachdem jedoch beide auf denselben Stand vereinheitlicht worden waren – dieselbe Needle-Engine-Version, fest über ein CDN verankert –, blieb die Überlagerung unverändert. Der Bezugsweg schied damit als Ursache aus; er war nicht die Lösung, sondern lediglich die Beseitigung einer Störvariablen. Übrig blieben als wirksame Unterschiede die Herkunft der Modelldatei und die Form der Einbindung.

Damit war klar, wo die Lösung liegen musste: in der Struktur des Modells selbst und in der Art, wie die Interaktions-Komponenten darin verankert sind. Die folgenden drei Ausschnitte zeigen die entscheidenden Stellen – wie eine Interaktion im GLB aussieht, wie ein Patch-Skript sie reparierte und wie das Frontend das Ganze schließlich ausspielt.

1. Wie eine Everywhere Action im GLB aussieht

So trägt z. B. der rote Start-Knopf (AudioButton) seine Actions – als builtin_components am Node (gekürztes, repräsentatives Beispiel aus dem verifizierten _patched.glb der Iteration W):

// Node "AudioButton" (roter Würfel) → Start / Neustart
"NEEDLE_components": {
  "builtin_components": [
    { "name": "ObjectRaycaster" },                // Pflicht, damit der Tap überhaupt ankommt
    { "name": "PlayAnimationOnClick",
      "animator": { "guid": "<Animator-der-Figur>" },
      "stateName": "FigurArmatureAction" },        // wechselt in den Lauf-State
    { "name": "SetActiveOnClick",
      "target": "/nodes/<AudioAnchor>",
      "targetState": true,                          // aktiviert den Audio-Träger → playOnAwake feuert
      "hideSelf": false,
      "toggleOnClick": false }
    // KEIN PlayAudioOnClick – Audio läuft bewusst über playOnAwake (Iteration W)
  ]
}

Der grüne Stopp-Knopf ist das Gegenstück: PlayAnimationOnClickstateName: "Idle" und SetActiveOnClicktargetState: false.

2. Wie das Patch-Skript die Actions funktionsfähig macht

Das ist das eigentlich Spannende: Der Blender-Exporter schreibt die Actions zwar raus, aber mit kaputten oder fehlenden Referenzen. Unser Skript zieht das nach. Zwei Kernstellen aus scripts/patch_needle_glb.py:

a) PlayAnimationOnClick mit dem richtigen Animator verbinden – auch über Objektgrenzen hinweg (Knopf triggert Animation auf der Figur):

# Fix 1, Fall B: Node hat PlayAnimationOnClick, aber keinen eigenen Animator
# → Cross-Object-Referenz auf den animator-haltigen Node der Szene setzen.
#   (Der Blender-Exporter erzeugt sonst eine zufällige GUID, die ins Nichts zeigt.)
if paoc_list and scene_animator_guid:
    for paoc in paoc_list:
        old = paoc.get("animator", {}).get("guid")
        if old != scene_animator_guid:
            paoc["animator"] = {"guid": scene_animator_guid}
            fixes += 1
            print(f"  [Node {node_index}] PAOC.animator.guid {old} → "
                  f"{scene_animator_guid} (cross-ref to Node {scene_animator_node})")

b) PlayAudioOnClick den fehlenden Audio-Clip beibringen (Plugin v1.2 schreibt ihn nicht mehr):

# Plugin v1.2 exportiert PlayAudioOnClick OHNE audio-clip-String.
# Steht auf demselben Node eine AudioSource mit clip, übernehmen wir den Wert.
for c in comps:
    if c.get("name") != "PlayAudioOnClick":
        continue
    if not c.get("clip") and audio_clip:
        c["clip"] = audio_clip
        print(f"  [PAUOC] clip ← '{audio_clip}' (from AudioSource)")
    if "target" not in c:
        c["target"] = None

3. Wie das GLB im Frontend eingebunden wird

Im Frontend wird das <needle-engine>-Element zur Laufzeit erzeugt, und WebXR (für Android-AR) wird nur außerhalb von iOS hinzugefügt (vereinfacht):

onStart(context => {
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);

  if (!isIOS) {
    // Android: AR-Button über WebXR. Auf iOS NICHT – dort übernimmt Quick Look (USDZ).
    context.scene.addComponent(WebXR, {
      createVRButton: false,
      createARButton: true,
      createQRCode: false,
      createSendToQuestButton: false,
    });
  }
  // USDZExporter kommt aus dem GLB (nicht programmatisch) → keine Doppelung.
});
const el = document.createElement("needle-engine");
el.setAttribute("src", src!);              // GLB-URL aus dem CMS
el.setAttribute("camera-controls", "");
if (autoplay) el.setAttribute("autoplay", "");
el.setAttribute("background-color", "transparent");
container.appendChild(el);

Parallel wurde eine Reihe naheliegender Ansätze erprobt, von denen sich mehrere als Sackgassen erwiesen. Eine einzelne Schaltfläche als Umschalter (toggleOnClick) scheiterte reproduzierbar an einer Grenze der Quick-Look-Laufzeit. Der alleinige Verzicht auf die Audio-Endlosschleife blieb ohne Wirkung. Das Deaktivieren oder Ausblenden des tönenden Objekts beim Stopp führte nicht zum Ziel, da die Animation hinter dem ausgeblendeten Objekt weiterlief. Und der Versuch, eine laufende Animation durch Wechsel in einen Ruhezustand anzuhalten, blieb wirkungslos, solange dieser Zustand selbst keine eigene Bewegungsinformation trug.

Dieser letzte Befund erwies sich als entscheidend. Der Zustandswechsel über den Animator scheiterte nicht grundsätzlich, sondern daran, dass der Zielzustand leer war und somit nichts abzuspielen hatte. Sobald dem Ruhezustand eine reale, wenn auch statische Ruhepose als eigene Bewegung zugewiesen wurde, ließ sich eine laufende Animation per Tap aktiv anhalten – das zweite Teilproblem war damit gelöst. Für die Audiowiedergabe wurde ein analog struktureller Weg gewählt: Statt die Tonspur über eine klick-gebundene Wiedergabe-Aktion (PlayAudioOnClick) zu starten, die bei jedem Auslösen eine zusätzliche Instanz erzeugen konnte, wurde sie über playOnAwake an die Aktivierung ihres Trägerobjekts gekoppelt. Da dessen Aktivierung pro Auslösung genau einmal erfolgt, ergibt sich genau ein Audiostart, während die Deaktivierung die Wiedergabe beendet. So ließen sich beide Teilprobleme in einem einzigen Mechanismus vereinen, der wiederholtes, sauberes Starten, Stoppen und Neustarten von Stimme und Bewegung erlaubt, ohne dass sich mehrere Tonspuren überlagern.

Der verifizierte Workflow: vom 3D-Modell zum AR-Erlebnis

Den Ausgangspunkt der Pipeline bildet Blender 4.2.3 LTS, das über eine Model-Context-Protocol-Schnittstelle (MCP) ferngesteuert werden kann. Dabei läuft im Blender-Prozess ein MCP-Add-on als Socket-Server, an den sich der externe Client verbindet; über diese Verbindung lassen sich Szeneninformationen auslesen und Python-Befehle direkt in den Blender-Interpreter einspeisen, ohne dass die grafische Oberfläche manuell bedient werden muss. Auf diese Weise werden Mesh und Armature aufgebaut, wobei die Verformung ausschließlich über Bones und Vertex-Gruppen erfolgt – Shape Keys bzw. Morph Targets scheiden aus, da Apples Quick Look sie ignoriert. Das Mesh wird als Kind der Armature geführt, die Bewegung als Action am Armature-Objekt hinterlegt, und es wird mit einem einzigen Material gearbeitet, da Mehr-Material-Setups in dieser Pipeline ungeprüft sind.

Die Interaktionslogik wird anschließend bereits in Blender modelliert, nicht erst in der Web-Anwendung. Über das Needle-Engine-Exporter-Plugin (Version 1.2.0) wird ein AnimatorController als NodeTree angelegt, der je einen Zustand pro Animation sowie einen Ruhezustand enthält; der Ruhezustand wird als Default markiert, damit das Modell beim Laden nicht sofort losläuft, sondern auf eine Interaktion wartet. Auf das Mesh werden die Komponenten Animator, ObjectRaycaster, PlayAnimationOnClick und die audio-bezogenen Auslöser gelegt, auf die Armature-Wurzel der USDZExporter, der WebARSessionRoot sowie eine zunächst deaktivierte WebXR-Komponente. Entscheidend ist eine leicht zu übersehende Szenen-Einstellung: Nur wenn NEEDLE_gltf_exporter_settings.useNeedleEngine auf True steht, werden diese Komponenten überhaupt in die Exportdatei geschrieben – der Standardwert ist False.

Exportiert wird über den Standard-glTF-Exporter als GLB, mit aktivierter Animations- und Skin-Ausgabe und mitgeschriebenen Extras. Da der Exporter der Plugin-Version 1.2.0 mehrere für die Laufzeit nötige Strukturen unvollständig oder gar nicht ausgibt – insbesondere fehlt der AnimatorController, und Referenzen wie die Animator-GUID einer Klick-Aktion oder der Audio-Clip-Name werden nicht korrekt aufgelöst –, durchläuft das exportierte GLB anschließend ein eigens entwickeltes Patch-Skript. Dieses Skript liest den JSON-Teil des GLB, synthetisiert bei Bedarf einen vollständigen AnimatorController, verdrahtet die Klick-Aktionen mit dem korrekten Animator – auch über Objektgrenzen hinweg –, leitet fehlende Audio-Clip-Namen aus der zugehörigen Audioquelle ab und schreibt die Datei binär wieder zurück. Dieser Schritt ist für iOS Quick Look zwingend; der Roh-Export genügt allein unter Android über WebXR, womit der Patch-Lauf der einzige plattformrelevante Unterschied der gesamten Pipeline ist.

Das gepatchte GLB wird gemeinsam mit der Tonspur, die physisch unmittelbar neben der Modelldatei liegen muss, im Verzeichnis der Web-Anwendung abgelegt und eingecheckt. Von dort baut der Hosting-Dienst die Seite automatisch innerhalb weniger Minuten neu und liefert sie aus. Die Web-Anwendung bindet das Modell nicht über ein gebündeltes Paket ein, sondern lädt die Needle Engine zur Laufzeit über ein CDN nach und erzeugt im Browser dynamisch ein <needle-engine>-Element mit der GLB-URL als Quelle. Beim Initialisieren wird die Plattform anhand des User-Agents unterschieden: Außerhalb von iOS wird programmatisch eine WebXR-Komponente mit AR-Schaltfläche hinzugefügt, während unter iOS die Verantwortung an Quick Look übergeht, das aus dem GLB ein USDZ ableitet. In Produktion tritt an die Stelle der Testseite der Weg über ein Content-Management-System: Das Modell wird dort als Asset hinterlegt und auf der jeweiligen Seite referenziert, was dieselbe Render-Kette speist.

Am Ende der Kette steht das Endgerät. Die ausgelieferte Seite wird unter iOS in Safari, unter Android in Chrome aufgerufen; über die AR-Schaltfläche wird das Modell in den Raum projiziert und reagiert dort auf Antippen mit dem in Blender modellierten und durch das Patch-Skript funktionsfähig gemachten Interaktionsverhalten – dem Starten, Stoppen und sauberen Neustarten von Stimme und Bewegung. Damit schließt sich der Weg von der MCP-gesteuerten Modellierung in Blender über Export, Patch, Deployment und CDN-gestützte Auslieferung bis zur tatsächlichen Darstellung im Augmented-Reality-Modus des Mobilgeräts.

i.dentity – Regionale Identität & Augmented Reality
Alle Features der i.appear-Plattform
Übersicht aller i.appear-Stadtrundgänge

Augmented Reality für den eigenen Ort? Sprich mit uns →

← Alle Artikel