UNPKG

@ar-js-org/ar.js-threejs

Version:

AR.js (THREEx, ARjs) modular package - Typescript version

470 lines (409 loc) 19.3 kB
import { ArBaseControls } from "./ArBaseControls"; import { Matrix4, Matrix4Tuple, Object3D } from "three"; import Worker from "worker-loader?inline=no-fallback!./Worker"; import { setParameters } from "./common-functions/utilityFunctions"; import { IArMarkerControls, IArMarkerControlsParameters, IArToolkitContext } from "./CommonInterfaces/THREEx-interfaces"; import jsartoolkit from "@ar-js-org/artoolkit5-js"; const { ARToolkit } = jsartoolkit; declare global { var arMarkerControls: IArMarkerControls; } /** * ArMarkerControls class. This is the class where you can set up the marker * and attach it to a Three.js Object3D * @class ArMarkerControls */ export class ArMarkerControls extends ArBaseControls implements IArMarkerControls { private context: IArToolkitContext; public parameters: IArMarkerControlsParameters; private smoothMatrices: any[]; private onGetMarker: Function; private className: string; declare public object3d: Object3D; /** * ArMarkerControls constructor, needs context, object3d and a bunch of parameters. * @constructor * @param {IArToolkitContext} context * @param {Object3D} object3d * @param {IArMarkerControlsParameters} parameters */ constructor(context: IArToolkitContext, object3d: Object3D, parameters: IArMarkerControlsParameters) { super(object3d) var _this = this; this.className = "ArMarkerControls"; this.context = context; // handle default parameters this.parameters = { // size of the marker in meter size: 1, // type of marker - ['pattern', 'barcode', 'nft', 'unknown' ] type: "unknown", // url of the pattern - IIF type='pattern' patternUrl: null, // value of the barcode - IIF type='barcode' barcodeValue: null, // url of the descriptors of image - IIF type='nft' descriptorsUrl: null, // change matrix mode - [modelViewMatrix, cameraTransformMatrix] changeMatrixMode: "modelViewMatrix", // minimal confidence in the marke recognition - between [0, 1] - default to 1 minConfidence: 0.6, // turn on/off camera smoothing smooth: false, // number of matrices to smooth tracking over, more = smoother but slower follow smoothCount: 5, // distance tolerance for smoothing, if smoothThreshold # of matrices are under tolerance, tracking will stay still smoothTolerance: 0.01, // threshold for smoothing, will keep still unless enough matrices are over tolerance smoothThreshold: 2, }; // sanity check var possibleValues = ["pattern", "barcode", "nft", "unknown"]; console.assert( possibleValues.indexOf(this.parameters.type) !== -1, "illegal value", this.parameters.type ); var possibleValues = ["modelViewMatrix", "cameraTransformMatrix"]; console.assert( possibleValues.indexOf(this.parameters.changeMatrixMode) !== -1, "illegal value", this.parameters.changeMatrixMode ); // create the marker Root this.object3d = object3d; this.object3d.matrixAutoUpdate = false; this.object3d.visible = false; ////////////////////////////////////////////////////////////////////////////// // setParameters ////////////////////////////////////////////////////////////////////////////// setParameters(parameters, this); if (this.parameters.smooth) { this.smoothMatrices = []; // last DEBOUNCE_COUNT modelViewMatrix } ////////////////////////////////////////////////////////////////////////////// // Code Separator ////////////////////////////////////////////////////////////////////////////// // add this marker to artoolkitsystem // TODO rename that .addMarkerControls context.addMarker(this); if (_this.context.parameters.trackingBackend === "artoolkit") { this._initArtoolkit(); } else console.assert(false); }; /** * dispose is used to dispose the marker and all objects associated. * @returns {void} void */ dispose(): void { if (this.context && this.context.arController) { this.context.arController.removeEventListener( "getMarker", this.onGetMarker ); } this.context.removeMarker(this); this.object3d = null; this.smoothMatrices = []; }; ////////////////////////////////////////////////////////////////////////////// // update controls with new modelViewMatrix ////////////////////////////////////////////////////////////////////////////// /** * When you actually got a new modelViewMatrix, you need to perfom a whole bunch * of things. it is done here. * @param {Matrix4} modelViewMatrix * @returns {boolean} renderReqd */ updateWithModelViewMatrix(modelViewMatrix: Matrix4) { var markerObject3D = this.object3d; // mark object as visible markerObject3D.visible = true; if (this.context.parameters.trackingBackend === "artoolkit") { // apply context._axisTransformMatrix - change artoolkit axis to match usual webgl one var tmpMatrix = new Matrix4().copy( this.context._artoolkitProjectionAxisTransformMatrix ); tmpMatrix.multiply(modelViewMatrix); modelViewMatrix.copy(tmpMatrix); } else { console.assert(false); } // change axis orientation on marker - artoolkit say Z is normal to the marker - ar.js say Y is normal to the marker var markerAxisTransformMatrix = new Matrix4().makeRotationX( Math.PI / 2 ); modelViewMatrix.multiply(markerAxisTransformMatrix); var renderReqd = false; // change markerObject3D.matrix based on parameters.changeMatrixMode if (this.parameters.changeMatrixMode === "modelViewMatrix") { if (this.parameters.smooth) { var sum, i, j, averages: any, // average values for matrix over last smoothCount exceedsAverageTolerance = 0; this.smoothMatrices.push(modelViewMatrix.elements.slice()); // add latest if (this.smoothMatrices.length < this.parameters.smoothCount + 1) { markerObject3D.matrix.copy(modelViewMatrix); // not enough for average } else { this.smoothMatrices.shift(); // remove oldest entry averages = []; for (i in modelViewMatrix.elements) { // loop over entries in matrix sum = 0; for (j in this.smoothMatrices) { // calculate average for this entry sum += this.smoothMatrices[j][i]; } averages[i] = sum / this.parameters.smoothCount; // check how many elements vary from the average by at least AVERAGE_MATRIX_TOLERANCE if ( Math.abs(averages[i] - modelViewMatrix.elements[i]) >= this.parameters.smoothTolerance ) { exceedsAverageTolerance++; } } // if moving (i.e. at least AVERAGE_MATRIX_THRESHOLD entries are over AVERAGE_MATRIX_TOLERANCE) if (exceedsAverageTolerance >= this.parameters.smoothThreshold) { // then update matrix values to average, otherwise, don't render to minimize jitter for (i in modelViewMatrix.elements) { modelViewMatrix.elements[i] = averages[i]; } markerObject3D.matrix.copy(modelViewMatrix); renderReqd = true; // render required in animation loop } } } else { markerObject3D.matrix.copy(modelViewMatrix); } } else if (this.parameters.changeMatrixMode === "cameraTransformMatrix") { markerObject3D.matrix.copy(modelViewMatrix).invert(); } else { console.assert(false); } // decompose - the matrix into .position, .quaternion, .scale markerObject3D.matrix.decompose( markerObject3D.position, markerObject3D.quaternion, markerObject3D.scale ); // dispatchEvent this.dispatchEvent({ type: "markerFound" }); return renderReqd; }; update(object3d: Object3D) { console.log("update not required", object3d) } ////////////////////////////////////////////////////////////////////////////// // utility functions ////////////////////////////////////////////////////////////////////////////// /** * Method to get the name of the marker. * @returns {string} */ name(): string { var name = ""; name += this.parameters.type; if (this.parameters.type === "pattern") { var url = this.parameters.patternUrl; var basename = url.replace(/^.*\//g, ""); name += " - " + basename; } else if (this.parameters.type === "barcode") { name += " - " + this.parameters.barcodeValue; } else if (this.parameters.type === "nft") { var url = this.parameters.descriptorsUrl; var basename = url.replace(/^.*\//g, ""); name += " - " + basename; } else { console.assert(false, "no .name() implemented for this marker controls"); } return name; }; ////////////////////////////////////////////////////////////////////////////// // init for Artoolkit ////////////////////////////////////////////////////////////////////////////// private _initArtoolkit() { var _this = this; var artoolkitMarkerId: any = null; var delayedInitTimerId = setInterval(() => { // check if arController is init var arController = _this.context.arController; if (arController === null) return; // stop looping if it is init clearInterval(delayedInitTimerId); delayedInitTimerId = null; // launch the _postInitArtoolkit postInit(); }, 1000 / 50); return; function postInit() { // check if arController is init var arController = _this.context.arController; console.assert(arController !== null); // start tracking this pattern if (_this.parameters.type === "pattern") { arController .loadMarker(_this.parameters.patternUrl) .then(function (markerId: any) { artoolkitMarkerId = markerId; arController.trackPatternMarkerId( artoolkitMarkerId, _this.parameters.size ); }); } else if (_this.parameters.type === "barcode") { artoolkitMarkerId = _this.parameters.barcodeValue; arController.trackBarcodeMarkerId( artoolkitMarkerId, _this.parameters.size ); } else if (_this.parameters.type === "nft") { // use workers as default handleNFT(_this.parameters.descriptorsUrl, arController); } else if (_this.parameters.type === "unknown") { artoolkitMarkerId = null; } else { console.log(false, "invalid marker type", _this.parameters.type); } // listen to the event arController.addEventListener("getMarker", function (event: { data: { type: any; marker: { idPatt: any; idMatrix: any; }; }; }) { //console.log(event); if ( event.data.type === ARToolkit.PATTERN_MARKER && _this.parameters.type === "pattern" ) { if (artoolkitMarkerId === null) return; if (event.data.marker.idPatt === artoolkitMarkerId) onMarkerFound(event); } else if ( event.data.type === ARToolkit.BARCODE_MARKER && _this.parameters.type === "barcode" ) { if (artoolkitMarkerId === null) return; if (event.data.marker.idMatrix === artoolkitMarkerId) onMarkerFound(event); } else if ( event.data.type === ARToolkit.UNKNOWN_MARKER && _this.parameters.type === "unknown" ) { onMarkerFound(event); } }); } function setMatrix(matrix: Matrix4, value: { [x: string]: number; }) { var array: number[] = []; for (var key in value) { //@ts-ignore array[key] = value[key]; } //@ts-ignore if (typeof matrix.elements.set === "function") { //@ts-ignore matrix.elements.set(array); } else { matrix.elements = [].slice.call(array) as Matrix4Tuple; } } function handleNFT(descriptorsUrl: any, arController: { canvas: { style: { clientWidth: string; clientHeight: string; }; width: number; height: number; getContext: (arg0: string, p: { willReadFrequently: boolean; alpha: boolean }) => any; }; cameraParam: any; }) { var worker = new Worker(); window.addEventListener("arjs-video-loaded", function (ev: any) { var video = ev.detail.component; var vw = video.clientWidth; var vh = video.clientHeight; var pscale = 320 / Math.max(vw, (vh / 3) * 4); const w = vw * pscale; const h = vh * pscale; const pw = Math.max(w, (h / 3) * 4); const ph = Math.max(h, (w / 4) * 3); const ox = (pw - w) / 2; const oy = (ph - h) / 2; arController.canvas.style.clientWidth = pw + "px"; arController.canvas.style.clientHeight = ph + "px"; arController.canvas.width = pw; arController.canvas.height = ph; var context_process = arController.canvas.getContext("2d", { alpha: false, willReadFrequently: true }); function process() { context_process.fillStyle = "black"; context_process.fillRect(0, 0, pw, ph); context_process.drawImage(video, 0, 0, vw, vh, ox, oy, w, h); var imageData = context_process.getImageData(0, 0, pw, ph); worker.postMessage({ type: "process", imagedata: imageData }, [ imageData.data.buffer, ]); } // initialize the worker worker.postMessage({ type: "init", pw: pw, ph: ph, marker: descriptorsUrl, param: arController.cameraParam, }); worker.onmessage = function (ev: any) { if (ev && ev.data && ev.data.type === "endLoading") { var loader = document.querySelector(".arjs-loader"); if (loader) { loader.remove(); } var endLoadingEvent = new Event("arjs-nft-loaded"); window.dispatchEvent(endLoadingEvent); } if (ev && ev.data && ev.data.type === "loaded") { var proj = JSON.parse(ev.data.proj); var ratioW = pw / w; var ratioH = ph / h; proj[0] *= ratioW; proj[4] *= ratioW; proj[8] *= ratioW; proj[12] *= ratioW; proj[1] *= ratioH; proj[5] *= ratioH; proj[9] *= ratioH; proj[13] *= ratioH; setMatrix(_this.object3d.matrix, proj); } if (ev && ev.data && ev.data.type === "markerInfos") { var nft = JSON.parse(ev.data.marker); var nftEvent = new CustomEvent("arjs-nft-init-data", { detail: { dpi: nft.dpi, width: nft.width, height: nft.height }, }); window.dispatchEvent(nftEvent); } if (ev && ev.data && ev.data.type === "found") { var matrix = JSON.parse(ev.data.matrix); onMarkerFound({ data: { type: ARToolkit.NFT_MARKER, matrix: matrix, msg: ev.data.type, }, }); _this.context.arController.showObject = true; } else { _this.context.arController.showObject = false; } process(); }; }); } function onMarkerFound(event: { data: any; }) { if ( event.data.type === ARToolkit.PATTERN_MARKER && event.data.marker.cfPatt < _this.parameters.minConfidence ) return; if ( event.data.type === ARToolkit.BARCODE_MARKER && event.data.marker.cfMatrix < _this.parameters.minConfidence ) return; var modelViewMatrix = new Matrix4().fromArray(event.data.matrix); _this.updateWithModelViewMatrix(modelViewMatrix); } } }