Needle Engine

Changes between version 3.11.6 and 3.12.0-beta
Files changed (12) hide show
  1. src/engine/debug/debug_overlay.ts +0 -2
  2. src/engine/debug/debug.ts +9 -3
  3. src/engine/engine_addressables.ts +18 -2
  4. src/engine/engine_license.ts +40 -46
  5. src/engine/engine_networking_utils.ts +10 -6
  6. src/engine-components/ui/EventSystem.ts +4 -0
  7. src/engine-components/timeline/index.ts +2 -1
  8. src/engine-components/ParticleSystem.ts +19 -9
  9. src/engine-components/ParticleSystemSubEmitter.ts +3 -0
  10. src/engine-components/timeline/PlayableDirector.ts +33 -1
  11. src/engine-components/export/usdz/ThreeUSDZExporter.ts +2 -1
  12. src/engine-components/timeline/TimelineTracks.ts +63 -53
src/engine/debug/debug_overlay.ts CHANGED
@@ -6,9 +6,7 @@
6
6
  let hide = false;
7
7
  if (getParam("noerrors")) hide = true;
8
8
 
9
- const arContainerClassName = "ar";
10
9
  const globalErrorContainerKey = "needle_engine_global_error_container";
11
- const locationRegex = new RegExp(" at .+\/(.+?\.ts)", "g");
12
10
 
13
11
  export enum LogType {
14
12
  Log,
src/engine/debug/debug.ts CHANGED
@@ -6,16 +6,22 @@
6
6
  export { LogType, setAllowOverlayMessages };
7
7
 
8
8
  /** Displays a debug message on screen for a certain amount of time */
9
- export function showBalloonMessage(text: string, logType: LogType = LogType.Log) {
9
+ export function showBalloonMessage(text: string, logType: LogType = LogType.Log): void {
10
10
  addLog(logType, text);
11
11
  }
12
12
 
13
13
  /** Displays a warning message on screen for a certain amount of time */
14
- export function showBalloonWarning(text: string) {
14
+ export function showBalloonWarning(text: string): void {
15
15
  showBalloonMessage(text, LogType.Warn);
16
16
  }
17
17
 
18
+ let _manuallySetDevEnvironment: boolean | undefined;
19
+
18
20
  /** True when the application runs on a local url */
19
- export function isDevEnvironment(){
21
+ export function isDevEnvironment(): boolean {
22
+ if (_manuallySetDevEnvironment !== undefined) return _manuallySetDevEnvironment;
20
23
  return isLocalNetwork();
24
+ }
25
+ export function setDevEnvironment(val: boolean): void {
26
+ _manuallySetDevEnvironment = val;
21
27
  }
src/engine/engine_addressables.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { registerPrefabProvider, syncInstantiate } from "./engine_networking_instantiate.js";
7
7
  import { download } from "./engine_web_api.js";
8
8
  import { getLoader } from "./engine_gltf.js";
9
- import { SourceIdentifier } from "./engine_types.js";
9
+ import { IComponent, SourceIdentifier } from "./engine_types.js";
10
10
  import { destroy, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
11
11
  import { IGameObject } from "./engine_types.js";
12
12
 
@@ -63,7 +63,23 @@
63
63
 
64
64
  export class AssetReference {
65
65
 
66
- static getOrCreate(sourceId: SourceIdentifier, url: string, context: Context): AssetReference {
66
+ /**
67
+ * Get an AssetReference for a URL to be easily loaded. AssetReferences are cached so calling this method multiple times with the same arguments will always return the same AssetReference.
68
+ */
69
+ static getOrCreate(sourceId: SourceIdentifier | IComponent, url: string, context?: Context): AssetReference {
70
+
71
+ if (typeof sourceId === "string") {
72
+ if (!context) {
73
+ context = Context.Current;
74
+ if (!context)
75
+ throw new Error("Context is required when sourceId is a string. When you call this method from a component you can call it with \"getOrCreate(this, url)\" where \"this\" is the component.");
76
+ }
77
+ }
78
+ else {
79
+ context = sourceId.context as Context;
80
+ sourceId = sourceId.sourceId!;
81
+ }
82
+
67
83
  const fullPath = resolveUrl(sourceId, url);
68
84
  if (debug) console.log("GetOrCreate Addressable from", sourceId, url, "FinalPath=", fullPath);
69
85
  const addressables = context.addressables;
src/engine/engine_license.ts CHANGED
@@ -48,11 +48,10 @@
48
48
  if (NEEDLE_ENGINE_LICENSE_TYPE === "basic") {
49
49
  try {
50
50
  const licenseUrl = "https://engine.needle.tools/licensing/check?location=" + encodeURIComponent(window.location.href) + "&version=" + VERSION + "&generator=" + encodeURIComponent(GENERATOR);
51
- const res = await fetch(licenseUrl).catch();
52
- if (debug) {
53
- const text = await res?.text();
54
- console.log("Response: \"" + text + "\"\n", res);
55
- }
51
+ const res = await fetch(licenseUrl, {
52
+ method: "GET",
53
+ mode: "no-cors",
54
+ }).catch();
56
55
  if (res?.status === 200) {
57
56
  applicationIsForbidden = false;
58
57
  if (debug) console.log("License check succeeded");
@@ -134,7 +133,7 @@
134
133
 
135
134
 
136
135
  const licenseElementIdentifier = "needle-license-element";
137
- const licenseDuration = 5000;
136
+ const licenseDuration = 10000;
138
137
  const licenseDelay = 200;
139
138
 
140
139
  async function onNonCommercialVersionDetected(ctx: IContext) {
@@ -171,7 +170,7 @@
171
170
  const textElement = document.createElement("div");
172
171
  textElement.classList.add("text");
173
172
  // if (!isMobileDevice())
174
- // textElement.innerHTML = "Needle Engine<br/><span class=\"non-commercial\">Non Commercial</span>";
173
+ // textElement.innerHTML = "Made with Needle";
175
174
  licenseElement.appendChild(textElement);
176
175
 
177
176
  licenseElement.title = "Needle Engine";
@@ -181,18 +180,20 @@
181
180
  globalThis.open("https://needle.tools", "_blank");
182
181
  });
183
182
 
184
- const removeDelay = licenseDuration + licenseDelay;
185
- setTimeout(() => {
186
- clearInterval(interval);
187
- licenseElement?.remove();
188
- style?.remove();
189
- // show the logo every x minutes
190
- const intervalInMinutes = 5;
183
+ if (hasIndieLicense()) {
184
+ const removeDelay = licenseDuration + licenseDelay;
191
185
  setTimeout(() => {
192
- if (ctx.domElement.parentNode)
193
- insertNonCommercialUseHint(ctx);
194
- }, 1000 * 60 * intervalInMinutes)
195
- }, removeDelay);
186
+ clearInterval(interval);
187
+ licenseElement?.remove();
188
+ style?.remove();
189
+ // show the logo every x minutes
190
+ const intervalInMinutes = 5;
191
+ setTimeout(() => {
192
+ if (ctx.domElement.parentNode)
193
+ insertNonCommercialUseHint(ctx);
194
+ }, 1000 * 60 * intervalInMinutes)
195
+ }, removeDelay);
196
+ }
196
197
 
197
198
  }
198
199
  let lastLogTime = 0;
@@ -251,45 +252,44 @@
251
252
 
252
253
  ${selector}:hover {
253
254
  cursor: pointer;
254
- transition: all 0.1s ease-in-out !important;
255
+ transition: all 0.3s ease-in-out !important;
255
256
  }
256
257
 
257
258
  ${selector}, ${selector} > * {
258
259
  display: inline-block !important;
259
260
  visibility: visible !important;
260
- background: none !important;
261
261
  border: none !important;
262
262
  text-decoration: none !important;
263
263
  vertical-align: middle !important;
264
264
  }
265
265
 
266
- @keyframes license-animation {
266
+ @keyframes license-animation-text {
267
267
  1% {
268
268
  opacity: 0;
269
269
  }
270
270
  2.5% {
271
271
  opacity: 1;
272
272
  }
273
- 98% {
273
+ 100% {
274
274
  opacity: 1;
275
275
  }
276
- 99% {
277
- opacity: 0;
278
- }
279
276
  }
280
277
  ${selector} .text {
278
+ position: relative;
279
+ display: inline-block;
281
280
  opacity: 0;
282
- animation: license-animation;
283
- animation-iteration-count: 1;
281
+ animation: license-animation-text;
284
282
  animation-duration: ${(licenseDuration / 1000)}s;
285
283
  animation-delay: ${licenseDelay / 1000}s;
286
- animation-easing: ease-in-out;
287
- mix-blend-mode: difference;
288
- color: rgb(0, 0, 0);
289
- mix-blend-mode: difference;
284
+ animation-fill-mode: forwards;
290
285
  line-height: 1em;
291
286
  margin-left: -3px;
292
- text-shadow: 0 0 2px rgba(200,200,200, .5);
287
+ color: rgba(20,20,20,1);
288
+ font-size: 14px;
289
+ font-weight: 800;
290
+ background-color: rgba(220, 220, 220, .8);
291
+ // padding: .51em .6em .43em .6em;
292
+ border-radius: .7em;
293
293
  }
294
294
 
295
295
  ${selector} .text .non-commercial {
@@ -303,34 +303,30 @@
303
303
  transform: translate(0px, 10px);
304
304
  pointer-events: none;
305
305
  }
306
- 8% {
306
+ 3% {
307
307
  transform: translate(0, 0px);
308
308
  pointer-events: all;
309
309
  opacity: 1;
310
310
  transform: scale(1.1)
311
311
  }
312
- 20% {
312
+ 12% {
313
313
  transform: scale(1)
314
314
  }
315
- 90% {
315
+ 100% {
316
316
  opacity: 1;
317
317
  pointer-events: all;
318
318
  transform: scale(1)
319
319
  }
320
- 100% {
321
- pointer-events: none;
322
- opacity: 0;
323
- }
324
320
  }
325
321
 
326
322
  ${selector} .logo {
327
323
  opacity: 0;
328
324
  pointer-events: none;
329
325
  animation: logo-animation;
330
- animation-iteration-count: 1;
331
326
  animation-duration: ${(licenseDuration / 1000)}s;
332
327
  animation-delay: ${licenseDelay / 1000}s;
333
328
  animation-easing: ease-in-out;
329
+ animation-fill-mode: forwards;
334
330
  }
335
331
 
336
332
  ${selector} .logo {
@@ -339,12 +335,12 @@
339
335
  border-radius: 50% !important;
340
336
  background-color: transparent !important;
341
337
  padding: 5px !important;
342
- transition: all 0.1s ease-in-out !important;
338
+ transition: all 0.2s ease-in-out !important;
343
339
  }
344
340
 
345
341
  ${selector}:hover .logo {
346
- transition: all 0.1s ease-in-out !important;
347
- transform: scale(1.1) !important;
342
+ transition: all 0.2s ease-in-out !important;
343
+ transform: scale(1.02) !important;
348
344
  cursor: pointer !important;
349
345
  }
350
346
  `
@@ -354,9 +350,7 @@
354
350
 
355
351
  async function sendUsageMessageToAnalyticsBackend() {
356
352
  try {
357
- const analyticsBackendUrlForward = "https://urls.needle.tools/analytics-endpoint-v2";
358
- const res = await fetch(analyticsBackendUrlForward);
359
- const analyticsUrl = await res.text();
353
+ const analyticsUrl = "https://needle-engine-analytics-v2-r26roub2hq-lz.a.run.app";
360
354
  if (analyticsUrl) {
361
355
  if (debug) console.log("Analytics backend url", analyticsUrl);
362
356
 
src/engine/engine_networking_utils.ts CHANGED
@@ -1,14 +1,11 @@
1
1
 
2
2
 
3
- // const testUrls = ["https://192.254.384.122:3000/", "https://my-glitch-page.glitch.me/"]
4
- // for (let url of testUrls)
5
- // console.log("Testing url: " + url, isLocalNetwork(url));
6
3
 
7
4
  const localNetworkResults = new Map<string, boolean>();
8
5
 
9
6
  export function isLocalNetwork(hostname = globalThis.location?.hostname) {
10
- if(localNetworkResults.has(hostname)) return localNetworkResults.get(hostname)!;
11
- const isLocalNetwork = new RegExp("[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|localhost", "gm").test(hostname);
7
+ if (localNetworkResults.has(hostname)) return localNetworkResults.get(hostname)!;
8
+ const isLocalNetwork = new RegExp("([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\:[0-9]{1,5})|localhost").test(hostname);
12
9
  localNetworkResults.set(hostname, isLocalNetwork);
13
10
  if (isLocalNetwork === true) return true;
14
11
  return false;
@@ -16,4 +13,11 @@
16
13
 
17
14
  export function isHostedOnGlitch() {
18
15
  return window.location.hostname.includes("glitch.me");
19
- }
16
+ }
17
+
18
+ // const testUrls = [
19
+ // "https://192.254.384.122:3000/",
20
+ // "https://my-glitch-page.glitch.me/",
21
+ // ]
22
+ // for (let url of testUrls)
23
+ // console.log("Testing url: " + url, isLocalNetwork(url));
src/engine-components/ui/EventSystem.ts CHANGED
@@ -257,7 +257,11 @@
257
257
 
258
258
  // if the pointer didnt change, theoretically we would need to check the camera as well
259
259
  if (!args.isDown && !args.isUp && !args.isClicked && !args.isPressed && !args.positionDelta.x && !args.positionDelta.y)
260
+ {
261
+ this.objectsHoveredThisFrame.length = 0;
262
+ this.objectsHoveredThisFrame.push(...this.objectsHoveredLastFrame);
260
263
  return
264
+ }
261
265
 
262
266
  if(debug) console.log("EventSystem: raycast");
263
267
  const hits = this.performRaycast(null);
src/engine-components/timeline/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./SignalAsset.js"
2
2
  export * from "./TimelineTracks.js"
3
- export * from "./TimelineModels.js"
3
+ export * from "./TimelineModels.js"
4
+ export { type ITimelineAnimationCallbacks as ITimelineAnimationOverride } from "./PlayableDirector.js"
src/engine-components/ParticleSystem.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  import { AxesHelper, BufferGeometry, Color, Material, Matrix4, Mesh, MeshStandardMaterial, Object3D, OneMinusDstAlphaFactor, PlaneGeometry, Quaternion, Sprite, SpriteMaterial, Vector3, Vector4 } from "three";
12
12
  import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldScale } from "../engine/engine_three_utils.js";
13
13
  import { assign } from "../engine/engine_serialization_core.js";
14
- import { BatchedParticleRenderer, Behavior, BillBoardSettings, BurstParameters, ColorGenerator, ConstantColor, ConstantValue, EmissionState, EmitSubParticleSystem, EmitterShape, FunctionColorGenerator, FunctionJSON, FunctionValueGenerator, IntervalValue, MeshSettings, Particle, ParticleEmitter, ParticleSystem as _ParticleSystem, ParticleSystemParameters, PointEmitter, RecordState, RenderMode, RotationGenerator, SizeOverLife, TrailBatch, TrailParticle, TrailSettings, ValueGenerator } from "three.quarks";
14
+ import { BatchedRenderer, BatchedParticleRenderer, Behavior, BillBoardSettings, BurstParameters, ColorGenerator, ConstantColor, ConstantValue, EmissionState, EmitSubParticleSystem, EmitterShape, FunctionColorGenerator, FunctionJSON, FunctionValueGenerator, IntervalValue, MeshSettings, Particle, ParticleEmitter, ParticleSystem as _ParticleSystem, ParticleSystemParameters, PointEmitter, RecordState, RenderMode, RotationGenerator, SizeOverLife, TrailBatch, TrailParticle, TrailSettings, ValueGenerator } from "three.quarks";
15
15
  import { createFlatTexture } from "../engine/engine_shaders.js";
16
16
  import { Mathf } from "../engine/engine_math.js";
17
17
  import { Context } from "../engine/engine_setup.js";
@@ -269,6 +269,8 @@
269
269
  }
270
270
  toJSON() { throw new Error("Method not implemented."); }
271
271
  clone(): Behavior { throw new Error("Method not implemented."); }
272
+ reset() {
273
+ }
272
274
  }
273
275
 
274
276
  const $startFrame = Symbol("startFrame")
@@ -346,7 +348,9 @@
346
348
  let size = 1;
347
349
  if (this.system.sizeOverLifetime.enabled)
348
350
  size *= this.system.sizeOverLifetime.evaluate(age01, undefined, particle[$sizeLerpFactor]).x;
349
- const scaleFactor = this.system.worldScale.x / this.system.cameraScale;
351
+ let scaleFactor = 1;
352
+ if (this.system.renderer.renderMode !== ParticleSystemRenderMode.Mesh)
353
+ scaleFactor = this.system.worldScale.x / this.system.cameraScale;
350
354
  particle.size = particle.startSize * size * scaleFactor;
351
355
  if (this.system.localspace) {
352
356
  const scale = getLocalSimulationScale(this.system, localScaleVec3);
@@ -544,6 +548,10 @@
544
548
  this.emission = new ParticleSystemEmissionOverTime(this.system);
545
549
  }
546
550
 
551
+ get prewarm() { return false; } // force disable three.quark prewarm, we have our own!
552
+ get material() { return this.system.renderer.getMaterial(this.system.trails.enabled) as Material;}
553
+ get layers() { return this.system.gameObject.layers; }
554
+
547
555
  update() {
548
556
  this.emission.update();
549
557
  }
@@ -575,9 +583,9 @@
575
583
  switch (this.system.renderer.renderMode) {
576
584
  case ParticleSystemRenderMode.Billboard: return RenderMode.BillBoard;
577
585
  case ParticleSystemRenderMode.Stretch: return RenderMode.StretchedBillBoard;
578
- case ParticleSystemRenderMode.HorizontalBillboard: return RenderMode.LocalSpace;
579
- case ParticleSystemRenderMode.VerticalBillboard: return RenderMode.LocalSpace;
580
- case ParticleSystemRenderMode.Mesh: return RenderMode.LocalSpace;
586
+ case ParticleSystemRenderMode.HorizontalBillboard: return RenderMode.BillBoard;
587
+ case ParticleSystemRenderMode.VerticalBillboard: return RenderMode.BillBoard;
588
+ case ParticleSystemRenderMode.Mesh: return RenderMode.Mesh;
581
589
  }
582
590
  return RenderMode.BillBoard;
583
591
  }
@@ -587,7 +595,7 @@
587
595
  };
588
596
  get speedFactor() { return this.system.main.simulationSpeed; }
589
597
  get texture(): THREE.Texture {
590
- const mat = this.system.renderer.getMaterial(this.system.trails.enabled);
598
+ const mat = this.material;
591
599
  if (mat && mat["map"]) {
592
600
  const tex = mat["map"]!;
593
601
  tex.premultiplyAlpha = false;
@@ -811,7 +819,7 @@
811
819
  }
812
820
 
813
821
  private _renderer!: ParticleSystemRenderer;
814
- private _batchSystem?: BatchedParticleRenderer;
822
+ private _batchSystem?: BatchedRenderer;
815
823
  private _particleSystem?: _ParticleSystem;
816
824
  private _interface!: ParticleSystemInterface;
817
825
 
@@ -882,13 +890,14 @@
882
890
  this._batchSystem.name = this.gameObject.name;
883
891
  this._container.add(this._batchSystem);
884
892
  this._interface = new ParticleSystemInterface(this);
885
- this._particleSystem = new _ParticleSystem(this._batchSystem, this._interface);
893
+ this._particleSystem = new _ParticleSystem(this._interface);
886
894
  this._particleSystem.addBehavior(new SizeBehaviour(this));
887
895
  this._particleSystem.addBehavior(new ColorBehaviour(this));
888
896
  this._particleSystem.addBehavior(new TextureSheetAnimationBehaviour(this));
889
897
  this._particleSystem.addBehavior(new RotationBehaviour(this));
890
898
  this._particleSystem.addBehavior(new VelocityBehaviour(this));
891
899
  this._particleSystem.addBehavior(new TrailBehaviour(this));
900
+ this._batchSystem.addSystem(this._particleSystem);
892
901
 
893
902
  const emitter = this._particleSystem.emitter;
894
903
  this.context.scene.add(emitter);
@@ -946,7 +955,7 @@
946
955
  const timeToSimulate = Math.min(Math.max(duration, lifetime) / Math.max(.01, this.main.simulationSpeed), maxDurationToPrewarm);
947
956
  const framesToSimulate = Math.ceil(timeToSimulate / dt);
948
957
  const startTime = Date.now();
949
- if (debug || this.name === "Snow")
958
+ if (debug)
950
959
  console.log(`Particles ${this.name} - Prewarm for ${framesToSimulate} frames (${timeToSimulate} sec). Duration: ${duration}, Lifetime: ${lifetime}`);
951
960
  for (let i = 0; i < framesToSimulate; i++) {
952
961
  if (this.currentParticles >= this.maxParticles) break;
@@ -993,6 +1002,7 @@
993
1002
 
994
1003
  private lastMaterialVersion: number = -1;
995
1004
  private onUpdate() {
1005
+
996
1006
  const mat = this.renderer.getMaterial(this.trails.enabled);
997
1007
  if (mat && mat.version != this.lastMaterialVersion && this._particleSystem) {
998
1008
  this.lastMaterialVersion = mat.version;
src/engine-components/ParticleSystemSubEmitter.ts CHANGED
@@ -68,6 +68,9 @@
68
68
  toJSON(): any {
69
69
  }
70
70
 
71
+ reset() {
72
+ }
73
+
71
74
  private run(particle: Particle) {
72
75
  if (this.subSystem.currentParticles >= this.subSystem.main.maxParticles)
73
76
  return;
src/engine-components/timeline/PlayableDirector.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  import * as Tracks from "./TimelineTracks.js";
9
9
  import { deepClone, delay, getParam } from '../../engine/engine_utils.js';
10
10
  import { GuidsMap } from '../../engine/engine_types.js';
11
- import { Object3D } from 'three';
11
+ import { Object3D, Quaternion, Vector3 } from 'three';
12
12
  import { isLocalNetwork } from '../../engine/engine_networking_utils.js';
13
13
  import { FrameEvent } from '../../engine/engine_context.js';
14
14
 
@@ -597,4 +597,36 @@
597
597
  }
598
598
  }
599
599
 
600
+
601
+
602
+ /** Experimental support for overriding timeline animation data (position or rotation) */
603
+ readonly animationCallbackReceivers: ITimelineAnimationCallbacks[] = [];
604
+ /** Experimental: Receive callbacks for timeline animation. Allows modification of final value */
605
+ registerAnimationCallback(receiver: ITimelineAnimationCallbacks) { this.animationCallbackReceivers.push(receiver); }
606
+ /** Experimental: Unregister callbacks for timeline animation. Allows modification of final value */
607
+ unregisterAnimationCallback(receiver: ITimelineAnimationCallbacks) {
608
+ const index = this.animationCallbackReceivers.indexOf(receiver);
609
+ if (index === -1) return;
610
+ this.animationCallbackReceivers.splice(index, 1);
611
+ }
600
612
  }
613
+
614
+ /**
615
+ * Experimental interface for receiving timeline animation callbacks. Register at the PlayableDirector
616
+ */
617
+ export interface ITimelineAnimationCallbacks {
618
+ /**
619
+ * @param director The director that is playing the timeline
620
+ * @param target The target object that is being animated
621
+ * @param time The current time of the timeline
622
+ * @param rotation The evaluated rotation of the target object at the current time
623
+ */
624
+ onTimelineRotation?(director: PlayableDirector, target: Object3D, time: number, rotation: Quaternion);
625
+ /**
626
+ * @param director The director that is playing the timeline
627
+ * @param target The target object that is being animated
628
+ * @param time The current time of the timeline
629
+ * @param position The evaluated position of the target object at the current time
630
+ */
631
+ onTimelinePosition?(director: PlayableDirector, target: Object3D, time: number, position: Vector3);
632
+ }
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -1189,6 +1189,7 @@
1189
1189
  const normalScale = material instanceof MeshStandardMaterial ? (material.normalScale ? material.normalScale.x * 2 : 2) : 2;
1190
1190
  const normalScaleValueString = normalScale.toFixed( PRECISION );
1191
1191
  const normalBiasString = (-1 * (normalScale / 2)).toFixed( PRECISION );
1192
+ const normalBiasZString = (1 - normalScale).toFixed( PRECISION );
1192
1193
 
1193
1194
  return `
1194
1195
  ${needsTextureTransform ? `def Shader "Transform2d_${mapType}" (
@@ -1216,7 +1217,7 @@
1216
1217
  ` : `` }
1217
1218
  ${needsNormalScaleAndBias ? `
1218
1219
  float4 inputs:scale = (${normalScaleValueString}, ${normalScaleValueString}, ${normalScaleValueString}, 1)
1219
- float4 inputs:bias = (${normalBiasString}, ${normalBiasString}, ${normalBiasString}, 0)
1220
+ float4 inputs:bias = (${normalBiasString}, ${normalBiasString}, ${normalBiasZString}, 0)
1220
1221
  ` : `` }
1221
1222
  token inputs:wrapS = "${ WRAPPINGS[ texture.wrapS ] }"
1222
1223
  token inputs:wrapT = "${ WRAPPINGS[ texture.wrapT ] }"
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { PlayableDirector } from "./PlayableDirector.js";
2
2
  import * as Models from "./TimelineModels.js";
3
- import * as THREE from 'three';
4
3
  import { GameObject } from "../Component.js";
5
4
  import { Context } from "../../engine/engine_setup.js";
6
5
  import { SignalReceiver } from "./SignalAsset.js";
7
- import { AnimationClip, Quaternion, Vector3 } from "three";
6
+ import { Audio, AudioListener, AnimationAction, AnimationClip, AnimationMixer, AudioLoader, Euler, Object3D, Quaternion, QuaternionKeyframeTrack, Vector3, VectorKeyframeTrack } from "three";
8
7
  import { getParam, resolveUrl } from "../../engine/engine_utils.js";
9
8
  import { AudioSource } from "../AudioSource.js";
10
9
  import { Animator } from "../Animator.js"
@@ -88,17 +87,17 @@
88
87
 
89
88
  class AnimationClipOffsetData {
90
89
  clip: AnimationClip;
91
- rootPositionOffset?: THREE.Vector3;
92
- rootQuaternionOffset?: THREE.Quaternion;
90
+ rootPositionOffset?: Vector3;
91
+ rootQuaternionOffset?: Quaternion;
93
92
  get hasOffsets(): boolean { return this.rootPositionOffset !== undefined || this.rootQuaternionOffset !== undefined; }
94
93
 
95
94
  // not necessary
96
- rootStartPosition?: THREE.Vector3;
97
- rootEndPosition?: THREE.Vector3;
98
- rootStartQuaternion?: THREE.Quaternion;
99
- rootEndQuaternion?: THREE.Quaternion;
95
+ rootStartPosition?: Vector3;
96
+ rootEndPosition?: Vector3;
97
+ rootStartQuaternion?: Quaternion;
98
+ rootEndQuaternion?: Quaternion;
100
99
 
101
- constructor(action: THREE.AnimationAction) {
100
+ constructor(action: AnimationAction) {
102
101
  const clip = action.getClip();
103
102
  this.clip = clip;
104
103
  const root = action.getRoot();
@@ -109,20 +108,20 @@
109
108
  for (const track of clip.tracks) {
110
109
  if (track.times.length <= 0) continue;
111
110
  if (track.name.endsWith(rootPositionTrackName)) {
112
- this.rootStartPosition = new THREE.Vector3().fromArray(track.values, 0);
113
- this.rootEndPosition = new THREE.Vector3().fromArray(track.values, track.values.length - 3);
111
+ this.rootStartPosition = new Vector3().fromArray(track.values, 0);
112
+ this.rootEndPosition = new Vector3().fromArray(track.values, track.values.length - 3);
114
113
  this.rootPositionOffset = this.rootEndPosition.clone().sub(this.rootStartPosition);
115
114
  if (debug)
116
115
  console.log(this.rootPositionOffset);
117
116
  // this.rootPositionOffset.set(0, 0, 0);
118
117
  }
119
118
  else if (track.name.endsWith(rootRotationTrackName)) {
120
- this.rootStartQuaternion = new THREE.Quaternion().fromArray(track.values, 0);
121
- this.rootEndQuaternion = new THREE.Quaternion().fromArray(track.values, track.values.length - 4);
119
+ this.rootStartQuaternion = new Quaternion().fromArray(track.values, 0);
120
+ this.rootEndQuaternion = new Quaternion().fromArray(track.values, track.values.length - 4);
122
121
  this.rootQuaternionOffset = this.rootEndQuaternion.clone().multiply(this.rootStartQuaternion);
123
122
 
124
123
  if (debug) {
125
- const euler = new THREE.Euler().setFromQuaternion(this.rootQuaternionOffset);
124
+ const euler = new Euler().setFromQuaternion(this.rootQuaternionOffset);
126
125
  console.log("ROT", euler);
127
126
  }
128
127
  }
@@ -135,11 +134,11 @@
135
134
  models: Array<Models.ClipModel> = [];
136
135
  trackOffset?: Models.TrackOffset;
137
136
 
138
- target?: THREE.Object3D;
137
+ target?: Object3D;
139
138
  /** The AnimationMixer, should be shared with the animator if an animator is bound */
140
- mixer?: THREE.AnimationMixer;
141
- clips: Array<THREE.AnimationClip> = [];
142
- actions: Array<THREE.AnimationAction> = [];
139
+ mixer?: AnimationMixer;
140
+ clips: Array<AnimationClip> = [];
141
+ actions: Array<AnimationAction> = [];
143
142
 
144
143
  /** holds data/info about clips differences */
145
144
  private _actionOffsets: Array<AnimationClipOffsetData> = [];
@@ -185,7 +184,7 @@
185
184
  // TODO: this currently assumes that there is only one root always that has offsets so it only does create the interpolator for the first track which might be incorrect. In general it would probably be better if we would not create additional tracks but apply the offsets for these objects elsewhere!?
186
185
 
187
186
  if (!foundPositionTrack || !foundRotationTrack) {
188
- const root = this.mixer?.getRoot() as THREE.Object3D;
187
+ const root = this.mixer?.getRoot() as Object3D;
189
188
  const track = clip.tracks[0];
190
189
  const indexOfProperty = track.name.lastIndexOf(".");
191
190
  const baseName = track.name.substring(0, indexOfProperty);
@@ -202,7 +201,7 @@
202
201
  if (debug) console.warn("Create position track", objName, targetObj);
203
202
  // apply initial local position so it doesnt get flipped or otherwise changed
204
203
  const pos = targetObj.position;
205
- const track = new THREE.VectorKeyframeTrack(trackName, [0, clip.duration], [pos.x, pos.y, pos.z, pos.x, pos.y, pos.z]);
204
+ const track = new VectorKeyframeTrack(trackName, [0, clip.duration], [pos.x, pos.y, pos.z, pos.x, pos.y, pos.z]);
206
205
  clip.tracks.push(track);
207
206
  this.createPositionInterpolant(clip, clipModel, track);
208
207
  }
@@ -210,7 +209,7 @@
210
209
  const trackName = clip.tracks[0].name.substring(0, indexOfProperty) + ".quaternion";
211
210
  if (debug) console.warn("Create quaternion track", objName, targetObj);
212
211
  const rot = targetObj.quaternion;
213
- const track = new THREE.QuaternionKeyframeTrack(trackName, [0, clip.duration], [rot.x, rot.y, rot.z, rot.w, rot.x, rot.y, rot.z, rot.w]);
212
+ const track = new QuaternionKeyframeTrack(trackName, [0, clip.duration], [rot.x, rot.y, rot.z, rot.w, rot.x, rot.y, rot.z, rot.w]);
214
213
  clip.tracks.push(track);
215
214
  this.createRotationInterpolant(clip, clipModel, track);
216
215
  }
@@ -224,7 +223,7 @@
224
223
  if (debug) console.log(this.models);
225
224
 
226
225
  // the object being animated
227
- if (this.mixer) this.target = this.mixer.getRoot() as THREE.Object3D;
226
+ if (this.mixer) this.target = this.mixer.getRoot() as Object3D;
228
227
  else console.warn("No mixer was assigned to animation track")
229
228
 
230
229
  for (const action of this.actions) {
@@ -265,13 +264,13 @@
265
264
  const pos = this.trackOffset.position as any;
266
265
  if (pos) {
267
266
  if (!pos.isVector3) {
268
- this.trackOffset.position = new THREE.Vector3(pos.x, pos.y, pos.z);
267
+ this.trackOffset.position = new Vector3(pos.x, pos.y, pos.z);
269
268
  }
270
269
  }
271
270
  const rot = this.trackOffset.rotation as any;
272
271
  if (rot) {
273
272
  if (!rot.isQuaternion) {
274
- this.trackOffset.rotation = new THREE.Quaternion(rot.x, rot.y, rot.z, rot.w);
273
+ this.trackOffset.rotation = new Quaternion(rot.x, rot.y, rot.z, rot.w);
275
274
  }
276
275
  }
277
276
  }
@@ -279,20 +278,20 @@
279
278
 
280
279
  private _useclipOffsets: boolean = true;
281
280
 
282
- private _totalOffsetPosition: THREE.Vector3 = new THREE.Vector3();
283
- private _totalOffsetRotation: THREE.Quaternion = new THREE.Quaternion();
284
- private _totalOffsetPosition2: THREE.Vector3 = new THREE.Vector3();
285
- private _totalOffsetRotation2: THREE.Quaternion = new THREE.Quaternion();
286
- private _summedPos = new THREE.Vector3();
287
- private _tempPos = new THREE.Vector3();
288
- private _summedRot = new THREE.Quaternion();
289
- private _tempRot = new THREE.Quaternion();
290
- private _clipRotQuat = new THREE.Quaternion();
281
+ private _totalOffsetPosition: Vector3 = new Vector3();
282
+ private _totalOffsetRotation: Quaternion = new Quaternion();
283
+ private _totalOffsetPosition2: Vector3 = new Vector3();
284
+ private _totalOffsetRotation2: Quaternion = new Quaternion();
285
+ private _summedPos = new Vector3();
286
+ private _tempPos = new Vector3();
287
+ private _summedRot = new Quaternion();
288
+ private _tempRot = new Quaternion();
289
+ private _clipRotQuat = new Quaternion();
291
290
 
292
291
  evaluate(time: number) {
293
292
  if (this.track.muted) return;
294
293
  if (!this.mixer) return;
295
-
294
+
296
295
  this.bind();
297
296
 
298
297
  // if (this._animator && this.director.isPlaying && this.director.weight > 0) this._animator.enabled = false;
@@ -349,7 +348,7 @@
349
348
  break;
350
349
  case Models.ClipExtrapolation.Loop:
351
350
  // TODO: this is not correct yet
352
- time += model.start;
351
+ time += model.start;
353
352
  handleLoop = true;
354
353
  break;
355
354
  default:
@@ -442,7 +441,7 @@
442
441
  tempPos.applyQuaternion(this._clipRotQuat);
443
442
 
444
443
  if (offsets.rootQuaternionOffset) {
445
- // console.log(new THREE.Euler().setFromQuaternion(offsets.rootQuaternionOffset).y.toFixed(2));
444
+ // console.log(new Euler().setFromQuaternion(offsets.rootQuaternionOffset).y.toFixed(2));
446
445
  tempRot.copy(offsets.rootQuaternionOffset);
447
446
  summedRot.multiply(tempRot);
448
447
  }
@@ -455,7 +454,7 @@
455
454
  totalRotation.multiply(summedRot);
456
455
 
457
456
  if (clipModel.position)
458
- summedPos.add(clipModel.position as THREE.Vector3);
457
+ summedPos.add(clipModel.position as Vector3);
459
458
  totalPosition.add(summedPos);
460
459
  }
461
460
 
@@ -484,9 +483,9 @@
484
483
 
485
484
  private createRotationInterpolant(_clip: AnimationClip, _clipModel: Models.AnimationClipModel, track: any) {
486
485
  const createInterpolantOriginal = track.createInterpolant.bind(track);
487
- const quat: THREE.Quaternion = new THREE.Quaternion();
486
+ const quat: Quaternion = new Quaternion();
488
487
  this.ensureTrackOffsets();
489
- const trackOffsetRot: THREE.Quaternion | null = this.trackOffset?.rotation as Quaternion;
488
+ const trackOffsetRot: Quaternion | null = this.trackOffset?.rotation as Quaternion;
490
489
  track.createInterpolant = () => {
491
490
  const createdInterpolant: any = createInterpolantOriginal();
492
491
  const interpolate = createdInterpolant.evaluate.bind(createdInterpolant);
@@ -495,13 +494,19 @@
495
494
  const res = interpolate(time);
496
495
  quat.set(res[0], res[1], res[2], res[3]);
497
496
  quat.premultiply(this._totalOffsetRotation);
498
- // console.log(new THREE.Euler().setFromQuaternion(quat).y.toFixed(2));
497
+ // console.log(new Euler().setFromQuaternion(quat).y.toFixed(2));
499
498
  if (trackOffsetRot) quat.premultiply(trackOffsetRot);
499
+
500
+ if (this.director.animationCallbackReceivers) {
501
+ for (const rec of this.director.animationCallbackReceivers) {
502
+ rec?.onTimelineRotation?.call(rec, this.director, this.target!, time, quat);
503
+ }
504
+ }
505
+
500
506
  res[0] = quat.x;
501
507
  res[1] = quat.y;
502
508
  res[2] = quat.z;
503
509
  res[3] = quat.w;
504
-
505
510
  return res;
506
511
  };
507
512
  return createdInterpolant;
@@ -510,10 +515,10 @@
510
515
 
511
516
  private createPositionInterpolant(clip: AnimationClip, clipModel: Models.AnimationClipModel, track: any) {
512
517
  const createInterpolantOriginal = track.createInterpolant.bind(track);
513
- const currentPosition: THREE.Vector3 = new THREE.Vector3();
518
+ const currentPosition: Vector3 = new Vector3();
514
519
  this.ensureTrackOffsets();
515
- const trackOffsetRot: THREE.Quaternion | null = this.trackOffset?.rotation as Quaternion;
516
- const trackOffsetPos: THREE.Vector3 | null = this.trackOffset?.position as Vector3;
520
+ const trackOffsetRot: Quaternion | null = this.trackOffset?.rotation as Quaternion;
521
+ const trackOffsetPos: Vector3 | null = this.trackOffset?.position as Vector3;
517
522
  let startOffset: Vector3 | null | undefined = undefined;
518
523
  track.createInterpolant = () => {
519
524
  const createdInterpolant: any = createInterpolantOriginal();
@@ -540,6 +545,11 @@
540
545
  currentPosition.y += trackOffsetPos.y;
541
546
  currentPosition.z += trackOffsetPos.z;
542
547
  }
548
+ if (this.director.animationCallbackReceivers) {
549
+ for (const rec of this.director.animationCallbackReceivers) {
550
+ rec?.onTimelinePosition?.call(rec, this.director, this.target!, time, currentPosition);
551
+ }
552
+ }
543
553
  res[0] = currentPosition.x;
544
554
  res[1] = currentPosition.y;
545
555
  res[2] = currentPosition.z;
@@ -556,12 +566,12 @@
556
566
  export class AudioTrackHandler extends TrackHandler {
557
567
  models: Array<Models.ClipModel> = [];
558
568
 
559
- listener!: THREE.AudioListener;
560
- audio: Array<THREE.Audio> = [];
569
+ listener!: AudioListener;
570
+ audio: Array<Audio> = [];
561
571
  audioContextTimeOffset: Array<number> = [];
562
572
  lastTime: number = 0;
563
573
 
564
- private _audioLoader: THREE.AudioLoader | null = null;
574
+ private _audioLoader: AudioLoader | null = null;
565
575
 
566
576
  private getAudioFilePath(path: string) {
567
577
  // TODO: this should be the timeline asset location probably which MIGHT be different
@@ -578,7 +588,7 @@
578
588
  }
579
589
 
580
590
  addModel(model: Models.ClipModel) {
581
- const audio = new THREE.Audio(this.listener);
591
+ const audio = new Audio(this.listener as any);
582
592
  this.audio.push(audio);
583
593
  this.models.push(model);
584
594
  }
@@ -653,7 +663,7 @@
653
663
  else {
654
664
  const targetOffset = model.clipIn + (time - model.start) * model.timeScale;
655
665
  // seems it's non-trivial to get the right time from audio sources;
656
- // https://github.com/mrdoob/three.js/blob/master/src/audio/Audio.js#L170
666
+ // https://github.com/mrdoob/js/blob/master/src/audio/Audio.js#L170
657
667
  const currentTime = audio.context.currentTime - audio["_startedAt"] + audio.offset;
658
668
  const diff = Math.abs(targetOffset - currentTime);
659
669
 
@@ -720,9 +730,9 @@
720
730
  AudioTrackHandler._audioBuffers.clear();
721
731
  }
722
732
 
723
- private handleAudioLoading(model: Models.ClipModel, audio: THREE.Audio): Promise<AudioBuffer | null> | null {
733
+ private handleAudioLoading(model: Models.ClipModel, audio: Audio): Promise<AudioBuffer | null> | null {
724
734
  if (!this._audioLoader) {
725
- this._audioLoader = new THREE.AudioLoader();
735
+ this._audioLoader = new AudioLoader();
726
736
  }
727
737
  // TODO: maybe we should cache the loaders / buffers here by path
728
738
  const path = this.getAudioFilePath(model.asset.clip);
@@ -861,7 +871,7 @@
861
871
  const clipTime = this.getClipTime(time, model);
862
872
 
863
873
  if (asset.controlActivation) {
864
- const obj = asset.sourceObject as THREE.Object3D;
874
+ const obj = asset.sourceObject as Object3D;
865
875
  obj.visible = true;
866
876
  }
867
877
 
@@ -881,7 +891,7 @@
881
891
  else {
882
892
  const previousActiveAsset = this._previousActiveModel?.asset as Models.ControlClipModel;
883
893
  if (asset.controlActivation) {
884
- const obj = asset.sourceObject as THREE.Object3D;
894
+ const obj = asset.sourceObject as Object3D;
885
895
  if (previousActiveAsset?.sourceObject !== obj)
886
896
  obj.visible = false;
887
897
  }