Develop

Environment Panel Placement sample

Updated: May 7, 2026

Overview

This sample demonstrates how to interactively place virtual UI panels on physical environment surfaces using MRUK’s depth-based raycasting system. It shows developers how to detect surfaces, reject invalid placement targets, and implement smooth magnetism-based snapping for intuitive mixed reality interactions.

What you will learn

  • Use EnvironmentRaycastManager to detect physical surfaces with depth data
  • Distinguish between vertical (wall), horizontal (floor), and invalid (ceiling) surfaces
  • Implement magnetic snapping that balances manual control with surface alignment
  • Validate placement targets using PlaceBox and CheckBox collision detection
  • Smooth surface normals and animate panel movement for polished interactions

Requirements

You need a Meta Quest device with passthrough capability and Unity installed. For detailed setup instructions, see Setting up your Quest development environment.

Get started

Clone the Unity-MRUtilityKitSample repository. Open the project in Unity and navigate to Assets/MRUKSamples/EnvironmentPanelPlacement/. Open the EnvironmentPanelPlacement.unity scene. Build and deploy to your Quest device following the build instructions in the repository README.

Explore the sample

File / SceneWhat it demonstratesKey concepts
EnvironmentPanelPlacement.cs
Raycast detection, surface validation, grab interaction, and magnetism-based snapping
EnvironmentRaycastManager, PlaceBox, CheckBox, surface classification, smooth animation
EnvironmentPanelPlacement.unity
Pre-configured scene with panel and raycast manager
Scene setup, component configuration, visual feedback objects
Textures/grab-glow.png
Visual feedback during grab interaction
Affordance design for mixed reality
Textures/panel-texture.png
Panel surface appearance
UI presentation in spatial contexts

Runtime behavior

When you run this scene, you see a virtual panel floating in front of you. Point your right controller at the panel and pull the index or hand trigger to grab it. While grabbing, the panel follows your controller and visualizes the raycast with a line renderer. When you aim at a wall, the panel snaps flush against it and aligns with the surface. When you aim at the floor, the panel stands vertically on the surface, facing you. If you aim at a ceiling or surface with low-quality depth data, the panel stays at manual placement distance without snapping. Release the trigger to commit placement. When not grabbing, use the right thumbstick to scale the panel between 0.2x and 1.5x its original size. Press X on the left controller to toggle world lock, which prevents the virtual scene from drifting as the headset’s tracking updates.

Key concepts

Depth-based surface raycasting

The sample uses EnvironmentRaycastManager.Raycast to detect physical surfaces using the Quest’s depth sensor data. The raycast returns an EnvironmentRaycastHit struct containing the intersection point, surface normal, and a confidence value. The sample rejects hits with normalConfidence below 0.5 to filter out unreliable depth data.
if (environmentRaycastManager.Raycast(ray, out var hit)
    && hit.status == EnvironmentRaycastHitStatus.Hit
    && hit.normalConfidence >= 0.5)
See the full implementation in EnvironmentPanelPlacement.cs and refer to the Environment Raycast API documentation.

Surface classification

The sample classifies surfaces by analyzing the surface normal’s orientation. Ceilings are rejected when the normal points downward with Vector3.Dot(hit.normal, Vector3.down) > 0.7. Vertical surfaces (walls) are detected when Mathf.Abs(Vector3.Dot(hit.normal, Vector3.up)) < 0.3, triggering the PlaceBox alignment logic. Horizontal surfaces (floors) orient the panel to stand vertically on the surface, facing the user, and use CheckBox to verify no collision with nearby environment geometry.
For the complete classification logic, see the TryGetEnvironmentPose method in EnvironmentPanelPlacement.cs.

Magnetism-based snapping

The sample compares two distances: manual placement to the detected surface, and manual placement to the user’s head. When their ratio is less than 0.5, the panel snaps to the surface. The 0.5 threshold represents the midpoint where the panel is equidistant between the surface and the user, creating a natural transition zone between snapped and free-floating states. This proportional heuristic means snapping feels consistent at any range — close to a wall, the snap activates easily; far from any surface, the panel stays under manual control.
float distToEnvPose = Vector3.Distance(manualPose.position, envPose.position);
float distToUser = Vector3.Distance(manualPose.position, userPosition);
if (distToEnvPose / distToUser < 0.5f)

Flush wall placement with PlaceBox

For vertical surfaces, the sample uses EnvironmentRaycastManager.PlaceBox to align the panel flush with the wall. This method checks that all four corners of the panel lie on the detected wall plane and verifies no collisions occur. The sample smooths jitter by maintaining a rolling average of the last 10 surface normals.
See the TryGetEnvironmentPose method in EnvironmentPanelPlacement.cs and the PlaceBox API reference.

Smooth animation

The sample uses Vector3.SmoothDamp for position and a combination of Mathf.SmoothDampAngle with Quaternion.SlerpUnclamped for rotation to animate panel movement. The 0.13-second smooth time creates natural, non-jarring transitions between placement targets. All animation state (velocity vectors, angular velocity) persists across frames to maintain continuity.

Extend the sample

  • Adjust snap sensitivity: Change the 0.5 magnetism threshold in the UpdateTargetPose method of EnvironmentPanelPlacement.cs. Lower values make snapping more eager; higher values require closer proximity to surfaces.
  • Add semantic placement rules: Extend the surface classification logic in TryGetEnvironmentPose to handle specific scene labels like “desk” or “door frame,” allowing different placement behaviors per surface type.
  • Automate multi-panel placement: Combine the raycasting approach from this sample with the SceneDecorator sample to implement rule-based automatic placement of multiple panels across a room.