@@ -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
|
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
|
235
|
+
await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, this.variantMaterial, 0);
|
236
236
|
}
|
237
237
|
}
|
238
238
|
|
@@ -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();
|
@@ -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
|
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
|
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
|
}
|
@@ -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";
|
@@ -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
|
-
|
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) {
|
@@ -1,11 +1,10 @@
|
|
1
|
-
import
|
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 {
|
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}${
|
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
|
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 {
|
@@ -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
|
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
|
}
|
@@ -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
|
-
|
21
|
-
|
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
|
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
|
91
|
-
if (
|
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,
|
290
|
+
// if (debug) console.log("-----------\n", "FIND", material?.name, slot, current?.name, current?.userData, current, material);
|
94
291
|
|
95
|
-
|
292
|
+
const info = this.onProgressiveLoadStart(context, source, material, slot);
|
293
|
+
return NEEDLE_progressive.getOrLoadLOD<Texture>(context, source, current, level).then(tex => {
|
96
294
|
|
97
|
-
|
295
|
+
// this can currently not happen
|
296
|
+
if (Array.isArray(tex)) return null;
|
98
297
|
|
99
|
-
|
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
|
-
|
302
|
+
// tex.needsUpdate = true;
|
102
303
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
304
|
+
if (material && slot) {
|
305
|
+
material[slot] = tex;
|
306
|
+
// material.needsUpdate = true;
|
307
|
+
}
|
107
308
|
|
108
|
-
|
109
|
-
|
110
|
-
if
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
if (
|
116
|
-
|
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
|
-
|
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:
|
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(
|
148
|
-
if (debug)
|
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 (
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
164
|
-
private static
|
165
|
-
|
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
|
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
|
443
|
+
const debugverbose = debug == "verbose";
|
170
444
|
|
171
|
-
|
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
|
175
|
-
|
176
|
-
|
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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
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
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
-
|
218
|
-
|
219
|
-
|
220
|
-
}
|
561
|
+
const parser = gltf.parser;
|
562
|
+
if (debugverbose) console.log("Loading finished " + lod_url, ext.guid);
|
563
|
+
let index = -1;
|
221
564
|
|
222
|
-
|
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:
|
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 (
|
246
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
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
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
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
|
-
|
269
|
-
|
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(
|
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 (
|
651
|
+
if (debugverbose)
|
279
652
|
console.log(progressiveInfo, tex);
|
280
653
|
}
|
281
|
-
else if (debug) console.warn("failed loading",
|
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(
|
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,
|
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,
|
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,
|
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
|
-
|
408
|
-
|
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
|
};
|
@@ -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
|
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";
|
@@ -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
|
104
|
+
NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, material);
|
105
105
|
}
|
106
106
|
|
107
107
|
return material;
|
@@ -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);
|
@@ -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 {
|
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("
|
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
|
-
|
484
|
-
|
485
|
-
|
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 =
|
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
|
-
|
645
|
-
if (
|
646
|
-
this.
|
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
|
-
|
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
|
-
|
724
|
-
material
|
725
|
-
|
726
|
-
|
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
|
-
|
796
|
-
super.
|
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
|
-
}
|
@@ -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
|
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
|
240
|
+
NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, mat, 0);
|
241
241
|
}
|
242
242
|
else {
|
243
243
|
this._currentSprite.geometry = SpriteUtils.getOrCreateGeometry(sprite);
|
@@ -62,9 +62,13 @@
|
|
62
62
|
if (loaded instanceof Object3D) {
|
63
63
|
if (!loaded.parent) {
|
64
64
|
if (loaded instanceof Mesh) {
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
}
|
@@ -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
|
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
|
-
|
229
|
-
Progress.report("export-usdz-textures", { message: "Loaded progressive texture", currentStep:
|
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("
|
251
|
+
if (debug) showBalloonMessage("Progressive Loading: " + progressiveLoading.length);
|
237
252
|
await Promise.all(progressiveLoading);
|
238
|
-
if (debug) showBalloonMessage("
|
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
|
|
@@ -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.
|
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);
|
@@ -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
|
+
}
|