Needle Engine

Changes between version 3.35.1-beta.2 and 3.36.0-beta
Files changed (17) hide show
  1. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +2 -2
  2. src/engine-components/Camera.ts +28 -1
  3. src/engine-components/Collider.ts +29 -3
  4. src/engine-components/codegen/components.ts +2 -0
  5. src/engine/engine_physics.ts +29 -2
  6. src/engine/engine_types.ts +6 -25
  7. src/engine-components/ui/Graphic.ts +1 -1
  8. src/engine/extensions/NEEDLE_progressive.ts +555 -156
  9. src/engine/webcomponents/needle menu/needle-menu.ts +5 -7
  10. src/engine-components/ParticleSystem.ts +1 -1
  11. src/engine/codegen/register_types.ts +6 -2
  12. src/engine-components/Renderer.ts +377 -366
  13. src/engine-components/SpriteRenderer.ts +2 -2
  14. src/engine/extensions/usage_tracker.ts +7 -3
  15. src/engine-components/export/usdz/USDZExporter.ts +31 -9
  16. src/engine-components/webxr/controllers/XRControllerModel.ts +7 -1
  17. src/engine-components/RendererInstancing.ts +720 -0
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -229,10 +229,10 @@
229
229
 
230
230
  // Ensure that the progressive textures have been loaded for all variants and materials
231
231
  if (this.materialToSwitch) {
232
- await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, this.materialToSwitch, 0);
232
+ await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, this.materialToSwitch, 0);
233
233
  }
234
234
  if (this.variantMaterial) {
235
- await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, this.variantMaterial, 0);
235
+ await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, this.variantMaterial, 0);
236
236
  }
237
237
  }
238
238
 
src/engine-components/Camera.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EquirectangularReflectionMapping, OrthographicCamera, PerspectiveCamera, Ray, SRGBColorSpace, Vector3 } from "three";
1
+ import { EquirectangularReflectionMapping, Frustum, Matrix, Matrix4, OrthographicCamera, PerspectiveCamera, Ray, SRGBColorSpace, Vector3 } from "three";
2
2
  import { Texture } from "three";
3
3
 
4
4
  import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
@@ -241,6 +241,27 @@
241
241
  }
242
242
  }
243
243
 
244
+ private _frustum?: Frustum;
245
+ public getFrustum(): Frustum {
246
+ if (!this._frustum) {
247
+ this._frustum = new Frustum();
248
+ this.updateFrustum();
249
+ }
250
+ return this._frustum;
251
+ }
252
+ /** Force frustum update - note that this also happens automatically every frame in onBeforeRender */
253
+ public updateFrustum() {
254
+ if (!this._frustum) this._frustum = new Frustum();
255
+ this._frustum.setFromProjectionMatrix(this.getProjectionScreenMatrix(this._projScreenMatrix, true), this.context.renderer.coordinateSystem);
256
+ }
257
+ public getProjectionScreenMatrix(target: Matrix4, forceUpdate?: boolean) {
258
+ if (forceUpdate) {
259
+ this._projScreenMatrix.multiplyMatrices(this.cam.projectionMatrix, this.cam.matrixWorldInverse);
260
+ }
261
+ if (target === this._projScreenMatrix) return target;
262
+ return target.copy(this._projScreenMatrix);
263
+ }
264
+
244
265
  /** @internal */
245
266
  awake() {
246
267
  if (debugscreenpointtoray) {
@@ -271,10 +292,16 @@
271
292
  this.context.removeCamera(this);
272
293
  }
273
294
 
295
+ private readonly _projScreenMatrix = new Matrix4();
296
+
274
297
  /** @internal */
275
298
  onBeforeRender() {
276
299
  if (this._cam) {
277
300
 
301
+ if (this._frustum) {
302
+ this.updateFrustum();
303
+ }
304
+
278
305
  // because the background color may be animated!
279
306
  if (this._clearFlags === ClearFlags.SolidColor)
280
307
  this.applyClearFlagsIfIsActiveCamera();
src/engine-components/Collider.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Group, Mesh, Vector3 } from "three"
1
+ import { BufferGeometry, Group, Mesh, Vector3 } from "three"
2
2
 
3
3
  import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
4
4
  import { serializable } from "../engine/engine_serialization_decorator.js";
@@ -7,6 +7,7 @@
7
7
  import type { IBoxCollider, ICollider, ISphereCollider } from "../engine/engine_types.js";
8
8
  import { validate } from "../engine/engine_util_decorator.js";
9
9
  import { unwatchWrite, watchWrite } from "../engine/engine_utils.js";
10
+ import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
10
11
  import { Behaviour } from "./Component.js";
11
12
  import { Rigidbody } from "./RigidBody.js";
12
13
 
@@ -148,25 +149,50 @@
148
149
 
149
150
  onEnable() {
150
151
  super.onEnable();
152
+ if (!this.context.physics.engine) return;
153
+
151
154
  if (!this.sharedMesh?.isMesh) {
152
155
  // HACK using the renderer mesh
153
156
  if (this.gameObject instanceof Mesh) {
154
157
  this.sharedMesh = this.gameObject;
155
158
  }
156
159
  }
160
+
161
+ const LOD = 0;
162
+
157
163
  if (this.sharedMesh?.isMesh) {
158
- this.context.physics.engine?.addMeshCollider(this, this.sharedMesh, this.convex, getWorldScale(this.gameObject));
164
+ this.context.physics.engine.addMeshCollider(this, this.sharedMesh, this.convex, getWorldScale(this.gameObject));
165
+ NEEDLE_progressive.assignMeshLOD(this.context, this.sourceId!, this.sharedMesh, LOD).then(res => {
166
+ if (res && this.activeAndEnabled && this.context.physics.engine && this.sharedMesh) {
167
+ this.context.physics.engine.removeBody(this);
168
+ this.sharedMesh.geometry = res;
169
+ this.context.physics.engine.addMeshCollider(this, this.sharedMesh, this.convex, getWorldScale(this.gameObject));
170
+ }
171
+ })
159
172
  }
160
173
  else {
161
174
  const group = this.sharedMesh as any as Group;
162
175
  if (group?.isGroup) {
163
176
  console.warn(`MeshCollider mesh is a group \"${this.sharedMesh?.name || this.gameObject.name}\", adding all children as colliders. This is currently not fully supported (colliders can not be removed from world again)`, this);
177
+ const promises = new Array<Promise<BufferGeometry | null>>();
164
178
  for (const ch in group.children) {
165
179
  const child = group.children[ch] as Mesh;
166
180
  if (child.isMesh) {
167
- this.context.physics.engine?.addMeshCollider(this, child, this.convex, getWorldScale(this.gameObject));
181
+ this.context.physics.engine.addMeshCollider(this, child, this.convex, getWorldScale(this.gameObject));
182
+ promises.push(NEEDLE_progressive.assignMeshLOD(this.context, this.sourceId!, child, LOD));
168
183
  }
169
184
  }
185
+ Promise.all(promises).then(res => {
186
+ if (res.some(r => r) == false) return;
187
+ this.context.physics.engine?.removeBody(this);
188
+ const mesh = new Mesh();
189
+ for (const r of res) {
190
+ if (r && this.activeAndEnabled) {
191
+ mesh.geometry = r;
192
+ this.context.physics.engine?.addMeshCollider(this, mesh, this.convex, getWorldScale(this.gameObject));
193
+ }
194
+ }
195
+ });
170
196
  }
171
197
  }
172
198
  }
src/engine-components/codegen/components.ts CHANGED
@@ -87,6 +87,8 @@
87
87
  export { Image } from "../ui/Image.js";
88
88
  export { InheritVelocityModule } from "../ParticleSystemModules.js";
89
89
  export { InputField } from "../ui/InputField.js";
90
+ export { InstanceHandle } from "../RendererInstancing.js";
91
+ export { InstancingHandler } from "../RendererInstancing.js";
90
92
  export { Interactable } from "../Interactable.js";
91
93
  export { Light } from "../Light.js";
92
94
  export { LimitVelocityOverLifetimeModule } from "../ParticleSystemModules.js";
src/engine/engine_physics.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ArrayCamera, AxesHelper, Box3, Camera, type Intersection, Layers, Line, Mesh, Object3D, PerspectiveCamera, Ray, Raycaster, Sphere, Vector2, Vector3 } from 'three'
1
+ import { ArrayCamera, AxesHelper, Box3, BufferGeometry,Camera, type Intersection, Layers, Line, Mesh, Object3D, PerspectiveCamera, Ray, Raycaster, Sphere, Vector2, Vector3 } from 'three'
2
2
 
3
3
  import { Gizmos } from './engine_gizmos.js';
4
4
  import { Context } from './engine_setup.js';
@@ -10,6 +10,26 @@
10
10
  const debugPhysics = getParam("debugphysics");
11
11
  const layerMaskHelper: Layers = new Layers();
12
12
 
13
+ export function getRaycastMesh(obj: Object3D) {
14
+ if (obj.userData?.["needle:raycast-mesh"] instanceof BufferGeometry) {
15
+ return obj.userData["needle:raycast-mesh"];
16
+ }
17
+ if (debugPhysics) {
18
+ if (!obj["needle:warned about missing raycast mesh"]) {
19
+ obj["needle:warned about missing raycast mesh"] = true;
20
+ console.warn("No raycast mesh found for object: " + obj.name);
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+ export function setRaycastMesh(obj: Object3D, geom: BufferGeometry) {
26
+ if (obj.type === "Mesh" || obj.type === "SkinnedMesh") {
27
+ if (!obj.userData) obj.userData = {};
28
+ obj.userData["needle:raycast-mesh"] = geom;
29
+ if(debugPhysics) delete obj["needle:warned about missing raycast mesh"];
30
+ }
31
+ }
32
+
13
33
  export declare type RaycastTestObjectReturnType = void | boolean | "continue in children";
14
34
  export declare type RaycastTestObjectCallback = (obj: Object3D) => RaycastTestObjectReturnType;
15
35
 
@@ -302,7 +322,14 @@
302
322
  if (testResult === false) continue;
303
323
  const checkObject = testResult !== "continue in children";
304
324
  if (checkObject) {
305
- raycaster.intersectObject(obj, false, results);
325
+ const mesh = obj as Mesh;
326
+ const geometry = mesh.geometry;
327
+ if (geometry) {
328
+ const raycastMesh = getRaycastMesh(obj);
329
+ if (raycastMesh) mesh.geometry = raycastMesh;
330
+ raycaster.intersectObject(obj, false, results);
331
+ mesh.geometry = geometry;
332
+ }
306
333
  }
307
334
 
308
335
  if (options.recursive !== false) {
src/engine/engine_types.ts CHANGED
@@ -1,11 +1,10 @@
1
- import type { Camera, Color, Material, Mesh, Object3D, Quaternion, Ray, Scene, WebGLRenderer } from "three";
1
+ import { Camera, Color, Material, Mesh, Object3D, Quaternion, Ray, Scene, WebGLRenderer } from "three";
2
2
  import { Vector3 } from "three";
3
3
  import { type GLTF as GLTF3 } from "three/examples/jsm/loaders/GLTFLoader.js";
4
4
 
5
- import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
5
+ import type { Camera as CameraComponent } from "../engine-components/api.js";
6
+ import type { Context } from "./engine_context.js";
6
7
  import { CollisionDetectionMode, type PhysicsMaterial, RigidbodyConstraints } from "./engine_physics.types.js";
7
- import type { Context } from "./engine_setup.js";
8
- import { RenderTexture } from "./engine_texture.js";
9
8
  import { CircularBuffer } from "./engine_utils.js";
10
9
  import type { INeedleXRSessionEventReceiver, NeedleXRSession } from "./engine_xr.js";
11
10
 
@@ -13,8 +12,8 @@
13
12
  type FilterTypes<T, TypesToFilter> = { [P in keyof T as T[P] extends TypesToFilter ? never : P]: T[P] };
14
13
  /** Removes all undefined functions */
15
14
  type NoUndefinedNoFunctions<T> = FilterTypes<T, Function | undefined | null>;
16
- /* Removes all properties that start with a specific prefix */
17
- type FilterStartingWith<T, Prefix extends string> = { [K in keyof T as K extends string ? (K extends `${Prefix}${string}` ? never : K) : never]: T[K] };
15
+ /* @ts-ignore Removes all properties that start with a specific prefix */
16
+ type FilterStartingWith<T, Prefix extends string> = { [K in keyof T as K extends string ? (K extends `${Prefix}${infer _}` ? never : K) : never]: T[K] };
18
17
  /** Removes all properties that start with an underscore */
19
18
  type NoInternals<T> = FilterStartingWith<T, "_">;
20
19
  type NoInternalNeedleEngineState<T> = Omit<T, "destroyed" | "gameObject" | "activeAndEnabled" | "context" | "isComponent" | "scene" | "up" | "forward" | "right" | "worldRotation" | "worldEuler" | "worldPosition" | "worldQuaternion">;
@@ -209,25 +208,7 @@
209
208
  }
210
209
 
211
210
 
212
- export declare interface ICamera extends IComponent {
213
- get isCamera(): boolean;
214
- applyClearFlagsIfIsActiveCamera(): unknown;
215
- applyClearFlags();
216
- buildCamera();
217
- get cam(): Camera;
218
- nearClipPlane: number;
219
- farClipPlane: number;
220
- backgroundColor: RGBAColor | null;
221
- backgroundBlurriness: number | undefined;
222
- environmentIntensity: number | undefined;
223
- clearFlags: number;
224
- cullingMask: number;
225
- aspect: number;
226
- fieldOfView?: number;
227
- /** x and y are in pixel on the canvas / dom element */
228
- screenPointToRay(x: number, y: number, ray?: Ray): Ray;
229
- targetTexture: RenderTexture | null;
230
- }
211
+ export type ICamera = CameraComponent;
231
212
 
232
213
  /** Interface for a camera controller component that can be attached to a camera to control it */
233
214
  export declare interface ICameraController {
src/engine-components/ui/Graphic.ts CHANGED
@@ -222,7 +222,7 @@
222
222
  }
223
223
  // }
224
224
  this.setOptions({ backgroundImage: tex, borderRadius: 0, backgroundOpacity: this.color.alpha, backgroundSize: "stretch" });
225
- NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, tex, 0).then(res => {
225
+ NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, tex, 0).then(res => {
226
226
  if (res instanceof Texture) {
227
227
  this.setOptions({ backgroundImage: res });
228
228
  }
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -1,7 +1,11 @@
1
- import { Material, RawShaderMaterial, Texture, TextureLoader } from "three";
1
+ import { BufferGeometry, Group, Material, Mesh, Object3D, RawShaderMaterial, Texture, TextureLoader } from "three";
2
2
  import { type GLTF, GLTFLoader, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
3
3
 
4
+ import { showBalloonMessage } from "../debug/index.js";
5
+ import { findResourceUsers, getResourceUserCount } from "../engine_assetdatabase.js";
6
+ import { ContextEvent, NeedleEngine } from "../engine_context_registry.js";
4
7
  import { addDracoAndKTX2Loaders } from "../engine_loaders.js";
8
+ import { getRaycastMesh, setRaycastMesh } from "../engine_physics.js";
5
9
  import { Context } from "../engine_setup.js";
6
10
  import { type SourceIdentifier } from "../engine_types.js";
7
11
  import { delay, getParam, PromiseAllWithErrors, PromiseErrorResult, resolveUrl } from "../engine_utils.js";
@@ -10,52 +14,230 @@
10
14
 
11
15
  const debug = getParam("debugprogressive");
12
16
 
13
- declare type ProgressiveTextureSchema = {
14
- uri: string;
15
- guid: string;
16
- }
17
17
  const $progressiveTextureExtension = Symbol("needle-progressive-texture");
18
18
 
19
+ /** Removes the readonly attribute from all properties of an object */
20
+ type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
19
21
 
20
- const debug_toggle_maps: Map<Material, { [key: string]: { original: Texture, lod0: Texture } }> = new Map();
21
- let show_lod0 = false;
22
+
23
+ const debug_toggle_maps: Map<object, { keys: string[], sourceId: string }> = new Map();
22
24
  if (debug) {
25
+ let currentDebugLodLevel = -1;
26
+ let maxLevel = 2;
27
+ function debugToggleProgressive() {
28
+ currentDebugLodLevel += 1;
29
+ showBalloonMessage(`Toggle LOD level: ${currentDebugLodLevel}<br/>Registered objects: ${debug_toggle_maps.size}`);
30
+ console.log("Toggle LOD level", currentDebugLodLevel, debug_toggle_maps);
31
+ const context = NeedleEngine.Current!;
32
+ debug_toggle_maps.forEach((arr, obj) => {
33
+ for (const key of arr.keys) {
34
+ const cur = obj[key];
35
+ if (cur instanceof BufferGeometry) {
36
+ const info = NEEDLE_progressive.getMeshLODInformation(cur);
37
+ const level = !info ? 0 : Math.min(currentDebugLodLevel, info.lods.length - 1);
38
+ obj["DEBUG:LOD"] = level;
39
+ NEEDLE_progressive.assignMeshLOD(context, arr.sourceId, obj as Mesh, level);
40
+ if(info) maxLevel = Math.max(maxLevel, info.lods.length);
41
+ }
42
+ else if (cur instanceof Texture) {
43
+ NEEDLE_progressive.assignTextureLOD(context, arr.sourceId, cur, currentDebugLodLevel);
44
+ }
45
+ }
46
+ });
47
+ if (currentDebugLodLevel >= maxLevel) {
48
+ currentDebugLodLevel = -1;
49
+ }
50
+ }
23
51
  window.addEventListener("keyup", evt => {
24
- if (evt.key === "p") {
25
- console.log("Toggle progressive textures", debug_toggle_maps);
26
- debug_toggle_maps.forEach((map, material) => {
27
- Object.entries(map).forEach(([key, value]) => {
28
- if (show_lod0) {
29
- material[key] = value.lod0;
30
- } else {
31
- material[key] = value.original;
32
- }
33
- material.needsUpdate = true;
34
- });
35
- });
36
- show_lod0 = !show_lod0;
37
- }
52
+ if (evt.key === "p") debugToggleProgressive();
38
53
  });
54
+ NeedleEngine.registerCallback(ContextEvent.ContextCreated, (ctx) => {
55
+ const button = document.createElement("button");
56
+ button.innerText = "Toggle Progressive";
57
+ button.onclick = debugToggleProgressive;
58
+ ctx.context.menu.appendChild(button);
59
+ });
39
60
  }
61
+ function registerDebug(obj: object, key: string, sourceId: string,) {
62
+ if (!debug) return;
63
+ if (!debug_toggle_maps.has(obj)) {
64
+ debug_toggle_maps.set(obj, { keys: [], sourceId });
65
+ }
66
+ const existing = debug_toggle_maps.get(obj);
67
+ if (existing?.keys?.includes(key) == false) {
68
+ existing.keys.push(key);
69
+ }
70
+ }
40
71
 
72
+ declare type NEEDLE_progressive_model_LOD = {
73
+ path: string,
74
+ }
75
+
76
+ /** This is the data structure we have in the NEEDLE_progressive extension */
77
+ declare type NEEDLE_progressive_model = {
78
+ guid: string,
79
+ lods: Array<NEEDLE_progressive_model_LOD>
80
+ }
81
+
82
+ declare type NEEDLE_progressive_texture_model = NEEDLE_progressive_model & {
83
+
84
+ }
85
+ declare type NEEDLE_progressive_mesh_model = NEEDLE_progressive_model & {
86
+ density: number;
87
+ lods: Array<NEEDLE_progressive_model_LOD & {
88
+ density: number,
89
+ indexCount: number;
90
+ vertexCount: number;
91
+ }>
92
+ }
93
+
41
94
  export class NEEDLE_progressive implements GLTFLoaderPlugin {
42
95
 
96
+ /** The name of the extension */
43
97
  get name(): string {
44
98
  return EXTENSION_NAME;
45
99
  }
46
100
 
101
+ static getMeshLODInformation(geo: BufferGeometry) {
102
+ const info = this.getAssignedLODInformation(geo);
103
+ if (info?.key) {
104
+ return this.lodInfos.get(info.key) as NEEDLE_progressive_mesh_model;
105
+ }
106
+ return null;
107
+ }
108
+
109
+ /** Check if a LOD level is available for a mesh or a texture
110
+ * @param obj the mesh or texture to check
111
+ * @param level the level of detail to check for (0 is the highest resolution). If undefined, the function checks if any LOD level is available
112
+ * @returns true if the LOD level is available (or if any LOD level is available if level is undefined)
113
+ */
114
+ static hasLODLevelAvailable(obj: Mesh | Texture | Material, level?: number): boolean {
115
+
116
+ if (obj instanceof Material) {
117
+ for (const slot of Object.keys(obj)) {
118
+ const val = obj[slot];
119
+ if (val instanceof Texture) {
120
+ if (this.hasLODLevelAvailable(val, level)) return true;
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+ else if (obj instanceof Group) {
126
+ for (const child of obj.children) {
127
+ if (child instanceof Mesh) {
128
+ if (this.hasLODLevelAvailable(child, level)) return true;
129
+ }
130
+ }
131
+ }
132
+
133
+
134
+ let lodObject: ObjectThatMightHaveLODs | undefined;
135
+ let lodInformation: NEEDLE_progressive_model | undefined;
136
+
137
+ if (obj instanceof Mesh) {
138
+ lodObject = obj.geometry as BufferGeometry;
139
+ }
140
+ else if (obj instanceof Texture) {
141
+ lodObject = obj;
142
+ }
143
+ if (lodObject) {
144
+ if (lodObject?.userData?.LODS) {
145
+ const lods = lodObject.userData.LODS;
146
+ lodInformation = this.lodInfos.get(lods.key);
147
+ if (level === undefined) return lodInformation != undefined;
148
+ if (lodInformation) {
149
+ if (Array.isArray(lodInformation.lods)) {
150
+ return level < lodInformation.lods.length;
151
+ }
152
+ return level === 0;
153
+ }
154
+ }
155
+ }
156
+
157
+ return false;
158
+ }
159
+
160
+ /** Load a different resolution of a mesh (if available)
161
+ * @param context the context
162
+ * @param source the sourceid of the file from which the mesh is loaded (this is usually the component's sourceId)
163
+ * @param mesh the mesh to load the LOD for
164
+ * @param level the level of detail to load (0 is the highest resolution)
165
+ * @returns a promise that resolves to the mesh with the requested LOD level
166
+ * @example
167
+ * ```javascript
168
+ * const mesh = this.gameObject as Mesh;
169
+ * NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
170
+ * console.log("Mesh with LOD level 1 loaded", mesh);
171
+ * });
172
+ * ```
173
+ */
174
+ static assignMeshLOD(context: Context, source: SourceIdentifier, mesh: Mesh, level: number): Promise<BufferGeometry | null> {
175
+
176
+ if (!mesh) return Promise.resolve(null);
177
+
178
+
179
+ if (mesh instanceof Mesh) {
180
+
181
+ const currentGeometry = mesh.geometry;
182
+ const lodinfo = this.getAssignedLODInformation(currentGeometry);
183
+ if (!lodinfo) {
184
+ return Promise.resolve(null);
185
+ }
186
+
187
+ if (!getRaycastMesh(mesh)) {
188
+ setRaycastMesh(mesh, mesh.geometry as BufferGeometry);
189
+ }
190
+
191
+ const info = this.onProgressiveLoadStart(context, source, mesh, null);
192
+ mesh["LOD:requested level"] = level;
193
+ return NEEDLE_progressive.getOrLoadLOD<BufferGeometry>(context, source, currentGeometry, level).then(geo => {
194
+ if (mesh["LOD:requested level"] === level) {
195
+ delete mesh["LOD:requested level"];
196
+
197
+ if (Array.isArray(geo)) {
198
+ const index = lodinfo.index || 0;
199
+ geo = geo[index];
200
+ }
201
+
202
+ if (geo && currentGeometry != geo) {
203
+ if (debug == "verbose") console.log("Progressive Mesh " + mesh.name + " loaded", currentGeometry, "→", geo, "\n", mesh)
204
+ if (geo instanceof BufferGeometry) {
205
+ mesh.geometry = geo;
206
+ if (debug) registerDebug(mesh, "geometry", source);
207
+ }
208
+ }
209
+ }
210
+ this.onProgressiveLoadEnd(info);
211
+ return geo;
212
+ }).catch(err => {
213
+ this.onProgressiveLoadEnd(info);
214
+ console.error("Error loading mesh LOD", mesh, err);
215
+ return null;
216
+ });
217
+ }
218
+ else if (debug) {
219
+ console.error("Invalid call to assignMeshLOD: Request mesh LOD but the object is not a mesh", mesh);
220
+ }
221
+
222
+ return Promise.resolve(null);
223
+ }
224
+
47
225
  /** Load a different resolution of a texture (if available)
48
226
  * @param context the context
49
227
  * @param source the sourceid of the file from which the texture is loaded (this is usually the component's sourceId)
50
228
  * @param materialOrTexture the material or texture to load the LOD for (if passing in a material all textures in the material will be loaded)
51
229
  * @param level the level of detail to load (0 is the highest resolution) - currently only 0 is supported
230
+ * @returns a promise that resolves to the material or texture with the requested LOD level
52
231
  */
53
- static assignTextureLOD(context: Context, source: SourceIdentifier | undefined, materialOrTexture: Material | Texture, level: number = 0): Promise<any> {
232
+ static assignTextureLOD(context: Context, source: SourceIdentifier, materialOrTexture: Material | Texture, level: number = 0)
233
+ : Promise<Array<{ slot: string, texture: Texture | null }> | Texture | null> {
234
+
54
235
  if (!materialOrTexture) return Promise.resolve(null);
55
236
 
56
237
  if (materialOrTexture instanceof Material) {
57
238
  const material = materialOrTexture;
58
239
  const promises: Array<Promise<Texture | null>> = [];
240
+ const slots = new Array<string>();
59
241
 
60
242
  if (material instanceof RawShaderMaterial) {
61
243
  // iterate uniforms of custom shaders
@@ -64,6 +246,7 @@
64
246
  if (val instanceof Texture) {
65
247
  const task = this.assignTextureLODForSlot(context, source, val, level, material, slot);
66
248
  promises.push(task);
249
+ slots.push(slot);
67
250
  }
68
251
  }
69
252
  }
@@ -73,10 +256,24 @@
73
256
  if (val instanceof Texture) {
74
257
  const task = this.assignTextureLODForSlot(context, source, val, level, material, slot);
75
258
  promises.push(task);
259
+ slots.push(slot);
76
260
  }
77
261
  }
78
262
  }
79
- return PromiseAllWithErrors(promises);
263
+ return PromiseAllWithErrors(promises).then(res => {
264
+ const textures = new Array<{ slot: string, texture: Texture | null }>();
265
+ for (let i = 0; i < res.results.length; i++) {
266
+ const tex = res.results[i];
267
+ const slot = slots[i];
268
+ if (tex instanceof Texture) {
269
+ textures.push({ slot, texture: tex });
270
+ }
271
+ else {
272
+ textures.push({ slot, texture: null });
273
+ }
274
+ }
275
+ return textures;
276
+ });
80
277
  }
81
278
 
82
279
  if (materialOrTexture instanceof Texture) {
@@ -87,153 +284,291 @@
87
284
  return Promise.resolve(null);
88
285
  }
89
286
 
90
- private static assignTextureLODForSlot(context: Context, source: SourceIdentifier | undefined, val: any, level: number, material: Material | null, slot: string | null): Promise<Texture | null> {
91
- if (val?.isTexture !== true) return Promise.resolve(null);
287
+ private static assignTextureLODForSlot(context: Context, source: SourceIdentifier, current: Texture, level: number, material: Material | null, slot: string | null): Promise<Texture | null> {
288
+ if (current?.isTexture !== true) return Promise.resolve(null);
92
289
 
93
- if (debug) console.log("-----------\n", "FIND", material?.name, slot, val?.name, val?.userData, val, material);
290
+ // if (debug) console.log("-----------\n", "FIND", material?.name, slot, current?.name, current?.userData, current, material);
94
291
 
95
- return NEEDLE_progressive.getOrLoadTexture(context, source, val, level, material, slot).then(t => {
292
+ const info = this.onProgressiveLoadStart(context, source, material, slot);
293
+ return NEEDLE_progressive.getOrLoadLOD<Texture>(context, source, current, level).then(tex => {
96
294
 
97
- if (t?.isTexture === true) {
295
+ // this can currently not happen
296
+ if (Array.isArray(tex)) return null;
98
297
 
99
- if (debug) console.warn("Assign LOD", material?.name, slot, t.name, t["guid"], material, "Prev:", val, "Now:", t, "\n--------------");
298
+ if (tex?.isTexture === true) {
299
+ if (tex != current) {
300
+ // if (debug) console.warn("Assign LOD", material?.name, slot, tex.name, tex["guid"], material, "Prev:", current, "Now:", tex, "\n--------------");
100
301
 
101
- t.needsUpdate = true;
302
+ // tex.needsUpdate = true;
102
303
 
103
- if (material && slot) {
104
- material[slot] = t;
105
- material.needsUpdate = true;
106
- }
304
+ if (material && slot) {
305
+ material[slot] = tex;
306
+ // material.needsUpdate = true;
307
+ }
107
308
 
108
- if (debug && material && slot) {
109
- let debug_map = debug_toggle_maps.get(material);
110
- if (!debug_map) {
111
- debug_map = {};
112
- debug_toggle_maps.set(material, debug_map);
113
- }
114
- let entry = debug_map[slot];
115
- if (!entry) {
116
- entry = debug_map[slot] = { original: val, lod0: t };
117
- }
118
- entry.lod0 = t;
309
+ if (debug && slot && material) registerDebug(material, slot, source);
310
+
311
+ // check if the old texture is still used by other objects
312
+ // if not we dispose it...
313
+ // this could also be handled elsewhere and not be done immediately
314
+ // const users = getResourceUserCount(current);
315
+ // if (!users) {
316
+ // if (debug) console.log("Progressive: Dispose texture", current.name, current.source.data, current.uuid);
317
+ // current?.dispose();
318
+ // }
119
319
  }
120
320
 
121
- return t;
321
+ this.onProgressiveLoadEnd(info);
322
+ return tex;
122
323
  }
324
+ else if (debug) {
325
+ console.warn("No LOD found for", level, current, tex);
326
+ }
123
327
 
328
+ this.onProgressiveLoadEnd(info);
124
329
  return null;
330
+
331
+ }).catch(err => {
332
+ this.onProgressiveLoadEnd(info);
333
+ console.error("Error loading LOD", current, err);
334
+ return null;
125
335
  });
126
336
  }
127
337
 
128
- private parser: GLTFParser;
129
- private sourceId: SourceIdentifier;
130
- private context: Context;
131
338
 
339
+
340
+
341
+ private readonly parser: GLTFParser;
342
+ private readonly sourceId: SourceIdentifier;
343
+ private readonly context: Context;
344
+
132
345
  constructor(parser: GLTFParser, sourceId: SourceIdentifier, context: Context) {
133
346
  this.parser = parser;
134
347
  this.sourceId = sourceId;
135
348
  this.context = context;
136
349
  }
137
350
 
138
-
139
351
  afterRoot(gltf: GLTF): null {
140
352
  if (debug)
141
353
  console.log("AFTER", this.sourceId, gltf);
354
+
142
355
  this.parser.json.textures?.forEach((textureInfo, index) => {
143
356
  if (textureInfo?.extensions) {
144
- const ext: ProgressiveTextureSchema = textureInfo?.extensions[EXTENSION_NAME];
357
+ const ext: NEEDLE_progressive_texture_model = textureInfo?.extensions[EXTENSION_NAME];
145
358
  if (ext) {
146
359
  const prom = this.parser.getDependency("texture", index);
147
- prom.then(t => {
148
- if (debug) console.log("> Progressive: register", t.name, t.uuid, ext);
360
+ prom.then(tex => {
361
+ if (debug)
362
+ console.log("> Progressive: register texture", index, tex.name, tex.uuid, tex, ext);
149
363
  // 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)
150
364
  // see https://github.com/needle-tools/needle-engine-support/issues/133
151
- if (t.source)
152
- t.source[$progressiveTextureExtension] = ext;
153
- NEEDLE_progressive.cache.set(t.uuid, ext);
154
- return t;
365
+ if (tex.source)
366
+ tex.source[$progressiveTextureExtension] = ext;
367
+ const LODKEY = tex.uuid;
368
+ if (!tex.userData) tex.userData = {};
369
+ NEEDLE_progressive.assignLODInformation(tex, LODKEY, 0, 0, undefined);
370
+ NEEDLE_progressive.lodInfos.set(LODKEY, ext);
371
+ NEEDLE_progressive.lowresCache.set(LODKEY, tex);
372
+ return tex;
373
+ }).catch(err => {
374
+ console.error(`Error loading progressive texture ${index}\n`, err);
155
375
  });
156
376
  }
157
377
  }
158
378
  });
159
379
 
380
+ this.parser.json.meshes?.forEach((meshInfo, index) => {
381
+ if (meshInfo?.extensions) {
382
+ const ext = meshInfo?.extensions[EXTENSION_NAME] as NEEDLE_progressive_mesh_model;
383
+ if (ext && ext.lods) {
384
+ const prom = this.parser.getDependency("mesh", index);
385
+ prom.then((obj: Mesh | Group) => {
386
+ if (debug) console.log("> Progressive: register mesh", index, obj.name, ext, obj.uuid, obj);
387
+ const LODKEY = obj.uuid;
388
+ const LODLEVEL = ext.lods.length;
389
+ if (obj instanceof Mesh) {
390
+ applyMeshLOD(LODKEY, obj, LODLEVEL, undefined, ext);
391
+ }
392
+ else {
393
+ const geometries = new Array<BufferGeometry>();
394
+ for (let i = 0; i < obj.children.length; i++) {
395
+ const child = obj.children[i];
396
+ if (child instanceof Mesh) {
397
+ geometries.push(child.geometry as BufferGeometry);
398
+ applyMeshLOD(LODKEY, child, LODLEVEL, i, ext);
399
+ }
400
+ }
401
+ NEEDLE_progressive.lowresCache.set(LODKEY, geometries);
402
+ }
403
+ return obj;
404
+ }).catch(err => {
405
+ console.error(`Error loading progressive mesh ${index}\n`, err);
406
+ });
407
+ }
408
+ }
409
+ });
410
+
411
+ const applyMeshLOD = (key: string, mesh: Mesh, level: number, index: number | undefined, ext: NEEDLE_progressive_mesh_model) => {
412
+ const geometry = mesh.geometry as BufferGeometry;
413
+ geometry["needle:raycast-mesh"] = true;
414
+ // save the low res mesh as a raycast mesh (if we have a geometry and no mesh assigned already)
415
+ if (geometry && !getRaycastMesh(mesh)) {
416
+ if (debug) console.log("Set raycast mesh", mesh.name, mesh.uuid, geometry);
417
+ setRaycastMesh(mesh, geometry);
418
+ findResourceUsers(geometry, true).forEach(user => {
419
+ if (user instanceof Mesh) {
420
+ setRaycastMesh(user, geometry);
421
+ }
422
+ });
423
+ }
424
+ if (!geometry.userData) geometry.userData = {};
425
+ NEEDLE_progressive.assignLODInformation(geometry, key, level, index, ext.density);
426
+ NEEDLE_progressive.lodInfos.set(key, ext);
427
+ };
428
+
160
429
  return null;
161
430
  }
162
431
 
163
- private static cache = new Map<string, ProgressiveTextureSchema>();
164
- private static resolved: { [key: string]: Texture } = {};
165
- private static currentlyLoading: { [key: string]: Promise<Texture | null> } = {};
432
+ /** A map of key = asset uuid and value = LOD information */
433
+ private static readonly lodInfos = new Map<string, NEEDLE_progressive_model>();
434
+ /** cache of already loaded mesh lods */
435
+ private static readonly previouslyLoaded: Map<string, Promise<null | Texture | BufferGeometry | BufferGeometry[]>> = new Map();
436
+ /** this contains the geometry/textures that were originally loaded */
437
+ private static readonly lowresCache: Map<string, Texture | BufferGeometry | BufferGeometry[]> = new Map();
166
438
 
167
- private static async getOrLoadTexture(context: Context, source: SourceIdentifier | undefined, current: Texture, _level: number, material: Material | null, slot: string | null): Promise<Texture | null> {
439
+ private static async getOrLoadLOD<T extends Texture | BufferGeometry>(
440
+ context: Context, source: SourceIdentifier | undefined, current: T & ObjectThatMightHaveLODs, level: number
441
+ ): Promise<T | null> {
168
442
 
169
- const key = current.uuid;
443
+ const debugverbose = debug == "verbose";
170
444
 
171
- let progressiveInfo: ProgressiveTextureSchema | undefined;
445
+ /** this key is used to lookup the LOD information */
446
+ const LOD: LODInformation | undefined = current.userData.LODS;
172
447
 
448
+ if (!LOD) {
449
+ return null;
450
+ }
451
+
452
+ const LODKEY = LOD?.key;
453
+
454
+ let progressiveInfo: NEEDLE_progressive_model | undefined;
455
+
173
456
  // See https://github.com/needle-tools/needle-engine-support/issues/133
174
- if (current.source && current.source[$progressiveTextureExtension])
175
- progressiveInfo = current.source[$progressiveTextureExtension];
176
- if (!progressiveInfo) progressiveInfo = NEEDLE_progressive.cache.get(key);
457
+ if (current instanceof Texture) {
458
+ if (current.source && current.source[$progressiveTextureExtension])
459
+ progressiveInfo = current.source[$progressiveTextureExtension];
460
+ }
177
461
 
462
+
463
+ if (!progressiveInfo) progressiveInfo = NEEDLE_progressive.lodInfos.get(LODKEY);
464
+
178
465
  if (progressiveInfo) {
179
- if (debug)
180
- console.log(key, progressiveInfo.uri, progressiveInfo.guid);
181
- const uri = resolveUrl(source, progressiveInfo.uri);
182
- if (uri.endsWith(".glb") || uri.endsWith(".gltf")) {
466
+
467
+ if (level > 0) {
468
+ let useLowRes = false;
469
+ const hasMultipleLevels = Array.isArray(progressiveInfo.lods);
470
+ if (hasMultipleLevels && level >= progressiveInfo.lods.length) {
471
+ useLowRes = true;
472
+ }
473
+ else if (!hasMultipleLevels) {
474
+ useLowRes = true;
475
+ }
476
+ if (useLowRes) {
477
+ const lowres = this.lowresCache.get(LODKEY) as T;
478
+ return lowres;
479
+ }
480
+ }
481
+
482
+ /** the unresolved LOD url */
483
+ const unresolved_lod_url = Array.isArray(progressiveInfo.lods) ? progressiveInfo.lods[level].path : progressiveInfo.lods;
484
+
485
+ // check if we have a uri
486
+ if (!unresolved_lod_url) {
487
+ if (debug && !progressiveInfo["missing:uri"]) {
488
+ progressiveInfo["missing:uri"] = true;
489
+ console.warn("Missing uri for progressive asset for LOD " + level, progressiveInfo);
490
+ }
491
+ return null;
492
+ }
493
+
494
+ /** the resolved LOD url */
495
+ const lod_url = resolveUrl(source, unresolved_lod_url);
496
+
497
+ // check if the requested file needs to be loaded via a GLTFLoader
498
+ if (lod_url.endsWith(".glb") || lod_url.endsWith(".gltf")) {
183
499
  if (!progressiveInfo.guid) {
184
500
  console.warn("missing pointer for glb/gltf texture", progressiveInfo);
185
501
  return null;
186
502
  }
187
- const resolveKey = uri + "_" + progressiveInfo.guid;
188
- if (this.resolved[resolveKey]) {
189
- let res = this.resolved[resolveKey];
190
- // check if the texture has been disposed or not
191
- if (res.image?.data || res.source?.data) {
192
- if (debug) console.log("Texture has already been loaded: " + resolveKey, material?.name, slot, current.name, res);
193
- res = this.copySettings(current, res);
194
- return res;
503
+ // check if the requested file has already been loaded
504
+ const KEY = lod_url + "_" + progressiveInfo.guid;
505
+
506
+ // check if the requested file is currently being loaded
507
+ const existing = this.previouslyLoaded.get(KEY);
508
+ if (existing !== undefined) {
509
+ if (debugverbose) console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
510
+ let res = await existing.catch(err => {
511
+ console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
512
+ return null;
513
+ });
514
+ let resouceIsDisposed = false;
515
+ if (res == null) {
516
+ // if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
517
+ // in which case we don't attempt to load it again
195
518
  }
196
- else if (res) {
197
- if (debug) console.warn("Texture has been disposed, will load again: " + resolveKey, material?.name, slot, current.name, res.source.data);
519
+ else if (res instanceof Texture && current instanceof Texture) {
520
+ // check if the texture has been disposed or not
521
+ if (res.image?.data || res.source?.data) {
522
+ res = this.copySettings(current, res);
523
+ }
524
+ // if it has been disposed we need to load it again
525
+ else {
526
+ resouceIsDisposed = true;
527
+ this.previouslyLoaded.delete(KEY);
528
+ }
198
529
  }
530
+ else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
531
+ if (res.attributes.position?.array) {
532
+ // the geometry is OK
533
+ }
534
+ else {
535
+ resouceIsDisposed = true;
536
+ this.previouslyLoaded.delete(KEY);
537
+ }
538
+ }
539
+ if (!resouceIsDisposed) {
540
+ return res as T;
541
+ }
199
542
  }
200
543
 
201
- const info = this.onProgressiveLoadStart(context, source, uri, material, slot);
202
- try {
203
- if (this.currentlyLoading[resolveKey] !== undefined) {
204
- if (debug)
205
- console.log("Already loading:", material?.name + "." + slot, resolveKey);
206
- let res = await this.currentlyLoading[resolveKey];
207
- if (res)
208
- res = this.copySettings(current, res);
209
- return res;
544
+ const ext = progressiveInfo;
545
+ const request = new Promise<null | Texture | BufferGeometry | BufferGeometry[]>(async (resolve, _) => {
546
+ const loader = new GLTFLoader();
547
+ addDracoAndKTX2Loaders(loader, context);
548
+
549
+
550
+ if (debug) {
551
+ await delay(Math.random() * 1000);
552
+ if (debugverbose) console.warn("Start loading (delayed) " + lod_url, ext.guid);
210
553
  }
211
- const ext = progressiveInfo;
212
- const request = new Promise<Texture | null>(async (resolve, _) => {
213
- const loader = new GLTFLoader();
214
- addDracoAndKTX2Loaders(loader, context);
215
554
 
555
+ const gltf = await loader.loadAsync(lod_url).catch(err => {
556
+ console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
557
+ return null;
558
+ });
559
+ if (!gltf) return null;
216
560
 
217
- if (debug) console.warn("Start loading " + uri, material?.name, slot, ext.guid);
218
- if (debug) {
219
- await delay(Math.random() * 1000);
220
- }
561
+ const parser = gltf.parser;
562
+ if (debugverbose) console.log("Loading finished " + lod_url, ext.guid);
563
+ let index = -1;
221
564
 
222
- const gltf = await loader.loadAsync(uri);
223
- const parser = gltf.parser;
224
- if (debug) console.log("Loading finished " + uri, material?.name, slot, ext.guid);
225
- let index = -1;
565
+ if (gltf.parser.json.textures) {
226
566
  let found = false;
227
-
228
- if (!gltf.parser.json?.textures) {
229
- if (debug) console.warn("No textures in glTF " + uri + " - may be a bug", material?.name, slot, ext.guid);
230
- return resolve(null);
231
- }
232
-
233
567
  for (const tex of gltf.parser.json.textures) {
568
+ // find the texture index
234
569
  index++;
235
570
  if (tex?.extensions) {
236
- const other: ProgressiveTextureSchema = tex?.extensions[EXTENSION_NAME];
571
+ const other: NEEDLE_progressive_model = tex?.extensions[EXTENSION_NAME];
237
572
  if (other?.guid) {
238
573
  if (other.guid === ext.guid) {
239
574
  found = true;
@@ -242,69 +577,103 @@
242
577
  }
243
578
  }
244
579
  }
245
- if (!found)
246
- return resolve(null);
580
+ if (found) {
581
+ let tex = await parser.getDependency("texture", index) as Texture;
582
+ if (debugverbose) console.log("change \"" + current.name + "\" → \"" + tex.name + "\"", lod_url, index, tex, KEY);
583
+ if (current instanceof Texture)
584
+ tex = this.copySettings(current, tex);
585
+ if (tex) {
586
+ (tex as any).guid = ext.guid;
587
+ }
588
+ return resolve(tex);
589
+ }
590
+ }
247
591
 
248
- let tex = await parser.getDependency("texture", index) as Texture;
249
- tex = this.copySettings(current, tex);
250
- if (tex) {
251
- (tex as any).guid = ext.guid;
592
+ if (gltf.parser.json.meshes) {
593
+ let found = false;
594
+ for (const mesh of gltf.parser.json.meshes) {
595
+ // find the mesh index
596
+ index++;
597
+ if (mesh?.extensions) {
598
+ const other: NEEDLE_progressive_model = mesh?.extensions[EXTENSION_NAME];
599
+ if (other?.guid) {
600
+ if (other.guid === ext.guid) {
601
+ found = true;
602
+ break;
603
+ }
604
+ }
605
+ }
252
606
  }
253
- this.resolved[resolveKey] = tex as Texture;
254
- if (debug)
255
- console.log(material?.name, slot, "change \"" + current.name + "\" → \"" + tex.name + "\"", uri, index, tex, material, resolveKey);
256
- resolve(tex);
257
- });
258
- this.currentlyLoading[resolveKey] = request;
259
- const tex = await request;
260
- return tex;
261
- }
262
- finally {
263
- delete this.currentlyLoading[resolveKey];
264
- this.onProgressiveLoadEnd(info);
265
- }
607
+ if (found) {
608
+ const mesh = await parser.getDependency("mesh", index) as Mesh | Group;
609
+
610
+ const meshExt = ext as NEEDLE_progressive_mesh_model;
611
+
612
+ if (debugverbose) console.log(`Loaded Mesh \"${mesh.name}\"`, lod_url, index, mesh, KEY);
613
+
614
+ if (mesh instanceof Mesh) {
615
+ const geo = mesh.geometry as BufferGeometry;
616
+ NEEDLE_progressive.assignLODInformation(geo, LODKEY, level, undefined, meshExt.density);
617
+ return resolve(geo);
618
+ }
619
+ else {
620
+ const geometries = new Array<BufferGeometry>();
621
+ for (let i = 0; i < mesh.children.length; i++) {
622
+ const child = mesh.children[i];
623
+ if (child instanceof Mesh) {
624
+ const geo = child.geometry as BufferGeometry;
625
+ NEEDLE_progressive.assignLODInformation(geo, LODKEY, level, i, meshExt.density);
626
+ geometries.push(geo);
627
+ }
628
+ }
629
+ return resolve(geometries);
630
+ }
631
+ }
632
+ }
633
+
634
+ // we could not find a texture or mesh with the given guid
635
+ return resolve(null);
636
+ });
637
+ this.previouslyLoaded.set(KEY, request);
638
+ const res = await request;
639
+ return res as T;
266
640
  }
267
641
  else {
268
- const info = this.onProgressiveLoadStart(context, source, uri, material, slot);
269
- try {
270
- if (debug) console.log("Load texture from uri: " + uri);
642
+ if (current instanceof Texture) {
643
+ if (debugverbose) console.log("Load texture from uri: " + lod_url);
271
644
  const loader = new TextureLoader();
272
- const tex = await loader.loadAsync(uri);
645
+ const tex = await loader.loadAsync(lod_url);
273
646
  if (tex) {
274
647
  (tex as any).guid = progressiveInfo.guid;
275
648
  tex.flipY = false;
276
649
  tex.needsUpdate = true;
277
650
  tex.colorSpace = current.colorSpace;
278
- if (debug)
651
+ if (debugverbose)
279
652
  console.log(progressiveInfo, tex);
280
653
  }
281
- else if (debug) console.warn("failed loading", uri);
282
- return tex;
654
+ else if (debug) console.warn("failed loading", lod_url);
655
+ return tex as T;
283
656
  }
284
- finally {
285
- this.onProgressiveLoadEnd(info);
286
- }
287
657
  }
288
- // loader.then((h: Texture) => {
289
- // // console.log(t, h);
290
- // // t.image = h.image;
291
- // // // this.context.renderer.copyTextureToTexture(new Vector2(0, 0), h, t);
292
- // // // console.log(h);
293
-
294
- // // // t.source = h.source;
295
- // // // t.version++;
296
- // // t.width = h.width;
297
- // // t.height = h.height;
298
- // // t.needsUpdate = true;
299
- // });
300
658
  }
301
659
  else {
302
660
  if (debug)
303
- console.warn("unknown texture", current.name, current.uuid, current);
661
+ console.warn(`Can not load LOD ${level}: no LOD info found for \"${LODKEY}\" ${current.name}`, current.type);
304
662
  }
305
663
  return null;
306
664
  }
307
665
 
666
+ private static assignLODInformation(res: DeepWriteable<ObjectThatMightHaveLODs>, key: string, level: number, index?: number, density?: number) {
667
+ if (!res) return;
668
+ if (!res.userData) res.userData = {};
669
+ const info: LODInformation = new LODInformation(key, level, index, density);
670
+ res.userData.LODS = info;
671
+ res.userData.LOD = level;
672
+ }
673
+ private static getAssignedLODInformation(res: ObjectThatMightHaveLODs | null | undefined): null | LODInformation {
674
+ return res?.userData?.LODS || null;
675
+ }
676
+
308
677
  private static copySettings(source: Texture, target: Texture): Texture {
309
678
  // We need to clone e.g. when the same texture is used multiple times (but with e.g. different wrap settings)
310
679
  // This is relatively cheap since it only stores settings
@@ -314,6 +683,9 @@
314
683
  res.copy(source);
315
684
  res.source = src;
316
685
  res.mipmaps = mips;
686
+ // we re-use the offset and repeat settings because it might be animated
687
+ res.offset = source.offset;
688
+ res.repeat = source.repeat;
317
689
  return res;
318
690
 
319
691
  }
@@ -361,11 +733,11 @@
361
733
  private static _currentProgressiveLoadingInfo: Map<Context, ProgressiveLoadingInfo[]> = new Map();
362
734
 
363
735
  // called whenever a progressive loading event starts
364
- private static onProgressiveLoadStart(context: Context, source: SourceIdentifier | undefined, uri: string, material: Material | null, slot: string | null): ProgressiveLoadingInfo {
736
+ private static onProgressiveLoadStart(context: Context, source: SourceIdentifier | undefined, material: Mesh | Material | null, slot: string | null): ProgressiveLoadingInfo {
365
737
  if (!this._currentProgressiveLoadingInfo.has(context)) {
366
738
  this._currentProgressiveLoadingInfo.set(context, []);
367
739
  }
368
- const info = new ProgressiveLoadingInfo(context, source, uri, material, slot);
740
+ const info = new ProgressiveLoadingInfo(context, source, material, slot);
369
741
  const current = this._currentProgressiveLoadingInfo.get(context)!;
370
742
  const listener = this._progressiveEventListeners.get(context);
371
743
  if (listener) listener.onStart(info);
@@ -391,21 +763,48 @@
391
763
  }
392
764
  }
393
765
 
766
+ declare type ObjectThatMightHaveLODs = { userData?: { LODS?: LODInformation, readonly LOD?: number } };
394
767
 
768
+ // declare type GetLODInformation = () => LODInformation | null;
769
+
770
+ class LODInformation {
771
+ /** the key to lookup the LOD information */
772
+ readonly key: string;
773
+ readonly level: number;
774
+ /** For multi objects (e.g. a group of meshes) this is the index of the object */
775
+ readonly index?: number;
776
+ /** the mesh density */
777
+ readonly density?: number;
778
+
779
+ constructor(key: string, level: number, index?: number, density?: number) {
780
+ this.key = key;
781
+ this.level = level;
782
+ if (index != undefined)
783
+ this.index = index;
784
+ if (density != undefined)
785
+ this.density = density;
786
+ }
787
+ };
788
+
789
+
790
+
395
791
  /** info object that holds information about a file that is currently being loaded */
396
792
  export class ProgressiveLoadingInfo {
397
793
  readonly context: Context;
398
794
  readonly source: SourceIdentifier | undefined;
399
- readonly uri: string;
400
795
  readonly material?: Material | null;
401
796
  readonly slot?: string | null;
797
+ readonly mesh?: Mesh | null;
402
798
  // TODO: can contain information if the event is a background process / preloading or if the object is currently visible
403
799
 
404
- constructor(context: Context, source: SourceIdentifier | undefined, uri: string, material?: Material | null, slot?: string | null) {
800
+ constructor(context: Context, source: SourceIdentifier | undefined, object?: Mesh | Material | null, slot?: string | null) {
405
801
  this.context = context;
406
802
  this.source = source;
407
- this.uri = uri;
408
- this.material = material;
803
+ if (object instanceof Mesh) {
804
+ this.mesh = object;
805
+ }
806
+ else
807
+ this.material = object;
409
808
  this.slot = slot;
410
809
  }
411
810
  };
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -412,16 +412,14 @@
412
412
  const rootObserver = new MutationObserver(mutations => {
413
413
  this.onChangeDetected(mutations);
414
414
 
415
- // ensure the menu is not hidden
416
- const parent = this.parentNode;
417
- if (parent != this._domElement?.shadowRoot) {
418
- this._domElement?.shadowRoot?.appendChild(this);
419
- }
420
-
421
- if (this.style.display != "flex" || this.style.visibility != "visible" || this.style.opacity != "1") {
415
+ // ensure the menu is not hidden or removed
416
+ const parent = this?.parentNode;
417
+ if (this.style.display != "flex" || this.style.visibility != "visible" || this.style.opacity != "1" || parent != this._domElement?.shadowRoot) {
422
418
  if (!hasProLicense()) {
423
419
  clearInterval(showInterval);
424
420
  showInterval = setInterval(() => {
421
+ if (parent != this._domElement?.shadowRoot)
422
+ this._domElement?.shadowRoot?.appendChild(this);
425
423
  this.style.display = "flex";
426
424
  this.style.visibility = "visible";
427
425
  this.style.opacity = "1";
src/engine-components/ParticleSystem.ts CHANGED
@@ -101,7 +101,7 @@
101
101
  if (debugProgressiveLoading) {
102
102
  console.log("Load material LOD", material.name);
103
103
  }
104
- NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, material);
104
+ NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, material);
105
105
  }
106
106
 
107
107
  return material;
src/engine/codegen/register_types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable */
2
2
  import { TypeStore } from "./../engine_typestore.js"
3
-
3
+
4
4
  // Import types
5
5
  import { __Ignore } from "../../engine-components/codegen/components.js";
6
6
  import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
@@ -89,6 +89,8 @@
89
89
  import { Image } from "../../engine-components/ui/Image.js";
90
90
  import { InheritVelocityModule } from "../../engine-components/ParticleSystemModules.js";
91
91
  import { InputField } from "../../engine-components/ui/InputField.js";
92
+ import { InstanceHandle } from "../../engine-components/RendererInstancing.js";
93
+ import { InstancingHandler } from "../../engine-components/RendererInstancing.js";
92
94
  import { Interactable } from "../../engine-components/Interactable.js";
93
95
  import { Light } from "../../engine-components/Light.js";
94
96
  import { LimitVelocityOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
@@ -216,7 +218,7 @@
216
218
  import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
217
219
  import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
218
220
  import { XRState } from "../../engine-components/webxr/XRFlag.js";
219
-
221
+
220
222
  // Register types
221
223
  TypeStore.add("__Ignore", __Ignore);
222
224
  TypeStore.add("ActionBuilder", ActionBuilder);
@@ -305,6 +307,8 @@
305
307
  TypeStore.add("Image", Image);
306
308
  TypeStore.add("InheritVelocityModule", InheritVelocityModule);
307
309
  TypeStore.add("InputField", InputField);
310
+ TypeStore.add("InstanceHandle", InstanceHandle);
311
+ TypeStore.add("InstancingHandler", InstancingHandler);
308
312
  TypeStore.add("Interactable", Interactable);
309
313
  TypeStore.add("Light", Light);
310
314
  TypeStore.add("LimitVelocityOverLifetimeModule", LimitVelocityOverLifetimeModule);
src/engine-components/Renderer.ts CHANGED
@@ -1,19 +1,22 @@
1
- import { AxesHelper, BufferGeometry, Color, InstancedMesh, Material, Matrix4, Mesh, MeshBasicMaterial, Object3D, RawShaderMaterial, SkinnedMesh, Texture, Vector4 } from "three";
1
+ import { AxesHelper, Box3, BufferGeometry, Color, InstancedMesh, Material, Matrix4, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, RawShaderMaterial, SkinnedMesh, Sphere, Texture, Vector3, Vector4 } from "three";
2
2
 
3
3
  import { showBalloonWarning } from "../engine/debug/index.js";
4
- import { Gizmos } from "../engine/engine_gizmos.js";
4
+ import { getComponent, getOrAddComponent } from "../engine/engine_components.js";
5
+ import { Gizmos, LabelHandle } from "../engine/engine_gizmos.js";
5
6
  import { $instancingAutoUpdateBounds, $instancingRenderer, InstancingUtil, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
6
7
  import { isLocalNetwork } from "../engine/engine_networking_utils.js";
8
+ import { getRaycastMesh } from "../engine/engine_physics.js";
7
9
  import { serializable } from "../engine/engine_serialization_decorator.js";
8
10
  import { Context, FrameEvent } from "../engine/engine_setup.js";
9
- import { getTempVector } from "../engine/engine_three_utils.js";
10
- import type { IRenderer, ISharedMaterials } from "../engine/engine_types.js";
11
- import { getParam } from "../engine/engine_utils.js";
11
+ import { getTempVector, getWorldDirection, getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js";
12
+ import type { IGameObject, IRenderer, ISharedMaterials } from "../engine/engine_types.js";
13
+ import { getParam, isMobileDevice } from "../engine/engine_utils.js";
12
14
  import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
13
15
  import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects.js";
14
16
  import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
15
17
  import { Behaviour, GameObject } from "./Component.js";
16
18
  import { ReflectionProbe } from "./ReflectionProbe.js";
19
+ import { InstanceHandle, InstancingHandler } from "./RendererInstancing.js"
17
20
  // import { RendererCustomShader } from "./RendererCustomShader.js";
18
21
  import { RendererLightmap } from "./RendererLightmap.js";
19
22
 
@@ -22,8 +25,7 @@
22
25
 
23
26
  const debugRenderer = getParam("debugrenderer");
24
27
  const debugskinnedmesh = getParam("debugskinnedmesh");
25
- const suppressInstancing = getParam("noInstancing");
26
- const debugInstancing = getParam("debuginstancing");
28
+ const suppressInstancing = getParam("noinstancing");
27
29
  const debugProgressiveLoading = getParam("debugprogressive");
28
30
  const suppressProgressiveLoading = getParam("noprogressive");
29
31
 
@@ -200,6 +202,24 @@
200
202
 
201
203
  export class Renderer extends Behaviour implements IRenderer {
202
204
 
205
+ /** Enable or disable instancing for an object. This will create a Renderer component if it does not exist yet.
206
+ * @returns the Renderer component that was created or already existed on the object
207
+ */
208
+ static setInstanced(obj: Object3D, enableInstancing: boolean): Renderer {
209
+ const renderer = getOrAddComponent(obj, Renderer);
210
+ renderer.setInstancingEnabled(enableInstancing);
211
+ return renderer;
212
+ }
213
+
214
+ /** Check if an object is currently rendered using instancing
215
+ * @returns true if the object is rendered using instancing
216
+ */
217
+ static isInstanced(obj: Object3D): boolean {
218
+ const renderer = getComponent(obj, Renderer);
219
+ if (renderer) return renderer.isInstancingActive;
220
+ return InstancingUtil.isUsingInstancing(obj);
221
+ }
222
+
203
223
  /** Set the rendering state only of an object (makes it visible or invisible) without affecting component state or child hierarchy visibility! You can also just enable/disable the Renderer component on that object for the same effect!
204
224
  *
205
225
  * If you want to activate or deactivate a complete object you can use obj.visible as usual (it acts the same as setActive in Unity) */
@@ -215,8 +235,12 @@
215
235
  lightmapIndex: number = -1;
216
236
  @serializable(Vector4)
217
237
  lightmapScaleOffset: Vector4 = new Vector4(1, 1, 0, 0);
238
+ /** If the renderer should use instancing
239
+ * If this is a boolean (true) all materials will be instanced or (false) none of them.
240
+ * If this is an array of booleans the materials will be instanced based on the index of the material.
241
+ */
218
242
  @serializable()
219
- enableInstancing: boolean[] | undefined = undefined;
243
+ enableInstancing: boolean | boolean[] | undefined = undefined;
220
244
  @serializable()
221
245
  renderOrder: number[] | undefined = undefined;
222
246
  @serializable()
@@ -363,6 +387,8 @@
363
387
 
364
388
  public allowProgressiveLoading: boolean = true;
365
389
 
390
+ private _firstFrame: number = -1;
391
+
366
392
  registering() {
367
393
  if (!this.enabled) {
368
394
  this.setVisibility(false);
@@ -370,6 +396,8 @@
370
396
  }
371
397
 
372
398
  awake() {
399
+ this._firstFrame = this.context.time.frame;
400
+
373
401
  if (debugRenderer) console.log("Renderer ", this.name, this);
374
402
  this.clearInstancingState();
375
403
 
@@ -480,17 +508,23 @@
480
508
  private _isInstancingEnabled: boolean = false;
481
509
  private handles: InstanceHandle[] | null | undefined = undefined;
482
510
 
483
- private clearInstancingState() {
484
- this._isInstancingEnabled = false;
485
- this.handles = undefined;
511
+ /**
512
+ * @returns true if this renderer has instanced objects
513
+ */
514
+ get isInstancingActive() {
515
+ return this.handles != undefined && this.handles.length > 0 && this._isInstancingEnabled;
486
516
  }
487
517
 
518
+ /** Enable or disable instancing for this renderer.
519
+ * @param enabled true to enable instancing, false to disable it
520
+ */
488
521
  setInstancingEnabled(enabled: boolean): boolean {
489
522
  if (this._isInstancingEnabled === enabled) return enabled && (this.handles === undefined || this.handles != null && this.handles.length > 0);
490
523
  this._isInstancingEnabled = enabled;
491
524
  if (enabled) {
525
+ if (this.enableInstancing === undefined) this.enableInstancing = true;
492
526
  if (this.handles === undefined) {
493
- this.handles = instancing.setup(this, this.gameObject, this.context, null, { rend: this, foundMeshes: 0, useMatrixWorldAutoUpdate: this.useInstanceMatrixWorldAutoUpdate() });
527
+ this.handles = InstancingHandler.instance.setup(this, this.gameObject, this.context, null, { rend: this, foundMeshes: 0, useMatrixWorldAutoUpdate: this.useInstanceMatrixWorldAutoUpdate() });
494
528
  if (this.handles) {
495
529
  GameObject.markAsInstancedRendered(this.gameObject, true);
496
530
  return true;
@@ -508,7 +542,7 @@
508
542
  else {
509
543
  if (this.handles) {
510
544
  for (const handler of this.handles) {
511
- handler.remove();
545
+ handler.remove(this.destroyed);
512
546
  }
513
547
  }
514
548
  return true;
@@ -517,6 +551,11 @@
517
551
  return false;
518
552
  }
519
553
 
554
+ private clearInstancingState() {
555
+ this._isInstancingEnabled = false;
556
+ this.handles = undefined;
557
+ }
558
+
520
559
  /** Return true to wrap matrix update events for instanced rendering to update instance matrices automatically when matrixWorld changes
521
560
  * This is a separate method to be overrideable from user code
522
561
  */
@@ -546,8 +585,8 @@
546
585
 
547
586
  this.setVisibility(true);
548
587
 
549
- if (this._isInstancingEnabled) {
550
- this.setInstancingEnabled(true);
588
+ if (this._isInstancingEnabled || this.enableInstancing) {
589
+ if (this.__internalDidAwakeAndStart) this.setInstancingEnabled(true);
551
590
  }
552
591
  else if (this.enabled) {
553
592
  // this.gameObject.visible = true;
@@ -555,6 +594,8 @@
555
594
  }
556
595
 
557
596
  this.updateReflectionProbe();
597
+
598
+ this.testIfLODLevelsAreAvailable();
558
599
  }
559
600
 
560
601
  onDisable() {
@@ -578,11 +619,6 @@
578
619
  }
579
620
 
580
621
  }
581
-
582
- applyStencil() {
583
- NEEDLE_render_objects.applyStencil(this);
584
- }
585
-
586
622
  onBeforeRender() {
587
623
  if (!this.gameObject) {
588
624
  return;
@@ -613,14 +649,13 @@
613
649
  // console.log(this.name, this.gameObject.matrixWorldNeedsUpdate);
614
650
  const needsUpdate: boolean = this.gameObject[NEED_UPDATE_INSTANCE_KEY] === true;// || this.gameObject.matrixWorldNeedsUpdate;
615
651
  if (needsUpdate) {
616
- if (debugInstancing)
617
- console.log("UPDATE INSTANCED MATRICES", this.context.time.frame);
652
+ // if (debugInstancing) console.log("UPDATE INSTANCED MATRICES at frame #" + this.context.time.frame);
618
653
  this.gameObject[NEED_UPDATE_INSTANCE_KEY] = false;
619
654
  const remove = false;// Math.random() < .01;
620
655
  for (let i = this.handles.length - 1; i >= 0; i--) {
621
656
  const h = this.handles[i];
622
657
  if (remove) {
623
- h.remove();
658
+ h.remove(this.destroyed);
624
659
  this.handles.splice(i, 1);
625
660
  }
626
661
  else
@@ -641,9 +676,16 @@
641
676
  }
642
677
  }
643
678
 
644
- for (const mat of this.sharedMaterials) {
645
- if (mat)
646
- this.loadProgressiveTextures(mat);
679
+ if (this._wasVisible && this.allowProgressiveLoading) {
680
+ if (this.automaticallyUpdateLODLevel) {
681
+ this.updateLODs();
682
+ }
683
+ // else {
684
+ // for (const mat of this.sharedMaterials) {
685
+ // if (!mat) continue;
686
+ // this.loadProgressiveTextures(mat, 0)
687
+ // }
688
+ // }
647
689
  }
648
690
 
649
691
  if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
@@ -652,9 +694,15 @@
652
694
 
653
695
  }
654
696
 
697
+ /** true when the object was rendered - this is used to start loading LOD levels */
698
+ private _wasVisible = false;
655
699
 
656
700
  private onBeforeRenderThree = (_renderer, _scene, _camera, _geometry, material, _group) => {
657
701
 
702
+ if (!this._wasVisible && this.context.time.frame > this._firstFrame + 1) {
703
+ if (debugProgressiveLoading) console.debug("onBeforeRenderThree: Object becomes visible for the first time", this.name, this.context.time.frame);
704
+ this._wasVisible = true;
705
+ }
658
706
 
659
707
  if (material.envMapIntensity !== undefined) {
660
708
  const factor = this.hasLightmap ? Math.PI : 1;
@@ -707,6 +755,7 @@
707
755
  if (this._isInstancingEnabled && this.handles) {
708
756
  for (let i = 0; i < this.handles.length; i++) {
709
757
  const handle = this.handles[i];
758
+ this._wasVisible = true;
710
759
  setCustomVisibility(handle.object, true);
711
760
  }
712
761
  }
@@ -714,24 +763,285 @@
714
763
  if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
715
764
  this._reflectionProbe.onUnset(this)
716
765
  }
766
+
767
+ if (debugProgressiveLoading && this._lastLodLevel >= 0)
768
+ this.drawGizmoLodLevel(false);
717
769
  }
718
770
 
719
- loadProgressiveTextures(material: Material) {
771
+ private testIfLODLevelsAreAvailable() {
772
+ this.automaticallyUpdateLODLevel = false;
773
+ this.hasMeshLODs = false;
774
+ this.hasTextureLODs = false;
775
+
776
+ for (const mesh of this.sharedMeshes) {
777
+ if (NEEDLE_progressive.hasLODLevelAvailable(mesh)) {
778
+ this.automaticallyUpdateLODLevel = true;
779
+ this.hasMeshLODs = true;
780
+ break;
781
+ }
782
+ }
783
+ for (const mat of this.sharedMaterials) {
784
+ if (!mat) continue;
785
+ if (NEEDLE_progressive.hasLODLevelAvailable(mat)) {
786
+ this.automaticallyUpdateLODLevel = true;
787
+ this.hasTextureLODs = true;
788
+ break;
789
+ }
790
+ }
791
+ if (debugProgressiveLoading) {
792
+ console.log(`${this.name}\nMesh LODs available? ${this.hasMeshLODs ? "YES" : "NO"}\nTexture LODs available? ${this.hasTextureLODs ? "YES" : "NO"}`);
793
+ }
794
+ }
795
+
796
+ /** Enable to automatically update the LOD level for the renderers meshes and materials.
797
+ * When disabled you can manually call `updateLODLevels` to update the LOD levels.
798
+ */
799
+ automaticallyUpdateLODLevel = true;
800
+ private hasTextureLODs: boolean = true;
801
+ private hasMeshLODs: boolean = true;
802
+
803
+ /** Update the LOD levels for the renderer. */
804
+ updateLODs() {
805
+ let level = 0;
806
+ // we currently only support auto LOD changes for meshes
807
+ if (this.hasMeshLODs) {
808
+ level = this.calculateLodLevel();
809
+ }
810
+
811
+ // TODO: we currently can not switch texture lods because we need better caching for the textures internally (see copySettings in progressive + NE-4431)
812
+ const textureLOD = 0;// Math.max(0, LODlevel - 2)
813
+
814
+ for (const mat of this.sharedMaterials) {
815
+ if (mat) this.loadProgressiveTextures(mat, textureLOD);
816
+ }
817
+
818
+ if (level >= 0) {
819
+ for (let i = 0; i < this.sharedMeshes.length; i++) {
820
+ const mesh = this.sharedMeshes[i];
821
+ if (!mesh) continue;
822
+ this.loadProgressiveMeshes(mesh, level);
823
+ }
824
+ }
825
+ }
826
+
827
+ private _lastLodLevel = -1;
828
+ private _nextLodTestTime = 0;
829
+ private _randomLodLevelCheckFrameOffset = Math.floor(Math.random() * 100);
830
+ private readonly _sphere = new Sphere();
831
+ private readonly _box = new Box3();
832
+ private static readonly tempMatrix = new Matrix4();
833
+
834
+ private calculateLodLevel(force: boolean = false): number {
835
+
836
+ // if this is using instancing we always load level 0
837
+ // if (this.isInstancingActive) return 0;
838
+
839
+ const interval = 3;
840
+ if (!force && (this.context.time.frame + this._randomLodLevelCheckFrameOffset) % interval != 0) {
841
+ return this._lastLodLevel;
842
+ }
843
+
844
+ if (this.context.time.realtimeSinceStartup < this._nextLodTestTime) {
845
+ return this._lastLodLevel;
846
+ }
847
+
848
+ const maxLevel = 10;
849
+ let level = maxLevel + 1;
850
+
851
+ if (this.context.mainCamera) {
852
+
853
+ const isLowPerformanceDevice = isMobileDevice();
854
+ const lod_0_threshold = isLowPerformanceDevice ? .6 : .4;
855
+
856
+
857
+ // TODO: we should save the LOD level in the shared mesh and not just calculate one level per renderer
858
+ for (const mesh of this.sharedMeshes) {
859
+ if (level <= 0) break;
860
+ if (!mesh) continue;
861
+
862
+ if (debugProgressiveLoading && mesh["DEBUG:LOD"] != undefined) {
863
+ return this._lastLodLevel = mesh["DEBUG:LOD"];
864
+ }
865
+
866
+ let meshDensity = 0;
867
+ // TODO: the mesh info contains also the density for all available LOD level so we can use this for selecting which level to show
868
+ const lodsInfo = NEEDLE_progressive.getMeshLODInformation(mesh.geometry);
869
+ // TODO: the substraction here is not clear - it should be some sort of mesh density value
870
+ if (lodsInfo?.density != undefined)
871
+ meshDensity = (Math.log2(lodsInfo.density || 0) / 2) - 6;
872
+
873
+ // TODO: we can skip all this if we dont have any LOD information - we can ask the progressive extension for that
874
+ const frustum = this.context.mainCameraComponent?.getFrustum();
875
+ if (!frustum?.intersectsObject(mesh)) {
876
+ if (debugProgressiveLoading && mesh.geometry.boundingSphere) {
877
+ const bounds = mesh.geometry.boundingSphere;
878
+ this._sphere.copy(bounds);
879
+ this._sphere.applyMatrix4(mesh.matrixWorld);
880
+ Gizmos.DrawWireSphere(this._sphere.center, this._sphere.radius * 1.01, 0xff5555, .5);
881
+ }
882
+ // the object is not visible by the camera
883
+ continue;
884
+ }
885
+
886
+ const box = mesh.geometry.boundingBox;
887
+ if (box && this.context.mainCamera instanceof PerspectiveCamera) {
888
+
889
+ // hack: if the mesh has vertex colors, has less than 100 vertices we always select the highest LOD
890
+ if (mesh.geometry.attributes.color && mesh.geometry.attributes.color.count < 100) {
891
+ if (mesh.geometry.boundingSphere) {
892
+ this._sphere.copy(mesh.geometry.boundingSphere);
893
+ this._sphere.applyMatrix4(mesh.matrixWorld);
894
+ if (this._sphere.containsPoint(getWorldPosition(this.context.mainCamera))) {
895
+ level = 0;
896
+ break;
897
+ }
898
+ }
899
+ }
900
+
901
+ // calculate size on screen
902
+ this._box.copy(box);
903
+ this._box.applyMatrix4(mesh.matrixWorld);
904
+ this._box.applyMatrix4(this.context.mainCameraComponent!.getProjectionScreenMatrix(Renderer.tempMatrix));
905
+ // Gizmos.DrawWireBox(this._box.getCenter(getTempVector()), this._box.getSize(getTempVector()), 0x00ff00, .02);
906
+ const boxSize = this._box.getSize(getTempVector());
907
+ // const verticalFov = this.context.mainCamera.fov;
908
+ let screenSize = boxSize.y / 2;// / (2 * Math.atan(Math.PI * verticalFov / 360));
909
+ // if (mesh.name.startsWith("Statue_")) console.log(mesh.name, "ScreenSize", screenSize, "Density", meshDensity, mesh.geometry.index!.count / 3);
910
+ screenSize /= Math.pow(2, meshDensity - .5);
911
+
912
+ // screenSize *= .2;
913
+
914
+ let expectedLevel = 999;
915
+ let threshold = lod_0_threshold;
916
+ for (let l = 0; l < maxLevel; l++) {
917
+ if (screenSize > threshold) {
918
+ expectedLevel = l;
919
+ break;
920
+ }
921
+ threshold /= 2;
922
+ }
923
+ // expectedLevel -= meshDensity - 5;
924
+ // expectedLevel += meshDensity;
925
+ const isLowerLod = expectedLevel < level;
926
+ if (isLowerLod)
927
+ level = expectedLevel;
928
+ }
929
+ }
930
+ }
931
+
932
+ level = Math.round(level);
933
+
934
+ if (this._lastLodLevel != level) {
935
+ this._nextLodTestTime = this.context.time.realtimeSinceStartup + .5;
936
+ if (debugProgressiveLoading) {
937
+ if (debugProgressiveLoading == "verbose") console.warn(`LOD Level changed from ${this._lastLodLevel} to ${level} for ${this.name}`);
938
+ this.drawGizmoLodLevel(true);
939
+ }
940
+
941
+ }
942
+ this._lastLodLevel = level;
943
+ return level;
944
+ }
945
+
946
+ private drawGizmoLodLevel(changed: boolean) {
947
+ const level = this._lastLodLevel;
948
+ const camForward = (this.context.mainCamera as any as IGameObject).worldForward;
949
+ const camWorld = (this.context.mainCamera as any as IGameObject).worldPosition;
950
+ for (const mesh of this.sharedMeshes) {
951
+ if (!mesh) continue;
952
+ if (mesh.geometry.boundingSphere) {
953
+ const bounds = mesh.geometry.boundingSphere;
954
+ this._sphere.copy(bounds);
955
+ this._sphere.applyMatrix4(mesh.matrixWorld);
956
+ const boundsCenter = this._sphere.center;
957
+ const radius = this._sphere.radius;
958
+ const colors = ["#76c43e", "#bcc43e", "#c4ac3e", "#c4673e", "#ff3e3e"];
959
+ // if the lod has changed we just want to draw the gizmo for the changed mesh
960
+ if (changed) {
961
+ Gizmos.DrawWireSphere(boundsCenter, radius, colors[level], .1);
962
+ }
963
+ else {
964
+ // let helper = mesh["LOD_level_label"] as LabelHandle | null
965
+ const text = "LOD " + level + "\n" + mesh.geometry.index!.count / 3;
966
+ // if (helper) {
967
+ // helper?.setText(text);
968
+ // continue;
969
+ // }
970
+ const pos = getTempVector(camForward).multiplyScalar(radius * .7).add(boundsCenter);
971
+ const distance = pos.distanceTo(camWorld);
972
+ const vertexCount = mesh.geometry.index!.count / 3;
973
+ // const vertexCountFactor = Math.min(1, vertexCount / 1000);
974
+ const col = colors[Math.min(colors.length - 1, level)] + "88";
975
+ const size = Math.min(10, radius);
976
+ Gizmos.DrawLabel(pos, text, distance * .001 + size * .03, undefined, 0xffffff, col);
977
+ // mesh["LOD_level_label"] = helper;
978
+ }
979
+
980
+ }
981
+ }
982
+ }
983
+
984
+ /** Applies stencil settings for this renderer's objects (if stencil settings are available) */
985
+ applyStencil() {
986
+ NEEDLE_render_objects.applyStencil(this);
987
+ }
988
+
989
+
990
+ /** Load progressive textures for the given material
991
+ * @param material the material to load the textures for
992
+ * @param level the LOD level to load. Level 0 is the best quality, higher levels are lower quality
993
+ * @returns Promise with true if the LOD was loaded, false if not
994
+ */
995
+ loadProgressiveTextures(material: Material, level: number) {
720
996
  // progressive load before rendering so we only load textures for visible materials
721
997
  if (!suppressProgressiveLoading && material) {
722
-
723
- if (material["_didRequestTextureLOD"] === undefined && this.allowProgressiveLoading) {
724
- material["_didRequestTextureLOD"] = 0;
725
- if (debugProgressiveLoading) {
726
- console.log("Load material LOD", material.name);
998
+ if (this.allowProgressiveLoading) {
999
+ if (!material.userData) material.userData = {};
1000
+ if (material.userData.LOD !== level) {
1001
+ material.userData.LOD = level;
1002
+ return NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, material, level);
727
1003
  }
728
- return NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, material);
729
1004
  }
730
1005
  }
731
-
732
1006
  return Promise.resolve(true);
733
1007
  }
734
1008
 
1009
+ /** Load progressive meshes for the given mesh
1010
+ * @param mesh the mesh to load the LOD for
1011
+ * @param index the index of the mesh if it's part of a group
1012
+ * @param level the LOD level to load. Level 0 is the best quality, higher levels are lower quality
1013
+ * @returns Promise with true if the LOD was loaded, false if not
1014
+ */
1015
+ loadProgressiveMeshes(mesh: Mesh, level: number) {
1016
+ if (!suppressProgressiveLoading && mesh) {
1017
+ if (this.allowProgressiveLoading) {
1018
+ if (!mesh.userData) mesh.userData = {};
1019
+ if (mesh.userData.LOD !== level) {
1020
+ mesh.userData.LOD = level;
1021
+ const originalGeometry = mesh.geometry;
1022
+ return NEEDLE_progressive.assignMeshLOD(this.context, this.sourceId!, mesh, level).then(res => {
1023
+ if (res && mesh.userData.LOD == level && originalGeometry != mesh.geometry) {
1024
+ // update the lightmap
1025
+ this.applyLightmapping();
1026
+
1027
+ if (this.handles) {
1028
+ for (const inst of this.handles) {
1029
+ // if (inst["LOD"] < level) continue;
1030
+ // inst["LOD"] = level;
1031
+ inst.setGeometry(mesh.geometry);
1032
+ }
1033
+ }
1034
+ }
1035
+ })
1036
+ }
1037
+ }
1038
+ }
1039
+ return true;
1040
+ }
1041
+
1042
+ /** Apply the settings of this renderer to the given object
1043
+ * Settings include shadow casting and receiving (e.g. this.receiveShadows, this.shadowCastingMode)
1044
+ */
735
1045
  applySettings(go: Object3D) {
736
1046
  go.receiveShadow = this.receiveShadows;
737
1047
  if (this.shadowCastingMode == ShadowCastingMode.On) {
@@ -785,21 +1095,52 @@
785
1095
  }
786
1096
 
787
1097
  export class SkinnedMeshRenderer extends MeshRenderer {
1098
+
1099
+ private _needUpdateBoundingSphere = false;
1100
+ // private _lastWorldPosition = new Vector3();
1101
+
788
1102
  awake() {
789
1103
  super.awake();
790
1104
  // disable skinned mesh occlusion because of https://github.com/mrdoob/js/issues/14499
791
1105
  this.allowOcclusionWhenDynamic = false;
792
1106
  // If we don't do that here the bounding sphere matrix used for raycasts will be wrong. Not sure *why* this is necessary
793
1107
  this.gameObject.parent?.updateWorldMatrix(false, true);
1108
+ this.markBoundsDirty();
794
1109
  }
795
- onBeforeRender(): void {
796
- super.onBeforeRender();
1110
+ onAfterRender(): void {
1111
+ super.onAfterRender();
797
1112
 
1113
+ // this.gameObject.parent.position.x += Math.sin(this.context.time.time) * .01;
1114
+
1115
+ // if (this.gameObject instanceof SkinnedMesh && this.gameObject.geometry.boundingSphere) {
1116
+ // const bounds = this.gameObject.geometry.boundingSphere;
1117
+ // const worldpos = getTempVector().setFromMatrixPosition(this.gameObject.matrixWorld);
1118
+ // if (worldpos.distanceTo(this._lastWorldPosition) > bounds.radius) {
1119
+ // this._lastWorldPosition.copy(worldpos);
1120
+ // this.markBoundsDirty();
1121
+ // };
1122
+ // }
1123
+
1124
+ if (this.gameObject instanceof SkinnedMesh && this._needUpdateBoundingSphere) {
1125
+ this._needUpdateBoundingSphere = false;
1126
+ const geometry = this.gameObject.geometry;
1127
+ const raycastmesh = getRaycastMesh(this.gameObject);
1128
+ if (raycastmesh) this.gameObject.geometry = raycastmesh;
1129
+ this.gameObject.computeBoundingSphere();
1130
+ this.gameObject.geometry = geometry;
1131
+ }
1132
+
1133
+ // if (this.context.time.frame % 30 === 0) this.markBoundsDirty();
1134
+
798
1135
  if (debugskinnedmesh && this.gameObject instanceof SkinnedMesh && this.gameObject.boundingSphere) {
799
1136
  const tempCenter = getTempVector(this.gameObject.boundingSphere.center).applyMatrix4(this.gameObject.matrixWorld);
800
1137
  Gizmos.DrawWireSphere(tempCenter, this.gameObject.boundingSphere.radius, "red");
801
1138
  }
802
1139
  }
1140
+
1141
+ markBoundsDirty() {
1142
+ this._needUpdateBoundingSphere = true;
1143
+ }
803
1144
  }
804
1145
 
805
1146
  export enum ShadowCastingMode {
@@ -820,333 +1161,3 @@
820
1161
  /// </summary>
821
1162
  ShadowsOnly,
822
1163
  }
823
-
824
-
825
-
826
- declare class InstancingSetupArgs {
827
- rend: Renderer;
828
- foundMeshes: number;
829
- useMatrixWorldAutoUpdate: boolean;
830
- };
831
-
832
- class InstancingHandler {
833
-
834
- public objs: InstancedMeshRenderer[] = [];
835
-
836
- public setup(renderer: Renderer, obj: Object3D, context: Context, handlesArray: InstanceHandle[] | null, args: InstancingSetupArgs, level: number = 0)
837
- : InstanceHandle[] | null {
838
-
839
- // make sure setting casting settings are applied so when we add the mesh to the InstancedMesh we can ask for the correct cast shadow setting
840
- renderer.applySettings(obj);
841
- const res = this.tryCreateOrAddInstance(obj, context, args);
842
- if (res) {
843
- renderer.loadProgressiveTextures(res.instancer.material);
844
- if (handlesArray === null) handlesArray = [];
845
- handlesArray.push(res);
846
- }
847
-
848
- else if (level <= 0 && obj.type !== "Mesh") {
849
- const nextLevel = level + 1;
850
- for (const ch of obj.children) {
851
- handlesArray = this.setup(renderer, ch, context, handlesArray, args, nextLevel);
852
- }
853
- }
854
-
855
- if (level === 0) {
856
- // For multi material objects we only want to track the root object's matrix
857
- if (args.useMatrixWorldAutoUpdate && handlesArray && handlesArray.length >= 0) {
858
- this.autoUpdateInstanceMatrix(obj);
859
- }
860
- }
861
-
862
- return handlesArray;
863
- }
864
-
865
- private tryCreateOrAddInstance(obj: Object3D, context: Context, args: InstancingSetupArgs): InstanceHandle | null {
866
- if (obj.type === "Mesh") {
867
- const index = args.foundMeshes;
868
- args.foundMeshes += 1;
869
- if (!args.rend.enableInstancing) return null;
870
- if (index >= args.rend.enableInstancing.length) {
871
- // console.error("Something is wrong with instance setup", obj, args.rend.enableInstancing, index);
872
- return null;
873
- }
874
- if (!args.rend.enableInstancing[index]) {
875
- // instancing is disabled
876
- // console.log("Instancing is disabled", obj);
877
- return null;
878
- }
879
- // instancing is enabled:
880
- const mesh = obj as Mesh;
881
- const geo = mesh.geometry as BufferGeometry;
882
- const mat = mesh.material as Material;
883
-
884
- for (const i of this.objs) {
885
- if (i.isFull()) continue;
886
- if (i.geo === geo && i.material === mat) {
887
- const handle = i.addInstance(mesh);
888
- return handle;
889
- }
890
- }
891
- // console.log("Add new instance mesh renderer", obj);
892
- const i = new InstancedMeshRenderer(obj.name, geo, mat, 200, context);
893
- this.objs.push(i);
894
- const handle = i.addInstance(mesh);
895
- return handle;
896
- }
897
- return null;
898
- }
899
-
900
- private autoUpdateInstanceMatrix(obj: Object3D) {
901
- const original = obj.matrixWorld["multiplyMatrices"].bind(obj.matrixWorld);
902
- const previousMatrix: Matrix4 = obj.matrixWorld.clone();
903
- const matrixChangeWrapper = (a: Matrix4, b: Matrix4) => {
904
- const newMatrixWorld = original(a, b);
905
- if (obj[NEED_UPDATE_INSTANCE_KEY] || previousMatrix.equals(newMatrixWorld) === false) {
906
- previousMatrix.copy(newMatrixWorld)
907
- obj[NEED_UPDATE_INSTANCE_KEY] = true;
908
- }
909
- return newMatrixWorld;
910
- };
911
- obj.matrixWorld["multiplyMatrices"] = matrixChangeWrapper;
912
- // wrap matrixWorldNeedsUpdate
913
- // let originalMatrixWorldNeedsUpdate = obj.matrixWorldNeedsUpdate;
914
- // Object.defineProperty(obj, "matrixWorldNeedsUpdate", {
915
- // get: () => {
916
- // return originalMatrixWorldNeedsUpdate;
917
- // },
918
- // set: (value: boolean) => {
919
- // if(value) console.warn("SET MATRIX WORLD NEEDS UPDATE");
920
- // originalMatrixWorldNeedsUpdate = value;
921
- // }
922
- // });
923
- }
924
- }
925
- const instancing: InstancingHandler = new InstancingHandler();
926
-
927
- class InstanceHandle {
928
-
929
- get name(): string {
930
- return this.object.name;
931
- }
932
-
933
- instanceIndex: number = -1;
934
- object: Mesh;
935
- instancer: InstancedMeshRenderer;
936
-
937
- constructor(instanceIndex: number, originalObject: Mesh, instancer: InstancedMeshRenderer) {
938
- this.instanceIndex = instanceIndex;
939
- this.object = originalObject;
940
- this.instancer = instancer;
941
- originalObject[$instancingRenderer] = instancer;
942
- GameObject.markAsInstancedRendered(originalObject, true);
943
- }
944
-
945
- updateInstanceMatrix(updateChildren: boolean = false) {
946
- if (this.instanceIndex < 0) return;
947
- this.object.updateWorldMatrix(true, updateChildren);
948
- this.instancer.updateInstance(this.object.matrixWorld, this.instanceIndex);
949
- }
950
-
951
- setMatrix(matrix: Matrix4) {
952
- if (this.instanceIndex < 0) return;
953
- this.instancer.updateInstance(matrix, this.instanceIndex);
954
- }
955
-
956
- add() {
957
- if (this.instanceIndex >= 0) return;
958
- this.instancer.add(this);
959
- }
960
-
961
- remove() {
962
- if (this.instanceIndex < 0) return;
963
- this.instancer.remove(this);
964
- }
965
- }
966
-
967
- class InstancedMeshRenderer {
968
- /** The three instanced mesh
969
- * @link https://threejs.org/docs/#api/en/objects/InstancedMesh
970
- */
971
- get mesh() {
972
- return this.inst;
973
- }
974
- get visible(): boolean {
975
- return this.inst.visible;
976
- }
977
- set visible(val: boolean) {
978
- this.inst.visible = val;
979
- }
980
- get castShadow(): boolean {
981
- return this.inst.castShadow;
982
- }
983
- set castShadow(val: boolean) {
984
- this.inst.castShadow = val;
985
- }
986
- set receiveShadow(val: boolean) {
987
- this.inst.receiveShadow = val;
988
- }
989
-
990
- updateBounds(box: boolean = true, sphere: boolean = true) {
991
- this._needUpdateBounds = false;
992
- if (box)
993
- this.inst.computeBoundingBox();
994
- if (sphere)
995
- this.inst.computeBoundingSphere();
996
- }
997
-
998
- public name: string = "";
999
- public geo: BufferGeometry;
1000
- public material: Material;
1001
- get currentCount(): number { return this.inst.count; }
1002
-
1003
- private context: Context;
1004
- private inst: InstancedMesh;
1005
- private handles: (InstanceHandle | null)[] = [];
1006
- private maxCount: number;
1007
-
1008
- private static nullMatrix: Matrix4 = new Matrix4();
1009
-
1010
- isFull(): boolean {
1011
- return this.currentCount >= this.maxCount;
1012
- }
1013
-
1014
- private _needUpdateBounds: boolean = false;
1015
-
1016
- constructor(name: string, geo: BufferGeometry, material: Material, count: number, context: Context) {
1017
- this.name = name;
1018
- this.geo = geo;
1019
- this.material = material;
1020
- this.context = context;
1021
- this.maxCount = count;
1022
- if (debugInstancing) {
1023
- material = new MeshBasicMaterial({ color: this.randomColor() });
1024
- }
1025
- this.inst = new InstancedMesh(geo, material, count);
1026
- this.inst[$instancingAutoUpdateBounds] = true;
1027
- this.inst.count = 0;
1028
- this.inst.visible = true;
1029
- this.context.scene.add(this.inst);
1030
-
1031
- // Not handled by RawShaderMaterial, so we need to set the define explicitly.
1032
- // Edge case: theoretically some users of the material could use it in an
1033
- // instanced fashion, and some others not. In that case, the material would not
1034
- // be able to be shared between the two use cases. We could probably add a
1035
- // onBeforeRender call for the InstancedMesh and set the define there.
1036
- // Same would apply if we support skinning -
1037
- // there we would have to split instanced batches so that the ones using skinning
1038
- // are all in the same batch.
1039
- if (material instanceof RawShaderMaterial) {
1040
- material.defines["USE_INSTANCING"] = true;
1041
- material.needsUpdate = true;
1042
- }
1043
-
1044
- context.pre_render_callbacks.push(this.onBeforeRender);
1045
- context.post_render_callbacks.push(this.onAfterRender);
1046
- }
1047
-
1048
- private onBeforeRender = () => {
1049
- // ensure the instanced mesh is rendered / has correct layers
1050
- this.inst.layers.enableAll();
1051
-
1052
- if (this._needUpdateBounds && this.inst[$instancingAutoUpdateBounds] === true) {
1053
- if (debugInstancing)
1054
- console.log("Update instancing bounds", this.name, this.inst.matrixWorldNeedsUpdate);
1055
- this.updateBounds();
1056
- }
1057
- }
1058
- private onAfterRender = () => {
1059
- // hide the instanced mesh again when its not being rendered (for raycasting we still use the original object)
1060
- this.inst.layers.disableAll();
1061
- }
1062
-
1063
- private randomColor() {
1064
- return new Color(Math.random(), Math.random(), Math.random());
1065
- }
1066
-
1067
- addInstance(obj: Mesh): InstanceHandle | null {
1068
- if (this.currentCount >= this.maxCount) {
1069
- console.error("TOO MANY INSTANCES - resize is not yet implemented!", this.inst.count); // todo: make it resize
1070
- return null;
1071
- }
1072
- const handle = new InstanceHandle(-1, obj, this);
1073
-
1074
- if (obj.castShadow === true && this.inst.castShadow === false) {
1075
- this.inst.castShadow = true;
1076
- }
1077
- if (obj.receiveShadow === true && this.inst.receiveShadow === false) {
1078
- this.inst.receiveShadow = true;
1079
- }
1080
-
1081
- this.add(handle);
1082
- return handle;
1083
- }
1084
-
1085
-
1086
- add(handle: InstanceHandle) {
1087
- if (handle.instanceIndex < 0) {
1088
- handle.instanceIndex = this.currentCount;
1089
- // console.log(handle.instanceIndex, this.currentCount);
1090
- if (handle.instanceIndex >= this.handles.length)
1091
- this.handles.push(handle);
1092
- else this.handles[handle.instanceIndex] = handle;
1093
- }
1094
- // console.log("Handle instance");
1095
- handle.object.updateWorldMatrix(true, true);
1096
- this.inst.setMatrixAt(handle.instanceIndex, handle.object.matrixWorld);
1097
- this.inst.instanceMatrix.needsUpdate = true;
1098
- this.inst.count += 1;
1099
- this._needUpdateBounds = true;
1100
-
1101
- if (this.inst.count > 0)
1102
- this.inst.visible = true;
1103
-
1104
- if (debugInstancing) console.log("Added", this.name, this.inst.count);
1105
- }
1106
-
1107
- remove(handle: InstanceHandle) {
1108
- if (!handle) return;
1109
- if (handle.instanceIndex < 0 || handle.instanceIndex >= this.handles.length || this.inst.count <= 0) {
1110
- return;
1111
- }
1112
- if (this.handles[handle.instanceIndex] !== handle) {
1113
- console.error("instance handle is not part of renderer, was it removed before?", handle.instanceIndex, this.name);
1114
- const index = this.handles.indexOf(handle);
1115
- if (index < 0)
1116
- return;
1117
- handle.instanceIndex = index;
1118
- }
1119
- this.handles[handle.instanceIndex] = null;
1120
- this.inst.setMatrixAt(handle.instanceIndex, InstancedMeshRenderer.nullMatrix);
1121
- const removedLastElement = handle.instanceIndex >= this.currentCount - 1;
1122
- // console.log(removedLastElement, this.currentCount, handle.instanceIndex, this.handles);
1123
- if (!removedLastElement && this.currentCount > 0) {
1124
- const lastElement = this.handles[this.currentCount - 1];
1125
- if (lastElement) {
1126
- lastElement.instanceIndex = handle.instanceIndex;
1127
- lastElement.updateInstanceMatrix();
1128
- this.handles[handle.instanceIndex] = lastElement;
1129
- this.handles[this.currentCount - 1] = null;
1130
- // this.inst.setMatrixAt(handle.instanceIndex, lastElement.object.matrixWorld);
1131
- // this.inst.setMatrixAt(this.currentCount - 1, InstancedMeshRenderer.nullMatrix);
1132
- }
1133
- }
1134
-
1135
- if (this.inst.count > 0)
1136
- this.inst.count -= 1;
1137
- handle.instanceIndex = -1;
1138
- this._needUpdateBounds = true;
1139
-
1140
- if (this.inst.count <= 0)
1141
- this.inst.visible = false;
1142
-
1143
- this.inst.instanceMatrix.needsUpdate = true;
1144
- if (debugInstancing) console.log("Removed", this.name, this.inst.count);
1145
- }
1146
-
1147
- updateInstance(mat: Matrix4, index: number) {
1148
- this.inst.setMatrixAt(index, mat);
1149
- this.inst.instanceMatrix.needsUpdate = true;
1150
- this._needUpdateBounds = true;
1151
- }
1152
- }
src/engine-components/SpriteRenderer.ts CHANGED
@@ -118,7 +118,7 @@
118
118
  if (!slice._hasLoadedProgressive) {
119
119
  slice._hasLoadedProgressive = true;
120
120
  const previousTexture = tex;
121
- NEEDLE_progressive.assignTextureLOD(context, sourceId, tex, 0).then(res => {
121
+ NEEDLE_progressive.assignTextureLOD(context, sourceId!, tex, 0).then(res => {
122
122
  if (res instanceof Texture) {
123
123
  slice.texture = res;
124
124
  const shouldUpdateInMaterial = material?.["map"] === previousTexture;
@@ -237,7 +237,7 @@
237
237
  }
238
238
  this.sharedMaterial = mat;
239
239
  this._currentSprite = new Mesh(SpriteUtils.getOrCreateGeometry(sprite), mat);
240
- NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, mat, 0);
240
+ NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, mat, 0);
241
241
  }
242
242
  else {
243
243
  this._currentSprite.geometry = SpriteUtils.getOrCreateGeometry(sprite);
src/engine/extensions/usage_tracker.ts CHANGED
@@ -62,9 +62,13 @@
62
62
  if (loaded instanceof Object3D) {
63
63
  if (!loaded.parent) {
64
64
  if (loaded instanceof Mesh) {
65
- if (debug) console.warn("> GLTF LOADER: Mesh not used in scene!", loaded);
66
- loaded.material = null;
67
- loaded.geometry = null;
65
+ // we need to delay this for other plugins to use the mesh
66
+ // TODO: do we even need to do this?
67
+ setTimeout(() => {
68
+ if (debug) console.warn("> GLTF LOADER: Mesh not used in scene!", loaded);
69
+ loaded.material = null;
70
+ loaded.geometry = null;
71
+ }, 1000)
68
72
  }
69
73
  }
70
74
  }
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -217,25 +217,40 @@
217
217
  // trigger progressive textures to be loaded:
218
218
  const renderers = GameObject.getComponentsInChildren(objectToExport, Renderer);
219
219
  const progressiveLoading = new Array<Promise<any>>();
220
- let loadedTextures = 0;
220
+ let progressiveTasks = 0;
221
221
  for (const rend of renderers) {
222
+ rend["didAutomaticallyUpdateLODLevel"] = rend.automaticallyUpdateLODLevel;
223
+ rend.automaticallyUpdateLODLevel = false;
224
+ for (const mesh of rend.sharedMeshes) {
225
+ if (mesh) {
226
+ const task = rend.loadProgressiveMeshes(mesh, 0);
227
+ if (task instanceof Promise)
228
+ progressiveLoading.push(new Promise<void>((resolve, reject) => {
229
+ task.then(() => {
230
+ progressiveTasks++;
231
+ Progress.report("export-usdz-textures", { message: "Loaded progressive mesh", currentStep: progressiveTasks, totalSteps: progressiveLoading.length });
232
+ resolve();
233
+ }).catch((err) => reject(err));
234
+ }));
235
+ }
236
+ }
222
237
  for (const mat of rend.sharedMaterials) {
223
238
  if (mat) {
224
- const task = rend.loadProgressiveTextures(mat);
239
+ const task = rend.loadProgressiveTextures(mat, 0);
225
240
  if (task instanceof Promise)
226
241
  progressiveLoading.push(new Promise<void>((resolve, reject) => {
227
242
  task.then(() => {
228
- loadedTextures++;
229
- Progress.report("export-usdz-textures", { message: "Loaded progressive texture", currentStep: loadedTextures, totalSteps: progressiveLoading.length });
243
+ progressiveTasks++;
244
+ Progress.report("export-usdz-textures", { message: "Loaded progressive texture", currentStep: progressiveTasks, totalSteps: progressiveLoading.length });
230
245
  resolve();
231
246
  }).catch((err) => reject(err));
232
247
  }));
233
248
  }
234
249
  }
235
250
  }
236
- if (debug) showBalloonMessage("Load textures: " + progressiveLoading.length);
251
+ if (debug) showBalloonMessage("Progressive Loading: " + progressiveLoading.length);
237
252
  await Promise.all(progressiveLoading);
238
- if (debug) showBalloonMessage("Load textures: done");
253
+ if (debug) showBalloonMessage("Progressive Loading: done");
239
254
  Progress.end("export-usdz-textures");
240
255
 
241
256
  // apply XRFlags
@@ -303,14 +318,21 @@
303
318
 
304
319
  const blob = new Blob([arraybuffer], { type: 'model/vnd.usdz+zip' });
305
320
 
321
+ Progress.report("export-usdz", "Invoking after-export");
322
+
323
+ this.dispatchEvent(new CustomEvent("after-export", { detail: eventArgs }))
324
+
306
325
  // cleanup – implicit animation behaviors need to be removed again
307
326
  for (const go of implicitBehaviors) {
308
327
  GameObject.destroy(go);
309
328
  }
329
+ // restore renderer state
330
+ for (const rend of renderers) {
331
+ const prevState = rend["didAutomaticallyUpdateLODLevel"];
332
+ if (prevState != undefined)
333
+ rend.automaticallyUpdateLODLevel = prevState;
334
+ }
310
335
 
311
- Progress.report("export-usdz", "Invoking after-export" );
312
- this.dispatchEvent(new CustomEvent("after-export", { detail: eventArgs }))
313
-
314
336
  // restore XR flags
315
337
  XRState.Global.Set(currentXRState);
316
338
 
src/engine-components/webxr/controllers/XRControllerModel.ts CHANGED
@@ -12,6 +12,8 @@
12
12
  import type { IGameObject } from "../../../engine/engine_types.js";
13
13
  import { getParam } from "../../../engine/engine_utils.js";
14
14
  import { NeedleXRController, type NeedleXRControllerEventArgs, type NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
15
+ import { registerExtensions } from "../../../engine/extensions/extensions.js";
16
+ import { NEEDLE_progressive } from "../../../engine/extensions/NEEDLE_progressive.js";
15
17
  import { Behaviour, GameObject } from "../../Component.js"
16
18
 
17
19
  const debug = getParam("debugwebxr");
@@ -238,6 +240,7 @@
238
240
 
239
241
  const loader = new GLTFLoader();
240
242
  addDracoAndKTX2Loaders(loader, context);
243
+ await registerExtensions(loader, context, this.sourceId!);
241
244
  loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/generic-hand/');
242
245
 
243
246
  // TODO: we should handle the loading here ourselves to not have this requirement of a specific model name
@@ -261,10 +264,13 @@
261
264
  // @ts-ignore
262
265
  const handmesh = new XRHandMeshModel(handObject, hand, loader.path, controller.inputSource.handedness, loader, (object: Object3D) => {
263
266
  // The hand mesh should not receive raycasts
264
- object.traverseVisible(child => {
267
+ object.traverse(child => {
265
268
  child.layers.set(2);
266
269
  if (NeedleXRSession.active?.isPassThrough)
267
270
  this.makeOccluder(child);
271
+ if (child instanceof Mesh) {
272
+ NEEDLE_progressive.assignMeshLOD(context, this.sourceId!, child, 0);
273
+ }
268
274
  });
269
275
  if (!controller.connected) {
270
276
  if (debug) Gizmos.DrawLabel(controller.rayWorldPosition, "Hand is loaded but not connected anymore", .05, 5);
src/engine-components/RendererInstancing.ts ADDED
@@ -0,0 +1,720 @@
1
+ import { BatchedMesh, BufferAttribute, BufferGeometry, Color, InstancedMesh, InterleavedBuffer, InterleavedBufferAttribute, Material, Matrix4, Mesh, MeshBasicMaterial, MeshStandardMaterial, Object3D, RawShaderMaterial, StaticDrawUsage, Texture, Vector3 } from "three";
2
+
3
+ import { isDevEnvironment, showBalloonError } from "../engine/debug/index.js";
4
+ import { $instancingAutoUpdateBounds, $instancingRenderer, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
5
+ import { Context } from "../engine/engine_setup.js";
6
+ import { getParam, makeIdFromRandomWords } from "../engine/engine_utils.js";
7
+ import { NEEDLE_progressive } from "../engine/extensions/index.js";
8
+ import { GameObject } from "./Component.js";
9
+ import type { Renderer } from "./Renderer.js";
10
+
11
+ const debugInstancing = getParam("debuginstancing");
12
+
13
+ declare class InstancingSetupArgs {
14
+ rend: Renderer;
15
+ foundMeshes: number;
16
+ useMatrixWorldAutoUpdate: boolean;
17
+ };
18
+
19
+ export class InstancingHandler {
20
+ static readonly instance: InstancingHandler = new InstancingHandler();
21
+
22
+ public objs: InstancedMeshRenderer[] = [];
23
+
24
+ public setup(renderer: Renderer, obj: Object3D, context: Context, handlesArray: InstanceHandle[] | null, args: InstancingSetupArgs, level: number = 0)
25
+ : InstanceHandle[] | null {
26
+
27
+ // make sure setting casting settings are applied so when we add the mesh to the InstancedMesh we can ask for the correct cast shadow setting
28
+ renderer.applySettings(obj);
29
+ const res = this.tryCreateOrAddInstance(obj, context, args);
30
+ if (res) {
31
+ renderer.loadProgressiveTextures(res.renderer.material, 0);
32
+ // renderer.loadProgressiveMeshes(res.instancer.mesh, 0);
33
+ if (handlesArray === null) handlesArray = [];
34
+ handlesArray.push(res);
35
+ }
36
+
37
+ else if (level <= 0 && obj.type !== "Mesh") {
38
+ const nextLevel = level + 1;
39
+ for (const ch of obj.children) {
40
+ handlesArray = this.setup(renderer, ch, context, handlesArray, args, nextLevel);
41
+ }
42
+ }
43
+
44
+ if (level === 0) {
45
+ // For multi material objects we only want to track the root object's matrix
46
+ if (args.useMatrixWorldAutoUpdate && handlesArray && handlesArray.length >= 0) {
47
+ this.autoUpdateInstanceMatrix(obj);
48
+ }
49
+ }
50
+
51
+ return handlesArray;
52
+ }
53
+
54
+ private tryCreateOrAddInstance(obj: Object3D, context: Context, args: InstancingSetupArgs): InstanceHandle | null {
55
+ if (obj.type === "Mesh") {
56
+ const index = args.foundMeshes;
57
+ args.foundMeshes += 1;
58
+ if (!args.rend.enableInstancing) return null;
59
+ if (args.rend.enableInstancing === true) {
60
+ // instancing is enabled globally
61
+ // continue....
62
+ }
63
+ else {
64
+ if (index >= args.rend.enableInstancing.length) {
65
+ if (debugInstancing) console.error("Something is wrong with instance setup", obj, args.rend.enableInstancing, index);
66
+ return null;
67
+ }
68
+ if (!args.rend.enableInstancing[index]) {
69
+ // instancing is disabled
70
+ // console.log("Instancing is disabled", obj);
71
+ return null;
72
+ }
73
+ }
74
+ // instancing is enabled:
75
+ const mesh = obj as Mesh;
76
+ // const geo = mesh.geometry as BufferGeometry;
77
+ const mat = mesh.material as Material;
78
+
79
+ for (const i of this.objs) {
80
+ if (!i.canAdd(mesh.geometry, mat)) continue;
81
+ const handle = i.addInstance(mesh);
82
+ return handle;
83
+ }
84
+ const maxInstances = 16;
85
+ let name = obj.name;
86
+ if (!name?.length) name = makeIdFromRandomWords();
87
+ const i = new InstancedMeshRenderer(name, mesh.geometry, mat, maxInstances, context);
88
+ this.objs.push(i);
89
+ const handle = i.addInstance(mesh);
90
+ return handle;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ private autoUpdateInstanceMatrix(obj: Object3D) {
96
+ const original = obj.matrixWorld["multiplyMatrices"].bind(obj.matrixWorld);
97
+ const previousMatrix: Matrix4 = obj.matrixWorld.clone();
98
+ const matrixChangeWrapper = (a: Matrix4, b: Matrix4) => {
99
+ const newMatrixWorld = original(a, b);
100
+ if (obj[NEED_UPDATE_INSTANCE_KEY] || previousMatrix.equals(newMatrixWorld) === false) {
101
+ previousMatrix.copy(newMatrixWorld)
102
+ obj[NEED_UPDATE_INSTANCE_KEY] = true;
103
+ }
104
+ return newMatrixWorld;
105
+ };
106
+ obj.matrixWorld["multiplyMatrices"] = matrixChangeWrapper;
107
+ // wrap matrixWorldNeedsUpdate
108
+ // let originalMatrixWorldNeedsUpdate = obj.matrixWorldNeedsUpdate;
109
+ // Object.defineProperty(obj, "matrixWorldNeedsUpdate", {
110
+ // get: () => {
111
+ // return originalMatrixWorldNeedsUpdate;
112
+ // },
113
+ // set: (value: boolean) => {
114
+ // if(value) console.warn("SET MATRIX WORLD NEEDS UPDATE");
115
+ // originalMatrixWorldNeedsUpdate = value;
116
+ // }
117
+ // });
118
+ }
119
+ }
120
+
121
+ export class InstanceHandle {
122
+
123
+ static readonly all: InstanceHandle[] = [];
124
+
125
+ /** The name of the object */
126
+ get name(): string {
127
+ return this.object.name;
128
+ }
129
+ get isActive() {
130
+ return this.__instanceIndex >= 0;
131
+ }
132
+ get vertexCount() {
133
+ return this.object.geometry.attributes.position.count;
134
+ }
135
+ get maxVertexCount() {
136
+ return this.meshInformation.vertexCount;
137
+ }
138
+ get reservedVertexCount() {
139
+ return this.__reservedVertexRange;
140
+ }
141
+ get indexCount() {
142
+ return this.object.geometry.index ? this.object.geometry.index.count : 0;
143
+ }
144
+ get maxIndexCount() {
145
+ return this.meshInformation.indexCount;
146
+ }
147
+ get reservedIndexCount() {
148
+ return this.__reservedIndexRange;
149
+ }
150
+
151
+ /** The object that is being instanced */
152
+ readonly object: Mesh;
153
+
154
+ /** The instancer/BatchedMesh that is rendering this object*/
155
+ readonly renderer: InstancedMeshRenderer;
156
+
157
+ /** @internal */
158
+ __instanceIndex: number = -1;
159
+ /** @internal */
160
+ __reservedVertexRange: number = 0;
161
+ /** @internal */
162
+ __reservedIndexRange: number = 0;
163
+
164
+ /** The mesh information of the object */
165
+ readonly meshInformation: MeshInformation;
166
+
167
+ constructor(originalObject: Mesh, instancer: InstancedMeshRenderer) {
168
+ this.__instanceIndex = -1;
169
+ this.object = originalObject;
170
+ this.renderer = instancer;
171
+ originalObject[$instancingRenderer] = instancer;
172
+ this.meshInformation = getMeshInformation(originalObject.geometry);
173
+ InstanceHandle.all.push(this);
174
+ }
175
+
176
+ /** Updates the matrix from the rendered object. Will also call updateWorldMatrix internally */
177
+ updateInstanceMatrix(updateChildren: boolean = false, updateMatrix: boolean = true) {
178
+ if (this.__instanceIndex < 0) return;
179
+ if (updateMatrix) this.object.updateWorldMatrix(true, updateChildren);
180
+ this.renderer.updateInstance(this.object.matrixWorld, this.__instanceIndex);
181
+ }
182
+ /** Updates the matrix of the instance */
183
+ setMatrix(matrix: Matrix4) {
184
+ if (this.__instanceIndex < 0) return;
185
+ this.renderer.updateInstance(matrix, this.__instanceIndex);
186
+ }
187
+
188
+ /** Can be used to change the geometry of this instance */
189
+ setGeometry(geo: BufferGeometry) {
190
+ if (this.__instanceIndex < 0) return false;
191
+ if (this.vertexCount > this.__reservedVertexRange) {
192
+ console.error(`Cannot update geometry, reserved range is too small: ${this.__reservedVertexRange} < ${this.vertexCount} vertices for ${this.name}`);
193
+ return false;
194
+ }
195
+ if (this.indexCount > this.__reservedIndexRange) {
196
+ console.error(`Cannot update geometry, reserved range is too small: ${this.__reservedIndexRange} < ${this.indexCount} indices for ${this.name}`);
197
+ return false;
198
+ }
199
+ return this.renderer.updateGeometry(geo, this.__instanceIndex);
200
+ }
201
+
202
+ /** Adds this object to the instancing renderer (effectively activating instancing) */
203
+ add() {
204
+ if (this.__instanceIndex >= 0) return;
205
+ this.renderer.add(this);
206
+ GameObject.markAsInstancedRendered(this.object, true);
207
+ }
208
+
209
+ /** Removes this object from the instancing renderer */
210
+ remove(delete_: boolean) {
211
+ if (this.__instanceIndex < 0) return;
212
+ this.renderer.remove(this, delete_);
213
+ GameObject.markAsInstancedRendered(this.object, false);
214
+ if (delete_) {
215
+ const i = InstanceHandle.all.indexOf(this);
216
+ if (i >= 0) {
217
+ InstanceHandle.all.splice(i, 1);
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ class InstancedMeshRenderer {
224
+ /** The three instanced mesh
225
+ * @link https://threejs.org/docs/#api/en/objects/InstancedMesh
226
+ */
227
+ get mesh() {
228
+ return this.inst;
229
+ }
230
+ get visible(): boolean {
231
+ return this.inst.visible;
232
+ }
233
+ set visible(val: boolean) {
234
+ this.inst.visible = val;
235
+ }
236
+ get castShadow(): boolean {
237
+ return this.inst.castShadow;
238
+ }
239
+ set castShadow(val: boolean) {
240
+ this.inst.castShadow = val;
241
+ }
242
+ set receiveShadow(val: boolean) {
243
+ this.inst.receiveShadow = val;
244
+ }
245
+
246
+ /** If true, the instancer is allowed to grow when the max instance count is reached */
247
+ allowResize: boolean = true;
248
+
249
+ /** Update the bounding box and sphere of the instanced mesh
250
+ * @param box If true, update the bounding box
251
+ * @param sphere If true, update the bounding sphere
252
+ */
253
+ updateBounds(box: boolean = true, sphere: boolean = true) {
254
+ this._needUpdateBounds = false;
255
+ if (box)
256
+ this.inst.computeBoundingBox();
257
+ if (sphere)
258
+ this.inst.computeBoundingSphere();
259
+ }
260
+
261
+ /** The name of the instancer */
262
+ name: string = "";
263
+
264
+ /** The added geometry */
265
+ readonly geometry: BufferGeometry;
266
+
267
+ /** The material used for the instanced mesh */
268
+ readonly material: Material;
269
+
270
+ /** The current number of instances */
271
+ get count(): number { return this._currentInstanceCount; }
272
+
273
+ private context: Context;
274
+ private inst: BatchedMesh;
275
+ private handles: (InstanceHandle | null)[] = [];
276
+ private maxInstanceCount: number;
277
+
278
+ private _currentInstanceCount = 0;
279
+ private _currentVertexCount = 0;
280
+ private _currentIndexCount = 0;
281
+
282
+ private _maxVertexCount: number;
283
+ private _maxIndexCount: number;
284
+
285
+ private static nullMatrix: Matrix4 = new Matrix4();
286
+
287
+ /** Check if the geometry can be added to this instancer
288
+ * @param geometry The geometry to check
289
+ * @param material The material of the geometry
290
+ * @returns true if the geometry can be added
291
+ */
292
+ canAdd(geometry: BufferGeometry, material: Material): boolean {
293
+
294
+ if (this._maxVertexCount > 10_000_000) return false;
295
+
296
+ // The material instance must match
297
+ // perhaps at some point later we *could* check if it's the same shader and properties but this would be risky
298
+ if (material !== this.material) return false;
299
+
300
+ // if(this.geometry !== _geometry) return false;
301
+
302
+ // console.log(geometry.name, geometry.uuid);
303
+
304
+ // if (!this.validateGeometry(geometry)) return false;
305
+
306
+ // const validationMethod = this.inst["_validateGeometry"];
307
+ // if (!validationMethod) throw new Error("InstancedMesh does not have a _validateGeometry method");
308
+ // try {
309
+ // validationMethod.call(this.inst, _geometry);
310
+ // }
311
+ // catch (err) {
312
+ // // console.error(err);
313
+ // return false;
314
+ // }
315
+
316
+ const hasSpace = !this.mustGrow(geometry);
317
+ if (hasSpace) return true;
318
+ if (this.allowResize) return true;
319
+
320
+ return false;
321
+ }
322
+
323
+ private _needUpdateBounds: boolean = false;
324
+ private _debugMaterial: MeshStandardMaterial | null = null;
325
+
326
+ constructor(name: string, geo: BufferGeometry, material: Material, initialMaxCount: number, context: Context) {
327
+ this.name = name;
328
+ this.geometry = geo;
329
+ this.material = material;
330
+ this.context = context;
331
+ this.maxInstanceCount = Math.max(2, initialMaxCount);
332
+ if (debugInstancing) {
333
+ this._debugMaterial = createDebugMaterial();
334
+ }
335
+ const estimate = this.tryEstimateVertexCountSize(this.maxInstanceCount, [geo], initialMaxCount);
336
+ this._maxVertexCount = estimate.vertexCount;
337
+ this._maxIndexCount = estimate.indexCount;
338
+ this.inst = new BatchedMesh(this.maxInstanceCount, this._maxVertexCount, this._maxIndexCount, this._debugMaterial ?? this.material);
339
+ // this.inst = new InstancedMesh(geo, material, count);
340
+ this.inst[$instancingAutoUpdateBounds] = true;
341
+ // this.inst.count = 0;
342
+ this.inst.visible = true;
343
+ this.context.scene.add(this.inst);
344
+
345
+ // Not handled by RawShaderMaterial, so we need to set the define explicitly.
346
+ // Edge case: theoretically some users of the material could use it in an
347
+ // instanced fashion, and some others not. In that case, the material would not
348
+ // be able to be shared between the two use cases. We could probably add a
349
+ // onBeforeRender call for the InstancedMesh and set the define there.
350
+ // Same would apply if we support skinning -
351
+ // there we would have to split instanced batches so that the ones using skinning
352
+ // are all in the same batch.
353
+ if (material instanceof RawShaderMaterial) {
354
+ material.defines["USE_INSTANCING"] = true;
355
+ material.needsUpdate = true;
356
+ }
357
+
358
+ context.pre_render_callbacks.push(this.onBeforeRender);
359
+ context.post_render_callbacks.push(this.onAfterRender);
360
+
361
+ if (debugInstancing) {
362
+ console.log(`Instanced renderer created with ${this.maxInstanceCount} instances, ${this._maxVertexCount} max vertices and ${this._maxIndexCount} max indices for \"${name}\"`)
363
+ }
364
+ }
365
+
366
+ dispose() {
367
+ if (debugInstancing) console.warn("Dispose instanced renderer", this.name);
368
+ this.context.scene.remove(this.inst);
369
+ this.inst.dispose();
370
+ this.inst = null as any;
371
+ this.handles = [];
372
+ }
373
+
374
+ addInstance(obj: Mesh): InstanceHandle | null {
375
+
376
+ const handle = new InstanceHandle(obj, this);
377
+
378
+ if (obj.castShadow === true && this.inst.castShadow === false) {
379
+ this.inst.castShadow = true;
380
+ }
381
+ if (obj.receiveShadow === true && this.inst.receiveShadow === false) {
382
+ this.inst.receiveShadow = true;
383
+ }
384
+
385
+ try {
386
+ this.add(handle);
387
+ }
388
+ catch (e) {
389
+ console.error("Failed adding mesh to instancing\n", e);
390
+ if (isDevEnvironment()) showBalloonError("Failed instancing mesh. See the browser console for details.");
391
+ return null;
392
+ }
393
+
394
+ return handle;
395
+ }
396
+
397
+
398
+ add(handle: InstanceHandle) {
399
+ const geo = handle.object.geometry as BufferGeometry;
400
+
401
+ if (!this.validateGeometry(geo)) {
402
+ if (debugInstancing) console.error("Cannot add instance, invalid geometry", this.name, geo);
403
+ return;
404
+ }
405
+
406
+ if (this.mustGrow(geo)) {
407
+ if (this.allowResize) {
408
+ this.grow(geo);
409
+ }
410
+ else {
411
+ console.error("Cannot add instance, max count reached", this.name, this.count, this.maxInstanceCount);
412
+ return;
413
+ }
414
+ }
415
+
416
+ handle.object.updateWorldMatrix(true, true);
417
+ this.addGeometry(handle);
418
+ this.handles[handle.__instanceIndex] = handle;
419
+ this._currentInstanceCount += 1;
420
+
421
+ this.markNeedsUpdate();
422
+
423
+ if (this._currentInstanceCount > 0)
424
+ this.inst.visible = true;
425
+ }
426
+
427
+ remove(handle: InstanceHandle, delete_: boolean) {
428
+ if (!handle) return;
429
+ if (handle.__instanceIndex < 0 || this.handles[handle.__instanceIndex] != handle || this._currentInstanceCount <= 0) {
430
+ return;
431
+ }
432
+
433
+ this.removeGeometry(handle, delete_);
434
+ this.handles[handle.__instanceIndex] = null;
435
+ handle.__instanceIndex = -1;
436
+
437
+ if (this._currentInstanceCount > 0) {
438
+ this._currentInstanceCount -= 1;
439
+ }
440
+
441
+ if (this._currentInstanceCount <= 0)
442
+ this.inst.visible = false;
443
+
444
+ this.markNeedsUpdate();
445
+ }
446
+
447
+ updateInstance(mat: Matrix4, index: number) {
448
+ this.inst.setMatrixAt(index, mat);
449
+ this.markNeedsUpdate();
450
+ }
451
+
452
+ updateGeometry(geo: BufferGeometry, index: number) {
453
+ if (!this.validateGeometry(geo)) {
454
+ return false;
455
+ }
456
+ if (this.mustGrow()) {
457
+ this.grow(geo);
458
+ }
459
+ if (debugInstancing) console.log("UPDATE MESH", index, geo.name, getMeshInformation(geo), geo.attributes.position.count, geo.index ? geo.index.count : 0);
460
+ this.inst.setGeometryAt(index, geo);
461
+ this.markNeedsUpdate();
462
+ return true;
463
+ }
464
+
465
+ private onBeforeRender = () => {
466
+ // ensure the instanced mesh is rendered / has correct layers
467
+ this.inst.layers.enableAll();
468
+
469
+ if (this._needUpdateBounds && this.inst[$instancingAutoUpdateBounds] === true) {
470
+ if (debugInstancing) console.log("Update instancing bounds", this.name, this.inst.matrixWorldNeedsUpdate);
471
+ this.updateBounds();
472
+ }
473
+ }
474
+
475
+ private onAfterRender = () => {
476
+ // hide the instanced mesh again when its not being rendered (for raycasting we still use the original object)
477
+ this.inst.layers.disableAll();
478
+ }
479
+
480
+ private validateGeometry(_geometry: BufferGeometry): boolean {
481
+ const batchGeometry = this.geometry;
482
+
483
+ // for (const attributeName in batchGeometry.attributes) {
484
+ // if (attributeName === "batchId") {
485
+ // continue;
486
+ // }
487
+ // if (!geometry.hasAttribute(attributeName)) {
488
+ // // geometry.setAttribute(attributeName, batchGeometry.getAttribute(attributeName).clone());
489
+ // return false;
490
+ // // throw new Error(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`);
491
+ // }
492
+ // // const srcAttribute = geometry.getAttribute(attributeName);
493
+ // // const dstAttribute = batchGeometry.getAttribute(attributeName);
494
+ // // if (srcAttribute.itemSize !== dstAttribute.itemSize || srcAttribute.normalized !== dstAttribute.normalized) {
495
+ // // if (debugInstancing) throw new Error('BatchedMesh: All attributes must have a consistent itemSize and normalized value.');
496
+ // // return false;
497
+ // // }
498
+ // }
499
+ return true;
500
+ }
501
+
502
+ private markNeedsUpdate() {
503
+ this._needUpdateBounds = true;
504
+ // this.inst.instanceMatrix.needsUpdate = true;
505
+ }
506
+
507
+ /**
508
+ * @param geo The geometry to add (if none is provided it means the geometry is already added and just updated)
509
+ */
510
+ private mustGrow(geo?: BufferGeometry): boolean {
511
+ if (this.count >= this.maxInstanceCount) return true;
512
+ if (!geo) return false;
513
+ const meshInfo = getMeshInformation(geo);
514
+ const newVertexCount = meshInfo.vertexCount;
515
+ const newIndexCount = meshInfo.indexCount;
516
+ return this._currentVertexCount + newVertexCount > this._maxVertexCount || this._currentIndexCount + newIndexCount > this._maxIndexCount;
517
+ }
518
+
519
+ private grow(geometry: BufferGeometry) {
520
+ const newSize = this.maxInstanceCount * 2;
521
+
522
+ // create a new BatchedMesh instance
523
+ const estimatedSpace = this.tryEstimateVertexCountSize(newSize, [geometry]);// geometry.attributes.position.count;
524
+ // const indices = geometry.index ? geometry.index.count : 0;
525
+ const newMaxVertexCount = Math.max(this._maxVertexCount, estimatedSpace.vertexCount);
526
+ const newMaxIndexCount = Math.max(this._maxIndexCount, estimatedSpace.indexCount, this._maxVertexCount * 2);
527
+
528
+ if (debugInstancing) {
529
+ const geometryInfo = getMeshInformation(geometry);
530
+ console.warn(`Growing batched mesh for \"${this.name}/${geometry.name}\" ${geometryInfo.vertexCount} vertices, ${geometryInfo.indexCount} indices\nMax count ${this.maxInstanceCount} → ${newSize}\nMax vertex count ${this._maxVertexCount} -> ${newMaxVertexCount}\nMax index count ${this._maxIndexCount} -> ${newMaxIndexCount}`);
531
+ this._debugMaterial = createDebugMaterial();
532
+ }
533
+
534
+ this._maxVertexCount = newMaxVertexCount;
535
+ this._maxIndexCount = newMaxIndexCount;
536
+ const newInst = new BatchedMesh(newSize, this._maxVertexCount, this._maxIndexCount, this._debugMaterial ?? this.material);
537
+ newInst.layers = this.inst.layers;
538
+ newInst.castShadow = this.inst.castShadow;
539
+ newInst.receiveShadow = this.inst.receiveShadow;
540
+ newInst.visible = this.inst.visible;
541
+ newInst[$instancingAutoUpdateBounds] = this.inst[$instancingAutoUpdateBounds];
542
+ newInst.matrixAutoUpdate = this.inst.matrixAutoUpdate;
543
+ newInst.matrixWorldNeedsUpdate = this.inst.matrixWorldNeedsUpdate;
544
+ newInst.matrixAutoUpdate = this.inst.matrixAutoUpdate;
545
+ newInst.matrixWorld.copy(this.inst.matrixWorld);
546
+ newInst.matrix.copy(this.inst.matrix);
547
+
548
+ // dispose the old batched mesh
549
+ this.inst.dispose();
550
+ this.inst.removeFromParent();
551
+
552
+ this.inst = newInst;
553
+ this.maxInstanceCount = newSize;
554
+
555
+ // add current instances to new instanced mesh
556
+ for (const handle of this.handles) {
557
+ if (handle && handle.__instanceIndex >= 0) {
558
+ this.addGeometry(handle);
559
+ }
560
+ }
561
+
562
+ this.context.scene.add(newInst);
563
+ }
564
+
565
+ private tryEstimateVertexCountSize(newMaxInstances: number, _newGeometries?: BufferGeometry[], newGeometriesFactor: number = 1): MeshInformation {
566
+ /** Used geometries and how many instances use them */
567
+ const usedGeometries = new Map<BufferGeometry, MeshInformation & { count: number }>();
568
+ for (const handle of this.handles) {
569
+ if (handle && handle.__instanceIndex >= 0) {
570
+ if (!usedGeometries.has(handle.object.geometry as BufferGeometry)) {
571
+ const meshinfo = { count: 1, ...getMeshInformation(handle.object.geometry as BufferGeometry) };
572
+ usedGeometries.set(handle.object.geometry as BufferGeometry, meshinfo);
573
+ }
574
+ else {
575
+ const entry = usedGeometries.get(handle.object.geometry as BufferGeometry)!;
576
+ entry.count += 1;
577
+ }
578
+
579
+ }
580
+ }
581
+
582
+ // then calculate the total vertex count
583
+ let totalVertices = 0;
584
+ let totalIndices = 0;
585
+ // let maxVertices = 0;
586
+ for (const [_geo, data] of usedGeometries) {
587
+ totalVertices += data.vertexCount * data.count;
588
+ totalIndices += data.indexCount * data.count;
589
+ // maxVertices = Math.max(maxVertices, geo.attributes.position.count * count);
590
+ }
591
+ // we calculate the average to make an educated guess of how many vertices will be needed with the new buffer count
592
+ const averageVerts = Math.ceil(totalVertices / Math.max(1, this._currentInstanceCount));
593
+ let maxVertexCount = averageVerts * newMaxInstances;
594
+ const averageIndices = Math.ceil(totalIndices / Math.max(1, this._currentInstanceCount));
595
+ let maxIndexCount = averageIndices * newMaxInstances * 2;
596
+
597
+ // if new geometries are provided we *know* that they will be added
598
+ // so we make sure to include them in the calculation
599
+ if (_newGeometries) {
600
+ for (const geo of _newGeometries) {
601
+ const meshinfo = getMeshInformation(geo);
602
+ maxVertexCount += meshinfo.vertexCount * newGeometriesFactor;
603
+ maxIndexCount += meshinfo.indexCount * newGeometriesFactor;
604
+
605
+ }
606
+ }
607
+
608
+ return { vertexCount: maxVertexCount, indexCount: maxIndexCount };
609
+ }
610
+
611
+
612
+ private readonly _availableBuckets = new Array<BucketInfo>();
613
+ private readonly _usedBuckets = new Array<BucketInfo>();
614
+
615
+ private addGeometry(handle: InstanceHandle) {
616
+ // if (handle.reservedVertexCount <= 0 || handle.reservedIndexCount <= 0) {
617
+ // console.error("Cannot add geometry with 0 vertices or indices", handle.name);
618
+ // return;
619
+ // }
620
+ // search the smallest available bucket that fits our handle
621
+ let smallestBucket: BucketInfo | null = null;
622
+ let smallestBucketIndex = -1;
623
+ for (let i = this._availableBuckets.length - 1; i >= 0; i--) {
624
+ const bucket = this._availableBuckets[i];
625
+ if (bucket.vertexCount >= handle.maxVertexCount && bucket.indexCount >= handle.maxIndexCount) {
626
+ if (smallestBucket == null || bucket.vertexCount < smallestBucket.vertexCount) {
627
+ smallestBucket = bucket;
628
+ smallestBucketIndex = i;
629
+ }
630
+ }
631
+ }
632
+ // if we have a bucket that is big enough, use it
633
+ if (smallestBucket != null) {
634
+ const bucket = smallestBucket;
635
+ if (debugInstancing)
636
+ console.log(`RE-USE SPACE #${bucket.index}, ${handle.maxVertexCount} vertices, ${handle.maxIndexCount} indices, ${handle.name}`);
637
+ this.inst.setGeometryAt(bucket.index, handle.object.geometry as BufferGeometry);
638
+ this.inst.setMatrixAt(bucket.index, handle.object.matrixWorld);
639
+ this.inst.setVisibleAt(bucket.index, true);
640
+ handle.__instanceIndex = bucket.index;
641
+ this._usedBuckets[bucket.index] = bucket;
642
+ this._availableBuckets.splice(smallestBucketIndex, 1);
643
+ return;
644
+ }
645
+
646
+ // otherwise add more geometry
647
+ const geo = handle.object.geometry as BufferGeometry;
648
+
649
+
650
+ if (debugInstancing) console.log("ADD GEOMETRY", geo.name, "\nvertex:", `${this._currentVertexCount} + ${handle.maxVertexCount} < ${this._maxVertexCount}?`, "\nindex:", handle.maxIndexCount, this._currentIndexCount, this._maxIndexCount);
651
+
652
+ const i = this.inst.addGeometry(geo, handle.maxVertexCount, handle.maxIndexCount);
653
+ handle.__instanceIndex = i;
654
+ handle.__reservedVertexRange = handle.maxVertexCount;
655
+ handle.__reservedIndexRange = handle.maxIndexCount;
656
+ this._currentVertexCount += handle.maxVertexCount;
657
+ this._currentIndexCount += handle.maxIndexCount;
658
+ this._usedBuckets[i] = { index: i, vertexCount: handle.maxVertexCount, indexCount: handle.maxIndexCount };
659
+ this.inst.setMatrixAt(i, handle.object.matrixWorld);
660
+ if (debugInstancing)
661
+ console.log(`ADD MESH & RESERVE SPACE #${i}, ${handle.maxVertexCount} vertices, ${handle.maxIndexCount} indices, ${handle.name} ${handle.object.uuid}`);
662
+
663
+ }
664
+
665
+ private removeGeometry(handle: InstanceHandle, _del: boolean) {
666
+ if (handle.__instanceIndex < 0) return;
667
+ this._usedBuckets.splice(handle.__instanceIndex, 1);
668
+ // deleteGeometry is currently not useable since there's no optimize method
669
+ // https://github.com/mrdoob/three.js/issues/27985
670
+ // if (del)
671
+ // this.inst.deleteGeometry(handle.__instanceIndex);
672
+ // else
673
+ this.inst.setVisibleAt(handle.__instanceIndex, false);
674
+ this._availableBuckets.push({
675
+ index: handle.__instanceIndex,
676
+ vertexCount: handle.reservedVertexCount,
677
+ indexCount: handle.reservedIndexCount
678
+ });
679
+ }
680
+ }
681
+
682
+ declare type BucketInfo = {
683
+ index: number;
684
+ vertexCount: number;
685
+ indexCount: number;
686
+ }
687
+
688
+ declare type MeshInformation = {
689
+ vertexCount: number;
690
+ indexCount: number;
691
+ }
692
+
693
+ function getMeshInformation(geo: BufferGeometry): MeshInformation {
694
+ let vertexCount = geo.attributes.position.count;
695
+ let indexCount = geo.index ? geo.index.count : 0;
696
+ const lodInfo = NEEDLE_progressive.getMeshLODInformation(geo);
697
+ if (lodInfo) {
698
+ const lod0 = lodInfo.lods[0];
699
+ let lod0Count = lod0.vertexCount;
700
+ let lod0IndexCount = lod0.indexCount;
701
+ // add some wiggle room: https://linear.app/needle/issue/NE-4505
702
+ lod0Count += 10;
703
+ lod0Count += lod0Count * .05;
704
+ lod0IndexCount += 20;
705
+ vertexCount = Math.max(vertexCount, lod0Count);
706
+ indexCount = Math.max(indexCount, lod0IndexCount);
707
+ }
708
+ vertexCount = Math.ceil(vertexCount);
709
+ indexCount = Math.ceil(indexCount);
710
+ return { vertexCount, indexCount };
711
+ }
712
+
713
+ function createDebugMaterial() {
714
+ const mat = new MeshStandardMaterial({ color: new Color(Math.random(), Math.random(), Math.random()) });
715
+ mat.emissive = mat.color;
716
+ mat.emissiveIntensity = .3;
717
+ if (getParam("wireframe"))
718
+ mat.wireframe = true;
719
+ return mat;
720
+ }