Skip to main content

3D View

Render an activity's animation motion data as an interactive 3D view — the same skeletal playback used in the Model Health web app, embeddable directly in your own app.

Not available in the Python SDK

3D View is a Swift/TypeScript-only feature. The Python SDK can still fetch the underlying animation motion data, but there's no bundled viewer for it.

Installation

If you've already added the Model Health Swift package (see Installation), add the ModelHealthUI product alongside ModelHealth to your target — it's a separate product so apps that don't need the 3D view don't pay for WebKit/SwiftUI.

3D View screen in the iOS demo app, showing a reconstruction of a subject performing an activity, with playback controls

Loading Animation Data

Swift loads the data automatically when you construct View3DController with an activity — see Rendering the Viewer below.

Rendering the Viewer

import SwiftUI
import ModelHealthUI

struct ActivityViewer: View {
@StateObject private var controller: View3DController

init(activity: Activity, client: ModelHealthClient) {
_controller = StateObject(wrappedValue: View3DController(for: activity, using: client))
}

var body: some View {
View3D(controller: controller)
}
}

Constructing View3DController(for:using:) immediately fetches and loads the activity's animation data — no separate load step. Check isLoadingTransforms to show a loading state, and call reload() to retry after a lastError.

Playback Controls

View3D renders no built-in playback UI on either platform — you drive playback yourself, so you can style controls to match your app rather than a fixed embedded toolbar.

  • SwiftView3DController exposes play(), pause(), seek(to:), step(_:), setPlaybackSpeed(_:), plus published isReady/currentTime/duration/isPlaying you can bind directly to SwiftUI controls (sliders, buttons).

  • TypeScriptView3D's ref exposes the same operations (play, pause, seek, step, setPlaybackSpeed) plus currentTime/duration/isPlaying/playbackSpeed getters. The package also exports a ready-made <PlaybackControls> component if you don't want to build your own:

    import { PlaybackControls } from '@modelhealth/viewer-react';

    <PlaybackControls
    currentTime={viewerRef.current?.currentTime ?? 0}
    duration={viewerRef.current?.duration ?? 0}
    playing={playing}
    playbackSpeed={playbackSpeed}
    onTimeChange={(time) => viewerRef.current?.seek(time)}
    onPlayingChange={(next) => (next ? viewerRef.current?.play() : viewerRef.current?.pause())}
    onPlaybackSpeedChange={setPlaybackSpeed}
    onStep={(direction) => viewerRef.current?.step(direction)}
    />

Ground Reaction Force Overlays

View3D can render ground reaction force (GRF) vector overlays on top of the animation. This isn't backed by a dedicated SDK motion data type: you upload your own externally measured GRF data — from a force plate, instrumented treadmill, or similar — in whatever format you have it (CSV, JSON, plain text, etc), tagged with a name of your choosing. The backend syncs it to the trial and produces a .sto-formatted result tagged <tag>-sync. That synced .sto result is what actually gets fetched and parsed for the overlay, regardless of the format you originally uploaded.

Synchronization has to be enabled for your data

Syncing external GRF data to the trial requires support for your specific data format and capture setup in our core algorithms. It is not enabled by default. Contact us at support@modelhealth.io if you'd like to use overlays with your data — without that support the upload will succeed but no -sync result is produced, and the overlay stays empty.

3D View in the iOS demo app, showing blue and green ground reaction force vectors overlaid on the skeleton

The full flow:

  1. Initialize the client.
  2. Upload your GRF data via addMotionData, tagged with a name of your choosing (e.g. "my-grf").
  3. Set up the viewer with that same tag — it fetches the synced .sto result automatically.
// 1. Initialize the client
let client = try ModelHealthClient(apiKey: "your-api-key-here")

// 2. Upload your GRF data, tagged with a name of your choosing — any format works
// (CSV shown here); the backend syncs it and produces the .sto result the viewer reads
let grfData = try Data(contentsOf: grfFileURL)
let file = ExternalResultFile(tag: "my-grf", fileExtension: "csv", data: grfData)
let updatedActivity = try await client.addMotionData([file], to: activity)

// 3. Construct the controller with the updated activity and the same tag — it fetches the
// synced .sto result (tagged "my-grf-sync") automatically, alongside the animation data
let controller = View3DController(for: updatedActivity, using: client, externalDataTag: "my-grf")

If you don't know the tag ahead of time (e.g. it was uploaded by a different part of your system), look for a Result on the activity whose tag ends in -sync and whose file extension is .sto, and strip the -sync suffix.

Troubleshooting

  • No animation data available — the activity hasn't finished processing yet. Wait for .analysing or .ready status (see Activity Recording) first. Surfaced via lastError (Swift) or a thrown error from fetchAnimationTransforms (TypeScript).
  • Swift: viewer fails to loadView3DController.lastError also surfaces WebView-level load failures (e.g. a malformed bundled resource). Check it if isReady never becomes true.
  • Overlay not appearing — fetching the overlay fails silently rather than throwing if the tagged file doesn't exist or hasn't been synced yet. Confirm the tag you passed matches what the file was actually uploaded under, and that a -sync result exists for it. If no -sync result ever appears, synchronization likely isn't enabled for your GRF data — contact support@modelhealth.io.

Next Steps

3D View picks up where Activity Recording leaves off — once an activity is .analysing or .ready, its animation data is available to visualize; there's no need to wait for analysis to complete.