@egjs/view3d
Version:
Fast & Customizable glTF 3D model viewer, packed with full of features!
295 lines (239 loc) • 9.8 kB
text/typescript
/*
* Copyright (c) 2020 NAVER Corp.
* egjs projects are licensed under the MIT license
*/
import * as THREE from "three";
import Motion from "../../core/Motion";
import * as DEFAULT from "../../const/default";
import { EASING } from "../../const/external";
import { XRRenderContext, XRInputs } from "../../type/xr";
import ARControl from "./ARControl";
enum STATE {
WAITING,
TRANSLATING,
BOUNCING,
}
/**
* Options for {@link ARTranslateControl}
* @interface
* @property {number} [threshold=0.05] Threshold until translation works, this value is relative to screen size.
* @property {number} [hoverHeight=0.1] How much model will float from the floor, in meter. Default value is 0.1(10cm).
* @property {number} [bounceDuration=1000] Bounce-to-floor animation's duration, in milisecond.
* @property {number} [bounceEasing=EASING.EASE_OUT_BOUNCE] Bounce-to-floor animation's easing function.
*/
export interface ARTranslateControlOptions {
threshold: number;
hoverHeight: number;
bounceDuration: number;
bounceEasing: (x: number) => number;
}
/**
* Model's translation(position) control for {@link WebARControl}
*/
class ARTranslateControl implements ARControl {
// Options
private _hoverHeight: number;
// Internal states
private _hoverPosition = new THREE.Vector3();
private _floorPosition = new THREE.Vector3();
private _wallRotation = new THREE.Quaternion();
private _dragPlane = new THREE.Plane();
private _enabled = false;
private _vertical = false;
private _state = STATE.WAITING;
private _initialPos = new THREE.Vector2();
private _bounceMotion: Motion;
/**
* Whether this control is enabled or not
* @readonly
*/
public get enabled() { return this._enabled; }
/**
* Last detected floor position
* @readonly
*/
public get floorPosition() { return this._floorPosition.clone(); }
/**
* How much model will float from the floor, in meter.
*/
public get hoverHeight() { return this._hoverHeight; }
public set hoverHeight(val: number) { this._hoverHeight = val; }
/**
* Create new instance of ARTranslateControl
* @param {ARTranslateControlOption} [options={}] Options
*/
public constructor({
hoverHeight = 0.1,
bounceDuration = 1000,
bounceEasing = EASING.EASE_OUT_BOUNCE
}: Partial<ARTranslateControlOptions> = {}) {
this._hoverHeight = hoverHeight;
this._bounceMotion = new Motion({
duration: bounceDuration,
easing: bounceEasing,
range: DEFAULT.INFINITE_RANGE
});
}
/**
* Enable this control
*/
public enable() {
this._enabled = true;
}
/**
* Disable this control
*/
public disable() {
this.deactivate();
this._enabled = false;
}
public activate() {
if (!this._enabled) return;
const dragPlane = this._dragPlane;
dragPlane.constant = this._calcDragPlaneConstant(this._floorPosition);
this._state = STATE.TRANSLATING;
}
public deactivate() {
if (!this._enabled || this._vertical || this._state === STATE.WAITING) {
this._state = STATE.WAITING;
return;
}
this._state = STATE.BOUNCING;
const floorPosition = this._floorPosition;
const hoverPosition = this._hoverPosition;
const bounceMotion = this._bounceMotion;
const hoveringAmount = hoverPosition.y - floorPosition.y;
bounceMotion.reset(hoveringAmount);
bounceMotion.setEndDelta(-hoveringAmount);
}
public init(position: THREE.Vector3, rotation: THREE.Quaternion, vertical: boolean) {
this._floorPosition.copy(position);
this._hoverPosition.copy(position);
const planeNormal = vertical
? new THREE.Vector3(0, 1, 0).applyQuaternion(rotation)
: new THREE.Vector3(0, 1, 0);
this._dragPlane.normal.copy(planeNormal);
this._wallRotation.copy(rotation);
this._vertical = vertical;
}
public setInitialPos(coords: THREE.Vector2[]) {
this._initialPos.copy(coords[0]);
}
public process({ frame, referenceSpace, xrCam }: XRRenderContext, { hitResults }: XRInputs) {
const state = this._state;
const notActive = state === STATE.WAITING || state === STATE.BOUNCING;
if (!hitResults || hitResults.length !== 1 || notActive) return;
const hitResult = hitResults[0];
const prevFloorPosition = this._floorPosition.clone();
const floorPosition = this._floorPosition;
const hoverPosition = this._hoverPosition;
const hoverHeight = this._hoverHeight;
const dragPlane = this._dragPlane;
const vertical = this._vertical;
const hitPose = hitResult.results[0] && hitResult.results[0].getPose(referenceSpace);
const hitMatrix = hitPose && new THREE.Matrix4().fromArray(hitPose.transform.matrix);
const isFloorHit = hitPose && hitMatrix.elements[5] > 0.75;
const isWallHit = hitPose && hitMatrix.elements[5] < 0.25;
const camPos = new THREE.Vector3().setFromMatrixPosition(xrCam.matrixWorld);
const hitPosition = hitPose && new THREE.Vector3().setFromMatrixPosition(hitMatrix);
if (!vertical) {
if (frame && (!hitPose || !isFloorHit)) {
// Use previous drag plane if no hit plane is found
const targetRayPose = frame.getPose(hitResult.inputSource.targetRaySpace, referenceSpace);
if (!targetRayPose) return;
const rayPos = targetRayPose.transform.position;
const fingerPos = new THREE.Vector3(rayPos.x, rayPos.y, rayPos.z);
const fingerDir = fingerPos.sub(camPos).normalize();
const fingerRay = new THREE.Ray(camPos, fingerDir);
const intersection = fingerRay.intersectPlane(dragPlane, new THREE.Vector3());
if (intersection) {
floorPosition.copy(intersection);
floorPosition.setY(prevFloorPosition.y);
hoverPosition.copy(intersection);
}
return;
}
// Set new floor level when it's increased at least 10cm
const currentDragPlaneHeight = -dragPlane.constant;
const hitDragPlaneHeight = hitPosition.y + hoverHeight;
if (hitDragPlaneHeight - currentDragPlaneHeight > 0.1) {
dragPlane.constant = -hitDragPlaneHeight;
}
const camToHitDir = new THREE.Vector3().subVectors(hitPosition, camPos).normalize();
const camToHitRay = new THREE.Ray(camPos, camToHitDir);
const hitOnDragPlane = camToHitRay.intersectPlane(dragPlane, new THREE.Vector3());
if (!hitOnDragPlane) return;
floorPosition.copy(hitOnDragPlane);
floorPosition.setY(hitPosition.y);
hoverPosition.copy(hitOnDragPlane);
} else {
if (frame && (!hitPose || !isWallHit)) {
// Use previous drag plane if no hit plane is found
const targetRayPose = frame.getPose(hitResult.inputSource.targetRaySpace, referenceSpace);
if (!targetRayPose) return;
const rayPos = targetRayPose.transform.position;
const fingerPos = new THREE.Vector3(rayPos.x, rayPos.y, rayPos.z);
const fingerDir = fingerPos.sub(camPos).normalize();
const fingerRay = new THREE.Ray(camPos, fingerDir);
const intersection = fingerRay.intersectPlane(dragPlane, new THREE.Vector3());
if (intersection) {
floorPosition.copy(intersection);
}
return;
}
const globalUp = new THREE.Vector3(0, 1, 0);
const hitOrientation = hitPose.transform.orientation;
const wallNormal = globalUp.clone()
.applyQuaternion(new THREE.Quaternion(hitOrientation.x, hitOrientation.y, hitOrientation.z, hitOrientation.w))
.normalize();
const wallX = new THREE.Vector3().crossVectors(new THREE.Vector3(0, 1, 0), wallNormal);
// Update rotation if it differs more than 10deg
const prevWallNormal = new THREE.Vector3(0, 1, 0).applyQuaternion(this._wallRotation).normalize();
if (Math.acos(Math.abs(prevWallNormal.dot(wallNormal))) >= Math.PI / 18) {
const wallMatrix = new THREE.Matrix4().makeBasis(wallX, globalUp, wallNormal);
const wallEuler = new THREE.Euler(0, 0, 0, "YXZ").setFromRotationMatrix(wallMatrix);
wallEuler.z = 0;
wallEuler.x = Math.PI / 2;
this._wallRotation.setFromEuler(wallEuler);
dragPlane.normal.copy(new THREE.Vector3(0, 1, 0).applyQuaternion(this._wallRotation));
dragPlane.constant = this._calcDragPlaneConstant(hitPosition);
}
const camToHitDir = new THREE.Vector3().subVectors(hitPosition, camPos).normalize();
const camToHitRay = new THREE.Ray(camPos, camToHitDir);
const hitOnDragPlane = camToHitRay.intersectPlane(dragPlane, new THREE.Vector3());
if (!hitOnDragPlane) return;
floorPosition.copy(hitOnDragPlane);
}
}
public update({ scene }: XRRenderContext, delta: number) {
const state = this._state;
const floorPosition = this._floorPosition;
const hoverPosition = this._hoverPosition;
const bounceMotion = this._bounceMotion;
const vertical = this._vertical;
if (state === STATE.BOUNCING) {
bounceMotion.update(delta);
hoverPosition.setY(floorPosition.y + bounceMotion.val);
if (bounceMotion.progress >= 1) {
this._state = STATE.WAITING;
}
}
scene.setRootPosition(floorPosition);
if (!vertical) {
scene.setModelHovering(hoverPosition.y - floorPosition.y);
} else {
scene.setWallRotation(this._wallRotation);
}
}
private _calcDragPlaneConstant(floor: THREE.Vector3) {
const vertical = this._vertical;
const dragPlaneNormal = this._dragPlane.normal.clone();
const dragPlaneAtZero = new THREE.Plane(dragPlaneNormal, 0);
const hoverHeight = vertical
? 0
: this._hoverHeight;
const dragPlaneConstant = -(dragPlaneAtZero.distanceToPoint(floor) + hoverHeight);
return dragPlaneConstant;
}
}
export default ARTranslateControl;