Needle Engine

Changes between version 3.37.2-alpha and 3.37.3-alpha
Files changed (9) hide show
  1. plugins/vite/meta.js +5 -2
  2. src/engine/engine_application.ts +9 -2
  3. src/engine/engine_gizmos.ts +1 -1
  4. src/engine/engine_loaders.ts +10 -2
  5. src/engine/engine_networking.ts +20 -1
  6. src/engine-components/LookAtConstraint.ts +14 -0
  7. src/engine/extensions/NEEDLE_progressive.ts +9 -15
  8. src/engine-components/Renderer.ts +150 -50
  9. src/engine-components/ScreenCapture.ts +3 -0
plugins/vite/meta.js CHANGED
@@ -5,6 +5,8 @@
5
5
  import { getPosterPath } from './poster.js';
6
6
 
7
7
  /**
8
+ * @param {string} command
9
+ * @param {import('../types').needleMeta} config
8
10
  * @param {import('../types').userSettings} userSettings
9
11
  */
10
12
  export const needleMeta = (command, config, userSettings) => {
@@ -25,14 +27,15 @@
25
27
  enforce: 'pre',
26
28
  transform(html, _ctx) {
27
29
 
28
- html = insertNeedleCredits(html);
29
-
30
30
  if (userSettings.allowMetaPlugin === false) return [];
31
31
 
32
32
  // this is useful to get the latest config exported from editor
33
33
  // whenever vite wants to transform the html
34
34
  updateConfig();
35
35
 
36
+ if (!config.license)
37
+ html = insertNeedleCredits(html);
38
+
36
39
  // early out of the config is invalid / doesn't contain meta information
37
40
  // TODO might be better to handle these edge cases / special cases right from Unity/Blender and not here
38
41
  if (!config) return [];
src/engine/engine_application.ts CHANGED
@@ -36,7 +36,12 @@
36
36
  return userInteractionRegistered;
37
37
  }
38
38
 
39
- public static registerWaitForAllowAudio(cb: Function) {
39
+ /**
40
+ * Register a callback that will be called when the user interacts with the page (click, touch, keypress, etc).
41
+ * If the user has already interacted with the page, the callback will be called immediately.
42
+ * This can be used to wait for user interaction before playing audio, for example.
43
+ */
44
+ public static registerWaitForInteraction(cb: Function) {
40
45
  if (cb !== null) {
41
46
  if (userInteractionRegistered) {
42
47
  cb();
@@ -46,6 +51,8 @@
46
51
  userInteractionCallbacks.push(cb);
47
52
  }
48
53
  }
54
+ /** @deprecated use Application.registerWaitForInteraction instead */
55
+ public static readonly registerWaitForAllowAudio = Application.registerWaitForInteraction;
49
56
 
50
57
  private _mute: boolean = false;
51
58
  /** audio muted? */
@@ -57,7 +64,7 @@
57
64
  this.dispatchEvent(new Event(ApplicationEvents.MuteChanged));
58
65
  }
59
66
 
60
- private context: Context;
67
+ private readonly context: Context;
61
68
 
62
69
  /** @returns true if the document is focused */
63
70
  public get hasFocus(): boolean {
src/engine/engine_gizmos.ts CHANGED
@@ -190,7 +190,7 @@
190
190
 
191
191
  if (!fontFamily) {
192
192
  fontFamily = ThreeMeshUI.FontLibrary.addFontFamily(this.familyName);
193
- const variant = fontFamily.addVariant("normal", "normal", "./include/needle/arial-msdf.json", "./include/needle/arial.png") as any as ThreeMeshUI.FontVariant;
193
+ const variant = fontFamily.addVariant("normal", "normal", "https://uploads.needle.tools/include/font-msdf.json", "https://uploads.needle.tools/include/font.png") as any as ThreeMeshUI.FontVariant;
194
194
  variant?.addEventListener('ready', () => {
195
195
  ThreeMeshUI.update();
196
196
  });
src/engine/engine_loaders.ts CHANGED
@@ -4,14 +4,22 @@
4
4
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
5
5
  import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
6
6
 
7
+ import { isDevEnvironment } from './debug/index.js';
7
8
  import { Context } from "./engine_setup.js"
8
9
  import { getParam } from "./engine_utils.js";
9
10
 
10
11
  const debug = getParam("debugdecoders");
11
12
 
12
- const DEFAULT_DRACO_DECODER_LOCATION = 'https://www.gstatic.com/draco/versioned/decoders/1.4.1/';
13
- const DEFAULT_KTX2_TRANSCODER_LOCATION = 'https://www.gstatic.com/basis-universal/versioned/2021-04-15-ba1c3e4/';
13
+ let DEFAULT_DRACO_DECODER_LOCATION = 'https://www.gstatic.com/draco/versioned/decoders/1.4.1/';
14
+ let DEFAULT_KTX2_TRANSCODER_LOCATION = 'https://www.gstatic.com/basis-universal/versioned/2021-04-15-ba1c3e4/';
14
15
 
16
+ fetch(DEFAULT_DRACO_DECODER_LOCATION + "draco_decoder.js", { method: "head" })
17
+ .catch(_ => {
18
+ if (isDevEnvironment()) console.warn("Failed to load draco decoder from \"" + DEFAULT_DRACO_DECODER_LOCATION + "\".\nFalling back to local version at \"./include/draco\"");
19
+ DEFAULT_DRACO_DECODER_LOCATION = "./include/draco/";
20
+ DEFAULT_KTX2_TRANSCODER_LOCATION = "./include/ktx2/";
21
+ });
22
+
15
23
  let dracoLoader: DRACOLoader;
16
24
  let meshoptDecoder: typeof MeshoptDecoder;
17
25
  let ktx2Loader: KTX2Loader;
src/engine/engine_networking.ts CHANGED
@@ -267,6 +267,9 @@
267
267
  return this._peer;
268
268
  }
269
269
 
270
+ /**
271
+ * Returns the state of a given guid.
272
+ */
270
273
  public tryGetState(guid: string): IModel | null {
271
274
  if (guid === "invalid") return null;
272
275
  return this._state[guid];
@@ -290,8 +293,21 @@
290
293
  public get currentRoomName(): string | null { return this._currentRoomName; }
291
294
  /** True when connected to a room via a regular url, otherwise (when using a view only url) false indicating that the user should not be able to modify the scene */
292
295
  public get allowEditing(): boolean { return this._currentRoomAllowEditing; }
293
- // use this to join a room in view mode (see SyncedRoom)
296
+ /**
297
+ * The view id of the room the user is currently connected to.
298
+ */
294
299
  public get currentRoomViewId(): string | null { return this._currentRoomViewId; }
300
+ /**
301
+ * Returns a url that can be shared with others to view the current room in view only mode.
302
+ * This is useful for sharing a room with others without allowing them to modify the scene.
303
+ * Use `connection.allowEditing` to check if the current room is in view only mode.
304
+ */
305
+ public getViewOnlyUrl() {
306
+ if (this.currentRoomViewId === null) return null;
307
+ const url = new URL(window.location.href);
308
+ url.searchParams.set("view", this.currentRoomViewId);
309
+ return url.href;
310
+ }
295
311
 
296
312
  /** True if connected to a networked room. Use the joinRoom function or a `SyncedRoom` component */
297
313
  public get isInRoom(): boolean {
@@ -303,6 +319,9 @@
303
319
  return this._currentDelay;
304
320
  }
305
321
 
322
+ /**
323
+ * The current server url that the networking backend is connected to (e.g. the url of the websocket server)
324
+ */
306
325
  public get currentServerUrl(): string | null {
307
326
  // @ts-ignore (in ts-websocket 2.x this property is exposed)
308
327
  return this._ws?.url ?? null;
src/engine-components/LookAtConstraint.ts CHANGED
@@ -3,10 +3,24 @@
3
3
  import { serializable } from "../engine/engine_serialization_decorator.js";
4
4
  import { Behaviour } from "./Component.js";
5
5
 
6
+ /**
7
+ * A LookAtConstraint is used by OrbitControls to make the camera look at a target.
8
+ */
6
9
  export class LookAtConstraint extends Behaviour {
7
10
 
11
+ /**
12
+ * When true the constraint is active.
13
+ */
14
+ @serializable()
8
15
  constraintActive: boolean = true;
16
+ /**
17
+ * When true the look at is locked to the position of the assigned sources.
18
+ */
19
+ @serializable()
9
20
  locked: boolean = false;
21
+ /**
22
+ * The sources to look at.
23
+ */
10
24
  @serializable(Object3D)
11
25
  sources: Object3D[] = [];
12
26
  }
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -434,27 +434,21 @@
434
434
  const ext = meshInfo?.extensions[EXTENSION_NAME] as NEEDLE_progressive_mesh_model;
435
435
  if (ext && ext.lods) {
436
436
  for (const key of this.parser.associations.keys()) {
437
- if (key instanceof Mesh || key instanceof Group) {
438
- const val = this.parser.associations.get(key) as { meshes: number };
437
+ if (key instanceof Mesh) {
438
+ const val = this.parser.associations.get(key) as { meshes: number, primitives: number };
439
439
  if (val.meshes === index) {
440
440
  const obj = key;
441
441
  if (debug) console.log("> Progressive: register mesh", index, obj.name, ext, obj.uuid, obj);
442
442
  const LODKEY = obj.uuid;
443
443
  const LODLEVEL = ext.lods.length;
444
444
  if (obj instanceof Mesh) {
445
- applyMeshLOD(LODKEY, obj, LODLEVEL, undefined, ext);
446
- NEEDLE_progressive.lowresCache.set(LODKEY, obj.geometry);
447
- }
448
- else {
449
- const geometries = new Array<BufferGeometry>();
450
- for (let i = 0; i < obj.children.length; i++) {
451
- const child = obj.children[i];
452
- if (child instanceof Mesh) {
453
- geometries.push(child.geometry as BufferGeometry);
454
- applyMeshLOD(LODKEY, child, LODLEVEL, i, ext);
455
- }
445
+ applyMeshLOD(LODKEY, obj, LODLEVEL, val.primitives, ext);
446
+ let existing = NEEDLE_progressive.lowresCache.get(LODKEY) as unknown as BufferGeometry[] | undefined;
447
+ if (existing) {
448
+ existing.push(obj.geometry as BufferGeometry);
456
449
  }
457
- NEEDLE_progressive.lowresCache.set(LODKEY, geometries);
450
+ else existing = [obj.geometry as BufferGeometry];
451
+ NEEDLE_progressive.lowresCache.set(LODKEY, existing);
458
452
  }
459
453
  }
460
454
  }
@@ -472,7 +466,7 @@
472
466
  /** cache of already loaded mesh lods */
473
467
  private static readonly previouslyLoaded: Map<string, Promise<null | Texture | BufferGeometry | BufferGeometry[]>> = new Map();
474
468
  /** this contains the geometry/textures that were originally loaded */
475
- private static readonly lowresCache: Map<string, Texture | BufferGeometry | BufferGeometry[]> = new Map();
469
+ private static readonly lowresCache: Map<string, Texture | BufferGeometry[]> = new Map();
476
470
 
477
471
  private static async getOrLoadLOD<T extends Texture | BufferGeometry>(
478
472
  context: Context, source: SourceIdentifier | undefined, current: T & ObjectThatMightHaveLODs, level: number
src/engine-components/Renderer.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { AxesHelper, Box3, BufferGeometry, Color, InstancedMesh, Material, Matrix4, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, RawShaderMaterial, SkinnedMesh, Sphere, Texture, Vector3, Vector4 } from "three";
1
+ import { AxesHelper, Box3, BufferGeometry, Color, InstancedMesh, Material, Matrix4, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, RawShaderMaterial, SkinnedMesh, Sphere, Texture, Vector2, Vector3, Vector4 } from "three";
2
2
 
3
3
  import { showBalloonWarning } from "../engine/debug/index.js";
4
4
  import { getComponent, getOrAddComponent } from "../engine/engine_components.js";
5
+ import { ContextEvent, NeedleEngine } from "../engine/engine_context_registry.js";
5
6
  import { Gizmos, LabelHandle } from "../engine/engine_gizmos.js";
6
7
  import { $instancingAutoUpdateBounds, $instancingRenderer, InstancingUtil, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
8
+ import { onStart } from "../engine/engine_lifecycle_api.js";
7
9
  import { isLocalNetwork } from "../engine/engine_networking_utils.js";
8
10
  import { getRaycastMesh } from "../engine/engine_physics.js";
9
11
  import { serializable } from "../engine/engine_serialization_decorator.js";
@@ -20,15 +22,33 @@
20
22
  // import { RendererCustomShader } from "./RendererCustomShader.js";
21
23
  import { RendererLightmap } from "./RendererLightmap.js";
22
24
 
25
+
23
26
  // for staying compatible with old code
24
27
  export { InstancingUtil } from "../engine/engine_instancing.js";
25
28
 
26
29
  const debugRenderer = getParam("debugrenderer");
27
30
  const debugskinnedmesh = getParam("debugskinnedmesh");
28
31
  const suppressInstancing = getParam("noinstancing");
29
- const debugProgressiveLoading = getParam("debugprogressive");
32
+ let debugProgressiveLoading = getParam("debugprogressive");
30
33
  const suppressProgressiveLoading = getParam("noprogressive");
31
34
 
35
+ if (debugProgressiveLoading && document) {
36
+ onStart(ctx => {
37
+ const button = document.createElement("button");
38
+ button.innerText = "Prog Debug Mode: " + debugProgressiveLoading;
39
+ button.onclick = () => {
40
+ if (debugProgressiveLoading == "density")
41
+ debugProgressiveLoading = false;
42
+ else if (debugProgressiveLoading == false)
43
+ debugProgressiveLoading = true;
44
+ else if (debugProgressiveLoading)
45
+ debugProgressiveLoading = "density";
46
+ button.innerText = "Prog Debug Mode: " + debugProgressiveLoading;
47
+ };
48
+ ctx.menu.appendChild(button);
49
+ });
50
+ }
51
+
32
52
  const showWireframe = getParam("wireframe");
33
53
 
34
54
  export enum ReflectionProbeUsage {
@@ -831,7 +851,8 @@
831
851
 
832
852
  private _lastLodLevel = -1;
833
853
  private _lastScreenCoverage = 0;
834
- private _lastScreenspaceVolume = 0;
854
+ private _lastScreenspaceVolume = new Vector3();
855
+ private _lastCentrality = 0;
835
856
  private _nextLodTestTime = 0;
836
857
  private _randomLodLevelCheckFrameOffset = Math.floor(Math.random() * 100);
837
858
  private readonly _sphere = new Sphere();
@@ -845,7 +866,7 @@
845
866
 
846
867
  const interval = 3;
847
868
 
848
- if (!debugProgressiveLoading) {
869
+ if (!(debugProgressiveLoading == "density")) {
849
870
  if (!force && (this.context.time.frame + this._randomLodLevelCheckFrameOffset) % interval != 0) {
850
871
  return this._lastLodLevel;
851
872
  }
@@ -855,14 +876,29 @@
855
876
  }
856
877
  }
857
878
 
879
+ /** rough measure of "triangles on quadratic screen" – we're switching LODs based on this metric. */
880
+ const desiredDensity = 100_000;
881
+ /** highest LOD level we'd ever expect to be generated */
858
882
  const maxLevel = 10;
883
+
884
+ let currentAllowedDensity = desiredDensity;
859
885
  let level = maxLevel + 1;
860
886
 
861
887
  if (this.context.mainCamera) {
862
888
 
863
889
  const isLowPerformanceDevice = isMobileDevice();
864
- const lod_0_threshold = isLowPerformanceDevice ? .8 : .6;
890
+ /** We probably want to make these numbers configurable. */
891
+ currentAllowedDensity = desiredDensity * (isLowPerformanceDevice ? 0.7 : 1.0);
865
892
 
893
+ // Experiment: quick & dirty performance-adaptive LODs
894
+ /*
895
+ if (this.context.time.smoothedFps < 59) {
896
+ currentAllowedDensity *= 0.5;
897
+ }
898
+ else if (this.context.time.smoothedFps >= 59) {
899
+ currentAllowedDensity *= 1.25;
900
+ }
901
+ */
866
902
 
867
903
  // TODO: we should save the LOD level in the shared mesh and not just calculate one level per renderer
868
904
  for (const mesh of this.sharedMeshes) {
@@ -873,16 +909,9 @@
873
909
  return this._lastLodLevel = mesh["DEBUG:LOD"];
874
910
  }
875
911
 
876
- let meshDensity = 0;
877
- // TODO: the mesh info contains also the density for all available LOD level so we can use this for selecting which level to show
912
+ // The mesh info contains also the density for all available LOD level so we can use this for selecting which level to show
878
913
  const lodsInfo = NEEDLE_progressive.getMeshLODInformation(mesh.geometry);
879
- // TODO: the substraction here is not clear - it should be some sort of mesh density value
880
- if (lodsInfo?.lods && lodsInfo?.lods.length > 0)
881
- meshDensity = (Math.log2(lodsInfo.lods[0].density || 0) / 2) - 8;
882
- else if(debugProgressiveLoading) console.warn("No LOD information found...", mesh.name);
883
- if (debugProgressiveLoading) {
884
- // ^console.log("Looking at LOD levels", lodsInfo)
885
- }
914
+ const lods = lodsInfo?.lods;
886
915
 
887
916
  // TODO: we can skip all this if we dont have any LOD information - we can ask the progressive extension for that
888
917
  const frustum = this.context.mainCameraComponent?.getFrustum();
@@ -915,17 +944,73 @@
915
944
  // calculate size on screen
916
945
  this._box.copy(box);
917
946
  this._box.applyMatrix4(mesh.matrixWorld);
947
+ const cam = this.context.mainCamera;
948
+
949
+ // Converting into projection space has the disadvantage that objects further to the side
950
+ // will have a much larger coverage, especially with high-field-of-view situations like in VR.
951
+ // Alternatively, we could attempt to calculate angular coverage (some kind of polar coordinates maybe?)
952
+ // or introduce a correction factor based on "expected distortion" of the object.
953
+ // High distortions would lead to lower LOD levels.
954
+ // "Centrality" of the calculated screen-space bounding box could be a factor here –
955
+ // what's the distance of the bounding box to the center of the screen?
918
956
  const mat = this.context.mainCameraComponent!.getProjectionScreenMatrix(Renderer.tempMatrix);
919
957
  this._box.applyMatrix4(mat);
920
- // Gizmos.DrawWireBox(this._box.getCenter(getTempVector()), this._box.getSize(getTempVector()), 0x00ff00, .02);
958
+
959
+ // TODO might need to be adjusted for cameras that are rendered during an XR session but are
960
+ // actually not XR cameras (e.g. a render texture)
961
+ if (this.context.isInXR && cam.fov > 70) {
962
+ // calculate centrality of the bounding box - how close is it to the screen center
963
+ const min = this._box.min;
964
+ const max = this._box.max;
965
+
966
+ let minX = min.x;
967
+ let minY = min.y;
968
+ let maxX = max.x;
969
+ let maxY = max.y;
970
+
971
+ // enlarge
972
+ const enlargementFactor = 2.0;
973
+ const centerBoost = 1.5;
974
+ const centerX = (min.x + max.x) * 0.5;
975
+ const centerY = (min.y + max.y) * 0.5;
976
+ minX = (minX - centerX) * enlargementFactor + centerX;
977
+ minY = (minY - centerY) * enlargementFactor + centerY;
978
+ maxX = (maxX - centerX) * enlargementFactor + centerX;
979
+ maxY = (maxY - centerY) * enlargementFactor + centerY;
980
+
981
+ const xCentrality = minX < 0 && maxX > 0 ? 0 : Math.min(Math.abs(min.x), Math.abs(max.x));
982
+ const yCentrality = minY < 0 && maxY > 0 ? 0 : Math.min(Math.abs(min.y), Math.abs(max.y));
983
+ const centrality = Math.max(xCentrality, yCentrality);
984
+
985
+ // heuristically determined to lower quality for objects at the edges of vision
986
+ this._lastCentrality = (centerBoost - centrality) * (centerBoost - centrality) * (centerBoost - centrality);
987
+ }
988
+ else {
989
+ this._lastCentrality = 1;
990
+ }
921
991
  const boxSize = this._box.getSize(getTempVector());
922
- // const verticalFov = this.context.mainCamera.fov;
923
- let screenSize = boxSize.y / 2;// / (2 * Math.atan(Math.PI * verticalFov / 360));
924
- // if (mesh.name.startsWith("Statue_")) console.log(mesh.name, "ScreenSize", screenSize, "Density", meshDensity, mesh.geometry.index!.count / 3);
925
- screenSize /= Math.pow(2, meshDensity - .5);
926
- this._lastScreenCoverage = Math.max(boxSize.x, boxSize.y, boxSize.z) / 2;
927
- this._lastScreenspaceVolume = boxSize.x * boxSize.y * boxSize.z / 8;
992
+ boxSize.multiplyScalar(0.5); // goes from -1..1, we want -0.5..0.5 for coverage in percent
993
+ if (screen.availHeight > 0)
994
+ boxSize.multiplyScalar(this.context.domHeight / screen.availHeight); // correct for size of context on screen
995
+ boxSize.x *= cam.aspect;
928
996
 
997
+ const matView = cam.matrixWorldInverse;
998
+ const box2 = new Box3();
999
+ box2.copy(box);
1000
+ box2.applyMatrix4(mesh.matrixWorld);
1001
+ box2.applyMatrix4(matView);
1002
+ const boxSize2 = box2.getSize(getTempVector());
1003
+
1004
+ // approximate depth coverage in relation to screenspace size
1005
+ const max2 = Math.max(boxSize2.x, boxSize2.y);
1006
+ const max1 = Math.max(boxSize.x, boxSize.y);
1007
+ if (max1 != 0 && max2 != 0)
1008
+ boxSize.z = boxSize2.z / Math.max(boxSize2.x, boxSize2.y) * Math.max(boxSize.x, boxSize.y);
1009
+
1010
+ this._lastScreenCoverage = Math.max(boxSize.x, boxSize.y, boxSize.z);
1011
+ this._lastScreenspaceVolume.copy(boxSize);
1012
+ this._lastScreenCoverage *= this._lastCentrality;
1013
+
929
1014
  // draw screen size box
930
1015
  if (debugProgressiveLoading) {
931
1016
  mat.invert();
@@ -941,8 +1026,9 @@
941
1026
  corner2.y = corner0.y;
942
1027
  const corner3 = getTempVector();
943
1028
  corner3.copy(this._box.max);
1029
+ // draw outlines at the center of the box
944
1030
  const z = (corner0.z + corner3.z) * 0.5;
945
-
1031
+ // all outlines should have the same depth in screen space
946
1032
  corner0.z = corner1.z = corner2.z = corner3.z = z;
947
1033
 
948
1034
  corner0.applyMatrix4(mat);
@@ -956,17 +1042,18 @@
956
1042
  Gizmos.DrawLine(corner2, corner3, 0x0000ff);
957
1043
  }
958
1044
 
959
- // screenSize *= .2;
960
-
961
1045
  let expectedLevel = 999;
962
- let threshold = lod_0_threshold;
963
- for (let l = 0; l < maxLevel; l++) {
964
- if (screenSize > threshold) {
965
- expectedLevel = l;
966
- break;
1046
+ const framerate = this.context.time.smoothedFps;
1047
+ if (lods && this._lastScreenCoverage > 0) {
1048
+ for (let l = 0; l < lods.length; l++) {
1049
+ const densityForThisLevel = lods[l].density;
1050
+ if (densityForThisLevel / this._lastScreenCoverage < currentAllowedDensity) {
1051
+ expectedLevel = l;
1052
+ break;
1053
+ }
967
1054
  }
968
- threshold /= 2;
969
1055
  }
1056
+
970
1057
  // expectedLevel -= meshDensity - 5;
971
1058
  // expectedLevel += meshDensity;
972
1059
  const isLowerLod = expectedLevel < level;
@@ -985,8 +1072,8 @@
985
1072
  if (debugProgressiveLoading == "verbose") console.warn(`LOD Level changed from ${this._lastLodLevel} to ${level} for ${this.name}`);
986
1073
  this.drawGizmoLodLevel(true);
987
1074
  }
1075
+ }
988
1076
 
989
- }
990
1077
  this._lastLodLevel = level;
991
1078
  return level;
992
1079
  }
@@ -994,8 +1081,12 @@
994
1081
  private drawGizmoLodLevel(changed: boolean) {
995
1082
  // Will be (maxLod + 1) (11) if no lod level is found
996
1083
  const _level = this._lastLodLevel;
997
- const camForward = (this.context.mainCamera as any as IGameObject).worldForward;
998
- const camWorld = (this.context.mainCamera as any as IGameObject).worldPosition;
1084
+
1085
+ const cam = this.context.mainCamera as any as PerspectiveCamera;
1086
+ const camGO = cam as any as GameObject;
1087
+ const camForward = camGO.worldForward;
1088
+ const camWorld = camGO.worldPosition;
1089
+
999
1090
  for (const mesh of this.sharedMeshes) {
1000
1091
  if (!mesh) continue;
1001
1092
  if (mesh.geometry.boundingSphere) {
@@ -1018,9 +1109,11 @@
1018
1109
  const lods = NEEDLE_progressive.getMeshLODInformation(mesh.geometry)?.lods;
1019
1110
  const level = lods ? Math.min(lods?.length - 1, _level) : 0;
1020
1111
  let allLods = "";
1021
- if (lods) {
1112
+ if (lods && this._lastScreenCoverage > 0) {
1022
1113
  for (let i = 0; i < lods.length; i++) {
1023
- allLods += lods[i].density.toFixed(0) + ",";
1114
+ const d = lods[i].density;
1115
+ const last = i == lods.length - 1;
1116
+ allLods += d.toFixed(0) + ">" + (d / this._lastScreenCoverage).toFixed(0) + (last ? "" : ",");
1024
1117
  }
1025
1118
  }
1026
1119
  const density = lods ? lods[level]?.density : -1;
@@ -1035,32 +1128,39 @@
1035
1128
  // Area is squared, so both maxBoxSize and wsMedian are squared here
1036
1129
  // Here, we're basically reverting the calculations that have happened in the pipeline for debugging.
1037
1130
  const surfaceArea = 1 / density * triangleCount * (maxBoxSize * maxBoxSize) * (wsMedian * wsMedian);
1038
- const idealDensity = this._lastScreenCoverage;
1039
1131
  let text = "LOD " + level;
1040
- if(debugProgressiveLoading == "density"){
1041
- text += "\n" +
1042
- triangleCount + " tris\n" +
1043
- (density / this._lastScreenCoverage * 0.01).toFixed(0) + " dens\n" +
1044
- (this._lastScreenCoverage * 100).toFixed(1) + "% cov" + "\n" +
1045
- (this._lastScreenspaceVolume * 100).toFixed(2) + " m3" + "\n" +
1046
- (surfaceArea).toFixed(2) + " m2" + "\n" +
1047
- (ws.x).toFixed(2) + "x" + " " + maxBoxSize.toFixed(2) + "b" + "\n" +
1048
- allLods + "\n" +
1049
- "----" + "\n" +
1050
- "1000" + " ideal dens";
1132
+ if(debugProgressiveLoading == "density") {
1133
+ text +=
1134
+ "\n" + triangleCount + " tris" +
1135
+ // This is key basically how we're switching
1136
+ "\n" + (density / this._lastScreenCoverage).toFixed(0) + " dens" +
1137
+ "\n" + (this._lastScreenCoverage * 100).toFixed(1) + "% cov" +
1138
+ // "\n" + (this._lastScreenspaceVolume.x.toFixed(2) + "x" + this._lastScreenspaceVolume.y.toFixed(2) + "x" + this._lastScreenspaceVolume.z.toFixed(2)) + " vol" +
1139
+ // + "\n" + (surfaceArea).toFixed(2) + " m2" +
1140
+ "\n" + (this._lastCentrality * 100).toFixed(1) + "% centr" +
1141
+ "\n" + (this._box.min.x.toFixed(2) + "-" + this._box.max.x.toFixed(2) + "x" + this._box.min.y.toFixed(2) + "-" + this._box.max.y.toFixed(2)) + " scr" +
1142
+ // "\n" + (ws.x).toFixed(2) + "x" + " " + maxBoxSize.toFixed(2) + "b" + "\n" +
1143
+ // allLods + "\n" +
1144
+ //"----" + "\n" +
1145
+ // "1000" + " ideal dens"
1146
+ "";
1051
1147
  }
1052
1148
 
1053
1149
  // if (helper) {
1054
1150
  // helper?.setText(text);
1055
1151
  // continue;
1056
1152
  // }
1057
- const pos = getTempVector(camForward).multiplyScalar(radius * .7).add(boundsCenter);
1153
+ const fwd = getTempVector(camForward);
1154
+ // for debugging very close LDOs, we need to flip the radius...
1155
+ const pos = fwd.multiplyScalar(radius * .7).add(boundsCenter);
1058
1156
  const distance = pos.distanceTo(camWorld);
1059
- const vertexCount = mesh.geometry.index!.count / 3;
1157
+ // const vertexCount = mesh.geometry.index!.count / 3;
1060
1158
  // const vertexCountFactor = Math.min(1, vertexCount / 1000);
1061
1159
  const col = colors[Math.min(colors.length - 1, level)] + "88";
1062
1160
  // const size = Math.min(10, radius);
1063
- Gizmos.DrawLabel(pos, text, distance * .01, undefined, 0xffffff, col);
1161
+ const windowScale = this.context.domHeight > 0 ? screen.height / this.context.domHeight : 1;
1162
+ const fieldOfViewScale = Math.tan(cam.fov * Math.PI / 180 / 2);
1163
+ Gizmos.DrawLabel(pos, text, distance * .01 * windowScale * fieldOfViewScale, undefined, 0xffffff, col);
1064
1164
  // mesh["LOD_level_label"] = helper;
1065
1165
  }
1066
1166
 
src/engine-components/ScreenCapture.ts CHANGED
@@ -46,15 +46,18 @@
46
46
  allowStartOnClick: boolean = true;
47
47
 
48
48
  onPointerEnter() {
49
+ if (this.context.connection.allowEditing == false) return;
49
50
  if (!this.allowStartOnClick) return;
50
51
  this.context.input.setCursorPointer();
51
52
  }
52
53
  onPointerExit() {
54
+ if (this.context.connection.allowEditing == false) return;
53
55
  if (!this.allowStartOnClick) return;
54
56
  this.context.input.setCursorNormal();
55
57
  }
56
58
 
57
59
  onPointerClick(evt: PointerEventData) {
60
+ if (this.context.connection.allowEditing == false) return;
58
61
  if (!this.allowStartOnClick) return;
59
62
  if (evt && evt.pointerId !== 0) return;
60
63
  if (this.isReceiving && this.videoPlayer?.isPlaying) {