@needle-tools/engine 4.3.0 → 4.3.2-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/components.needle.json +1 -1
- package/dist/needle-engine.bundle.js +3803 -3770
- package/dist/needle-engine.bundle.light.js +3805 -3775
- package/dist/needle-engine.bundle.light.min.js +125 -112
- package/dist/needle-engine.bundle.light.umd.cjs +113 -100
- package/dist/needle-engine.bundle.min.js +125 -112
- package/dist/needle-engine.bundle.umd.cjs +113 -100
- package/dist/needle-engine.d.ts +9 -9
- package/dist/needle-engine.js +509 -507
- package/dist/needle-engine.light.d.ts +9 -9
- package/dist/needle-engine.light.js +509 -507
- package/dist/needle-engine.light.min.js +1 -1
- package/dist/needle-engine.light.umd.cjs +1 -1
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/engine_time.d.ts +11 -2
- package/lib/engine/engine_time.js +12 -1
- package/lib/engine/engine_time.js.map +1 -1
- package/lib/engine-components/Duplicatable.d.ts +2 -6
- package/lib/engine-components/Duplicatable.js +21 -20
- package/lib/engine-components/Duplicatable.js.map +1 -1
- package/lib/engine-components/OrbitControls.d.ts +1 -0
- package/lib/engine-components/OrbitControls.js +8 -0
- package/lib/engine-components/OrbitControls.js.map +1 -1
- package/lib/engine-components/Renderer.js +12 -6
- package/lib/engine-components/Renderer.js.map +1 -1
- package/lib/engine-components/RendererInstancing.d.ts +1 -3
- package/lib/engine-components/RendererInstancing.js +31 -63
- package/lib/engine-components/RendererInstancing.js.map +1 -1
- package/lib/engine-components/postprocessing/PostProcessingHandler.d.ts +1 -1
- package/lib/engine-components/postprocessing/PostProcessingHandler.js +6 -14
- package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
- package/lib/engine-components/postprocessing/Volume.d.ts +15 -0
- package/lib/engine-components/postprocessing/Volume.js +73 -4
- package/lib/engine-components/postprocessing/Volume.js.map +1 -1
- package/lib/engine-components/ui/Canvas.js +2 -2
- package/lib/engine-components/ui/Canvas.js.map +1 -1
- package/lib/needle-engine.js +8 -6
- package/lib/needle-engine.js.map +1 -1
- package/package.json +1 -1
- package/plugins/common/buildinfo.js +8 -3
- package/plugins/vite/copyfiles.js +1 -1
- package/src/engine/engine_time.ts +14 -2
- package/src/engine-components/Duplicatable.ts +21 -19
- package/src/engine-components/OrbitControls.ts +8 -0
- package/src/engine-components/Renderer.ts +12 -5
- package/src/engine-components/RendererInstancing.ts +32 -68
- package/src/engine-components/postprocessing/PostProcessingHandler.ts +8 -14
- package/src/engine-components/postprocessing/Volume.ts +82 -6
- package/src/engine-components/ui/Canvas.ts +2 -2
- package/src/needle-engine.ts +8 -6
|
@@ -65,7 +65,7 @@ async function run(buildstep, config) {
|
|
|
65
65
|
|
|
66
66
|
const outDir = resolve(baseDir, outdirName);
|
|
67
67
|
if (!existsSync(outDir)) {
|
|
68
|
-
mkdirSync(outDir);
|
|
68
|
+
mkdirSync(outDir, { recursive: true });
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// copy a list of files or directories declared in build.copy = [] in the needle.config.json
|
|
@@ -42,7 +42,19 @@ export class Time implements ITime {
|
|
|
42
42
|
return this.clock.elapsedTime;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* @returns {Number} FPS for this frame.
|
|
47
|
+
* Note that this returns the raw value (e.g. 59.88023952362959) and will fluctuate a lot between frames.
|
|
48
|
+
* If you want a more stable FPS, use `smoothedFps` instead.
|
|
49
|
+
*/
|
|
50
|
+
get fps() {
|
|
51
|
+
return 1 / this.deltaTime;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Approximated frames per second
|
|
56
|
+
* @returns the smoothed FPS value over the last 60 frames with decimals.
|
|
57
|
+
*/
|
|
46
58
|
get smoothedFps() { return this._smoothedFps; }
|
|
47
59
|
/** The smoothed time in seconds it took to complete the last frame (Read Only). */
|
|
48
60
|
get smoothedDeltaTime() { return 1 / this._smoothedFps; }
|
|
@@ -51,7 +63,7 @@ export class Time implements ITime {
|
|
|
51
63
|
private clock = new Clock();
|
|
52
64
|
private _smoothedFps: number = 0;
|
|
53
65
|
private _smoothedDeltaTime: number = 0;
|
|
54
|
-
private _fpsSamples: number[] = [];
|
|
66
|
+
private readonly _fpsSamples: number[] = [];
|
|
55
67
|
private _fpsSampleIndex: number = 0;
|
|
56
68
|
|
|
57
69
|
constructor() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Object3D, Quaternion, Vector3 } from "three";
|
|
2
2
|
|
|
3
3
|
import { isDevEnvironment } from "../engine/debug/index.js";
|
|
4
|
+
import { WaitForSeconds } from "../engine/engine_coroutine.js";
|
|
4
5
|
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
|
5
6
|
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
|
6
7
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
|
@@ -30,17 +31,10 @@ export class Duplicatable extends Behaviour implements IPointerEventHandler {
|
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* The maximum number of objects that can be duplicated in the interval.
|
|
33
|
-
* @default 10
|
|
34
|
-
*/
|
|
35
|
-
@serializable()
|
|
36
|
-
limitCount = 10;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The interval in seconds in which the limitCount is reset.
|
|
40
34
|
* @default 60
|
|
41
35
|
*/
|
|
42
36
|
@serializable()
|
|
43
|
-
|
|
37
|
+
limitCount = 60;
|
|
44
38
|
|
|
45
39
|
private _currentCount = 0;
|
|
46
40
|
private _startPosition: Vector3 | null = null;
|
|
@@ -94,8 +88,9 @@ export class Duplicatable extends Behaviour implements IPointerEventHandler {
|
|
|
94
88
|
|
|
95
89
|
if (!this.gameObject.getComponentInParent(ObjectRaycaster))
|
|
96
90
|
this.gameObject.addComponent(ObjectRaycaster);
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
}
|
|
92
|
+
onEnable(): void {
|
|
93
|
+
this.startCoroutine(this.cloneLimitIntervalFn());
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
private _forwardPointerEvents: Map<Object3D, DragControls> = new Map();
|
|
@@ -131,7 +126,12 @@ export class Duplicatable extends Behaviour implements IPointerEventHandler {
|
|
|
131
126
|
}
|
|
132
127
|
}
|
|
133
128
|
else {
|
|
134
|
-
|
|
129
|
+
if (this._currentCount >= this.limitCount) {
|
|
130
|
+
console.warn(`[Duplicatable] Limit of ${this.limitCount} objects created within a few seconds reached. Please wait a moment before creating more objects.`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.warn(`[Duplicatable] Could not duplicate object.`);
|
|
134
|
+
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -145,19 +145,21 @@ export class Duplicatable extends Behaviour implements IPointerEventHandler {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
private cloneLimitIntervalFn() {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
private *cloneLimitIntervalFn() {
|
|
149
|
+
while (this.activeAndEnabled && !this.destroyed) {
|
|
150
|
+
if (this._currentCount > 0) {
|
|
151
|
+
this._currentCount -= 1;
|
|
152
|
+
}
|
|
153
|
+
else if (this._currentCount < 0) {
|
|
154
|
+
this._currentCount = 0;
|
|
155
|
+
}
|
|
156
|
+
yield WaitForSeconds(1);
|
|
152
157
|
}
|
|
153
|
-
setTimeout(() => {
|
|
154
|
-
this.cloneLimitIntervalFn();
|
|
155
|
-
}, (this.limitInterval / this.limitCount) * 1000);
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
private handleDuplication(): Object3D | null {
|
|
159
161
|
if (!this.object) return null;
|
|
160
|
-
if (this._currentCount >= this.limitCount) return null;
|
|
162
|
+
if (this.limitCount > 0 && this._currentCount >= this.limitCount) return null;
|
|
161
163
|
if (this.object === this.gameObject) return null;
|
|
162
164
|
if (GameObject.isDestroyed(this.object)) {
|
|
163
165
|
this.object = null;
|
|
@@ -332,6 +332,7 @@ export class OrbitControls extends Behaviour implements ICameraController {
|
|
|
332
332
|
|
|
333
333
|
this._activePointerEvents = [];
|
|
334
334
|
this.context.input.addEventListener("pointerdown", this._onPointerDown, { queue: InputEventQueue.Early });
|
|
335
|
+
this.context.input.addEventListener("pointerdown", this._onPointerDownLate, { queue: InputEventQueue.Late });
|
|
335
336
|
this.context.input.addEventListener("pointerup", this._onPointerUp, { queue: InputEventQueue.Early });
|
|
336
337
|
this.context.input.addEventListener("pointerup", this._onPointerUpLate, { queue: InputEventQueue.Late });
|
|
337
338
|
}
|
|
@@ -352,6 +353,7 @@ export class OrbitControls extends Behaviour implements ICameraController {
|
|
|
352
353
|
}
|
|
353
354
|
this._activePointerEvents.length = 0;
|
|
354
355
|
this.context.input.removeEventListener("pointerdown", this._onPointerDown);
|
|
356
|
+
this.context.input.removeEventListener("pointerdown", this._onPointerDownLate);
|
|
355
357
|
this.context.input.removeEventListener("pointerup", this._onPointerUp);
|
|
356
358
|
this.context.input.removeEventListener("pointerup", this._onPointerUpLate);
|
|
357
359
|
}
|
|
@@ -363,6 +365,12 @@ export class OrbitControls extends Behaviour implements ICameraController {
|
|
|
363
365
|
private _onPointerDown = (_evt: NEPointerEvent) => {
|
|
364
366
|
this._activePointerEvents.push(_evt);
|
|
365
367
|
}
|
|
368
|
+
private _onPointerDownLate = (evt: NEPointerEvent) => {
|
|
369
|
+
if(evt.used && this._controls) {
|
|
370
|
+
// Disabling orbit controls here because otherwise we get a slight movement when e.g. using DragControls
|
|
371
|
+
this._controls.enabled = false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
366
374
|
|
|
367
375
|
private _onPointerUp = (evt: NEPointerEvent) => {
|
|
368
376
|
// make sure we cleanup the active pointer events
|
|
@@ -847,11 +847,18 @@ export class SkinnedMeshRenderer extends MeshRenderer {
|
|
|
847
847
|
for (const mesh of this.sharedMeshes) {
|
|
848
848
|
if (mesh instanceof SkinnedMesh) {
|
|
849
849
|
this._needUpdateBoundingSphere = false;
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
850
|
+
try {
|
|
851
|
+
const geometry = mesh.geometry;
|
|
852
|
+
const raycastmesh = getRaycastMesh(mesh);
|
|
853
|
+
if (raycastmesh) {
|
|
854
|
+
mesh.geometry = raycastmesh;
|
|
855
|
+
}
|
|
856
|
+
mesh.computeBoundingSphere();
|
|
857
|
+
mesh.geometry = geometry;
|
|
858
|
+
}
|
|
859
|
+
catch(err) {
|
|
860
|
+
console.error(`Error updating bounding sphere for ${mesh.name}`, err);
|
|
861
|
+
}
|
|
855
862
|
}
|
|
856
863
|
}
|
|
857
864
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BatchedMesh, BufferGeometry, Color, Material, Matrix4, Mesh, MeshStandardMaterial, Object3D, RawShaderMaterial } from "three";
|
|
2
2
|
|
|
3
3
|
import { isDevEnvironment, showBalloonError } from "../engine/debug/index.js";
|
|
4
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
|
4
5
|
import { $instancingAutoUpdateBounds, $instancingRenderer, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
|
|
5
6
|
import { Context } from "../engine/engine_setup.js";
|
|
6
7
|
import { getParam, makeIdFromRandomWords } from "../engine/engine_utils.js";
|
|
@@ -337,6 +338,12 @@ class InstancedMeshRenderer {
|
|
|
337
338
|
this._batchedMesh.computeBoundingBox();
|
|
338
339
|
if (sphere)
|
|
339
340
|
this._batchedMesh.computeBoundingSphere();
|
|
341
|
+
if (debugInstancing && this._batchedMesh.boundingSphere) {
|
|
342
|
+
const sphere = this._batchedMesh.boundingSphere;
|
|
343
|
+
// const worldPos = this._batchedMesh.worldPosition.add(sphere.center);
|
|
344
|
+
// const worldRadius = sphere!.radius;
|
|
345
|
+
Gizmos.DrawWireSphere(sphere.center, sphere.radius, 0x00ff00);
|
|
346
|
+
}
|
|
340
347
|
}
|
|
341
348
|
|
|
342
349
|
private _context: Context;
|
|
@@ -401,7 +408,7 @@ class InstancedMeshRenderer {
|
|
|
401
408
|
// break;
|
|
402
409
|
// }
|
|
403
410
|
// }
|
|
404
|
-
if(!canMergeMaterial) {
|
|
411
|
+
if (!canMergeMaterial) {
|
|
405
412
|
return false;
|
|
406
413
|
}
|
|
407
414
|
}
|
|
@@ -566,7 +573,7 @@ class InstancedMeshRenderer {
|
|
|
566
573
|
this.markNeedsUpdate();
|
|
567
574
|
}
|
|
568
575
|
|
|
569
|
-
updateGeometry(geo: BufferGeometry,
|
|
576
|
+
updateGeometry(geo: BufferGeometry, geometryIndex: number): boolean {
|
|
570
577
|
if (!this.validateGeometry(geo)) {
|
|
571
578
|
return false;
|
|
572
579
|
}
|
|
@@ -575,10 +582,11 @@ class InstancedMeshRenderer {
|
|
|
575
582
|
this.grow(geo);
|
|
576
583
|
}
|
|
577
584
|
if (debugInstancing)
|
|
578
|
-
console.debug("UPDATE
|
|
579
|
-
|
|
585
|
+
console.debug("[Instancing] UPDATE GEOMETRY at " + geometryIndex, this._batchedMesh["_geometryCount"], geo.name, getMeshInformation(geo), geo.attributes.position.count, geo.index ? geo.index.count : 0);
|
|
580
586
|
|
|
581
|
-
this._batchedMesh.setGeometryAt(
|
|
587
|
+
this._batchedMesh.setGeometryAt(geometryIndex, geo);
|
|
588
|
+
// for LOD mesh updates we need to make sure to save the geometry index
|
|
589
|
+
this._geometryIds.set(geo, geometryIndex);
|
|
582
590
|
this.markNeedsUpdate();
|
|
583
591
|
return true;
|
|
584
592
|
}
|
|
@@ -588,7 +596,7 @@ class InstancedMeshRenderer {
|
|
|
588
596
|
this._batchedMesh.layers.enableAll();
|
|
589
597
|
|
|
590
598
|
if (this._needUpdateBounds && this._batchedMesh[$instancingAutoUpdateBounds] === true) {
|
|
591
|
-
if (debugInstancing) console.log("Update instancing bounds", this.name, this._batchedMesh.matrixWorldNeedsUpdate);
|
|
599
|
+
if (debugInstancing === "verbose") console.log("Update instancing bounds", this.name, this._batchedMesh.matrixWorldNeedsUpdate);
|
|
592
600
|
this.updateBounds();
|
|
593
601
|
}
|
|
594
602
|
}
|
|
@@ -623,7 +631,9 @@ class InstancedMeshRenderer {
|
|
|
623
631
|
}
|
|
624
632
|
|
|
625
633
|
private markNeedsUpdate() {
|
|
626
|
-
if (debugInstancing
|
|
634
|
+
if (debugInstancing === "verbose") {
|
|
635
|
+
console.warn("Marking instanced mesh dirty", this.name);
|
|
636
|
+
}
|
|
627
637
|
this._needUpdateBounds = true;
|
|
628
638
|
// this.inst.instanceMatrix.needsUpdate = true;
|
|
629
639
|
}
|
|
@@ -641,19 +651,23 @@ class InstancedMeshRenderer {
|
|
|
641
651
|
}
|
|
642
652
|
|
|
643
653
|
private grow(geometry: BufferGeometry) {
|
|
644
|
-
const
|
|
654
|
+
const growFactor = 2;
|
|
655
|
+
const newSize = Math.ceil(this._maxInstanceCount * growFactor);
|
|
645
656
|
|
|
646
657
|
// create a new BatchedMesh instance
|
|
647
658
|
const estimatedSpace = this.tryEstimateVertexCountSize(newSize, [geometry]);// geometry.attributes.position.count;
|
|
648
659
|
// const indices = geometry.index ? geometry.index.count : 0;
|
|
649
660
|
const newMaxVertexCount = Math.max(this._maxVertexCount, estimatedSpace.vertexCount);
|
|
650
|
-
const newMaxIndexCount = Math.max(this._maxIndexCount, estimatedSpace.indexCount, this._maxVertexCount *
|
|
661
|
+
const newMaxIndexCount = Math.max(this._maxIndexCount, estimatedSpace.indexCount, Math.ceil(this._maxVertexCount * growFactor));
|
|
651
662
|
|
|
652
663
|
if (debugInstancing) {
|
|
653
664
|
const geometryInfo = getMeshInformation(geometry);
|
|
654
|
-
console.warn(`Growing
|
|
665
|
+
console.warn(`[Instancing] Growing Buffer\nMesh: \"${this.name}${geometry.name?.length ? "/" + geometry.name : ""}\"\n${geometryInfo.vertexCount} vertices, ${geometryInfo.indexCount} indices\nMax count ${this._maxInstanceCount} → ${newSize}\nMax vertex count ${this._maxVertexCount} -> ${newMaxVertexCount}\nMax index count ${this._maxIndexCount} -> ${newMaxIndexCount}`);
|
|
655
666
|
this._debugMaterial = createDebugMaterial();
|
|
656
667
|
}
|
|
668
|
+
else if (isDevEnvironment()) {
|
|
669
|
+
console.debug(`[Instancing] Growing Buffer\nMesh: \"${this.name}${geometry.name?.length ? "/" + geometry.name : ""}\"\nMax count ${this._maxInstanceCount} → ${newSize}\nMax vertex count ${this._maxVertexCount} -> ${newMaxVertexCount}\nMax index count ${this._maxIndexCount} -> ${newMaxIndexCount}`);
|
|
670
|
+
}
|
|
657
671
|
|
|
658
672
|
this._maxVertexCount = newMaxVertexCount;
|
|
659
673
|
this._maxIndexCount = newMaxIndexCount;
|
|
@@ -679,8 +693,6 @@ class InstancedMeshRenderer {
|
|
|
679
693
|
|
|
680
694
|
// since we have a new batched mesh we need to re-add all the instances
|
|
681
695
|
// fixes https://linear.app/needle/issue/NE-5711
|
|
682
|
-
this._usedBuckets.length = 0;
|
|
683
|
-
this._availableBuckets.length = 0;
|
|
684
696
|
|
|
685
697
|
// add current instances to new instanced mesh
|
|
686
698
|
const original = [...this._handles];
|
|
@@ -745,70 +757,26 @@ class InstancedMeshRenderer {
|
|
|
745
757
|
}
|
|
746
758
|
|
|
747
759
|
|
|
748
|
-
private readonly _availableBuckets = new Array<BucketInfo>();
|
|
749
|
-
private readonly _usedBuckets = new Array<BucketInfo>();
|
|
750
|
-
|
|
751
760
|
private addGeometry(handle: InstanceHandle) {
|
|
752
761
|
|
|
753
|
-
const
|
|
762
|
+
const obj = handle.object;
|
|
763
|
+
const geo = obj.geometry as BufferGeometry;
|
|
754
764
|
if (!geo) {
|
|
755
765
|
// if the geometry is null we cannot add it
|
|
756
766
|
return;
|
|
757
767
|
}
|
|
758
768
|
|
|
759
|
-
// if (handle.reservedVertexCount <= 0 || handle.reservedIndexCount <= 0) {
|
|
760
|
-
// console.error("Cannot add geometry with 0 vertices or indices", handle.name);
|
|
761
|
-
// return;
|
|
762
|
-
// }
|
|
763
|
-
// search the smallest available bucket that fits our handle
|
|
764
|
-
let smallestBucket: BucketInfo | null = null;
|
|
765
|
-
let smallestBucketIndex = -1;
|
|
766
|
-
for (let i = this._availableBuckets.length - 1; i >= 0; i--) {
|
|
767
|
-
const bucket = this._availableBuckets[i];
|
|
768
|
-
if (bucket.vertexCount >= handle.maxVertexCount && bucket.indexCount >= handle.maxIndexCount) {
|
|
769
|
-
if (smallestBucket == null || bucket.vertexCount < smallestBucket.vertexCount) {
|
|
770
|
-
smallestBucket = bucket;
|
|
771
|
-
smallestBucketIndex = i;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
// if we have a bucket that is big enough, use it
|
|
776
|
-
if (smallestBucket != null) {
|
|
777
|
-
const bucket = smallestBucket;
|
|
778
|
-
if (debugInstancing)
|
|
779
|
-
console.debug(`RE-USE SPACE #${bucket.geometryIndex}, ${handle.maxVertexCount} vertices, ${handle.maxIndexCount} indices, ${handle.name}`);
|
|
780
|
-
try {
|
|
781
|
-
this._batchedMesh.setGeometryAt(bucket.geometryIndex, handle.object.geometry as BufferGeometry);
|
|
782
|
-
const newIndex = this._batchedMesh.addInstance(bucket.geometryIndex);
|
|
783
|
-
this._batchedMesh.setMatrixAt(newIndex, handle.object.matrixWorld);
|
|
784
|
-
this._batchedMesh.setVisibleAt(newIndex, true);
|
|
785
|
-
handle.__instanceIndex = newIndex;
|
|
786
|
-
this._usedBuckets[bucket.geometryIndex] = bucket;
|
|
787
|
-
this._availableBuckets.splice(smallestBucketIndex, 1);
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
catch (err) {
|
|
791
|
-
if (debugInstancing)
|
|
792
|
-
console.error("Failed to re-use space", err);
|
|
793
|
-
else if (isDevEnvironment()) {
|
|
794
|
-
console.warn(`Failed to re-use space \"${err instanceof Error ? err.message : err}\" in bucket ${bucket.geometryIndex} (${bucket.vertexCount}) - will add new geometry instead`);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
769
|
// otherwise add more geometry / instances
|
|
800
770
|
let geometryId = this._geometryIds.get(geo);
|
|
801
771
|
if (geometryId === undefined || geometryId === null) {
|
|
802
772
|
if (debugInstancing)
|
|
803
|
-
console.debug(
|
|
804
|
-
|
|
805
|
-
|
|
773
|
+
console.debug(`[Instancing] > ADD NEW GEOMETRY \"${handle.name} (${geo.name}; ${geo.uuid})\"\n${this._currentInstanceCount} instances, ${handle.maxVertexCount} max vertices, ${handle.maxIndexCount} max indices`);
|
|
806
774
|
|
|
807
775
|
geometryId = this._batchedMesh.addGeometry(geo, handle.maxVertexCount, handle.maxIndexCount);
|
|
808
776
|
this._geometryIds.set(geo, geometryId);
|
|
809
777
|
}
|
|
810
778
|
else {
|
|
811
|
-
if (debugInstancing) console.log(
|
|
779
|
+
if (debugInstancing === "verbose") console.log(`[Instancing] > ADD INSTANCE \"${handle.name}\"\nGEOMETRY_ID=${geometryId}\n${this._currentInstanceCount} instances`);
|
|
812
780
|
}
|
|
813
781
|
this._currentVertexCount += handle.maxVertexCount;
|
|
814
782
|
this._currentIndexCount += handle.maxIndexCount;
|
|
@@ -817,10 +785,9 @@ class InstancedMeshRenderer {
|
|
|
817
785
|
handle.__instanceIndex = i;
|
|
818
786
|
handle.__reservedVertexRange = handle.maxVertexCount;
|
|
819
787
|
handle.__reservedIndexRange = handle.maxIndexCount;
|
|
820
|
-
this._usedBuckets[i] = { geometryIndex: geometryId, vertexCount: handle.maxVertexCount, indexCount: handle.maxIndexCount };
|
|
821
788
|
this._batchedMesh.setMatrixAt(i, handle.object.matrixWorld);
|
|
822
789
|
if (debugInstancing)
|
|
823
|
-
console.debug(`
|
|
790
|
+
console.debug(`[Instancing] > ADDED INSTANCE \"${handle.name}\"\nGEOMETRY_ID=${geometryId}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}`);
|
|
824
791
|
|
|
825
792
|
}
|
|
826
793
|
|
|
@@ -830,19 +797,16 @@ class InstancedMeshRenderer {
|
|
|
830
797
|
console.warn("Cannot remove geometry, instance index is invalid", handle.name);
|
|
831
798
|
return;
|
|
832
799
|
}
|
|
833
|
-
this._usedBuckets.splice(handle.__instanceIndex, 1);
|
|
834
800
|
// deleteGeometry is currently not useable since there's no optimize method
|
|
835
801
|
// https://github.com/mrdoob/three.js/issues/27985
|
|
836
802
|
// if (del)
|
|
837
803
|
// this.inst.deleteGeometry(handle.__instanceIndex);
|
|
838
804
|
// else
|
|
839
805
|
// this._batchedMesh.setVisibleAt(handle.__instanceIndex, false);
|
|
806
|
+
if(debugInstancing) {
|
|
807
|
+
console.debug(`[Instancing] < REMOVE INSTANCE \"${handle.name}\" at [${handle.__instanceIndex}]\nGEOMETRY_ID=${handle.__geometryIndex}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}`);
|
|
808
|
+
}
|
|
840
809
|
this._batchedMesh.deleteInstance(handle.__instanceIndex);
|
|
841
|
-
this._availableBuckets.push({
|
|
842
|
-
geometryIndex: handle.__geometryIndex,
|
|
843
|
-
vertexCount: handle.reservedVertexCount,
|
|
844
|
-
indexCount: handle.reservedIndexCount
|
|
845
|
-
});
|
|
846
810
|
}
|
|
847
811
|
}
|
|
848
812
|
|
|
@@ -43,20 +43,20 @@ export class PostProcessingHandler {
|
|
|
43
43
|
this.context = context;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
apply(components: PostProcessingEffect[]) {
|
|
46
|
+
apply(components: PostProcessingEffect[]) : Promise<void> {
|
|
47
47
|
if ("env" in import.meta && import.meta.env.VITE_NEEDLE_USE_POSTPROCESSING === "false") {
|
|
48
48
|
if (debug) console.warn("Postprocessing is disabled via vite env setting");
|
|
49
49
|
else console.debug("Postprocessing is disabled via vite env setting");
|
|
50
|
-
return;
|
|
50
|
+
return Promise.resolve();
|
|
51
51
|
}
|
|
52
52
|
if (!NEEDLE_USE_POSTPROCESSING) {
|
|
53
53
|
if (debug) console.warn("Postprocessing is disabled via global vite define setting");
|
|
54
54
|
else console.debug("Postprocessing is disabled via vite define");
|
|
55
|
-
return;
|
|
55
|
+
return Promise.resolve();
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
this._isActive = true;
|
|
59
|
-
this.onApply(this.context, components);
|
|
59
|
+
return this.onApply(this.context, components);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
unapply() {
|
|
@@ -98,11 +98,12 @@ export class PostProcessingHandler {
|
|
|
98
98
|
|
|
99
99
|
// IMPORTANT
|
|
100
100
|
// Load postprocessing modules ONLY here to get lazy loading of the postprocessing package
|
|
101
|
-
|
|
101
|
+
await Promise.all([
|
|
102
102
|
MODULES.POSTPROCESSING.load(),
|
|
103
103
|
MODULES.POSTPROCESSING_AO.load(),
|
|
104
104
|
// import("./Effects/Sharpening.effect")
|
|
105
105
|
]);
|
|
106
|
+
|
|
106
107
|
// try {
|
|
107
108
|
// internal_SetSharpeningEffectModule(modules[2]);
|
|
108
109
|
// }
|
|
@@ -179,16 +180,15 @@ export class PostProcessingHandler {
|
|
|
179
180
|
// https://github.com/pmndrs/postprocessing/blob/271944b74b543a5b743a62803a167b60cc6bb4ee/src/core/EffectComposer.js#L230C12-L230C12
|
|
180
181
|
renderer[autoclearSetting] = renderer.autoClear;
|
|
181
182
|
|
|
182
|
-
const maxSamples = renderer.capabilities.maxSamples;
|
|
183
183
|
// create composer and set active on context
|
|
184
184
|
if (!this._composer) {
|
|
185
185
|
// const hdrRenderTarget = new WebGLRenderTarget(window.innerWidth, window.innerHeight, { type: HalfFloatType });
|
|
186
186
|
this._composer = new MODULES.POSTPROCESSING.MODULE.EffectComposer(renderer, {
|
|
187
187
|
frameBufferType: HalfFloatType,
|
|
188
188
|
stencilBuffer: true,
|
|
189
|
-
multisampling: Math.min(DeviceUtilities.isMobileDevice() ? 4 : 8, maxSamples),
|
|
190
189
|
});
|
|
191
190
|
}
|
|
191
|
+
|
|
192
192
|
if (context.composer && context.composer !== this._composer) {
|
|
193
193
|
console.warn("There's already an active EffectComposer in your scene: replacing it with a new one. This might cause unexpected behaviour. Make sure to only use one PostprocessingManager/Volume in your scene.");
|
|
194
194
|
}
|
|
@@ -219,18 +219,12 @@ export class PostProcessingHandler {
|
|
|
219
219
|
if (ef instanceof MODULES.POSTPROCESSING.MODULE.Effect)
|
|
220
220
|
effects.push(ef as Effect);
|
|
221
221
|
else if (ef instanceof MODULES.POSTPROCESSING.MODULE.Pass) {
|
|
222
|
-
// const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ...effects);
|
|
223
|
-
// pass.mainScene = scene;
|
|
224
|
-
// pass.name = effects.map(e => e.constructor.name).join(", ");
|
|
225
|
-
// pass.enabled = true;
|
|
226
|
-
// composer.addPass(pass);
|
|
227
|
-
// effects.length = 0;
|
|
228
222
|
composer.addPass(ef as Pass);
|
|
229
223
|
}
|
|
230
224
|
else {
|
|
231
225
|
// seems some effects are not correctly typed, but three can deal with them,
|
|
232
226
|
// so we might need to just pass them through
|
|
233
|
-
|
|
227
|
+
composer.addPass(ef);
|
|
234
228
|
}
|
|
235
229
|
}
|
|
236
230
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Effect } from "postprocessing";
|
|
2
2
|
|
|
3
3
|
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
|
4
|
+
import { Context } from "../../engine/engine_context.js";
|
|
4
5
|
import type { EditorModification, IEditorModification as IEditorModificationReceiver } from "../../engine/engine_editor-sync.js";
|
|
5
6
|
import { serializeable } from "../../engine/engine_serialization_decorator.js";
|
|
6
|
-
import { getParam } from "../../engine/engine_utils.js";
|
|
7
|
+
import { DeviceUtilities, getParam } from "../../engine/engine_utils.js";
|
|
7
8
|
import { Behaviour } from "../Component.js";
|
|
8
9
|
import { EffectWrapper } from "./Effects/EffectWrapper.js";
|
|
9
10
|
import { PostProcessingEffect } from "./PostProcessingEffect.js";
|
|
@@ -58,6 +59,16 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
|
|
|
58
59
|
@serializeable(VolumeProfile)
|
|
59
60
|
sharedProfile?: VolumeProfile;
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Set multisampling to "auto" to automatically adjust the multisampling level based on performance.
|
|
64
|
+
* Set to a number to manually set the multisampling level.
|
|
65
|
+
* @default "auto"
|
|
66
|
+
* @min 0
|
|
67
|
+
* @max renderer.capabilities.maxSamples
|
|
68
|
+
*/
|
|
69
|
+
@serializeable()
|
|
70
|
+
multisampling: "auto" | number = "auto";
|
|
71
|
+
|
|
61
72
|
/**
|
|
62
73
|
* Add a post processing effect to the stack and schedules the effect stack to be re-created.
|
|
63
74
|
*/
|
|
@@ -66,12 +77,15 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
|
|
|
66
77
|
if (!(entry instanceof PostProcessingEffect)) {
|
|
67
78
|
entry = new EffectWrapper(entry);
|
|
68
79
|
}
|
|
69
|
-
if(entry.gameObject === undefined) this.gameObject.addComponent(entry);
|
|
80
|
+
if (entry.gameObject === undefined) this.gameObject.addComponent(entry);
|
|
70
81
|
if (this._effects.includes(entry)) return effect;
|
|
71
82
|
this._effects.push(entry);
|
|
72
83
|
this._isDirty = true;
|
|
73
84
|
return effect;
|
|
74
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Remove a post processing effect from the stack and schedules the effect stack to be re-created.
|
|
88
|
+
*/
|
|
75
89
|
removeEffect<T extends PostProcessingEffect | Effect>(effect: T): T {
|
|
76
90
|
|
|
77
91
|
let index = -1;
|
|
@@ -106,7 +120,7 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
|
|
|
106
120
|
/**
|
|
107
121
|
* When dirty the post processing effects will be re-applied
|
|
108
122
|
*/
|
|
109
|
-
markDirty() {
|
|
123
|
+
markDirty(): void {
|
|
110
124
|
this._isDirty = true;
|
|
111
125
|
}
|
|
112
126
|
|
|
@@ -128,7 +142,13 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
|
|
|
128
142
|
this.sharedProfile?.__init(this);
|
|
129
143
|
}
|
|
130
144
|
|
|
145
|
+
private _componentEnabledTime: number = -1;
|
|
146
|
+
private _multisampleAutoChangeTime: number = 0;
|
|
147
|
+
private _multisampleAutoDecreaseTime: number = 0;
|
|
148
|
+
|
|
149
|
+
/** @internal */
|
|
131
150
|
onEnable(): void {
|
|
151
|
+
this._componentEnabledTime = this.context.time.realtimeSinceStartup;
|
|
132
152
|
this._isDirty = true;
|
|
133
153
|
}
|
|
134
154
|
|
|
@@ -155,7 +175,7 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
|
|
|
155
175
|
}
|
|
156
176
|
}
|
|
157
177
|
|
|
158
|
-
if (this.context.composer && this._postprocessing
|
|
178
|
+
if (this.context.composer && this._postprocessing && this._postprocessing.composer === this.context.composer) {
|
|
159
179
|
if (this.context.renderer.getContext().isContextLost()) {
|
|
160
180
|
this.context.renderer.forceContextRestore();
|
|
161
181
|
}
|
|
@@ -164,6 +184,40 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
|
|
|
164
184
|
|
|
165
185
|
this.context.composer.setMainScene(this.context.scene);
|
|
166
186
|
|
|
187
|
+
const composer = this.context.composer;
|
|
188
|
+
if (this.multisampling === "auto") {
|
|
189
|
+
|
|
190
|
+
const timeSinceLastChange = this.context.time.realtimeSinceStartup - this._multisampleAutoChangeTime;
|
|
191
|
+
|
|
192
|
+
if (this.context.time.realtimeSinceStartup - this._componentEnabledTime > 2
|
|
193
|
+
&& timeSinceLastChange > .5
|
|
194
|
+
) {
|
|
195
|
+
const prev = composer.multisampling;
|
|
196
|
+
|
|
197
|
+
if (composer.multisampling > 0 && this.context.time.smoothedFps <= 50) {
|
|
198
|
+
this._multisampleAutoChangeTime = this.context.time.realtimeSinceStartup;
|
|
199
|
+
this._multisampleAutoDecreaseTime = this.context.time.realtimeSinceStartup;
|
|
200
|
+
composer.multisampling *= .5;
|
|
201
|
+
composer.multisampling = Math.floor(composer.multisampling);
|
|
202
|
+
if (debug) console.debug(`[PostProcessing] Reduced multisampling from ${prev} to ${composer.multisampling}`);
|
|
203
|
+
}
|
|
204
|
+
// if performance is good for a while try increasing multisampling again
|
|
205
|
+
else if (timeSinceLastChange > 1
|
|
206
|
+
&& this.context.time.smoothedFps >= 59
|
|
207
|
+
&& composer.multisampling < this.context.renderer.capabilities.maxSamples
|
|
208
|
+
&& this.context.time.realtimeSinceStartup - this._multisampleAutoDecreaseTime > 10
|
|
209
|
+
) {
|
|
210
|
+
this._multisampleAutoChangeTime = this.context.time.realtimeSinceStartup;
|
|
211
|
+
composer.multisampling = composer.multisampling <= 0 ? 1 : composer.multisampling * 2;
|
|
212
|
+
composer.multisampling = Math.floor(composer.multisampling);
|
|
213
|
+
if (debug) console.debug(`[PostProcessing] Increased multisampling from ${prev} to ${composer.multisampling}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
composer.multisampling = Math.max(0, Math.min(this.multisampling, this.context.renderer.capabilities.maxSamples));
|
|
219
|
+
}
|
|
220
|
+
|
|
167
221
|
// only set the main camera if any pass has a different camera
|
|
168
222
|
// trying to avoid doing this regularly since it involves doing potentially unnecessary work
|
|
169
223
|
// https://github.com/pmndrs/postprocessing/blob/3d3df0576b6d49aec9e763262d5a1ff7429fd91a/src/core/EffectComposer.js#L406
|
|
@@ -222,8 +276,30 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
|
|
|
222
276
|
if (this._activeEffects.length > 0) {
|
|
223
277
|
if (!this._postprocessing)
|
|
224
278
|
this._postprocessing = new PostProcessingHandler(this.context);
|
|
225
|
-
|
|
226
|
-
this.
|
|
279
|
+
|
|
280
|
+
this._postprocessing.apply(this._activeEffects)
|
|
281
|
+
?.then(() => {
|
|
282
|
+
if (!this.activeAndEnabled) return;
|
|
283
|
+
|
|
284
|
+
this._applyPostQueue();
|
|
285
|
+
|
|
286
|
+
const composer = this._postprocessing?.composer;
|
|
287
|
+
if (composer) {
|
|
288
|
+
if (this.multisampling === "auto") {
|
|
289
|
+
composer.multisampling = DeviceUtilities.isMobileDevice()
|
|
290
|
+
? 2
|
|
291
|
+
: 4;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
composer.multisampling = Math.max(0, Math.min(this.multisampling, this.context.renderer.capabilities.maxSamples));
|
|
295
|
+
}
|
|
296
|
+
if (debug) console.debug(`[PostProcessing] Set multisampling to ${composer.multisampling} (Is Mobile: ${DeviceUtilities.isMobileDevice()})`);
|
|
297
|
+
}
|
|
298
|
+
else if (debug) {
|
|
299
|
+
console.warn(`[PostProcessing] No composer found`);
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
227
303
|
}
|
|
228
304
|
|
|
229
305
|
}
|
|
@@ -141,14 +141,14 @@ export class Canvas extends UIRootComponent implements ICanvas {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
start() {
|
|
144
|
-
this.
|
|
144
|
+
this.applyRenderSettings();
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
onEnable() {
|
|
148
148
|
super.onEnable();
|
|
149
149
|
this._updateRenderSettingsRoutine = undefined;
|
|
150
150
|
this._lastMatrixWorld = new Matrix4();
|
|
151
|
-
this.
|
|
151
|
+
this.applyRenderSettings();
|
|
152
152
|
document.addEventListener("resize", this._boundRenderSettingsChanged);
|
|
153
153
|
// We want to run AFTER all regular onBeforeRender callbacks
|
|
154
154
|
this.context.pre_render_callbacks.push(this.onBeforeRenderRoutine);
|