Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebXR Tracked Hands API #2316

Merged
merged 7 commits into from
Aug 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ var categories = [
'ar-hit-test',
'vr-basic',
'vr-controllers',
'vr-hands',
'vr-movement',
'xr-picking'
]
Expand Down
229 changes: 229 additions & 0 deletions examples/xr/vr-hands.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html>
<head>
<title>PlayCanvas VR Hands</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link rel="icon" type="image/png" href="../playcanvas-favicon.png" />
<script src="../../build/playcanvas.js"></script>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
}
.message {
position: absolute;
padding: 8px 16px;
left: 20px;
bottom: 20px;
color: #ccc;
background-color: rgba(0, 0, 0, .5);
font-family: "Proxima Nova", Arial, sans-serif;
}
</style>
</head>

<body>
<canvas id="application-canvas"></canvas>
<div class="message"></div>
<script>
var message = function (msg) {
var el = document.querySelector('.message');
el.textContent = msg;
};

var canvas = document.getElementById('application-canvas');
var app = new pc.Application(canvas, {
mouse: new pc.Mouse(canvas),
touch: new pc.TouchDevice(canvas),
keyboard: new pc.Keyboard(window)
});
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);

window.addEventListener("resize", function () {
app.resizeCanvas(canvas.width, canvas.height);
});

// use device pixel ratio
app.graphicsDevice.maxPixelRatio = window.devicePixelRatio;

app.scene.ambientLight = new pc.Color(0.1, 0.1, 0.1);

app.start();

// create camera
var c = new pc.Entity();
c.addComponent('camera', {
clearColor: new pc.Color(44 / 255, 62 / 255, 80 / 255)
});
app.root.addChild(c);

var l = new pc.Entity();
l.addComponent("light", {
type: "directional"
});
l.setEulerAngles(45, 135, 0);
app.root.addChild(l);

var createCube = function (x, y, z) {
var cube = new pc.Entity();
cube.addComponent("model", {
type: "box",
material: new pc.StandardMaterial()
});
cube.translate(x, y, z);
app.root.addChild(cube);
};

var controllers = [];

// create controller model
var createController = function (inputSource) {
var entity = new pc.Entity();

if (inputSource.hand) {
// hand input
entity.joints = [];

var material = new pc.StandardMaterial();

// create box for each hand joint
for(var i = 0; i < inputSource.hand.joints.length; i++) {
var joint = inputSource.hand.joints[i];
var jointEntity = new pc.Entity();
jointEntity.addComponent('model', {
type: 'box',
material: material
});
jointEntity.joint = joint;
entity.joints.push(jointEntity);
entity.addChild(jointEntity);
}
// when tracking lost, paint joints to red
inputSource.hand.on('trackinglost', function() {
entity.joints[0].model.material.diffuse.set(1, 0, 0);
entity.joints[0].model.material.update();
});
// when tracking recovered, paint joints to white
inputSource.hand.on('tracking', function() {
entity.joints[0].model.material.diffuse.set(1, 1, 1);
entity.joints[0].model.material.update();
});
} else {
// other inputs
entity.addComponent('model', {
type: 'box',
castShadows: true
});
entity.setLocalScale(0.05, 0.05, 0.05);
}

app.root.addChild(entity);
entity.inputSource = inputSource;
controllers.push(entity);

// destroy input source related entity
// when input source is removed
inputSource.on('remove', function () {
controllers.splice(controllers.indexOf(entity), 1);
entity.destroy();
});
};

// create a grid of cubes
var SIZE = 4;
for (var x = 0; x <= SIZE; x++) {
for (var y = 0; y <= SIZE; y++) {
createCube(2 * x - SIZE, -1.5, 2 * y - SIZE);
}
}

if (app.xr.supported) {
var activate = function () {
if (app.xr.isAvailable(pc.XRTYPE_VR)) {
c.camera.startXr(pc.XRTYPE_VR, pc.XRSPACE_LOCAL, function (err) {
if (err) message("Immersive VR failed to start: " + err.message);
});
} else {
message("Immersive VR is not available");
}
};

app.mouse.on("mousedown", function () {
if (! app.xr.active)
activate();
});

if (app.touch) {
app.touch.on("touchend", function (evt) {
if (! app.xr.active) {
// if not in VR, activate
activate();
} else {
// otherwise reset camera
c.camera.endXr();
}

evt.event.preventDefault();
evt.event.stopPropagation();
});
}

// end session by keyboard ESC
app.keyboard.on('keydown', function (evt) {
if (evt.key === pc.KEY_ESCAPE && app.xr.active) {
app.xr.end();
}
});

// when new input source added
app.xr.input.on('add', function (inputSource) {
message("Controller Added");
createController(inputSource);
});

if (window.XRHand) {
message("Tap on screen to enter VR, and switch to hand input");
} else {
message("WebXR Hands Input is not supported by your platform");
}

// update position and rotation for each controller
app.on('update', function () {
for (var i = 0; i < controllers.length; i++) {
var inputSource = controllers[i].inputSource;

if (inputSource.hand) {
// hand input source
controllers[i].enabled = true;
// update each hand joint
for(var j = 0; j < controllers[i].joints.length; j++) {
var joint = controllers[i].joints[j].joint;
var r = joint.radius * 2;
controllers[i].joints[j].setLocalScale(r, r, r);
controllers[i].joints[j].setPosition(joint.getPosition());
controllers[i].joints[j].setRotation(joint.getRotation());
}
} else if (inputSource.grip) {
// grippable input source
controllers[i].enabled = true;
controllers[i].setLocalPosition(inputSource.getLocalPosition());
controllers[i].setLocalRotation(inputSource.getLocalRotation());
} else {
// some controllers cannot be gripped
controllers[i].enabled = false;
}
}
});
} else {
message("WebXR is not supported");
}
</script>
</body>
</html>
45 changes: 45 additions & 0 deletions src/xr/xr-finger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @class
* @name pc.XrFinger
* @classdesc Represents finger with related joints and index
* @description Represents finger with related joints and index
* @param {number} index - Index of a finger
Maksims marked this conversation as resolved.
Show resolved Hide resolved
* @param {pc.XrHand} hand - Hand that finger relates to
* @property {number} index Index of a finger, numeration is: thumb, index, middle, ring, little
* @property {pc.XrHand} hand Hand that finger relates to
* @property {pc.XrJoint[]} joints List of joints that relates to this finger, starting from joint closest to wrist all the way to the tip of a finger
* @property {pc.XrJoint|null} tip Tip of a finger, or null if not available
*/
function XrFinger(index, hand) {
this._index = index;
this._hand = hand;
this._hand._fingers.push(this);
this._joints = [];
this._tip = null;
}

Object.defineProperty(XrFinger.prototype, 'index', {
get: function () {
return this._index;
}
});

Object.defineProperty(XrFinger.prototype, 'hand', {
get: function () {
return this._hand;
}
});

Object.defineProperty(XrFinger.prototype, 'joints', {
get: function () {
return this._joints;
}
});

Object.defineProperty(XrFinger.prototype, 'tip', {
get: function () {
return this._tip;
}
});

export { XrFinger };
Loading