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

WebXR AR hit test support #1926

Merged
merged 16 commits into from
Mar 22, 2020
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