This guide explains how to use the exported props from RetroVisionCabsConverter in your VisionOS app.
When you export a prop for VisionOS, you get:
- A USDZ 3D model optimized for RealityKit
- Video files converted to HEVC (H.265) for Apple Silicon
- Audio files in original format
- Interactive Swift code with gesture support
- A JSON configuration file
- Integration documentation
All exported props include full interaction support following Apple's Human Interface Guidelines for visionOS:
| Gesture | Action | Description |
|---|---|---|
| Drag | Move | Drag the prop freely in 3D space |
| Two-finger Rotate | Spin | Rotate the prop around its center |
| Pinch | Scale | Resize from 10% to 500% of original |
| Look (Gaze) | Highlight | Subtle hover effect when looking at prop |
| Tap | Select | Select the prop for focused interaction |
- Full VoiceOver support with descriptive labels
- Accessibility traits for assistive technologies
- Proper focus management
/Output/VisionOS_Props/
└── discostage/
├── Assets/
│ ├── Models/
│ │ └── discostage.usdz # 3D model for RealityKit
│ ├── Video/
│ │ └── disco.mp4 # HEVC video (VisionOS optimized)
│ ├── Textures/
│ │ └── cutout2.png # Texture files
│ └── Audio/
│ └── (audio files if any)
├── prop_config.json # Configuration metadata
├── PropDiscostage.swift # Ready-to-use Swift code
└── README.md # Prop-specific documentation
- Open your VisionOS project in Xcode
- Right-click on your project in the navigator
- Select Add Files to "YourProject"...
- Navigate to the exported prop folder
- Select the
Assetsfolder and the.swiftfile - Check Copy items if needed
- Check your app target
- Click Add
Ensure your project links these frameworks:
- RealityKit
- AVFoundation (for video/audio)
- SwiftUI
Each exported prop includes both interactive and static views:
import SwiftUI
import RealityKit
struct ContentView: View {
var body: some View {
// Interactive - users can drag, rotate, and scale
PropDiscostageView()
}
}For props that shouldn't be moved by users:
struct ContentView: View {
var body: some View {
// Static - no interaction, display only
PropDiscostageStaticView()
}
}Add a floating control panel for reset/play buttons:
struct ContentView: View {
@StateObject private var prop = PropDiscostage()
var body: some View {
RealityView { content in
if let entity = prop.entity {
content.add(entity)
}
}
.task { await prop.load() }
.ornament(attachmentAnchor: .scene(.bottom)) {
PropDiscostageControls(prop: prop)
}
}
}import SwiftUI
import RealityKit
struct ArcadeRoomView: View {
@StateObject private var discostage = PropDiscostage()
@StateObject private var drwho = PropCutoutDrwho()
var body: some View {
RealityView { content in
// Load props
await discostage.load()
await drwho.load()
// Position props
if let entity = discostage.entity {
entity.position = [0, 0, -2]
content.add(entity)
}
if let entity = drwho.entity {
entity.position = [-1.5, 0, -2]
content.add(entity)
}
// Start videos
discostage.playVideo()
drwho.playVideo()
}
// Add gestures for all props
.gesture(DragGesture().targetedToAnyEntity().onChanged { value in
// Drag handling is built into each prop
})
.onDisappear {
discostage.cleanup()
drwho.cleanup()
}
}
}// Move prop with animation
prop.moveTo(SIMD3<Float>(0, 1, -2), animated: true)
// Make prop face a point
prop.lookAt(SIMD3<Float>(0, 0, 0))
// Reset to original transform
prop.resetTransform()
// Scale programmatically (0.1 to 5.0)
prop.handleScale(1.5) // 150% sizeVideos are configured to loop automatically. The generated code handles this.
// Play
discostage.playVideo()
// Pause
discostage.pauseVideo()
// Toggle
discostage.toggleVideo()The generated code attempts to apply the video to the appropriate mesh. If you need to customize:
// After loading
if let entity = prop.entity {
entity.visit { entity in
if let modelEntity = entity as? ModelEntity,
modelEntity.name.contains("screen") {
// Apply custom video material
modelEntity.components[VideoPlayerComponent.self] =
VideoPlayerComponent(avPlayer: prop.videoPlayer!)
}
}
}// Play all audio files
prop.playAudio()
// Stop all audio
prop.stopAudio()The prop_config.json file contains all metadata:
{
"id": "discostage_12345",
"name": "Disco Stage",
"type": "stage",
"placement": "floor",
"model": {
"file": "discostage.usdz",
"scale": 1.0
},
"video": {
"file": "disco.mp4",
"width": 1920,
"height": 1080,
"loop": true,
"autoplay": true,
"meshTarget": "screen"
},
"interaction": {
"blockers": ["blocker1"],
"triggers": []
},
"dimensions": {
"width": 2.5,
"height": 1.5,
"depth": 1.0
}
}If you want to load props based on the JSON config:
import Foundation
import RealityKit
class DynamicPropLoader {
func loadProp(from configURL: URL) async throws -> Entity {
let data = try Data(contentsOf: configURL)
let config = try JSONDecoder().decode(VisionOSPropConfig.self, from: data)
// Get model path
let modelURL = configURL.deletingLastPathComponent()
.appendingPathComponent("Assets/Models/\(config.model?.file ?? "")")
let entity = try await Entity(contentsOf: modelURL)
// Apply scale
if let scale = config.model?.scale {
entity.scale = SIMD3(repeating: scale)
}
return entity
}
}Use the placement field to position props appropriately:
| Placement | Description | Example Position |
|---|---|---|
floor |
Ground level | y = 0 |
wall |
Mounted on wall | Against wall, y = 1.5 |
ceiling |
Hanging | y = 2.5 |
corner |
In corner | Near room corner |
center |
Room center | x = 0, z = 0 |
table |
On surface | y = 0.8 (table height) |
Blocker meshes prevent the player from walking through the prop:
// The prop entity includes collision shapes for blockers
entity.visit { entity in
if entity.name.contains("blocker") {
entity.components[CollisionComponent.self] = CollisionComponent(
shapes: [.generateConvex(from: entity as! ModelEntity)]
)
}
}Trigger meshes detect when the player enters an area:
// Add a trigger volume
if let trigger = entity.findEntity(named: "trigger") as? ModelEntity {
trigger.components[TriggerComponent.self] = TriggerComponent(
onEnter: {
print("Player entered trigger zone")
}
)
}- Best compression for file size
- Hardware-accelerated on Apple Silicon
- Uses
hvc1tag for Apple compatibility
- Widely compatible
- Larger files than HEVC
- Good for older hardware
- Minimal compression artifacts
- Very large files
- Best for editing or archival
- Ensure the video file is in the app bundle
- Check the file extension matches
- Verify video codec is supported (HEVC or H.264)
- Verify .usdz file is in the bundle
- Check file permissions
- Ensure model path is correct
- Check AVAudioSession category
- Verify audio file format
- Ensure volume is not muted
- Use HEVC video for smaller files
- Keep polygon counts reasonable
- Use texture atlases when possible
- Preload props before showing
import SwiftUI
@main
struct ArcadeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
ImmersiveSpace(id: "ArcadeRoom") {
ArcadeRoomView()
}
}
}
struct ContentView: View {
@Environment(\.openImmersiveSpace) var openImmersiveSpace
var body: some View {
Button("Enter Arcade") {
Task {
await openImmersiveSpace(id: "ArcadeRoom")
}
}
}
}For issues with prop conversion or export:
- Check the conversion logs in the app
- Verify source files are valid
- Ensure Blender is installed at
/Applications/Blender.app - Ensure FFmpeg is installed for video conversion
Generated by RetroVisionCabsConverter