Skip to content

Commit 38a7a31

Browse files
committed
optimize the performance (fps) of mesh detector in xrblocks
1 parent 2132916 commit 38a7a31

File tree

3 files changed

+212
-4
lines changed

3 files changed

+212
-4
lines changed

demos/ballpit/BallPit.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as THREE from 'three';
22
import * as xb from 'xrblocks';
33
import {palette} from 'xrblocks/addons/utils/Palette.js';
4+
import Stats from 'three/addons/libs/stats.module.js';
45

56
import {BallShooter} from './BallShooter.js';
67

@@ -30,19 +31,70 @@ export class BallPit extends xb.Script {
3031
this.lastBallCreatedTimeForController = new Map();
3132
this.pointer = new THREE.Vector2();
3233
this.velocity = new THREE.Vector3();
34+
35+
// Initialize Stats for FPS dashboard
36+
this.stats = new Stats();
37+
this.stats.dom.style.width = '80px';
38+
this.stats.dom.style.height = '48px';
39+
this.stats.showPanel(0); // 0: fps, 1: ms, 2: mb
40+
document.body.appendChild(this.stats.dom);
41+
42+
this.statsMesh = null;
3343
}
3444

3545
init() {
3646
xb.add(this);
47+
this.createStatsMesh();
3748
}
3849

3950
update() {
4051
super.update();
52+
53+
// Update stats
54+
this.stats.update();
55+
if (this.statsMesh && this.statsMesh.material.map) {
56+
this.statsMesh.material.map.needsUpdate = true;
57+
}
58+
59+
// Update stats mesh position to follow camera in XR mode
60+
if (this.statsMesh && xb.core.renderer.xr.isPresenting) {
61+
const camera = xb.core.renderer.xr.getCamera();
62+
if (camera && camera.cameras && camera.cameras.length > 0) {
63+
const xrCamera = camera.cameras[0];
64+
// Position stats mesh relative to camera
65+
const offset = new THREE.Vector3(-0.15, 0.15, -0.5);
66+
offset.applyQuaternion(xrCamera.quaternion);
67+
this.statsMesh.position.copy(xrCamera.position).add(offset);
68+
this.statsMesh.quaternion.copy(xrCamera.quaternion);
69+
this.statsMesh.rotateY(Math.PI / 6);
70+
}
71+
}
72+
4173
for (const controller of xb.core.input.controllers) {
4274
this.controllerUpdate(controller);
4375
}
4476
}
4577

78+
/**
79+
* Creates a 3D mesh to display the FPS stats in XR space.
80+
*/
81+
createStatsMesh() {
82+
const statsCanvas = this.stats.dom.children[0];
83+
const geometry = new THREE.PlaneGeometry(0.25, 0.15);
84+
const texture = new THREE.CanvasTexture(statsCanvas);
85+
const material = new THREE.MeshBasicMaterial({
86+
map: texture,
87+
transparent: true,
88+
opacity: 0.9,
89+
side: THREE.DoubleSide,
90+
});
91+
this.statsMesh = new THREE.Mesh(geometry, material);
92+
93+
this.statsMesh.position.set(-0.15, 0.15, -0.5);
94+
95+
this.add(this.statsMesh);
96+
}
97+
4698
// Adds hemisphere light for ambient lighting and directional light.
4799
addLights() {
48100
this.add(new THREE.HemisphereLight(0xbbbbbb, 0x888888, 3));

src/world/mesh/DetectedMesh.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,16 @@ export class DetectedMesh extends THREE.Mesh {
6060
);
6161
}
6262
}
63+
64+
dispose() {
65+
if (this.blendedWorld && this.collider) {
66+
this.blendedWorld.removeCollider(this.collider, false);
67+
this.collider = undefined;
68+
}
69+
if (this.blendedWorld && this.rigidBody) {
70+
this.blendedWorld.removeRigidBody(this.rigidBody);
71+
this.rigidBody = undefined;
72+
}
73+
this.geometry.dispose();
74+
}
6375
}

src/world/mesh/MeshDetector.ts

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {DetectedMesh} from './DetectedMesh';
55
import {MeshDetectionOptions} from './MeshDetectionOptions';
66
import {Physics} from '../../physics/Physics';
77

8-
const SEMANTIC_LABELS = ['Floor', 'Ceiling', 'Wall', 'Table'];
9-
const SEMANTIC_COLORS = [0x00ff00, 0xff0000, 0x0000ff, 0xffff00];
8+
const SEMANTIC_LABELS = ['floor', 'ceiling', 'wall'];
9+
const SEMANTIC_COLORS = [0x00ff00, 0xffff00, 0x0000ff];
1010

1111
// Wrapper around WebXR Mesh Detection API
1212
// https://immersive-web.github.io/real-world-meshing/
@@ -23,6 +23,23 @@ export class MeshDetector extends Script {
2323
private physics?: Physics;
2424
private defaultMaterial = new THREE.MeshBasicMaterial({visible: false});
2525

26+
// Optimization1: Camera culling constants
27+
private readonly kMaxViewDistance = 3.0;
28+
private readonly kFOVCosThreshold = 0.25;
29+
30+
// Optimization2: Limit the number of meshes processed per frame (Not used)
31+
private readonly MAX_MESHES_PER_FRAME = 100000; // Process only n meshes per frame
32+
33+
// Optimization3: Mesh update throttling (similar to ARCore reflection cube map in /usr/local/google/home/adamren/Desktop/xrlabs/arlabs/xrblocks/samples/lighting)
34+
private lastMeshUpdateTime = 0;
35+
private readonly MESH_UPDATE_INTERVAL_MS = 1000;
36+
37+
// Optimization4: Cleanup old meshes (ToDo: Not used)
38+
private meshLastSeenTime = new Map<XRMesh, number>();
39+
private readonly MAX_MESH_COUNT = 200; // Limit total mesh count
40+
private readonly MESH_CLEANUP_INTERVAL_MS = 5000; // Cleanup every 5 seconds
41+
private lastCleanupTime = 0;
42+
2643
override init({
2744
options,
2845
renderer,
@@ -62,19 +79,66 @@ export class MeshDetector extends Script {
6279
const meshes = frame?.detectedMeshes;
6380
if (!meshes) return;
6481

82+
// Throttle mesh updates to ~30fps while rendering continues at full rate
83+
const now = performance.now();
84+
const timeSinceLastUpdate = now - this.lastMeshUpdateTime;
85+
86+
if (timeSinceLastUpdate < this.MESH_UPDATE_INTERVAL_MS) {
87+
// Skip mesh update this frame - rendering continues without blocking
88+
return;
89+
}
90+
91+
this.lastMeshUpdateTime = now;
92+
93+
const referenceSpace = this.renderer.xr.getReferenceSpace();
94+
if (!referenceSpace) return;
95+
const {position: cameraPosition, forward: cameraForward} =
96+
this.getCameraInfo(frame, referenceSpace);
97+
6598
// Delete old meshes
6699
for (const [xrMesh, threeMesh] of this.xrMeshToThreeMesh.entries()) {
67100
if (!meshes.has(xrMesh)) {
68101
this.xrMeshToThreeMesh.delete(xrMesh);
69102
this.threeMeshToXrMesh.delete(threeMesh);
70-
threeMesh.geometry.dispose();
103+
threeMesh.dispose();
71104
this.remove(threeMesh);
72105
}
73106
}
74107

75-
// Add new meshes
108+
// Limit processing to avoid frame drops
109+
let processedCount = 0;
110+
// const limitedMeshes = Array.from(meshes).slice(0, this.MAX_MESHES_PER_FRAME);
111+
// const testMeshes = new Set(limitedMeshes);
112+
// Process meshes with camera culling BEFORE creating/updating them
76113
for (const xrMesh of meshes) {
114+
if (processedCount >= this.MAX_MESHES_PER_FRAME) break;
115+
116+
// Camera culling: only process visible meshes
117+
if (
118+
!this.shouldShowMeshInView(
119+
xrMesh,
120+
cameraPosition,
121+
cameraForward,
122+
frame,
123+
referenceSpace
124+
)
125+
) {
126+
// If mesh exists but is not visible, remove it for performance
127+
if (this.xrMeshToThreeMesh.has(xrMesh)) {
128+
const threeMesh = this.xrMeshToThreeMesh.get(xrMesh)!;
129+
this.xrMeshToThreeMesh.delete(xrMesh);
130+
this.threeMeshToXrMesh.delete(threeMesh);
131+
threeMesh.dispose();
132+
this.remove(threeMesh);
133+
}
134+
continue; // Skip this mesh - don't create or update it
135+
}
136+
137+
processedCount++;
138+
139+
// Only process meshes that pass camera culling
77140
if (!this.xrMeshToThreeMesh.has(xrMesh)) {
141+
// New mesh - create it
78142
const threeMesh = this.createMesh(frame, xrMesh);
79143
this.xrMeshToThreeMesh.set(xrMesh, threeMesh);
80144
this.threeMeshToXrMesh.set(threeMesh, xrMesh);
@@ -86,6 +150,7 @@ export class MeshDetector extends Script {
86150
);
87151
}
88152
} else {
153+
// Existing mesh - update vertices and pose
89154
const threeMesh = this.xrMeshToThreeMesh.get(xrMesh)!;
90155
threeMesh.updateVertices(xrMesh);
91156
this.updateMeshPose(frame, xrMesh, threeMesh);
@@ -114,4 +179,83 @@ export class MeshDetector extends Script {
114179
mesh.quaternion.copy(pose.transform.orientation);
115180
}
116181
}
182+
183+
/**
184+
* Gets camera position and forward vector from XR frame.
185+
*/
186+
private getCameraInfo(
187+
frame: XRFrame,
188+
referenceSpace: XRReferenceSpace
189+
): {
190+
position: THREE.Vector3;
191+
forward: THREE.Vector3;
192+
} {
193+
const viewerPose = frame.getViewerPose(referenceSpace);
194+
const cameraPosition = new THREE.Vector3(0, 0, 0);
195+
let cameraForward = new THREE.Vector3(0, 0, -1);
196+
197+
if (viewerPose && viewerPose.views && viewerPose.views.length > 0) {
198+
// Get camera position from first view's transform
199+
const viewTransform = viewerPose.views[0].transform;
200+
const viewMatrix = new THREE.Matrix4().fromArray(viewTransform.matrix);
201+
cameraPosition.setFromMatrixPosition(viewMatrix);
202+
203+
// Extract forward vector from matrix (typically -Z axis)
204+
const forward = new THREE.Vector3(0, 0, -1);
205+
forward.applyMatrix4(viewMatrix);
206+
forward.sub(cameraPosition).normalize();
207+
cameraForward = forward;
208+
}
209+
210+
return {position: cameraPosition, forward: cameraForward};
211+
}
212+
213+
/**
214+
* Checks if mesh should be visible based on camera position and FOV.
215+
*/
216+
private shouldShowMeshInView(
217+
mesh: XRMesh,
218+
cameraPosition: THREE.Vector3,
219+
cameraForward: THREE.Vector3,
220+
frame: XRFrame,
221+
referenceSpace: XRReferenceSpace
222+
): boolean {
223+
const meshPose = frame.getPose(mesh.meshSpace, referenceSpace);
224+
if (!meshPose) {
225+
return true; // Default to showing if no pose available
226+
}
227+
228+
const meshMatrix = new THREE.Matrix4().fromArray(meshPose.transform.matrix);
229+
const meshPosition = new THREE.Vector3();
230+
meshPosition.setFromMatrixPosition(meshMatrix);
231+
232+
// Calculate distance
233+
const dx = meshPosition.x - cameraPosition.x;
234+
const dy = meshPosition.y - cameraPosition.y;
235+
const dz = meshPosition.z - cameraPosition.z;
236+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
237+
238+
if (distance > this.kMaxViewDistance) {
239+
return false;
240+
}
241+
242+
// Calculate direction vector and dot product (FOV check)
243+
if (distance > 0.001) {
244+
const invDistance = 1.0 / distance;
245+
const dirX = dx * invDistance;
246+
const dirY = dy * invDistance;
247+
const dirZ = dz * invDistance;
248+
249+
const dotForward =
250+
dirX * cameraForward.x +
251+
dirY * cameraForward.y +
252+
dirZ * cameraForward.z;
253+
254+
if (dotForward < this.kFOVCosThreshold) {
255+
return false;
256+
}
257+
}
258+
259+
return true;
260+
}
117261
}

0 commit comments

Comments
 (0)