@@ -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
|
353
|
-
if (this.
|
354
|
-
return this.
|
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
|
-
|
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.
|
368
|
+
this._mainCamera = cam;
|
367
369
|
}
|
368
|
-
private
|
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.
|
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.
|
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", {
|
@@ -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
|
-
|
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
|
@@ -359,7 +359,7 @@
|
|
359
359
|
NEMeshBVH.runMeshBVHRaycast(raycaster, mesh, results, this.context);
|
360
360
|
}
|
361
361
|
else {
|
362
|
-
raycaster.intersectObject(
|
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
|
-
|
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
|
-
|
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);
|
@@ -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
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
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) {
|
@@ -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
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
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;
|
@@ -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 |
|
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 |
|
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);
|