Saturday, July 19, 2025

Simple and Quick Physics in WebXR using A-Frame | by Muadh Al Kalbani | Samsung Internet Developers | Jul, 2025

Share

A bronze statue of Albert Einstein looking at his raised pointing index finger
Photo by Crisoforo Gaspar Hernandez on Unsplash

Simulating physical properties on virtual objects can go a long way in enhancing the realism and fidelity of WebXR scenes. An immersive scene that obeys the rules of physics as we know them in the real world greatly strengthens the illusion of merging reality with virtuality for end users. As a developer, I personally never enjoyed implementing physical properties for individual objects, but the good news — and the reason why I am writing this post—is: adding physics to a WebXR scene has never been easier. In this post I will illustrate how you can easily get a physics engine up and running in your WebXR scene with a few lines of code, and will also walkthrough an extra demo to showcase physics with user interaction. All aboard the Physics Train!

I’ll start with a simple A-Frame scene with three RGB cubes next to each other. Note the positioning of these objects worked well for my working space, so feel free to adjust these values based on your requirements.




WebXR Physics with Hand Input

},
});

AFRAME.registerComponent('pinch-handler', {
init: function () {

};
});

. . .

A-Frame’s page on hand-grab-controls mentions obb-collider for debugging purposes, so I thought that may be a good option to use for detecting collisions, for that I’ll add two events in the collision-handler component which will be later attached to every grabbable object:

. . .

AFRAME.registerComponent('collision-handler', {
init: function () {
this.el.addEventListener('obbcollisionstarted', (e) => {
const pinchHandler = document.querySelector('[pinch-handler]').components['pinch-handler'];
if (!pinchHandler.grabbedObject) {
pinchHandler.collidingObject = this.el;
}
});

this.el.addEventListener('obbcollisionended', (e) => {
const pinchHandler = document.querySelector('[pinch-handler]').components['pinch-handler'];
if (!pinchHandler.grabbedObject || pinchHandler.grabbedObject === this.el) {
pinchHandler.collidingObject = null;
}
});
}
});

. . .

Essentially here we are listening for collisions on each object, if there is a collision with a given object we pass a reference of that object via collidingObject to the pinch-handler component. There are a couple of checks that I am doing here as well to avoid any unforeseen serious fights between collision and pinching events — when a collision starts, we set collidingObject to the current object being collided with only if no other object is being grabbed by pinching (we will get that information from the pinch-handler component). Similarly when a collision ends, we clear collidingObject (i.e. set it to null) only if the collidingObject is no longer being grabbed by a pinch.

Next up is adding pinch events to the pinch-handler component:

. . .

AFRAME.registerComponent('pinch-handler', {
init: function () {
this.collidingObject = null;
this.grabbedObject = null;

const rightHand = document.querySelector('#rightHand');
const leftHand = document.querySelector('#leftHand');

rightHand.addEventListener('pinchstarted', () => {
if (this.collidingObject && !this.grabbedObject) {
this.grabbedObject = this.collidingObject;
this.grabbedObject.removeAttribute('dynamic-body');
}
});

rightHand.addEventListener('pinchended', () => {
if (this.grabbedObject) {
this.grabbedObject.setAttribute('dynamic-body', 'mass: 1');
this.grabbedObject = null;
}
});

leftHand.addEventListener('pinchstarted', () => {
if (this.collidingObject && !this.grabbedObject) {
this.grabbedObject = this.collidingObject;
this.grabbedObject.removeAttribute('dynamic-body');
}
});

leftHand.addEventListener('pinchended', () => {
if (this.grabbedObject) {
this.grabbedObject.setAttribute('dynamic-body', 'mass: 1');
this.grabbedObject = null;
}
});
}
});

. . .

Here we add two pinching events for each hand in our scene. Starting a pinch removes dynamic-body from the object being interacted with (which comes from the collision-handler component) allowing it to move freely with your hand, and ending a pinch reinstates the dynamic-body property with the default mass which makes the object fall down with gravity and collide with other objects as it would having physics. Again an important check here — grabbing an object with pinching can happen only if there is an active collision and no other object is being grabbed by pinching. When the pinch ends, we clear the reference to grabbedObject (i.e. set it to null).

We should now have all the components to make my stacking cubes dream come true. We do however need to prep the objects to be grabbable and listened to by the collision-handler component as follows. First we should add pinch-handler component to the scene:

. . .

. . .

Then add the grabbable and collision-handler properties to all the dynamic objects in our scene:

. . .

. . .

A gif showing a tracked hand overlayed with a 3D model in an augmented reality environment stacking three red, green and blue virtual cubes on a physical table.
Stack ’em up! User input with physics enabled in action

Upon reviewing this post, I had a few points that I wanted to end on:

And that’s that for getting started with physics using A-Frame! I hope this was useful in showcasing what physics can do in WebXR.

Muadh out — Until next time! 🤜🤛

P.S.

The full script should now look like this. You can also grab the full script from the physics-hand-input-aframe GitHub repo:




WebXR Physics with Hand Input

this.el.addEventListener('obbcollisionended', (e) => {
const pinchHandler = document.querySelector('[pinch-handler]').components['pinch-handler'];
if (!pinchHandler.grabbedObject || pinchHandler.grabbedObject === this.el) {
pinchHandler.collidingObject = null;
}
});
}
});

AFRAME.registerComponent('pinch-handler', {
init: function () {
this.collidingObject = null;
this.grabbedObject = null;

const rightHand = document.querySelector('#rightHand');
const leftHand = document.querySelector('#leftHand');

rightHand.addEventListener('pinchstarted', () => {
if (this.collidingObject && !this.grabbedObject) {
this.grabbedObject = this.collidingObject;
this.grabbedObject.removeAttribute('dynamic-body');
}
});

rightHand.addEventListener('pinchended', () => {
if (this.grabbedObject) {
this.grabbedObject.setAttribute('dynamic-body', 'mass: 1');
this.grabbedObject = null;
}
});

leftHand.addEventListener('pinchstarted', () => {
if (this.collidingObject && !this.grabbedObject) {
this.grabbedObject = this.collidingObject;
this.grabbedObject.removeAttribute('dynamic-body');
}
});

leftHand.addEventListener('pinchended', () => {
if (this.grabbedObject) {
this.grabbedObject.setAttribute('dynamic-body', 'mass: 1');
this.grabbedObject = null;
}
});
}
});

Read more

Trending News