Develop
Develop
Select your platform

Panel resolution and display options

Updated: Oct 1, 2025

Overview

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.
Diagram showing Quest 3 field of view with 110° x 96° per eye and panel positioning within the VR viewport

How panel rendering works

The rendering process has three steps:
  1. Generate a 2D texture from the panel UI.
  2. Place the texture in the 3D scene.
  3. 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

Match media resolution

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)
    }
)

Parameter relationships

  • 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

Performance considerations

  • 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.

When this occurs

  • 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:

Design guidelines

Did you find this page helpful?
Thumbs up icon
Thumbs down icon