Skip to content

Commit

Permalink
WebXR AR hit test support (playcanvas#1926)
Browse files Browse the repository at this point in the history
* webxr hit test

* lint fixes

* add missing error event

* fix build:all

* tipes -> types

* tipes -> types

* Fix links

* Fix links

* Fix links

* simplify hit-test start function

* small fixes

* XrHitTest.hitTestSources > XrHitTest.sources

* ar hit test example

* fix merge

Co-authored-by: Will Eastcott <will@playcanvas.com>
  • Loading branch information
2 people authored and Elliott Thompson committed Apr 20, 2020
1 parent b604bc0 commit 7d3e982
Show file tree
Hide file tree
Showing 11 changed files with 746 additions and 12 deletions.
2 changes: 2 additions & 0 deletions build/dependencies.txt
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@
../src/xr/xr-manager.js
../src/xr/xr-input.js
../src/xr/xr-input-source.js
../src/xr/xr-hit-test.js
../src/xr/xr-hit-test-source.js
../src/net/http.js
../src/script/script.js
../src/script/script-type.js
Expand Down
2 changes: 2 additions & 0 deletions build/externs.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ var WebAssembly = {};

// WebXR
var XRWebGLLayer = {};
var XRRay = {};
var DOMPoint = {};
1 change: 1 addition & 0 deletions examples/examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ var categories = [
name: "xr",
examples: [
'ar-basic',
'ar-hit-test',
'vr-basic',
'vr-controllers',
'vr-movement',
Expand Down
2 changes: 1 addition & 1 deletion examples/xr/ar-basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
var createCube = function(x,y,z) {
var cube = new pc.Entity();
cube.addComponent("model", {
type: "box",
type: "box"
});
cube.setLocalScale(.5, .5, .5);
cube.translate(x * .5, y, z * .5);
Expand Down
191 changes: 191 additions & 0 deletions examples/xr/ar-hit-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html>
<head>
<title>PlayCanvas AR Hit Test</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/output/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: 0px;
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>
<p class="message"></p>
</div>
<script>
// draw some axes
var drawAxes = function (pos, scale) {
var color = new pc.Color(1,0,0);

var axis = new pc.Vec3();
var end = new pc.Vec3().copy(pos).add(axis.set(scale,0,0));

app.renderLine(pos, end, color);

color.set(0, 1, 0);
end.sub(axis.set(scale,0,0)).add(axis.set(0,scale,0));
app.renderLine(pos, end, color);

color.set(0, 0, 1);
end.sub(axis.set(0,scale,0)).add(axis.set(0,0,scale));
app.renderLine(pos, end, color);
}
</script>


<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.start();

// create camera
var c = new pc.Entity();
c.addComponent('camera', {
clearColor: new pc.Color(0, 0, 0, 0),
farClip: 10000
});
app.root.addChild(c);

var l = new pc.Entity();
l.addComponent("light", {
type: "spot",
range: 30
});
l.translate(0,10,0);
app.root.addChild(l);

var target = new pc.Entity();
target.addComponent("model", {
type: "cylinder",
});
target.setLocalScale(.5, .01, .5);
app.root.addChild(target);

if (app.xr.supported) {
var activate = function () {
if (app.xr.isAvailable(pc.XRTYPE_AR)) {
c.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, function (err) {
if (err) message("WebXR Immersive AR failed to start: " + err.message);
});
} else {
message("Immersive AR 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();
}
});

app.xr.on('start', function () {
message("Immersive AR session has started");

if (! app.xr.hitTest.supported)
return;

app.xr.hitTest.start({
entityTypes: [ pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE ],
callback: function(err, hitTestSource) {
if (err) {
message("Failed to start AR hit test");
return;
}

hitTestSource.on('result', function (position, rotation) {
target.setPosition(position);
target.setRotation(rotation);
});
}
});
});
app.xr.on('end', function () {
message("Immersive AR session has ended");
});
app.xr.on('available:' + pc.XRTYPE_AR, function (available) {
if (available) {
if (app.xr.hitTest.supported) {
message("Touch screen to start AR session and look at the floor or walls");
} else {
message("AR Hit Test is not supported");
}
} else {
message("Immersive AR is unavailable");
}
});

if (! app.xr.isAvailable(pc.XRTYPE_AR)) {
message("Immersive AR is not available");
} else if (! app.xr.hitTest.supported) {
message("AR Hit Test is not supported");
} else {
message("Touch screen to start AR session and look at the floor or walls");
}
} else {
message("WebXR is not supported");
}
</script>
</body>
</html>
7 changes: 7 additions & 0 deletions src/callbacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,10 @@
* @description Callback used by {@link pc.XrManager#endXr} and {@link pc.XrManager#startXr}.
* @param {Error|null} err - The Error object or null if operation was successfull.
*/

/**
* @callback pc.callbacks.XrHitTestStart
* @description Callback used by {@link pc.XrHitTest#start} and {@link pc.XrHitTest#startForInputSource}.
* @param {Error|null} err - The Error object if failed to create hit test source or null.
* @param {pc.XrHitTestSource|null} hitTestSource - object that provides access to hit results against real world geometry.
*/
117 changes: 117 additions & 0 deletions src/xr/xr-hit-test-source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
Object.assign(pc, function () {
var poolVec3 = [];
var poolQuat = [];


/**
* @class
* @name pc.XrHitTestSource
* @augments pc.EventHandler
* @classdesc Represents XR hit test source, which provides access to hit results of real world geometry from AR session.
* @description Represents XR hit test source, which provides access to hit results of real world geometry from AR session.
* @param {pc.XrManager} manager - WebXR Manager.
* @param {object} xrHitTestSource - XRHitTestSource object that is created by WebXR API.
* @param {boolean} transient - True if XRHitTestSource created for input source profile.
* @example
* hitTestSource.on('result', function (position, rotation) {
* target.setPosition(position);
* });
*/
var XrHitTestSource = function (manager, xrHitTestSource, transient) {
pc.EventHandler.call(this);

this.manager = manager;
this._xrHitTestSource = xrHitTestSource;
this._transient = transient;
};
XrHitTestSource.prototype = Object.create(pc.EventHandler.prototype);
XrHitTestSource.prototype.constructor = XrHitTestSource;

/**
* @event
* @name pc.XrHitTestSource#remove
* @description Fired when {pc.XrHitTestSource} is removed.
* @example
* hitTestSource.once('remove', function () {
* // hit test source has been removed
* });
*/

/**
* @event
* @name pc.XrHitTestSource#result
* @description Fired when hit test source receives new results. It provides transform information that tries to match real world picked geometry.
* @param {pc.Vec3} position - Position of hit test
* @param {pc.Quat} rotation - Rotation of hit test
* @param {pc.XrInputSource|null} inputSource - If is transient hit test source, then it will provide related input source
* @example
* hitTestSource.on('result', function (position, rotation, inputSource) {
* target.setPosition(position);
* target.setRotation(rotation);
* });
*/

/**
* @function
* @name pc.XrHitTestSource#remove
* @description Stop and remove hit test source.
*/
XrHitTestSource.prototype.remove = function () {
if (! this._xrHitTestSource)
return;

var sources = this.manager.hitTest.sources;
var ind = sources.indexOf(this);
if (ind !== -1) sources.splice(ind, 1);

this.onStop();
};

XrHitTestSource.prototype.onStop = function () {
this._xrHitTestSource.cancel();
this._xrHitTestSource = null;

this.fire('remove');
this.manager.hitTest.fire('remove', this);
};

XrHitTestSource.prototype.update = function (frame) {
if (this._transient) {
var transientResults = frame.getHitTestResultsForTransientInput(this._xrHitTestSource);
for (var i = 0; i < transientResults.length; i++) {
var transientResult = transientResults[i];
var inputSource;

if (transientResult.inputSource)
inputSource = this.manager.input._getByInputSource(transientResult.inputSource);

this.updateHitResults(transientResult.results, inputSource);
}
} else {
this.updateHitResults(frame.getHitTestResults(this._xrHitTestSource));
}
};

XrHitTestSource.prototype.updateHitResults = function (results, inputSource) {
for (var i = 0; i < results.length; i++) {
var pose = results[i].getPose(this.manager._referenceSpace);

var position = poolVec3.pop();
if (! position) position = new pc.Vec3();
position.copy(pose.transform.position);

var rotation = poolQuat.pop();
if (! rotation) rotation = new pc.Quat();
rotation.copy(pose.transform.orientation);

this.fire('result', position, rotation, inputSource);
this.manager.hitTest.fire('result', this, position, rotation, inputSource);

poolVec3.push(position);
poolQuat.push(rotation);
}
};


return { XrHitTestSource: XrHitTestSource };
}());
Loading

0 comments on commit 7d3e982

Please sign in to comment.