Needle Engine

Changes between version 3.10.5-beta and 3.10.6-beta
Files changed (10) hide show
  1. src/engine-components/AnimationUtils.ts +26 -0
  2. src/engine-components/Animator.ts +4 -1
  3. src/engine/engine_physics_rapier.ts +1 -1
  4. src/engine/engine_scenelighting.ts +7 -0
  5. src/engine/engine_serialization_builtin_serializer.ts +5 -0
  6. src/engine/extensions/NEEDLE_techniques_webgl.ts +36 -3
  7. src/engine-components/ParticleSystem.ts +37 -7
  8. src/engine-components/ParticleSystemModules.ts +4 -4
  9. src/engine-components/timeline/PlayableDirector.ts +19 -2
  10. src/engine-components/timeline/TimelineTracks.ts +28 -17
src/engine-components/AnimationUtils.ts CHANGED
@@ -5,7 +5,33 @@
5
5
  import { Animation } from "./Animation";
6
6
  import { GameObject } from "./Component";
7
7
  import { PlayableDirector } from "./timeline/PlayableDirector";
8
+ import { Object3D } from "three";
8
9
 
10
+
11
+ const $objectAnimationKey = Symbol("objectIsAnimatedData");
12
+
13
+ /** Internal method - This marks an object as being animated. Make sure to always call isAnimated=false if you stop animating the object
14
+ * @param obj The object to mark
15
+ * @param isAnimated Whether the object is animated or not
16
+ */
17
+ export function setObjectAnimated(obj: Object3D, isAnimated: boolean) {
18
+ if (obj[$objectAnimationKey] === undefined) {
19
+ if (!isAnimated) return;
20
+ obj[$objectAnimationKey] = 1;
21
+ }
22
+ else {
23
+ if (isAnimated)
24
+ obj[$objectAnimationKey] += 1;
25
+ else if (obj[$objectAnimationKey] > 0)
26
+ obj[$objectAnimationKey] -= 1;
27
+ }
28
+ }
29
+
30
+ /** Get is the object is currently animated. Currently used by the Animator to check if a timeline animationtrack is actively animating an object */
31
+ export function getObjectAnimated(obj: Object3D): boolean {
32
+ return obj[$objectAnimationKey] !== undefined && obj[$objectAnimationKey] > 0;
33
+ }
34
+
9
35
  ContextRegistry.registerCallback(ContextEvent.ContextCreated, args => {
10
36
  const autoplay = args.context.domElement.getAttribute("autoplay");
11
37
  if (autoplay !== undefined && (autoplay === "" || autoplay === "true")) {
src/engine-components/Animator.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import { AnimatorController } from "./AnimatorController";
6
6
  import { serializable } from "../engine/engine_serialization_decorator";
7
7
  import { Mathf } from "../engine/engine_math";
8
+ import { getObjectAnimated } from "./AnimationUtils";
8
9
 
9
10
  const debug = getParam("debuganimator");
10
11
 
@@ -102,7 +103,7 @@
102
103
  /**@deprecated use setTrigger */
103
104
  SetTrigger(name: string | number) { this.setTrigger(name); }
104
105
  setTrigger(name: string | number) {
105
- if(debug) console.log("SetTrigger", name);
106
+ if (debug) console.log("SetTrigger", name);
106
107
  this.runtimeAnimatorController?.setTrigger(name);
107
108
  }
108
109
 
@@ -168,6 +169,8 @@
168
169
  }
169
170
 
170
171
  onBeforeRender() {
172
+ if (getObjectAnimated(this.gameObject)) return;
173
+
171
174
  if (this._animatorController) {
172
175
  this._animatorController.update();
173
176
  }
src/engine/engine_physics_rapier.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  import { isDevEnvironment } from './debug/debug';
25
25
 
26
26
  const debugPhysics = getParam("debugphysics");
27
- const debugColliderPlacement = getParam("debugphysicscolliders");
27
+ const debugColliderPlacement = getParam("debugcolliderplacement");
28
28
  const debugCollisions = getParam("debugcollisions");
29
29
  const showColliders = getParam("showcolliders");
30
30
 
src/engine/engine_scenelighting.ts CHANGED
@@ -49,6 +49,13 @@
49
49
  }
50
50
 
51
51
  private _timevec4: Vector4 = new Vector4();
52
+
53
+ /** Time data used for custom shaders
54
+ * x: time
55
+ * y: sin(time)
56
+ * z: cos(time)
57
+ * w: deltaTime
58
+ */
52
59
  get timeVec4(): Vector4 {
53
60
  return this._timevec4;
54
61
  }
src/engine/engine_serialization_builtin_serializer.ts CHANGED
@@ -74,6 +74,11 @@
74
74
  onDeserialize(data: ObjectData | string | null, context: SerializationContext) {
75
75
 
76
76
  if (typeof data === "string") {
77
+ if (data.endsWith(".glb") || data.endsWith(".gltf")) {
78
+ if (isDevEnvironment())
79
+ showBalloonWarning("Detected wrong usage of @serializable with Object3D or GameObject. Instead you should use AssetReference here! Please see the console for details.");
80
+ console.warn("Wrong usage of @serializable detected:\n\nIt looks like you used @serializable(Object3D) or @serializable(GameObject) for a prefab or scene reference named \"" + context.path + "\" in your script \"" + context.target?.constructor?.name + "\". \nTo fix this replace it with @serializable(AssetReference). Make sure to replace the type with AssetReference too.\n\nIt should then look something like this:\n\n@serializable(AssetReference)\n" + context.path + "! : AssetReference;\n\0");
81
+ }
77
82
  // ACTUALLY: this is already handled by the extension_utils where we resolve json pointers recursively
78
83
  // if(data.startsWith("/nodes/")){
79
84
  // const node = parseInt(data.substring("/nodes/".length));
src/engine/extensions/NEEDLE_techniques_webgl.ts CHANGED
@@ -212,9 +212,34 @@
212
212
  if (this.uniforms["_TimeParameters"]) {
213
213
  this.uniforms["_TimeParameters"].value = context.sceneLighting.timeVec4;
214
214
  }
215
- else if (this.uniforms["_Time"]) {
216
- this.uniforms["_Time"].value = context.sceneLighting.timeVec4;
215
+ if (this.uniforms["_Time"]) {
216
+ const _time = this.uniforms["_Time"].value as Vector4;
217
+ _time.x = context.sceneLighting.timeVec4.x / 20;
218
+ _time.y = context.sceneLighting.timeVec4.x;
219
+ _time.z = context.sceneLighting.timeVec4.x * 2;
220
+ _time.w = context.sceneLighting.timeVec4.x * 3;
217
221
  }
222
+ if (this.uniforms["_SinTime"]) {
223
+ const _time = this.uniforms["_SinTime"].value as Vector4;
224
+ _time.x = Math.sin(context.sceneLighting.timeVec4.x / 8);
225
+ _time.y = Math.sin(context.sceneLighting.timeVec4.x / 4);
226
+ _time.z = Math.sin(context.sceneLighting.timeVec4.x / 2);
227
+ _time.w = Math.sin(context.sceneLighting.timeVec4.x);
228
+ }
229
+ if (this.uniforms["_CosTime"]) {
230
+ const _time = this.uniforms["_CosTime"].value as Vector4;
231
+ _time.x = Math.cos(context.sceneLighting.timeVec4.x / 8);
232
+ _time.y = Math.cos(context.sceneLighting.timeVec4.x / 4);
233
+ _time.z = Math.cos(context.sceneLighting.timeVec4.x / 2);
234
+ _time.w = Math.cos(context.sceneLighting.timeVec4.x);
235
+ }
236
+ if (this.uniforms["unity_DeltaTime"]) {
237
+ const _time = this.uniforms["unity_DeltaTime"].value as Vector4;
238
+ _time.x = context.time.deltaTime;
239
+ _time.y = 1 / context.time.deltaTime;
240
+ _time.z = context.time.smoothedDeltaTime;
241
+ _time.w = 1 / context.time.smoothedDeltaTime;
242
+ }
218
243
 
219
244
  const mainLight: ILight | null = context.mainLight;
220
245
  if (mainLight) {
@@ -340,9 +365,17 @@
340
365
  const uniforms: {} = {};
341
366
  const techniqueUniforms = technique.uniforms;
342
367
 
343
- if (vert.includes("_Time"))
368
+ // BiRP time uniforms
369
+ if (vert.includes("_Time") || frag.includes("_Time"))
344
370
  uniforms["_Time"] = { value: new Vector4(0, 0, 0, 0) };
371
+ if (vert.includes("_SinTime") || frag.includes("_SinTime"))
372
+ uniforms["_SinTime"] = { value: new Vector4(0, 0, 0, 0) };
373
+ if (vert.includes("_CosTime") || frag.includes("_CosTime"))
374
+ uniforms["_CosTime"] = { value: new Vector4(0, 0, 0, 0) };
375
+ if (vert.includes("unity_DeltaTime") || frag.includes("unity_DeltaTime"))
376
+ uniforms["unity_DeltaTime"] = { value: new Vector4(0, 0, 0, 0) };
345
377
 
378
+
346
379
  for (const u in techniqueUniforms) {
347
380
  const uniformName = u;
348
381
  // const uniformValues = techniqueUniforms[u];
src/engine-components/ParticleSystem.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Behaviour, GameObject } from "./Component";
2
2
  import * as THREE from "three";
3
- import { MainModule, EmissionModule, ShapeModule, ParticleSystemShapeType, MinMaxCurve, MinMaxGradient, ColorOverLifetimeModule, SizeOverLifetimeModule, NoiseModule, ParticleSystemSimulationSpace, ParticleBurst, IParticleSystem, ParticleSystemRenderMode, TrailModule, VelocityOverLifetimeModule, TextureSheetAnimationModule, RotationOverLifetimeModule, LimitVelocityOverLifetimeModule, RotationBySpeedModule, InheritVelocityModule, SizeBySpeedModule, ColorBySpeedModule } from "./ParticleSystemModules"
3
+ import { MainModule, EmissionModule, ShapeModule, ParticleSystemShapeType, MinMaxCurve, MinMaxGradient, ColorOverLifetimeModule, SizeOverLifetimeModule, NoiseModule, ParticleSystemSimulationSpace, ParticleBurst, IParticleSystem, ParticleSystemRenderMode, TrailModule, VelocityOverLifetimeModule, TextureSheetAnimationModule, RotationOverLifetimeModule, LimitVelocityOverLifetimeModule, RotationBySpeedModule, InheritVelocityModule, SizeBySpeedModule, ColorBySpeedModule, ParticleSystemScalingMode } from "./ParticleSystemModules"
4
4
  import { getParam } from "../engine/engine_utils";
5
5
 
6
6
  // https://github.dev/creativelifeform/three-nebula
@@ -18,7 +18,7 @@
18
18
  import { ParticleSubEmitter } from "./ParticleSystemSubEmitter";
19
19
  import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive";
20
20
  import { Gizmos } from "../engine/engine_gizmos";
21
- import { isDevEnvironment } from "../engine/debug";
21
+ import { isDevEnvironment, showBalloonWarning } from "../engine/debug";
22
22
 
23
23
  const debug = getParam("debugparticles");
24
24
  const suppressProgressiveLoading = getParam("noprogressive");
@@ -327,6 +327,7 @@
327
327
  }
328
328
 
329
329
  const $sizeLerpFactor = Symbol("sizeLerpFactor");
330
+ const localScaleVec3 = new Vector3();
330
331
  class SizeBehaviour extends ParticleSystemBaseBehaviour {
331
332
 
332
333
  type: string = "NeedleSize";
@@ -347,6 +348,10 @@
347
348
  size *= this.system.sizeOverLifetime.evaluate(age01, undefined, particle[$sizeLerpFactor]).x;
348
349
  const scaleFactor = this.system.worldScale.x / this.system.cameraScale;
349
350
  particle.size = particle.startSize * size * scaleFactor;
351
+ if (this.system.localspace) {
352
+ const scale = getLocalSimulationScale(this.system, localScaleVec3);
353
+ particle.size *= scale.x;
354
+ }
350
355
  // in Unity this is viewport size, we don't really support this yet (and the renderer is logging a warning)
351
356
  // so for now it's disabled again
352
357
  // particle.size = Mathf.clamp(particle.size, this._minSize, this._maxSize);
@@ -770,6 +775,9 @@
770
775
  get worldspace() {
771
776
  return this.main.simulationSpace === ParticleSystemSimulationSpace.World;
772
777
  }
778
+ get localspace() {
779
+ return this.main.simulationSpace === ParticleSystemSimulationSpace.Local;
780
+ }
773
781
 
774
782
  private __worldQuaternion = new Quaternion();
775
783
  get worldQuaternion(): Quaternion {
@@ -999,11 +1007,7 @@
999
1007
 
1000
1008
  // Handle LOCALSPACE
1001
1009
  if (isLocalSpace && this._container && this.gameObject?.parent) {
1002
- // TODO: there's probably a more efficient way to do this
1003
- const scale = getWorldScale(this.gameObject.parent);
1004
- scale.x = 1 / scale.x;
1005
- scale.y = 1 / scale.y;
1006
- scale.z = 1 / scale.z;
1010
+ const scale = getLocalSimulationScale(this, temp3);
1007
1011
  this._container.matrix.makeScale(scale.x, scale.y, scale.z);
1008
1012
  this._container.matrix.makeRotationFromQuaternion(this.__worldQuaternion);
1009
1013
  this._container.matrix.setPosition(this.worldPos);
@@ -1063,4 +1067,30 @@
1063
1067
  console.warn("Could not find particle system for sub emitter", guid, gameObject, this);
1064
1068
  }
1065
1069
  }
1070
+ }
1071
+
1072
+
1073
+ function getLocalSimulationScale(system: ParticleSystem, vec: Vector3) {
1074
+ vec.set(1, 1, 1);
1075
+ if (system.gameObject.parent && system.localspace) {
1076
+ switch (system.main.scalingMode) {
1077
+ case ParticleSystemScalingMode.Local:
1078
+ vec = getWorldScale(system.gameObject.parent, vec);
1079
+ vec.x = 1 / vec.x;
1080
+ vec.y = 1 / vec.y;
1081
+ vec.z = 1 / vec.z;
1082
+ break;
1083
+ default:
1084
+ if (!system["unsupported_scaling_mode"]) {
1085
+ system["unsupported_scaling_mode"] = true;
1086
+ const msg = "ParticleSystem scale mode " + ParticleSystemScalingMode[system.main.scalingMode] + " is not supported";
1087
+ if (isDevEnvironment())
1088
+ showBalloonWarning(msg);
1089
+ console.warn(msg, system.name, system);
1090
+ }
1091
+ vec = getWorldScale(system.gameObject, vec);
1092
+ break;
1093
+ }
1094
+ }
1095
+ return vec;
1066
1096
  }
src/engine-components/ParticleSystemModules.ts CHANGED
@@ -270,10 +270,10 @@
270
270
  }
271
271
  }
272
272
 
273
- declare type ParticleSystemScalingMode = {
274
- Hierarchy: number;
275
- Local: number;
276
- Shape: number;
273
+ export enum ParticleSystemScalingMode {
274
+ Hierarchy = 0,
275
+ Local = 1,
276
+ Shape = 2,
277
277
  }
278
278
 
279
279
  export class MainModule {
src/engine-components/timeline/PlayableDirector.ts CHANGED
@@ -110,6 +110,9 @@
110
110
  for (const track of this._customTracks) {
111
111
  track.onEnable?.();
112
112
  }
113
+ for (const track of this._animationTracks) {
114
+ track.onEnable?.();
115
+ }
113
116
  if (this.playOnAwake) {
114
117
  this.play();
115
118
  }
@@ -135,6 +138,9 @@
135
138
  for (const track of this._customTracks) {
136
139
  track.onDisable?.();
137
140
  }
141
+ for (const track of this._animationTracks) {
142
+ track.onDisable?.();
143
+ }
138
144
  if (this._visibilityChangeEvt)
139
145
  window.removeEventListener('visibilitychange', this._visibilityChangeEvt);
140
146
  }
@@ -296,6 +302,12 @@
296
302
  if (track.muted) continue;
297
303
  switch (track.type) {
298
304
  case Models.TrackType.Activation:
305
+ // when the timeline is being disabled or stopped
306
+ // then we want to leave objects active state as they were
307
+ // see NE-3241
308
+ // TODO: support all "post-playback-state" settings an activation track has, this is just "Leave as is"
309
+ if (!this._isPlaying) continue;
310
+
299
311
  for (let i = 0; i < track.outputs.length; i++) {
300
312
  const binding = track.outputs[i];
301
313
  if (typeof binding === "object") {
@@ -306,8 +318,13 @@
306
318
  }
307
319
  }
308
320
  const obj = binding as THREE.Object3D;
309
- if (obj.visible !== undefined)
310
- obj.visible = isActive;
321
+ if (obj.visible !== undefined) {
322
+ if (obj.visible !== isActive) {
323
+ obj.visible = isActive;
324
+ if (debug)
325
+ console.warn(this.name, "set ActivationTrack-" + i, obj.name, isActive, time);
326
+ }
327
+ }
311
328
  }
312
329
  }
313
330
  break;
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import { getParam, resolveUrl } from "../../engine/engine_utils";
9
9
  import { AudioSource } from "../AudioSource";
10
10
  import { Animator } from "../Animator"
11
+ import { setObjectAnimated } from "../AnimationUtils";
11
12
 
12
13
  const debug = getParam("debugtimeline");
13
14
 
@@ -140,16 +141,23 @@
140
141
  private _actionOffsets: Array<AnimationClipOffsetData> = [];
141
142
  private _didBind: boolean = false;
142
143
  private _animator: Animator | null = null;
143
- private _animatorWasEnabled?: boolean = false;
144
144
 
145
145
  onPauseChanged() {
146
- // When the timeline is paused the original animator will be enabled again if it was before
147
- if (this._animator && this._animatorWasEnabled !== undefined) {
148
- this._animator.enabled = this.director.isPlaying === false ? this._animatorWasEnabled : false;
149
- }
146
+ if (this._animator)
147
+ setObjectAnimated(this._animator.gameObject, this.director.isPaused);
150
148
  }
151
149
 
150
+ onEnable() {
151
+ if (this._animator)
152
+ setObjectAnimated(this._animator.gameObject, true);
153
+ }
152
154
 
155
+ onDisable() {
156
+ if (this._animator)
157
+ setObjectAnimated(this._animator.gameObject, false);
158
+ }
159
+
160
+
153
161
  createHooks(clipModel: Models.AnimationClipModel, clip) {
154
162
  if (clip.tracks?.length <= 0) {
155
163
  console.warn("No tracks in AnimationClip", clip);
@@ -225,11 +233,6 @@
225
233
  // We need to disable the animator component in case it also animates
226
234
  // which overrides the timeline
227
235
  this._animator = GameObject.getComponent(this.target, Animator) ?? null;
228
- if (this._animator) {
229
- this._animatorWasEnabled = this._animator.enabled;
230
- this._animator.enabled = false;
231
- }
232
- else this._animatorWasEnabled = false;
233
236
  }
234
237
 
235
238
  // Clip Offsets
@@ -285,7 +288,7 @@
285
288
  if (!this.mixer) return;
286
289
  this.bind();
287
290
 
288
- if (this._animator && this.director.isPlaying && this.director.weight > 0) this._animator.enabled = false;
291
+ // if (this._animator && this.director.isPlaying && this.director.weight > 0) this._animator.enabled = false;
289
292
 
290
293
  this._totalOffsetPosition.set(0, 0, 0);
291
294
  this._totalOffsetRotation.set(0, 0, 0, 1);
@@ -731,6 +734,13 @@
731
734
  didTrigger: boolean[] = [];
732
735
  receivers: Array<SignalReceiver | null> = [];
733
736
 
737
+ // TODO: test when timeline signals are being reset in Unity
738
+ // onEnable() {
739
+ // for (let i = 0; i < this.didTrigger?.length; i++) {
740
+ // this.didTrigger[i] = false;
741
+ // }
742
+ // }
743
+
734
744
  // private _lastTime: number = -1;
735
745
 
736
746
  evaluate(time: number) {
@@ -750,20 +760,21 @@
750
760
  if (model.retroActive) {
751
761
  isActive = td <= 0.000001;
752
762
  }
753
- // TODO: handle signal asset at time 0 (only trigger when time is 0)
754
- // else if (model.time < .001) {
755
- // if (td <= 0.000001 && lastTime > time)
756
- // isActive = true;
757
- // }
758
763
  else {
759
764
  const abs = Math.abs(td);
760
- if (abs >= .00001 && abs < estimatedFrameLengthWithPadding) {
765
+ // e.g. if the signal is at frame 0 and the timeline duration also 0 (no tracks, just a signal at frame 0)
766
+ if (abs === 0) {
761
767
  isActive = true;
762
768
  }
769
+ else if (abs >= .00001 && abs < estimatedFrameLengthWithPadding) {
770
+ isActive = true;
771
+ }
763
772
  }
764
773
  // console.log(time, td, isActive);
765
774
  if (isActive) {
766
775
  if (!wasTriggered) {
776
+ if (debug)
777
+ console.log("Trigger signal", time, model.time, model);
767
778
  this.didTrigger[i] = true;
768
779
  // If a signal doesnt have any explicit receivers it will invoke the signal globally
769
780
  if (this.receivers?.length <= 0) {