Needle Engine

Changes between version 3.47.4-beta.3 and 3.47.5-beta
Files changed (6) hide show
  1. src/engine/engine_context.ts +14 -11
  2. src/engine/engine_lifecycle_functions_internal.ts +15 -3
  3. src/engine/engine_physics.ts +14 -3
  4. src/engine/engine_scenetools.ts +14 -4
  5. src/engine/engine_serialization_builtin_serializer.ts +23 -7
  6. src/engine-components/OrbitControls.ts +6 -3
src/engine/engine_context.ts CHANGED
@@ -345,13 +345,12 @@
345
345
  readonly new_scripts_xr: INeedleXRSessionEventReceiver[] = [];
346
346
 
347
347
  /** The main camera component of the scene - this camera is used for rendering */
348
- mainCameraComponent: ICamera | undefined;
348
+ mainCameraComponent: ICamera | undefined = undefined;
349
349
 
350
-
351
350
  /** The main camera of the scene - this camera is used for rendering */
352
- get mainCamera(): Camera | null {
353
- if (this._camera) {
354
- return this._camera;
351
+ get mainCamera(): Camera {
352
+ if (this._mainCamera) {
353
+ return this._mainCamera;
355
354
  }
356
355
  if (this.mainCameraComponent) {
357
356
  const cam = this.mainCameraComponent as ICamera;
@@ -359,13 +358,17 @@
359
358
  cam.buildCamera();
360
359
  return cam.cam;
361
360
  }
362
- return null;
361
+ if (!this._fallbackCamera) {
362
+ this._fallbackCamera = new PerspectiveCamera(75, this.domWidth / this.domHeight, 0.1, 1000);
363
+ }
364
+ return this._fallbackCamera;
363
365
  }
364
366
  /** Set the main camera of the scene. If set to null the camera of the {@link mainCameraComponent} will be used - this camera is used for rendering */
365
367
  set mainCamera(cam: Camera | null) {
366
- this._camera = cam;
368
+ this._mainCamera = cam;
367
369
  }
368
- private _camera: Camera | null = null;
370
+ private _mainCamera: Camera | null = null;
371
+ private _fallbackCamera: PerspectiveCamera | null = null;
369
372
 
370
373
  application: Application;
371
374
  /** access animation mixer used by components in the scene */
@@ -413,7 +416,7 @@
413
416
  if (args?.runInBackground !== undefined) this.runInBackground = args.runInBackground;
414
417
  if (args?.scene) this.scene = args.scene;
415
418
  else this.scene = new Scene();
416
- if (args?.camera) this._camera = args.camera;
419
+ if (args?.camera) this._mainCamera = args.camera;
417
420
 
418
421
  this.application = new Application(this);
419
422
  this.time = new Time();
@@ -920,7 +923,7 @@
920
923
  }
921
924
  }
922
925
 
923
- if (!this.mainCamera) {
926
+ if (!this._mainCamera) {
924
927
  Context.Current = this;
925
928
  let camera: ICamera | null = null;
926
929
  foreachComponent(this.scene, comp => {
@@ -1093,7 +1096,7 @@
1093
1096
  for (let i = 0; i < res.file.parser.json.materials.length; i++) {
1094
1097
  const mat = await res.file.parser.getDependency("material", i);
1095
1098
  const parent = new Object3D();
1096
- parent.position.x = i;
1099
+ parent.position.x = i * 1.1;
1097
1100
  parent.position.y = y;
1098
1101
  this.scene.add(parent);
1099
1102
  ObjectUtils.createPrimitive("ShaderBall", {
src/engine/engine_lifecycle_functions_internal.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  import { type Context, FrameEvent } from "./engine_context.js";
2
2
  import { ContextEvent } from "./engine_context_registry.js";
3
- import { safeInvoke } from "./engine_generic_utils.js";
4
3
 
5
4
  export declare type Event = ContextEvent | FrameEvent;
6
5
 
6
+ declare type LifecycleHookContext = {
7
+ context: Context;
8
+ }
9
+
7
10
  /**
8
11
  * A function that can be called during the Needle Engine frame event at a specific point
9
12
  * @link https://engine.needle.tools/docs/scripting.html#special-lifecycle-hooks
10
13
  */
11
- export declare type LifecycleMethod = (ctx: Context) => void;
14
+ export declare type LifecycleMethod = (this: LifecycleHookContext, ctx: Context) => void;
12
15
  /**
13
16
  * Options for `onStart(()=>{})` etc event hooks
14
17
  * @link https://engine.needle.tools/docs/scripting.html#special-lifecycle-hooks
@@ -139,6 +142,9 @@
139
142
  }
140
143
 
141
144
  const bufferArray = new Array<RegisteredLifecycleMethod>();
145
+ const hookContext: LifecycleHookContext = {
146
+ context: null as any as Context
147
+ };
142
148
 
143
149
  function invoke(ctx: Context, methods: Array<RegisteredLifecycleMethod>, invokeOnce: boolean) {
144
150
  bufferArray.length = 0;
@@ -155,7 +161,13 @@
155
161
  }
156
162
 
157
163
  if (invoke) {
158
- safeInvoke(entry.method, ctx);
164
+ try {
165
+ hookContext.context = ctx;
166
+ entry.method?.call(hookContext, ctx);
167
+ }
168
+ catch (e) {
169
+ console.error("Error in lifecycle method", e);
170
+ }
159
171
  }
160
172
 
161
173
  // Remove the method if it's a one time call
src/engine/engine_physics.ts CHANGED
@@ -359,7 +359,7 @@
359
359
  NEMeshBVH.runMeshBVHRaycast(raycaster, mesh, results, this.context);
360
360
  }
361
361
  else {
362
- raycaster.intersectObject(obj, false, results);
362
+ raycaster.intersectObject(mesh, false, results);
363
363
  }
364
364
 
365
365
  if (debugPhysics && results.length != lastResultsCount)
@@ -615,15 +615,21 @@
615
615
  }
616
616
  }
617
617
  // if there are no workers available, create a new one
618
- else if (workerInstances.length < 1) {
618
+
619
+ if (!workerInstance && workerInstances.length < 3) {
619
620
  workerInstance = new _GenerateMeshBVHWorker();
620
621
  workerInstances.push(workerInstance);
621
622
  }
622
623
 
623
624
  if (workerInstance != null && !workerInstance.running) {
625
+ if (debugPhysics) console.log("<<<< worker start", workerInstance);
624
626
  geom[workerTaskSymbol] = "queued";
625
627
  performance.mark("bvh.create.start");
626
- workerInstance.generate(geom)
628
+ // If we don't clone the buffer geometry here we will get a "Transferable ArrayBuffer" error
629
+ // see https://linear.app/needle/issue/NE-5602
630
+ // Additionally normal raycasts stop working if we don't clone the geometry
631
+ const copy = geom.clone();
632
+ workerInstance.generate(copy)
627
633
  .then(bvh => {
628
634
  geom[workerTaskSymbol] = "done";
629
635
  geom.boundsTree = bvh;
@@ -633,7 +639,9 @@
633
639
  geom[canUseWorkerSymbol] = false;
634
640
  })
635
641
  .finally(() => {
642
+ if (debugPhysics) console.log(">>>>> worker done");
636
643
  availableWorkers.push(workerInstance);
644
+ copy.dispose();
637
645
  performance.mark("bvh.create.end");
638
646
  performance.measure("bvh.create (worker)", "bvh.create.start", "bvh.create.end");
639
647
  });
@@ -670,6 +678,9 @@
670
678
  }
671
679
  mesh.raycast = mesh.acceleratedRaycast;
672
680
  }
681
+ else {
682
+ if (debugPhysics) console.warn("No bounds tree found for mesh", mesh.name);
683
+ }
673
684
  const prevFirstHitOnly = raycaster.firstHitOnly;
674
685
  raycaster.firstHitOnly = false;
675
686
  raycaster.intersectObject(mesh, false, results);
src/engine/engine_scenetools.ts CHANGED
@@ -275,10 +275,20 @@
275
275
 
276
276
  async function compileAsync(scene: Object3D, context: Context, camera?: Camera | null) {
277
277
  if (!camera) camera = context.mainCamera;
278
- if (camera)
279
- await context.renderer.compileAsync(scene, camera, context.scene);
280
- else
281
- registerPrewarmObject(scene, context);
278
+ try {
279
+ if (camera)
280
+ {
281
+ await context.renderer.compileAsync(scene, camera, context.scene)
282
+ .catch(err => {
283
+ console.warn(err.message);
284
+ });
285
+ }
286
+ else
287
+ registerPrewarmObject(scene, context);
288
+ }
289
+ catch (err:Error | any) {
290
+ console.warn(err?.message || err);
291
+ }
282
292
  }
283
293
 
284
294
  function checkIfUserAttemptedToLoadALocalFile(url: string) {
src/engine/engine_serialization_builtin_serializer.ts CHANGED
@@ -399,6 +399,18 @@
399
399
  export const eventListSerializer = new EventListSerializer();
400
400
 
401
401
 
402
+ /** Map<Clone, Original> texture. This is used for compressed textures (or when the GLTFLoader is cloning RenderTextures)
403
+ * It's a weak map so we don't have to worry about memory leaks
404
+ */
405
+ const cloneOriginalMap = new WeakMap<Texture, Texture>();
406
+ const textureClone = Texture.prototype.clone;
407
+ Texture.prototype.clone = function () {
408
+ const clone = textureClone.call(this);
409
+ if (!cloneOriginalMap.has(clone)) {
410
+ cloneOriginalMap.set(clone, this);
411
+ }
412
+ return clone;
413
+ }
402
414
 
403
415
  export class RenderTextureSerializer extends TypeSerializer {
404
416
  constructor() {
@@ -410,20 +422,20 @@
410
422
 
411
423
  onDeserialize(data: any, context: SerializationContext) {
412
424
  if (data instanceof Texture && context.type === RenderTexture) {
413
- const tex = data as Texture;
414
- const rt = new RenderTexture(tex.image.width, tex.image.height, {
415
- colorSpace: LinearSRGBColorSpace,
416
- });
417
- rt.texture = tex;
425
+ let tex = data as Texture;
426
+ // If this is a cloned render texture we want to map it back to the original texture
427
+ // See https://linear.app/needle/issue/NE-5530
428
+ if (cloneOriginalMap.has(tex)) {
429
+ const original = cloneOriginalMap.get(tex)!;
430
+ tex = original;
431
+ }
418
432
  tex.isRenderTargetTexture = true;
419
433
  tex.flipY = true;
420
434
  tex.offset.y = 1;
421
435
  tex.repeat.y = -1;
422
436
  tex.needsUpdate = true;
423
-
424
437
  // when we have a compressed texture using mipmaps causes error in threejs because the bindframebuffer call will then try to set an array of framebuffers https://linear.app/needle/issue/NE-4294
425
438
  tex.mipmaps = [];
426
-
427
439
  if (tex instanceof CompressedTexture) {
428
440
  //@ts-ignore
429
441
  tex["isCompressedTexture"] = false;
@@ -431,6 +443,10 @@
431
443
  tex.format = RGBAFormat;
432
444
  }
433
445
 
446
+ const rt = new RenderTexture(tex.image.width, tex.image.height, {
447
+ colorSpace: LinearSRGBColorSpace,
448
+ });
449
+ rt.texture = tex;
434
450
  return rt;
435
451
  }
436
452
  return undefined;
src/engine-components/OrbitControls.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Box3Helper, Object3D, PerspectiveCamera, Ray, Vector2, Vector3 } from "three";
1
+ import { Box3Helper, Object3D, PerspectiveCamera, Ray, Vector2, Vector3, Vector3Like } from "three";
2
2
  import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
3
3
  import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js";
4
4
 
@@ -586,7 +586,7 @@
586
586
  * @param position The position in local space of the controllerObject to move the camera to. If null the camera will stop lerping to the target.
587
587
  * @param immediateOrDuration If true the camera will move immediately to the new position, otherwise it will lerp. If a number is passed in it will be used as the duration of the lerp.
588
588
  */
589
- public setCameraTargetPosition(position?: Object3D | Vector3 | null, immediateOrDuration: boolean | number = false) {
589
+ public setCameraTargetPosition(position?: Object3D | Vector3Like | null, immediateOrDuration: boolean | number = false) {
590
590
  if (!position) return;
591
591
  if (position instanceof Object3D) {
592
592
  position = getWorldPosition(position) as Vector3;
@@ -618,7 +618,7 @@
618
618
  * @param position The position in world space to move the camera target to. If null the camera will stop lerping to the target.
619
619
  * @param immediateOrDuration If true the camera target will move immediately to the new position, otherwise it will lerp. If a number is passed in it will be used as the duration of the lerp.
620
620
  */
621
- public setLookTargetPosition(position: Object3D | Vector3 | null = null, immediateOrDuration: boolean | number = false) {
621
+ public setLookTargetPosition(position: Object3D | Vector3Like | null = null, immediateOrDuration: boolean | number = false) {
622
622
  if (!this._controls) return;
623
623
  if (!position) return
624
624
  if (position instanceof Object3D) {
@@ -626,6 +626,9 @@
626
626
  }
627
627
  this._lookTargetEndPosition.copy(position);
628
628
 
629
+ // if a user calls setLookTargetPosition we don't want to perform autoTarget in onBeforeRender (and override whatever the user set here)
630
+ this._didSetTarget++;
631
+
629
632
  if (debug) {
630
633
  console.warn("OrbitControls: setLookTargetPosition", position, immediateOrDuration);
631
634
  Gizmos.DrawWireSphere(this._lookTargetEndPosition, .2, 0xff0000, 2);