Needle Engine

Changes between version 3.37.0-alpha and 3.37.1-alpha
Files changed (6) hide show
  1. src/engine/engine_context.ts +0 -7
  2. src/engine-components/EventList.ts +7 -2
  3. src/engine/assets/index.ts +1 -1
  4. src/engine/extensions/NEEDLE_progressive.ts +69 -59
  5. src/engine-components/Renderer.ts +94 -13
  6. src/engine-components/webxr/WebXR.ts +3 -1
src/engine/engine_context.ts CHANGED
@@ -976,13 +976,6 @@
976
976
  break;
977
977
  }
978
978
  const file = files[i];
979
- if (!file.includes(".glb") && !file.includes(".gltf")) {
980
- // TODO this may not be true if the URL just forwards to a proper GLB file resource
981
- const warning = `Needle Engine: found suspicious src "${file}"`;
982
- console.warn(warning);
983
- if (isLocalNetwork()) showBalloonWarning(warning);
984
- }
985
-
986
979
  args?.onLoadingStart?.call(this, i, file);
987
980
  if (debug) console.log("Context Load " + file);
988
981
  const res = await loader.loadSync(this, file, file, loadingHash, prog => {
src/engine-components/EventList.ts CHANGED
@@ -66,7 +66,8 @@
66
66
  private _isInvoking: boolean = false;
67
67
 
68
68
  // TODO: can we make functions serializable?
69
- private methods: CallInfo[] = [];
69
+ private readonly methods: CallInfo[] = [];
70
+ private readonly _methodsCopy: CallInfo[] = [];
70
71
 
71
72
  constructor(evts?: CallInfo[]) {
72
73
  this.methods = evts ?? [];
@@ -84,8 +85,12 @@
84
85
  this._isInvoking = true;
85
86
  try {
86
87
 
88
+ // make a copy of the methods array to avoid issues when removing listeners during invocation
89
+ this._methodsCopy.length = 0;
90
+ this._methodsCopy.push(...this.methods);
91
+
87
92
  // first invoke all the methods that were subscribed to this eventlist
88
- for (const m of this.methods) {
93
+ for (const m of this._methodsCopy) {
89
94
  m.invoke(...args);
90
95
  }
91
96
 
src/engine/assets/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Box3, DoubleSide, Group, Mesh, MeshBasicMaterial, ShapeGeometry, Vector3 } from "three";
2
- import { SVGLoader } from "three/examples/jsm/loaders/SVGLoader";
2
+ import { SVGLoader } from "three/examples/jsm/loaders/SVGLoader.js";
3
3
 
4
4
 
5
5
  const logoSvgString = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 160 187.74"><defs><linearGradient id="a" x1="89.64" y1="184.81" x2="90.48" y2="21.85" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#62d399"/><stop offset=".51" stop-color="#acd842"/><stop offset=".9" stop-color="#d7db0a"/></linearGradient><linearGradient id="b" x1="69.68" y1="178.9" x2="68.08" y2="16.77" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0ba398"/><stop offset=".5" stop-color="#4ca352"/><stop offset="1" stop-color="#76a30a"/></linearGradient><linearGradient id="c" x1="36.6" y1="152.17" x2="34.7" y2="84.19" gradientUnits="userSpaceOnUse"><stop offset=".19" stop-color="#36a382"/><stop offset=".54" stop-color="#49a459"/><stop offset="1" stop-color="#76a30b"/></linearGradient><linearGradient id="d" x1="15.82" y1="153.24" x2="18" y2="90.86" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#267880"/><stop offset=".51" stop-color="#457a5c"/><stop offset="1" stop-color="#717516"/></linearGradient><linearGradient id="e" x1="135.08" y1="135.43" x2="148.93" y2="63.47" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0d939"/><stop offset="1" stop-color="#eadb04"/></linearGradient><linearGradient id="f" x1="-4163.25" y1="2285.12" x2="-4160.81" y2="2215.34" gradientTransform="rotate(20 4088.49 13316.712)" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#74af52"/><stop offset=".48" stop-color="#99be32"/><stop offset="1" stop-color="#c0c40a"/></linearGradient><symbol id="g" viewBox="0 0 160 187.74"><path style="fill:url(#a)" d="M79.32 36.98v150.76L95 174.54l6.59-156.31-22.27 18.75z"/><path style="fill:url(#b)" d="M79.32 36.98 57.05 18.23l6.59 156.31 15.68 13.2V36.98z"/><path style="fill:url(#c)" d="m25.19 104.83 8.63 49.04 12.5-14.95-2.46-56.42-18.67 22.33z"/><path style="fill:url(#d)" d="M25.19 104.83 0 90.24l16.97 53.86 16.85 9.77-8.63-49.04z"/><path style="fill:#9c3" d="M43.86 82.5 18.69 67.98 0 90.24l25.18 14.59L43.86 82.5z"/><path style="fill:url(#e)" d="m134.82 78.69-9.97 56.5 15.58-9.04L160 64.1l-25.18 14.59z"/><path style="fill:url(#f)" d="m134.82 78.69-18.68-22.33-2.86 65 11.57 13.83 9.97-56.5z"/><path style="fill:#ffe113" d="m160 64.1-18.69-22.26-25.17 14.52 18.67 22.33L160 64.1z"/><path style="fill:#f3e600" d="M101.59 18.23 79.32 0 57.05 18.23l22.27 18.75 22.27-18.75z"/></symbol></defs><use width="160" height="187.74" xlink:href="#g"/></svg>`;
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -240,7 +240,7 @@
240
240
  }
241
241
  this.onProgressiveLoadEnd(info);
242
242
  return geo;
243
-
243
+
244
244
  }).catch(err => {
245
245
  this.onProgressiveLoadEnd(info);
246
246
  console.error("Error loading mesh LOD", mesh, err);
@@ -353,8 +353,8 @@
353
353
  this.onProgressiveLoadEnd(info);
354
354
  return tex;
355
355
  }
356
- else if (debug) {
357
- console.warn("No LOD found for", level, current, tex);
356
+ else if (debug == "verbose") {
357
+ console.warn("No LOD found for", current, level);
358
358
  }
359
359
 
360
360
  this.onProgressiveLoadEnd(info);
@@ -388,59 +388,30 @@
388
388
  if (textureInfo?.extensions) {
389
389
  const ext: NEEDLE_progressive_texture_model = textureInfo?.extensions[EXTENSION_NAME];
390
390
  if (ext) {
391
- const prom = this.parser.getDependency("texture", index);
392
- prom.then(tex => {
393
- if (debug)
394
- console.log("> Progressive: register texture", index, tex.name, tex.uuid, tex, ext);
395
- // Put the extension info into the source (seems like tiled textures are cloned and the userdata etc is not properly copied BUT the source of course is not cloned)
396
- // see https://github.com/needle-tools/needle-engine-support/issues/133
397
- if (tex.source)
398
- tex.source[$progressiveTextureExtension] = ext;
399
- const LODKEY = tex.uuid;
400
- if (!tex.userData) tex.userData = {};
401
- NEEDLE_progressive.assignLODInformation(tex, LODKEY, 0, 0, undefined);
402
- NEEDLE_progressive.lodInfos.set(LODKEY, ext);
403
- NEEDLE_progressive.lowresCache.set(LODKEY, tex);
404
- return tex;
405
- }).catch(err => {
406
- console.error(`Error loading progressive texture ${index}\n`, err);
407
- });
408
- }
409
- }
410
- });
411
-
412
- this.parser.json.meshes?.forEach((meshInfo, index) => {
413
- if (meshInfo?.extensions) {
414
- const ext = meshInfo?.extensions[EXTENSION_NAME] as NEEDLE_progressive_mesh_model;
415
- if (ext && ext.lods) {
416
- const prom = this.parser.getDependency("mesh", index);
417
- prom.then((obj: Mesh | Group) => {
418
- if (debug) console.log("> Progressive: register mesh", index, obj.name, ext, obj.uuid, obj);
419
- const LODKEY = obj.uuid;
420
- const LODLEVEL = ext.lods.length;
421
- if (obj instanceof Mesh) {
422
- applyMeshLOD(LODKEY, obj, LODLEVEL, undefined, ext);
423
- NEEDLE_progressive.lowresCache.set(LODKEY, obj.geometry);
424
- }
425
- else {
426
- const geometries = new Array<BufferGeometry>();
427
- for (let i = 0; i < obj.children.length; i++) {
428
- const child = obj.children[i];
429
- if (child instanceof Mesh) {
430
- geometries.push(child.geometry as BufferGeometry);
431
- applyMeshLOD(LODKEY, child, LODLEVEL, i, ext);
432
- }
391
+ for (const key of this.parser.associations.keys()) {
392
+ if (key instanceof Texture) {
393
+ const val = this.parser.associations.get(key) as { textures: number };
394
+ if (val.textures === index) {
395
+ const tex = key;
396
+ if (debug)
397
+ console.log("> Progressive: register texture", index, tex.name, tex.uuid, tex, ext);
398
+ // Put the extension info into the source (seems like tiled textures are cloned and the userdata etc is not properly copied BUT the source of course is not cloned)
399
+ // see https://github.com/needle-tools/needle-engine-support/issues/133
400
+ if (tex.source)
401
+ tex.source[$progressiveTextureExtension] = ext;
402
+ const LODKEY = tex.uuid;
403
+ NEEDLE_progressive.assignLODInformation(tex, LODKEY, 0, 0, undefined);
404
+ NEEDLE_progressive.lodInfos.set(LODKEY, ext);
405
+ NEEDLE_progressive.lowresCache.set(LODKEY, tex);
433
406
  }
434
- NEEDLE_progressive.lowresCache.set(LODKEY, geometries);
435
407
  }
436
- return obj;
437
- }).catch(err => {
438
- console.error(`Error loading progressive mesh ${index}\n`, err);
439
- });
408
+
409
+ }
440
410
  }
441
411
  }
442
412
  });
443
413
 
414
+
444
415
  const applyMeshLOD = (key: string, mesh: Mesh, level: number, index: number | undefined, ext: NEEDLE_progressive_mesh_model) => {
445
416
  const geometry = mesh.geometry as BufferGeometry;
446
417
  geometry["needle:raycast-mesh"] = true;
@@ -458,7 +429,41 @@
458
429
  NEEDLE_progressive.assignLODInformation(geometry, key, level, index, ext.density);
459
430
  NEEDLE_progressive.lodInfos.set(key, ext);
460
431
  };
432
+ this.parser.json.meshes?.forEach((meshInfo, index: number) => {
433
+ if (meshInfo?.extensions) {
434
+ const ext = meshInfo?.extensions[EXTENSION_NAME] as NEEDLE_progressive_mesh_model;
435
+ if (ext && ext.lods) {
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 };
439
+ if (val.meshes === index) {
440
+ const obj = key;
441
+ if (debug) console.log("> Progressive: register mesh", index, obj.name, ext, obj.uuid, obj);
442
+ const LODKEY = obj.uuid;
443
+ const LODLEVEL = ext.lods.length;
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
+ }
456
+ }
457
+ NEEDLE_progressive.lowresCache.set(LODKEY, geometries);
458
+ }
459
+ }
460
+ }
461
+ }
461
462
 
463
+ }
464
+ }
465
+ });
466
+
462
467
  return null;
463
468
  }
464
469
 
@@ -709,19 +714,24 @@
709
714
  return res?.userData?.LODS || null;
710
715
  }
711
716
 
717
+ private static readonly _copiedTextures: WeakMap<Texture, Texture> = new Map();
718
+
712
719
  private static copySettings(source: Texture, target: Texture): Texture {
720
+ // don't copy again if the texture was processed before
721
+ const existingClone = this._copiedTextures.get(source);
722
+ if (existingClone) {
723
+ return existingClone;
724
+ }
713
725
  // We need to clone e.g. when the same texture is used multiple times (but with e.g. different wrap settings)
714
726
  // This is relatively cheap since it only stores settings
715
- const res = source.clone();
716
- const src = target.source;
717
- const mips = target.mipmaps;
718
- res.copy(source);
719
- res.source = src;
720
- res.mipmaps = mips;
727
+ // This should only happen once ever for every texture
728
+ target = target.clone();
729
+ this._copiedTextures.set(source, target);
721
730
  // we re-use the offset and repeat settings because it might be animated
722
- res.offset = source.offset;
723
- res.repeat = source.repeat;
724
- return res;
731
+ target.offset = source.offset;
732
+ target.repeat = source.repeat;
733
+ target.colorSpace = source.colorSpace;
734
+ return target;
725
735
 
726
736
  }
727
737
 
src/engine-components/Renderer.ts CHANGED
@@ -830,6 +830,8 @@
830
830
  }
831
831
 
832
832
  private _lastLodLevel = -1;
833
+ private _lastScreenCoverage = 0;
834
+ private _lastScreenspaceVolume = 0;
833
835
  private _nextLodTestTime = 0;
834
836
  private _randomLodLevelCheckFrameOffset = Math.floor(Math.random() * 100);
835
837
  private readonly _sphere = new Sphere();
@@ -842,12 +844,15 @@
842
844
  // if (this.isInstancingActive) return 0;
843
845
 
844
846
  const interval = 3;
845
- if (!force && (this.context.time.frame + this._randomLodLevelCheckFrameOffset) % interval != 0) {
846
- return this._lastLodLevel;
847
- }
848
847
 
849
- if (this.context.time.realtimeSinceStartup < this._nextLodTestTime) {
850
- return this._lastLodLevel;
848
+ if (!debugProgressiveLoading) {
849
+ if (!force && (this.context.time.frame + this._randomLodLevelCheckFrameOffset) % interval != 0) {
850
+ return this._lastLodLevel;
851
+ }
852
+
853
+ if (this.context.time.realtimeSinceStartup < this._nextLodTestTime) {
854
+ return this._lastLodLevel;
855
+ }
851
856
  }
852
857
 
853
858
  const maxLevel = 10;
@@ -875,6 +880,9 @@
875
880
  if (lodsInfo?.lods && lodsInfo?.lods.length > 0)
876
881
  meshDensity = (Math.log2(lodsInfo.lods[0].density || 0) / 2) - 8;
877
882
  else if(debugProgressiveLoading) console.warn("No LOD information found...", mesh.name);
883
+ if (debugProgressiveLoading) {
884
+ // ^console.log("Looking at LOD levels", lodsInfo)
885
+ }
878
886
 
879
887
  // TODO: we can skip all this if we dont have any LOD information - we can ask the progressive extension for that
880
888
  const frustum = this.context.mainCameraComponent?.getFrustum();
@@ -907,14 +915,47 @@
907
915
  // calculate size on screen
908
916
  this._box.copy(box);
909
917
  this._box.applyMatrix4(mesh.matrixWorld);
910
- this._box.applyMatrix4(this.context.mainCameraComponent!.getProjectionScreenMatrix(Renderer.tempMatrix));
918
+ const mat = this.context.mainCameraComponent!.getProjectionScreenMatrix(Renderer.tempMatrix);
919
+ this._box.applyMatrix4(mat);
911
920
  // Gizmos.DrawWireBox(this._box.getCenter(getTempVector()), this._box.getSize(getTempVector()), 0x00ff00, .02);
912
921
  const boxSize = this._box.getSize(getTempVector());
913
922
  // const verticalFov = this.context.mainCamera.fov;
914
923
  let screenSize = boxSize.y / 2;// / (2 * Math.atan(Math.PI * verticalFov / 360));
915
924
  // if (mesh.name.startsWith("Statue_")) console.log(mesh.name, "ScreenSize", screenSize, "Density", meshDensity, mesh.geometry.index!.count / 3);
916
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;
917
928
 
929
+ // draw screen size box
930
+ if (debugProgressiveLoading) {
931
+ mat.invert();
932
+
933
+ // get box corners, transform with camera space, and draw as quad lines
934
+ const corner0 = getTempVector();
935
+ corner0.copy(this._box.min);
936
+ const corner1 = getTempVector();
937
+ corner1.copy(this._box.max);
938
+ corner1.x = corner0.x;
939
+ const corner2 = getTempVector();
940
+ corner2.copy(this._box.max);
941
+ corner2.y = corner0.y;
942
+ const corner3 = getTempVector();
943
+ corner3.copy(this._box.max);
944
+ const z = (corner0.z + corner3.z) * 0.5;
945
+
946
+ corner0.z = corner1.z = corner2.z = corner3.z = z;
947
+
948
+ corner0.applyMatrix4(mat);
949
+ corner1.applyMatrix4(mat);
950
+ corner2.applyMatrix4(mat);
951
+ corner3.applyMatrix4(mat);
952
+
953
+ Gizmos.DrawLine(corner0, corner1, 0x0000ff);
954
+ Gizmos.DrawLine(corner0, corner2, 0x0000ff);
955
+ Gizmos.DrawLine(corner1, corner3, 0x0000ff);
956
+ Gizmos.DrawLine(corner2, corner3, 0x0000ff);
957
+ }
958
+
918
959
  // screenSize *= .2;
919
960
 
920
961
  let expectedLevel = 999;
@@ -929,8 +970,9 @@
929
970
  // expectedLevel -= meshDensity - 5;
930
971
  // expectedLevel += meshDensity;
931
972
  const isLowerLod = expectedLevel < level;
932
- if (isLowerLod)
973
+ if (isLowerLod) {
933
974
  level = expectedLevel;
975
+ }
934
976
  }
935
977
  }
936
978
  }
@@ -950,7 +992,8 @@
950
992
  }
951
993
 
952
994
  private drawGizmoLodLevel(changed: boolean) {
953
- const level = this._lastLodLevel;
995
+ // Will be (maxLod + 1) (11) if no lod level is found
996
+ const _level = this._lastLodLevel;
954
997
  const camForward = (this.context.mainCamera as any as IGameObject).worldForward;
955
998
  const camWorld = (this.context.mainCamera as any as IGameObject).worldPosition;
956
999
  for (const mesh of this.sharedMeshes) {
@@ -964,11 +1007,49 @@
964
1007
  const colors = ["#76c43e", "#bcc43e", "#c4ac3e", "#c4673e", "#ff3e3e"];
965
1008
  // if the lod has changed we just want to draw the gizmo for the changed mesh
966
1009
  if (changed) {
967
- Gizmos.DrawWireSphere(boundsCenter, radius, colors[level], .1);
1010
+ Gizmos.DrawWireSphere(boundsCenter, radius, colors[_level], .1);
968
1011
  }
969
1012
  else {
970
- // let helper = mesh["LOD_level_label"] as LabelHandle | null
971
- const text = "LOD " + level + "\n" + mesh.geometry.index!.count / 3;
1013
+ // Mesh Density is calculated as: triangle count per square meter of surface area, normalized to the bounding box size of the model.
1014
+ // Our goal for automatic switching of LODs is that the resulting triangle count per screen area is constant.
1015
+ // We assume a uniform distribution of triangles over the surface area; which means that
1016
+ // we can express a ratio of "screen area to surface area".
1017
+ const triangleCount = mesh.geometry.index!.count / 3;
1018
+ const lods = NEEDLE_progressive.getMeshLODInformation(mesh.geometry)?.lods;
1019
+ const level = lods ? Math.min(lods?.length - 1, _level) : 0;
1020
+ let allLods = "";
1021
+ if (lods) {
1022
+ for (let i = 0; i < lods.length; i++) {
1023
+ allLods += lods[i].density.toFixed(0) + ",";
1024
+ }
1025
+ }
1026
+ const density = lods ? lods[level]?.density : -1;
1027
+ const box = mesh.geometry.boundingBox;
1028
+ const boxSize = box ? box.getSize(getTempVector()) : new Vector3();
1029
+ const maxBoxSize = Math.max(boxSize.x, boxSize.y, boxSize.z);
1030
+
1031
+ // Surface area is in local space of the model;
1032
+ // we need to scale it by the model's world scale and the model's geometry bounding box size.
1033
+ const ws = mesh.getWorldScale(getTempVector());
1034
+ const wsMedian = (ws.x + ws.y + ws.z) / 3;
1035
+ // Area is squared, so both maxBoxSize and wsMedian are squared here
1036
+ // Here, we're basically reverting the calculations that have happened in the pipeline for debugging.
1037
+ const surfaceArea = 1 / density * triangleCount * (maxBoxSize * maxBoxSize) * (wsMedian * wsMedian);
1038
+ const idealDensity = this._lastScreenCoverage;
1039
+ 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";
1051
+ }
1052
+
972
1053
  // if (helper) {
973
1054
  // helper?.setText(text);
974
1055
  // continue;
@@ -978,8 +1059,8 @@
978
1059
  const vertexCount = mesh.geometry.index!.count / 3;
979
1060
  // const vertexCountFactor = Math.min(1, vertexCount / 1000);
980
1061
  const col = colors[Math.min(colors.length - 1, level)] + "88";
981
- const size = Math.min(10, radius);
982
- Gizmos.DrawLabel(pos, text, distance * .001 + size * .03, undefined, 0xffffff, col);
1062
+ // const size = Math.min(10, radius);
1063
+ Gizmos.DrawLabel(pos, text, distance * .01, undefined, 0xffffff, col);
983
1064
  // mesh["LOD_level_label"] = helper;
984
1065
  }
985
1066
 
src/engine-components/webxr/WebXR.ts CHANGED
@@ -90,8 +90,10 @@
90
90
  if (!existingUSDZExporter) {
91
91
  // if no USDZ Exporter is found we add one and assign the scene to be exported
92
92
  if (debug) console.log("WebXR: Adding USDZExporter");
93
- this._usdzExporter = GameObject.addNewComponent(this.gameObject, USDZExporter);
93
+ this._usdzExporter = GameObject.addComponent(this.gameObject, USDZExporter);
94
94
  this._usdzExporter.objectToExport = this.context.scene;
95
+ this._usdzExporter.autoExportAnimations = true;
96
+ this._usdzExporter.autoExportAudioSources = true;
95
97
  }
96
98
  }