Needle Engine

Changes between version 3.44.0 and 3.44.1
Files changed (7) hide show
  1. src/engine-components/DragControls.ts +38 -25
  2. src/engine/engine_gameobject.ts +35 -22
  3. src/engine/engine_loaders.ts +25 -27
  4. src/engine/extensions/extension_utils.ts +2 -2
  5. src/engine-components/OrbitControls.ts +43 -36
  6. src/engine-components/postprocessing/PostProcessingHandler.ts +1 -0
  7. src/engine-components/postprocessing/Volume.ts +0 -1
src/engine-components/DragControls.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { RaycastOptions } from "../engine/engine_physics.js";
7
7
  import { serializable } from "../engine/engine_serialization_decorator.js";
8
8
  import { Context } from "../engine/engine_setup.js";
9
- import { getBoundingBox, getWorldPosition, setWorldPosition } from "../engine/engine_three_utils.js";
9
+ import { getBoundingBox, getTempVector, getWorldPosition, setWorldPosition } from "../engine/engine_three_utils.js";
10
10
  import { type IGameObject } from "../engine/engine_types.js";
11
11
  import { getParam } from "../engine/engine_utils.js";
12
12
  import { NeedleXRSession } from "../engine/engine_xr.js";
@@ -341,6 +341,21 @@
341
341
  }
342
342
  }
343
343
 
344
+ /** Common interface for pointer handlers (single touch and multi touch) */
345
+ interface IDragHandler {
346
+ /** Used to determine if a drag has happened for this handler */
347
+ getTotalMovement?(): Vector3;
348
+ /** Target object can change mid-flight (e.g. in Duplicatable), handlers should react properly to that */
349
+ setTargetObject(obj: Object3D | null): void;
350
+
351
+ /** Prewarms the drag – can already move internal points around here but should not move the object itself */
352
+ collectMovementInfo?(): void;
353
+ onDragStart?(args: PointerEventData): void;
354
+ onDragEnd?(args: PointerEventData): void;
355
+ /** The target object is moved around */
356
+ onDragUpdate?(numberOfPointers: number): void;
357
+ }
358
+
344
359
  /** Handles two touch points affecting one object. Allows movement, scale and rotation of objects. */
345
360
  class MultiTouchDragHandler implements IDragHandler {
346
361
 
@@ -581,20 +596,6 @@
581
596
  }
582
597
  }
583
598
 
584
- /** Common interface for pointer handlers (single touch and multi touch) */
585
- interface IDragHandler {
586
- /** Used to determine if a drag has happened for this handler */
587
- getTotalMovement?(): Vector3;
588
- /** Target object can change mid-flight (e.g. in Duplicatable), handlers should react properly to that */
589
- setTargetObject(obj: Object3D | null): void;
590
-
591
- /** Prewarms the drag – can already move internal points around here but should not move the object itself */
592
- collectMovementInfo?(): void;
593
- onDragStart?(args: PointerEventData): void;
594
- onDragEnd?(args: PointerEventData): void;
595
- /** The target object is moved around */
596
- onDragUpdate?(numberOfPointers: number): void;
597
- }
598
599
 
599
600
  /** Handles a single pointer on an object. DragPointerHandlers are created on pointer enter,
600
601
  * help with determining if there's actually a drag, and then perform operations based on spatial pointer data.
@@ -908,7 +909,7 @@
908
909
 
909
910
  // Acceleration for moving the object - move followObject along the ray distance by _totalMovementAlongRayDirection
910
911
  let currentDist = 1.0;
911
- let lerpFactor = 1.0;
912
+ let lerpFactor = 2.0;
912
913
  if (isSpatialInput && this._grabStartDistance > 0.5) // hands and controllers, but not touches
913
914
  {
914
915
  const factor = 1 + this._totalMovementAlongRayDirection * (2 * this.settings.xrDistanceDragFactor);
@@ -960,18 +961,23 @@
960
961
  }
961
962
 
962
963
  if (hit.face) {
964
+ const dragTimeThreshold = 0.15;
965
+ const dragTimeSatisfied = this._draggedOverObjectDuration >= dragTimeThreshold;
963
966
  // Adjust drag plane if we're dragging over a different object (for a certain amount of time)
964
967
  // or if the surface normal changed
965
- if (this._draggedOverObjectDuration > 0.15 &&
966
- (this._draggedOverObjectLastSetUp !== this._draggedOverObject ||
967
- this._draggedOverObjectLastNormal.dot(hit.face.normal) < 0.999999)
968
+ if (dragTimeSatisfied &&
969
+ (this._draggedOverObjectLastSetUp !== this._draggedOverObject
970
+ || this._draggedOverObjectLastNormal.dot(hit.face.normal) < 0.999999
971
+ // if we're dragging on a flat surface with different levels (like the sandbox floor)
972
+ || this.context.time.frame % 60 === 0
973
+ )
968
974
  ) {
969
975
  this._draggedOverObjectLastSetUp = this._draggedOverObject;
970
976
  this._draggedOverObjectLastNormal.copy(hit.face.normal);
971
977
 
972
- const center = new Vector3();
973
- const size = new Vector3();
974
-
978
+ const center = getTempVector();
979
+ const size = getTempVector();
980
+
975
981
  this._bounds.getCenter(center);
976
982
  this._bounds.getSize(size);
977
983
  center.sub(size.multiplyScalar(0.5).multiply(hit.face.normal));
@@ -986,7 +992,7 @@
986
992
  this._bounds.getSize(size);
987
993
  center.add(size.multiplyScalar(0.5).multiply(hit.face.normal));
988
994
 
989
- const offset = this._hitPointInLocalSpace.clone().add(center);
995
+ const offset = getTempVector(this._hitPointInLocalSpace).add(center);
990
996
  this._followObject.localToWorld(offset);
991
997
 
992
998
  // See https://linear.app/needle/issue/NE-5004
@@ -994,6 +1000,13 @@
994
1000
  const point = hit.point;//.sub(offsetWP);
995
1001
  this._dragPlane.setFromNormalAndCoplanarPoint(hit.face.normal, point);
996
1002
  }
1003
+ // If the drag has just started and we're not yet really starting to update the object's position
1004
+ // we want to return here and wait until the drag has been going on for a bit
1005
+ // Otherwise the object will either immediately change it's position (when the user starts dragging)
1006
+ // Or interpolate to a wrong position for a short moment
1007
+ else if (!dragTimeSatisfied) {
1008
+ return;
1009
+ }
997
1010
  }
998
1011
  }
999
1012
  }
@@ -1345,7 +1358,7 @@
1345
1358
  private onUpdateGroundPlane() {
1346
1359
  if (!this._selected || !this._context) return;
1347
1360
  const wp = getWorldPosition(this._selected);
1348
- const ray = new Ray(new Vector3(0, .1, 0).add(wp), new Vector3(0, -1, 0));
1361
+ const ray = new Ray(getTempVector(0, .1, 0).add(wp), getTempVector(0, -1, 0));
1349
1362
  const opts = new RaycastOptions();
1350
1363
  opts.testObject = o => o !== this._selected;
1351
1364
  const hits = this._context.physics.raycastFromRay(ray, opts);
@@ -1354,7 +1367,7 @@
1354
1367
  if (!hit.face || this.contains(this._selected, hit.object)) {
1355
1368
  continue;
1356
1369
  }
1357
- const normal = new Vector3(0, 1, 0); // hit.face.normal
1370
+ const normal = getTempVector(0, 1, 0); // hit.face.normal
1358
1371
  this._groundPlane.setFromNormalAndCoplanarPoint(normal, hit.point);
1359
1372
  break;
1360
1373
  }
src/engine/engine_gameobject.ts CHANGED
@@ -44,6 +44,10 @@
44
44
  /** if the instantiated object should be visible */
45
45
  visible?: boolean;
46
46
  context?: Context;
47
+ /** If true the components will be cloned as well
48
+ * @default true
49
+ */
50
+ components?: boolean;
47
51
  }
48
52
 
49
53
  export class InstantiateOptions implements IInstantiateOptions {
@@ -55,8 +59,9 @@
55
59
  scale?: Vector3 | undefined;
56
60
  visible?: boolean | undefined;
57
61
  context?: Context | undefined;
62
+ components?: boolean | undefined;
58
63
 
59
- clone(){
64
+ clone() {
60
65
  const clone = new InstantiateOptions();
61
66
  clone.idProvider = this.idProvider;
62
67
  clone.parent = this.parent;
@@ -64,17 +69,23 @@
64
69
  clone.position = this.position?.clone();
65
70
  clone.rotation = this.rotation?.clone();
66
71
  clone.scale = this.scale?.clone();
72
+ clone.visible = this.visible;
73
+ clone.context = this.context;
74
+ clone.components = this.components;
67
75
  return clone;
68
76
  }
69
77
 
70
78
  /** Copy fields from another object, clone field references */
71
- cloneAssign(other: InstantiateOptions | IInstantiateOptions){
79
+ cloneAssign(other: InstantiateOptions | IInstantiateOptions) {
72
80
  this.idProvider = other.idProvider;
73
81
  this.parent = other.parent;
74
82
  this.keepWorldPosition = other.keepWorldPosition;
75
83
  this.position = other.position?.clone();
76
84
  this.rotation = other.rotation?.clone();
77
85
  this.scale = other.scale?.clone();
86
+ this.visible = other.visible;
87
+ this.context = other.context;
88
+ this.components = other.components;
78
89
  }
79
90
  }
80
91
 
@@ -326,29 +337,31 @@
326
337
  }
327
338
 
328
339
  const guidsMap: GuidsMap = {};
329
- for (const i in components) {
330
- const copy = components[i];
331
- const oldGuid = copy.guid;
332
- if (options && options.idProvider) {
333
- copy.guid = options.idProvider.generateUUID();
334
- guidsMap[oldGuid] = copy.guid;
335
- if (debug)
336
- console.log(copy.name, copy.guid)
340
+ if (options?.components !== false) {
341
+ for (const i in components) {
342
+ const copy = components[i];
343
+ const oldGuid = copy.guid;
344
+ if (options && options.idProvider) {
345
+ copy.guid = options.idProvider.generateUUID();
346
+ guidsMap[oldGuid] = copy.guid;
347
+ if (debug)
348
+ console.log(copy.name, copy.guid)
349
+ }
350
+ registerComponent(copy, context);
351
+ if (copy.__internalNewInstanceCreated)
352
+ copy.__internalNewInstanceCreated();
337
353
  }
338
- registerComponent(copy, context);
339
- if (copy.__internalNewInstanceCreated)
340
- copy.__internalNewInstanceCreated();
354
+ for (const i in components) {
355
+ const copy = components[i];
356
+ if (copy.resolveGuids)
357
+ copy.resolveGuids(guidsMap);
358
+ if (copy.enabled === false) continue;
359
+ else copy.enabled = true;
360
+ }
361
+
362
+ processNewScripts(context);
341
363
  }
342
- for (const i in components) {
343
- const copy = components[i];
344
- if (copy.resolveGuids)
345
- copy.resolveGuids(guidsMap);
346
- if (copy.enabled === false) continue;
347
- else copy.enabled = true;
348
- }
349
364
 
350
- processNewScripts(context);
351
-
352
365
  return clone as GameObject;
353
366
  }
354
367
 
src/engine/engine_loaders.ts CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { setDracoDecoderLocation, setKTX2TranscoderLocation } from '@needle-tools/gltf-progressive';
2
+ import { createLoaders, setDracoDecoderLocation, setKTX2TranscoderLocation } from '@needle-tools/gltf-progressive';
3
3
  import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
4
4
  import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
5
5
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
@@ -22,35 +22,40 @@
22
22
  });
23
23
 
24
24
 
25
- let dracoLoader: DRACOLoader;
26
25
  let meshoptDecoder: typeof MeshoptDecoder;
27
- let ktx2Loader: KTX2Loader;
28
26
 
27
+ let loaders: null | { dracoLoader: DRACOLoader, ktx2Loader: KTX2Loader, meshoptDecoder: typeof MeshoptDecoder } = null;
28
+
29
+ function ensureLoaders() {
30
+ if (!loaders) {
31
+ const res = createLoaders(null);
32
+ loaders = { dracoLoader: res.dracoLoader, ktx2Loader: res.ktx2Loader, meshoptDecoder: res.meshoptDecoder };
33
+ }
34
+ return loaders;
35
+ }
36
+
29
37
  export function setDracoDecoderPath(path: string | undefined) {
30
38
  if (path !== undefined && typeof path === "string") {
31
- if (!dracoLoader)
32
- dracoLoader = new DRACOLoader();
39
+ const loaders = ensureLoaders();
33
40
  if (debug) console.log("Setting draco decoder path to", path);
34
- dracoLoader.setDecoderPath(path);
41
+ loaders.dracoLoader.setDecoderPath(path);
35
42
  setDracoDecoderLocation(path);
36
43
  }
37
44
  }
38
45
 
39
46
  export function setDracoDecoderType(type: string | undefined) {
40
47
  if (type !== undefined && typeof type === "string") {
41
- if (!dracoLoader)
42
- dracoLoader = new DRACOLoader();
48
+ const loaders = ensureLoaders();
43
49
  if (debug) console.log("Setting draco decoder type to", type);
44
- dracoLoader.setDecoderConfig({ type: type });
50
+ loaders.dracoLoader.setDecoderConfig({ type: type });
45
51
  }
46
52
  }
47
53
 
48
54
  export function setKtx2TranscoderPath(path: string) {
49
55
  if (path !== undefined && typeof path === "string") {
50
- if (!ktx2Loader)
51
- ktx2Loader = new KTX2Loader();
56
+ const loaders = ensureLoaders();
52
57
  if (debug) console.log("Setting ktx2 transcoder path to", path);
53
- ktx2Loader.setTranscoderPath(path);
58
+ loaders.ktx2Loader.setTranscoderPath(path);
54
59
  setKTX2TranscoderLocation(path);
55
60
  }
56
61
  }
@@ -67,32 +72,25 @@
67
72
  * @returns The GLTFLoader instance with the loaders added.
68
73
  */
69
74
  export function addDracoAndKTX2Loaders(loader: GLTFLoader, context: Pick<Context, "renderer">) {
70
- if (!dracoLoader) {
71
- dracoLoader = new DRACOLoader();
72
- dracoLoader.setDecoderPath(DEFAULT_DRACO_DECODER_LOCATION);
73
- dracoLoader.setDecoderConfig({ type: 'js' });
74
- if (debug) console.log("Setting draco decoder path to", DEFAULT_DRACO_DECODER_LOCATION);
75
- }
76
- if (!ktx2Loader) {
77
- ktx2Loader = new KTX2Loader();
78
- ktx2Loader.setTranscoderPath(DEFAULT_KTX2_TRANSCODER_LOCATION);
79
- if (debug) console.log("Setting ktx2 transcoder path to", DEFAULT_KTX2_TRANSCODER_LOCATION);
80
- }
75
+
76
+ const loaders = ensureLoaders();
77
+
81
78
  if (!meshoptDecoder) {
82
- meshoptDecoder = MeshoptDecoder;
79
+ meshoptDecoder = loaders.meshoptDecoder;
83
80
  if (debug) console.log("Using the default meshopt decoder");
84
81
  }
85
82
 
83
+
86
84
  if (context.renderer) {
87
- ktx2Loader.detectSupport(context.renderer);
85
+ loaders.ktx2Loader.detectSupport(context.renderer);
88
86
  }
89
87
  else
90
88
  console.warn("No renderer provided to detect ktx2 support - loading KTX2 textures will probably fail");
91
89
 
92
90
  if (!loader.dracoLoader)
93
- loader.setDRACOLoader(dracoLoader);
91
+ loader.setDRACOLoader(loaders.dracoLoader);
94
92
  if (!(loader as any).ktx2Loader)
95
- loader.setKTX2Loader(ktx2Loader);
93
+ loader.setKTX2Loader(loaders.ktx2Loader);
96
94
  if (!(loader as any).meshoptDecoder)
97
95
  loader.setMeshoptDecoder(meshoptDecoder);
98
96
  return loader;
src/engine/extensions/extension_utils.ts CHANGED
@@ -76,10 +76,10 @@
76
76
  // e.g. prefix = "/materials/";
77
77
  const res = tryResolveDependency(paths, parser, val);
78
78
  if (res) {
79
- res.then(res => {
79
+ promises.push(res.then(res => {
80
80
  obj[key] = res;
81
81
  return res;
82
- });
82
+ }));
83
83
  continue;
84
84
  }
85
85
  }
src/engine-components/OrbitControls.ts CHANGED
@@ -271,6 +271,7 @@
271
271
 
272
272
  /** @internal */
273
273
  onEnable() {
274
+ this._didSetTarget = 0;
274
275
  this._enableTime = this.context.time.time;
275
276
  const cameraComponent = GameObject.getComponent(this.gameObject, Camera);
276
277
  this._camera = cameraComponent;
@@ -330,6 +331,7 @@
330
331
  // if (this.autoFit) this.fitCamera()
331
332
  // }
332
333
  this.context.input.addEventListener("pointerup", this._onPointerDown);
334
+ this.context.pre_render_callbacks.push(this.__onPreRender);
333
335
  }
334
336
 
335
337
  /** @internal */
@@ -406,8 +408,6 @@
406
408
  }
407
409
  this._controls.enabled = true;
408
410
 
409
- this.__handleSetTargetWhenBecomingActiveTheFirstTime();
410
-
411
411
  if (this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2) || this.context.input.mouseWheelChanged || (this.context.input.getPointerPressed(0) && this.context.input.getPointerPositionDelta(0)?.length() || 0 > .1)) {
412
412
  this._inputs += 1;
413
413
  }
@@ -420,7 +420,36 @@
420
420
  this._lookTargetLerpActive = false;
421
421
  }
422
422
  this._inputs = 0;
423
+
423
424
 
425
+ if (this.autoTarget) {
426
+ // we want to wait one frame so all matrixWorlds are updated
427
+ // otherwise raycasting will not work correctly
428
+ if (this._didSetTarget++ === 0) {
429
+ const camGo = GameObject.getComponent(this.gameObject, Camera);
430
+ if (camGo && !this.setLookTargetFromConstraint()) {
431
+ if (this.debugLog)
432
+ console.log("NO TARGET");
433
+ const worldPosition = getWorldPosition(camGo.cam);
434
+ const distanceToCenter = worldPosition.length();
435
+ const forward = new Vector3(0, 0, -distanceToCenter).applyMatrix4(camGo.cam.matrixWorld);
436
+ this.setLookTargetPosition(forward, true);
437
+ }
438
+ if (!this.setLookTargetFromConstraint()) {
439
+ const opts = new RaycastOptions();
440
+ // center of the screen:
441
+ opts.screenPoint = new Vector2(0, 0);
442
+ opts.lineThreshold = 0.1;
443
+ const hits = this.context.physics.raycast(opts);
444
+ if (hits.length > 0) {
445
+ this.setLookTargetPosition(hits[0].point, true);
446
+ }
447
+ if (debugCameraFit)
448
+ console.log("OrbitControls hits", ...hits);
449
+ }
450
+ }
451
+ }
452
+
424
453
  let focusAtPointer = (this.middleClickToFocus && this.context.input.getPointerClicked(1));
425
454
  focusAtPointer ||= (this.doubleClickToFocus && this.context.input.getPointerDoubleClicked(0) && this.context.time.time - this._enableTime > .3);
426
455
  if (focusAtPointer) {
@@ -509,42 +538,20 @@
509
538
 
510
539
  }
511
540
 
512
- private __handleSetTargetWhenBecomingActiveTheFirstTime() {
541
+ private __onPreRender = () => {
513
542
 
514
- if (this.autoTarget) {
543
+ // We call this only once when the camera becomes active and use the engine pre_render_callbacks because they are run
544
+ // after all scripts have been executed
545
+ const index = this.context.pre_render_callbacks.indexOf(this.__onPreRender);
546
+ if (index >= 0) {
547
+ this.context.pre_render_callbacks.splice(index, 1);
548
+ }
515
549
 
516
- if (this.autoFit) {
517
- this.fitCamera(this.scene.children, {
518
- centerCamera: "y",
519
- immediate: true,
520
- })
521
- }
522
-
523
- // we want to wait one frame so all matrixWorlds are updated
524
- // otherwise raycasting will not work correctly
525
- if (this._didSetTarget++ === 0) {
526
- const camGo = GameObject.getComponent(this.gameObject, Camera);
527
- if (camGo && !this.setLookTargetFromConstraint()) {
528
- if (this.debugLog)
529
- console.log("NO TARGET");
530
- const worldPosition = getWorldPosition(camGo.cam);
531
- const distanceToCenter = worldPosition.length();
532
- const forward = new Vector3(0, 0, -distanceToCenter).applyMatrix4(camGo.cam.matrixWorld);
533
- this.setLookTargetPosition(forward, true);
534
- }
535
- if (!this.autoFit && !this.setLookTargetFromConstraint()) {
536
- const opts = new RaycastOptions();
537
- // center of the screen:
538
- opts.screenPoint = new Vector2(0, 0);
539
- opts.lineThreshold = 0.1;
540
- const hits = this.context.physics.raycast(opts);
541
- if (hits.length > 0) {
542
- this.setLookTargetPosition(hits[0].point, true);
543
- }
544
- if (debugCameraFit)
545
- console.log("OrbitControls hits", ...hits);
546
- }
547
- }
550
+ if (this.autoFit) {
551
+ this.fitCamera(this.scene.children, {
552
+ centerCamera: "y",
553
+ immediate: true,
554
+ })
548
555
  }
549
556
  }
550
557
 
src/engine-components/postprocessing/PostProcessingHandler.ts CHANGED
@@ -53,6 +53,7 @@
53
53
  }
54
54
 
55
55
  unapply() {
56
+ if(debug) console.log("Unapplying postprocessing effects");
56
57
  this._isActive = false;
57
58
  if (this._lastVolumeComponents) {
58
59
  for (const component of this._lastVolumeComponents) {
src/engine-components/postprocessing/Volume.ts CHANGED
@@ -198,7 +198,6 @@
198
198
  }
199
199
 
200
200
  private unapply() {
201
- if (debug) console.log("Unapply PostProcessing " + this.name);
202
201
  this._postprocessing?.unapply();
203
202
  }