This repository is a collection of code samples and projects that demonstrate the capabilities of Meta Spatial SDK. Meta Spatial SDK is a new way to build immersive apps for Meta Horizon OS. Meta Spatial SDK lets you combine the rich ecosystem of Android development and the unique capabilities of Meta Quest via accessible APIs.
The samples in this repository showcase various features of the SDK, such as spatial anchors, scene understanding, and object recognition. Each sample project includes source code, build scripts, and documentation to help developers understand how to use the SDK to build their own spatially-aware applications.
Whether you're a seasoned developer or just starting out with Meta Quest/Horizon OS, the Meta Spatial SDK Samples are a valuable resource for learning how to leverage the power of spatial computing in your applications.
To try out these sample apps, you will need:
- A Meta Quest device (Quest 2/3/3S/Pro)
- Mac or Windows
- Android Studio Hedgehog or newer
- Meta Spatial Editor
First, ensure that all of the requirements are met.
Then, to build and run a sample:
- Clone this repository to your computer
- Open the specific sample app with Android Studio
- Plug in your Quest device to your computer
- Click the "Run" button in the Android Studio toolbar, the app will now be running on your headset
Notes:
- All samples, except MRUKSample and PremiumMediaSample, require you to install Meta Spatial Editor.
- MediaPlayerSample and PremiumMediaSample contain examples of custom shaders,
which requires the NDK to be installed and set up in app/build.gradle.kts
(ex.
ndkVersion = "27.0.12077973") - Our samples support our custom OVRMetrics integration
We have 13 sample apps, demonstrating various features of Meta Spatial SDK:
- AnimationsSample shows how to play animation clips, create reusable animation drivers, and demonstrates frame-based procedural animation.
- HybridSample shows how to begin with a standard Android-based 2D panel experience and switch between an immersive experience that hosts the same panel.
- CustomComponentsSample shows how to create a custom component that embodies the data shared across various instances of an application.
- PremiumMediaSample shows how to stream DRM-protected content, play 180-degree videos, and cast reflections from panels into the user's spatial setup with MRUK.
- MediaPlayerSample shows how to build an immersive video playback experience.
- MixedRealitySample shows an immersive experience that interacts with the user's physical surroundings.
- MrukSample shows an immersive experience influenced by the user's physical surroundings.
- Object3DSample shows inserting 3D objects into a scene and adjusting their properties in Meta Spatial Editor.
- PhysicsSample shows adding a physics component and adjusting its properties in Meta Spatial Editor.
- PremiumMediaSample shows a media streaming experience integrated into the users spatial environment.
- SpatialVideoSample shows how to play video with spatialized audio.
- StarterSample is a starter project that is part of Getting Started with Meta Spatial SDK.
- BodyTrackingSample shows how to access body tracking and utilize skeleton joint data.
We also have a starter app CustomComponentsStarter, which only contains the boilerplate code of CustomComponentsSample. You can download this starter app and follow this tutorial to build a LookAt app with Meta Spatial Editor and SDK.
The Showcases folder contains five apps. These are fully-featured applications built with Meta Spatial SDK, and are open-sourced here in this repository.
The documentation for Meta Spatial SDK can be found here.
Find our official release notes here.
-
Entity.removeComponent<T>()andEntity.tryRemoveComponent<T>()for removing components from entities at runtime.// Remove a component (throws if not present) entity.removeComponent<Grabbable>() // Safe removal (returns false if not present) val wasRemoved = entity.tryRemoveComponent<Grabbable>()
-
Visual grab handle rendering for panels with animated hover, grab, and resize state transitions. Audio feedback (grab and drop sounds) for panel grab interactions.
IsdkPanelPaddingRenderSystemexposesgrabbableEdgeMesh,grabbableCornerMesh,resizeCornerMesh,grabAudio, anddropAudiofields for customization. -
Entity.toString()now prints the entity ID and all attached components for easier debugging. OptionalEntityDebugInfo.includeComponentsInToStringflag enables full component value dumps.EntityDebugInfo.captureCreationCallstackrecords creation callstacks and timestamps for debugging entity lifecycle issues. -
SpatialLogger- centralized logging utility with runtime-configurable log levels.- Supports global and per-category log level configuration via ADB
properties:
adb shell setprop debug.spatial.log.level <LEVEL>.
- Supports global and per-category log level configuration via ADB
properties:
-
Entity.willBeDeleted()method that returnstrueifdestroy()has been called on the entity in the current frame, allowing systems to check whether an entity is scheduled for deletion before it is fully removed. -
SceneTexture.clear(r, g, b, a)method to clear a texture with a solid color specified by RGBA float values. -
Feature-level hit testing support, enabling features such as GSplat to participate in ray intersection and ISDK interactions alongside standard scene objects.
-
Batch node transform update APIs for
SceneObjectsetLocalNodePoses()for sparse or sequential node updates.setLocalNodePosesRange()for contiguous range updates.setLocalNodeTransformsBatch()andsetLocalNodeTransformsRange()for direct float array APIs with transform component flags.- Static companion method for multi-object batch updates.
-
World locking for MRUK
- Keeps virtual content stationary relative to the real world without manually parenting every object to anchors.
- Enabled by default. Toggle with
MRUKFeature.setWorldLockingEnabled(Boolean)and query withMRUKFeature.isWorldLockingEnabled(). - After recentering, scene anchors stay aligned with the real world.
-
Entity.requireComponent<T>(errorMessage)method that throws aRuntimeExceptionwith a custom mßessage if the component is not found on the entity. -
HEAD_LOCKEDandHEAD_RELATIVEstereo audio offset modes added to theAudioSessionStereoOffsetscomponent.HEAD_LOCKEDanchors audio to the user's head position and rotation, ignoring the entity's position.HEAD_RELATIVEanchors audio to the user's head position but rotates stereo space based on the emitter's horizontal orientation.
-
Support for playing animations by name in the
Animatedcomponent. Set theanimationNameproperty to the name of the animation track in the glTF file instead of using thetrackindex. WhenanimationNameis set, it takes precedence overtrack. -
Support for 44.1kHz and other non-standard audio sample rates. Audio files are no longer restricted to multiples of 16kHz. Files with other sample rates are automatically resampled to 48kHz.
-
Experimental:
SceneMaterial.setRenderOrder()API for fine-grained control over material render ordering. Valid range is -3 to +3; higher values render later. -
DataModel.getComponentIdsForEntity()to query which components are attached to an entity. -
SceneTexture.fromResource()for loading textures from any Android drawable resource type (bitmaps, vectors, shapes) with optional scaling.baseTextureScaleattribute onMaterialfor scaling textures in component XML.val texture = SceneTexture.fromResource(context, R.drawable.my_bitmap) val scaled = SceneTexture.fromResource(context, R.drawable.my_bitmap, scale = 0.5f)
-
URI-based mesh creators with query parameter support via
registerMeshCreator(baseUrl, (entity, uri) -> SceneMesh). Enables parameterized procedural mesh generation from a single registered creator.registerMeshCreator("mesh://custom/box") { entity, uri -> val size = uri.getQueryParameter("size")?.toFloatOrNull() ?: 0.1f SceneMesh.box(-size, -size, -size, size, size, size, material) } // Use: Mesh(Uri.parse("mesh://custom/box?size=0.25"))
-
Quaternionfactory methods for common rotation patternsfromAxisAngle(axis, angleDegrees)andfromAxisAngleRadians(axis, angleRadians).fromEuler(pitch, yaw, roll)(also acceptsVector3).fromTwoVectors(from, to)for vector-to-vector rotation.fromDirection(direction, up)for look-at style orientation.
-
OVRMetricsSystemnow supports runtime addition and removal of metrics and overlay messages.registerMetric(),registerMetrics(),unregisterMetric()for dynamic metric management.registerOverlayMessage()andunregisterOverlayMessage()for dynamic overlay messages.OVRMetricsTicksprovidesticksPerSecond()andmaxTickTimeMs()for monitoring ECS tick performance.
-
Distance and angle utility methods on
Pose,Quaternion, andVector3Vector3.isWithinDistance(other, distance)for proximity checks using squared distance internally.Quaternion.isWithinAngle(other, angleRadians)andisWithinAngleDegrees()for angular similarity checks.- Corresponding
Pose.isWithinDistance()andPose.isWithinAngle()convenience methods.
-
Audio focus awareness
- Override
VrActivity.audioPauseMode()to chooseACTIVITY_FOCUS(default),OPENXR_FOCUS, orMANUAL. Scene.setAudioEnabled()for manual audio control.
- Override
-
Scene.removeObject()method to remove aSceneObjectfrom a scene without destroying it, allowing natural garbage collection. -
Experimental Panel Resize API
-
PanelSceneObject.resize(newWidthInPx, newHeightInPx)allows dynamically resizing panels at runtime (programmatic). -
IsdkPanelResizecomponent enables interactive user-driven panel resizing through corner handles. Adding this component auto-createsIsdkPanelGrabHandleon the entity.entity.setComponent(IsdkPanelResize( enabled = true, resizeMode = ResizeMode.Relayout, preserveAspectRatio = false, minDimensions = Vector2(0.3f, 0.3f), maxDimensions = Vector2(1.5f, 1.5f), ))
-
ResizeModecontrols resize behavior:Simple-- modifies entityScalecomponent (default).Relayout-- adjusts panel pixel dimensions and re-renders UI, preserving layout quality.None-- handles do nothing; write custom resize logic viaInputListeneron thePanelSceneObject.
-
HandleSegmentTypeenum for identifying grab edges, grab corners, and resize corners on panels. -
Haptic and audio feedback on resize start and end.
-
-
Experimental: Sticky grab interaction support via ISDK
- Allows users to maintain grip on objects after the grab gesture ends, until an explicit release gesture.
- For hand tracking, release is triggered when all fingers are fully extended.
- For controllers, the grip button toggles grab on and off.
-
Experimental:
CachedQueryfor incremental entity tracking- Maintains a stable entity set and updates incrementally, avoiding full re-queries each frame.
- Supports
onAdd,onUpdate, andonDeletecallbacks for entity lifecycle events with filtering. - Native-level
.filterDSL for attribute-based filtering. where { has(...) }DSL syntax for consistency withQuery.hasChangedSincequery opcode for efficient incremental change detection.
-
Experimental: Full body tracking support
- Added 14 new lower-body joints to
JointType(legs, ankles, and feet) for full-body skeleton tracking. JointSetenum to select betweenDEFAULT(upper body and hands) andFULL_BODYjoint topologies.Scene.setBodyTrackingJointSet()to configure which joint set the body tracking system provides.BodyTrackingFidelityenum withLOWandHIGHlevels, andScene.setBodyTrackingFidelity()for quality/performance control.
- Added 14 new lower-body joints to
-
AIDebugToolsFeaturefor headless, AI-driven app testing and debugging via ADB broadcast intents.- Debug commands:
get_entities,get_viewer_pose,set_viewer_pose,get_crash_history,get_stack_trace. - Panel UI inspection: panel discovery, UI element inspection, and programmatic view clicking.
- Entity interaction: lookup by name, detail inspection, transform modification, and teleportation.
CrashMonitor,LogcatMonitor, andTombstoneMonitorfor runtime crash detection and diagnostics.
- Debug commands:
-
Hand outlines are now brighter.
-
Physics APIs have been decoupled from the core library into the
com.meta.spatial.physicsfeature module. This is a breaking change.// Before import com.meta.spatial.runtime.ScenePhysicsObject spatial.enablePhysicsDebugLines(true) spatial.setGravity(0f, -10f, 0f) // After import com.meta.spatial.physics.ScenePhysicsObject val physicsFeature = PhysicsFeature(spatial) // register in registerFeatures() physicsFeature.enablePhysicsDebugLines(true) physicsFeature.setGravity(Vector3(0f, -10f, 0f))
-
Removed the synthetic GLTF root node from the mesh hierarchy. Node indices now correspond directly to their GLTF node indices.
-
Entity and component debug logging improvements
getComponenterror messages now include the attribute name and entity ID.- Mesh creation provides explicit error messages when a required component is missing, including the entity ID, component name, and a listing of all attached components.
- Entity debug error messages include human-readable attribute
names and full entity debug info (creation callstack, time
alive, component list) when
EntityDebugInfoflags are enabled.
-
CastInputForwardFeaturenow accepts an optionalinitialPoseparameter to customize the starting position of the input forwarding virtual camera. -
IsdkPanelGrabHandlehas been redesigned with a multi-segment handle system (edges, corners, resize corners) replacing the single box collider. This is a breaking change. Theoffsetandpaddingproperties have been removed and replaced withgrabHandleCollisionWidths,resizeCornerCollisionSizes,resizeCornerCollisionInset,outset,zOffset,color, andscaleFactor. -
Audio engine now supports up to 256 virtual sounds with automatic focus management, selecting the best 16 for simultaneous playback based on distance, volume, and priority. Sounds transition smoothly to prevent audio artifacts.
-
Component registration is now auto-generated during the build. Use
ComponentRegistrations.all()incomponentsToRegister()instead of maintaining manual lists. -
SceneObject.materialsis now lazily populated on first access, providing up to 16x faster entity creation. The property type changed fromArray<SceneMaterial>?toList<SceneMaterial>?.// Before sceneObject.materials?.set(0, differentMaterial) // After (modify properties in place) sceneObject.materials?.get(0)?.setColor(Color4(1f, 0f, 0f, 1f))
-
Material.sceneTextureCacheis now private. UseMaterial.registerSceneTexture(key, texture)to register dynamically generated textures for use with theMaterialcomponent viabaseTextureAndroidResourceId. This is a breaking change.// Before Material.sceneTextureCache[resourceId] = sceneTexture // After Material.registerSceneTexture(resourceId, sceneTexture)
PanelSceneObject.getDisplay(),getTexture(),getLayer(),getPanelShapeConfig(),getSwapchain(), andgetSurface()are deprecated. Use property accessors instead.
- Removed the experimental
PanelConfigOptions2API. This is a breaking change. AllPanelConfigOptions2classes, constructors, extension functions, and builder methods have been deleted. UsePanelConfigOptionsandPanelShapeConfiginstead.- Replace
PanelSceneObjectconstructors that acceptedPanelConfigOptions2with constructors usingPanelConfigOptionsorPanelShapeConfig. - Replace
getPanelConfigOptions2()withgetPanelShapeConfig(). - Replace
reshape(PanelConfigOptions2)withreshape(PanelShapeConfig). - Replace
PanelRegistration.fromConfigOptions2 { ... }with the standardPanelRegistrationconstructors.
- Replace
- Removed
SpatialInterfacephysics methods:enablePhysicsDebugLines(),setGravity(),createPhysicsObject(),deletePhysics(),tickPhysics(),tickUpdatePhysicsState(). UsePhysicsFeatureandPhysicsBridgeinstead. - Removed the deprecated
generateComponentsGradle task. This task has been deprecated since version 0.6.0 and is no longer needed. Remove any references togenerateComponentsin yourbuild.gradle.
- Fixed Compose panels receiving duplicate hover and input events when two controllers pointed at the same panel. Now only one controller can hover a panel at a time.
- Fixed incorrect 2D
HitInfoUV coordinates on curved panels. UV coordinates fromPointerEventwere calculated as if the panel was flat. - Fixed a bug where interacting with a panel during a shape change (e.g., panel animation) could permanently block locomotion and hide the raycast indicator.
- Fixed entity deletion timing so that destroyed entities remain
accessible during the same frame in which
destroy()is called. Previously, entities were immediately removed, which could cause inconsistent behavior when systems accessed entities during the same tick. - Fixed
HitInfo.distanceto return the actual distance from the interactor to the hit point instead of a hardcoded value of1.0. - Fixed bug in
IsdkSystemwhere lambda observers registered withregisterObserver()orregisterInteractableObserver()would be garbage collected after a few seconds, causing them to stop receiving events. - Fixed misleading native assert message when calling
getComponenton an entity that does not have the requested component. The error now correctly identifies the missing attribute. - Fixed
SamplerConfignot being applied to dynamically createdSceneTextureobjects. Sampler settings now work correctly for textures created in code, not just those loaded from glTF files. - Fixed
changedSincequeries not correctly detecting newly created entities due to incorrect version update ordering in the ECS data model. - Fixed texture bug during texture initialization where textures would briefly appear black.
The samples all include the Spatial SDK Gradle Plugin in their build files. This plugin is used for the Spatial Editor integration and for build-related features like custom shaders.
Meta collects telemetry data from the Spatial SDK Gradle Plugin to help improve MPT Products. You can read the Supplemental Meta Platforms Technologies Privacy Policy to learn more.
The Meta Spatial SDK Samples package is multi-licensed.
The majority of the project is licensed under the MIT License, as found in the LICENSE file.
The Meta Platform Technologies SDK license applies to the Meta Spatial SDK and supporting material, and to the assets used in the Meta Spatial SDK Samples package. The MPT SDK license can be found in the asset folder of each sample.
Specifically, all the supporting materials in each sample's
app/src/main/res/raw and app/src/main/assets folders including 3D models,
videos, sounds, and others, are licensed under the
MPT SDK license.