UNPKG

@wonderlandengine/components

Version:

Wonderland Engine's official component library.

225 lines 8.55 kB
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