mind-ar
Version:
web augmented reality framework
330 lines (281 loc) • 9.7 kB
JavaScript
//const {Controller, UI} = window.MINDAR.FACE;
import {Controller,UI} from './index.js'
const THREE = AFRAME.THREE;
const needsDOMRefresh=document.readyState === 'complete'||document.readyState=='interactive';
//console.log("Registering custom AFRAME stuff. Needs Refresh:",needsDOMRefresh,document.readyState);
AFRAME.registerSystem('mindar-face-system', {
container: null,
video: null,
shouldFaceUser: true,
lastHasFace: false,
init: function() {
this.anchorEntities = [];
this.faceMeshEntities = [];
},
setup: function({uiLoading, uiScanning, uiError, filterMinCF, filterBeta}) {
this.ui = new UI({uiLoading, uiScanning, uiError});
this.filterMinCF = filterMinCF;
this.filterBeta = filterBeta;
},
registerFaceMesh: function(el) {
this.faceMeshEntities.push({el});
},
registerAnchor: function(el, anchorIndex) {
this.anchorEntities.push({el: el, anchorIndex});
},
start: function() {
this.ui.showLoading();
this.container = this.el.sceneEl.parentNode;
//this.__startVideo();
this._startVideo();
},
stop: function() {
this.pause();
const tracks = this.video.srcObject.getTracks();
tracks.forEach(function(track) {
track.stop();
});
this.video.remove();
},
switchCamera: function() {
this.shouldFaceUser = !this.shouldFaceUser;
this.stop();
this.start();
},
pause: function(keepVideo=false) {
if (!keepVideo) {
this.video.pause();
}
this.controller.stopProcessVideo();
},
unpause: function() {
this.video.play();
this.controller.processVideo(this.video);
},
// mock a video with an image
__startVideo: function() {
this.video = document.createElement("img");
this.video.onload = async () => {
this.video.videoWidth = this.video.width;
this.video.videoHeight = this.video.height;
await this._setupAR();
this._processVideo();
this.ui.hideLoading();
}
this.video.style.position = 'absolute'
this.video.style.top = '0px'
this.video.style.left = '0px'
this.video.style.zIndex = '-2'
this.video.src = "./assets/face1.jpeg";
this.container.appendChild(this.video);
},
_startVideo: function() {
this.video = document.createElement('video');
this.video.setAttribute('autoplay', '');
this.video.setAttribute('muted', '');
this.video.setAttribute('playsinline', '');
this.video.style.position = 'absolute'
this.video.style.top = '0px'
this.video.style.left = '0px'
this.video.style.zIndex = '-2'
this.container.appendChild(this.video);
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
this.el.emit("arError", {error: 'VIDEO_FAIL'});
this.ui.showCompatibility();
return;
}
navigator.mediaDevices.getUserMedia({audio: false, video: {
facingMode: (this.shouldFaceUser? 'face': 'environment'),
}}).then((stream) => {
this.video.addEventListener( 'loadedmetadata', async () => {
this.video.setAttribute('width', this.video.videoWidth);
this.video.setAttribute('height', this.video.videoHeight);
await this._setupAR();
this._processVideo();
this.ui.hideLoading();
});
this.video.srcObject = stream;
}).catch((err) => {
console.log("getUserMedia error", err);
this.el.emit("arError", {error: 'VIDEO_FAIL'});
});
},
_processVideo: function() {
this.controller.onUpdate = ({hasFace, estimateResult}) => {
if (hasFace && !this.lastHasFace) {
this.el.emit("targetFound");
}
if (!hasFace && this.lastHasFace) {
this.el.emit("targetLost");
}
this.lastHasFace = hasFace;
if (hasFace) {
const {faceMatrix} = estimateResult;
for (let i = 0; i < this.anchorEntities.length; i++) {
const landmarkMatrix = this.controller.getLandmarkMatrix(this.anchorEntities[i].anchorIndex);
this.anchorEntities[i].el.updateVisibility(true);
this.anchorEntities[i].el.updateMatrix(landmarkMatrix);
}
for (let i = 0; i < this.faceMeshEntities.length; i++) {
this.faceMeshEntities[i].el.updateVisibility(true);
this.faceMeshEntities[i].el.updateMatrix(faceMatrix);
}
} else {
for (let i = 0; i < this.anchorEntities.length; i++) {
this.anchorEntities[i].el.updateVisibility(false);
}
for (let i = 0; i < this.faceMeshEntities.length; i++) {
this.faceMeshEntities[i].el.updateVisibility(false);
}
}
}
this.controller.processVideo(this.video);
},
_setupAR: async function() {
this.controller = new Controller({
filterMinCF: this.filterMinCF,
filterBeta: this.filterBeta,
});
this._resize();
await this.controller.setup(this.video);
await this.controller.dummyRun(this.video);
const {fov, aspect, near, far} = this.controller.getCameraParams();
const camera = new THREE.PerspectiveCamera();
camera.fov = fov;
camera.aspect = aspect;
camera.near = near;
camera.far = far;
camera.updateProjectionMatrix();
const cameraEle = this.container.getElementsByTagName("a-camera")[0];
cameraEle.setObject3D('camera', camera);
cameraEle.setAttribute('camera', 'active', true);
for (let i = 0; i < this.faceMeshEntities.length; i++) {
this.faceMeshEntities[i].el.addFaceMesh(this.controller.createThreeFaceGeometry(THREE));
}
this._resize();
window.addEventListener('resize', this._resize.bind(this));
this.el.emit("arReady");
},
_resize: function() {
const video = this.video;
const container = this.container;
let vw, vh; // display css width, height
const videoRatio = video.videoWidth / video.videoHeight;
const containerRatio = container.clientWidth / container.clientHeight;
if (videoRatio > containerRatio) {
vh = container.clientHeight;
vw = vh * videoRatio;
} else {
vw = container.clientWidth;
vh = vw / videoRatio;
}
this.video.style.top = (-(vh - container.clientHeight) / 2) + "px";
this.video.style.left = (-(vw - container.clientWidth) / 2) + "px";
this.video.style.width = vw + "px";
this.video.style.height = vh + "px";
const sceneEl = container.getElementsByTagName("a-scene")[0];
sceneEl.style.top = this.video.style.top;
sceneEl.style.left = this.video.style.left;
sceneEl.style.width = this.video.style.width;
sceneEl.style.height = this.video.style.height;
}
});
AFRAME.registerComponent('mindar-face', {
dependencies: ['mindar-face-system'],
schema: {
autoStart: {type: 'boolean', default: true},
faceOccluder: {type: 'boolean', default: true},
uiLoading: {type: 'string', default: 'yes'},
uiScanning: {type: 'string', default: 'yes'},
uiError: {type: 'string', default: 'yes'},
filterMinCF: {type: 'number', default: -1},
filterBeta: {type: 'number', default: -1},
},
init: function() {
const arSystem = this.el.sceneEl.systems['mindar-face-system'];
if (this.data.faceOccluder) {
const faceOccluderMeshEntity = document.createElement('a-entity');
faceOccluderMeshEntity.setAttribute("mindar-face-default-face-occluder", true);
this.el.sceneEl.appendChild(faceOccluderMeshEntity);
}
arSystem.setup({
uiLoading: this.data.uiLoading,
uiScanning: this.data.uiScanning,
uiError: this.data.uiError,
filterMinCF: this.data.filterMinCF === -1? null: this.data.filterMinCF,
filterBeta: this.data.filterBeta === -1? null: this.data.filterBeta,
});
if (this.data.autoStart) {
this.el.sceneEl.addEventListener('renderstart', () => {
arSystem.start();
});
}
},
});
AFRAME.registerComponent('mindar-face-target', {
dependencies: ['mindar-face-system'],
schema: {
anchorIndex: {type: 'number'},
},
init: function() {
const arSystem = this.el.sceneEl.systems['mindar-face-system'];
arSystem.registerAnchor(this, this.data.anchorIndex);
const root = this.el.object3D;
root.visible = false;
root.matrixAutoUpdate = false;
},
updateVisibility(visible) {
this.el.object3D.visible = visible;
},
updateMatrix(matrix) {
const root = this.el.object3D;
root.matrix.set(...matrix);
}
});
AFRAME.registerComponent('mindar-face-occluder', {
init: function() {
const root = this.el.object3D;
this.el.addEventListener('model-loaded', () => {
this.el.getObject3D('mesh').traverse((o) => {
if (o.isMesh) {
const material = new THREE.MeshStandardMaterial({
colorWrite: false,
});
o.material = material;
}
});
});
},
});
AFRAME.registerComponent('mindar-face-default-face-occluder', {
init: function() {
const arSystem = this.el.sceneEl.systems['mindar-face-system'];
arSystem.registerFaceMesh(this);
const root = this.el.object3D;
root.matrixAutoUpdate = false;
},
updateVisibility(visible) {
this.el.object3D.visible = visible;
},
updateMatrix(matrix) {
const root = this.el.object3D;
root.matrix.set(...matrix);
},
addFaceMesh(faceGeometry) {
const material = new THREE.MeshBasicMaterial({colorWrite: false});
//const material = new THREE.MeshBasicMaterial({colorWrite: '#CCCCCC'});
const mesh = new THREE.Mesh(faceGeometry, material);
this.el.setObject3D('mesh', mesh);
},
});
/*
This is a hack.
If the user's browser has cached A-Frame,
then A-Frame will process the webpage *before* the system and components get registered.
Resulting in a blank page. This happens because module loading is deferred.
*/
/* if(needsDOMRefresh){
console.log("mindar-face-aframe::Refreshing DOM...")
document.body.innerHTML=document.body.innerHTML;
}
*/