This sample demonstrates physics-based furniture placement in mixed reality. It shows how to combine WebXR plane detection with a physics engine to create natural placement interactions, build a 3D UI catalog panel anchored to controllers, and use real-world surfaces as physics obstacles. Use this sample when building AR experiences that require intuitive object placement with realistic collision behavior.
Clone the webxr-showcases repository and navigate to the chairs-etc directory. Install dependencies with npm install, then run npm run serve to launch the webpack dev server on https://localhost:8081. Open the URL on your Quest browser and tap “ENTER AR” to begin the session. For detailed build instructions, see the sample README.
When you run the sample, you enter an AR session and the app begins detecting planes in your environment. If no planes appear within five seconds, the system prompts room capture. As vertical planes are detected, the app creates wall colliders; when a floor plane appears, it adjusts the ground collider height.
A 3D catalog panel appears above your left controller, displaying 11 chair thumbnails in a grid. You navigate with the left thumbstick and select a chair with the left trigger. The selected chair loads as a 3D model attached to a dynamic physics body, marked with a gradient cube outline.
Aiming your right controller at the floor causes the furniture to slide toward the raycasted target via physics impulses. You rotate the furniture with the right thumbstick and finalize placement with the right trigger, converting the dynamic body to a fixed obstacle that affects future placements.
Key concepts
Physics-driven placement
The sample uses Rapier3D to create natural placement behavior. Each furniture model attaches to a dynamic rigid body. Every frame during placement, a raycaster fires from the right controller to the floor, finding the target point. The system applies an impulse proportional to the body’s mass toward the target:
let velocity = dir.normalize().multiplyScalar(0.1);
let impulse = velocity.multiplyScalar(this.rigidBody.mass());
this.rigidBody.applyImpulse(impulse, true);
Wall colliders prevent furniture from passing through detected surfaces. See src/furniture.js for the complete implementation.
Plane detection feeding physics
RATK’s plane detection callbacks create physics colliders from real-world geometry. Vertical planes become wall cuboid colliders positioned and rotated to match the detected surface. Floor planes adjust the ground collider height to the detected floor level:
This bridges WebXR spatial understanding with physics simulation, enabling placed objects to respect real-world boundaries. See src/furniture.js for the onPlaneAdded callback implementation.
Furniture finalization
When the user presses the right trigger, the system detaches the furniture model from the dynamic mover body and places it in the scene with a new fixed rigid body and collider. Placed furniture becomes a physics obstacle for subsequent placements:
The dynamic mover resets to position (0, 3, 0) for the next item. See the finalizeFurniturePosition method in src/furniture.js.
3D UI panel system
The catalog UI uses @pmndrs/uikit to render a flexbox-based panel directly in the Three.js scene. The panel anchors to the left controller position with a slight Y-axis offset and rotates via lookAt() to always face the player’s head:
This demonstrates controller-anchored UI that remains readable without requiring the user to reposition. See src/panel.js for the complete UI system.
Extend the sample
Replace the chair catalog: Update src/search.json and add GLB files to src/assets/. Models must include DRACO-compressed geometry and KTX2-compressed textures for optimal performance.
Add scale controls: Map a thumbstick axis to adjust the furniture model’s scale before finalization, enabling users to resize objects to fit their space.
Implement undo functionality: Store placed furniture references in an array and provide a button input that removes the most recent placement from the scene and physics world.