Needle Engine

Changes between version 3.19.9 and 3.20.0
Files changed (6) hide show
  1. src/engine/engine_context.ts +29 -21
  2. src/engine/engine_input.ts +7 -2
  3. src/engine/engine_physics_rapier.ts +12 -2
  4. src/engine/engine_types.ts +6 -0
  5. src/engine-components/export/gltf/GltfExport.ts +8 -2
  6. src/engine-components/webxr/WebXRPlaneTracking.ts +156 -61
src/engine/engine_context.ts CHANGED
@@ -131,6 +131,14 @@
131
131
  Context._defaultTargetFramerate.value = val;
132
132
  }
133
133
 
134
+ private static _defaultWebglRendererParameters: WebGLRendererParameters = {
135
+ antialias: true,
136
+ alpha: false,
137
+ };
138
+ static get DefaultWebGLRendererParameters(): WebGLRendererParameters {
139
+ return Context._defaultWebglRendererParameters;
140
+ }
141
+
134
142
  /** the needle engine version */
135
143
  get version() {
136
144
  return VERSION;
@@ -324,10 +332,6 @@
324
332
  this.renderer = args.renderer;
325
333
  this.isManagedExternally = true;
326
334
  }
327
- else {
328
- this.createRenderer();
329
- }
330
-
331
335
  if (args?.runInBackground !== undefined) this.runInBackground = args.runInBackground;
332
336
  if (args?.scene) this.scene = args.scene;
333
337
  else this.scene = new Scene();
@@ -361,15 +365,16 @@
361
365
  ContextRegistry.register(this);
362
366
  }
363
367
 
364
- private createRenderer() {
368
+ private createNewRenderer() {
365
369
  this.renderer?.dispose();
366
370
 
367
- const params = this.getWebGLRendererParameters();
371
+ const params = Context.DefaultWebGLRendererParameters;
372
+ if (!params.canvas) {
373
+ // get canvas already configured in the Needle Engine Web Component
374
+ const canvas = this.domElement?.shadowRoot?.querySelector("canvas");
375
+ if (canvas) params.canvas = canvas;
376
+ }
368
377
 
369
- // get canvas already configured in the Needle Engine Web Component
370
- const canvas = this.domElement?.shadowRoot?.querySelector("canvas");
371
- if (canvas) params.canvas = canvas;
372
-
373
378
  this.renderer = new WebGLRenderer(params);
374
379
 
375
380
  this.renderer.debug.checkShaderErrors = isDevEnvironment() || getParam("checkshadererrors") === true;
@@ -389,14 +394,7 @@
389
394
  this.renderer.useLegacyLights = false;
390
395
  }
391
396
 
392
- private getWebGLRendererParameters(): WebGLRendererParameters {
393
- return {
394
- antialias: true,
395
- alpha: false,
396
- };
397
- }
398
397
 
399
-
400
398
  private _intersectionObserver: IntersectionObserver | null = null;
401
399
  private internalOnUpdateVisible() {
402
400
  this._intersectionObserver?.disconnect();
@@ -470,9 +468,11 @@
470
468
  this.physics?.engine?.clearCaches();
471
469
 
472
470
  if (!this.isManagedExternally) {
473
- this.renderer.renderLists.dispose();
474
- this.renderer.state.reset();
475
- this.renderer.resetState();
471
+ if (this.renderer) {
472
+ this.renderer.renderLists.dispose();
473
+ this.renderer.state.reset();
474
+ this.renderer.resetState();
475
+ }
476
476
  }
477
477
  // We do not want to clear the renderer here because when switching src we want to keep the last rendered frame in case the loading screen is not visible
478
478
  // if a user wants to see the background they can still call setClearAlpha(0) and clear manually
@@ -661,8 +661,10 @@
661
661
  this.clear();
662
662
  // stop the animation loop if its running during creation
663
663
  // since we do not want to start enabling scripts etc before they are deserialized
664
- if (this.isManagedExternally === false)
664
+ if (this.isManagedExternally === false) {
665
+ this.createNewRenderer();
665
666
  this.renderer?.setAnimationLoop(null);
667
+ }
666
668
 
667
669
  await delay(1);
668
670
 
@@ -764,6 +766,12 @@
764
766
  Context.Current = this;
765
767
  looputils.processNewScripts(this);
766
768
 
769
+ // We have to step once so that colliders that have been created in onEnable can be raycasted in start
770
+ if (this.physics.engine) {
771
+ this.physics.engine?.step(0);
772
+ this.physics.engine?.postStep();
773
+ }
774
+
767
775
  // const mainCam = this.mainCameraComponent as Camera;
768
776
  // if (mainCam) {
769
777
  // mainCam.applyClearFlagsIfIsActiveCamera();
src/engine/engine_input.ts CHANGED
@@ -633,8 +633,13 @@
633
633
  lf.copy(this._pointerPositions[evt.button]);
634
634
  // accumulate delta (it's reset in end of frame), if we just write it here it's not correct when the browser console is open
635
635
  const delta = this._pointerPositionsDelta[evt.button];
636
- const dx = evt.clientX - lf.x;
637
- const dy = evt.clientY - lf.y;
636
+ let dx = evt.clientX - lf.x;
637
+ let dy = evt.clientY - lf.y;
638
+ // if pointer is locked, clientX and Y are not changed, but Movement is.
639
+ if(dx === 0 && evt.movementX !== 0)
640
+ dx = evt.movementX || 0;
641
+ if(dy === 0 && evt.movementY !== 0)
642
+ dy = evt.movementY || 0;
638
643
  delta.x += dx;
639
644
  delta.y += dy;
640
645
 
src/engine/engine_physics_rapier.ts CHANGED
@@ -234,7 +234,7 @@
234
234
  await RAPIER.init()
235
235
  }
236
236
  if (debugPhysics) console.log("Physics engine initialized, creating world...");
237
- this.world = new World(this._gravity);
237
+ this._world = new World(this._gravity);
238
238
  this.enabled = true;
239
239
  this._isInitialized = true;
240
240
  if (debugPhysics) console.log("Physics world created");
@@ -427,6 +427,8 @@
427
427
  // physics simulation
428
428
 
429
429
  enabled: boolean = false;
430
+ /** Get access to the rapier world */
431
+ public get world(): World | undefined { return this._world };
430
432
 
431
433
  private _tempPosition: Vector3 = new Vector3();
432
434
  private _tempQuaternion: Quaternion = new Quaternion();
@@ -439,7 +441,7 @@
439
441
  get isUpdating(): boolean { return this._isUpdatingPhysicsWorld; }
440
442
 
441
443
 
442
- private world?: World;
444
+ private _world?: World;
443
445
  private _hasCreatedWorld: boolean = false;
444
446
  private eventQueue?: EventQueue;
445
447
  private collisionHandler?: PhysicsCollisionHandler;
@@ -593,12 +595,20 @@
593
595
  }
594
596
  }
595
597
 
598
+ /** Get the rapier body for a Needle component */
596
599
  getBody(obj: ICollider | IRigidbody): null | any {
597
600
  if (!obj) return null;
598
601
  const body = obj[$bodyKey];
599
602
  return body;
600
603
  }
601
604
 
605
+ /** Get the Needle Engine component for a rapier object */
606
+ getComponent(rapierObject:object) : IComponent | null {
607
+ if(!rapierObject) return null;
608
+ const component = rapierObject[$componentKey];
609
+ return component;
610
+ }
611
+
602
612
  private createCollider(collider: ICollider, desc: ColliderDesc, center?: Vector3) {
603
613
  if (!this.world) throw new Error("Physics world not initialized");
604
614
  const matrix = this._tempMatrix;
src/engine/engine_types.ts CHANGED
@@ -395,9 +395,15 @@
395
395
  clearCaches();
396
396
 
397
397
  enabled: boolean;
398
+ get world(): any;
398
399
 
399
400
  set gravity(vec3: Vec3);
400
401
  get gravity(): Vec3;
402
+
403
+ /** Get the rapier body for a Needle component */
404
+ getBody(obj: ICollider | IRigidbody): null | any;
405
+ /** Get the Needle Engine component for a rapier object */
406
+ getComponent(rapierObject:object) : IComponent | null;
401
407
 
402
408
  // raycasting
403
409
  /** Fast raycast against physics colliders
src/engine-components/export/gltf/GltfExport.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import { Object3D, Vector3 } from "three";
2
+ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
3
+
1
4
  import { Behaviour, GameObject } from "../../Component.js";
2
- import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
3
5
  import GLTFMeshGPUInstancingExtension from '../../../include/three/EXT_mesh_gpu_instancing_exporter.js';
4
6
  import { Renderer } from "../../Renderer.js";
5
- import { Object3D, Vector3 } from "three";
6
7
  import { SerializationContext } from "../../../engine/engine_serialization_core.js";
8
+ import { serializable } from "../../../engine/engine_serialization_decorator.js";
7
9
  import { NEEDLE_components } from "../../../engine/extensions/NEEDLE_components.js";
8
10
  import { getWorldPosition } from "../../../engine/engine_three_utils.js";
9
11
  import { BoxHelperComponent } from "../../BoxHelperComponent.js";
@@ -36,7 +38,11 @@
36
38
  }
37
39
 
38
40
  export class GltfExport extends Behaviour {
41
+
42
+ @serializable()
39
43
  binary: boolean = true;
44
+
45
+ @serializable(Object3D)
40
46
  objects: Object3D[] = [];
41
47
 
42
48
  private exporter?: GLTFExporter;
src/engine-components/webxr/WebXRPlaneTracking.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BufferAttribute, BufferGeometry, Group, Mesh, Object3D, Vector3 } from "three";
1
+ import { BufferAttribute, BufferGeometry, Group, Material, Mesh, Object3D, TypedArray, Vector3 } from "three";
2
2
  import { MeshCollider } from "../Collider.js";
3
3
  import { Behaviour, GameObject } from "../Component.js";
4
4
  import { WebXR, WebXREvent } from "./WebXR.js";
@@ -9,13 +9,22 @@
9
9
 
10
10
  const debug = getParam("debugplanetracking");
11
11
 
12
+ declare type XRMesh = {
13
+ meshSpace: XRSpace;
14
+ lastChangedTime: number;
15
+ vertices: Float32Array;
16
+ indices: Uint32Array;
17
+ semanticLabel?: string;
18
+ }
19
+
12
20
  declare type XRFramePlanes = XRFrame & {
13
21
  detectedPlanes?: Set<XRPlane>;
22
+ detectedMeshes?: Set<XRMesh>;
14
23
  }
15
24
 
16
25
  export declare type XRPlaneContext = {
17
26
  id: number;
18
- xrPlane: XRPlane;
27
+ xrData: XRPlane | XRMesh;
19
28
  timestamp: number;
20
29
  mesh?: Mesh | Group;
21
30
  collider?: MeshCollider;
@@ -28,13 +37,21 @@
28
37
 
29
38
  export class WebXRPlaneTracking extends Behaviour {
30
39
 
31
- /** Optional: if assigned it will be instantiated per tracked plane */
40
+ /** Optional: if assigned it will be instantiated per tracked plane/tracked mesh */
32
41
  @serializable(Object3D)
33
- planeTemplate?: Object3D;
42
+ dataTemplate?: Object3D;
43
+
34
44
  @serializable()
35
- initiateRoomCaptureIfNoPlanes = true;
45
+ initiateRoomCaptureIfNoData = true;
36
46
 
47
+ @serializable()
48
+ usePlaneData: boolean = true;
49
+
50
+ @serializable()
51
+ useMeshData: boolean = true;
52
+
37
53
  get trackedPlanes() { return this._allPlanes.values(); }
54
+ get trackedMeshes() { return this._allMeshes.values(); }
38
55
 
39
56
  onEnable(): void {
40
57
  WebXR.addEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
@@ -49,60 +66,40 @@
49
66
  private onModifyAROptions = (event: any) => {
50
67
  const options = event.detail;
51
68
  const features = options.optionalFeatures || [];
52
- if (!features.includes("plane-detection"))
69
+
70
+ if (this.usePlaneData && !features.includes("plane-detection"))
53
71
  features.push("plane-detection");
72
+ if (this.useMeshData && !features.includes("mesh-detection"))
73
+ features.push("mesh-detection");
74
+
54
75
  options.optionalFeatures = features;
55
76
  }
56
77
 
57
78
  private onXRUpdate = (evt) => {
58
- this.processPlanes(evt.rig, evt.frame);
59
- }
60
-
61
- private _planeId = 1;
62
- private readonly _allPlanes = new Map<XRPlane, XRPlaneContext>();
63
- private firstTimeNoPlanesDetected = -100;
64
-
65
- private processPlanes(rig: Object3D, frame: XRFramePlanes) {
66
- const renderer = this.context.renderer;
67
-
79
+
68
80
  // parenting tracked planes to the XR rig ensures that they synced with the real-world user data;
69
81
  // otherwise they would "swim away" when the user rotates / moves / teleports and so on.
70
82
  // There may be cases where we want that! E.g. a user walks around on their own table in castle builder
71
- if (!rig) return;
83
+ if (!evt.rig) return;
72
84
 
73
- // If we dont have the detectedPlanes field this means the user didnt add the option
74
- if (frame.detectedPlanes === undefined) return;
75
-
85
+ const frame = evt.frame as XRFramePlanes;
86
+ const renderer = this.context.renderer;
76
87
  const referenceSpace = renderer.xr.getReferenceSpace();
77
88
  if (!referenceSpace) return;
89
+
90
+ const planes = frame.detectedPlanes;
91
+ const meshes = frame.detectedMeshes;
92
+ const hasAnyPlanes = planes !== undefined && planes.size > 0;
93
+ const hasAnyMeshes = meshes !== undefined && meshes.size > 0;
78
94
 
79
- for (const plane of this._allPlanes.keys()) {
80
- if (!frame.detectedPlanes.has(plane)) {
81
- const planeContext = this._allPlanes.get(plane)!;
82
- // plane was removed
83
- this._allPlanes.delete(plane);
84
- if (debug) console.log("Plane no longer tracked, id=" + planeContext.id);
85
- if (planeContext.mesh)
86
- rig?.remove(planeContext.mesh);
87
-
88
- const evt = new CustomEvent<WebXRPlaneTrackingEvent>("plane-tracking", {
89
- detail: {
90
- type: "plane-removed",
91
- context: planeContext
92
- }
93
- })
94
- this.dispatchEvent(evt);
95
- }
96
- }
97
-
98
95
  // When no planes are found and we haven't already run the coroutine,
99
96
  // we start it and then wait for 2s before opening the settings.
100
97
  // This only works on Quest through a magic method on the frame,
101
98
  // see https://developer.oculus.com/documentation/web/webxr-mixed-reality/#:~:text=Because%20few%20people,once%20per%20session.
102
- if (this.initiateRoomCaptureIfNoPlanes) {
103
- if (frame.detectedPlanes.size == 0 && this.firstTimeNoPlanesDetected < -10)
99
+ if (this.initiateRoomCaptureIfNoData) {
100
+ if (!hasAnyPlanes && !hasAnyMeshes && this.firstTimeNoPlanesDetected < -10)
104
101
  this.firstTimeNoPlanesDetected = Date.now();
105
- if (frame.detectedPlanes.size > 0)
102
+ if (hasAnyPlanes || hasAnyMeshes)
106
103
  this.firstTimeNoPlanesDetected = -1; // we're done
107
104
  if (this.firstTimeNoPlanesDetected > 0 && Date.now() - this.firstTimeNoPlanesDetected > 2500) {
108
105
  if ("initiateRoomCapture" in frame.session) {
@@ -113,31 +110,91 @@
113
110
  }
114
111
  }
115
112
 
116
- for (const plane of frame.detectedPlanes) {
117
- const planePose = frame.getPose(plane.planeSpace, referenceSpace);
113
+ if (planes !== undefined)
114
+ this.processFrameData(evt.rig, evt.frame, planes, this._allPlanes);
118
115
 
116
+ if (meshes !== undefined)
117
+ this.processFrameData(evt.rig, evt.frame, meshes, this._allMeshes);
118
+ }
119
+
120
+
121
+
122
+ private _dataId = 1;
123
+ private readonly _allPlanes = new Map<XRPlane, XRPlaneContext>();
124
+ private readonly _allMeshes = new Map<XRMesh, XRPlaneContext>();
125
+ private firstTimeNoPlanesDetected = -100;
126
+
127
+ private processFrameData(rig: Object3D, frame: XRFramePlanes, detected: Set<XRPlane | XRMesh>, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
128
+ const renderer = this.context.renderer;
129
+ const referenceSpace = renderer.xr.getReferenceSpace();
130
+ if (!referenceSpace) return;
131
+
132
+ for (const data of _all.keys()) {
133
+ if (!detected.has(data)) {
134
+ const dataContext = _all.get(data)!;
135
+ // plane was removed
136
+ _all.delete(data);
137
+ if (debug) console.log("Plane no longer tracked, id=" + dataContext.id);
138
+ if (dataContext.mesh)
139
+ rig?.remove(dataContext.mesh);
140
+
141
+ const evt = new CustomEvent<WebXRPlaneTrackingEvent>("plane-tracking", {
142
+ detail: {
143
+ type: "plane-removed",
144
+ context: dataContext
145
+ }
146
+ })
147
+ this.dispatchEvent(evt);
148
+ }
149
+ }
150
+
151
+ for (const data of detected) {
152
+ const space = "planeSpace" in data ? data.planeSpace
153
+ : ("meshSpace" in data ? data.meshSpace
154
+ : undefined);
155
+ if (!space) continue;
156
+ const planePose = frame.getPose(space, referenceSpace);
157
+
119
158
  let planeMesh: Object3D | undefined;
120
159
 
160
+ const makeBlockerMaterials = (m: Material | Array<Material>) => {
161
+ if (!m) return;
162
+ if (m instanceof Array) {
163
+ for (const m0 of m)
164
+ makeBlockerMaterials(m0);
165
+ return;
166
+ }
167
+ if (!m.name.includes("Occlu")) return;
168
+ m.colorWrite = false;
169
+ m.depthWrite = true;
170
+ m.transparent = false;
171
+ m.polygonOffset = true;
172
+ m.polygonOffsetFactor = 1;
173
+ m.polygonOffsetUnits = 0.1;
174
+ m["_renderOrder"] = -1000;
175
+ }
176
+
121
177
  // If the plane already existed just update it
122
- if (this._allPlanes.has(plane)) {
123
- const planeContext = this._allPlanes.get(plane)!;
178
+ if (_all.has(data)) {
179
+ const planeContext = _all.get(data)!;
124
180
  planeMesh = planeContext.mesh;
125
- if (planeContext.timestamp < plane.lastChangedTime) {
126
- planeContext.timestamp = plane.lastChangedTime;
181
+ if (planeContext.timestamp < data.lastChangedTime) {
182
+ planeContext.timestamp = data.lastChangedTime;
127
183
 
128
-
129
184
  // Update the mesh geometry
130
185
  if (planeContext.mesh) {
131
- const geometry = this.createGeometry(plane.polygon);
186
+ const geometry = this.createGeometry(data);
132
187
  if (planeContext.mesh instanceof Mesh) {
133
188
  planeContext.mesh.geometry.dispose();
134
189
  planeContext.mesh.geometry = geometry;
190
+ makeBlockerMaterials(planeContext.mesh.material);
135
191
  }
136
192
  else if (planeContext.mesh instanceof Group) {
137
193
  for (const ch of planeContext.mesh.children) {
138
194
  if (ch instanceof Mesh) {
139
195
  ch.geometry.dispose();
140
196
  ch.geometry = geometry;
197
+ makeBlockerMaterials(ch.material);
141
198
  }
142
199
  }
143
200
  }
@@ -164,19 +221,30 @@
164
221
  else {
165
222
 
166
223
  // if we don't have any template assigned we just use a simple mesh object
167
- if (!this.planeTemplate) {
168
- this.planeTemplate = new Mesh();
224
+ if (!this.dataTemplate) {
225
+ this.dataTemplate = new Mesh();
169
226
  }
170
227
 
171
- if (this.planeTemplate) {
228
+ if (this.dataTemplate) {
172
229
  // Create instance
173
- const newPlane = GameObject.instantiate(this.planeTemplate) as GameObject;
230
+ const newPlane = GameObject.instantiate(this.dataTemplate) as GameObject;
174
231
  planeMesh = newPlane;
175
232
 
176
233
  if (newPlane instanceof Mesh) {
177
234
  disposeObjectResources(newPlane.geometry);
178
- newPlane.geometry = this.createGeometry(plane.polygon);
235
+ newPlane.geometry = this.createGeometry(data);
236
+ makeBlockerMaterials(newPlane.material);
179
237
  }
238
+ else if (newPlane instanceof Group) {
239
+ for (const ch of newPlane.children) {
240
+ if (ch instanceof Mesh) {
241
+ disposeObjectResources(ch.geometry);
242
+ ch.geometry = this.createGeometry(data);
243
+ makeBlockerMaterials(ch.material);
244
+ }
245
+ }
246
+ }
247
+
180
248
 
181
249
  const mc = newPlane.getComponent(MeshCollider) as MeshCollider;
182
250
  if (mc) {
@@ -194,13 +262,13 @@
194
262
  rig.add(newPlane);
195
263
 
196
264
  const planeContext: XRPlaneContext = {
197
- id: this._planeId++,
198
- xrPlane: plane,
199
- timestamp: plane.lastChangedTime,
265
+ id: this._dataId++,
266
+ xrData: data,
267
+ timestamp: data.lastChangedTime,
200
268
  mesh: newPlane as unknown as Mesh,
201
269
  collider: mc
202
270
  };
203
- this._allPlanes.set(plane, planeContext);
271
+ _all.set(data, planeContext);
204
272
 
205
273
  if (debug) console.log("New plane detected, id=" + planeContext.id);
206
274
 
@@ -230,9 +298,36 @@
230
298
  };
231
299
  }
232
300
 
233
- createGeometry(polygon: Vec3[]) {
301
+ createGeometry(data: XRPlane | XRMesh) {
302
+ if ("polygon" in data) {
303
+ return this.createPlaneGeometry(data.polygon);
304
+ }
305
+ else if ("vertices" in data && "indices" in data) {
306
+ return this.createMeshGeometry(data.vertices, data.indices);
307
+ }
308
+ return new BufferGeometry();
309
+ }
310
+
311
+ createMeshGeometry(vertices: Float32Array, indices: Uint32Array) {
234
312
  const geometry = new BufferGeometry();
313
+ geometry.setIndex(new BufferAttribute(indices, 1));
314
+ geometry.setAttribute('position', new BufferAttribute(vertices, 3));
315
+ // set UVs in worldspace
316
+ const uvs = Array<number>();
317
+ for (let i = 0; i < vertices.length; i+=3) {
318
+ uvs.push(vertices[i], vertices[i + 2]);
319
+ }
320
+ geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2));
235
321
 
322
+ geometry.computeVertexNormals();
323
+ geometry.computeTangents();
324
+
325
+ return geometry;
326
+ }
327
+
328
+ createPlaneGeometry(polygon: Vec3[]) {
329
+ const geometry = new BufferGeometry();
330
+
236
331
  const vertices: number[] = [];
237
332
  const uvs: number[] = [];
238
333
  polygon.forEach(point => {
@@ -268,4 +363,4 @@
268
363
 
269
364
  return geometry;
270
365
  }
271
- }
366
+ }