Panel resolution and display options
Updated: Oct 1, 2025
Spatial SDK provides several display options to control panel resolution and visual quality. Panel clarity depends on resolution and FOV relative to the Quest device’s eye buffer. Adjust clarity by modifying panel size and resolution relative to the VR viewpoint.
Quest 3’s eye buffer resolution is 2064 x 2208 pixels with a FOV of 110° x 96° per eye.
How panel rendering works
The rendering process has three steps:
- Generate a 2D texture from the panel UI.
- Place the texture in the 3D scene.
- Render to Quest’s eye buffer.
Panel clarity depends on how texture resolution maps to eye buffer pixels. Distance and FOV determine this mapping.
Display options for different panel types
The display options APIs provide simple, intuitive ways to configure panel resolution without complex manual calculations.
Automatically calculate resolution
DpPerMeterDisplayOptions automatically calculates resolution based on physical panel size and desired density:
UIPanelSettings(
shape = QuadShapeOptions(width = 1.0f, height = 0.8f), // Physical size in meters
display = DpPerMeterDisplayOptions(
dpPerMeter = 500f, // Default: 500 dp per meter
resolutionScale = 1.2f // Optional: 20% higher resolution
)
)
Benefits:
- Resize panels without changing layouts
- Consistent UI element sizes across different panel sizes
Considerations:
Works with Quad and Cylinder shapes only
Control UI layouts with density-independent pixels
DpDisplayOptions provides precise control over UI layouts using density-independent pixels. The default DPI is 288:
UIPanelSettings(
shape = QuadShapeOptions(width = 1.0f, height = 0.8f),
display = DpDisplayOptions(
width = 400f, // Layout width in dp
height = 320f, // Layout height in dp
dpi = 288 // Default DPI is 288
)
)
Benefits:
Precise control over UI layouts using density-independent pixels
Considerations:
UI elements scale when you change the physical panel size
Using PixelDisplayOptions is best for media panels where you want to match video content resolution exactly:
MediaPanelSettings(
shape = QuadShapeOptions(width = 1.6f, height = 0.9f),
display = PixelDisplayOptions(
width = 1920, // Match your video width
height = 1080 // Match your video height
)
)
Benefits:
Perfect pixel-to-pixel match with media content
Considerations:
Not recommended for UI panels, which are traditionally designed in dp
Set panel resolution based on screen space
ScreenFractionDisplayOptions is a quick option for setting panel resolution based on how much screen space it should occupy:
UIPanelSettings(
shape = QuadShapeOptions(width = 1.0f, height = 0.8f),
display = ScreenFractionDisplayOptions(
fraction = 0.5f // Default: 50% of screen width
)
)
Benefits:
- Easy to set up
- Requires no calculations for dp or pixels
Considerations:
- Adjusting panel size after UI design requires manual layout adjustments
- You don’t have direct access to precise pixel/dp panel size until runtime, making complex UI design difficult
Panel positioning and scaling
Panel physical size is set in the shape options. You can also scale panels dynamically:
// Scale the panel 50% larger
panelEntity.setComponent(Scale(Vector3(1.5f, 1.5f, 1.5f)))
// Position the panel in 3D space
panelEntity.setComponent(Transform(Pose(Vector3(0f, 1.8f, -2f))))
(Advanced) Directly control display options
Advanced Usage: This section is for developers working with legacy code or needing direct control beyond Display Options APIs. For easier configuration, use
Display options for different panel types instead.
Legacy configuration parameters
import com.meta.spatial.toolkit.PanelCreator
PanelCreator(
registrationId = R.id.custom_panel,
panelCreator = { entity ->
val panelConfigOptions = PanelConfigOptions().apply {
// Direct pixel control
layoutWidthInPx = 1920
layoutHeightInPx = 1080
// Or DP-based control
layoutWidthInDp = 400f
layoutHeightInDp = 300f
layoutDpi = 288
// Or screen fraction
fractionOfScreen = 0.5f
// Physical size in meters
width = 1.6f
height = 0.9f
}
PanelSceneObject(scene, spatialContext, R.layout.my_layout, entity, panelConfigOptions)
}
)
- Cannot combine
layoutWidthInPx with layoutWidthInDp - If no pixel/dp values set, falls back to
fractionOfScreen layoutDpi affects conversion: layoutWidthInPx = layoutWidthInDp × layoutDpi / 160- Physical
width/height determines 3D mesh size, separate from texture resolution
- Avoid exceeding 2064x2208px due to memory limitations
- Higher DPI improves quality but increases memory usage
- Use
fractionOfScreen for automatic scaling across Quest models
Previewing your UI layouts
When designing Jetpack Compose UI for your panels, use the @Preview annotation to test layouts before deploying to VR. This is another reason to use dp-based resolution settings. It makes previewing and iterating on UI much easier on your computer. Set preview dimensions to match your panel’s dp configuration for accurate representation:
For DpDisplayOptions panels
@Preview(
widthDp = 400, // Match your DpDisplayOptions width
heightDp = 320 // Match your DpDisplayOptions height
)
@Composable
fun MyPanelPreview() {
MyPanelComposable()
}
For DpPerMeterDisplayOptions panels
Calculate dp dimensions based on your physical size and dpPerMeter setting:
// Panel: (width = 1.0m, height = 0.8m) with 500 dpPerMeter
@Preview(
widthDp = 500, // 1.0m × 500 dpPerMeter
heightDp = 400 // 0.8m × 500 dpPerMeter
)
@Composable
fun MyPanelPreview() {
MyPanelComposable()
}
Troubleshooting: large panel cropping
Large panels (especially 4K media) may display cropping or black pixels. This affects media playback quality.
- Panels with high resolution (approaching 2064x2208px limits)
- Video panels displaying 4K content
- Panels using
PixelDisplayOptions with large dimensions
Workaround: direct-to-surface rendering
For video-only panels experiencing cropping, enabling direct-to-surface rendering is a usable workaround.
Prerequisites:
Implementation:
class VideoActivity : AppSystemActivity() {
lateinit var exoPlayer: ExoPlayer
lateinit var videoEntity: Entity
override fun onVRReady() {
super.onVRReady()
videoEntity = Entity.create(Transform(), Grabbable())
createVideoPanel()
}
private fun createVideoPanel() {
val panelConfigOptions = PanelConfigOptions().apply {
layoutWidthInPx = 3840 // 4K width
layoutHeightInPx = 2160 // 4K height
layerConfig = LayerConfig()
mips = 1 // Required for performance
enableTransparent = false // Required for quality
}
// Create surface-only panel
val panelSceneObject = PanelSceneObject(scene, videoEntity, panelConfigOptions)
// Connect ExoPlayer directly to surface
exoPlayer.setVideoSurface(panelSceneObject.getSurface())
// Attach to ECS
systemManager.findSystem<SceneObjectSystem>()
.addSceneObject(videoEntity,
CompletableFuture<SceneObject>().apply { complete(panelSceneObject) })
// Enable input handling
videoEntity.setComponent(Hittable())
}
}
Note: This removes Android input handling. Use SceneObject input listeners for interactions.
Example implementations: