Skip to content

Commit 11700a6

Browse files
WebXR AR hit test support (playcanvas#1926)
* 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>
1 parent 0bea589 commit 11700a6

File tree

11 files changed

+746
-12
lines changed

11 files changed

+746
-12
lines changed

build/dependencies.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@
115115
../src/xr/xr-manager.js
116116
../src/xr/xr-input.js
117117
../src/xr/xr-input-source.js
118+
../src/xr/xr-hit-test.js
119+
../src/xr/xr-hit-test-source.js
118120
../src/net/http.js
119121
../src/script/script.js
120122
../src/script/script-type.js

build/externs.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ var WebAssembly = {};
1414

1515
// WebXR
1616
var XRWebGLLayer = {};
17+
var XRRay = {};
18+
var DOMPoint = {};

examples/examples.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ var categories = [
7979
name: "xr",
8080
examples: [
8181
'ar-basic',
82+
'ar-hit-test',
8283
'vr-basic',
8384
'vr-controllers',
8485
'vr-movement',

examples/xr/ar-basic.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
var createCube = function(x,y,z) {
100100
var cube = new pc.Entity();
101101
cube.addComponent("model", {
102-
type: "box",
102+
type: "box"
103103
});
104104
cube.setLocalScale(.5, .5, .5);
105105
cube.translate(x * .5, y, z * .5);

examples/xr/ar-hit-test.html

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>PlayCanvas AR Hit Test</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
7+
<link rel="icon" type="image/png" href="../playcanvas-favicon.png" />
8+
<script src="../../build/output/playcanvas.js"></script>
9+
<style>
10+
body {
11+
margin: 0;
12+
padding: 0;
13+
overflow: hidden;
14+
}
15+
canvas {
16+
width:100%;
17+
height:100%;
18+
19+
}
20+
.message {
21+
position: absolute;
22+
padding: 8px 16px;
23+
left: 20px;
24+
bottom: 0px;
25+
color: #ccc;
26+
background-color: rgba(0, 0, 0, .5);
27+
font-family: "Proxima Nova", Arial, sans-serif;
28+
}
29+
</style>
30+
</head>
31+
32+
<body>
33+
<canvas id="application-canvas"></canvas>
34+
<div>
35+
<p class="message"></p>
36+
</div>
37+
<script>
38+
// draw some axes
39+
var drawAxes = function (pos, scale) {
40+
var color = new pc.Color(1,0,0);
41+
42+
var axis = new pc.Vec3();
43+
var end = new pc.Vec3().copy(pos).add(axis.set(scale,0,0));
44+
45+
app.renderLine(pos, end, color);
46+
47+
color.set(0, 1, 0);
48+
end.sub(axis.set(scale,0,0)).add(axis.set(0,scale,0));
49+
app.renderLine(pos, end, color);
50+
51+
color.set(0, 0, 1);
52+
end.sub(axis.set(0,scale,0)).add(axis.set(0,0,scale));
53+
app.renderLine(pos, end, color);
54+
}
55+
</script>
56+
57+
58+
<script>
59+
var message = function (msg) {
60+
var el = document.querySelector('.message');
61+
el.textContent = msg;
62+
}
63+
64+
var canvas = document.getElementById('application-canvas');
65+
var app = new pc.Application(canvas, {
66+
mouse: new pc.Mouse(canvas),
67+
touch: new pc.TouchDevice(canvas),
68+
keyboard: new pc.Keyboard(window)
69+
});
70+
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
71+
app.setCanvasResolution(pc.RESOLUTION_AUTO);
72+
73+
window.addEventListener("resize", function () {
74+
app.resizeCanvas(canvas.width, canvas.height);
75+
});
76+
77+
// use device pixel ratio
78+
app.graphicsDevice.maxPixelRatio = window.devicePixelRatio;
79+
80+
app.start();
81+
82+
// create camera
83+
var c = new pc.Entity();
84+
c.addComponent('camera', {
85+
clearColor: new pc.Color(0, 0, 0, 0),
86+
farClip: 10000
87+
});
88+
app.root.addChild(c);
89+
90+
var l = new pc.Entity();
91+
l.addComponent("light", {
92+
type: "spot",
93+
range: 30
94+
});
95+
l.translate(0,10,0);
96+
app.root.addChild(l);
97+
98+
var target = new pc.Entity();
99+
target.addComponent("model", {
100+
type: "cylinder",
101+
});
102+
target.setLocalScale(.5, .01, .5);
103+
app.root.addChild(target);
104+
105+
if (app.xr.supported) {
106+
var activate = function () {
107+
if (app.xr.isAvailable(pc.XRTYPE_AR)) {
108+
c.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, function (err) {
109+
if (err) message("WebXR Immersive AR failed to start: " + err.message);
110+
});
111+
} else {
112+
message("Immersive AR is not available");
113+
}
114+
};
115+
116+
app.mouse.on("mousedown", function () {
117+
if (! app.xr.active)
118+
activate();
119+
});
120+
121+
if (app.touch) {
122+
app.touch.on("touchend", function (evt) {
123+
if (! app.xr.active) {
124+
// if not in VR, activate
125+
activate();
126+
} else {
127+
// otherwise reset camera
128+
c.camera.endXr();
129+
}
130+
131+
evt.event.preventDefault();
132+
evt.event.stopPropagation();
133+
});
134+
}
135+
136+
// end session by keyboard ESC
137+
app.keyboard.on('keydown', function (evt) {
138+
if (evt.key === pc.KEY_ESCAPE && app.xr.active) {
139+
app.xr.end();
140+
}
141+
});
142+
143+
app.xr.on('start', function () {
144+
message("Immersive AR session has started");
145+
146+
if (! app.xr.hitTest.supported)
147+
return;
148+
149+
app.xr.hitTest.start({
150+
entityTypes: [ pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE ],
151+
callback: function(err, hitTestSource) {
152+
if (err) {
153+
message("Failed to start AR hit test");
154+
return;
155+
}
156+
157+
hitTestSource.on('result', function (position, rotation) {
158+
target.setPosition(position);
159+
target.setRotation(rotation);
160+
});
161+
}
162+
});
163+
});
164+
app.xr.on('end', function () {
165+
message("Immersive AR session has ended");
166+
});
167+
app.xr.on('available:' + pc.XRTYPE_AR, function (available) {
168+
if (available) {
169+
if (app.xr.hitTest.supported) {
170+
message("Touch screen to start AR session and look at the floor or walls");
171+
} else {
172+
message("AR Hit Test is not supported");
173+
}
174+
} else {
175+
message("Immersive AR is unavailable");
176+
}
177+
});
178+
179+
if (! app.xr.isAvailable(pc.XRTYPE_AR)) {
180+
message("Immersive AR is not available");
181+
} else if (! app.xr.hitTest.supported) {
182+
message("AR Hit Test is not supported");
183+
} else {
184+
message("Touch screen to start AR session and look at the floor or walls");
185+
}
186+
} else {
187+
message("WebXR is not supported");
188+
}
189+
</script>
190+
</body>
191+
</html>

src/callbacks.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,10 @@
177177
* @description Callback used by {@link pc.XrManager#endXr} and {@link pc.XrManager#startXr}.
178178
* @param {Error|null} err - The Error object or null if operation was successfull.
179179
*/
180+
181+
/**
182+
* @callback pc.callbacks.XrHitTestStart
183+
* @description Callback used by {@link pc.XrHitTest#start} and {@link pc.XrHitTest#startForInputSource}.
184+
* @param {Error|null} err - The Error object if failed to create hit test source or null.
185+
* @param {pc.XrHitTestSource|null} hitTestSource - object that provides access to hit results against real world geometry.
186+
*/

src/xr/xr-hit-test-source.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
Object.assign(pc, function () {
2+
var poolVec3 = [];
3+
var poolQuat = [];
4+
5+
6+
/**
7+
* @class
8+
* @name pc.XrHitTestSource
9+
* @augments pc.EventHandler
10+
* @classdesc Represents XR hit test source, which provides access to hit results of real world geometry from AR session.
11+
* @description Represents XR hit test source, which provides access to hit results of real world geometry from AR session.
12+
* @param {pc.XrManager} manager - WebXR Manager.
13+
* @param {object} xrHitTestSource - XRHitTestSource object that is created by WebXR API.
14+
* @param {boolean} transient - True if XRHitTestSource created for input source profile.
15+
* @example
16+
* hitTestSource.on('result', function (position, rotation) {
17+
* target.setPosition(position);
18+
* });
19+
*/
20+
var XrHitTestSource = function (manager, xrHitTestSource, transient) {
21+
pc.EventHandler.call(this);
22+
23+
this.manager = manager;
24+
this._xrHitTestSource = xrHitTestSource;
25+
this._transient = transient;
26+
};
27+
XrHitTestSource.prototype = Object.create(pc.EventHandler.prototype);
28+
XrHitTestSource.prototype.constructor = XrHitTestSource;
29+
30+
/**
31+
* @event
32+
* @name pc.XrHitTestSource#remove
33+
* @description Fired when {pc.XrHitTestSource} is removed.
34+
* @example
35+
* hitTestSource.once('remove', function () {
36+
* // hit test source has been removed
37+
* });
38+
*/
39+
40+
/**
41+
* @event
42+
* @name pc.XrHitTestSource#result
43+
* @description Fired when hit test source receives new results. It provides transform information that tries to match real world picked geometry.
44+
* @param {pc.Vec3} position - Position of hit test
45+
* @param {pc.Quat} rotation - Rotation of hit test
46+
* @param {pc.XrInputSource|null} inputSource - If is transient hit test source, then it will provide related input source
47+
* @example
48+
* hitTestSource.on('result', function (position, rotation, inputSource) {
49+
* target.setPosition(position);
50+
* target.setRotation(rotation);
51+
* });
52+
*/
53+
54+
/**
55+
* @function
56+
* @name pc.XrHitTestSource#remove
57+
* @description Stop and remove hit test source.
58+
*/
59+
XrHitTestSource.prototype.remove = function () {
60+
if (! this._xrHitTestSource)
61+
return;
62+
63+
var sources = this.manager.hitTest.sources;
64+
var ind = sources.indexOf(this);
65+
if (ind !== -1) sources.splice(ind, 1);
66+
67+
this.onStop();
68+
};
69+
70+
XrHitTestSource.prototype.onStop = function () {
71+
this._xrHitTestSource.cancel();
72+
this._xrHitTestSource = null;
73+
74+
this.fire('remove');
75+
this.manager.hitTest.fire('remove', this);
76+
};
77+
78+
XrHitTestSource.prototype.update = function (frame) {
79+
if (this._transient) {
80+
var transientResults = frame.getHitTestResultsForTransientInput(this._xrHitTestSource);
81+
for (var i = 0; i < transientResults.length; i++) {
82+
var transientResult = transientResults[i];
83+
var inputSource;
84+
85+
if (transientResult.inputSource)
86+
inputSource = this.manager.input._getByInputSource(transientResult.inputSource);
87+
88+
this.updateHitResults(transientResult.results, inputSource);
89+
}
90+
} else {
91+
this.updateHitResults(frame.getHitTestResults(this._xrHitTestSource));
92+
}
93+
};
94+
95+
XrHitTestSource.prototype.updateHitResults = function (results, inputSource) {
96+
for (var i = 0; i < results.length; i++) {
97+
var pose = results[i].getPose(this.manager._referenceSpace);
98+
99+
var position = poolVec3.pop();
100+
if (! position) position = new pc.Vec3();
101+
position.copy(pose.transform.position);
102+
103+
var rotation = poolQuat.pop();
104+
if (! rotation) rotation = new pc.Quat();
105+
rotation.copy(pose.transform.orientation);
106+
107+
this.fire('result', position, rotation, inputSource);
108+
this.manager.hitTest.fire('result', this, position, rotation, inputSource);
109+
110+
poolVec3.push(position);
111+
poolQuat.push(rotation);
112+
}
113+
};
114+
115+
116+
return { XrHitTestSource: XrHitTestSource };
117+
}());

0 commit comments

Comments
 (0)