Skip to content

Commit

Permalink
Support XR input sources (#59)
Browse files Browse the repository at this point in the history
* Support XR input sources

* Add missing file
  • Loading branch information
willeastcott authored Nov 21, 2024
1 parent 2ad25c6 commit 1ffdc0e
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 4 deletions.
2 changes: 2 additions & 0 deletions examples/animation.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<pc-app antialias="false">
<!-- Assets -->
<pc-asset id="camera-controls" src="scripts/camera-controls.mjs" preload></pc-asset>
<pc-asset id="vr" src="scripts/vr.mjs" preload></pc-asset>
<pc-asset id="shadow-catcher" src="scripts/shadow-catcher.mjs" preload></pc-asset>
<pc-asset id="lake-bed" src="assets/skies/dry-lake-bed-2k.hdr" preload></pc-asset>
<pc-asset id="t-rex" src="assets/models/t-rex.glb" preload></pc-asset>
Expand All @@ -39,6 +40,7 @@
"zoomMax": 15
}'
></pc-script>
<pc-script name="vr"></pc-script>
</pc-scripts>
</pc-entity>
<!-- T-Rex -->
Expand Down
9 changes: 5 additions & 4 deletions examples/scripts/ui.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Vec3 } from 'playcanvas';

document.addEventListener('DOMContentLoaded', async () => {
const camera = await document.querySelector('pc-camera').ready();
const appElement = await document.querySelector('pc-app').ready();
const app = appElement.app;

// Create container for buttons
const container = document.createElement('div');
Expand Down Expand Up @@ -51,19 +52,19 @@ document.addEventListener('DOMContentLoaded', async () => {
}

// Add VR button if available
if (camera.xrAvailable) {
if (app.xr.isAvailable('immersive-vr')) {
const vrButton = createButton({
icon: `<svg width="32" height="32" viewBox="0 0 48 48">
<path d="M30,34 L26,30 L22,30 L18,34 L14,34 C11.7908610,34 10,32.2091390 10,30 L10,18 C10,15.7908610 11.7908610,14 14,14 L34,14 C36.2091390,14 38,15.7908610 38,18 L38,30 C38,32.2091390 36.2091390,34 34,34 L30,34 Z M44,28 C44,29.1045694 43.1045694,30 42,30 C40.8954306,30 40,29.1045694 40,28 L40,20 C40,18.8954305 40.8954306,18 42,18 C43.1045694,18 44,18.8954305 44,20 L44,28 Z M8,28 C8,29.1045694 7.10456940,30 6,30 C4.89543060,30 4,29.1045694 4,28 L4,20 C4,18.8954305 4.89543060,18 6,18 C7.10456940,18 8,18.8954305 8,20 L8,28 Z" fill="currentColor">
</svg>`,
title: 'Enter VR',
onClick: () => camera.startXr('immersive-vr', 'local-floor')
onClick: () => app.fire('vr:start', 'local-floor')
});
container.appendChild(vrButton);

window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
camera.endXr();
app.fire('vr:end');
}
});
}
Expand Down
119 changes: 119 additions & 0 deletions examples/scripts/vr.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Script } from 'playcanvas';

const jointIds = [
'wrist',
'thumb-metacarpal', 'thumb-phalanx-proximal', 'thumb-phalanx-distal', 'thumb-tip',
'index-finger-metacarpal', 'index-finger-phalanx-proximal', 'index-finger-phalanx-intermediate', 'index-finger-phalanx-distal', 'index-finger-tip',
'middle-finger-metacarpal', 'middle-finger-phalanx-proximal', 'middle-finger-phalanx-intermediate', 'middle-finger-phalanx-distal', 'middle-finger-tip',
'ring-finger-metacarpal', 'ring-finger-phalanx-proximal', 'ring-finger-phalanx-intermediate', 'ring-finger-phalanx-distal', 'ring-finger-tip',
'pinky-finger-metacarpal', 'pinky-finger-phalanx-proximal', 'pinky-finger-phalanx-intermediate', 'pinky-finger-phalanx-distal', 'pinky-finger-tip'
];

export default class Vr extends Script {
controllers = new Map();

initialize() {
this.app.on('vr:start', (space) => {
this.entity.camera.startXr('immersive-vr', space);
});

this.app.on('vr:end', () => {
this.entity.camera.endXr();
});

this.app.xr.input.on('add', async (inputSource) => {
if (!inputSource.profiles?.length) {
console.warn('No profiles available for input source');
return;
}

// console.log the profiles
console.log(inputSource.profiles);

// Try each profile in order until one works
const basePath = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets/dist/profiles';

for (const profileId of inputSource.profiles) {
const profileUrl = `${basePath}/${profileId}/profile.json`;

try {
// Fetch the profile
const response = await fetch(profileUrl);
if (!response.ok) {
console.warn(`Profile ${profileId} not found, trying next...`);
continue;
}

const profile = await response.json();
const layoutPath = profile.layouts[inputSource.handedness]?.assetPath || '';
const assetPath = `${basePath}/${profile.profileId}/${inputSource.handedness}${layoutPath.replace(/^\/?(left|right)/, '')}`;

// Try to load the model
try {
const asset = await new Promise((resolve, reject) => {
this.app.assets.loadFromUrl(assetPath, 'container', (err, asset) => {
if (err) reject(err);
else resolve(asset);
});
});

const container = asset.resource;
const entity = container.instantiateRenderEntity();
this.app.root.addChild(entity);

const jointMap = new Map();
if (inputSource.hand) {
for (const jointId of jointIds) {
const jointEntity = entity.findByName(jointId);
const joint = inputSource.hand.getJointById(jointId);
jointMap.set(jointId, { jointEntity, joint });
}
}

this.controllers.set(inputSource, { entity, jointMap });

// Successfully loaded a profile, exit the loop
console.log(`Successfully loaded profile: ${profileId}`);
return;

} catch (error) {
console.warn(`Failed to load model for profile ${profileId}, trying next...`);
continue;
}

} catch (error) {
console.warn(`Failed to fetch profile ${profileId}, trying next...`);
continue;
}
}

// If we get here, none of the profiles worked
console.warn('No compatible profiles found, using basic controller');
this.createBasicController(inputSource);
});

this.app.xr.input.on('remove', (inputSource) => {
const entity = this.controllers.get(inputSource);
if (entity) {
entity.destroy();
this.controllers.delete(inputSource);
}
});
}

update(dt) {
if (this.app.xr.active) {
for (const [inputSource, { entity, jointMap }] of this.controllers) {
if (inputSource.hand) {
for (const [jointId, { jointEntity, joint }] of jointMap) {
jointEntity.setPosition(joint.getPosition());
jointEntity.setRotation(joint.getRotation());
}
} else {
entity.setPosition(inputSource.getPosition());
entity.setRotation(inputSource.getRotation());
}
}
}
}
}

0 comments on commit 1ffdc0e

Please sign in to comment.