Environment Panel Placement sample
Updated: May 7, 2026
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.
- 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
You need a Meta Quest device with passthrough capability and Unity installed. For detailed setup instructions, see Setting up your Quest development environment.
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.
| File / Scene | What it demonstrates | Key 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 |
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.
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.
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.
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.
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.
- 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.