Needle Engine

Changes between version 3.38.0-alpha.3 and 3.39.0-alpha
Files changed (14) hide show
  1. src/engine/debug/debug_console.ts +1 -0
  2. src/engine/debug/debug_overlay.ts +1 -0
  3. src/engine/engine_gizmos.ts +10 -3
  4. src/engine/engine_license.ts +1 -0
  5. src/engine/engine_lods.ts +19 -8
  6. src/engine/engine_math.ts +24 -0
  7. src/engine/engine_scenelighting.ts +7 -1
  8. src/engine/engine_shims.ts +4 -0
  9. src/engine/engine_texture.ts +3 -0
  10. src/engine/engine_three_utils.ts +12 -2
  11. src/engine/engine_time.ts +8 -0
  12. src/engine/engine_utils.ts +39 -13
  13. src/engine-components/OrbitControls.ts +11 -5
  14. src/engine-components/webxr/WebXRImageTracking.ts +8 -4
src/engine/debug/debug_console.ts CHANGED
@@ -180,6 +180,7 @@
180
180
  text-align: center;
181
181
  background: #ffffff5c;
182
182
  backdrop-filter: blur(16px);
183
+ -webkit-backdrop-filter: blur(16px);
183
184
  user-select: none;
184
185
  pointer-events: auto;
185
186
  transition: transform .2s ease-in-out;
src/engine/debug/debug_overlay.ts CHANGED
@@ -297,6 +297,7 @@
297
297
  element.style.maxWidth = "250px";
298
298
  element.style.whiteSpace = "pre-wrap";
299
299
  element.style["backdrop-filter"] = "blur(10px)";
300
+ element.style["-webkit-backdrop-filter"] = "blur(10px)";
300
301
  element.style.backgroundColor = "rgba(20,20,20,.8)";
301
302
  element.style.boxShadow = "inset 0 0 80px rgba(0,0,0,.2), 0 0 5px rgba(0,0,0,.2)";
302
303
  element.style.border = "1px solid rgba(160,160,160,.2)";
src/engine/engine_gizmos.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { AxesHelper, Box3, BoxGeometry, BufferAttribute, BufferGeometry, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Matrix4, Mesh, MeshBasicMaterial,Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
1
+ import { AxesHelper, Box3, BoxGeometry, BufferAttribute, BufferGeometry, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Matrix4, Mesh, MeshBasicMaterial, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
2
2
  import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
3
3
  import { type Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
4
4
 
5
5
  import { isDestroyed } from './engine_gameobject.js';
6
- import { Context } from './engine_setup.js';
6
+ import { Context, FrameEvent } from './engine_setup.js';
7
7
  import { getWorldPosition, lookAtObject, setWorldPositionXYZ } from './engine_three_utils.js';
8
8
  import type { Vec3, Vec4 } from './engine_types.js';
9
9
  import { getParam } from './engine_utils.js';
@@ -380,7 +380,14 @@
380
380
  object.renderOrder = 999999;
381
381
  object[$cacheSymbol] = cache;
382
382
  this.timedObjectsBuffer.push(object);
383
- this.timesBuffer.push(Context.Current.time.realtimeSinceStartup + duration);
383
+
384
+ // Ensure if duration is 0 it is rendered for one frame only
385
+ if (duration <= 0 && context.currentFrameEvent >= FrameEvent.OnBeforeRender) {
386
+ this.timesBuffer.push(0);
387
+ }
388
+ else {
389
+ this.timesBuffer.push(Context.Current.time.realtimeSinceStartup + duration);
390
+ }
384
391
  context.scene.add(object);
385
392
  }
386
393
 
src/engine/engine_license.ts CHANGED
@@ -109,6 +109,7 @@
109
109
  zIndex: 2147483647;
110
110
  line-height: 1.5;
111
111
  backdrop-filter: blur(15px);
112
+ -webkit-backdrop-filter: blur(15px);
112
113
  `;
113
114
  const expectedStyle = div.style.cssText;
114
115
 
src/engine/engine_lods.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { LODsManager as _LODsManager, NEEDLE_progressive, NEEDLE_progressive_mesh_model, NEEDLE_progressive_plugin } from "@needle-tools/gltf-progressive";
2
+ import { LOD_Results } from "@needle-tools/gltf-progressive/src/lods_manager.js";
2
3
  import { Box3, BufferGeometry, Camera, Mesh, PerspectiveCamera, Scene, Sphere, Vector3, WebGLRenderer } from "three";
3
4
 
4
5
  import { findResourceUsers } from "./engine_assetdatabase.js";
@@ -14,13 +15,23 @@
14
15
  const _tempSphere: Sphere = new Sphere();
15
16
 
16
17
  /**
17
- * Needle Engine LODs manager. Wrapper around the internal LODs manager.
18
+ * Needle Engine LODs manager. Wrapper around the internal LODs manager.
19
+ * It uses the @needle-tools/gltf-progressive package to manage LODs.
20
+ * @link https://npmjs.com/package/@needle-tools/gltf-progressive
18
21
  */
19
22
  export class LODsManager implements NEEDLE_progressive_plugin {
20
23
  readonly context: Context;
21
24
  private _lodsManager?: _LODsManager;
22
25
 
23
26
  /**
27
+ * The internal LODs manager. See @needle-tools/gltf-progressive for more information.
28
+ * @link https://npmjs.com/package/@needle-tools/gltf-progressive
29
+ */
30
+ get manager() {
31
+ return this._lodsManager;
32
+ }
33
+
34
+ /**
24
35
  * The target triangle density is the desired max amount of triangles on screen when the mesh is filling the screen.
25
36
  * @default 200_000
26
37
  */
@@ -50,20 +61,21 @@
50
61
 
51
62
 
52
63
  /** @internal */
53
- onAfterUpdatedLOD(_renderer: WebGLRenderer, _scene: Scene, camera: Camera, mesh: Mesh, level: number): void {
64
+ onAfterUpdatedLOD(_renderer: WebGLRenderer, _scene: Scene, camera: Camera, mesh: Mesh, level: LOD_Results): void {
54
65
  if (debug) this.onRenderDebug(camera, mesh, level);
55
66
  }
56
67
 
57
- private onRenderDebug(camera: Camera, mesh: Mesh, level: number) {
68
+ private onRenderDebug(camera: Camera, mesh: Mesh, results: LOD_Results) {
58
69
 
59
70
  if (!mesh.geometry) return;
60
- if (!NEEDLE_progressive.hasLODLevelAvailable(mesh.geometry)) return;
71
+ if (!NEEDLE_progressive.hasLODLevelAvailable(mesh.geometry) && !NEEDLE_progressive.hasLODLevelAvailable(mesh.material)) return;
61
72
 
62
73
  const state = _LODsManager.getObjectLODState(mesh);
63
74
  if (!state) return;
64
75
 
65
76
 
66
- const changed = level != state.lastLodLevel;;
77
+ let level = results.mesh_lod;
78
+ const changed = results.mesh_lod != state.lastLodLevel_Mesh || results.texture_lod != state.lasLodLevel_Texture;
67
79
 
68
80
  if (debug && mesh.geometry.boundingSphere) {
69
81
  const bounds = mesh.geometry.boundingSphere;
@@ -105,7 +117,7 @@
105
117
  // Area is squared, so both maxBoxSize and wsMedian are squared here
106
118
  // Here, we're basically reverting the calculations that have happened in the pipeline for debugging.
107
119
  // const surfaceArea = 1 / density * triangleCount * (maxBoxSize * maxBoxSize) * (wsMedian * wsMedian);
108
- let text = "LOD " + level;
120
+ let text = "LOD " + results.mesh_lod + "\nTEX " + results.texture_lod;
109
121
  if (debug == "density") {
110
122
  text +=
111
123
  "\n" + triangleCount + " tris" +
@@ -141,8 +153,7 @@
141
153
  // const size = Math.min(10, radius);
142
154
  const windowScale = this.context.domHeight > 0 ? screen.height / this.context.domHeight : 1;
143
155
  const fieldOfViewScale = (camera as PerspectiveCamera).isPerspectiveCamera ? Math.tan((camera as PerspectiveCamera).fov * Math.PI / 180 / 2) : 1;
144
- Gizmos.DrawLabel(pos, text, distance * .01 * windowScale * fieldOfViewScale, undefined, 0xffffff, col);
145
- // mesh["LOD_level_label"] = helper;
156
+ Gizmos.DrawLabel(pos, text, distance * .012 * windowScale * fieldOfViewScale, undefined, 0xffffff, col);
146
157
  }
147
158
 
148
159
  }
src/engine/engine_math.ts CHANGED
@@ -148,13 +148,37 @@
148
148
  }
149
149
  }
150
150
 
151
+ /**
152
+ * OneEuroFilter is a simple low-pass filter for noisy signals. It uses a one-euro filter to smooth the signal.
153
+ */
151
154
  export class OneEuroFilter {
155
+ /**
156
+ * An estimate of the frequency in Hz of the signal (> 0), if timestamps are not available.
157
+ */
152
158
  freq: number;
159
+ /**
160
+ * Min cutoff frequency in Hz (> 0). Lower values allow to remove more jitter.
161
+ */
153
162
  minCutOff: number;
163
+ /**
164
+ * Parameter to reduce latency (> 0). Higher values make the filter react faster to changes.
165
+ */
154
166
  beta: number;
167
+ /**
168
+ * Used to filter the derivates. 1 Hz by default. Change this parameter if you know what you are doing.
169
+ */
155
170
  dCutOff: number;
171
+ /**
172
+ * The low-pass filter for the signal.
173
+ */
156
174
  x: LowPassFilter;
175
+ /**
176
+ * The low-pass filter for the derivates.
177
+ */
157
178
  dx: LowPassFilter;
179
+ /**
180
+ * The last time the filter was called.
181
+ */
158
182
  lasttime: number | null;
159
183
 
160
184
  /** Create a new OneEuroFilter
src/engine/engine_scenelighting.ts CHANGED
@@ -10,13 +10,14 @@
10
10
 
11
11
  const debug = getParam("debugenvlight");
12
12
 
13
-
13
+ /** @internal */
14
14
  export declare type SphericalHarmonicsData = {
15
15
  array: number[],
16
16
  texture: WebGLCubeRenderTarget | Texture,
17
17
  lightProbe?: LightProbe
18
18
  }
19
19
 
20
+ /** @internal */
20
21
  export enum AmbientMode {
21
22
  Skybox = 0,
22
23
  Trilight = 1,
@@ -24,11 +25,16 @@
24
25
  Custom = 4,
25
26
  }
26
27
 
28
+ /** @internal */
27
29
  export enum DefaultReflectionMode {
28
30
  Skybox = 0,
29
31
  Custom = 1,
30
32
  }
31
33
 
34
+ /**
35
+ * The RendererData class is used to manage the lighting settings of a scene.
36
+ * It is created and used within the Needle Engine Context.
37
+ */
32
38
  export class RendererData {
33
39
 
34
40
  private context: Context;
src/engine/engine_shims.ts CHANGED
@@ -1,5 +1,9 @@
1
+
2
+
1
3
  // REMOVE once we switch to TypeScript 5.x
2
4
  // OffscreenCanvas has a convertToBlob method, but TypeScript 4.x doesn't have the proper types for it.
5
+
6
+ /** @internal */
3
7
  export declare type OffscreenCanvasExt = OffscreenCanvas & {
4
8
  convertToBlob: (options?: any) => Promise<Blob>;
5
9
  }
src/engine/engine_texture.ts CHANGED
@@ -23,6 +23,9 @@
23
23
 
24
24
  /**
25
25
  * Render the scene to the texture
26
+ * @param scene The scene to render
27
+ * @param camera The camera to render from
28
+ * @param renderer The renderer or effectcomposer to use
26
29
  */
27
30
  render(scene: Object3D, camera: Camera, renderer: WebGLRenderer | EffectComposer) {
28
31
  if (renderer instanceof EffectComposer) {
src/engine/engine_three_utils.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  import { Mathf } from "./engine_math.js"
7
7
  import { CircularBuffer } from "./engine_utils.js";
8
8
 
9
-
9
+ /**
10
+ * Slerp between two vectors
11
+ */
10
12
  export function slerp(vec: Vector3, end: Vector3, t: number) {
11
13
  const len1 = vec.length();
12
14
  const len2 = end.length();
@@ -18,7 +20,6 @@
18
20
  const flipYQuat: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
19
21
 
20
22
  export function lookAtInverse(obj: Object3D, target: Vector3) {
21
-
22
23
  obj.lookAt(target);
23
24
  obj.quaternion.multiply(flipYQuat);
24
25
  }
@@ -76,6 +77,15 @@
76
77
  * @param y the y value
77
78
  * @param z the z value
78
79
  * @returns a temporary vector
80
+ *
81
+ * @example
82
+ * ``` javascript
83
+ * const vec = getTempVector(1, 2, 3);
84
+ * const vec2 = getTempVector(vec);
85
+ * const vec3 = getTempVector(new Vector3(1, 2, 3));
86
+ * const vec4 = getTempVector(new DOMPointReadOnly(1, 2, 3));
87
+ * const vec5 = getTempVector();
88
+ * ```
79
89
  */
80
90
  export function getTempVector(vecOrX?: Vector3 | number | DOMPointReadOnly, y?: number, z?: number) {
81
91
  const vec = _tempVecs.get();
src/engine/engine_time.ts CHANGED
@@ -7,6 +7,10 @@
7
7
  let timeScale = 1;
8
8
  if (typeof timescaleUrl === "number") timeScale = timescaleUrl;
9
9
 
10
+ /**
11
+ * Time is a class that provides time-related information.
12
+ * It is created and used within the Needle Engine Context.
13
+ */
10
14
  export class Time implements ITime {
11
15
 
12
16
  /** The time in seconds since the start of Needle Engine. */
@@ -19,6 +23,7 @@
19
23
  private set deltaTime(value: number) { this._deltaTime = value; }
20
24
  private _deltaTime = 0;
21
25
 
26
+ /** The time in seconds it took to complete the last frame (Read Only). Timescale is not applied. */
22
27
  get deltaTimeUnscaled() { return this._deltaTimeUnscaled; }
23
28
  private _deltaTimeUnscaled = 0;
24
29
 
@@ -54,6 +59,9 @@
54
59
  this.timeScale = timeScale;
55
60
  }
56
61
 
62
+ /** Step the time. This is called automatically by the Needle Engine Context.
63
+ * @internal
64
+ */
57
65
  update() {
58
66
  this.deltaTime = this.clock.getDelta();
59
67
  // clamp delta time because if tab is not active clock.getDelta can get pretty big
src/engine/engine_utils.ts CHANGED
@@ -8,7 +8,10 @@
8
8
  import { type SourceIdentifier } from "./engine_types.js";
9
9
 
10
10
  // https://schneidenbach.gitbooks.io/typescript-cookbook/content/nameof-operator.html
11
+ /** @internal */
11
12
  export const nameofFactory = <T>() => (name: keyof T) => name;
13
+
14
+ /** @internal */
12
15
  export function nameof<T>(name: keyof T) {
13
16
  return nameofFactory<T>()(name);
14
17
  }
@@ -16,12 +19,28 @@
16
19
  type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
17
20
  export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
18
21
 
22
+ /** @internal */
19
23
  export function isDebugMode(): boolean {
20
24
  return getParam("debug") ? true : false;
21
25
  }
22
26
 
23
27
 
24
-
28
+ /**
29
+ * The circular buffer class can be used to cache objects that don't need to be created every frame.
30
+ * This structure is used for e.g. Vector3 or Quaternion objects in the engine when calling `getTempVector3` or `getTempQuaternion`.
31
+ *
32
+ * @example Create a circular buffer that caches Vector3 objects. Max size is 10.
33
+ * ```typescript
34
+ * const buffer = new CircularBuffer(() => new Vector3(), 10);
35
+ * const vec = buffer.get();
36
+ * ```
37
+ *
38
+ * @example Create a circular buffer that caches Quaternion objects. Max size is 1000.
39
+ * ```typescript
40
+ * const buffer = new CircularBuffer(() => new Quaternion(), 1000);
41
+ * const quat = buffer.get();
42
+ * ```
43
+ */
25
44
  export class CircularBuffer<T> {
26
45
  private _factory: () => T;
27
46
  private _cache: T[] = [];
@@ -43,29 +62,36 @@
43
62
  }
44
63
  }
45
64
 
46
- let saveParams: boolean = false;
65
+ let showHelp: Param<"help"> = false;
47
66
  const requestedParams: Array<string> = new Array();
48
67
 
49
68
  if (typeof window !== "undefined") {
50
69
  setTimeout(() => {
51
- const debugHelp = getParam("debughelp");
52
- if (saveParams) {
70
+ // const debugHelp = getParam("debughelp");
71
+ if (showHelp) {
53
72
  const params = {};
54
73
  const url = new URL(window.location.href);
55
- const exampleUrlT = new URL(url);
56
- exampleUrlT.searchParams.append("help", "");
57
- const exampleUrl = url.toString().replace(/=$|=(?=&)/g, '');
74
+ const exampleUrl = new URL(url);
75
+ exampleUrl.searchParams.append("console", "");
76
+ const exampleUrlStr = exampleUrl.toString().replace(/=$|=(?=&)/g, '');
77
+ // Filter the params we're interested in
58
78
  for (const param of requestedParams) {
59
79
  const url2 = new URL(url);
60
80
  url2.searchParams.append(param, "");
81
+ // Save url with clean parameters (remove trailing = and empty parameters)
61
82
  params[param] = url2.toString().replace(/=$|=(?=&)/g, '');
62
83
  }
63
- console.log(
64
- "🌵 ?help: Debug Options for Needle Engine.\n" +
84
+ console.log(
85
+ "🌵 ?help: Debug Options for Needle Engine.\n" +
65
86
  "Append any of these parameters to the URL to enable specific debug options.\n" +
66
- `Example: ${exampleUrl} will show an onscreen console window.`);
67
- console.group("Available URL parameters:");
87
+ `Example: ${exampleUrlStr} will show an onscreen console window.`);
88
+ const postfix = showHelp === true ? "" : ` (containing "${showHelp}")`;
89
+ console.group("Available URL parameters:" + postfix);
68
90
  for (const key in params) {
91
+ // If ?help= is a string we only want to show the parameters that contain the string
92
+ if (typeof showHelp === "string") {
93
+ if (!key.toLowerCase().includes(showHelp.toLowerCase())) continue;
94
+ }
69
95
  console.groupCollapsed(key);
70
96
  // Needs to be a separate log, otherwise Safari doesn't turn the next line into a URL:
71
97
  console.log("Reload with this flag enabled:");
@@ -92,7 +118,7 @@
92
118
  * Returns the value if it exists e.g. ?message=hello
93
119
  */
94
120
  export function getParam<T extends string>(paramName: T): Param<T> {
95
- if (saveParams && !requestedParams.includes(paramName))
121
+ if (showHelp && !requestedParams.includes(paramName))
96
122
  requestedParams.push(paramName);
97
123
  const urlParams = getUrlParams();
98
124
  if (urlParams.has(paramName)) {
@@ -106,7 +132,7 @@
106
132
  }
107
133
  return false;
108
134
  }
109
- saveParams = getParam("help") === true;
135
+ showHelp = getParam("help");
110
136
 
111
137
  export function setParam(paramName: string, paramValue: string): void {
112
138
  const urlParams = getUrlParams();
src/engine-components/OrbitControls.ts CHANGED
@@ -242,14 +242,14 @@
242
242
  private _camera: Camera | null = null;
243
243
  private _syncedTransform?: SyncedTransform;
244
244
  private _didStart = false;
245
- private _didSetTarget = false;
245
+ private _didSetTarget = 0;
246
246
 
247
247
  targetElement: HTMLElement | null = null;
248
248
 
249
249
  /** @internal */
250
250
  awake(): void {
251
251
  this._didStart = false;
252
- this._didSetTarget = false;
252
+ this._didSetTarget = 0;
253
253
  this._startedListeningToKeyEvents = false;
254
254
  }
255
255
 
@@ -356,7 +356,7 @@
356
356
  if (!evt.hasRay) {
357
357
  evt.intersections.push(...this.context.physics.raycast());
358
358
  }
359
-
359
+
360
360
  if (evt.intersections.length <= 0) {
361
361
  const dt = this.context.time.time - this._lastTimeClickOnBackground;
362
362
  this._lastTimeClickOnBackground = this.context.time.time;
@@ -508,8 +508,9 @@
508
508
  }
509
509
 
510
510
  private __handleSetTargetWhenBecomingActiveTheFirstTime() {
511
- if (this._didSetTarget) return;
512
- this._didSetTarget = true;
511
+ // we want to wait one frame so all matrixWorlds are updated
512
+ // otherwise raycasting will not work correctly
513
+ if (this._didSetTarget++ != 1) return;
513
514
  if (this.autoTarget) {
514
515
 
515
516
  const camGo = GameObject.getComponent(this.gameObject, Camera);
@@ -593,6 +594,11 @@
593
594
  }
594
595
  this._lookTargetEndPosition.copy(position);
595
596
 
597
+ if (debug) {
598
+ console.warn("OrbitControls: setLookTargetPosition", position, immediateOrDuration);
599
+ Gizmos.DrawWireSphere(this._lookTargetEndPosition, .2, 0xff0000, 2);
600
+ }
601
+
596
602
  if (immediateOrDuration === true) {
597
603
  this._controls.target.copy(this._lookTargetEndPosition);
598
604
  }
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -41,9 +41,10 @@
41
41
 
42
42
  applyToObject(object: Object3D, t01: number | undefined = undefined) {
43
43
  this.ensureTransformData();
44
- // check if position/_position or rotation/_rotation changed more than just a little bit
45
- const haveChanged = object.position.distanceToSquared(this._position) > 0.05 || object.quaternion.angleTo(this._rotation) > 0.05;
46
- if (t01 === undefined || t01 >= 1 || haveChanged) {
44
+ // check if position/_position or rotation/_rotation changed more than just a little bit and adjust smoothing accordingly
45
+ const changeAmount = object.position.distanceToSquared(this._position) / 0.05 + object.quaternion.angleTo(this._rotation) / 0.05;
46
+ if (t01) t01 *= Math.max(1, changeAmount);
47
+ if (t01 === undefined || t01 >= 1) {
47
48
  object.position.copy(this._position);
48
49
  object.quaternion.copy(this._rotation);
49
50
  // InstancingUtil.markDirty(object);
@@ -231,6 +232,9 @@
231
232
  @serializable(WebXRImageTrackingModel)
232
233
  trackedImages?: WebXRImageTrackingModel[];
233
234
 
235
+ /** Applies smoothing based on detected jitter to the tracked image. */
236
+ smooth: boolean = true;
237
+
234
238
  private readonly trackedImageIndexMap: Map<number, WebXRImageTrackingModel> = new Map();
235
239
  private static _imageElements: Map<string, ImageBitmap | null> = new Map();
236
240
 
@@ -481,7 +485,7 @@
481
485
 
482
486
  xr.rig.gameObject.add(trackedData.object);
483
487
 
484
- image.applyToObject(trackedData.object);
488
+ image.applyToObject(trackedData.object, this.smooth ? this.context.time.deltaTimeUnscaled * 3 : undefined);
485
489
  if (!trackedData.object.activeSelf) {
486
490
  GameObject.setActive(trackedData.object, true);
487
491
  }