Needle Engine

Changes between version 3.23.1 and 3.24.0
Files changed (28) hide show
  1. src/engine-components/Animator.ts +6 -3
  2. src/engine-components/AnimatorController.ts +5 -4
  3. src/engine-components/ui/Button.ts +3 -3
  4. src/engine-components/Camera.ts +25 -13
  5. src/engine-components/CameraUtils.ts +6 -5
  6. src/engine-components/codegen/components.ts +1 -0
  7. src/engine/debug/debug_overlay.ts +1 -1
  8. src/engine/engine_context_registry.ts +2 -0
  9. src/engine/engine_context.ts +36 -11
  10. src/engine/engine_create_objects.ts +5 -5
  11. src/engine/engine_gizmos.ts +8 -4
  12. src/engine/engine_physics_rapier.ts +4 -3
  13. src/engine/engine_scenetools.ts +1 -0
  14. src/engine-components/export/gltf/GltfExport.ts +12 -18
  15. src/engine-components/ui/Graphic.ts +9 -3
  16. src/engine/extensions/NEEDLE_techniques_webgl.ts +2 -1
  17. src/engine-components/ParticleSystem.ts +18 -6
  18. src/engine-components/timeline/PlayableDirector.ts +2 -0
  19. src/engine/codegen/register_types.ts +4 -2
  20. src/engine-components/ShadowCatcher.ts +73 -35
  21. src/engine-components/Skybox.ts +15 -8
  22. src/engine-components/ui/Text.ts +4 -2
  23. src/engine-components/export/usdz/ThreeUSDZExporter.ts +10 -6
  24. src/engine-components/timeline/TimelineModels.ts +1 -0
  25. src/engine-components/timeline/TimelineTracks.ts +9 -3
  26. src/engine-components/webxr/WebXRImageTracking.ts +1 -1
  27. src/engine-components/webxr/WebXRSync.ts +2 -2
  28. src/engine-components/ContactShadows.ts +249 -0
src/engine-components/Animator.ts CHANGED
@@ -87,8 +87,9 @@
87
87
  /**@deprecated use setBool */
88
88
  SetBool(name: string | number, val: boolean) { this.setBool(name, val); }
89
89
  setBool(name: string | number, value: boolean) {
90
+ if(this.runtimeAnimatorController?.getBool(name) !== value)
91
+ this._parametersAreDirty = true;
90
92
  this.runtimeAnimatorController?.setBool(name, value);
91
- this._parametersAreDirty = true;
92
93
  }
93
94
 
94
95
  /**@deprecated use getBool */
@@ -100,8 +101,9 @@
100
101
  /**@deprecated use setFloat */
101
102
  SetFloat(name: string | number, val: number) { this.setFloat(name, val); }
102
103
  setFloat(name: string | number, val: number) {
104
+ if(this.runtimeAnimatorController?.getFloat(name) !== val)
105
+ this._parametersAreDirty = true;
103
106
  this.runtimeAnimatorController?.setFloat(name, val);
104
- this._parametersAreDirty = true;
105
107
  }
106
108
 
107
109
  /**@deprecated use getFloat */
@@ -113,8 +115,9 @@
113
115
  /**@deprecated use setInteger */
114
116
  SetInteger(name: string | number, val: number) { this.setInteger(name, val); }
115
117
  setInteger(name: string | number, val: number) {
118
+ if(this.runtimeAnimatorController?.getInteger(name) !== val)
119
+ this._parametersAreDirty = true;
116
120
  this.runtimeAnimatorController?.setInteger(name, val);
117
- this._parametersAreDirty = true;
118
121
  }
119
122
 
120
123
  /**@deprecated use getInteger */
src/engine-components/AnimatorController.ts CHANGED
@@ -483,8 +483,9 @@
483
483
 
484
484
  offsetNormalized = Math.max(0, Math.min(1, offsetNormalized));
485
485
  if (state.cycleOffsetParameter) {
486
- const val = this.getFloat(state.cycleOffsetParameter);
486
+ let val = this.getFloat(state.cycleOffsetParameter);
487
487
  if (typeof val === "number") {
488
+ if (val < 0) val += 1;
488
489
  offsetNormalized += val;
489
490
  offsetNormalized %= 1;
490
491
  }
@@ -494,13 +495,13 @@
494
495
  offsetNormalized += state.cycleOffset
495
496
  offsetNormalized %= 1;
496
497
  }
497
-
498
498
  if (action.isRunning())
499
499
  action.stop();
500
500
  action.reset();
501
501
  action.enabled = true;
502
502
  const duration = state.motion.clip!.duration;
503
- action.time = offsetNormalized * duration;
503
+ // if we are looping to the same state we don't want to offset the current start time
504
+ action.time = isSelf ? 0 : offsetNormalized * duration;
504
505
  if (action.timeScale < 0) action.time = duration - action.time;
505
506
  action.clampWhenFinished = true;
506
507
  action.setLoop(LoopOnce, 0);
@@ -1012,7 +1013,7 @@
1012
1013
 
1013
1014
  }
1014
1015
  onDeserialize(data: AnimatorControllerModel & { __type?: string }, context: SerializationContext) {
1015
- if (context.type === AnimatorController && data.__type === "AnimatorController")
1016
+ if (context.type === AnimatorController && data?.__type === "AnimatorController")
1016
1017
  return new AnimatorController(data);
1017
1018
  return undefined;
1018
1019
  }
src/engine-components/ui/Button.ts CHANGED
@@ -271,10 +271,10 @@
271
271
  image.setupState(disabledState);
272
272
  }
273
273
 
274
- private getFinalColor(col: RGBAColor, col2?: RGBAColor) {
274
+ private getFinalColor(col: RGBAColor, col2?: RGBAColor): RGBAColor {
275
275
  if (col2) {
276
- return col.clone().multiply(col2);
276
+ return col.clone().multiply(col2).convertLinearToSRGB() as RGBAColor;
277
277
  }
278
- return col.clone();
278
+ return col.clone().convertLinearToSRGB() as RGBAColor;
279
279
  }
280
280
  }
src/engine-components/Camera.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  import { EquirectangularReflectionMapping, OrthographicCamera, PerspectiveCamera, Ray, SRGBColorSpace, Vector3 } from "three";
12
12
  import { OrbitControls } from "./OrbitControls.js";
13
13
  import { RenderTexture } from "../engine/engine_texture.js";
14
+ import { Texture } from "three";
14
15
 
15
16
  export enum ClearFlags {
16
17
  Skybox = 1,
@@ -172,18 +173,18 @@
172
173
 
173
174
  private _backgroundColor?: RGBAColor;
174
175
  private _fov?: number;
175
- private _cam: THREE.PerspectiveCamera | THREE.OrthographicCamera | null = null;
176
+ private _cam: PerspectiveCamera | OrthographicCamera | null = null;
176
177
  private _clearFlags: ClearFlags = ClearFlags.SolidColor;
177
178
  private _skybox?: CameraSkybox;
178
179
 
179
- public get cam(): THREE.PerspectiveCamera | THREE.OrthographicCamera {
180
+ public get cam(): PerspectiveCamera | OrthographicCamera {
180
181
  if (this.activeAndEnabled)
181
182
  this.buildCamera();
182
183
  return this._cam!;
183
184
  }
184
185
 
185
- private static _origin: THREE.Vector3 = new Vector3();
186
- private static _direction: THREE.Vector3 = new Vector3();
186
+ private static _origin: Vector3 = new Vector3();
187
+ private static _direction: Vector3 = new Vector3();
187
188
  public screenPointToRay(x: number, y: number, ray?: Ray): Ray {
188
189
  const cam = this.cam;
189
190
  const origin = Camera._origin;
@@ -234,6 +235,11 @@
234
235
 
235
236
  onBeforeRender() {
236
237
  if (this._cam) {
238
+
239
+ // because the background color may be animated!
240
+ if (this._clearFlags === ClearFlags.SolidColor)
241
+ this.applyClearFlagsIfIsActiveCamera();
242
+
237
243
  if (this._targetTexture) {
238
244
  if (this.context.isManagedExternally) {
239
245
  // TODO: rendering with r3f renderer does throw an shader error for some reason?
@@ -264,7 +270,7 @@
264
270
  const cameraAlreadyCreated = this.gameObject["isCamera"];
265
271
 
266
272
  // TODO: when exporting from blender we already have a camera in the children
267
- let cam: THREE.PerspectiveCamera | THREE.OrthographicCamera | null = null;
273
+ let cam: PerspectiveCamera | OrthographicCamera | null = null;
268
274
  if (cameraAlreadyCreated) {
269
275
  cam = this.gameObject as any;
270
276
  cam?.layers.enableAll();
@@ -272,7 +278,7 @@
272
278
  this._fov = cam.fov;
273
279
  }
274
280
  else
275
- cam = this.gameObject.children[0] as THREE.PerspectiveCamera | THREE.OrthographicCamera | null;
281
+ cam = this.gameObject.children[0] as PerspectiveCamera | OrthographicCamera | null;
276
282
  if (cam && cam.isCamera) {
277
283
  if (cam instanceof PerspectiveCamera) {
278
284
  if (this._fov)
@@ -370,25 +376,28 @@
370
376
  static backgroundShouldBeTransparent(context: Context) {
371
377
  const session = context.renderer.xr?.getSession();
372
378
  if (!session) return false;
379
+ if (typeof session["_transparent"] === "boolean") {
380
+ return session["_transparent"];
381
+ }
373
382
  const environmentBlendMode = session.environmentBlendMode;
374
383
  if (debug)
375
384
  showBalloonMessage("Environment blend mode: " + environmentBlendMode + " on " + navigator.userAgent);
376
- const transparent = environmentBlendMode === 'additive' || environmentBlendMode === 'alpha-blend';
377
-
385
+ let transparent = environmentBlendMode === 'additive' || environmentBlendMode === 'alpha-blend';
378
386
  if (context.xrSessionMode === XRSessionMode.ImmersiveAR) {
379
387
  if (environmentBlendMode === "opaque") {
380
388
  // workaround for Quest 2 returning opaque when it should be alpha-blend
381
389
  // check user agent if this is the Quest browser and return true if so
382
390
  if (navigator.userAgent?.includes("OculusBrowser")) {
383
- return true;
391
+ transparent = true;
384
392
  }
385
393
  // Mozilla WebXR Viewer
386
394
  else if (navigator.userAgent?.includes("Mozilla") && navigator.userAgent?.includes("Mobile WebXRViewer/v2")) {
387
- return true;
395
+ transparent = true;
388
396
  }
389
397
  }
390
398
  }
391
399
 
400
+ session["_transparent"] = transparent;
392
401
  return transparent;
393
402
  }
394
403
  }
@@ -397,7 +406,7 @@
397
406
  class CameraSkybox {
398
407
 
399
408
  private _camera: Camera;
400
- private _skybox?: THREE.Texture;
409
+ private _skybox?: Texture;
401
410
 
402
411
  get context() { return this._camera?.context; }
403
412
 
@@ -406,9 +415,12 @@
406
415
  }
407
416
 
408
417
  enable() {
409
- this._skybox = this.context.lightmaps.tryGetSkybox(this._camera.sourceId) as THREE.Texture;
418
+ this._skybox = this.context.lightmaps.tryGetSkybox(this._camera.sourceId) as Texture;
410
419
  if (!this._skybox) {
411
- console.warn(`Camera \"${this._camera.name}\" failed to load/find skybox texture`, this._camera.sourceId, this.context.lightmaps, "Current background: ", this.context.scene.background);
420
+ if (!this["_did_log_failed_to_find_skybox"]) {
421
+ this["_did_log_failed_to_find_skybox"] = true;
422
+ console.warn(`Camera \"${this._camera.name}\" failed to load/find skybox texture`, this._camera.sourceId, this.context.lightmaps, "Current background: ", this.context.scene.background);
423
+ }
412
424
  if (debug || isDevEnvironment())
413
425
  showBalloonWarning(`Camera \"${this._camera.name}\" has no skybox texture`);
414
426
  }
src/engine-components/CameraUtils.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import { RGBAColor } from "./js-extensions/RGBAColor.js";
6
6
  import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
7
7
  import { getCameraController } from "../engine/engine_camera.js";
8
- import { Camera } from "./Camera.js";
8
+ import { Camera, ClearFlags } from "./Camera.js";
9
9
  import { NeedleEngineHTMLElement } from "../engine/engine_element.js";
10
10
  import { getParam } from "../engine/engine_utils.js";
11
11
  import { Context } from "../engine/engine_context.js";
@@ -22,12 +22,13 @@
22
22
 
23
23
  const camInstance = new Camera();
24
24
  camInstance.sourceId = evt.files?.[0]?.src ?? "unknown"
25
- // Set the clearFlags to a skybox if we have one
26
- if((evt.context as Context).lightmaps.tryGetSkybox(camInstance.sourceId))
27
- camInstance.clearFlags = 1;
25
+
26
+ // Set the clearFlags to a skybox if we have one OR if the user set a skybox image attribute
27
+ if(evt.context.domElement.getAttribute("skybox-image")?.length || 0 > 0 || (evt.context as Context).lightmaps.tryGetSkybox(camInstance.sourceId))
28
+ camInstance.clearFlags = ClearFlags.Skybox;
28
29
  else
29
30
  // TODO provide a nice default skybox
30
- camInstance.clearFlags = 2;
31
+ camInstance.clearFlags = ClearFlags.SolidColor;
31
32
  camInstance.backgroundColor = new RGBAColor(0.5, 0.5, 0.5, 1);
32
33
  camInstance.fieldOfView = 35;
33
34
  // TODO: can we store the backgroundBlurriness in the gltf file somewhere except inside the camera?
src/engine-components/codegen/components.ts CHANGED
@@ -51,6 +51,7 @@
51
51
  export { ColorBySpeedModule } from "../ParticleSystemModules.js";
52
52
  export { ColorOverLifetimeModule } from "../ParticleSystemModules.js";
53
53
  export { Component } from "../Component.js";
54
+ export { ContactShadows } from "../ContactShadows.js";
54
55
  export { ControlTrackHandler } from "../timeline/TimelineTracks.js";
55
56
  export { CustomBranding } from "../export/usdz/USDZExporter.js";
56
57
  export { Deletable } from "../DeleteBox.js";
src/engine/debug/debug_overlay.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  const debug = getParam("debugdebug");
6
6
  let hide = false;
7
- if (getParam("noerrors")) hide = true;
7
+ if (getParam("noerrors") || getParam("nooverlaymessages")) hide = true;
8
8
 
9
9
  const globalErrorContainerKey = "needle_engine_global_error_container";
10
10
 
src/engine/engine_context_registry.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  ContextCreationStart = "ContextCreationStart",
8
8
  /** Called when the context has been created, before the first frame */
9
9
  ContextCreated = "ContextCreated",
10
+ /** Called after the first frame has been rendered after creation */
11
+ ContextFirstFrameRendered = "ContextFirstFrameRendered",
10
12
  /** Called when the context has been destroyed */
11
13
  ContextDestroyed = "ContextDestroyed",
12
14
  /** Called when the context could not find a camera during creation */
src/engine/engine_context.ts CHANGED
@@ -173,6 +173,13 @@
173
173
  */
174
174
  targetFrameRate?: number | { value?: number };
175
175
 
176
+ /** Use a higher number for more accurate physics simulation.
177
+ * When undefined physics steps will be 1 for mobile devices and 5 for desktop devices
178
+ * Set to 0 to disable physics updates
179
+ * TODO: changing physics steps is currently not supported because then forces that we get from the character controller and rigidbody et al are not correct anymore - this needs to be properly tested before making this configureable
180
+ */
181
+ private physicsSteps?: number = 1;
182
+
176
183
  /** used to append to loaded assets */
177
184
  hash?: string;
178
185
 
@@ -941,6 +948,14 @@
941
948
  }
942
949
  }
943
950
 
951
+ /** Call to **manually** perform physics steps.
952
+ * By default the context uses the `physicsSteps` property to perform steps during the update loop
953
+ * If you just want to increase the accuracy of physics you can instead set the `physicsSteps` property to a higher value
954
+ * */
955
+ public updatePhysics(steps: number) {
956
+ this.internalUpdatePhysics(steps);
957
+ }
958
+
944
959
  private _lastTimestamp = 0;
945
960
  private _accumulatedTime = 0;
946
961
  private _dispatchReadyAfterFrame = false;
@@ -1039,18 +1054,12 @@
1039
1054
  this.executeCoroutines(FrameEvent.LateUpdate);
1040
1055
  if (this.onHandlePaused()) return false;
1041
1056
 
1042
- if (this.physics.engine) {
1043
- const physicsSteps = 1;
1044
- const dt = this.time.deltaTime / physicsSteps;
1045
- for (let i = 0; i < physicsSteps; i++) {
1046
- this._currentFrameEvent = FrameEvent.PrePhysicsStep;
1047
- this.executeCoroutines(FrameEvent.PrePhysicsStep);
1048
- this.physics.engine.step(dt);
1049
- this._currentFrameEvent = FrameEvent.PostPhysicsStep;
1050
- this.executeCoroutines(FrameEvent.PostPhysicsStep);
1051
- }
1052
- this.physics.engine.postStep();
1057
+ if (this.physicsSteps === undefined) {
1058
+ this.physicsSteps = 1;
1053
1059
  }
1060
+ if (this.physics.engine && this.physicsSteps > 0) {
1061
+ this.internalUpdatePhysics(this.physicsSteps);
1062
+ }
1054
1063
 
1055
1064
  if (this.onHandlePaused()) return false;
1056
1065
 
@@ -1085,6 +1094,21 @@
1085
1094
  return true;
1086
1095
  }
1087
1096
 
1097
+ private internalUpdatePhysics(steps: number) {
1098
+ if (!this.physics.engine) return false;
1099
+ const physicsSteps = steps;
1100
+ const dt = this.time.deltaTime / physicsSteps;
1101
+ for (let i = 0; i < physicsSteps; i++) {
1102
+ this._currentFrameEvent = FrameEvent.PrePhysicsStep;
1103
+ this.executeCoroutines(FrameEvent.PrePhysicsStep);
1104
+ this.physics.engine.step(dt);
1105
+ this._currentFrameEvent = FrameEvent.PostPhysicsStep;
1106
+ this.executeCoroutines(FrameEvent.PostPhysicsStep);
1107
+ }
1108
+ this.physics.engine.postStep();
1109
+ return true;
1110
+ }
1111
+
1088
1112
  private internalOnRender() {
1089
1113
  if (!this.isManagedExternally) {
1090
1114
  looputils.runPrewarm(this);
@@ -1127,6 +1151,7 @@
1127
1151
  if (this._dispatchReadyAfterFrame) {
1128
1152
  this._dispatchReadyAfterFrame = false;
1129
1153
  this.domElement.dispatchEvent(new CustomEvent("ready"));
1154
+ ContextRegistry.dispatchCallback(ContextEvent.ContextFirstFrameRendered, this);
1130
1155
  }
1131
1156
  }
1132
1157
 
src/engine/engine_create_objects.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { PlaneGeometry, MeshBasicMaterial, DoubleSide, Mesh, Material, MeshStandardMaterial, BoxGeometry, SphereGeometry } from "three"
1
+ import { PlaneGeometry, MeshBasicMaterial, DoubleSide, Mesh, Material, MeshStandardMaterial, BoxGeometry, SphereGeometry, ColorRepresentation } from "three"
2
2
 
3
-
4
3
  export enum PrimitiveType {
5
4
  Quad = 0,
6
5
  Cube = 1,
@@ -16,20 +15,21 @@
16
15
 
17
16
  static createPrimitive(type: PrimitiveType, opts?: ObjectOptions): Mesh {
18
17
  let obj: Mesh;
18
+ const color = 0xffffff;
19
19
  switch (type) {
20
20
  case PrimitiveType.Quad:
21
21
  const quadGeo = new PlaneGeometry(1, 1, 1, 1);
22
- const quadMat = opts?.material ?? new MeshBasicMaterial({ color: 0xffffff });
22
+ const quadMat = opts?.material ?? new MeshStandardMaterial({ color: color });
23
23
  obj = new Mesh(quadGeo, quadMat);
24
24
  break;
25
25
  case PrimitiveType.Cube:
26
26
  const boxGeo = new BoxGeometry(1, 1, 1);
27
- const boxMat = opts?.material ?? new MeshStandardMaterial({ color: 0xdddddd });
27
+ const boxMat = opts?.material ?? new MeshStandardMaterial({ color: color });
28
28
  obj = new Mesh(boxGeo, boxMat);
29
29
  break;
30
30
  case PrimitiveType.Sphere:
31
31
  const sphereGeo = new SphereGeometry(.5, 16, 16);
32
- const sphereMat = opts?.material ?? new MeshStandardMaterial({ color: 0xdddddd });
32
+ const sphereMat = opts?.material ?? new MeshStandardMaterial({ color: color });
33
33
  obj = new Mesh(sphereGeo, sphereMat);
34
34
  break;
35
35
  }
src/engine/engine_gizmos.ts CHANGED
@@ -3,11 +3,13 @@
3
3
  import { getWorldPosition, lookAtObject, setWorldPositionXYZ } from './engine_three_utils.js';
4
4
  import type { Vec3, Vec4 } from './engine_types.js';
5
5
  import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
6
+ import { getParam } from './engine_utils.js';
6
7
 
7
8
  const _tmp = new Vector3();
8
9
  const _tmp2 = new Vector3();
9
10
  const _quat = new Quaternion();
10
11
 
12
+ const debug = getParam("debuggizmos");
11
13
  const defaultColor: ColorRepresentation = 0x888888;
12
14
 
13
15
  export type LabelHandle = {
@@ -167,7 +169,8 @@
167
169
  if (backgroundColor && typeof backgroundColor === "string" && backgroundColor?.length >= 8 && backgroundColor.startsWith("#")) {
168
170
  opacity = parseInt(backgroundColor.substring(7), 16) / 255;
169
171
  backgroundColor = backgroundColor.substring(0, 7);
170
- console.log(backgroundColor, opacity);
172
+ if (debug)
173
+ console.log(backgroundColor, opacity);
171
174
  }
172
175
  else if (typeof backgroundColor === "object" && backgroundColor["a"] !== undefined) {
173
176
  opacity = backgroundColor["a"]
@@ -184,13 +187,13 @@
184
187
  backgroundColor: backgroundColor ?? undefined,
185
188
  backgroundOpacity: opacity,
186
189
  textContent: text,
187
- borderRadius: .1,
188
- padding: .1,
190
+ borderRadius: 1 * size,
191
+ padding: 1 * size,
189
192
  });
190
193
  const global = this;
191
194
  const labelHandle = element as LabelHandle & Text;
192
195
  labelHandle.setText = function (str: string) {
193
- this.set({ textContent: str });
196
+ this.set({ textContent: str, whiteSpace: 'pre' });
194
197
  global.tmuiNeedsUpdate = true;
195
198
  };
196
199
  }
@@ -201,6 +204,7 @@
201
204
  backgroundColor: backgroundColor ?? undefined,
202
205
  backgroundOpacity: opacity,
203
206
  textContent: text,
207
+ whiteSpace: 'pre',
204
208
  });
205
209
  // const handle = element as any as LabelHandle;
206
210
  // handle.setText(text);
src/engine/engine_physics_rapier.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  import { foreachComponent } from './engine_gameobject.js';
19
19
 
20
20
  import { ActiveCollisionTypes, ActiveEvents, CoefficientCombineRule, Ball, Collider, ColliderDesc, EventQueue, JointData, QueryFilterFlags, RigidBody, RigidBodyType, ShapeColliderTOI, World, Ray, ShapeType, Cuboid } from '@dimforge/rapier3d-compat';
21
- import { CollisionDetectionMode, PhysicsMaterial, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
21
+ import { CollisionDetectionMode, type PhysicsMaterial, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
22
22
  import { Gizmos } from './engine_gizmos.js';
23
23
  import { Mathf } from './engine_math.js';
24
24
  import { SphereOverlapResult } from './engine_types.js';
@@ -748,7 +748,8 @@
748
748
  // otherwise rapier will compute the mass properties based on the collider shape and density
749
749
  // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#mass-properties
750
750
  if (useExplicitMassProperties) {
751
- desc.setDensity(0);
751
+ desc.setDensity(.000001);
752
+ desc.setMass(.000001);
752
753
  }
753
754
 
754
755
  const col = this.world.createCollider(desc, rigidBody);
@@ -886,7 +887,7 @@
886
887
  rigidbody.setAdditionalMass(rb.mass, false);
887
888
  for (let i = 0; i < rigidbody.numColliders(); i++) {
888
889
  const col = rigidbody.collider(i);
889
- col.setDensity(0);
890
+ col.setDensity(0.0000001);
890
891
  }
891
892
  rigidbody.recomputeMassPropertiesFromColliders();
892
893
  }
src/engine/engine_scenetools.ts CHANGED
@@ -105,6 +105,7 @@
105
105
  export async function parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined> {
106
106
  if (typeof path !== "string") {
107
107
  console.warn("Parse gltf binary without path, this might lead to errors in resolving extensions. Please provide the source path of the gltf/glb file", path, typeof path);
108
+ path = "";
108
109
  }
109
110
  if (printGltf) console.log("Parse glTF", path)
110
111
  const loader = await createGLTFLoader(path, context);
src/engine-components/export/gltf/GltfExport.ts CHANGED
@@ -24,21 +24,10 @@
24
24
  // @generate-component
25
25
  export class GltfExportBox extends BoxHelperComponent {
26
26
  sceneRoot?: THREE.Object3D;
27
-
28
- start() {
29
- this.startCoroutine(this.updateGltfBox());
30
- }
31
-
32
- *updateGltfBox() {
33
- while (true) {
34
- for (let i = 0; i < 10; i++) yield;
35
-
36
- }
37
- }
38
27
  }
39
28
 
40
29
  export class GltfExport extends Behaviour {
41
-
30
+
42
31
  @serializable()
43
32
  binary: boolean = true;
44
33
 
@@ -55,7 +44,11 @@
55
44
  this.objects = [this.context.scene];
56
45
 
57
46
  const opts = { binary: this.binary, pivot: GltfExport.calculateCenter(this.objects) };
58
- const res = await this.export(this.objects, opts);
47
+ const res = await this.export(this.objects, opts).catch(err => {
48
+ console.error(err);
49
+ return false;
50
+ })
51
+ if (res === false) return false;
59
52
 
60
53
  if (!this.binary) {
61
54
  if (!name.endsWith(".gltf"))
@@ -67,6 +60,7 @@
67
60
  GltfExport.saveArrayBuffer(res, name);
68
61
  else
69
62
  GltfExport.saveJson(res, name);
63
+ return true;
70
64
  }
71
65
 
72
66
  async export(objectsToExport: Object3D[], opts?: ExportOptions): Promise<any> {
@@ -79,12 +73,11 @@
79
73
  if (!this.exporter) {
80
74
  // Instantiate a exporter
81
75
  this.exporter = new GLTFExporter();
82
- //@ts-ignore
83
76
  this.exporter.register(writer => new GLTFMeshGPUInstancingExtension(writer));
84
77
 
85
- this.ext = new NEEDLE_components();
86
- //@ts-ignore
87
- this.ext.registerExport(this.exporter);
78
+ // TODO
79
+ // this.ext = new NEEDLE_components();
80
+ // this.ext.registerExport(this.exporter);
88
81
  }
89
82
 
90
83
  GltfExport.filterTopmostParent(objectsToExport);
@@ -128,7 +121,8 @@
128
121
  });
129
122
 
130
123
  const serializationContext = new SerializationContext(exportScene);
131
- this.ext!.context = serializationContext;
124
+ if (this.ext)
125
+ this.ext.context = serializationContext;
132
126
 
133
127
  return new Promise((resolve, reject) => {
134
128
  if (debugExport) console.log("Starting glTF export.")
src/engine-components/ui/Graphic.ts CHANGED
@@ -33,6 +33,7 @@
33
33
  this._color = new RGBAColor(1, 1, 1, 1);
34
34
  }
35
35
  this._color.copy(col);
36
+ this.onColorChanged();
36
37
  }
37
38
 
38
39
  private _alphaFactor: number = 1;
@@ -44,9 +45,12 @@
44
45
  return this._alphaFactor;
45
46
  }
46
47
 
48
+ private sRGBColor: Color = new Color(1, 0, 1);
47
49
  protected onColorChanged() {
48
50
  if (this.uiObject) {
49
- _colorStateObject.backgroundColor = this._color;
51
+ this.sRGBColor.copy(this._color);
52
+ this.sRGBColor.convertLinearToSRGB();
53
+ _colorStateObject.backgroundColor = this.sRGBColor;
50
54
  _colorStateObject.backgroundOpacity = this._color.alpha * this._alphaFactor;
51
55
  this.applyEffects(_colorStateObject, this._alphaFactor);
52
56
  this.uiObject.set(_colorStateObject);
@@ -164,6 +168,8 @@
164
168
  this.controlsChildLayout = false;
165
169
  this._currentlyCreatingPanel = false;
166
170
  this.onAfterCreated();
171
+
172
+ this.onColorChanged();
167
173
  }
168
174
 
169
175
  protected onBeforeCreate(_opts: any) { }
@@ -192,7 +198,7 @@
192
198
  this.setOptions({ backgroundOpacity: 0 });
193
199
  if (tex) {
194
200
  // workaround for https://github.com/needle-tools/needle-engine-support/issues/109
195
- if (tex.colorSpace === SRGBColorSpace) {
201
+ // if (tex.colorSpace === SRGBColorSpace || !tex.colorSpace || true) {
196
202
  if (Graphic.textureCache.has(tex)) {
197
203
  tex = Graphic.textureCache.get(tex)!;
198
204
  } else {
@@ -209,7 +215,7 @@
209
215
  tex = clone;
210
216
  }
211
217
  }
212
- }
218
+ // }
213
219
  this.setOptions({ backgroundImage: tex, borderRadius: 0, backgroundOpacity: this.color.alpha, backgroundSize: "stretch" });
214
220
  }
215
221
  else {
src/engine/extensions/NEEDLE_techniques_webgl.ts CHANGED
@@ -452,8 +452,9 @@
452
452
  const indexString = val.substring("/textures/".length);
453
453
  const texIndex = Number.parseInt(indexString);
454
454
  if (texIndex >= 0) {
455
- const tex = await this.parser.getDependency("texture", texIndex);
455
+ let tex = await this.parser.getDependency("texture", texIndex);
456
456
  if (tex instanceof Texture) {
457
+ tex = tex.clone();
457
458
  tex.colorSpace = LinearSRGBColorSpace;
458
459
  tex.needsUpdate = true;
459
460
  }
src/engine-components/ParticleSystem.ts CHANGED
@@ -509,6 +509,7 @@
509
509
 
510
510
  const $colorLerpFactor = Symbol("colorLerpFactor");
511
511
  const tempColor = new RGBAColor(1, 1, 1, 1);
512
+ const col = new RGBAColor(1, 1, 1, 1);
512
513
  class ColorBehaviour extends ParticleSystemBaseBehaviour {
513
514
  type: string = "NeedleColor";
514
515
 
@@ -517,7 +518,7 @@
517
518
 
518
519
  private _init(particle: Particle) {
519
520
  const materialColor = this.system.renderer.particleMaterial;
520
- const col = this.system.main.startColor.evaluate(Math.random());
521
+ col.copy(this.system.main.startColor.evaluate(Math.random()));
521
522
  if (materialColor?.color) {
522
523
  tempColor.copy(materialColor.color);
523
524
  col.multiply(tempColor)
@@ -607,15 +608,26 @@
607
608
  }
608
609
  return factor;
609
610
  }
611
+ private flatWhiteTexture?: THREE.Texture;
612
+ private clonedTexture: { original?: THREE.Texture, clone?: THREE.Texture } = { original: undefined, clone: undefined };
610
613
  get texture(): THREE.Texture {
611
614
  const mat = this.material;
612
615
  if (mat && mat["map"]) {
613
- const tex = mat["map"]!;
614
- tex.premultiplyAlpha = false;
615
- tex.colorSpace = THREE.LinearSRGBColorSpace;
616
- return tex;
616
+ const original = mat["map"]! as THREE.Texture;
617
+ // cache the last original one so we're not creating tons of clones
618
+ if (this.clonedTexture.original !== original || !this.clonedTexture.clone)
619
+ {
620
+ const tex = original.clone();
621
+ tex.premultiplyAlpha = false;
622
+ tex.colorSpace = THREE.LinearSRGBColorSpace;
623
+ this.clonedTexture.original = original;
624
+ this.clonedTexture.clone = tex;
625
+ }
626
+ return this.clonedTexture.clone;
617
627
  }
618
- return createFlatTexture(new RGBAColor(1, 1, 1, 1), 1)
628
+ if (!this.flatWhiteTexture)
629
+ this.flatWhiteTexture = createFlatTexture(new RGBAColor(1, 1, 1, 1), 1)
630
+ return this.flatWhiteTexture;
619
631
  }
620
632
  get startTileIndex() { return new TextureSheetStartFrameGenerator(this.system); }
621
633
  get uTileCount() { return this.anim.enabled ? this.anim?.numTilesX : undefined }
src/engine-components/timeline/PlayableDirector.ts CHANGED
@@ -560,6 +560,8 @@
560
560
  const audio = new Tracks.AudioTrackHandler();
561
561
  audio.director = this;
562
562
  audio.track = track;
563
+ audio.audioSource = track.outputs.find(o => o instanceof AudioSource) as AudioSource;
564
+
563
565
  this._audioTracks.push(audio);
564
566
  if (!audioListener) {
565
567
  // If the scene doesnt have an AudioListener we add one to the main camera
src/engine/codegen/register_types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TypeStore } from "./../engine_typestore.js"
2
-
2
+
3
3
  // Import types
4
4
  import { __Ignore } from "../../engine-components/codegen/components.js";
5
5
  import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
@@ -53,6 +53,7 @@
53
53
  import { ColorBySpeedModule } from "../../engine-components/ParticleSystemModules.js";
54
54
  import { ColorOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
55
55
  import { Component } from "../../engine-components/Component.js";
56
+ import { ContactShadows } from "../../engine-components/ContactShadows.js";
56
57
  import { ControlTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
57
58
  import { CustomBranding } from "../../engine-components/export/usdz/USDZExporter.js";
58
59
  import { Deletable } from "../../engine-components/DeleteBox.js";
@@ -217,7 +218,7 @@
217
218
  import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering.js";
218
219
  import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
219
220
  import { XRState } from "../../engine-components/XRFlag.js";
220
-
221
+
221
222
  // Register types
222
223
  TypeStore.add("__Ignore", __Ignore);
223
224
  TypeStore.add("ActionBuilder", ActionBuilder);
@@ -271,6 +272,7 @@
271
272
  TypeStore.add("ColorBySpeedModule", ColorBySpeedModule);
272
273
  TypeStore.add("ColorOverLifetimeModule", ColorOverLifetimeModule);
273
274
  TypeStore.add("Component", Component);
275
+ TypeStore.add("ContactShadows", ContactShadows);
274
276
  TypeStore.add("ControlTrackHandler", ControlTrackHandler);
275
277
  TypeStore.add("CustomBranding", CustomBranding);
276
278
  TypeStore.add("Deletable", Deletable);
src/engine-components/ShadowCatcher.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Behaviour, GameObject } from "./Component.js";
2
2
  import { RGBAColor } from "./js-extensions/RGBAColor.js";
3
+ import { ShadowMaterial, AdditiveBlending, Material, MeshBasicMaterial, Mesh, MeshStandardMaterial } from "three";
4
+ import { serializable } from "../engine/engine_serialization_decorator.js";
5
+ import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
3
6
  import { Renderer } from "./Renderer.js";
4
- import { ShadowMaterial, AdditiveBlending, Material } from "three";
5
- import { serializable } from "../engine/engine_serialization_decorator.js";
6
7
 
7
8
  enum ShadowMode {
8
9
  ShadowMask = 0,
@@ -20,8 +21,44 @@
20
21
  @serializable(RGBAColor)
21
22
  shadowColor: RGBAColor = new RGBAColor(0, 0, 0, 1);
22
23
 
24
+ private targetMesh?: Mesh;
25
+
23
26
  awake() {
27
+ // if there's no geometry, make a basic quad
28
+ if (!(this.gameObject instanceof Mesh)) {
29
+ const quad = ObjectUtils.createPrimitive(PrimitiveType.Quad, {
30
+ name: "ShadowCatcher",
31
+ material: new MeshStandardMaterial({
32
+ // HACK heuristic to get approx. the same colors out as with the current default ShadowCatcher material
33
+ // not clear why this is needed; assumption is that the Renderer component does something we're not respecting here
34
+ color: 0x999999,
35
+ roughness: 1,
36
+ metalness: 0,
37
+ transparent: true,
38
+ })
39
+ });
40
+ quad.receiveShadow = true;
41
+ quad.geometry.rotateX(-Math.PI / 2);
24
42
 
43
+ // TODO breaks shadow catching right now
44
+ // const renderer = new Renderer();
45
+ // renderer.receiveShadows = true;
46
+ // GameObject.addComponent(quad, Renderer);
47
+
48
+ this.gameObject.add(quad);
49
+ this.targetMesh = quad;
50
+ }
51
+ else if (this.gameObject instanceof Mesh && this.gameObject.material) {
52
+ // make sure we have a unique material to work with
53
+ this.gameObject.material = this.gameObject.material.clone();
54
+ this.targetMesh = this.gameObject;
55
+ }
56
+
57
+ if(!this.targetMesh) {
58
+ console.warn("ShadowCatcher: no mesh to apply shadow catching to. Groups are currently not supported.");
59
+ return;
60
+ }
61
+
25
62
  switch (this.mode) {
26
63
  case ShadowMode.ShadowMask:
27
64
  this.applyShadowMaterial();
@@ -33,7 +70,6 @@
33
70
  this.applyOccluderMaterial();
34
71
  break;
35
72
  }
36
-
37
73
  }
38
74
 
39
75
  // Custom blending, diffuse-only lighting blended onto the scene additively.
@@ -42,31 +78,30 @@
42
78
  // Works even better with an additional black-ish gradient to darken parts of the AR scene
43
79
  // so that lights become more visible on bright surfaces.
44
80
  applyLightBlendMaterial() {
45
- const renderer = GameObject.getComponent(this.gameObject, Renderer);
46
- if (renderer) {
47
- const material = renderer.sharedMaterial;
48
- material.blending = AdditiveBlending;
49
- this.applyMaterialOptions(material);
50
- material.onBeforeCompile = (shader) => {
51
- // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshphysical.glsl.js#L181
52
- // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib.js#LL284C11-L284C11
53
- // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/shadow.glsl.js#L40
54
- // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js#L2
55
- // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js#L281
81
+ if (!this.targetMesh) return;
56
82
 
57
- shader.fragmentShader = shader.fragmentShader.replace("vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;",
58
- `vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;
59
- // diffuse-only lighting with overdrive to somewhat compensate
60
- // for the loss of indirect lighting and to make it more visible.
61
- vec3 direct = reflectedLight.directSpecular * 6.6;
62
- float max = max(direct.r, max(direct.g, direct.b));
63
-
64
- // early out - we're simply returning direct lighting and some alpha based on it so it can
65
- // be blended onto the scene.
66
- gl_FragColor = vec4(direct, max);
67
- return;
68
- `);
69
- }
83
+ const material = this.targetMesh.material as Material;
84
+ material.blending = AdditiveBlending;
85
+ this.applyMaterialOptions(material);
86
+ material.onBeforeCompile = (shader) => {
87
+ // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshphysical.glsl.js#L181
88
+ // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib.js#LL284C11-L284C11
89
+ // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/shadow.glsl.js#L40
90
+ // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js#L2
91
+ // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js#L281
92
+
93
+ shader.fragmentShader = shader.fragmentShader.replace("vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;",
94
+ `vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;
95
+ // diffuse-only lighting with overdrive to somewhat compensate
96
+ // for the loss of indirect lighting and to make it more visible.
97
+ vec3 direct = (reflectedLight.directDiffuse + reflectedLight.directSpecular) * 6.6;
98
+ float max = max(direct.r, max(direct.g, direct.b));
99
+
100
+ // early out - we're simply returning direct lighting and some alpha based on it so it can
101
+ // be blended onto the scene.
102
+ gl_FragColor = vec4(direct, max);
103
+ return;
104
+ `);
70
105
  }
71
106
  }
72
107
 
@@ -74,17 +109,16 @@
74
109
  // doesn't take light attenuation into account.
75
110
  // works great for Directional Lights.
76
111
  applyShadowMaterial() {
77
- const renderer = GameObject.getComponent(this.gameObject, Renderer);
78
- if (renderer) {
79
- if (renderer.sharedMaterial?.type !== "ShadowMaterial") {
112
+ if (this.targetMesh) {
113
+ if ((this.targetMesh.material as Material).type !== "ShadowMaterial") {
80
114
  const material = new ShadowMaterial();
81
115
  material.color = this.shadowColor;
82
116
  material.opacity = this.shadowColor.alpha;
83
117
  this.applyMaterialOptions(material);
84
- renderer.sharedMaterial = material;
118
+ this.targetMesh.material = material;
85
119
  }
86
120
  else {
87
- const material = renderer.sharedMaterial as ShadowMaterial;
121
+ const material = this.targetMesh.material as ShadowMaterial;
88
122
  material.color = this.shadowColor;
89
123
  material.opacity = this.shadowColor.alpha;
90
124
  this.applyMaterialOptions(material);
@@ -93,9 +127,13 @@
93
127
  }
94
128
 
95
129
  applyOccluderMaterial() {
96
- const renderer = GameObject.getComponent(this.gameObject, Renderer);
97
- if (renderer) {
98
- const material = renderer.sharedMaterial;
130
+ if (this.targetMesh) {
131
+ let material = this.targetMesh.material as Material;
132
+ if (!material) {
133
+ const mat = new MeshBasicMaterial();
134
+ this.targetMesh.material = mat;
135
+ material = mat;
136
+ }
99
137
  material.depthWrite = true;
100
138
  material.stencilWrite = true;
101
139
  material.colorWrite = false;
src/engine-components/Skybox.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
5
5
  import { EquirectangularRefractionMapping, NeverDepth, SRGBColorSpace, sRGBEncoding, Texture, TextureLoader } from "three"
6
6
  import { syncField } from "../engine/engine_networking_auto.js";
7
- import { Camera } from "./Camera.js";
7
+ import { Camera, ClearFlags } from "./Camera.js";
8
8
  import { PromiseAllWithErrors, addAttributeChangeCallback, getParam, removeAttributeChangeCallback } from "../engine/engine_utils.js";
9
9
  import { ContextRegistry } from "../engine/engine_context_registry.js";
10
10
  import { registerObservableAttribute } from "../engine/engine_element_extras.js";
@@ -43,12 +43,18 @@
43
43
  const promises = new Array<Promise<any>>();
44
44
 
45
45
  if (skyboxImage) {
46
- if (debug) console.log("Creating remote skybox to load " + skyboxImage);
46
+ if (debug)
47
+ console.log("Creating remote skybox to load " + skyboxImage);
48
+ // if the user is loading a GLB without a camera then the CameraUtils (which creates the default camera)
49
+ // checks if we have this attribute set and then sets the skybox clearflags accordingly
50
+ // if the user has a GLB with a camera but set to solid color then the skybox image is not visible -> we will just warn then and not override the camera settings
51
+ if(context.mainCameraComponent?.clearFlags !== ClearFlags.Skybox) console.warn("\"skybox-image\" attribute has no effect: camera clearflags are not set to \"Skybox\"");
47
52
  const promise = createRemoteSkyboxComponent(context, skyboxImage, true, false, "skybox-image");
48
53
  promises.push(promise);
49
54
  }
50
55
  if (environmentImage) {
51
- if (debug) console.log("Creating remote environment to load " + environmentImage);
56
+ if (debug)
57
+ console.log("Creating remote environment to load " + environmentImage);
52
58
  const promise = createRemoteSkyboxComponent(context, environmentImage, false, true, "environment-image");
53
59
  promises.push(promise);
54
60
  }
@@ -137,8 +143,8 @@
137
143
  }
138
144
 
139
145
  async setSkybox(url: string | undefined | null) {
140
- if (!this.activeAndEnabled) return;
141
- if (!url) return;
146
+ if (!this.activeAndEnabled) return false;
147
+ if (!url) return false;
142
148
  if (!url?.endsWith(".hdr") && !url.endsWith(".exr") && !url.endsWith(".jpg") && !url.endsWith(".png") && !url.endsWith(".jpeg")) {
143
149
  console.warn("Potentially invalid skybox url", this.url, "on", this.name);
144
150
  }
@@ -147,7 +153,7 @@
147
153
 
148
154
  if (this._prevUrl === url && this._prevLoadedEnvironment) {
149
155
  this.applySkybox();
150
- return;
156
+ return true;
151
157
  }
152
158
  else {
153
159
  this._prevLoadedEnvironment?.dispose();
@@ -156,9 +162,9 @@
156
162
  this._prevUrl = url;
157
163
 
158
164
  const envMap = await this.loadTexture(url);
159
- if (!envMap) return;
165
+ if (!envMap) return false;
160
166
  // Check if we're still enabled
161
- if (!this.enabled) return;
167
+ if (!this.enabled) return false;
162
168
  // Update the current url
163
169
  this.url = url;
164
170
  const nameIndex = url.lastIndexOf("/");
@@ -168,6 +174,7 @@
168
174
  }
169
175
  this._prevLoadedEnvironment = envMap;
170
176
  this.applySkybox();
177
+ return true;
171
178
  }
172
179
 
173
180
  private async loadTexture(url: string) {
src/engine-components/ui/Text.ts CHANGED
@@ -90,9 +90,11 @@
90
90
  this.uiObject?.set({ fontSize: val });
91
91
  }
92
92
 
93
-
93
+ private sRGBTextColor: Color = new Color(1, 0, 1);
94
94
  protected onColorChanged(): void {
95
- this.uiObject?.set({ color: this.color, fontOpacity: this.color.alpha });
95
+ this.sRGBTextColor.copy(this.color);
96
+ this.sRGBTextColor.convertLinearToSRGB();
97
+ this.uiObject?.set({ color: this.sRGBTextColor, fontOpacity: this.color.alpha });
96
98
  }
97
99
 
98
100
  onParentRectTransformChanged(): void {
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -37,6 +37,12 @@
37
37
  return str;
38
38
  }
39
39
 
40
+ // TODO: remove once we update to TypeScript 5 that has proper types for OffscreenCanvas
41
+ declare type OffsetCanvasExt = OffscreenCanvas & {
42
+ convertToBlob: (options?: any) => Promise<Blob>;
43
+
44
+ }
45
+
40
46
  class USDObject {
41
47
 
42
48
  static USDObject_export_id = 0;
@@ -481,7 +487,7 @@
481
487
 
482
488
  if ( canvas ) {
483
489
 
484
- const blob = await new Promise( resolve => canvas.toBlob( resolve, isRGBA ? 'image/png' : 'image/jpeg', 0.95 ) ) as any;
490
+ const blob = await canvas.convertToBlob( {type: isRGBA ? 'image/png' : 'image/jpeg', quality: 0.95 } );
485
491
  files[ `textures/Texture_${id}.${isRGBA ? 'png' : 'jpg'}` ] = new Uint8Array( await blob.arrayBuffer() );
486
492
 
487
493
  } else {
@@ -814,11 +820,9 @@
814
820
  // max. canvas size on Safari is still 4096x4096
815
821
  const scale = 4096 / Math.max( image.width, image.height );
816
822
 
817
- const canvas = document.createElement( 'canvas' );
818
- canvas.width = image.width * Math.min( 1, scale );
819
- canvas.height = image.height * Math.min( 1, scale );
823
+ const canvas = new OffscreenCanvas( image.width * Math.min( 1, scale ), image.height * Math.min( 1, scale ) );
820
824
 
821
- const context = canvas.getContext( '2d' );
825
+ const context = canvas.getContext( '2d' ) as OffscreenCanvasRenderingContext2D;
822
826
  if (!context) throw new Error('Could not get canvas 2D context');
823
827
 
824
828
  if ( flipY === true ) {
@@ -854,7 +858,7 @@
854
858
 
855
859
  }
856
860
 
857
- return canvas;
861
+ return canvas as OffsetCanvasExt;
858
862
 
859
863
  } else {
860
864
 
src/engine-components/timeline/TimelineModels.ts CHANGED
@@ -31,6 +31,7 @@
31
31
  clips?: Array<ClipModel>;
32
32
  markers?: Array<MarkerModel>;
33
33
  trackOffset?: TrackOffset;
34
+ volume?: number;
34
35
  }
35
36
 
36
37
  declare type Vec3 = { x: number, y: number, z: number };
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -564,12 +564,13 @@
564
564
  const muteAudioTracks = getParam("mutetimeline");
565
565
 
566
566
  export class AudioTrackHandler extends TrackHandler {
567
+
567
568
  models: Array<Models.ClipModel> = [];
568
-
569
569
  listener!: AudioListener;
570
570
  audio: Array<Audio> = [];
571
571
  audioContextTimeOffset: Array<number> = [];
572
572
  lastTime: number = 0;
573
+ audioSource?:AudioSource;
573
574
 
574
575
  private _audioLoader: AudioLoader | null = null;
575
576
 
@@ -642,6 +643,7 @@
642
643
  for (let i = 0; i < this.models.length; i++) {
643
644
  const model = this.models[i];
644
645
  const audio = this.audio[i];
646
+ const asset = model.asset as Models.AudioClipModel;
645
647
  // only trigger loading for tracks that are CLOSE to being played
646
648
  if ((!audio || !audio.buffer) && this.isInTimeRange(model, time - 1, time + 1)) {
647
649
  this.handleAudioLoading(model, audio);
@@ -649,7 +651,7 @@
649
651
  if (AudioSource.userInteractionRegistered === false) continue;
650
652
  if (audio === null || !audio.buffer) continue;
651
653
  audio.playbackRate = this.director.context.time.timeScale * this.director.speed;
652
- audio.loop = model.asset.loop;
654
+ audio.loop = asset.loop;
653
655
  if (time >= model.start && time <= model.end && time < this.director.duration) {
654
656
  if (this.director.isPlaying == false) {
655
657
  if (audio.isPlaying)
@@ -673,7 +675,11 @@
673
675
  audio.play(playTimeOffset);
674
676
  }
675
677
  }
676
- let vol = model.asset.volume as number;
678
+ let vol = asset.volume as number;
679
+
680
+ if(this.track.volume !== undefined)
681
+ vol *= this.track.volume;
682
+
677
683
  if (isMuted) vol = 0;
678
684
  if (model.easeInDuration > 0) {
679
685
  const easeIn = Math.min((time - model.start) / model.easeInDuration, 1);
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -164,7 +164,7 @@
164
164
  // TODO better would be to do that once we actually need it
165
165
  const canvas = await imageToCanvas(img);
166
166
  if (canvas) {
167
- const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png', 1)) as any;
167
+ const blob = await canvas.convertToBlob({type: 'image/png'});
168
168
  const arrayBuffer = await blob.arrayBuffer();
169
169
 
170
170
  const exporter = GameObject.findObjectOfType(USDZExporter);
src/engine-components/webxr/WebXRSync.ts CHANGED
@@ -345,9 +345,9 @@
345
345
  }
346
346
 
347
347
  private buildLocalAvatar() {
348
- if (this.localAvatar) return;
348
+ if (this.localAvatar || !this.webXR) return;
349
349
  const connectionId = this.context.connection?.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
350
- this.localAvatar = new WebXRAvatar(this.context, connectionId, this.webXR!);
350
+ this.localAvatar = new WebXRAvatar(this.context, connectionId, this.webXR);
351
351
  this.localAvatar.isLocalAvatar = true;
352
352
  this.localAvatar.setAvatarOverride(this.getAvatarId());
353
353
  this.avatars[this.localAvatar.guid] = this.localAvatar;
src/engine-components/ContactShadows.ts ADDED
@@ -0,0 +1,249 @@
1
+ import { Behaviour } from "./Component.js";
2
+ import { serializable } from "../engine/engine_serialization_decorator.js";
3
+
4
+ import { CustomBlending, DoubleSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, OrthographicCamera, PlaneGeometry, ShaderMaterial, WebGLRenderTarget } from "three";
5
+ import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js';
6
+ import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js';
7
+
8
+ // Adapted from https://github.com/mrdoob/three.js/blob/master/examples/webgl_shadow_contact.html.
9
+
10
+ // Improved with
11
+ // - ground occluder
12
+ // - backface shadowing (slightly less than front faces)
13
+ // - node can simply be scaled in Y to adjust max. ground height
14
+
15
+ export class ContactShadows extends Behaviour {
16
+
17
+ @serializable()
18
+ darkness: number = 0.5;
19
+ @serializable()
20
+ opacity: number = 0.5;
21
+ @serializable()
22
+ blur: number = 4.0;
23
+ @serializable()
24
+ occludeBelowGround: boolean = true;
25
+ @serializable()
26
+ backfaceShadows: boolean = true;
27
+
28
+ private shadowCamera?: OrthographicCamera;
29
+ private shadowGroup?: Group;
30
+
31
+ private renderTarget?: WebGLRenderTarget;
32
+ private renderTargetBlur?: WebGLRenderTarget;
33
+
34
+ private plane?: Mesh;
35
+ private occluderMesh?: Mesh;
36
+ private blurPlane?: Mesh;
37
+
38
+ private depthMaterial?: MeshDepthMaterial;
39
+ private horizontalBlurMaterial?: ShaderMaterial;
40
+ private verticalBlurMaterial?: ShaderMaterial;
41
+
42
+ awake(): void {
43
+ const textureSize = 512;
44
+
45
+ this.shadowGroup = new Group();
46
+ this.gameObject.add(this.shadowGroup);
47
+
48
+ // the render target that will show the shadows in the plane texture
49
+ this.renderTarget = new WebGLRenderTarget(textureSize, textureSize);
50
+ this.renderTarget.texture.generateMipmaps = false;
51
+
52
+ // the render target that we will use to blur the first render target
53
+ this.renderTargetBlur = new WebGLRenderTarget(textureSize, textureSize);
54
+ this.renderTargetBlur.texture.generateMipmaps = false;
55
+
56
+ // make a plane and make it face up
57
+ const planeGeometry = new PlaneGeometry(1, 1).rotateX(Math.PI / 2);
58
+ //@ts-ignore
59
+ if (this.gameObject.isMesh) {
60
+ this.plane = this.gameObject as any as Mesh;
61
+ const mat = this.plane!.material as MeshBasicMaterial;
62
+ mat.map = this.renderTarget.texture;
63
+ // When someone makes a custom mesh, they can set these values right on the material.
64
+ // mat.opacity = this.state.plane.opacity;
65
+ // mat.transparent = true;
66
+ // mat.depthWrite = false;
67
+ }
68
+ else {
69
+ const planeMaterial = new MeshBasicMaterial({
70
+ map: this.renderTarget.texture,
71
+ opacity: this.opacity,
72
+ color: 0x000000,
73
+ transparent: true,
74
+ depthWrite: false,
75
+ });
76
+ this.plane = new Mesh(planeGeometry, planeMaterial);
77
+ this.plane.scale.y = - 1;
78
+ this.gameObject.add(this.plane);
79
+ }
80
+ if (this.plane) this.plane.renderOrder = 1;
81
+
82
+
83
+ if (this.occludeBelowGround) {
84
+ this.occluderMesh = new Mesh(this.plane.geometry, new MeshBasicMaterial({
85
+ depthWrite: true,
86
+ stencilWrite: true,
87
+ colorWrite: false,
88
+ })).rotateX(Math.PI).translateY(0.0001);
89
+ this.occluderMesh.renderOrder = -100;
90
+ this.gameObject.add(this.occluderMesh);
91
+ }
92
+
93
+ // the plane onto which to blur the texture
94
+ this.blurPlane = new Mesh(planeGeometry);
95
+ this.blurPlane.visible = false;
96
+ this.shadowGroup.add(this.blurPlane);
97
+
98
+ // max. ground distance is controlled via object scale
99
+ this.shadowCamera = new OrthographicCamera(-1 /2, 1/2, 1/2, -1/2, 0, 1.0);
100
+ this.shadowCamera.rotation.x = Math.PI / 2; // get the camera to look up
101
+ this.shadowGroup.add(this.shadowCamera);
102
+
103
+ // like MeshDepthMaterial, but goes from black to transparent
104
+ this.depthMaterial = new MeshDepthMaterial();
105
+ this.depthMaterial.userData.darkness = { value: this.darkness };
106
+ // this will properly overlap calculated shadows
107
+ this.depthMaterial.blending = CustomBlending;
108
+ this.depthMaterial.blendEquation = MaxEquation;
109
+ if (this.backfaceShadows)
110
+ this.depthMaterial.side = DoubleSide;
111
+
112
+ // this.depthMaterial.blendEquation = MinEquation;
113
+ this.depthMaterial.onBeforeCompile = shader => {
114
+ if (!this.depthMaterial) return;
115
+ shader.uniforms.darkness = this.depthMaterial.userData.darkness;
116
+ shader.fragmentShader = /* glsl */`
117
+ uniform float darkness;
118
+ ${shader.fragmentShader.replace(
119
+ 'gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );',
120
+ // we're scaling the shadow value down a bit when it's a backface (looks better)
121
+ 'gl_FragColor = vec4( vec3( 1.0 ), ( 1.0 - fragCoordZ ) * darkness * opacity * (gl_FrontFacing ? 1.0 : 0.66) );'
122
+ )}
123
+ `;
124
+ };
125
+
126
+ this.depthMaterial.depthTest = false;
127
+ this.depthMaterial.depthWrite = false;
128
+
129
+ this.horizontalBlurMaterial = new ShaderMaterial(HorizontalBlurShader);
130
+ this.horizontalBlurMaterial.depthTest = false;
131
+
132
+ this.verticalBlurMaterial = new ShaderMaterial(VerticalBlurShader);
133
+ this.verticalBlurMaterial.depthTest = false;
134
+
135
+ this.shadowGroup.visible = false;
136
+ }
137
+
138
+ onDestroy(): void {
139
+ // dispose the render targets
140
+ this.renderTarget?.dispose();
141
+ this.renderTargetBlur?.dispose();
142
+
143
+ // dispose the materials
144
+ this.depthMaterial?.dispose();
145
+ this.horizontalBlurMaterial?.dispose();
146
+ this.verticalBlurMaterial?.dispose();
147
+
148
+ // dispose the geometries
149
+ this.blurPlane?.geometry.dispose();
150
+ this.plane?.geometry.dispose();
151
+ this.occluderMesh?.geometry.dispose();
152
+ }
153
+
154
+ onBeforeRender(_frame: XRFrame | null): void {
155
+ const scene = this.context.scene;
156
+ const renderer = this.context.renderer;
157
+ const initialRenderTarget = renderer.getRenderTarget();
158
+
159
+ if (!this.renderTarget || !this.renderTargetBlur ||
160
+ !this.depthMaterial || !this.shadowCamera ||
161
+ !this.blurPlane || !this.shadowGroup || !this.plane ||
162
+ !this.horizontalBlurMaterial || !this.verticalBlurMaterial)
163
+ return;
164
+
165
+ //@ts-ignore
166
+ if (this.gameObject.isMesh)
167
+ this.gameObject.visible = false;
168
+
169
+ // Idea: shear the shadowCamera matrix to add some light direction to the ground shadows
170
+ /*
171
+ const mat = this.shadowCamera.projectionMatrix.clone();
172
+ this.shadowCamera.projectionMatrix.multiply(new Matrix4().makeShear(0, 0, 0, 0, 0, 0));
173
+ */
174
+
175
+ this.shadowGroup.visible = true;
176
+ if (this.occluderMesh) this.occluderMesh.visible = false;
177
+ const planeWasVisible = this.plane.visible;
178
+ this.plane.visible = false;
179
+
180
+ // remove the background
181
+ const initialBackground = scene.background;
182
+ scene.background = null;
183
+
184
+ // force the depthMaterial to everything
185
+ scene.overrideMaterial = this.depthMaterial;
186
+
187
+ // set renderer clear alpha
188
+ const initialClearAlpha = renderer.getClearAlpha();
189
+ renderer.setClearAlpha(0);
190
+
191
+ // render to the render target to get the depths
192
+ renderer.setRenderTarget(this.renderTarget);
193
+ renderer.render(scene, this.shadowCamera);
194
+
195
+ // for the shearing idea
196
+ // this.shadowCamera.projectionMatrix.copy(mat);
197
+
198
+ // and reset the override material
199
+ scene.overrideMaterial = null;
200
+
201
+ this.blurShadow(this.blur);
202
+
203
+ // a second pass to reduce the artifacts
204
+ // (0.4 is the minimum blur amout so that the artifacts are gone)
205
+ this.blurShadow(this.blur * 0.4);
206
+
207
+ this.shadowGroup.visible = false;
208
+ if (this.occluderMesh) this.occluderMesh.visible = true;
209
+ this.plane.visible = planeWasVisible;
210
+
211
+ // reset and render the normal scene
212
+ renderer.setRenderTarget(initialRenderTarget);
213
+ renderer.setClearAlpha(initialClearAlpha);
214
+ scene.background = initialBackground;
215
+ }
216
+
217
+ // renderTarget --> blurPlane (horizontalBlur) --> renderTargetBlur --> blurPlane (verticalBlur) --> renderTarget
218
+ private blurShadow(amount) {
219
+ if (!this.blurPlane || !this.shadowCamera ||
220
+ !this.renderTarget || !this.renderTargetBlur ||
221
+ !this.horizontalBlurMaterial || !this.verticalBlurMaterial)
222
+ return;
223
+
224
+ this.blurPlane.visible = true;
225
+
226
+ // blur horizontally and draw in the renderTargetBlur
227
+ this.blurPlane.material = this.horizontalBlurMaterial;
228
+ (this.blurPlane.material as ShaderMaterial).uniforms.tDiffuse.value = this.renderTarget.texture;
229
+ this.horizontalBlurMaterial.uniforms.h.value = amount * 1 / 256;
230
+
231
+ const renderer = this.context.renderer;
232
+
233
+ const currentRt = renderer.getRenderTarget();
234
+ renderer.setRenderTarget(this.renderTargetBlur);
235
+ renderer.render(this.blurPlane, this.shadowCamera);
236
+
237
+ // blur vertically and draw in the main renderTarget
238
+ this.blurPlane.material = this.verticalBlurMaterial;
239
+ (this.blurPlane.material as ShaderMaterial).uniforms.tDiffuse.value = this.renderTargetBlur.texture;
240
+ this.verticalBlurMaterial.uniforms.v.value = amount * 1 / 256;
241
+
242
+ renderer.setRenderTarget(this.renderTarget);
243
+ renderer.render(this.blurPlane, this.shadowCamera);
244
+
245
+ this.blurPlane.visible = false;
246
+
247
+ renderer.setRenderTarget(currentRt);
248
+ }
249
+ }