Needle Engine

Changes between version 3.32.3-alpha and 3.32.4-alpha
Files changed (11) hide show
  1. src/engine/debug/debug_console.ts +5 -2
  2. src/engine/debug/debug_overlay.ts +6 -5
  3. src/engine-components/Duplicatable.ts +2 -1
  4. src/engine-components/Light.ts +1 -1
  5. src/needle-engine.ts +0 -3
  6. src/engine/xr/NeedleXRController.ts +3 -1
  7. src/engine/xr/NeedleXRSession.ts +91 -8
  8. src/engine-components/PlayerColor.ts +1 -1
  9. src/engine-components-experimental/networking/PlayerSync.ts +1 -1
  10. src/engine-components/webxr/WebXR.ts +1 -1
  11. src/engine-components/webxr/controllers/XRControllerMovement.ts +13 -1
src/engine/debug/debug_console.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { getErrorCount } from "./debug_overlay.js";
1
+ import { getErrorCount, makeErrorsVisibleForDevelopment } from "./debug_overlay.js";
2
2
  import { getParam, isMobileDevice, isQuest } from "../engine_utils.js";
3
3
  import { isLocalNetwork } from "../engine_networking_utils.js";
4
4
  import { isDevEnvironment } from "./debug.js";
@@ -25,6 +25,9 @@
25
25
  }
26
26
  const isMobile = isMobileDevice() || (isQuest() && isDevEnvironment());
27
27
  if (isMobile) {
28
+ // we need to invoke this here - otherwise we will miss errors that happen after the console is loaded
29
+ // and calling the method from the root needle-engine.ts import is evaluated later (if we import the method from the toplevel file and then invoke it)
30
+ makeErrorsVisibleForDevelopment();
28
31
  beginWatchingLogs();
29
32
  createConsole(true);
30
33
  if (isMobile) {
@@ -192,7 +195,7 @@
192
195
  }
193
196
  `;
194
197
  consoleHtmlElement?.prepend(styles);
195
- if (startHidden === true)
198
+ if (startHidden === true && getErrorCount() <= 0)
196
199
  hideDebugConsole();
197
200
  console.log("🌵 Debug console has loaded");
198
201
  }
src/engine/debug/debug_overlay.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  }
16
16
 
17
17
  export function getErrorCount() {
18
- return errorCount;
18
+ return _errorCount;
19
19
  }
20
20
 
21
21
  const originalConsoleError = console.error;
@@ -37,9 +37,10 @@
37
37
  if (hide) return;
38
38
  const isLocal = isLocalNetwork();
39
39
  if (debug) console.log("Is this a local network?", isLocal);
40
- if (isLocal) {
40
+ if (isLocal)
41
+ {
41
42
  if (debug)
42
- console.log(window.location.hostname);
43
+ console.warn("Patch console", window.location.hostname);
43
44
  console.error = patchedConsoleError;
44
45
  window.addEventListener("error", (event) => {
45
46
  if (hide) return;
@@ -66,10 +67,10 @@
66
67
  }
67
68
 
68
69
 
69
- let errorCount = 0;
70
+ let _errorCount = 0;
70
71
 
71
72
  function onReceivedError() {
72
- errorCount += 1;
73
+ _errorCount += 1;
73
74
  }
74
75
 
75
76
  function onParseError(args: Array<any>) {
src/engine-components/Duplicatable.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  import { Vector3, Quaternion, Object3D } from "three";
4
4
  import { serializable } from "../engine/engine_serialization_decorator.js";
5
5
  import { InstantiateOptions } from "../engine/engine_gameobject.js";
6
- import { IPointerEventHandler, ObjectRaycaster, PointerEventData } from "./api.js";
6
+ import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
7
+ import { ObjectRaycaster } from "./ui/Raycaster.js";
7
8
 
8
9
  export class Duplicatable extends Behaviour implements IPointerEventHandler {
9
10
 
src/engine-components/Light.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { Color, DirectionalLight, OrthographicCamera } from "three";
8
8
  import { WebARSessionRoot } from "./webxr/WebARSessionRoot.js";
9
9
  import type { ILight } from "../engine/engine_types.js";
10
- import { NeedleXREventArgs } from "../needle-engine.js";
10
+ import { NeedleXREventArgs } from "../engine/xr/index.js";
11
11
 
12
12
  // https://threejs.org/examples/webgl_shadowmap_csm.html
13
13
 
src/needle-engine.ts CHANGED
@@ -1,6 +1,3 @@
1
- import { makeErrorsVisibleForDevelopment } from "./engine/debug/debug_overlay.js";
2
- makeErrorsVisibleForDevelopment();
3
-
4
1
  import "./engine/engine_element.js";
5
2
  import "./engine/engine_setup.js";
6
3
  export * from "./engine/api.js";
src/engine/xr/NeedleXRController.ts CHANGED
@@ -116,7 +116,9 @@
116
116
  get hitTestSource() { return this._hitTestSource; }
117
117
  private _hitTestSource: XRTransientInputHitTestSource | undefined = undefined;
118
118
 
119
- /** Perform a hit test against the XR planes or meshes. shorthand for `xr.getHitTest(controller)` */
119
+ /** Perform a hit test against the XR planes or meshes. shorthand for `xr.getHitTest(controller)`
120
+ * @returns the hit test result (with position and rotation in worldspace) or null if no hit was found
121
+ */
120
122
  getHitTest(): NeedleXRHitTestResult | null {
121
123
  return this.xr.getHitTest(this);
122
124
  }
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AxesHelper, Matrix4, Object3D, PerspectiveCamera, Quaternion, Ray, Vector3, WebXRArrayCamera } from "three";
1
+ import { Mesh, Object3D, PerspectiveCamera, Quaternion, Vector3, PlaneHelper, PlaneGeometry, MeshBasicMaterial, Camera, WebXRArrayCamera, DoubleSide } from "three";
2
2
  import { Context, FrameEvent } from "../engine_context.js";
3
3
  import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
4
4
  import { Gizmos } from "../engine_gizmos.js";
@@ -13,11 +13,18 @@
13
13
  import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
14
14
  import { isDestroyed } from "../engine_gameobject.js";
15
15
  import { TemporaryXRContext } from "./TempXRContext.js";
16
+ import { Mathf } from "../engine_math.js";
16
17
 
17
18
  export type NeedleXREventArgs = { xr: NeedleXRSession }
18
19
  export type SessionChangedEvt = (args: NeedleXREventArgs) => void;
19
20
  export type SessionRequestedEvent = (args: { mode: XRSessionMode, init: XRSessionInit }) => void;
20
21
  export type SessionRequestedEndEvent = (args: { mode: XRSessionMode, init: XRSessionInit, newSession: XRSession | null }) => void;
22
+
23
+ /** Result of a XR hit-test
24
+ * @property {XRHitTestResult} hit The original XRHitTestResult
25
+ * @property {Vector3} position The hit position in world space
26
+ * @property {Quaternion} quaternion The hit rotation in world space
27
+ */
21
28
  export type NeedleXRHitTestResult = { hit: XRHitTestResult, position: Vector3, quaternion: Quaternion };
22
29
 
23
30
  const debug = getParam("debugwebxr");
@@ -675,8 +682,8 @@
675
682
  this._controllerRemoved = extra.controller_removed;
676
683
 
677
684
  registerFrameEventCallback(this.onBefore, FrameEvent.LateUpdate);
678
- // this.context.pre_render_callbacks.push(this.onBefore);
679
- this.context.post_render_callbacks.push(this.onAfter);
685
+ this.context.pre_render_callbacks.push(this.onBeforeRender);
686
+ this.context.post_render_callbacks.push(this.onAfterRender);
680
687
 
681
688
 
682
689
  if (extra.init.optionalFeatures?.includes("hit-test") || extra.init.requiredFeatures?.includes("hit-test")) {
@@ -763,15 +770,15 @@
763
770
 
764
771
  deleteSessionInfo();
765
772
 
766
- this.onAfter();
773
+ this.onAfterRender();
767
774
  this.revertCustomForward();
768
775
  this._didStart = false;
769
776
  this._previousCameraParent = null;
770
777
 
771
- // const index = this.context.pre_render_callbacks.indexOf(this.onBefore);
772
- // if (index >= 0) this.context.pre_render_callbacks.splice(index, 1);
773
778
  unregisterFrameEventCallback(this.onBefore, FrameEvent.LateUpdate);
774
- const index2 = this.context.post_render_callbacks.indexOf(this.onAfter);
779
+ const index = this.context.pre_render_callbacks.indexOf(this.onBeforeRender);
780
+ if (index >= 0) this.context.pre_render_callbacks.splice(index, 1);
781
+ const index2 = this.context.post_render_callbacks.indexOf(this.onAfterRender);
775
782
  if (index2 >= 0) this.context.post_render_callbacks.splice(index2, 1);
776
783
 
777
784
  this.context.xr = null;
@@ -999,7 +1006,14 @@
999
1006
  }
1000
1007
  }
1001
1008
 
1002
- private onAfter = () => {
1009
+ private onBeforeRender = () => {
1010
+ if (this.context.mainCamera)
1011
+ this.updateFade(this.context.mainCamera);
1012
+ }
1013
+
1014
+ private onAfterRender = () => {
1015
+ this.onUpdateFade_PostRender();
1016
+
1003
1017
  // render spectator view if we're in VR using Link
1004
1018
  if (isDesktop()) {
1005
1019
  const renderer = this.context.renderer;
@@ -1178,4 +1192,73 @@
1178
1192
  this._transformOrientation.set(transform.orientation.x, transform.orientation.y, transform.orientation.z, transform.orientation.w);
1179
1193
  }
1180
1194
  }
1195
+
1196
+ // TODO: for scene transitions (e.g. SceneSwitcher) where creating the scene might take a few moments we might want more control over when/how this fading occurs and how long the scene stays black
1197
+
1198
+ /** Call to fade rendering to black for a short moment (the returned promise will be resolved when fully black)
1199
+ * This can be used to mask scene transitions or teleportation
1200
+ * @returns a promise that is resolved when the screen is fully black
1201
+ * @example `fadeTransition().then(() => { <fully_black> })`
1202
+ */
1203
+ fadeTransition() {
1204
+ if (this._transitionPromise) return this._transitionPromise;
1205
+ this._requestedFadeValue = 1;
1206
+ const promise = new Promise<void>(resolve => {
1207
+ this._transitionResolve = resolve;
1208
+ });
1209
+ this._transitionPromise = promise;
1210
+ return promise;
1211
+ }
1212
+
1213
+ private _requestedFadeValue: number = 0;
1214
+ private _transitionPromise: Promise<void> | null = null;
1215
+ private _transitionResolve: (() => void) | null = null;
1216
+ private _fadeToColorQuad: Mesh | null = null;
1217
+ private _fadeToColorMaterial!: MeshBasicMaterial;
1218
+
1219
+ /** e.g. FadeToBlack */
1220
+ private updateFade(camera: Camera) {
1221
+ if (!(camera instanceof PerspectiveCamera)) return;
1222
+ if (!this._fadeToColorQuad || !this._fadeToColorMaterial) {
1223
+ this._fadeToColorMaterial = new MeshBasicMaterial({
1224
+ color: 0x000000,
1225
+ transparent: true,
1226
+ depthTest: false,
1227
+ fog: false,
1228
+ side: DoubleSide,
1229
+ });
1230
+ this._fadeToColorQuad = new Mesh(new PlaneGeometry(10, 10), this._fadeToColorMaterial);
1231
+ }
1232
+
1233
+ // ensure that the quad is setup with the right properties
1234
+ const quad = this._fadeToColorQuad;
1235
+ const mat = this._fadeToColorMaterial;
1236
+
1237
+ // make sure the quad is in the scene
1238
+ if (quad.parent !== camera && mat.opacity > 0) {
1239
+ camera!.add(quad);
1240
+ }
1241
+ else if (mat.opacity === 0) {
1242
+ quad.removeFromParent();
1243
+ }
1244
+ quad.layers.set(2);
1245
+ quad.material = this._fadeToColorMaterial!;
1246
+ quad.position.z = -1;
1247
+ // perform the fade
1248
+ const fadeValue = this._requestedFadeValue;
1249
+ mat.opacity = Mathf.lerp(mat.opacity, fadeValue, this.context.time.deltaTime / .03);
1250
+ // check if we're close enough to the desired value:
1251
+ if (Math.abs(mat.opacity - fadeValue) <= .01) {
1252
+ if (this._transitionResolve) {
1253
+ this._transitionResolve();
1254
+ this._transitionResolve = null;
1255
+ this._transitionPromise = null;
1256
+ this._requestedFadeValue = 0;
1257
+ }
1258
+ }
1259
+ }
1260
+
1261
+ private onUpdateFade_PostRender() {
1262
+ this._fadeToColorQuad?.removeFromParent();
1263
+ }
1181
1264
  }
src/engine-components/PlayerColor.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import * as THREE from "three";
4
4
  import { WaitForSeconds } from "../engine/engine_coroutine.js";
5
5
  import { PlayerState } from "../engine-components-experimental/networking/PlayerSync.js";
6
- import { AvatarMarker } from "./api.js";
6
+ import { AvatarMarker } from "./webxr/WebXRAvatar.js";
7
7
 
8
8
 
9
9
  export class PlayerColor extends Behaviour {
src/engine-components-experimental/networking/PlayerSync.ts CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { Object3D } from "three";
10
10
  import { EventList } from "../../engine-components/EventList.js";
11
- import { IGameObject } from "../../needle-engine.js";
11
+ import { IGameObject } from "../../engine/engine_types.js";
12
12
 
13
13
 
14
14
  const debug = getParam("debugplayersync");
src/engine-components/webxr/WebXR.ts CHANGED
@@ -267,7 +267,7 @@
267
267
  if (this.createARButton || this.createVRButton || this.useQuicklookExport) {
268
268
  // Quicklook / iOS
269
269
  if ((isiOS() && isSafari()) || debugQuicklook) {
270
- if (this.createARButton || this.useQuicklookExport) {
270
+ if (this.useQuicklookExport) {
271
271
  this.getButtonsContainer().createQuicklookButton();
272
272
  }
273
273
  }
src/engine-components/webxr/controllers/XRControllerMovement.ts CHANGED
@@ -34,6 +34,10 @@
34
34
  @serializable()
35
35
  useTeleportTarget = false;
36
36
 
37
+ /** Enable to fade out the scene when teleporting */
38
+ @serializable()
39
+ useTeleportFade = false;
40
+
37
41
  /** enable to visualize controller rays in the 3D scene */
38
42
  @serializable()
39
43
  showRays: boolean = true;
@@ -137,8 +141,16 @@
137
141
  const teleportTarget = GameObject.getComponentInParent(hit.object, TeleportTarget);
138
142
  if (!teleportTarget) return;
139
143
  }
140
- rig.worldPosition = hit.point;
141
144
  if (debug) Gizmos.DrawSphere(hit.point, .025, 0xff0000, 5);
145
+ const point = hit.point.clone();
146
+ if (this.useTeleportFade) {
147
+ controller.xr.fadeTransition()?.then(() => {
148
+ rig.worldPosition = point;
149
+ })
150
+ }
151
+ else {
152
+ rig.worldPosition = point;
153
+ }
142
154
  }
143
155
  else {
144
156
  // TODO: add option to allow teleportation on current ground plane