Needle Engine

Changes between version 3.19.7 and 3.19.8
Files changed (9) hide show
  1. plugins/vite/dependency-watcher.js +44 -9
  2. src/engine-components/Animation.ts +12 -4
  3. src/engine-components/AnimatorController.ts +122 -30
  4. src/engine/engine_context.ts +2 -2
  5. src/engine/engine_scenetools.ts +4 -0
  6. src/engine/engine_utils_screenshot.ts +9 -0
  7. src/engine/extensions/NEEDLE_animator_controller_model.ts +5 -5
  8. src/engine-components/OrbitControls.ts +0 -1
  9. src/engine-components/webxr/WebXRImageTracking.ts +1 -0
plugins/vite/dependency-watcher.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { exec, execSync } from 'child_process';
2
2
  import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
3
3
  import path from 'path';
4
- import { fileURLToPath } from 'url';
5
4
 
6
5
  const prefix = "[needle-dependency-watcher] ";
7
6
  function log(...msg) {
@@ -82,8 +81,9 @@
82
81
  modified = true;
83
82
  }
84
83
  if (modified || requireInstall) {
85
- if (modified)
86
- log("package.json has changed")
84
+ if (modified) {
85
+ log("package.json has changed. Require install?", requireInstall)
86
+ }
87
87
 
88
88
  let requireReload = false;
89
89
  if (!requireInstall) {
@@ -92,12 +92,24 @@
92
92
 
93
93
  // test if dependencies changed
94
94
  let newPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
95
- for (const key in newPackageJson.dependencies) {
96
- if (packageJson.dependencies[key] !== newPackageJson.dependencies[key] && newPackageJson.dependencies[key] !== undefined) {
97
- log("Dependency added", key)
98
- requireReload = true;
95
+ if (newPackageJson.dependencies) {
96
+ for (const key in newPackageJson.dependencies) {
97
+ if (packageJson.dependencies[key] !== newPackageJson.dependencies[key] && newPackageJson.dependencies[key] !== undefined) {
98
+ log("Dependency added", key)
99
+ requireReload = true;
100
+ requireInstall = true;
101
+ }
99
102
  }
100
103
  }
104
+ if (packageJson.devDependencies) {
105
+ for (const key in packageJson.devDependencies) {
106
+ if (packageJson.devDependencies[key] !== newPackageJson.devDependencies[key] && newPackageJson.devDependencies[key] !== undefined) {
107
+ log("DevDependency added", key)
108
+ requireReload = true;
109
+ requireInstall = true;
110
+ }
111
+ }
112
+ }
101
113
 
102
114
 
103
115
  packageJsonSize = packageJsonStat.size;
@@ -111,7 +123,6 @@
111
123
  }
112
124
 
113
125
  function testIfInstallIsRequired(projectDir, packageJson) {
114
-
115
126
  if (packageJson.dependencies) {
116
127
  for (const key in packageJson.dependencies) {
117
128
  // make sure the dependency is installed
@@ -124,7 +135,7 @@
124
135
  // check first the value in case it's a absolute path
125
136
  if (!existsSync(dirPath)) {
126
137
  // then check concatenated with the project dir in case it's a relative path
127
- const relPath = path.join(projectDir, val.substr(5));
138
+ const relPath = path.join(projectDir, val.substr(5));
128
139
  if (!existsSync(relPath)) {
129
140
  // if neither directories exist then the local path is invalid and we ignore it
130
141
  continue;
@@ -134,6 +145,30 @@
134
145
  log("Dependency not installed", key)
135
146
  return true;
136
147
  }
148
+ else {
149
+ let expectedVersion = "";
150
+ const value = packageJson.dependencies[key];
151
+ if (value.startsWith("file:")) {
152
+ const packageJsonPath = path.join(projectDir, value.substr(5), "package.json");
153
+ if (existsSync(packageJsonPath)) {
154
+ expectedVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
155
+ }
156
+ }
157
+ else {
158
+ expectedVersion = value;
159
+ }
160
+ if (expectedVersion?.length > 0) {
161
+ const isRange = expectedVersion.includes("^") || expectedVersion.includes(">") || expectedVersion.includes("<");
162
+ if (!isRange) {
163
+ const packageJsonPath = path.join(depPath, "package.json");
164
+ const installedVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
165
+ if (expectedVersion !== installedVersion) {
166
+ log(`Dependency ${key} is installed but version is not correct. Expected ${expectedVersion} but got ${installedVersion}`)
167
+ return true;
168
+ }
169
+ }
170
+ }
171
+ }
137
172
  }
138
173
  }
139
174
  return false;
src/engine-components/Animation.ts CHANGED
@@ -61,14 +61,18 @@
61
61
  this.animations = animations;
62
62
  }
63
63
 
64
+ private _tempAnimationsArray: AnimationClip[] | undefined;
64
65
  set animations(animations: AnimationClip[]) {
65
- if(animations === null || animations === undefined || !Array.isArray(animations)) return;
66
- // if (debug) console.warn("assign animations", this.name, animations, this.gameObject);
67
- this.gameObject.animations = animations;
66
+ if (animations === null || animations === undefined || !Array.isArray(animations)) return;
67
+ if (this.gameObject)
68
+ this.gameObject.animations = animations;
69
+ else {
70
+ this._tempAnimationsArray = animations;
71
+ }
68
72
  }
69
73
 
70
74
  get animations(): AnimationClip[] {
71
- return this.gameObject.animations || [];
75
+ return this.gameObject.animations || this._tempAnimationsArray || [];
72
76
  }
73
77
  /**
74
78
  * @deprecated Currently unsupported
@@ -100,6 +104,10 @@
100
104
 
101
105
  awake() {
102
106
  if (debug) console.log("Animation Awake", this.name, this);
107
+ if (this._tempAnimationsArray) {
108
+ this.animations = this._tempAnimationsArray;
109
+ this._tempAnimationsArray = undefined;
110
+ }
103
111
  if (this._tempAnimationClipBeforeGameObjectExisted) {
104
112
  this.clip = this._tempAnimationClipBeforeGameObjectExisted;
105
113
  this._tempAnimationClipBeforeGameObjectExisted = null;
src/engine-components/AnimatorController.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Animator } from "./Animator.js";
2
- import { AnimatorConditionMode, AnimatorControllerModel, AnimatorControllerParameterType, AnimatorStateInfo, Condition, createMotion, State, StateMachineBehaviour } from "../engine/extensions/NEEDLE_animator_controller_model.js";
2
+ import { AnimatorConditionMode, AnimatorControllerModel, AnimatorControllerParameterType, AnimatorStateInfo, Condition, createMotion, State, StateMachineBehaviour, Transition } from "../engine/extensions/NEEDLE_animator_controller_model.js";
3
3
  import { AnimationAction, AnimationClip, AnimationMixer, AxesHelper, Euler, KeyframeTrack, LoopOnce, Object3D, Quaternion, Vector3 } from "three";
4
4
  import { deepClone, getParam } from "../engine/engine_utils.js";
5
5
  import { Context } from "../engine/engine_setup.js";
@@ -8,12 +8,86 @@
8
8
  import { Mathf } from "../engine/engine_math.js";
9
9
  import { isAnimationAction } from "../engine/engine_three_utils.js";
10
10
  import { isDevEnvironment } from "../engine/debug/index.js";
11
+ import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
11
12
 
12
13
  const debug = getParam("debuganimatorcontroller");
13
14
  const debugRootMotion = getParam("debugrootmotion");
14
15
 
16
+ function stringToHash(str): number {
17
+ let hash = 0;
18
+ for (let i = 0; i < str.length; i++) {
19
+ const char = str.charCodeAt(i);
20
+ hash = ((hash << 5) - hash) + char;
21
+ hash = hash & hash;
22
+ }
23
+ return hash;
24
+ }
25
+
26
+ declare type CreateAnimatorControllerOptions = {
27
+ /** Should each animationstate loop */
28
+ looping?: boolean,
29
+ /** Set to false to disable generating transitions between animationclips */
30
+ autoTransition?: boolean,
31
+ /** Set to a positive value in seconds for transition duration between states */
32
+ transitionDuration?: number,
33
+ }
34
+
15
35
  export class AnimatorController {
16
36
 
37
+ /** Create an animatorcontroller with clips assigned */
38
+ static createFromClips(clips: AnimationClip[], options: CreateAnimatorControllerOptions = { looping: false, autoTransition: true, transitionDuration: 0 }): AnimatorController {
39
+ const states: State[] = [];
40
+ for (let i = 0; i < clips.length; i++) {
41
+ const clip = clips[i];
42
+ const transitions: Transition[] = [];
43
+
44
+ if (options.autoTransition !== false) {
45
+ const dur = options.transitionDuration ?? 0;
46
+ const normalizedDuration = dur / clip.duration;
47
+ // automatically transition to self by default
48
+ let nextState = i;
49
+ if (options.autoTransition === undefined || options.autoTransition === true) {
50
+ nextState = (i + 1) % clips.length;
51
+ }
52
+ transitions.push({
53
+ exitTime: 1 - normalizedDuration,
54
+ offset: 0,
55
+ duration: dur,
56
+ hasExitTime: true,
57
+ destinationState: nextState,
58
+ conditions: [],
59
+ })
60
+ }
61
+
62
+ const state: State = {
63
+ name: clip.name,
64
+ hash: stringToHash(clip.name),
65
+ motion: {
66
+ name: clip.name,
67
+ clip: clip,
68
+ isLooping: options?.looping ?? false,
69
+ },
70
+ transitions: transitions,
71
+ behaviours: []
72
+ }
73
+ states.push(state);
74
+ }
75
+ const model: AnimatorControllerModel = {
76
+ name: "AnimatorController",
77
+ guid: new InstantiateIdProvider(Date.now()).generateUUID(),
78
+ parameters: [],
79
+ layers: [{
80
+ name: "Base Layer",
81
+ stateMachine: {
82
+ defaultState: 0,
83
+ states: states
84
+ }
85
+ }]
86
+ }
87
+ const controller = new AnimatorController(model);
88
+ return controller;
89
+ }
90
+
17
91
  play(name: string | number, layerIndex: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, durationInSec: number = 0) {
18
92
  if (layerIndex < 0) layerIndex = 0;
19
93
  else if (layerIndex >= this.model.layers.length) {
@@ -361,7 +435,12 @@
361
435
  if (isSelf) {
362
436
  const motion = state.motion;
363
437
  if (!motion.action_loopback && motion.clip) {
438
+ // uncache action immediately resets the applied animation which breaks the root motion
439
+ // this happens if we have a transition to self and the clip is not cached yet
440
+ const previousMatrix = this.rootMotionHandler ? this.animator.gameObject.matrix.clone() : null;
364
441
  this._mixer.uncacheAction(motion.clip, this.animator.gameObject);
442
+ if (previousMatrix)
443
+ previousMatrix.decompose(this.animator.gameObject.position, this.animator.gameObject.quaternion, this.animator.gameObject.scale);
365
444
  motion.action_loopback = this.createAction(motion.clip);
366
445
  }
367
446
  }
@@ -688,18 +767,18 @@
688
767
 
689
768
  onStart(action: AnimationAction) {
690
769
  if (action.getClip() !== this.clip) return;
691
-
692
- if (!RootMotionAction.lastObjRotation[this.root.uuid])
693
- RootMotionAction.lastObjRotation[this.root.uuid] = new Quaternion();
770
+
771
+ if (!RootMotionAction.lastObjRotation[this.root.uuid]) {
772
+ RootMotionAction.lastObjRotation[this.root.uuid] = this.root.quaternion.clone()
773
+ }
694
774
  const lastRotation = RootMotionAction.lastObjRotation[this.root.uuid];
695
775
  // const firstKeyframe = RootMotionAction.firstKeyframeRotation[this.clip.uuid];
696
776
  // lastRotation.invert().premultiply(firstKeyframe).invert();
697
777
  RootMotionAction.spaceRotation[this.clip.uuid].copy(lastRotation);
698
778
 
699
- if (debugRootMotion)
700
- {
779
+ if (debugRootMotion) {
701
780
  const euler = new Euler().setFromQuaternion(lastRotation);
702
- console.log("START", this.clip.name, Mathf.toDegrees(euler.y));
781
+ console.log("START", this.clip.name, Mathf.toDegrees(euler.y), this.root.position.z);
703
782
  }
704
783
  }
705
784
 
@@ -707,21 +786,24 @@
707
786
  return RootMotionAction.clipOffsetRotation[this.clip.uuid];
708
787
  }
709
788
 
789
+ private _prevTime = 0;
790
+
710
791
  private handlePosition(_clip: AnimationClip, track: KeyframeTrack | null) {
711
792
  if (track) {
793
+
712
794
  const root = this.root;
713
795
  if (debugRootMotion)
714
796
  root.add(new AxesHelper());
715
- if (!RootMotionAction.lastObjPosition[root.uuid]) RootMotionAction.lastObjPosition[root.uuid] = new Vector3();
797
+ if (!RootMotionAction.lastObjPosition[root.uuid])
798
+ RootMotionAction.lastObjPosition[root.uuid] = this.root.position.clone();
799
+
716
800
  const valuesDiff = new Vector3();
717
801
  const valuesPrev = new Vector3();
718
- let prevTime: number = 0;
719
802
  // const rotation = new Quaternion();
720
803
  this.positionWrapper = new TrackEvaluationWrapper(track, (time, value: Float64Array) => {
721
804
 
722
805
  const weight = this.action.getEffectiveWeight();
723
806
 
724
- // root.position.copy(RootMotionAction.lastObjPosition[root.uuid]);
725
807
 
726
808
  // reset for testing
727
809
  if (debugRootMotion) {
@@ -729,22 +811,22 @@
729
811
  root.position.set(0, root.position.y, 0);
730
812
  }
731
813
 
732
-
733
- if (time > prevTime) {
814
+ if (time > this._prevTime) {
734
815
  valuesDiff.set(value[0], value[1], value[2]);
735
816
  valuesDiff.sub(valuesPrev);
736
817
  valuesDiff.multiplyScalar(weight);
737
818
  valuesDiff.applyQuaternion(this.getClipRotationOffset());
738
819
 
739
- const id = this.clip.uuid;
740
820
  // RootMotionAction.effectiveSpaceRotation[id].slerp(RootMotionAction.spaceRotation[id], weight);
741
- valuesDiff.applyQuaternion(RootMotionAction.spaceRotation[id]);
821
+ valuesDiff.applyQuaternion(RootMotionAction.spaceRotation[this.clip.uuid]);
742
822
  this.positionChange.copy(valuesDiff);
743
- // root.position.add(valuesDiff);
823
+
824
+ // this.root.position.add(valuesDiff);
744
825
  }
745
- // RootMotionAction.lastObjPosition[root.uuid].copy(root.position);
826
+
746
827
  valuesPrev.fromArray(value);
747
- prevTime = time;
828
+
829
+ this._prevTime = time;
748
830
  value[0] = 0;
749
831
  value[1] = 0;
750
832
  value[2] = 0;
@@ -770,7 +852,7 @@
770
852
 
771
853
 
772
854
  const root = this.root;
773
- if (!RootMotionAction.lastObjRotation[root.uuid]) RootMotionAction.lastObjRotation[root.uuid] = new Quaternion();
855
+ // if (!RootMotionAction.lastObjRotation[root.uuid]) RootMotionAction.lastObjRotation[root.uuid] = new Quaternion();
774
856
  // const temp = new Quaternion();
775
857
  let prevTime: number = 0;
776
858
  const valuesPrev = new Quaternion();
@@ -808,16 +890,13 @@
808
890
  this.rotationChange.set(0, 0, 0, 1);
809
891
  }
810
892
 
811
- onAfterUpdate() {
812
- if (!this.action) return;
893
+ onAfterUpdate(): boolean {
894
+ if (!this.action) return false;
813
895
  const weight = this.action.getEffectiveWeight();
896
+ if (weight <= 0) return false;
814
897
  this.positionChange.multiplyScalar(weight);
815
898
  this.rotationChange.slerp(RootMotionAction.identityQuaternion, 1 - weight);
816
- // const root = this.root;
817
- // RootMotionAction.lastObjRotation[root.uuid].slerp(root.quaternion, weight);
818
- // if (weight > .5) {
819
- // }
820
- // this.obj.position.copy(this.lastPos).add(this.positionDiff);
899
+ return true;
821
900
  }
822
901
  }
823
902
 
@@ -827,6 +906,10 @@
827
906
  private handler: RootMotionAction[] = [];
828
907
  private root!: Object3D;
829
908
 
909
+
910
+ private basePosition: Vector3 = new Vector3();
911
+ private baseRotation: Quaternion = new Quaternion();
912
+
830
913
  constructor(controller: AnimatorController) {
831
914
  this.controller = controller;
832
915
  }
@@ -849,13 +932,19 @@
849
932
  return action;
850
933
  }
851
934
 
935
+
852
936
  onStart(action: AnimationAction) {
853
937
  for (const handler of this.handler) {
854
938
  handler.onStart(action);
855
939
  }
940
+
856
941
  }
857
942
 
858
943
  onBeforeUpdate() {
944
+ // capture the position of the object
945
+ this.basePosition.copy(this.root.position);
946
+ this.baseRotation.copy(this.root.quaternion);
947
+
859
948
  for (const hand of this.handler)
860
949
  hand.onBeforeUpdate();
861
950
  }
@@ -864,12 +953,17 @@
864
953
  private summedRotation: Quaternion = new Quaternion();
865
954
 
866
955
  onAfterUpdate() {
956
+ // apply the accumulated changes
957
+ this.root.position.copy(this.basePosition);
958
+ this.root.quaternion.copy(this.baseRotation);
959
+
867
960
  this.summedPosition.set(0, 0, 0);
868
961
  this.summedRotation.set(0, 0, 0, 1);
869
962
  for (const entry of this.handler) {
870
- entry.onAfterUpdate();
871
- this.summedPosition.add(entry.positionChange);
872
- this.summedRotation.multiply(entry.rotationChange);
963
+ if (entry.onAfterUpdate()) {
964
+ this.summedPosition.add(entry.positionChange);
965
+ this.summedRotation.multiply(entry.rotationChange);
966
+ }
873
967
  }
874
968
  this.root.position.add(this.summedPosition);
875
969
  this.root.quaternion.multiply(this.summedRotation);
@@ -881,8 +975,6 @@
881
975
  if (!tracks) return null;
882
976
  for (const track of tracks) {
883
977
  if (track.name.endsWith(name)) {
884
- // if (track.name.includes("Hips"))
885
- // return track;
886
978
  return track;
887
979
  }
888
980
  }
src/engine/engine_context.ts CHANGED
@@ -420,8 +420,8 @@
420
420
  // avoid setting pixel values here since this can cause pingpong updates
421
421
  // e.g. when system scale is set to 125%
422
422
  // https://github.com/needle-tools/needle-engine-support/issues/69
423
- // this.renderer.domElement.style.width = "100%";
424
- // this.renderer.domElement.style.height = "100%";
423
+ this.renderer.domElement.style.width = "100%";
424
+ this.renderer.domElement.style.height = "100%";
425
425
  if (this.composer) {
426
426
  this.composer.setSize?.call(this.composer, width, height);
427
427
  if ("setPixelRatio" in this.composer && typeof this.composer.setPixelRatio === "function")
src/engine/engine_scenetools.ts CHANGED
@@ -87,6 +87,10 @@
87
87
  async function handleLoadedGltf(context: Context, gltfId: string, gltf, seed: number | null | UIDProvider, componentsExtension) {
88
88
  if (printGltf)
89
89
  console.warn("glTF", gltfId, gltf);
90
+ // Remove query parameters from gltfId
91
+ if (gltfId.includes("?")) {
92
+ gltfId = gltfId.split("?")[0];
93
+ }
90
94
  await getLoader().createBuiltinComponents(context, gltfId, gltf, seed, componentsExtension);
91
95
  }
92
96
 
src/engine/engine_utils_screenshot.ts CHANGED
@@ -30,6 +30,15 @@
30
30
  if (!width) width = prevWidth;
31
31
  if (!height) height = prevHeight;
32
32
 
33
+ // apply page zoom
34
+ const zoomLevel = window.devicePixelRatio || 1;
35
+ width /= zoomLevel;
36
+ height /= zoomLevel;
37
+
38
+ // reset style during screenshot
39
+ context.renderer.domElement.style.width = width + "px";
40
+ context.renderer.domElement.style.height = height + "px";
41
+
33
42
  try {
34
43
  const canvas = context.renderer.domElement;
35
44
 
src/engine/extensions/NEEDLE_animator_controller_model.ts CHANGED
@@ -86,9 +86,9 @@
86
86
  export declare type Motion = {
87
87
  name: string,
88
88
  isLooping: boolean,
89
- guid: string,
89
+ guid?: string,
90
90
  /** clip index in gltf animations array */
91
- index: number,
91
+ index?: number,
92
92
  /** the resolved clip */
93
93
  clip?: AnimationClip,
94
94
  /** the clip mapping -> which object has which animationclip */
@@ -116,12 +116,12 @@
116
116
  }
117
117
 
118
118
  export declare type Transition = {
119
- isExit: boolean;
119
+ isExit?: boolean;
120
120
  exitTime: number,
121
- hasFixedDuration: boolean,
121
+ hasFixedDuration?: boolean,
122
122
  offset: number,
123
123
  duration: number,
124
- hasExitTime: number,
124
+ hasExitTime: number | boolean,
125
125
  destinationState: number | State,
126
126
  conditions: Condition[],
127
127
  // isAny?: boolean
src/engine-components/OrbitControls.ts CHANGED
@@ -323,7 +323,6 @@
323
323
  this._controls.enableDamping = true;
324
324
  const factor = typeof smoothcam === "number" ? smoothcam : .99;
325
325
  this._controls.dampingFactor = Math.max(.001, 1 - Math.min(1, factor));
326
- console.log(this._controls.dampingFactor)
327
326
  }
328
327
  //@ts-ignore
329
328
  // this._controls.zoomToCursor = this.zoomToCursor;
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -145,6 +145,7 @@
145
145
  private static _imageElements: Map<string, ImageBitmap | null> = new Map();
146
146
 
147
147
  awake(): void {
148
+ if(debug) console.log(this)
148
149
  if (!this.trackedImages) return;
149
150
  for (const trackedImage of this.trackedImages) {
150
151
  if (trackedImage.image) {