Needle Engine

Changes between version 3.37.3-alpha and 3.37.4-alpha
Files changed (9) hide show
  1. src/engine-components/Collider.ts +27 -4
  2. src/engine/engine_physics_rapier.ts +48 -14
  3. src/engine/engine_physics.ts +4 -4
  4. src/engine/engine_types.ts +29 -6
  5. src/engine/extensions/NEEDLE_lighting_settings.ts +0 -5
  6. src/engine/webcomponents/needle menu/needle-menu.ts +14 -1
  7. src/engine/codegen/register_types.ts +2 -2
  8. src/engine-components/RigidBody.ts +5 -2
  9. src/engine-components/SyncedTransform.ts +4 -17
src/engine-components/Collider.ts CHANGED
@@ -15,45 +15,65 @@
15
15
  * Collider is the base class for all colliders. A collider is a physical shape that is used to detect collisions with other objects in the scene.
16
16
  * Colliders are used in combination with a Rigidbody to create physical interactions between objects.
17
17
  * Colliders are registered with the physics engine when they are enabled and removed when they are disabled.
18
- * @category Physics
18
+ * @category Physics
19
+ * @inheritdoc
19
20
  */
20
21
  export class Collider extends Behaviour implements ICollider {
21
22
 
23
+ /** @internal */
22
24
  get isCollider(): any {
23
25
  return true;
24
26
  }
25
27
 
28
+ /**
29
+ * The Rigidbody that this collider is attached to.
30
+ */
26
31
  @serializable(Rigidbody)
27
32
  attachedRigidbody: Rigidbody | null = null;
28
33
 
34
+ /**
35
+ * When `true` the collider will not be used for collision detection but will still trigger events.
36
+ */
29
37
  @serializable()
30
38
  isTrigger: boolean = false;
31
39
 
40
+ /**
41
+ * The physics material that is used for the collider. This material defines physical properties of the collider such as friction and bounciness.
42
+ */
32
43
  @serializable()
33
44
  sharedMaterial?: PhysicsMaterial;
34
45
 
46
+ /**
47
+ * The layers that the collider is assigned to.
48
+ */
35
49
  @serializable()
36
50
  membership: number[] = [0];
51
+
52
+ /**
53
+ * The layers that the collider will interact with.
54
+ * @inheritdoc
55
+ */
37
56
  @serializable()
38
57
  filter?: number[];
39
58
 
59
+ /** @internal */
40
60
  awake() {
41
61
  super.awake();
42
62
  if (!this.attachedRigidbody)
43
63
  this.attachedRigidbody = this.gameObject.getComponentInParent(Rigidbody);
44
64
  }
45
-
65
+ /** @internal */
46
66
  start() {
47
67
  if (!this.attachedRigidbody)
48
68
  this.attachedRigidbody = this.gameObject.getComponentInParent(Rigidbody);
49
69
  }
50
-
70
+ /** @internal */
51
71
  onEnable() {
52
72
  // a rigidbody is not assigned if we export an asset
53
73
  if (!this.attachedRigidbody)
54
74
  this.attachedRigidbody = this.gameObject.getComponentInParent(Rigidbody);
55
75
  }
56
-
76
+ /** @internal */
57
77
  onDisable() {
58
78
  this.context.physics.engine?.removeBody(this);
59
79
  }
@@ -63,6 +83,9 @@
63
83
  return this.context.physics.engine?.getBody(this);
64
84
  }
65
85
 
86
+ /**
87
+ * Apply the collider properties to the physics engine.
88
+ */
66
89
  updateProperties = () => {
67
90
  this.context.physics.engine?.updateProperties(this);
68
91
  }
src/engine/engine_physics_rapier.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ActiveCollisionTypes, ActiveEvents, Ball, CoefficientCombineRule, Collider, ColliderDesc, Cuboid, EventQueue, JointData, QueryFilterFlags, Ray, RigidBody, RigidBodyType, ShapeColliderTOI, ShapeType, World } from '@dimforge/rapier3d-compat';
1
+ import { ActiveCollisionTypes, ActiveEvents, Ball, CoefficientCombineRule, Collider, ColliderDesc, Cuboid, EventQueue, JointData, QueryFilterFlags, Ray, RigidBody, RigidBodyDesc, RigidBodyType, ShapeColliderTOI, ShapeType, World } from '@dimforge/rapier3d-compat';
2
2
  import { BufferAttribute, BufferGeometry, LineBasicMaterial, LineSegments, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from 'three'
3
3
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
4
4
 
@@ -295,9 +295,20 @@
295
295
 
296
296
  private rapierRay = new Ray({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 1 });
297
297
  private raycastVectorsBuffer = new CircularBuffer(() => new Vector3(), 10);
298
- public raycast(origin?: Vec2 | Vec3, direction?: Vec3, maxDistance?: number, solid?: boolean)
298
+ public raycast(origin?: Vec2 | Vec3, direction?: Vec3, options?: {
299
+ maxDistance?: number,
300
+ /** True if you want to also hit objects when the raycast starts from inside a collider */
301
+ solid?: boolean,
302
+ queryFilterFlags?: QueryFilterFlags,
303
+ filterGroups?: number,
304
+ /** Return false to ignore this collider */
305
+ filterPredicate?: (c: ICollider) => boolean,
306
+ })
299
307
  : null | { point: Vector3, collider: ICollider } {
300
308
 
309
+ let maxDistance = options?.maxDistance;
310
+ let solid = options?.solid;
311
+
301
312
  if (maxDistance === undefined) maxDistance = Infinity;
302
313
  if (solid === undefined) solid = true;
303
314
 
@@ -306,9 +317,12 @@
306
317
 
307
318
  if (this.debugRenderRaycasts || showPhysicsRaycasts) Gizmos.DrawRay(ray.origin, ray.dir, 0x0000ff, 1);
308
319
 
309
- const hit = this.world?.castRay(ray, maxDistance, solid, undefined, undefined, undefined, undefined, (c) => {
320
+ const hit = this.world?.castRay(ray, maxDistance, solid, options?.queryFilterFlags, options?.filterGroups, undefined, undefined, (c) => {
321
+ const component = c[$componentKey];
322
+ if (options?.filterPredicate) return options.filterPredicate(component);
323
+
310
324
  // ignore objects in the IgnoreRaycast=2 layer
311
- return !c[$componentKey]?.gameObject.layers.isEnabled(2);
325
+ return !component?.gameObject.layers.isEnabled(2);
312
326
  });
313
327
  if (hit) {
314
328
  const point = ray.pointAt(hit.toi);
@@ -320,9 +334,20 @@
320
334
  return null;
321
335
  }
322
336
 
323
- public raycastAndGetNormal(origin?: Vec2 | Vec3, direction?: Vec3, maxDistance?: number, solid?: boolean)
337
+ public raycastAndGetNormal(origin?: Vec2 | Vec3, direction?: Vec3, options?: {
338
+ maxDistance?: number,
339
+ /** True if you want to also hit objects when the raycast starts from inside a collider */
340
+ solid?: boolean,
341
+ queryFilterFlags?: QueryFilterFlags,
342
+ filterGroups?: number,
343
+ /** Return false to ignore this collider */
344
+ filterPredicate?: (c: ICollider) => boolean,
345
+ })
324
346
  : null | { point: Vector3, normal: Vector3, collider: ICollider } {
325
347
 
348
+ let maxDistance = options?.maxDistance;
349
+ let solid = options?.solid;
350
+
326
351
  if (maxDistance === undefined) maxDistance = Infinity;
327
352
  if (solid === undefined) solid = true;
328
353
 
@@ -331,9 +356,11 @@
331
356
 
332
357
  if (this.debugRenderRaycasts || showPhysicsRaycasts) Gizmos.DrawRay(ray.origin, ray.dir, 0x0000ff, 1);
333
358
 
334
- const hit = this.world?.castRayAndGetNormal(ray, maxDistance, solid, undefined, undefined, undefined, undefined, (c) => {
359
+ const hit = this.world?.castRayAndGetNormal(ray, maxDistance, solid, options?.queryFilterFlags, options?.filterGroups, undefined, undefined, (c) => {
360
+ const component = c[$componentKey];
361
+ if (options?.filterPredicate) return options.filterPredicate(component);
335
362
  // ignore objects in the IgnoreRaycast=2 layer
336
- return !c[$componentKey]?.gameObject.layers.isEnabled(2);
363
+ return !component?.gameObject.layers.isEnabled(2);
337
364
  });
338
365
  if (hit) {
339
366
  const point = ray.pointAt(hit.toi);
@@ -802,30 +829,36 @@
802
829
  }
803
830
  }
804
831
 
805
- /** Updates the collision groups */
832
+ /**
833
+ * Updates the collision groups of a collider.
834
+ *
835
+ * @param collider - The collider to update.
836
+ */
806
837
  private updateColliderCollisionGroups(collider: ICollider) {
807
- const body = collider[$bodyKey];
838
+ const body = collider[$bodyKey] as Collider;
808
839
  const members = collider.membership;
809
840
  let memberMask = 0;
810
841
  if (members == undefined) {
811
- memberMask = 0xffffffff;
842
+ memberMask = 0xffff;
812
843
  }
813
844
  else {
814
845
  for (let i = 0; i < members.length; i++) {
815
846
  const member = members[i];
816
- memberMask |= 1 << member;
847
+ if (member > 31) console.error(`Rapier only supports 32 layers, layer ${member} is not supported`);
848
+ else memberMask |= 1 << Math.floor(member);
817
849
  }
818
850
  }
819
851
 
820
852
  const mask = collider.filter;
821
853
  let filterMask = 0;
822
854
  if (mask == undefined) {
823
- filterMask = 0xffffffff;
855
+ filterMask = 0xffff;
824
856
  }
825
857
  else {
826
858
  for (let i = 0; i < mask.length; i++) {
827
859
  const member = mask[i];
828
- filterMask |= 1 << member;
860
+ if (member > 31) console.error(`Rapier only supports 32 layers, layer ${member} is not supported`);
861
+ else filterMask |= 1 << Math.floor(member);
829
862
  }
830
863
  }
831
864
  body.setCollisionGroups((memberMask << 16) | filterMask);
@@ -843,10 +876,11 @@
843
876
  const kinematic = rb.isKinematic && !debugColliderPlacement;
844
877
  if (debugPhysics)
845
878
  console.log("Create rigidbody", kinematic);
846
- const rigidBodyDesc = kinematic ? RAPIER.RigidBodyDesc.kinematicPositionBased() : RAPIER.RigidBodyDesc.dynamic();
879
+ const rigidBodyDesc = (kinematic ? RAPIER.RigidBodyDesc.kinematicPositionBased() : RAPIER.RigidBodyDesc.dynamic()) as RigidBodyDesc;
847
880
  const pos = getWorldPosition(collider.attachedRigidbody.gameObject);
848
881
  rigidBodyDesc.setTranslation(pos.x, pos.y, pos.z);
849
882
  rigidBodyDesc.setRotation(getWorldQuaternion(collider.attachedRigidbody.gameObject));
883
+ rigidBodyDesc.centerOfMass = new RAPIER.Vector3(rb.centerOfMass.x, rb.centerOfMass.y, rb.centerOfMass.z);
850
884
  rigidBody = this.world.createRigidBody(rigidBodyDesc);
851
885
  this.bodies.push(rigidBody);
852
886
  this.objects.push(rb);
src/engine/engine_physics.ts CHANGED
@@ -125,14 +125,14 @@
125
125
 
126
126
  export class Physics {
127
127
 
128
- /**@deprecated use this.context.physics.engine.raycast */
128
+ /**@deprecated use `this.context.physics.engine.raycast` {@link IPhysicsEngine.raycast} */
129
129
  public raycastPhysicsFast(origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined, maxDistance: number = Infinity, solid: boolean = true) {
130
- return this.context.physics.engine?.raycast(origin, direction, maxDistance, solid) ?? null;
130
+ return this.context.physics.engine?.raycast(origin, direction, { maxDistance, solid }) ?? null;
131
131
  }
132
132
 
133
- /**@deprecated use this.context.physics.engine.raycastAndGetNormal */
133
+ /**@deprecated use `this.context.physics.engine.raycastAndGetNormal` {@link IPhysicsEngine.raycastAndGetNormal} */
134
134
  public raycastPhysicsFastAndGetNormal(origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined, maxDistance: number = Infinity, solid: boolean = true) {
135
- return this.context.physics.engine?.raycastAndGetNormal(origin, direction, maxDistance, solid) ?? null;
135
+ return this.context.physics.engine?.raycastAndGetNormal(origin, direction, { maxDistance, solid }) ?? null;
136
136
  }
137
137
 
138
138
  /**@deprecated use this.context.physics.engine.sphereOverlap */
src/engine/engine_types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { QueryFilterFlags } from "@dimforge/rapier3d-compat";
1
2
  import { Camera, Color, Material, Mesh, Object3D, Quaternion, Ray, Scene, WebGLRenderer } from "three";
2
3
  import { Vector3 } from "three";
3
4
  import { type GLTF as GLTF3 } from "three/examples/jsm/loaders/GLTFLoader.js";
@@ -251,12 +252,14 @@
251
252
  updatePhysicsMaterial(): void;
252
253
  /** The collider membership indicates what groups the collider is part of (e.g. group 2 and 3)
253
254
  * An `undefined` array indicates that the collider is part of all groups
254
- * Note: Make sure to call updateProperties after having changed this property
255
+ * Note: Make sure to call updateProperties after having changed this property
256
+ * Default: [0]
255
257
  */
256
258
  membership?: number[];
257
259
  /** The collider filter indicates what groups the collider can interact with (e.g. group 3 and 4)
258
260
  * An `undefined` array indicates that the collider can interact with all groups
259
- * Note: Make sure to call updateProperties after having changed this property
261
+ * Note: Make sure to call updateProperties after having changed this property
262
+ * Default: undefined
260
263
  */
261
264
  filter?: number[];
262
265
  }
@@ -278,6 +281,7 @@
278
281
  drag: number;
279
282
  angularDrag: number;
280
283
  useGravity: boolean;
284
+ centerOfMass: Vec3;
281
285
  gravityScale: number;
282
286
  dominanceGroup: number;
283
287
 
@@ -436,12 +440,31 @@
436
440
  /** Fast raycast against physics colliders
437
441
  * @param origin ray origin in screen or worldspace
438
442
  * @param direction ray direction in worldspace
439
- * @param maxDistance max distance to raycast
440
- * @param solid if true it will also hit the collider if origin is already inside it
443
+ * @param options additional options
441
444
  */
442
- raycast(origin?: Vec2 | Vec3, direction?: Vec3, maxDistance?: number, solid?: boolean): RaycastResult;
445
+ raycast(origin?: Vec2 | Vec3, direction?: Vec3, options?: {
446
+ maxDistance?: number,
447
+ /** True if you want to also hit objects when the raycast starts from inside a collider */
448
+ solid?: boolean,
449
+ queryFilterFlags?: QueryFilterFlags,
450
+ /** Raycast filter groups. Groups are used to apply the collision group rules for the scene query. The scene query will only consider hits with colliders with collision groups compatible with this collision group (using the bitwise test described in the collision groups section). See https://rapier.rs/docs/user_guides/javascript/colliders#collision-groups-and-solver-groups
451
+ * For example membership 0x0001 and filter 0x0002 should be 0x00010002 */
452
+ filterGroups?: number,
453
+ /** Return false to ignore this collider */
454
+ filterPredicate?: (collider: ICollider) => boolean
455
+ }): RaycastResult;
443
456
  /** raycast that also gets the normal vector. If you don't need it use raycast() */
444
- raycastAndGetNormal(origin?: Vec2 | Vec3, direction?: Vec3, maxDistance?: number, solid?: boolean): RaycastResult;
457
+ raycastAndGetNormal(origin?: Vec2 | Vec3, direction?: Vec3, options?: {
458
+ maxDistance?: number,
459
+ /** True if you want to also hit objects when the raycast starts from inside a collider */
460
+ solid?: boolean,
461
+ queryFilterFlags?: QueryFilterFlags,
462
+ /** Raycast filter groups. Groups are used to apply the collision group rules for the scene query. The scene query will only consider hits with colliders with collision groups compatible with this collision group (using the bitwise test described in the collision groups section). See https://rapier.rs/docs/user_guides/javascript/colliders#collision-groups-and-solver-groups
463
+ * For example membership 0x0001 and filter 0x0002 should be 0x00010002 */
464
+ filterGroups?: number,
465
+ /** Return false to ignore this collider */
466
+ filterPredicate?: (collider: ICollider) => boolean
467
+ }): RaycastResult;
445
468
  sphereOverlap(point: Vector3, radius: number): Array<SphereOverlapResult>;
446
469
 
447
470
  // Collider methods
src/engine/extensions/NEEDLE_lighting_settings.ts CHANGED
@@ -94,8 +94,6 @@
94
94
  private _hasReflection: boolean = false;
95
95
  private _ambientLightObj?: AmbientLight;
96
96
  private _hemisphereLightObj?: HemisphereLight;
97
- // used when skybox is used to support ambient intensity for "non custom shaders"
98
- private _lightProbeObj?: LightProbe;
99
97
 
100
98
  awake() {
101
99
  if (this.sourceId) {
@@ -154,7 +152,6 @@
154
152
  if (this._ambientLightObj) {
155
153
  this.gameObject.add(this._ambientLightObj)
156
154
  }
157
- if (this._lightProbeObj) this._lightProbeObj.removeFromParent();
158
155
  }
159
156
  else if (this.ambientMode === AmbientMode.Trilight) {
160
157
  if (this.ambientTrilight) {
@@ -180,8 +177,6 @@
180
177
  onDisable() {
181
178
  if (debug)
182
179
  console.warn("💡⚫ <<< Disable lighting:", this.sourceId, this);
183
- if (this._lightProbeObj)
184
- this._lightProbeObj.removeFromParent();
185
180
  if (this._ambientLightObj)
186
181
  this._ambientLightObj.removeFromParent();
187
182
  if (this._hemisphereLightObj)
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -371,7 +371,20 @@
371
371
  width: 100%;
372
372
  }
373
373
 
374
-
374
+ @media (max-width: 300px) or (max-height: 350px){
375
+ .options {
376
+ display: none;
377
+ padding: 0;
378
+ margin: 0;
379
+ border: none;
380
+ }
381
+ .logo.any-options {
382
+ border: none !important;
383
+ padding: 0;
384
+ margin-bottom: 0 !important;
385
+ padding-bottom: 0 !important;
386
+ }
387
+ }
375
388
 
376
389
  /* dark mode */
377
390
  /*
src/engine/codegen/register_types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable */
2
2
  import { TypeStore } from "./../engine_typestore.js"
3
-
3
+
4
4
  // Import types
5
5
  import { __Ignore } from "../../engine-components/codegen/components.js";
6
6
  import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
@@ -220,7 +220,7 @@
220
220
  import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
221
221
  import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
222
222
  import { XRState } from "../../engine-components/webxr/XRFlag.js";
223
-
223
+
224
224
  // Register types
225
225
  TypeStore.add("__Ignore", __Ignore);
226
226
  TypeStore.add("ActionBuilder", ActionBuilder);
src/engine-components/RigidBody.ts CHANGED
@@ -167,8 +167,11 @@
167
167
  @serializable()
168
168
  useGravity: boolean = true;
169
169
 
170
- @serializable()
171
- centerOfMass: Vector3 = new Vector3();
170
+ /**
171
+ * The center of mass is the point around which the mass of the rigid-body is evenly distributed. It is used to compute the torque applied to the rigid-body when forces are applied to it.
172
+ */
173
+ @serializable(Vector3)
174
+ centerOfMass: Vector3 = new Vector3(0, 0, 0);
172
175
 
173
176
  /**
174
177
  * Constraints are used to lock the position or rotation of an object in a specific axis.
src/engine-components/SyncedTransform.ts CHANGED
@@ -282,22 +282,14 @@
282
282
  this.lastWorldRotation.copy(wr);
283
283
 
284
284
 
285
- // if (this._model.isOwned === false && this.autoOwnership) {
286
- // this.requestOwnership();
287
- // }
288
-
289
285
  if (!this._model) return;
290
- // only run if we are the owner
286
+
291
287
  if (!this._model || this._model.hasOwnership === undefined || !this._model.hasOwnership) {
292
- if (this.rb) {
293
- this.rb.isKinematic = this._model.isOwned ?? false;
294
- this.rb.setVelocity(0, 0, 0);
295
- }
288
+ // if we're not the owner of this synced transform then don't send any data
296
289
  return;
297
290
  }
298
291
 
299
292
  // local user is owner:
300
-
301
293
  if (this.rb) {
302
294
  if (this._wasKinematic !== undefined) {
303
295
  if (debug)
@@ -305,13 +297,8 @@
305
297
  this.rb.isKinematic = this._wasKinematic;
306
298
  }
307
299
 
308
- // hacky reset if too far off
309
- if (this.gameObject.position.distanceTo(new Vector3(0, 0, 0)) > 1000) {
310
- if (debug)
311
- console.log("RESET", this.name)
312
- this.gameObject.position.set(0, 1, 0);
313
- this.rb.setVelocity(0, 0, 0);
314
- }
300
+ // TODO: if the SyncedTransform has a dynamic rigidbody we should probably synchronize the Rigidbody's properties (velocity etc)
301
+ // Or the Rigidbody should be synchronized separately in which case the SyncedTransform should be passive
315
302
  }
316
303
 
317
304
  const updateInterval = 10;