Skip to content

Commit

Permalink
When placing a model in WebXR, the model now faces the viewer rather …
Browse files Browse the repository at this point in the history
…than the scene origin. Fixes #269
  • Loading branch information
jsantell committed Dec 13, 2018
1 parent f795233 commit 61aa2f5
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 30 deletions.
100 changes: 76 additions & 24 deletions src/test/three-components/ARRenderer-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* limitations under the License.
*/

import {Matrix4, Plane, Ray, Vector3} from 'three';

import {IS_AR_CANDIDATE} from '../../constants.js';
import ModelViewerElementBase, {$renderer, $scene} from '../../model-viewer-base.js';
import {ARRenderer} from '../../three-components/ARRenderer.js';
Expand All @@ -24,36 +26,56 @@ const expect = chai.expect;

customElements.define('model-viewer-element', ModelViewerElementBase);


const stubWebXrInterface = (arRenderer) => {
arRenderer.resolveARSession = () => {
class FakeSession extends EventTarget {
requestFrameOfReference() {
return {};
}

requestAnimationFrame() {
return 1;
}

cancelAnimationFrame() {
}

end() {
this.dispatchEvent(new CustomEvent('end'));
}
}

return new FakeSession();
};
};

suite('ARRenderer', () => {
let element;
let scene;
let renderer;
let arRenderer;

const stubWebXrInterface = (arRenderer) => {
const xzPlane = new Plane(new Vector3(0, 1, 0));
const mat4 = new Matrix4();
const vec3 = new Vector3();

arRenderer.resolveARSession = () => {
class FakeSession extends EventTarget {
requestFrameOfReference() {
return {};
}

/**
* Returns a hit if ray collides with the XZ plane
*/
requestHitTest(origin, dir, frameOfRef) {
const hits = [];
const ray = new Ray(new Vector3(...origin), new Vector3(...dir));
const success = ray.intersectPlane(xzPlane, vec3);

if (success) {
const hitMatrix = new Float32Array(16);
mat4.identity().setPosition(vec3).toArray(hitMatrix);
hits.push({hitMatrix});
}

return hits;
}

requestAnimationFrame() {
return 1;
}

cancelAnimationFrame() {
}

end() {
this.dispatchEvent(new CustomEvent('end'));
}
}

return new FakeSession();
};
};

setup(() => {
element = new ModelViewerElementBase();
renderer = element[$renderer];
Expand Down Expand Up @@ -111,5 +133,35 @@ suite('ARRenderer', () => {
expect(originalModelScale.z).to.be.equal(model.scale.z);
});
});

suite('placing a model', () => {
test('places the model oriented to the camera', async () => {
const epsilon = 0.0001;
const pivotRotation = 0.123;
modelScene.pivot.rotation.y = pivotRotation;

// Set camera to (10, 2, 0), rotated 180 degrees on Y (so
// our dolly will need to rotate to face camera) and angled 45
// degrees towards the ground, like someone holding a phone.
arRenderer.camera.matrix.identity()
.makeRotationAxis(new Vector3(0, 1, 0), Math.PI)
.multiply(new Matrix4().makeRotationAxis(new Vector3(1, 0, 0), -Math.PI / 4))
.setPosition(new Vector3(10, 2, 0));
arRenderer.camera.updateMatrixWorld(true);

await arRenderer.present(modelScene);
await arRenderer.placeModel();

expect(arRenderer.dolly.position.x).to.be.equal(10);
expect(arRenderer.dolly.position.y).to.be.equal(0);
expect(arRenderer.dolly.position.z).to.be.equal(2);
// Quaternion rotation results in the rotation towards the viewer
// with -X and -Z, and the offset applied to Y to invert pivotRotation,
// but it's inverted again here due to the -X/-Z rotation encoding
expect(arRenderer.dolly.rotation.x).to.be.equal(-Math.PI);
expect(arRenderer.dolly.rotation.y).to.be.closeTo(pivotRotation, epsilon);
expect(arRenderer.dolly.rotation.z).to.be.equal(-Math.PI);
});
});
});
});
13 changes: 7 additions & 6 deletions src/three-components/ARRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const $outputCanvas = Symbol('outputCanvas');
const $outputContext = Symbol('outputContext');

const $onWebXRFrame = Symbol('onWebXRFrame');
const $onOutputCanvasClick = Symbol('onOutputCanvasClick');
const $onFullscreenchange = Symbol('onFullscreenchange');
const $postSessionCleanup = Symbol('postSessionCleanup');

Expand Down Expand Up @@ -231,8 +230,7 @@ height: 100%;`);
// want to rely on XRInputSource and "select" XRInputSourceEvent (or their
// successors) to abstract these input details for us.
// @see https://immersive-web.github.io/webxr/#xrinputsource-interface
this[$outputCanvas].addEventListener(
'click', () => this[$onOutputCanvasClick]());
this[$outputCanvas].addEventListener('click', () => this.placeModel());
}

return this[$outputCanvas];
Expand All @@ -246,8 +244,7 @@ height: 100%;`);
return this[$outputContext];
}


async[$onOutputCanvasClick]() {
async placeModel() {
if (this[$currentSession] == null) {
return;
}
Expand All @@ -272,7 +269,11 @@ height: 100%;`);
const hitMatrix = matrix4.fromArray(hit.hitMatrix);

this.dolly.position.setFromMatrixPosition(hitMatrix);
this.dolly.rotation.set(0, -presentedScene.pivot.rotation.y, 0);

// Orient the dolly/model to face the camera
const camPosition = vector3.setFromMatrixPosition(this.camera.matrix);
this.dolly.lookAt(camPosition.x, this.dolly.position.y, camPosition.z);
this.dolly.rotateY(-presentedScene.pivot.rotation.y);

presentedScene.skysphere.visible = false;
this.dolly.add(presentedScene);
Expand Down

0 comments on commit 61aa2f5

Please sign in to comment.