@wonderlandengine/components
Version:
Wonderland Engine's official component library.
225 lines • 8.55 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Collider, CollisionComponent, Component, Emitter, Mesh, MeshAttribute, MeshComponent, MeshIndexType, } from '@wonderlandengine/api';
import { property } from '@wonderlandengine/api/decorators.js';
import { setXRRigidTransformLocal } from './utils/webxr.js';
// FIXME: earcut overrides default export, breaking our tests
import earcut from 'earcut';
const tempVec3 = new Float32Array(3);
/** Compute minimum and maxium extents of given list of contour points */
function extentsFromContour(out, points) {
if (points.length == 0)
return out;
let absMaxX = Math.abs(points[0].x);
let absMaxZ = Math.abs(points[0].z);
for (let i = 1; i < points.length; ++i) {
absMaxX = Math.max(absMaxX, Math.abs(points[i].x));
absMaxZ = Math.max(absMaxZ, Math.abs(points[i].z));
}
out[0] = absMaxX;
out[1] = 0;
out[2] = absMaxZ;
}
/** Check whether x lies between a and b */
function within(x, a, b) {
if (a > b)
return x < a && x > b;
return x > a && x < b;
}
/**
* Check whether given point on plane's bounding box is inside plane's polygon
*
* @param p 3D point in plane's local space, Y value is ignored, since it is assumed
* that the point was checked against the plane's bounding box.
* @param plane XRPlane that has `XRPlane.polygon`
* @returns `true` if the point lies on the plane
*/
export function isPointLocalOnXRPlanePolygon(p, plane) {
const points = plane.polygon;
if (points.length < 3)
return false;
/* Count ray intersections: even == inside, odd == outside */
const pX = p[0];
const pZ = p[2];
let intersections = 0;
for (let n = 0, l = points.length - 1; n < points.length; ++n) {
const aX = points[l].x;
const aZ = points[l].z;
const s = (points[n].z - aZ) / (points[n].x - aX);
const x = Math.abs((pZ - aZ) / s);
if (x >= 0.0 && x <= 1.0 && within(x + pX, aX, points[n].x))
++intersections;
l = n;
}
return (intersections & 1) == 0;
}
/**
* Check whether given point on plane's bounding box is inside plane's polygon
*
* @param p 3D point to test. It is assumed that the point was checked against
* the plane's bounding box beforehand.
* @param plane XRPlane that has `XRPlane.polygon`
* @returns `true` if the point lies on the plane
*/
export function isPointWorldOnXRPlanePolygon(object, p, plane) {
if (plane.polygon.length < 3)
return false;
isPointLocalOnXRPlanePolygon(object.transformPointInverseWorld(tempVec3, p), plane);
}
/**
* Create a plane mesh from a list of contour points
*
* @param engine Engine to create the mesh with
* @param points Contour points
* @param meshToUpdate Optional mesh to update instead of creating a new one.
*/
function planeMeshFromContour(engine, points, meshToUpdate = null) {
const vertexCount = points.length;
const vertices = new Float32Array(vertexCount * 2);
for (let i = 0, d = 0; i < vertexCount; ++i, d += 2) {
vertices[d] = points[i].x;
vertices[d + 1] = points[i].z;
}
const triangles = earcut(vertices);
const mesh = meshToUpdate ||
new Mesh(engine, {
vertexCount,
/* Assumption here that we will never have more than 256 points
* in the detected plane meshes! */
indexType: MeshIndexType.UnsignedByte,
indexData: triangles,
});
if (mesh.vertexCount !== vertexCount) {
console.warn('vertexCount of meshToUpdate did not match required vertexCount');
return mesh;
}
const positions = mesh.attribute(MeshAttribute.Position);
const textureCoords = mesh.attribute(MeshAttribute.TextureCoordinate);
const normals = mesh.attribute(MeshAttribute.Normal);
tempVec3[1] = 0;
for (let i = 0, s = 0; i < vertexCount; ++i, s += 2) {
tempVec3[0] = vertices[s];
tempVec3[2] = vertices[s + 1];
positions.set(i, tempVec3);
}
textureCoords?.set(0, vertices);
if (normals) {
tempVec3[0] = 0;
tempVec3[1] = 1;
tempVec3[2] = 0;
for (let i = 0; i < vertexCount; ++i) {
normals.set(i, tempVec3);
}
}
if (meshToUpdate)
mesh.update();
return mesh;
}
/**
* Generate meshes and collisions for XRPlanes using [WebXR Device API - Plane Detection](https://immersive-web.github.io/real-world-geometry/plane-detection.html).
*/
class PlaneDetection extends Component {
static TypeName = 'plane-detection';
/**
* Material to assign to created plane meshes or `null` if meshes should not be created.
*/
planeMaterial = null;
/**
* Collision mask to assign to newly created collision components or a negative value if
* collision components should not be created.
*/
collisionMask = -1;
/** Map of all planes and their last updated timestamps */
planes = new Map();
/** Objects generated for each XRPlane */
planeObjects = new Map();
/** Called when a plane starts tracking */
onPlaneFound = new Emitter();
/** Called when a plane stops tracking */
onPlaneLost = new Emitter();
update() {
if (!this.engine.xr?.frame)
return;
// @ts-ignore
if (this.engine.xr.frame.detectedPlanes === undefined) {
console.error('plane-detection: WebXR feature not available.');
this.active = false;
return;
}
// @ts-ignore
const detectedPlanes = this.engine.xr.frame.detectedPlanes;
for (const [plane, _] of this.planes) {
if (!detectedPlanes.has(plane)) {
this.#planeLost(plane);
}
}
detectedPlanes.forEach((plane) => {
if (this.planes.has(plane)) {
if (plane.lastChangedTime > this.planes.get(plane)) {
this.#planeUpdate(plane);
}
}
else {
this.#planeFound(plane);
}
this.#planeUpdatePose(plane);
});
}
#planeLost(plane) {
this.planes.delete(plane);
const o = this.planeObjects.get(plane);
this.onPlaneLost.notify(plane, o);
/* User might destroy the object */
if (o.objectId > 0)
o.destroy();
}
#planeFound(plane) {
this.planes.set(plane, plane.lastChangedTime);
const o = this.engine.scene.addObject(this.object);
this.planeObjects.set(plane, o);
if (this.planeMaterial) {
o.addComponent(MeshComponent, {
mesh: planeMeshFromContour(this.engine, plane.polygon),
material: this.planeMaterial,
});
}
if (this.collisionMask >= 0) {
extentsFromContour(tempVec3, plane.polygon);
tempVec3[1] = 0.025;
o.addComponent(CollisionComponent, {
group: this.collisionMask,
collider: Collider.Box,
extents: tempVec3,
});
}
this.onPlaneFound.notify(plane, o);
}
#planeUpdate(plane) {
this.planes.set(plane, plane.lastChangedTime);
const planeMesh = this.planeObjects.get(plane).getComponent(MeshComponent);
if (!planeMesh)
return;
planeMeshFromContour(this.engine, plane.polygon, planeMesh.mesh);
}
#planeUpdatePose(plane) {
const o = this.planeObjects.get(plane);
const pose = this.engine.xr.frame.getPose(plane.planeSpace, this.engine.xr.currentReferenceSpace);
if (!pose) {
o.active = false;
return;
}
setXRRigidTransformLocal(o, pose.transform);
}
}
__decorate([
property.material()
], PlaneDetection.prototype, "planeMaterial", void 0);
__decorate([
property.int()
], PlaneDetection.prototype, "collisionMask", void 0);
export { PlaneDetection };
//# sourceMappingURL=plane-detection.js.map