@@ -49,7 +49,11 @@
|
|
49
49
|
private duration = 0;
|
50
50
|
private useRootMotion = false;
|
51
51
|
|
52
|
-
|
52
|
+
/** This value can theoretically be anything – a value of 1 is good to clearly see animation gaps.
|
53
|
+
* For production, a value of 1/60 is enough, since the files can then still properly play back at 60fps.
|
54
|
+
*/
|
55
|
+
static animationDurationPadding = 1 / 60;
|
56
|
+
static restPoseClipDuration = 1 / 60;
|
53
57
|
|
54
58
|
constructor(root: Object3D | null, target: Object3D, clip: AnimationClip | undefined) {
|
55
59
|
this.root = root;
|
@@ -59,7 +63,7 @@
|
|
59
63
|
// this is a rest pose clip.
|
60
64
|
// we assume duration 1/60 and no tracks, and when queried for times we just return [0, duration]
|
61
65
|
if (!clip) {
|
62
|
-
this.duration =
|
66
|
+
this.duration = TransformData.restPoseClipDuration;
|
63
67
|
}
|
64
68
|
else {
|
65
69
|
this.duration = clip.duration;
|
@@ -171,7 +175,6 @@
|
|
171
175
|
|
172
176
|
export class AnimationExtension implements IUSDExporterExtension {
|
173
177
|
|
174
|
-
|
175
178
|
get extensionName(): string { return "animation" }
|
176
179
|
|
177
180
|
private dict: AnimationDict = new Map();
|
@@ -181,9 +184,9 @@
|
|
181
184
|
|
182
185
|
private serializers: SerializeAnimation[] = [];
|
183
186
|
|
184
|
-
|
187
|
+
/** Determines if we inject a rest pose clip for each root - only makes sense for QuickLook */
|
185
188
|
injectRestPoses = false;
|
186
|
-
|
189
|
+
/** Determines if we inject a PlayAnimationOnClick component with "scenestart" trigger - only makes sense for QuickLook */
|
187
190
|
injectImplicitBehaviours = false;
|
188
191
|
|
189
192
|
constructor(quickLookCompatible: boolean) {
|
@@ -191,6 +194,24 @@
|
|
191
194
|
this.injectImplicitBehaviours = quickLookCompatible;
|
192
195
|
}
|
193
196
|
|
197
|
+
getStartTimeCode() {
|
198
|
+
// return the end time + padding of the rest clip, if any exists
|
199
|
+
if (!this.injectRestPoses) return 0;
|
200
|
+
if (this.rootAndClipToRegisteredAnimationMap.size === 0) return 0;
|
201
|
+
return (TransformData.restPoseClipDuration + TransformData.animationDurationPadding) * 60;
|
202
|
+
}
|
203
|
+
|
204
|
+
/** Returns the end time code, based on 60 frames per second, for all registered animations.
|
205
|
+
* This matches the highest time value in the USDZ file. */
|
206
|
+
getEndTimeCode() {
|
207
|
+
let max = 0;
|
208
|
+
for (const [_, info] of this.rootAndClipToRegisteredAnimationMap) {
|
209
|
+
const end = info.start + info.duration;
|
210
|
+
if (end > max) max = end;
|
211
|
+
}
|
212
|
+
return max * 60;
|
213
|
+
}
|
214
|
+
|
194
215
|
getClipCount(root: Object3D): number {
|
195
216
|
// don't count the rest pose
|
196
217
|
let currentCount = this.rootToRegisteredClipCount.get(root);
|
@@ -642,6 +663,17 @@
|
|
642
663
|
|
643
664
|
const timeSampleObjects = createAllTimeSampleObjects ( boneAndInverse.map( x => x.bone ) );
|
644
665
|
|
666
|
+
if (debug) {
|
667
|
+
// find the first..last value in the time samples
|
668
|
+
let min = 10000000;
|
669
|
+
let max = 0;
|
670
|
+
for (const key of timeSampleObjects.position?.keys() ?? []) {
|
671
|
+
min = Math.min(min, key);
|
672
|
+
max = Math.max(max, key);
|
673
|
+
}
|
674
|
+
console.log("Time samples", min, max, timeSampleObjects);
|
675
|
+
}
|
676
|
+
|
645
677
|
writer.beginBlock( `def SkelAnimation "_anim"` );
|
646
678
|
|
647
679
|
// TODO if we include blendshapes we likely need subdivision?
|
@@ -739,6 +771,10 @@
|
|
739
771
|
let currentStartTime = 0;
|
740
772
|
for (let i = 0; i < arr.length; i++) {
|
741
773
|
startTimes.push(currentStartTime);
|
774
|
+
if (arr[i] === undefined) {
|
775
|
+
console.error("Got an undefined transform data, this is likely a bug.", object, arr);
|
776
|
+
continue;
|
777
|
+
}
|
742
778
|
currentStartTime += arr[i].getDuration() + TransformData.animationDurationPadding;
|
743
779
|
}
|
744
780
|
|
@@ -3,9 +3,11 @@
|
|
3
3
|
import { getParam } from "../../../../engine/engine_utils.js";
|
4
4
|
import { Animation } from "../../../Animation.js";
|
5
5
|
import { Animator } from "../../../Animator.js";
|
6
|
+
import { AudioSource } from "../../../AudioSource.js";
|
6
7
|
import { Behaviour, GameObject } from "../../../Component.js";
|
7
|
-
import { AnimationExtension } from "../extensions/Animation.js";
|
8
|
-
import {
|
8
|
+
import type { AnimationExtension } from "../extensions/Animation.js";
|
9
|
+
import type { AudioExtension } from "../extensions/behavior/AudioExtension.js";
|
10
|
+
import { PlayAnimationOnClick, PlayAudioOnClick } from "../extensions/behavior/BehaviourComponents.js";
|
9
11
|
|
10
12
|
const debug = getParam("debugusdz");
|
11
13
|
|
@@ -99,4 +101,36 @@
|
|
99
101
|
}
|
100
102
|
|
101
103
|
return constructedObjects;
|
104
|
+
}
|
105
|
+
|
106
|
+
export function registerAudioSourcesImplictly(root: Object3D, _ext: AudioExtension): Array<Object3D> {
|
107
|
+
const audioSources = GameObject.getComponentsInChildren(root, AudioSource);
|
108
|
+
const playAudioOnClicks = GameObject.getComponentsInChildren(root, PlayAudioOnClick);
|
109
|
+
|
110
|
+
const constructedObjects = new Array<Object3D>();
|
111
|
+
|
112
|
+
// Remove all audio sources that are already referenced from existing PlayAudioOnClick components
|
113
|
+
for (const player of playAudioOnClicks) {
|
114
|
+
if (!player.target) continue;
|
115
|
+
const index = audioSources.indexOf(player.target);
|
116
|
+
if (index > -1) audioSources.splice(index, 1);
|
117
|
+
}
|
118
|
+
|
119
|
+
// for the remaning ones, we want to build a PlayAudioOnClick component
|
120
|
+
for (const audioSource of audioSources) {
|
121
|
+
if (!audioSource || !audioSource.clip) continue;
|
122
|
+
if (audioSource.volume <= 0) continue;
|
123
|
+
|
124
|
+
const newComponent = new PlayAudioOnClick();
|
125
|
+
newComponent.target = audioSource;
|
126
|
+
newComponent.name = "PlayAudioOnClick_implicitAtStart_";
|
127
|
+
const go = new Object3D();
|
128
|
+
GameObject.addComponent(go, newComponent);
|
129
|
+
console.log("implicit PlayAudioOnStart", go, newComponent);
|
130
|
+
constructedObjects.push(go);
|
131
|
+
|
132
|
+
root.add(go);
|
133
|
+
}
|
134
|
+
|
135
|
+
return constructedObjects;
|
102
136
|
}
|
@@ -16,5 +16,6 @@
|
|
16
16
|
import "./CameraUtils.js"
|
17
17
|
import "./AnimationUtils.js"
|
18
18
|
|
19
|
+
export { DragMode } from "./DragControls.js"
|
19
20
|
export { ParticleSystemBaseBehaviour, type QParticle, type QParticleBehaviour } from "./ParticleSystem.js"
|
20
21
|
export { ParticleSystemShapeType } from "./ParticleSystemModules.js"
|
@@ -43,6 +43,7 @@
|
|
43
43
|
export * from "./engine_texture.js";
|
44
44
|
export * from "./engine_three_utils.js";
|
45
45
|
export * from "./engine_time.js";
|
46
|
+
export * from "./engine_time_utils.js";
|
46
47
|
export * from "./engine_types.js";
|
47
48
|
export { registerType,TypeStore } from "./engine_typestore.js";
|
48
49
|
export { prefix,validate } from "./engine_util_decorator.js";
|
@@ -1,17 +1,30 @@
|
|
1
1
|
import { Object3D } from "three";
|
2
2
|
|
3
|
+
import { getParam } from "../../../../../engine/engine_utils.js";
|
3
4
|
import { AudioSource } from "../../../../AudioSource.js";
|
4
5
|
import { GameObject } from "../../../../Component.js";
|
5
6
|
import type { IUSDExporterExtension } from "../../Extension.js";
|
6
|
-
import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
7
|
+
import { makeNameSafeForUSD,USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
7
8
|
|
9
|
+
const debug = getParam("debugusdz");
|
10
|
+
|
8
11
|
export class AudioExtension implements IUSDExporterExtension {
|
9
12
|
|
13
|
+
static getName(clip: string) {
|
14
|
+
const clipExt = clip.split(".").pop();
|
15
|
+
const fileWithoutExt = clip.split(".").slice(0, -1).join(".");
|
16
|
+
let clipName = fileWithoutExt.split("/").pop()?.replace(".", "_");
|
17
|
+
if (!clipName) {
|
18
|
+
clipName = "Audio_" + Math.random().toString(36).substring(2, 15);
|
19
|
+
}
|
20
|
+
return makeNameSafeForUSD(clipName) + "." + clipExt;
|
21
|
+
}
|
22
|
+
|
10
23
|
get extensionName(): string {
|
11
24
|
return "Audio";
|
12
25
|
}
|
13
26
|
|
14
|
-
private files: string
|
27
|
+
private files = new Array<{ path: string, name: string }>();
|
15
28
|
|
16
29
|
onExportObject?(object: Object3D, model : USDObject, _context: USDZExporterContext) {
|
17
30
|
// check if this object has an audio source, add the relevant schema in that case.
|
@@ -27,25 +40,33 @@
|
|
27
40
|
if (!audioSource.playOnAwake)
|
28
41
|
continue;
|
29
42
|
|
30
|
-
const clipName = audioSource.clip.split("/").pop();
|
31
|
-
|
32
|
-
|
43
|
+
const clipName = audioSource.clip.split("/").pop() || "Audio";
|
44
|
+
const safeClipNameWithExt = AudioExtension.getName(audioSource.clip);
|
45
|
+
const safeClipName = makeNameSafeForUSD(safeClipNameWithExt);
|
33
46
|
|
34
|
-
|
35
|
-
|
36
|
-
this.files.push(audioSource.clip);
|
47
|
+
if (!this.files.some(f => f.path === audioSource.clip)) {
|
48
|
+
this.files.push({ path: audioSource.clip, name: safeClipNameWithExt });
|
37
49
|
}
|
38
50
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
51
|
+
// Turns out that in visionOS versions, using UsdAudio together with preliminary behaviours
|
52
|
+
// is not supported. So we need to NOT export SpatialAudio in those cases, and just rely
|
53
|
+
// on Preliminary_Behavior to play the audio.
|
54
|
+
if (!_context.quickLookCompatible)
|
55
|
+
{
|
56
|
+
model.addEventListener('serialize', (writer: USDWriter, _context: USDZExporterContext) => {
|
57
|
+
writer.appendLine();
|
58
|
+
writer.beginBlock(`def SpatialAudio "${safeClipName}"`, "(", false );
|
59
|
+
writer.appendLine(`displayName = "${clipName}"`);
|
60
|
+
writer.closeBlock( ")" );
|
61
|
+
writer.beginBlock();
|
62
|
+
writer.appendLine(`uniform asset filePath = @audio/${safeClipNameWithExt}@`);
|
63
|
+
writer.appendLine(`uniform token auralMode = "${ audioSource.spatialBlend > 0 ? "spatial" : "nonSpatial" }"`);
|
64
|
+
// theoretically we could do timeline-like audio sequencing with this.
|
65
|
+
writer.appendLine(`uniform token playbackMode = "${audioSource.loop ? "loopFromStage" : "onceFromStart" }"`);
|
66
|
+
writer.appendLine(`uniform float gain = ${audioSource.volume}`);
|
67
|
+
writer.closeBlock();
|
68
|
+
});
|
69
|
+
}
|
49
70
|
}
|
50
71
|
}
|
51
72
|
}
|
@@ -53,17 +74,20 @@
|
|
53
74
|
async onAfterSerialize(context: USDZExporterContext) {
|
54
75
|
// write the files to the context.
|
55
76
|
for (const file of this.files) {
|
77
|
+
const key = "audio/" + file.name;
|
78
|
+
if (context.files[key]) {
|
79
|
+
if(debug) console.warn("Audio file with name " + key + " already exists in the context. Skipping.");
|
80
|
+
continue;
|
81
|
+
}
|
56
82
|
|
57
|
-
const clipName = file.split("/").pop();
|
58
|
-
|
59
83
|
// convert file (which is a path) to a blob.
|
60
|
-
const audio = await fetch(file);
|
84
|
+
const audio = await fetch(file.path);
|
61
85
|
const audioBlob = await audio.blob();
|
62
86
|
const arrayBuffer = await audioBlob.arrayBuffer();
|
63
87
|
|
64
88
|
const audioData: Uint8Array = new Uint8Array(arrayBuffer)
|
65
89
|
|
66
|
-
context.files[
|
90
|
+
context.files[key] = audioData;
|
67
91
|
}
|
68
92
|
}
|
69
93
|
}
|
@@ -313,6 +313,7 @@
|
|
313
313
|
sound.setDistanceModel('linear');
|
314
314
|
break;
|
315
315
|
case AudioRolloffMode.Custom:
|
316
|
+
console.warn("Custom rolloff for AudioSource is not supported: " + this.name);
|
316
317
|
break;
|
317
318
|
}
|
318
319
|
|
@@ -3,9 +3,9 @@
|
|
3
3
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
4
4
|
import { ObjectUtils, PrimitiveType } from "../../engine/engine_create_objects.js";
|
5
5
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
6
|
-
import { IGameObject } from "../../engine/engine_types.js";
|
6
|
+
import type { IGameObject } from "../../engine/engine_types.js";
|
7
7
|
import { getParam,PromiseAllWithErrors } from "../../engine/engine_utils.js";
|
8
|
-
import { NeedleXREventArgs, NeedleXRSession, NeedleXRUtils } from "../../engine/xr/index.js";
|
8
|
+
import { type NeedleXREventArgs, NeedleXRSession, NeedleXRUtils } from "../../engine/xr/index.js";
|
9
9
|
import { PlayerState } from "../../engine-components-experimental/networking/PlayerSync.js";
|
10
10
|
import { Behaviour, GameObject } from "../Component.js";
|
11
11
|
import { SyncedTransform } from "../SyncedTransform.js";
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
5
5
|
import { BehaviorModel } from "./BehavioursBuilder.js";
|
6
6
|
|
7
|
-
const debug = getParam("
|
7
|
+
const debug = getParam("debugusdzbehaviours");
|
8
8
|
|
9
9
|
export interface UsdzBehaviour {
|
10
10
|
createBehaviours?(ext: BehaviorExtension, model: USDObject, context: USDZExporterContext): void;
|
@@ -3,6 +3,7 @@
|
|
3
3
|
import { isDevEnvironment, showBalloonWarning } from "../../../../../engine/debug/index.js";
|
4
4
|
import { serializable } from "../../../../../engine/engine_serialization_decorator.js";
|
5
5
|
import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../../../../../engine/engine_three_utils.js";
|
6
|
+
import { getParam } from "../../../../../engine/engine_utils.js";
|
6
7
|
import type { State } from "../../../../../engine/extensions/NEEDLE_animator_controller_model.js";
|
7
8
|
import { NEEDLE_progressive } from "../../../../../engine/extensions/NEEDLE_progressive.js";
|
8
9
|
import { Animator } from "../../../../Animator.js";
|
@@ -13,9 +14,12 @@
|
|
13
14
|
import { ObjectRaycaster,Raycaster } from "../../../../ui/Raycaster.js";
|
14
15
|
import { USDDocument, USDObject, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
15
16
|
import { AnimationExtension, RegisteredAnimationInfo, type UsdzAnimation } from "../Animation.js";
|
17
|
+
import { AudioExtension } from "./AudioExtension.js";
|
16
18
|
import type { BehaviorExtension, UsdzBehaviour } from "./Behaviour.js";
|
17
|
-
import { ActionBuilder, ActionModel, AuralMode, BehaviorModel,
|
19
|
+
import { ActionBuilder, ActionModel, AuralMode, BehaviorModel, type IBehaviorElement, MotionType, MultiplePerformOperation,PlayAction, Space, TriggerBuilder } from "./BehavioursBuilder.js";
|
18
20
|
|
21
|
+
const debug = getParam("debugusdzbehaviours");
|
22
|
+
|
19
23
|
function ensureRaycaster(obj: GameObject) {
|
20
24
|
if (!obj) return;
|
21
25
|
if (!obj.getComponentInParent(Raycaster)) {
|
@@ -651,11 +655,16 @@
|
|
651
655
|
if (typeof clipUrl !== "string") return;
|
652
656
|
|
653
657
|
const playbackTarget = this.target ? this.target.gameObject : this.gameObject;
|
654
|
-
const clipName =
|
658
|
+
const clipName = AudioExtension.getName(clipUrl);
|
655
659
|
const volume = this.target ? this.target.volume : 1;
|
656
660
|
const auralMode = this.target && this.target.spatialBlend == 0 ? AuralMode.NonSpatial : AuralMode.Spatial;
|
657
661
|
|
658
|
-
//
|
662
|
+
// This checks if any child is clickable – if yes, the tap trigger is added; if not, we omit it.
|
663
|
+
let anyChildHasGeometry = false;
|
664
|
+
this.gameObject.traverse(c => {
|
665
|
+
if (c instanceof Mesh && c.visible) anyChildHasGeometry = true;
|
666
|
+
});
|
667
|
+
if (anyChildHasGeometry)
|
659
668
|
{
|
660
669
|
let playAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, "audio/" + clipName, PlayAction.Play, volume, auralMode);
|
661
670
|
// does not seem to work in iOS / QuickLook...
|
@@ -674,7 +683,7 @@
|
|
674
683
|
let playAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, "audio/" + clipName, PlayAction.Play, volume, auralMode);
|
675
684
|
if (this.target.loop)
|
676
685
|
playAction = ActionBuilder.sequence(playAction).makeLooping();
|
677
|
-
const playClipOnStart = new BehaviorModel("playAudioOnStart " + this.name,
|
686
|
+
const playClipOnStart = new BehaviorModel("playAudioOnStart" + (this.name ? "_" + this.name : ""),
|
678
687
|
TriggerBuilder.sceneStartTrigger(),
|
679
688
|
playAction,
|
680
689
|
);
|
@@ -688,15 +697,17 @@
|
|
688
697
|
const clipUrl = this.clip ? this.clip : this.target ? this.target.clip : undefined;
|
689
698
|
if (!clipUrl) return;
|
690
699
|
if (typeof clipUrl !== "string") return;
|
691
|
-
const clipName = clipUrl.split("/").pop();
|
692
700
|
|
701
|
+
const clipName = AudioExtension.getName(clipUrl);
|
702
|
+
const filesKey = "audio/" + clipName;
|
703
|
+
// if the clip was already added, don't add it again
|
704
|
+
if (context.files[filesKey]) return;
|
705
|
+
|
693
706
|
const audio = await fetch(clipUrl);
|
694
707
|
const audioBlob = await audio.blob();
|
695
708
|
const arrayBuffer = await audioBlob.arrayBuffer();
|
696
|
-
|
697
709
|
const audioData: Uint8Array = new Uint8Array(arrayBuffer)
|
698
|
-
|
699
|
-
context.files["audio/" + clipName] = audioData;
|
710
|
+
context.files[filesKey] = audioData;
|
700
711
|
}
|
701
712
|
}
|
702
713
|
|
@@ -821,7 +832,7 @@
|
|
821
832
|
});
|
822
833
|
}
|
823
834
|
|
824
|
-
createAnimation(ext, model, _context) {
|
835
|
+
createAnimation(ext: AnimationExtension, model: USDObject, _context: USDZExporterContext) {
|
825
836
|
if (!this.target || !this.animator) return;
|
826
837
|
|
827
838
|
// If there's a separate state specified to play after this one, we
|
@@ -880,12 +891,12 @@
|
|
880
891
|
const firstStateInLoop = visitedStates.indexOf(currentState);
|
881
892
|
statesUntilLoop = visitedStates.slice(0, firstStateInLoop); // can be empty, which means we're looping all
|
882
893
|
statesLooping = visitedStates.slice(firstStateInLoop); // can be empty, which means nothing is looping
|
883
|
-
console.log("found loop from " + this.stateName, "states until loop", statesUntilLoop, "states looping", statesLooping);
|
894
|
+
if (debug) console.log("found loop from " + this.stateName, "states until loop", statesUntilLoop, "states looping", statesLooping);
|
884
895
|
}
|
885
896
|
else {
|
886
897
|
statesUntilLoop = visitedStates;
|
887
898
|
statesLooping = [];
|
888
|
-
console.log("found no loop from " + this.stateName, "states", statesUntilLoop);
|
899
|
+
if (debug) console.log("found no loop from " + this.stateName, "states", statesUntilLoop);
|
889
900
|
}
|
890
901
|
}
|
891
902
|
|
@@ -900,17 +911,28 @@
|
|
900
911
|
}
|
901
912
|
this.stateAnimationModel = model;
|
902
913
|
|
914
|
+
const addStateToSequence = (state: State, sequence: Array<RegisteredAnimationInfo>) => {
|
915
|
+
if (!this.target) return;
|
916
|
+
if (!state.motion?.clip) {
|
917
|
+
console.warn("No clip found for state " + state.name + " on " + this.animator?.name + ", can't export animation data");
|
918
|
+
return;
|
919
|
+
}
|
920
|
+
const anim = ext.registerAnimation(this.target, state.motion.clip);
|
921
|
+
if (anim) sequence.push(anim);
|
922
|
+
else console.warn("Couldn't register animation for state " + state.name + " on " + this.animator?.name);
|
923
|
+
};
|
924
|
+
|
903
925
|
// Register all the animation states we found.
|
904
926
|
if (statesUntilLoop.length > 0) {
|
905
927
|
this.animationSequence = new Array<RegisteredAnimationInfo>();
|
906
928
|
for (const state of statesUntilLoop) {
|
907
|
-
|
929
|
+
addStateToSequence(state, this.animationSequence);
|
908
930
|
}
|
909
931
|
}
|
910
932
|
if (statesLooping.length > 0) {
|
911
933
|
this.animationLoopAfterSequence = new Array<RegisteredAnimationInfo>();
|
912
934
|
for (const state of statesLooping) {
|
913
|
-
|
935
|
+
addStateToSequence(state, this.animationLoopAfterSequence);
|
914
936
|
}
|
915
937
|
}
|
916
938
|
}
|
@@ -404,12 +404,12 @@
|
|
404
404
|
export class ActionBuilder {
|
405
405
|
|
406
406
|
static sequence(...params: IBehaviorElement[]) {
|
407
|
-
const group = new GroupActionModel("
|
407
|
+
const group = new GroupActionModel("Group_" + GroupActionModel.getId(), params);
|
408
408
|
return group.makeSequence();
|
409
409
|
}
|
410
410
|
|
411
411
|
static parallel(...params: IBehaviorElement[]) {
|
412
|
-
const group = new GroupActionModel("
|
412
|
+
const group = new GroupActionModel("Group_" + GroupActionModel.getId(), params);
|
413
413
|
return group.makeParallel();
|
414
414
|
}
|
415
415
|
|
@@ -5,7 +5,7 @@
|
|
5
5
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
6
6
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
7
7
|
import { delayForFrames, getParam } from "../../engine/engine_utils.js";
|
8
|
-
import { NeedleXREventArgs } from "../../engine/xr/index.js";
|
8
|
+
import { type NeedleXREventArgs } from "../../engine/xr/index.js";
|
9
9
|
import { Camera } from "../Camera.js";
|
10
10
|
import { GameObject } from "../Component.js";
|
11
11
|
import { BaseUIComponent, UIRootComponent } from "./BaseUIComponent.js";
|
@@ -3,14 +3,14 @@
|
|
3
3
|
import { isDevEnvironment } from "../engine/debug/index.js";
|
4
4
|
import { addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../engine/engine_components.js";
|
5
5
|
import { activeInHierarchyFieldName } from "../engine/engine_constants.js";
|
6
|
-
import { destroy, findByGuid, foreachComponent, HideFlags, IInstantiateOptions, instantiate, isActiveInHierarchy, isActiveSelf, isDestroyed, isUsingInstancing, markAsInstancedRendered, setActive } from "../engine/engine_gameobject.js";
|
6
|
+
import { destroy, findByGuid, foreachComponent, HideFlags, type IInstantiateOptions, instantiate, isActiveInHierarchy, isActiveSelf, isDestroyed, isUsingInstancing, markAsInstancedRendered, setActive } from "../engine/engine_gameobject.js";
|
7
7
|
import * as main from "../engine/engine_mainloop_utils.js";
|
8
8
|
import { syncDestroy, syncInstantiate } from "../engine/engine_networking_instantiate.js";
|
9
9
|
import { Context, FrameEvent } from "../engine/engine_setup.js";
|
10
10
|
import * as threeutils from "../engine/engine_three_utils.js";
|
11
11
|
import type { Collision, Constructor, ConstructorConcrete, GuidsMap, ICollider, IComponent, IGameObject, SourceIdentifier } from "../engine/engine_types.js";
|
12
|
-
import { INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs } from "../engine/engine_xr.js";
|
13
|
-
import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
12
|
+
import type { INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs } from "../engine/engine_xr.js";
|
13
|
+
import { type IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
14
14
|
|
15
15
|
|
16
16
|
// export interface ISerializationCallbackReceiver {
|
@@ -19,9 +19,9 @@
|
|
19
19
|
|
20
20
|
if (!suppressConsole && (showConsole || isLocalNetwork())) {
|
21
21
|
if (isLocalNetwork()) {
|
22
|
-
const
|
23
|
-
|
24
|
-
console.log("🌵 Tip: You can add the \"?console\" query parameter to the url to show the debug console (on mobile it will automatically open in the bottom right corner when your get errors during development)", "\nOpen this page console: " +
|
22
|
+
const consoleUrl = new URL(window.location.href);
|
23
|
+
consoleUrl.searchParams.set("console", "1");
|
24
|
+
console.log("🌵 Tip: You can add the \"?console\" query parameter to the url to show the debug console (on mobile it will automatically open in the bottom right corner when your get errors during development. In VR a spatial console will appear.)", "\nOpen this page to get the console: " + consoleUrl.toString());
|
25
25
|
}
|
26
26
|
const isMobile = isMobileDevice() || (isQuest() && isDevEnvironment());
|
27
27
|
if (isMobile) {
|
@@ -5,6 +5,7 @@
|
|
5
5
|
|
6
6
|
export { showDebugConsole }
|
7
7
|
export { LogType, setAllowOverlayMessages };
|
8
|
+
export { enableSpatialConsole } from "./debug_spatial_console.js";
|
8
9
|
|
9
10
|
const noDevLogs = getParam("nodevlogs");
|
10
11
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { AxesHelper, Box3, BufferGeometry, Camera, Color,
|
1
|
+
import { AxesHelper, Box3, BufferGeometry, Camera, Color, Line, LineBasicMaterial, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Quaternion, Ray, Raycaster, SphereGeometry, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { Gizmos } from "../engine/engine_gizmos.js";
|
4
4
|
import { InstancingUtil } from "../engine/engine_instancing.js";
|
@@ -7,7 +7,7 @@
|
|
7
7
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
8
|
import { Context } from "../engine/engine_setup.js";
|
9
9
|
import { getWorldPosition, setWorldPosition } from "../engine/engine_three_utils.js";
|
10
|
-
import { IGameObject } from "../engine/engine_types.js";
|
10
|
+
import { type IGameObject } from "../engine/engine_types.js";
|
11
11
|
import { getParam } from "../engine/engine_utils.js";
|
12
12
|
import { NeedleXRSession } from "../engine/engine_xr.js";
|
13
13
|
import { Avatar_POI } from "./avatar/Avatar_Brain_LookAt.js";
|
@@ -32,6 +32,8 @@
|
|
32
32
|
DynamicViewAngle = 3,
|
33
33
|
/** The drag plane is adjusted dynamically while dragging. */
|
34
34
|
SnapToSurfaces = 4,
|
35
|
+
/** Don't allow dragging the object */
|
36
|
+
None = 5,
|
35
37
|
}
|
36
38
|
|
37
39
|
export class DragControls extends Behaviour implements IPointerEventHandler {
|
@@ -121,6 +123,12 @@
|
|
121
123
|
onPointerEnter(evt: PointerEventData) {
|
122
124
|
if (!this.allowEdit(this.gameObject)) return;
|
123
125
|
if (evt.mode !== "screen") return;
|
126
|
+
|
127
|
+
// get the drag mode and check if we need to abort early here
|
128
|
+
const isSpatialInput = evt.event.mode === "tracked-pointer";
|
129
|
+
const dragMode = isSpatialInput ? this.xrDragMode : this.dragMode;
|
130
|
+
if (dragMode === DragMode.None) return;
|
131
|
+
|
124
132
|
const dc = GameObject.getComponentInParent(evt.object, DragControls);
|
125
133
|
if (!dc || dc !== this) return;
|
126
134
|
DragControls.lastHovered = evt.object;
|
@@ -137,6 +145,12 @@
|
|
137
145
|
onPointerDown(args: PointerEventData) {
|
138
146
|
if (!this.allowEdit(this.gameObject)) return;
|
139
147
|
if (args.used) return;
|
148
|
+
|
149
|
+
// get the drag mode and check if we need to abort early here
|
150
|
+
const isSpatialInput = args.mode === "tracked-pointer";
|
151
|
+
const dragMode = isSpatialInput ? this.xrDragMode : this.dragMode;
|
152
|
+
if (dragMode === DragMode.None) return;
|
153
|
+
|
140
154
|
DragControls.lastHovered = args.object;
|
141
155
|
|
142
156
|
if (args.button === 0) {
|
@@ -743,6 +757,8 @@
|
|
743
757
|
else
|
744
758
|
this._dragPlane.setFromNormalAndCoplanarPoint(rayDirection, hitWP);
|
745
759
|
break;
|
760
|
+
case DragMode.None:
|
761
|
+
break;
|
746
762
|
}
|
747
763
|
|
748
764
|
// calculate bounding box and snapping points. We want to either snap the "back" point or the "bottom" point.
|
@@ -864,6 +880,8 @@
|
|
864
880
|
const keepRotation = isSpatialInput ? this.settings.xrKeepRotation : this.settings.keepRotation;
|
865
881
|
const dragMode = isSpatialInput ? this.settings.xrDragMode : this.settings.dragMode;
|
866
882
|
|
883
|
+
if (dragMode === DragMode.None) return;
|
884
|
+
|
867
885
|
const lerpStrength = 10;
|
868
886
|
// - keeping rotation constant during dragging
|
869
887
|
if (keepRotation) this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
|
@@ -5,7 +5,7 @@
|
|
5
5
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
6
6
|
import { Behaviour, GameObject } from "./Component.js";
|
7
7
|
import { DragControls } from "./DragControls.js";
|
8
|
-
import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
8
|
+
import { type IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
9
9
|
import { ObjectRaycaster } from "./ui/Raycaster.js";
|
10
10
|
|
11
11
|
export class Duplicatable extends Behaviour implements IPointerEventHandler {
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Group, Object3D, Texture, TextureLoader } from "three";
|
2
2
|
|
3
3
|
import { deepClone, getParam, resolveUrl } from "../engine/engine_utils.js";
|
4
|
-
import { destroy, IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
|
4
|
+
import { destroy, type IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
|
5
5
|
import { getLoader } from "./engine_gltf.js";
|
6
6
|
import { processNewScripts } from "./engine_mainloop_utils.js";
|
7
7
|
import { registerPrefabProvider, syncInstantiate } from "./engine_networking_instantiate.js";
|
@@ -27,7 +27,7 @@
|
|
27
27
|
import { RendererData as SceneLighting } from './engine_scenelighting.js';
|
28
28
|
import { logHierarchy } from './engine_three_utils.js';
|
29
29
|
import { Time } from './engine_time.js';
|
30
|
-
import {
|
30
|
+
import type { CoroutineData, GLTF, ICamera, IComponent, IContext, ILight, LoadedGLTF } from "./engine_types.js";
|
31
31
|
import * as utils from "./engine_utils.js";
|
32
32
|
import { delay, getParam } from './engine_utils.js';
|
33
33
|
import type { INeedleXRSessionEventReceiver, NeedleXRSession } from './engine_xr.js';
|
@@ -1,6 +1,6 @@
|
|
1
|
-
import { BoxGeometry,
|
1
|
+
import { BoxGeometry, Material, Mesh, MeshStandardMaterial, PlaneGeometry, SphereGeometry } from "three"
|
2
2
|
|
3
|
-
import { Vec3 } from "./engine_types.js";
|
3
|
+
import type { Vec3 } from "./engine_types.js";
|
4
4
|
|
5
5
|
export enum PrimitiveType {
|
6
6
|
Quad = 0,
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import { showBalloonMessage, showBalloonWarning } from './debug/debug.js';
|
4
4
|
import { Context } from './engine_setup.js';
|
5
5
|
import type { ButtonName, IGameObject, IInput, MouseButtonName, Vec2 } from './engine_types.js';
|
6
|
-
import { EnumToPrimitiveUnion, getParam, isMozillaXR } from './engine_utils.js';
|
6
|
+
import { type EnumToPrimitiveUnion, getParam, isMozillaXR } from './engine_utils.js';
|
7
7
|
|
8
8
|
const debug = getParam("debuginput");
|
9
9
|
|
@@ -226,7 +226,10 @@
|
|
226
226
|
}
|
227
227
|
}
|
228
228
|
private dispatchEvent(evt: NEPointerEvent | NEKeyboardEvent) {
|
229
|
-
|
229
|
+
/** True when the next event queue should not be invoked */
|
230
|
+
let preventNextEventQueue = false;
|
231
|
+
|
232
|
+
// Handle keyboard event
|
230
233
|
if (evt instanceof NEKeyboardEvent) {
|
231
234
|
const listeners = this._eventListeners[evt.type];
|
232
235
|
if (listeners) {
|
@@ -236,17 +239,24 @@
|
|
236
239
|
}
|
237
240
|
}
|
238
241
|
}
|
239
|
-
|
242
|
+
|
243
|
+
// Hnadle pointer event
|
244
|
+
if (evt instanceof NEPointerEvent) {
|
240
245
|
const listeners = this._eventListeners[evt.type];
|
241
246
|
if (listeners) {
|
242
247
|
for (const queue of listeners) {
|
243
|
-
if (
|
248
|
+
if (preventNextEventQueue) break;
|
244
249
|
for (const l of queue.listeners) {
|
245
250
|
if (evt.immediatePropagationStopped) {
|
246
|
-
|
251
|
+
preventNextEventQueue = true;
|
247
252
|
if (debug) console.log("immediatePropagationStopped", evt.type);
|
248
253
|
break;
|
249
254
|
}
|
255
|
+
else if (evt.propagationStopped) {
|
256
|
+
preventNextEventQueue = true;
|
257
|
+
if (debug) console.log("propagationStopped", evt.type);
|
258
|
+
// we do not break here but continue invoking the listeners in the queue
|
259
|
+
}
|
250
260
|
(l as PointerEventListener)(evt);
|
251
261
|
}
|
252
262
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { FrameEvent } from "./engine_context.js";
|
2
2
|
import { ContextEvent } from "./engine_context_registry.js";
|
3
|
-
import { LifecycleMethod, registerFrameEventCallback } from "./engine_lifecycle_functions_internal.js";
|
3
|
+
import { type LifecycleMethod, registerFrameEventCallback } from "./engine_lifecycle_functions_internal.js";
|
4
4
|
|
5
5
|
|
6
6
|
/**
|
@@ -1,5 +1,7 @@
|
|
1
1
|
import type { Vector } from "three";
|
2
2
|
|
3
|
+
import type { Vec3 } from "./engine_types.js";
|
4
|
+
|
3
5
|
class MathHelper {
|
4
6
|
|
5
7
|
random(min?: number, max?: number): number {
|
@@ -142,6 +144,12 @@
|
|
142
144
|
dx: LowPassFilter;
|
143
145
|
lasttime: number | null;
|
144
146
|
|
147
|
+
/** Create a new OneEuroFilter
|
148
|
+
* @param freq - An estimate of the frequency in Hz of the signal (> 0), if timestamps are not available.
|
149
|
+
* @param minCutOff - Min cutoff frequency in Hz (> 0). Lower values allow to remove more jitter.
|
150
|
+
* @param beta - Parameter to reduce latency (> 0). Higher values make the filter react faster to changes.
|
151
|
+
* @param dCutOff - Used to filter the derivates. 1 Hz by default. Change this parameter if you know what you are doing.
|
152
|
+
*/
|
145
153
|
constructor(freq: number, minCutOff = 1.0, beta = 0.0, dCutOff = 1.0) {
|
146
154
|
if (freq <= 0 || minCutOff <= 0 || dCutOff <= 0) {
|
147
155
|
throw new Error();
|
@@ -161,6 +169,7 @@
|
|
161
169
|
return 1.0 / (1.0 + tau / te);
|
162
170
|
}
|
163
171
|
|
172
|
+
/** Filter your value: call with your value and the current timestamp (e.g. from this.context.time.time) */
|
164
173
|
filter(x: number, time: number | null = null) {
|
165
174
|
if (this.lasttime && time) {
|
166
175
|
this.freq = 1.0 / (time - this.lasttime);
|
@@ -172,4 +181,28 @@
|
|
172
181
|
const cutOff = this.minCutOff + this.beta * Math.abs(edx);
|
173
182
|
return this.x.filter(x, this.alpha(cutOff));
|
174
183
|
}
|
184
|
+
}
|
185
|
+
|
186
|
+
export class OneEuroFilterXYZ {
|
187
|
+
readonly x: OneEuroFilter;
|
188
|
+
readonly y: OneEuroFilter;
|
189
|
+
readonly z: OneEuroFilter;
|
190
|
+
|
191
|
+
/** Create a new OneEuroFilter
|
192
|
+
* @param freq - An estimate of the frequency in Hz of the signal (> 0), if timestamps are not available.
|
193
|
+
* @param minCutOff - Min cutoff frequency in Hz (> 0). Lower values allow to remove more jitter.
|
194
|
+
* @param beta - Parameter to reduce latency (> 0). Higher values make the filter react faster to changes.
|
195
|
+
* @param dCutOff - Used to filter the derivates. 1 Hz by default. Change this parameter if you know what you are doing.
|
196
|
+
*/
|
197
|
+
constructor(freq: number, minCutOff = 1.0, beta = 0.0, dCutOff = 1.0) {
|
198
|
+
this.x = new OneEuroFilter(freq, minCutOff, beta, dCutOff);
|
199
|
+
this.y = new OneEuroFilter(freq, minCutOff, beta, dCutOff);
|
200
|
+
this.z = new OneEuroFilter(freq, minCutOff, beta, dCutOff);
|
201
|
+
}
|
202
|
+
|
203
|
+
filter(value: Vec3, target: Vec3, time: number | null = null) {
|
204
|
+
target.x = this.x.filter(value.x, time);
|
205
|
+
target.y = this.y.filter(value.y, time);
|
206
|
+
target.z = this.z.filter(value.z, time);
|
207
|
+
}
|
175
208
|
}
|
@@ -6,7 +6,7 @@
|
|
6
6
|
import { v5 } from 'uuid';
|
7
7
|
|
8
8
|
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
9
|
-
import { destroy, findByGuid, IInstantiateOptions, instantiate } from "./engine_gameobject.js";
|
9
|
+
import { destroy, findByGuid, type IInstantiateOptions, instantiate } from "./engine_gameobject.js";
|
10
10
|
import { InstantiateOptions } from "./engine_gameobject.js";
|
11
11
|
import type { INetworkConnection } from "./engine_networking_types.js";
|
12
12
|
import type { IModel } from "./engine_networking_types.js";
|
@@ -383,15 +383,17 @@
|
|
383
383
|
const tex = data as Texture;
|
384
384
|
const rt = new RenderTexture(tex.image.width, tex.image.height, {
|
385
385
|
colorSpace: THREE.LinearSRGBColorSpace,
|
386
|
-
});
|
386
|
+
});
|
387
387
|
rt.texture = tex;
|
388
|
-
|
389
388
|
tex.isRenderTargetTexture = true;
|
390
389
|
tex.flipY = true;
|
391
390
|
tex.offset.y = 1;
|
392
391
|
tex.repeat.y = -1;
|
393
392
|
tex.needsUpdate = true;
|
394
393
|
|
394
|
+
// when we have a compressed texture using mipmaps causes error in threejs because the bindframebuffer call will then try to set an array of framebuffers https://linear.app/needle/issue/NE-4294
|
395
|
+
tex.mipmaps = [];
|
396
|
+
|
395
397
|
if (tex instanceof CompressedTexture) {
|
396
398
|
//@ts-ignore
|
397
399
|
tex["isCompressedTexture"] = false;
|
@@ -5,7 +5,7 @@
|
|
5
5
|
import { addLog,LogType } from "./debug/debug_overlay.js";
|
6
6
|
import { isLocalNetwork } from "./engine_networking_utils.js";
|
7
7
|
import { Context } from "./engine_setup.js";
|
8
|
-
import { Constructor,
|
8
|
+
import type { Constructor, ConstructorConcrete, SourceIdentifier } from "./engine_types.js";
|
9
9
|
import { $BuiltInTypeFlag } from "./engine_typestore.js";
|
10
10
|
import { getParam } from "./engine_utils.js";
|
11
11
|
import { isPersistentAsset } from "./extensions/NEEDLE_persistent_assets.js";
|
@@ -47,6 +47,7 @@
|
|
47
47
|
|
48
48
|
export interface ITime {
|
49
49
|
get time(): number;
|
50
|
+
get deltaTime(): number;
|
50
51
|
}
|
51
52
|
|
52
53
|
export interface IInput {
|
@@ -1,16 +1,16 @@
|
|
1
|
-
import { Intersection, Object3D } from "three";
|
1
|
+
import { type Intersection, Object3D } from "three";
|
2
2
|
|
3
3
|
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
4
|
-
import {
|
4
|
+
import { type InputEventNames, InputEvents, NEPointerEvent, PointerType } from "../../engine/engine_input.js";
|
5
5
|
import { Mathf } from "../../engine/engine_math.js";
|
6
|
-
import { RaycastOptions, RaycastTestObjectReturnType } from "../../engine/engine_physics.js";
|
6
|
+
import { RaycastOptions, type RaycastTestObjectReturnType } from "../../engine/engine_physics.js";
|
7
7
|
import { Context } from "../../engine/engine_setup.js";
|
8
|
-
import { IComponent } from "../../engine/engine_types.js";
|
8
|
+
import type { IComponent } from "../../engine/engine_types.js";
|
9
9
|
import { getParam } from "../../engine/engine_utils.js";
|
10
10
|
import { Behaviour, GameObject } from "../Component.js";
|
11
11
|
import { $shadowDomOwner } from "./BaseUIComponent.js";
|
12
12
|
import type { ICanvasGroup } from "./Interfaces.js";
|
13
|
-
import { hasPointerEventComponent, type IPointerEventHandler, IPointerUpHandler, PointerEventData } from "./PointerEvents.js";
|
13
|
+
import { hasPointerEventComponent, type IPointerEventHandler, type IPointerUpHandler, PointerEventData } from "./PointerEvents.js";
|
14
14
|
import { ObjectRaycaster, Raycaster } from "./Raycaster.js";
|
15
15
|
import { UIRaycastUtils } from "./RaycastUtils.js";
|
16
16
|
import { isUIObject } from "./Utils.js";
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import { GLTFExporter
|
2
|
-
import { GLTFLoader
|
1
|
+
import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
2
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
3
|
|
4
4
|
import { isDevEnvironment } from "../debug/index.js";
|
5
5
|
import { isResourceTrackingEnabled } from "../engine_assetdatabase.js";
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import { Matrix4, Object3D, Quaternion, Scene, Vector3 } from 'three';
|
2
2
|
|
3
3
|
import { CreateWireCube, Gizmos } from '../engine_gizmos.js';
|
4
|
-
import { IGameObject } from '../engine_types.js';
|
4
|
+
import type { IGameObject } from '../engine_types.js';
|
5
5
|
import { getParam } from '../engine_utils.js';
|
6
|
-
import { IXRRig } from './XRRig.js';
|
6
|
+
import type { IXRRig } from './XRRig.js';
|
7
7
|
|
8
8
|
export const flipForwardMatrix = new Matrix4().makeRotationY(Math.PI);
|
9
9
|
export const flipForwardQuaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
@@ -6,7 +6,7 @@
|
|
6
6
|
import { setWorldPositionXYZ } from "../engine/engine_three_utils.js";
|
7
7
|
import type { ILight } from "../engine/engine_types.js";
|
8
8
|
import { getParam, isMobileDevice } from "../engine/engine_utils.js";
|
9
|
-
import { NeedleXREventArgs } from "../engine/xr/index.js";
|
9
|
+
import { type NeedleXREventArgs } from "../engine/xr/index.js";
|
10
10
|
import { Behaviour, GameObject } from "./Component.js";
|
11
11
|
import { WebARSessionRoot } from "./webxr/WebARSessionRoot.js";
|
12
12
|
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import { RGBAColor } from "../../engine-components/js-extensions/RGBAColor.js";
|
5
5
|
import { Context } from "../engine_context.js";
|
6
6
|
import { Gizmos } from "../engine_gizmos.js";
|
7
|
-
import { InputEventNames, InputEvents, NEPointerEvent, NEPointerEventInit
|
7
|
+
import { type InputEventNames, InputEvents, NEPointerEvent, type NEPointerEventInit } from "../engine_input.js";
|
8
8
|
import { getTempQuaternion, getTempVector, getWorldQuaternion } from "../engine_three_utils.js";
|
9
9
|
import type { ButtonName, IGameObject, Vec3, XRControllerButtonName, XRGestureName } from "../engine_types.js";
|
10
10
|
import { getParam } from "../engine_utils.js";
|
@@ -1,13 +1,13 @@
|
|
1
1
|
import { Camera, Object3D, PerspectiveCamera, Quaternion, Vector3 } from "three";
|
2
2
|
|
3
|
-
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../debug/index.js";
|
3
|
+
import { enableSpatialConsole, isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../debug/index.js";
|
4
4
|
import { Context, FrameEvent } from "../engine_context.js";
|
5
5
|
import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
|
6
6
|
import { isDestroyed } from "../engine_gameobject.js";
|
7
7
|
import { Gizmos } from "../engine_gizmos.js";
|
8
8
|
import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
|
9
9
|
import { getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
|
10
|
-
import { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
|
10
|
+
import type { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
|
11
11
|
import { delay, getParam, isDesktop, isQuest } from "../engine_utils.js";
|
12
12
|
import { flipForwardMatrix, flipForwardQuaternion, ImplictXRRig } from "./internal.js";
|
13
13
|
import { NeedleXRController } from "./NeedleXRController.js";
|
@@ -714,6 +714,8 @@
|
|
714
714
|
this.mode = mode;
|
715
715
|
this.context = context;
|
716
716
|
|
717
|
+
if(debug || getParam("console")) enableSpatialConsole(true);
|
718
|
+
|
717
719
|
this.context.xr = this;
|
718
720
|
this.context.renderer.xr.setSession(this.session).then(this.onRendererSessionSet)
|
719
721
|
|
@@ -874,6 +876,8 @@
|
|
874
876
|
this.context.requestSizeUpdate();
|
875
877
|
|
876
878
|
this._defaultRig.gameObject.removeFromParent();
|
879
|
+
|
880
|
+
enableSpatialConsole(false);
|
877
881
|
};
|
878
882
|
|
879
883
|
/** Disconnects the controller, invokes events and notifies previou controller (if any) */
|
@@ -1,11 +1,10 @@
|
|
1
1
|
|
2
2
|
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
3
3
|
import { serializable } from "../../engine/engine_serialization.js";
|
4
|
-
import { isiOS,isSafari } from "../../engine/engine_utils.js";
|
4
|
+
import { isiOS, isSafari } from "../../engine/engine_utils.js";
|
5
5
|
import { Behaviour } from "../Component.js";
|
6
6
|
import { type IPointerClickHandler, PointerEventData } from "../ui/index.js";
|
7
7
|
import { ObjectRaycaster, Raycaster } from "../ui/Raycaster.js";
|
8
|
-
import { tryGetUIComponent } from "../ui/Utils.js";
|
9
8
|
|
10
9
|
export enum OpenURLMode {
|
11
10
|
NewTab = 0,
|
@@ -32,29 +31,34 @@
|
|
32
31
|
|
33
32
|
this._validateUrl();
|
34
33
|
|
35
|
-
|
34
|
+
let url = this.url;
|
35
|
+
if (!url.startsWith("mailto:") && url.includes("@")) {
|
36
|
+
url = "mailto:" + url;
|
37
|
+
}
|
36
38
|
|
39
|
+
if (isDevEnvironment()) showBalloonMessage("Open URL: " + url)
|
40
|
+
|
37
41
|
switch (this.mode) {
|
38
42
|
case OpenURLMode.NewTab:
|
39
43
|
if (isSafari()) {
|
40
|
-
globalThis.open(
|
44
|
+
globalThis.open(url, "_blank");
|
41
45
|
}
|
42
46
|
else
|
43
|
-
globalThis.open(
|
47
|
+
globalThis.open(url, "_blank");
|
44
48
|
break;
|
45
49
|
case OpenURLMode.SameTab:
|
46
50
|
// TODO: test if "same tab" now also works on iOS
|
47
51
|
if (isSafari() && isiOS()) {
|
48
|
-
globalThis.open(
|
52
|
+
globalThis.open(url, "_top");
|
49
53
|
}
|
50
54
|
else
|
51
|
-
globalThis.open(
|
55
|
+
globalThis.open(url, "_self");
|
52
56
|
break;
|
53
57
|
case OpenURLMode.NewWindow:
|
54
58
|
if (isSafari()) {
|
55
|
-
globalThis.open(
|
59
|
+
globalThis.open(url, "_top");
|
56
60
|
}
|
57
|
-
else globalThis.open(
|
61
|
+
else globalThis.open(url, "_new");
|
58
62
|
break;
|
59
63
|
|
60
64
|
}
|
@@ -5,7 +5,7 @@
|
|
5
5
|
import { syncField } from "../../engine/engine_networking_auto.js"
|
6
6
|
import { syncDestroy } from "../../engine/engine_networking_instantiate.js";
|
7
7
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
8
|
-
import { IGameObject } from "../../engine/engine_types.js";
|
8
|
+
import type { IGameObject } from "../../engine/engine_types.js";
|
9
9
|
import { delay, getParam } from "../../engine/engine_utils.js";
|
10
10
|
import { Behaviour, Component, GameObject } from "../../engine-components/Component.js";
|
11
11
|
import { EventList } from "../../engine-components/EventList.js";
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import { Face, Object3D, Vector3 } from "three";
|
1
|
+
import { type Face, Object3D, Vector3 } from "three";
|
2
2
|
|
3
|
-
import { Input, InputEventNames, NEPointerEvent } from "../../engine/engine_input.js";
|
4
|
-
import { GamepadButtonName, MouseButtonName } from "../../engine/engine_types.js";
|
3
|
+
import { Input, type InputEventNames, NEPointerEvent } from "../../engine/engine_input.js";
|
4
|
+
import type { GamepadButtonName, MouseButtonName } from "../../engine/engine_types.js";
|
5
5
|
import { GameObject } from "../Component.js";
|
6
6
|
|
7
7
|
export interface IInputEventArgs {
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { SkinnedMesh } from "three";
|
2
2
|
|
3
|
-
import { IRaycastOptions, RaycastOptions } from "../../engine/engine_physics.js";
|
3
|
+
import { type IRaycastOptions, RaycastOptions } from "../../engine/engine_physics.js";
|
4
4
|
import { serializable } from "../../engine/engine_serialization.js";
|
5
5
|
import { NeedleXRSession } from "../../engine/engine_xr.js";
|
6
6
|
import { Behaviour } from "../Component.js";
|
@@ -7,6 +7,7 @@
|
|
7
7
|
BufferGeometry,
|
8
8
|
Color,
|
9
9
|
DoubleSide,
|
10
|
+
LinearFilter,
|
10
11
|
Material,
|
11
12
|
MathUtils,
|
12
13
|
Matrix4,
|
@@ -18,19 +19,23 @@
|
|
18
19
|
OrthographicCamera,
|
19
20
|
PerspectiveCamera,
|
20
21
|
PlaneGeometry,
|
22
|
+
RGBAFormat,
|
21
23
|
Scene,
|
22
24
|
ShaderMaterial,
|
23
25
|
SkinnedMesh,
|
24
26
|
SRGBColorSpace,
|
25
27
|
Texture,
|
26
28
|
Uniform,
|
29
|
+
UnsignedByteType,
|
27
30
|
Vector4,
|
28
|
-
WebGLRenderer
|
31
|
+
WebGLRenderer,
|
32
|
+
WebGLRenderTarget} from 'three';
|
29
33
|
import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
|
30
34
|
|
31
35
|
import type { OffscreenCanvasExt } from '../../../engine/engine_shims.js';
|
32
|
-
import {
|
33
|
-
import {
|
36
|
+
import { Progress } from '../../../engine/engine_time_utils.js';
|
37
|
+
import type { IUSDExporterExtension } from './Extension.js';
|
38
|
+
import type { AnimationExtension } from './extensions/Animation.js';
|
34
39
|
|
35
40
|
function makeNameSafe( str ) {
|
36
41
|
// Remove characters that are not allowed in USD ASCII identifiers
|
@@ -336,7 +341,7 @@
|
|
336
341
|
}
|
337
342
|
|
338
343
|
|
339
|
-
buildHeader( endTimeCode ) {
|
344
|
+
buildHeader( { startTimeCode, endTimeCode } ) {
|
340
345
|
|
341
346
|
return `#usda 1.0
|
342
347
|
(
|
@@ -346,7 +351,7 @@
|
|
346
351
|
defaultPrim = "${makeNameSafe( this.name )}"
|
347
352
|
metersPerUnit = 1
|
348
353
|
upAxis = "Y"
|
349
|
-
startTimeCode =
|
354
|
+
startTimeCode = ${startTimeCode}
|
350
355
|
endTimeCode = ${endTimeCode}
|
351
356
|
timeCodesPerSecond = 60
|
352
357
|
framesPerSecond = 60
|
@@ -452,7 +457,7 @@
|
|
452
457
|
class USDZExporterContext {
|
453
458
|
root: any;
|
454
459
|
exporter: any;
|
455
|
-
extensions:
|
460
|
+
extensions: Array<IUSDExporterExtension> = [];
|
456
461
|
quickLookCompatible: boolean;
|
457
462
|
materials: Map<string, Material>;
|
458
463
|
textures: TextureMap;
|
@@ -502,7 +507,8 @@
|
|
502
507
|
class USDZExporter {
|
503
508
|
debug: boolean;
|
504
509
|
sceneAnchoringOptions: {} = {};
|
505
|
-
extensions:
|
510
|
+
extensions: Array<IUSDExporterExtension> = [];
|
511
|
+
keepObject?: (object: Object3D) => boolean;
|
506
512
|
|
507
513
|
constructor() {
|
508
514
|
|
@@ -510,20 +516,6 @@
|
|
510
516
|
|
511
517
|
}
|
512
518
|
|
513
|
-
getEndTimeCode( animations ) {
|
514
|
-
let endTimeCode = 0;
|
515
|
-
|
516
|
-
for( const animation of animations ) {
|
517
|
-
const currentEndTimeCode = animation.duration * 60;
|
518
|
-
|
519
|
-
if ( endTimeCode < currentEndTimeCode ) {
|
520
|
-
endTimeCode = currentEndTimeCode;
|
521
|
-
}
|
522
|
-
}
|
523
|
-
|
524
|
-
return endTimeCode;
|
525
|
-
}
|
526
|
-
|
527
519
|
async parse( scene, options: USDZExporterOptions = new USDZExporterOptions() ) {
|
528
520
|
|
529
521
|
options = Object.assign( new USDZExporterOptions(), options );
|
@@ -541,8 +533,11 @@
|
|
541
533
|
const materials = context.materials;
|
542
534
|
const textures = context.textures;
|
543
535
|
|
536
|
+
Progress.report('export-usdz', "Invoking onBeforeBuildDocument");
|
544
537
|
await invokeAll( context, 'onBeforeBuildDocument' );
|
538
|
+
Progress.report('export-usdz', "Done onBeforeBuildDocument");
|
545
539
|
|
540
|
+
Progress.report('export-usdz', "Reparent bones to common ancestor");
|
546
541
|
// HACK let's find all skeletons and reparent them to their skelroot / armature / uppermost bone parent
|
547
542
|
const reparentings: Array<any> = [];
|
548
543
|
scene.traverseVisible(object => {
|
@@ -559,20 +554,26 @@
|
|
559
554
|
|
560
555
|
for ( const reparenting of reparentings ) {
|
561
556
|
const { object, originalParent, newParent } = reparenting;
|
562
|
-
if (this.debug) console.log("REPARENTING", object, "from", originalParent, "to", newParent);
|
557
|
+
// if (this.debug) console.log("REPARENTING", object, "from", originalParent, "to", newParent);
|
563
558
|
newParent.add( object );
|
564
559
|
}
|
565
560
|
|
566
|
-
|
561
|
+
Progress.report('export-usdz', "Traversing hierarchy");
|
562
|
+
traverseVisible( scene, context.document, context, this.keepObject);
|
567
563
|
|
564
|
+
Progress.report('export-usdz', "Invoking onAfterBuildDocument");
|
568
565
|
await invokeAll( context, 'onAfterBuildDocument' );
|
569
566
|
|
570
|
-
|
567
|
+
Progress.report('export-usdz', { message: "Parsing document", autoStep: 10 });
|
568
|
+
await parseDocument( context, () => {
|
571
569
|
// injected after stageRoot.
|
572
570
|
// TODO property use context/writer instead of string concat
|
573
|
-
|
571
|
+
Progress.report('export-usdz', "Building materials");
|
572
|
+
const result = buildMaterials( materials, textures, options.quickLookCompatible );
|
573
|
+
return result;
|
574
574
|
} );
|
575
575
|
|
576
|
+
Progress.report("export-usdz", "Invoking onAfterSerialize");
|
576
577
|
await invokeAll( context, 'onAfterSerialize' );
|
577
578
|
|
578
579
|
// repair the parenting again
|
@@ -584,40 +585,57 @@
|
|
584
585
|
// Moved into parseDocument callback for proper defaultPrim encapsulation
|
585
586
|
// context.output += buildMaterials( materials, textures, options.quickLookCompatible );
|
586
587
|
|
587
|
-
const
|
588
|
+
const animationExtension: AnimationExtension | undefined = context.extensions.find( ext => ext.extensionName === 'animation' ) as AnimationExtension;
|
589
|
+
const startTimeCode = animationExtension?.getStartTimeCode() ?? 0;
|
590
|
+
const endTimeCode = animationExtension?.getEndTimeCode() ?? 0;
|
588
591
|
|
589
|
-
const header = context.document.buildHeader( endTimeCode );
|
592
|
+
const header = context.document.buildHeader( { startTimeCode, endTimeCode } );
|
590
593
|
const final = header + '\n' + context.output;
|
591
594
|
|
592
595
|
// full output file
|
593
|
-
if ( this.debug )
|
594
|
-
console.log( final );
|
596
|
+
// if ( this.debug ) console.log( final );
|
595
597
|
|
596
598
|
files[ modelFileName ] = fflate.strToU8( final );
|
597
599
|
context.output = '';
|
598
600
|
|
599
|
-
|
601
|
+
Progress.report("export-usdz", { message: "Exporting textures", autoStep: 10 });
|
602
|
+
Progress.start("export-usdz-textures", "export-usdz");
|
603
|
+
const decompressionRenderer = new WebGLRenderer( {
|
604
|
+
antialias: false,
|
605
|
+
alpha: true,
|
606
|
+
premultipliedAlpha: false,
|
607
|
+
preserveDrawingBuffer: true
|
608
|
+
} );
|
600
609
|
|
601
|
-
|
610
|
+
const textureCount = Object.keys(textures).length;
|
611
|
+
Progress.report("export-usdz-textures", { totalSteps: textureCount * 3, currentStep: 0 });
|
612
|
+
const convertTexture = async (id: string) => {
|
602
613
|
|
603
614
|
const textureData = textures[ id ];
|
604
|
-
|
615
|
+
const texture = textureData.texture;
|
605
616
|
|
606
617
|
const isRGBA = formatsWithAlphaChannel.includes( texture.format );
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
618
|
+
|
619
|
+
// Change: we need to always read back the texture now, otherwise the unpremultiplied workflow doesn't work.
|
620
|
+
let img: ImageReadbackResult = {
|
621
|
+
imageData: texture.image
|
622
|
+
};
|
623
|
+
|
624
|
+
Progress.report("export-usdz-textures", { message: "read back texture", autoStep: true });
|
625
|
+
const anyColorScale = textureData.scale !== undefined && textureData.scale.x !== 1 && textureData.scale.y !== 1 && textureData.scale.z !== 1 && textureData.scale.w !== 1;
|
626
|
+
// @ts-ignore
|
627
|
+
if ( texture.isCompressedTexture || texture.isRenderTargetTexture || anyColorScale ) {
|
628
|
+
img = await decompressGpuTexture( texture, options.maxTextureSize, decompressionRenderer, textureData.scale );
|
613
629
|
}
|
614
630
|
|
615
|
-
|
631
|
+
Progress.report("export-usdz-textures", { message: "convert texture to canvas", autoStep: true });
|
632
|
+
const canvas = await imageToCanvasUnpremultiplied( img.imageBitmap || img.imageData, options.maxTextureSize ).catch( err => {
|
616
633
|
console.error("Error converting texture to canvas", texture, err);
|
617
634
|
});
|
618
635
|
|
619
636
|
if ( canvas ) {
|
620
637
|
|
638
|
+
Progress.report("export-usdz-textures", { message: "convert canvas to blob", autoStep: true });
|
621
639
|
const blob = await canvas.convertToBlob( {type: isRGBA ? 'image/png' : 'image/jpeg', quality: 0.95 } );
|
622
640
|
files[ `textures/${id}.${isRGBA ? 'png' : 'jpg'}` ] = new Uint8Array( await blob.arrayBuffer() );
|
623
641
|
|
@@ -626,7 +644,11 @@
|
|
626
644
|
console.warn( 'Can`t export texture: ', texture );
|
627
645
|
|
628
646
|
}
|
647
|
+
};
|
629
648
|
|
649
|
+
for ( const id in textures ) {
|
650
|
+
|
651
|
+
await convertTexture( id );
|
630
652
|
}
|
631
653
|
|
632
654
|
decompressionRenderer.dispose();
|
@@ -634,6 +656,8 @@
|
|
634
656
|
// 64 byte alignment
|
635
657
|
// https://github.com/101arrowz/fflate/issues/39#issuecomment-777263109
|
636
658
|
|
659
|
+
Progress.end("export-usdz-textures");
|
660
|
+
|
637
661
|
let offset = 0;
|
638
662
|
|
639
663
|
for ( const filename in files ) {
|
@@ -658,13 +682,14 @@
|
|
658
682
|
|
659
683
|
}
|
660
684
|
|
685
|
+
Progress.report("export-usdz", "zip archive");
|
661
686
|
return fflate.zipSync( files, { level: 0 } );
|
662
687
|
|
663
688
|
}
|
664
689
|
|
665
690
|
}
|
666
691
|
|
667
|
-
function traverseVisible( object: Object3D, parentModel: USDObject, context: USDZExporterContext ) {
|
692
|
+
function traverseVisible( object: Object3D, parentModel: USDObject, context: USDZExporterContext, keepObject?: (object: Object3D) => boolean ) {
|
668
693
|
|
669
694
|
if ( ! object.visible ) return;
|
670
695
|
|
@@ -677,10 +702,7 @@
|
|
677
702
|
material = object.material;
|
678
703
|
}
|
679
704
|
|
680
|
-
|
681
|
-
// Here we just assume they're off, and don't export them
|
682
|
-
const renderer = GameObject.getComponent( object, Renderer )
|
683
|
-
if (renderer && !renderer.enabled) {
|
705
|
+
if (keepObject && !keepObject(object)) {
|
684
706
|
geometry = undefined;
|
685
707
|
material = undefined;
|
686
708
|
}
|
@@ -741,7 +763,7 @@
|
|
741
763
|
|
742
764
|
for ( const ch of object.children ) {
|
743
765
|
|
744
|
-
traverseVisible( ch, parentModel, context );
|
766
|
+
traverseVisible( ch, parentModel, context, keepObject );
|
745
767
|
|
746
768
|
}
|
747
769
|
|
@@ -749,12 +771,23 @@
|
|
749
771
|
|
750
772
|
async function parseDocument( context: USDZExporterContext, afterStageRoot: () => string ) {
|
751
773
|
|
774
|
+
Progress.start("export-usdz-resources", "export-usdz");
|
775
|
+
const resources: Array<() => void> = [];
|
752
776
|
for ( const child of context.document.children ) {
|
753
|
-
|
754
|
-
addResources( child, context );
|
755
|
-
|
777
|
+
addResources( child, context, resources );
|
756
778
|
}
|
757
|
-
|
779
|
+
// addResources now only collects promises for better progress reporting.
|
780
|
+
// We are resolving them here and reporting progress on that:
|
781
|
+
const total = resources.length;
|
782
|
+
for (let i = 0; i < total; i++) {
|
783
|
+
Progress.report("export-usdz-resources", { totalSteps: total, currentStep: i });
|
784
|
+
await new Promise<void>((resolve, _reject) => {
|
785
|
+
resources[i]();
|
786
|
+
resolve();
|
787
|
+
});
|
788
|
+
}
|
789
|
+
Progress.end("export-usdz-resources");
|
790
|
+
|
758
791
|
const writer = new USDWriter();
|
759
792
|
|
760
793
|
writer.beginBlock( `def Xform "${context.document.name}"` );
|
@@ -780,12 +813,22 @@
|
|
780
813
|
writer.appendLine( `rel preliminary:imageAnchoring:referenceImage = </${context.document.name}/Scenes/Scene/AnchoringReferenceImage>` );
|
781
814
|
writer.appendLine();
|
782
815
|
|
816
|
+
const count = (object: USDObject | null) => {
|
817
|
+
if (!object) return 0;
|
818
|
+
let total = 1;
|
819
|
+
for ( const child of object.children ) total += count( child );
|
820
|
+
return total;
|
821
|
+
}
|
822
|
+
const totalXformCount = count(context.document);
|
823
|
+
Progress.start("export-usdz-xforms", "export-usdz");
|
824
|
+
Progress.report("export-usdz-xforms", { totalSteps: totalXformCount, currentStep: 1 });
|
825
|
+
|
783
826
|
for ( const child of context.document.children ) {
|
784
|
-
|
785
827
|
buildXform( child, writer, context );
|
786
|
-
|
787
828
|
}
|
829
|
+
Progress.end("export-usdz-xforms");
|
788
830
|
|
831
|
+
Progress.report("export-usdz", "invoke onAfterHierarchy");
|
789
832
|
invokeAll( context, 'onAfterHierarchy', writer );
|
790
833
|
|
791
834
|
writer.closeBlock();
|
@@ -793,15 +836,15 @@
|
|
793
836
|
writer.appendLine(afterStageRoot());
|
794
837
|
writer.closeBlock();
|
795
838
|
|
839
|
+
Progress.report("export-usdz", "write to string")
|
796
840
|
context.output += writer.toString();
|
797
841
|
|
798
842
|
}
|
799
843
|
|
800
|
-
function addResources( object: USDObject | null, context: USDZExporterContext ) {
|
844
|
+
function addResources( object: USDObject | null, context: USDZExporterContext, resources: Array<() => void>) {
|
801
845
|
|
802
|
-
if ( object
|
803
|
-
|
804
|
-
}
|
846
|
+
if ( !object ) return;
|
847
|
+
|
805
848
|
const geometry = object.geometry;
|
806
849
|
const material = object.material;
|
807
850
|
|
@@ -813,9 +856,13 @@
|
|
813
856
|
|
814
857
|
if ( ! ( geometryFileName in context.files ) ) {
|
815
858
|
|
816
|
-
const
|
817
|
-
|
859
|
+
const action = () => {
|
860
|
+
const meshObject = buildMeshObject( geometry, object.skinnedMesh?.skeleton?.bones );
|
861
|
+
context.files[ geometryFileName ] = buildUSDFileAsString( meshObject, context);
|
862
|
+
};
|
818
863
|
|
864
|
+
resources.push(action);
|
865
|
+
|
819
866
|
}
|
820
867
|
|
821
868
|
} else {
|
@@ -826,7 +873,7 @@
|
|
826
873
|
|
827
874
|
}
|
828
875
|
|
829
|
-
if( material ) {
|
876
|
+
if ( material ) {
|
830
877
|
|
831
878
|
if ( ! ( material.uuid in context.materials ) ) {
|
832
879
|
|
@@ -837,7 +884,7 @@
|
|
837
884
|
|
838
885
|
for ( const ch of object.children ) {
|
839
886
|
|
840
|
-
addResources( ch, context );
|
887
|
+
addResources( ch, context, resources );
|
841
888
|
|
842
889
|
}
|
843
890
|
|
@@ -866,24 +913,40 @@
|
|
866
913
|
|
867
914
|
}
|
868
915
|
let _renderer: WebGLRenderer | null = null;
|
916
|
+
let renderTarget: WebGLRenderTarget | null = null;
|
869
917
|
let fullscreenQuadGeometry: PlaneGeometry | null;
|
870
918
|
let fullscreenQuadMaterial: ShaderMaterial | null;
|
871
919
|
let fullscreenQuad: Mesh | null;
|
872
920
|
|
873
|
-
|
921
|
+
declare type ImageReadbackResult = {
|
922
|
+
imageData: ImageData;
|
923
|
+
imageBitmap?: ImageBitmap;
|
924
|
+
}
|
874
925
|
|
926
|
+
/** Reads back a texture from the GPU (can be compressed, a render texture, or aynthing), apply colorScale to it, and return CPU data for further usage.
|
927
|
+
* Note that there are WebGL / WebGPU rules preventing some use of data between WebGL contexts.
|
928
|
+
*/
|
929
|
+
async function decompressGpuTexture( texture, maxTextureSize = Infinity, renderer: WebGLRenderer | null = null, colorScale: Vector4 | undefined = undefined): Promise<ImageReadbackResult> {
|
930
|
+
|
875
931
|
if ( ! fullscreenQuadGeometry ) fullscreenQuadGeometry = new PlaneGeometry( 2, 2, 1, 1 );
|
876
932
|
if ( ! fullscreenQuadMaterial ) fullscreenQuadMaterial = new ShaderMaterial( {
|
877
|
-
uniforms: {
|
933
|
+
uniforms: {
|
934
|
+
blitTexture: new Uniform( texture ),
|
935
|
+
flipY: new Uniform( false ),
|
936
|
+
scale: new Uniform( new Vector4( 1, 1, 1, 1 ) ),
|
937
|
+
},
|
878
938
|
vertexShader: `
|
879
939
|
varying vec2 vUv;
|
940
|
+
uniform bool flipY;
|
880
941
|
void main(){
|
881
942
|
vUv = uv;
|
882
|
-
|
943
|
+
if (flipY)
|
944
|
+
vUv.y = 1. - vUv.y;
|
883
945
|
gl_Position = vec4(position.xy * 1.0,0.,.999999);
|
884
946
|
}`,
|
885
947
|
fragmentShader: `
|
886
|
-
uniform sampler2D blitTexture;
|
948
|
+
uniform sampler2D blitTexture;
|
949
|
+
uniform vec4 scale;
|
887
950
|
varying vec2 vUv;
|
888
951
|
|
889
952
|
void main(){
|
@@ -894,11 +957,18 @@
|
|
894
957
|
#else
|
895
958
|
gl_FragColor = texture2D( blitTexture, vUv);
|
896
959
|
#endif
|
897
|
-
|
960
|
+
|
961
|
+
gl_FragColor.rgba *= scale.rgba;
|
898
962
|
}`
|
899
963
|
} );
|
900
964
|
|
901
|
-
|
965
|
+
// update uniforms
|
966
|
+
const uniforms = fullscreenQuadMaterial.uniforms;
|
967
|
+
uniforms.blitTexture.value = texture;
|
968
|
+
uniforms.flipY.value = false;
|
969
|
+
uniforms.scale.value = new Vector4( 1, 1, 1, 1 );
|
970
|
+
if ( colorScale !== undefined ) uniforms.scale.value.copy( colorScale );
|
971
|
+
|
902
972
|
fullscreenQuadMaterial.defines.IS_SRGB = texture.colorSpace == SRGBColorSpace;
|
903
973
|
fullscreenQuadMaterial.needsUpdate = true;
|
904
974
|
|
@@ -915,22 +985,31 @@
|
|
915
985
|
|
916
986
|
if ( ! renderer ) {
|
917
987
|
|
918
|
-
renderer = _renderer = new WebGLRenderer( { antialias: false, alpha: true } );
|
988
|
+
renderer = _renderer = new WebGLRenderer( { antialias: false, alpha: true, premultipliedAlpha: false, preserveDrawingBuffer: true } );
|
919
989
|
|
920
990
|
}
|
921
991
|
|
922
|
-
|
992
|
+
const width = Math.min( texture.image.width, maxTextureSize );
|
993
|
+
const height = Math.min( texture.image.height, maxTextureSize );
|
994
|
+
|
995
|
+
// dispose render target if the size is wrong
|
996
|
+
if ( renderTarget && ( renderTarget.width !== width || renderTarget.height !== height ) ) {
|
997
|
+
|
998
|
+
renderTarget.dispose();
|
999
|
+
renderTarget = null;
|
1000
|
+
|
1001
|
+
}
|
1002
|
+
|
1003
|
+
if ( ! renderTarget ) {
|
1004
|
+
|
1005
|
+
renderTarget = new WebGLRenderTarget( width, height, { format: RGBAFormat, type: UnsignedByteType, minFilter: LinearFilter, magFilter: LinearFilter } );
|
1006
|
+
}
|
1007
|
+
|
1008
|
+
renderer.setRenderTarget( renderTarget );
|
1009
|
+
renderer.setSize( width, height );
|
923
1010
|
renderer.clear();
|
924
1011
|
renderer.render( _scene, _camera );
|
925
1012
|
|
926
|
-
const readableTexture = new Texture( renderer.domElement );
|
927
|
-
|
928
|
-
readableTexture.minFilter = texture.minFilter;
|
929
|
-
readableTexture.magFilter = texture.magFilter;
|
930
|
-
readableTexture.wrapS = texture.wrapS;
|
931
|
-
readableTexture.wrapT = texture.wrapT;
|
932
|
-
readableTexture.name = texture.name;
|
933
|
-
|
934
1013
|
if ( _renderer ) {
|
935
1014
|
|
936
1015
|
_renderer.dispose();
|
@@ -938,11 +1017,18 @@
|
|
938
1017
|
|
939
1018
|
}
|
940
1019
|
|
941
|
-
|
1020
|
+
const buffer = new Uint8ClampedArray( renderTarget.width * renderTarget.height * 4 );
|
1021
|
+
renderer.readRenderTargetPixels( renderTarget, 0, 0, renderTarget.width, renderTarget.height, buffer );
|
1022
|
+
const imageData = new ImageData( buffer, renderTarget.width, renderTarget.height, undefined );
|
1023
|
+
const bmp = await createImageBitmap( imageData, { premultiplyAlpha: "none" } );
|
1024
|
+
return {
|
1025
|
+
imageData,
|
1026
|
+
imageBitmap: bmp
|
1027
|
+
};
|
942
1028
|
|
943
1029
|
}
|
944
1030
|
|
945
|
-
|
1031
|
+
/** Checks if the given image is of a type with readable data and width/height */
|
946
1032
|
function isImageBitmap( image ) {
|
947
1033
|
|
948
1034
|
return ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
|
@@ -952,8 +1038,32 @@
|
|
952
1038
|
|
953
1039
|
}
|
954
1040
|
|
955
|
-
|
1041
|
+
/** This method uses a 'bitmaprenderer' context and doesn't do any pixel manipulation.
|
1042
|
+
* This way, we can keep the alpha channel as it was, but we're losing the ability to do pixel manipulations or resize operations. */
|
1043
|
+
async function imageToCanvasUnpremultiplied( image: ImageBitmapSource & { width: number, height: number }, maxTextureSize = 4096) {
|
956
1044
|
|
1045
|
+
const scale = maxTextureSize / Math.max( image.width, image.height );
|
1046
|
+
const width = image.width * Math.min( 1, scale );
|
1047
|
+
const height = image.height * Math.min( 1, scale );
|
1048
|
+
|
1049
|
+
const canvas = new OffscreenCanvas( width, height );
|
1050
|
+
const settings: ImageBitmapOptions = { premultiplyAlpha: "none" };
|
1051
|
+
if (image.width !== width) settings.resizeWidth = width;
|
1052
|
+
if (image.height !== height) settings.resizeHeight = height;
|
1053
|
+
|
1054
|
+
const imageBitmap = await createImageBitmap(image, settings);
|
1055
|
+
const ctx = canvas.getContext("bitmaprenderer") as ImageBitmapRenderingContext | null;
|
1056
|
+
if (ctx) {
|
1057
|
+
ctx.transferFromImageBitmap(imageBitmap);
|
1058
|
+
}
|
1059
|
+
return canvas as OffscreenCanvasExt;
|
1060
|
+
}
|
1061
|
+
|
1062
|
+
/** This method uses a '2d' canvas context for pixel manipulation, and can apply a color scale or Y flip to the given image.
|
1063
|
+
* Unfortunately, canvas always uses premultiplied data, and thus images with low alpha values (or multiplying by a=0) will result in black pixels.
|
1064
|
+
*/
|
1065
|
+
async function imageToCanvas( image: HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap, color: Vector4 | undefined = undefined, flipY = false, maxTextureSize = 4096 ) {
|
1066
|
+
|
957
1067
|
if ( isImageBitmap( image ) ) {
|
958
1068
|
|
959
1069
|
// max. canvas size on Safari is still 4096x4096
|
@@ -961,7 +1071,7 @@
|
|
961
1071
|
|
962
1072
|
const canvas = new OffscreenCanvas( image.width * Math.min( 1, scale ), image.height * Math.min( 1, scale ) );
|
963
1073
|
|
964
|
-
const context = canvas.getContext( '2d' ) as OffscreenCanvasRenderingContext2D;
|
1074
|
+
const context = canvas.getContext( '2d', { alpha: true, premultipliedAlpha: false } ) as OffscreenCanvasRenderingContext2D;
|
965
1075
|
if (!context) throw new Error('Could not get canvas 2D context');
|
966
1076
|
|
967
1077
|
if ( flipY === true ) {
|
@@ -1044,9 +1154,15 @@
|
|
1044
1154
|
}
|
1045
1155
|
|
1046
1156
|
function getGeometryName(geometry: BufferGeometry, fallbackName: string) {
|
1047
|
-
|
1157
|
+
// TODO using object names here breaks instancing...
|
1158
|
+
// Likely need to remove fallbackName again
|
1159
|
+
return makeNameSafe(geometry.name || fallbackName || 'Geometry') + "_" + geometry.id;
|
1048
1160
|
}
|
1049
1161
|
|
1162
|
+
function getMaterialName(material: Material) {
|
1163
|
+
return makeNameSafe(material.name || 'Material') + "_" + material.id;
|
1164
|
+
}
|
1165
|
+
|
1050
1166
|
function getPathToSkeleton(bone: Object3D, assumedRoot: Object3D) {
|
1051
1167
|
let path = getBoneName(bone);
|
1052
1168
|
let current = bone.parent;
|
@@ -1064,6 +1180,8 @@
|
|
1064
1180
|
if ( model == null)
|
1065
1181
|
return;
|
1066
1182
|
|
1183
|
+
Progress.report("export-usdz-xforms", { message: "buildXform " + model.displayName || model.name, autoStep: true });
|
1184
|
+
|
1067
1185
|
const matrix = model.matrix;
|
1068
1186
|
const geometry = model.geometry;
|
1069
1187
|
const material = model.material;
|
@@ -1109,7 +1227,7 @@
|
|
1109
1227
|
writer.beginBlock();
|
1110
1228
|
|
1111
1229
|
if ( geometry && material ) {
|
1112
|
-
const materialName =
|
1230
|
+
const materialName = getMaterialName(material);
|
1113
1231
|
writer.appendLine( `rel material:binding = </StageRoot/Materials/${materialName}>` );
|
1114
1232
|
|
1115
1233
|
// Turns out QuickLook / RealityKit doesn't support the doubleSided attribute, so we
|
@@ -1469,7 +1587,7 @@
|
|
1469
1587
|
const pad = ' ';
|
1470
1588
|
const inputs: Array<string> = [];
|
1471
1589
|
const samplers: Array<string> = [];
|
1472
|
-
const materialName =
|
1590
|
+
const materialName = getMaterialName(material);
|
1473
1591
|
|
1474
1592
|
function texName(tex: Texture) {
|
1475
1593
|
return makeNameSafe(tex.name) + '_' + tex.id;
|
@@ -1837,8 +1955,10 @@
|
|
1837
1955
|
decompressGpuTexture,
|
1838
1956
|
findStructuralNodesInBoneHierarchy,
|
1839
1957
|
getBoneName,
|
1958
|
+
getMaterialName as getMaterialNameForUSD,
|
1840
1959
|
getPathToSkeleton,
|
1841
1960
|
imageToCanvas,
|
1961
|
+
imageToCanvasUnpremultiplied,
|
1842
1962
|
makeNameSafe as makeNameSafeForUSD,
|
1843
1963
|
USDDocument,
|
1844
1964
|
fn as usdNumberFormatting,
|
@@ -1,20 +0,0 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
export function getFormattedDate() {
|
5
|
-
var date = new Date();
|
6
|
-
|
7
|
-
const month = date.getMonth() + 1;
|
8
|
-
const day = date.getDate();
|
9
|
-
const hour = date.getHours();
|
10
|
-
const min = date.getMinutes();
|
11
|
-
const sec = date.getSeconds();
|
12
|
-
|
13
|
-
const s_month = (month < 10 ? "0" : "") + month;
|
14
|
-
const s_day = (day < 10 ? "0" : "") + day;
|
15
|
-
const s_hour = (hour < 10 ? "0" : "") + hour;
|
16
|
-
const s_min = (min < 10 ? "0" : "") + min;
|
17
|
-
const s_sec = (sec < 10 ? "0" : "") + sec;
|
18
|
-
|
19
|
-
return date.getFullYear() + s_month + s_day + "-" + s_hour + s_min + s_sec;
|
20
|
-
}
|
@@ -1,15 +1,15 @@
|
|
1
|
-
import { Matrix4,Mesh, Object3D } from "three";
|
1
|
+
import { Matrix4, Mesh, Object3D } from "three";
|
2
2
|
|
3
3
|
import { showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
|
4
4
|
import { hasProLicense } from "../../../engine/engine_license.js";
|
5
5
|
import { serializable } from "../../../engine/engine_serialization.js";
|
6
|
-
import {
|
7
|
-
import {
|
6
|
+
import { getFormattedDate, Progress } from "../../../engine/engine_time_utils.js";
|
7
|
+
import { getParam, isiOS, isMobileDevice, isSafari } from "../../../engine/engine_utils.js";
|
8
8
|
import { Behaviour, GameObject } from "../../Component.js";
|
9
9
|
import { Renderer } from "../../Renderer.js"
|
10
10
|
import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
|
11
11
|
import { NeedleWebXRHtmlElement } from "../../webxr/WebXRButtons.js";
|
12
|
-
import {
|
12
|
+
import { XRState, XRStateFlag } from "../../webxr/XRFlag.js";
|
13
13
|
import type { IUSDExporterExtension } from "./Extension.js";
|
14
14
|
import { AnimationExtension } from "./extensions/Animation.js"
|
15
15
|
import { AudioExtension } from "./extensions/behavior/AudioExtension.js";
|
@@ -17,9 +17,8 @@
|
|
17
17
|
import { TextExtension } from "./extensions/USDZText.js";
|
18
18
|
import { USDZUIExtension } from "./extensions/USDZUI.js";
|
19
19
|
import { USDZExporter as ThreeUSDZExporter } from "./ThreeUSDZExporter.js";
|
20
|
-
import { registerAnimatorsImplictly } from "./utils/animationutils.js";
|
20
|
+
import { registerAnimatorsImplictly, registerAudioSourcesImplictly } from "./utils/animationutils.js";
|
21
21
|
import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js";
|
22
|
-
import { getFormattedDate } from "./utils/timeutils.js";
|
23
22
|
|
24
23
|
const debug = getParam("debugusdz");
|
25
24
|
|
@@ -41,10 +40,21 @@
|
|
41
40
|
@serializable(Object3D)
|
42
41
|
objectToExport?: Object3D;
|
43
42
|
|
43
|
+
/** Collect all Animations/Animators automatically on export and emit them as playing at the start.
|
44
|
+
* Animator state chains and loops will automatically be collected and exported in order as well.
|
45
|
+
* If this setting is off, Animators need to be registered by components – for example from PlayAnimationOnClick.
|
46
|
+
*/
|
44
47
|
@serializable()
|
45
48
|
autoExportAnimations: boolean = false;
|
46
49
|
|
50
|
+
/** Collect all AudioSources automatically on export and emit them as playing at the start.
|
51
|
+
* They will loop according to their settings.
|
52
|
+
* If this setting is off, Audio Sources need to be registered by components – for example from PlayAudioOnClick.
|
53
|
+
*/
|
47
54
|
@serializable()
|
55
|
+
autoExportAudioSources: boolean = true;
|
56
|
+
|
57
|
+
@serializable()
|
48
58
|
exportFileName?: string;
|
49
59
|
|
50
60
|
@serializable(URL)
|
@@ -64,6 +74,10 @@
|
|
64
74
|
@serializable()
|
65
75
|
planeAnchoringAlignment: "horizontal" | "vertical" | "any" = "horizontal";
|
66
76
|
|
77
|
+
/** Enabling this option will export QuickLook-specific preliminary behaviours along with the USDZ files.
|
78
|
+
* These extensions are only supported on QuickLook on iOS/visionOS/MacOS.
|
79
|
+
* Keep this option off for general USDZ usage.
|
80
|
+
*/
|
67
81
|
@serializable()
|
68
82
|
interactive: boolean = true;
|
69
83
|
|
@@ -194,21 +208,35 @@
|
|
194
208
|
if (!objectToExport)
|
195
209
|
return null;
|
196
210
|
|
211
|
+
Progress.start("export-usdz", { onProgress: (progress) => {
|
212
|
+
this.dispatchEvent(new CustomEvent("export-progress", { detail: { progress } }));
|
213
|
+
}});
|
214
|
+
Progress.report("export-usdz", { message: "Starting export", totalSteps: 40, currentStep: 0 });
|
215
|
+
Progress.report("export-usdz", { message: "Load progressive textures", autoStep: 5 });
|
216
|
+
Progress.start("export-usdz-textures", "export-usdz");
|
197
217
|
// trigger progressive textures to be loaded:
|
198
218
|
const renderers = GameObject.getComponentsInChildren(objectToExport, Renderer);
|
199
219
|
const progressiveLoading = new Array<Promise<any>>();
|
220
|
+
let loadedTextures = 0;
|
200
221
|
for (const rend of renderers) {
|
201
222
|
for (const mat of rend.sharedMaterials) {
|
202
223
|
if (mat) {
|
203
224
|
const task = rend.loadProgressiveTextures(mat);
|
204
225
|
if (task instanceof Promise)
|
205
|
-
progressiveLoading.push(
|
226
|
+
progressiveLoading.push(new Promise<void>((resolve, reject) => {
|
227
|
+
task.then(() => {
|
228
|
+
loadedTextures++;
|
229
|
+
Progress.report("export-usdz-textures", { message: "Loaded progressive texture", currentStep: loadedTextures, totalSteps: progressiveLoading.length });
|
230
|
+
resolve();
|
231
|
+
}).catch((err) => reject(err));
|
232
|
+
}));
|
206
233
|
}
|
207
234
|
}
|
208
235
|
}
|
209
236
|
if (debug) showBalloonMessage("Load textures: " + progressiveLoading.length);
|
210
237
|
await Promise.all(progressiveLoading);
|
211
238
|
if (debug) showBalloonMessage("Load textures: done");
|
239
|
+
Progress.end("export-usdz-textures");
|
212
240
|
|
213
241
|
// apply XRFlags
|
214
242
|
const currentXRState = XRState.Global.Mask;
|
@@ -225,17 +253,31 @@
|
|
225
253
|
extensions.push(animExt);
|
226
254
|
|
227
255
|
const eventArgs = { self: this, exporter: exporter, extensions: extensions, object: objectToExport };
|
256
|
+
Progress.report("export-usdz", "Invoking before-export" );
|
228
257
|
this.dispatchEvent(new CustomEvent("before-export", { detail: eventArgs }))
|
229
258
|
|
230
259
|
// Implicit registration and actions for Animators and Animation components
|
231
260
|
// Currently, Animators properly build PlayAnimation actions, but Animation components don't.
|
232
|
-
|
261
|
+
|
262
|
+
Progress.report("export-usdz", "auto export animations and audio sources");
|
263
|
+
const implicitBehaviors = new Array<Object3D>();
|
233
264
|
if (this.autoExportAnimations) {
|
234
|
-
|
265
|
+
implicitBehaviors.push(...registerAnimatorsImplictly(objectToExport, animExt));
|
235
266
|
}
|
267
|
+
const audioExt = this.extensions.find(ext => ext.extensionName === "Audio");
|
268
|
+
if (audioExt && this.autoExportAudioSources)
|
269
|
+
implicitBehaviors.push(...registerAudioSourcesImplictly(objectToExport, audioExt as AudioExtension));
|
236
270
|
|
237
271
|
//@ts-ignore
|
238
272
|
exporter.debug = debug;
|
273
|
+
exporter.keepObject = (object) => {
|
274
|
+
// TODO We need to take more care with disabled renderers. This currently breaks when any renderer is disabled
|
275
|
+
// and then enabled at runtime by e.g. SetActiveOnClick, requiring extra work to enable them before export,
|
276
|
+
// cache their state, and then reset their state after export. See
|
277
|
+
const renderer = GameObject.getComponent( object, Renderer )
|
278
|
+
if (renderer && !renderer.enabled) return false;
|
279
|
+
return true;
|
280
|
+
}
|
239
281
|
|
240
282
|
// sanitize anchoring types
|
241
283
|
if (this.anchoringType !== "plane" && this.anchoringType !== "none" && this.anchoringType !== "image" && this.anchoringType !== "face")
|
@@ -243,6 +285,7 @@
|
|
243
285
|
if (this.planeAnchoringAlignment !== "horizontal" && this.planeAnchoringAlignment !== "vertical" && this.planeAnchoringAlignment !== "any")
|
244
286
|
this.planeAnchoringAlignment = "horizontal";
|
245
287
|
|
288
|
+
Progress.report("export-usdz", "Invoking exporter.parse" );
|
246
289
|
//@ts-ignore
|
247
290
|
const arraybuffer = await exporter.parse(this.objectToExport, {
|
248
291
|
ar: {
|
@@ -260,10 +303,11 @@
|
|
260
303
|
const blob = new Blob([arraybuffer], { type: 'application/octet-stream' });
|
261
304
|
|
262
305
|
// cleanup – implicit animation behaviors need to be removed again
|
263
|
-
for (const go of
|
306
|
+
for (const go of implicitBehaviors) {
|
264
307
|
GameObject.destroy(go);
|
265
308
|
}
|
266
309
|
|
310
|
+
Progress.report("export-usdz", "Invoking after-export" );
|
267
311
|
this.dispatchEvent(new CustomEvent("after-export", { detail: eventArgs }))
|
268
312
|
|
269
313
|
// restore XR flags
|
@@ -277,6 +321,7 @@
|
|
277
321
|
// this.link.href = URL.createObjectURL(blob2);
|
278
322
|
// this.link.click();
|
279
323
|
|
324
|
+
Progress.end("export-usdz");
|
280
325
|
return blob;
|
281
326
|
}
|
282
327
|
|
@@ -396,7 +441,7 @@
|
|
396
441
|
// that#s the case when no objectToExport is explictly assigned and the whole scene is being exported
|
397
442
|
if(!sessionRoot) sessionRoot = GameObject.getComponentInChildren(this.objectToExport, WebARSessionRoot);
|
398
443
|
|
399
|
-
if(debug) console.log("applyWebARSessionRoot", sessionRoot);
|
444
|
+
if (debug) console.log("applyWebARSessionRoot", sessionRoot);
|
400
445
|
|
401
446
|
if (!sessionRoot) {
|
402
447
|
if(debug) console.warn("No WebARSessionRoot found in parent hierarchy", this.objectToExport);
|
@@ -406,7 +451,6 @@
|
|
406
451
|
// either apply the scale to the object being exported or to the sessionRoot object itself
|
407
452
|
const target = hasSessionRootInParentHierarchy ? this.objectToExport : sessionRoot.gameObject;
|
408
453
|
const scale = 1 / sessionRoot!.arScale;
|
409
|
-
if (debug) console.log("AR Session Root scale", scale, target);
|
410
454
|
target.matrix.makeScale(scale, scale, scale);
|
411
455
|
if (sessionRoot.invertForward) {
|
412
456
|
target.matrix.multiply(USDZExporter.invertForwardMatrix);
|
@@ -6,7 +6,7 @@
|
|
6
6
|
import { TextAnchor } from "../../../ui/Text.js";
|
7
7
|
import type { IUSDExporterExtension } from "../Extension.js";
|
8
8
|
import type { IBehaviorElement } from "../extensions/behavior/BehavioursBuilder.js";
|
9
|
-
import { USDDocument, USDObject, USDWriter, USDZExporterContext } from "../ThreeUSDZExporter.js";
|
9
|
+
import { getMaterialNameForUSD,USDDocument, USDObject, USDWriter, USDZExporterContext } from "../ThreeUSDZExporter.js";
|
10
10
|
|
11
11
|
|
12
12
|
export enum TextWrapMode {
|
@@ -106,11 +106,10 @@
|
|
106
106
|
writer.appendLine(`token verticalAlignment = "${this.verticalAlignment}"`);
|
107
107
|
|
108
108
|
if (this.material !== undefined) {
|
109
|
-
writer.appendLine(`rel material:binding = </StageRoot/Materials
|
109
|
+
writer.appendLine(`rel material:binding = </StageRoot/Materials/${getMaterialNameForUSD(this.material)}>`)
|
110
110
|
}
|
111
111
|
|
112
112
|
writer.closeBlock();
|
113
|
-
|
114
113
|
}
|
115
114
|
|
116
115
|
}
|
@@ -12,7 +12,7 @@
|
|
12
12
|
|
13
13
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
14
14
|
import { getParam } from "../../engine/engine_utils.js";
|
15
|
-
import { NeedleXREventArgs } from "../../engine/engine_xr.js";
|
15
|
+
import type { NeedleXREventArgs } from "../../engine/engine_xr.js";
|
16
16
|
import { Behaviour } from "../Component.js";
|
17
17
|
import { RGBAColor } from "../js-extensions/RGBAColor.js"
|
18
18
|
|
@@ -5,11 +5,11 @@
|
|
5
5
|
import { Context } from "../../engine/engine_context.js";
|
6
6
|
import { ObjectUtils, PrimitiveType } from "../../engine/engine_create_objects.js";
|
7
7
|
import { destroy, instantiate } from "../../engine/engine_gameobject.js";
|
8
|
-
import { NEPointerEvent } from "../../engine/engine_input.js";
|
8
|
+
import { InputEventQueue, NEPointerEvent } from "../../engine/engine_input.js";
|
9
9
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
10
|
-
import { IComponent, IGameObject } from "../../engine/engine_types.js";
|
10
|
+
import type { IComponent, IGameObject } from "../../engine/engine_types.js";
|
11
11
|
import { getParam } from "../../engine/engine_utils.js";
|
12
|
-
import { NeedleXRController, NeedleXREventArgs, NeedleXRHitTestResult, NeedleXRSession } from "../../engine/engine_xr.js";
|
12
|
+
import { NeedleXRController, type NeedleXREventArgs, type NeedleXRHitTestResult, NeedleXRSession } from "../../engine/engine_xr.js";
|
13
13
|
import { Behaviour, GameObject } from "../Component.js";
|
14
14
|
|
15
15
|
// https://github.com/takahirox/takahirox.github.io/blob/master/js.mmdeditor/examples/js/controls/DeviceOrientationControls.js
|
@@ -135,11 +135,11 @@
|
|
135
135
|
}
|
136
136
|
this._reticle.length = 0;
|
137
137
|
this._isPlacing = true;
|
138
|
-
this.context.input.addEventListener("pointerup", this.onPlaceScene);
|
138
|
+
this.context.input.addEventListener("pointerup", this.onPlaceScene, InputEventQueue.Early);
|
139
139
|
}
|
140
140
|
onLeaveXR() {
|
141
141
|
// TODO: WebARSessionRoot doesnt work when we enter passthrough and leave XR without having placed the session!!!
|
142
|
-
this.context.input.removeEventListener("pointerup", this.onPlaceScene)
|
142
|
+
this.context.input.removeEventListener("pointerup", this.onPlaceScene, InputEventQueue.Early);
|
143
143
|
this.onRevertSceneChanges();
|
144
144
|
// this._anchor?.delete();
|
145
145
|
this._anchor = null;
|
@@ -305,6 +305,7 @@
|
|
305
305
|
|
306
306
|
// if we place the scene we don't want this event to be propagated to any sub-objects (via the EventSystem) anymore and trigger e.g. a click on objects for the "place tap" event
|
307
307
|
evt.stopImmediatePropagation();
|
308
|
+
evt.use();
|
308
309
|
|
309
310
|
this._isPlacing = false;
|
310
311
|
this.context.input.removeEventListener("pointerup", this.onPlaceScene);
|
@@ -455,20 +456,7 @@
|
|
455
456
|
}
|
456
457
|
|
457
458
|
private _enabled: boolean = false;
|
458
|
-
|
459
|
-
if (this._enabled) return;
|
460
|
-
this._enabled = true;
|
461
|
-
window.addEventListener('touchstart', this.touchStart, { passive: false });
|
462
|
-
window.addEventListener('touchmove', this.touchMove, { passive: false });
|
463
|
-
window.addEventListener('touchend', this.touchEnd, { passive: false });
|
464
|
-
}
|
465
|
-
disable() {
|
466
|
-
if (!this._enabled) return;
|
467
|
-
this._enabled = false;
|
468
|
-
window.removeEventListener('touchstart', this.touchStart);
|
469
|
-
window.removeEventListener('touchmove', this.touchMove);
|
470
|
-
window.removeEventListener('touchend', this.touchEnd);
|
471
|
-
}
|
459
|
+
|
472
460
|
reset() {
|
473
461
|
this._scale = 1;
|
474
462
|
this.offset.identity();
|
@@ -494,6 +482,51 @@
|
|
494
482
|
// matrix.premultiply(this.offset).premultiply(this._rotationMatrix)
|
495
483
|
}
|
496
484
|
|
485
|
+
|
486
|
+
private readonly currentlyUsedPointerIds = new Set<number>();
|
487
|
+
private readonly currentlyUnusedPointerIds = new Set<number>();
|
488
|
+
get isActive() {
|
489
|
+
return this.currentlyUsedPointerIds.size <= 0 && this.currentlyUnusedPointerIds.size > 0;
|
490
|
+
}
|
491
|
+
|
492
|
+
enable() {
|
493
|
+
if (this._enabled) return;
|
494
|
+
this._enabled = true;
|
495
|
+
|
496
|
+
this.context.input.addEventListener("pointerdown", this.onPointerDownEarly, InputEventQueue.Early);
|
497
|
+
this.context.input.addEventListener("pointerdown", this.onPointerDownLate, InputEventQueue.Late);
|
498
|
+
this.context.input.addEventListener("pointerup", this.onPointerUpEarly, InputEventQueue.Early);
|
499
|
+
|
500
|
+
// TODO: refactor the following events to use the input system
|
501
|
+
window.addEventListener('touchstart', this.touchStart, { passive: false });
|
502
|
+
window.addEventListener('touchmove', this.touchMove, { passive: false });
|
503
|
+
window.addEventListener('touchend', this.touchEnd, { passive: false });
|
504
|
+
}
|
505
|
+
disable() {
|
506
|
+
if (!this._enabled) return;
|
507
|
+
this._enabled = false;
|
508
|
+
|
509
|
+
this.context.input.removeEventListener("pointerdown", this.onPointerDownEarly, InputEventQueue.Early);
|
510
|
+
this.context.input.removeEventListener("pointerdown", this.onPointerDownLate, InputEventQueue.Late);
|
511
|
+
this.context.input.removeEventListener("pointerup", this.onPointerUpEarly, InputEventQueue.Early);
|
512
|
+
|
513
|
+
window.removeEventListener('touchstart', this.touchStart);
|
514
|
+
window.removeEventListener('touchmove', this.touchMove);
|
515
|
+
window.removeEventListener('touchend', this.touchEnd);
|
516
|
+
}
|
517
|
+
|
518
|
+
private onPointerDownEarly = (e: NEPointerEvent) => {
|
519
|
+
if (this.isActive) e.stopPropagation();
|
520
|
+
};
|
521
|
+
private onPointerDownLate = (e: NEPointerEvent) => {
|
522
|
+
if (e.used) this.currentlyUsedPointerIds.add(e.pointerId);
|
523
|
+
else if(this.currentlyUsedPointerIds.size <= 0) this.currentlyUnusedPointerIds.add(e.pointerId);
|
524
|
+
};
|
525
|
+
private onPointerUpEarly = (e: NEPointerEvent) => {
|
526
|
+
this.currentlyUsedPointerIds.delete(e.pointerId);
|
527
|
+
this.currentlyUnusedPointerIds.delete(e.pointerId);
|
528
|
+
};
|
529
|
+
|
497
530
|
// private _needsUpdate: boolean = true;
|
498
531
|
// private _rotationMatrix: Matrix4 = new Matrix4();
|
499
532
|
// private updateMatrix() {
|
@@ -540,6 +573,7 @@
|
|
540
573
|
}
|
541
574
|
private touchMove = (evt: TouchEvent) => {
|
542
575
|
if (evt.defaultPrevented) return;
|
576
|
+
if (!this.isActive) return;
|
543
577
|
|
544
578
|
if (evt.touches.length === 1) {
|
545
579
|
// if we had multiple touches before due to e.g. pinching / rotating
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
5
5
|
import { serializable } from "../../engine/engine_serialization.js";
|
6
6
|
import { getParam, isDesktop, isiOS, isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
|
7
|
-
import { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
7
|
+
import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
8
8
|
import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync.js";
|
9
9
|
import { Behaviour, GameObject } from "../Component.js";
|
10
10
|
import { USDZExporter } from "../export/usdz/USDZExporter.js";
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
5
5
|
import { serializable } from "../../engine/engine_serialization.js";
|
6
6
|
import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
|
7
|
-
import { NeedleXREventArgs, NeedleXRSession } from "../../engine/xr/index.js";
|
7
|
+
import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/xr/index.js";
|
8
8
|
import { imageToCanvas,USDWriter, USDZExporterContext } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
9
9
|
import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.js";
|
10
10
|
import { Behaviour, GameObject } from "../Component.js";
|
@@ -6,7 +6,7 @@
|
|
6
6
|
import { serializable } from "../../engine/engine_serialization.js";
|
7
7
|
import type { Vec3 } from "../../engine/engine_types.js";
|
8
8
|
import { getParam } from "../../engine/engine_utils.js";
|
9
|
-
import { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
9
|
+
import type { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
10
10
|
import { MeshCollider } from "../Collider.js";
|
11
11
|
import { Behaviour, GameObject } from "../Component.js";
|
12
12
|
// import { SimplifyModifier } from 'three/examples/jsm/modifiers/SimplifyModifier.js';
|
@@ -1,10 +1,10 @@
|
|
1
|
-
import { AxesHelper,
|
1
|
+
import { AxesHelper, Object3D, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
4
4
|
import type { IGameObject } from "../../engine/engine_types.js";
|
5
5
|
import { getParam } from "../../engine/engine_utils.js";
|
6
|
-
import {
|
7
|
-
import {
|
6
|
+
import type { IXRRig } from "../../engine/engine_xr.js";
|
7
|
+
import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
8
8
|
import { Behaviour } from "../Component.js";
|
9
9
|
import { BoxGizmo } from "../Gizmos.js";
|
10
10
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
2
|
import { serializable } from "../../../engine/engine_serialization_decorator.js";
|
3
|
-
import { NeedleXREventArgs } from "../../../engine/engine_xr.js";
|
3
|
+
import type { NeedleXREventArgs } from "../../../engine/engine_xr.js";
|
4
4
|
import { Behaviour } from "../../Component.js";
|
5
5
|
|
6
6
|
|
@@ -9,9 +9,9 @@
|
|
9
9
|
import { Gizmos } from "../../../engine/engine_gizmos.js";
|
10
10
|
import { addDracoAndKTX2Loaders } from "../../../engine/engine_loaders.js";
|
11
11
|
import { serializable } from "../../../engine/engine_serialization_decorator.js";
|
12
|
-
import { IGameObject } from "../../../engine/engine_types.js";
|
12
|
+
import type { IGameObject } from "../../../engine/engine_types.js";
|
13
13
|
import { getParam } from "../../../engine/engine_utils.js";
|
14
|
-
import { NeedleXRController, NeedleXRControllerEventArgs, NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
|
14
|
+
import { NeedleXRController, type NeedleXRControllerEventArgs, type NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
|
15
15
|
import { Behaviour, GameObject } from "../../Component.js"
|
16
16
|
|
17
17
|
const debug = getParam("debugwebxr");
|
@@ -5,15 +5,15 @@
|
|
5
5
|
|
6
6
|
import { Gizmos } from "../../../engine/engine_gizmos.js";
|
7
7
|
import { Mathf } from "../../../engine/engine_math.js";
|
8
|
-
import { RaycastTestObjectCallback } from "../../../engine/engine_physics.js";
|
8
|
+
import type { RaycastTestObjectCallback } from "../../../engine/engine_physics.js";
|
9
9
|
import { serializable } from "../../../engine/engine_serialization.js"
|
10
10
|
import { getTempVector, getWorldQuaternion, getWorldScale } from "../../../engine/engine_three_utils.js";
|
11
|
-
import { IGameObject } from "../../../engine/engine_types.js";
|
11
|
+
import type { IGameObject } from "../../../engine/engine_types.js";
|
12
12
|
import { getParam } from "../../../engine/engine_utils.js";
|
13
|
-
import { NeedleXRController, NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
|
13
|
+
import { NeedleXRController, type NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
|
14
14
|
import { Behaviour, GameObject } from "../../Component.js"
|
15
15
|
import { TeleportTarget } from "../TeleportTarget.js";
|
16
|
-
import { XRMovementBehaviour } from "../types.js";
|
16
|
+
import type { XRMovementBehaviour } from "../types.js";
|
17
17
|
|
18
18
|
const debug = getParam("debugwebxr");
|
19
19
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { IComponent } from "../engine_types.js";
|
1
|
+
import type { IComponent } from "../engine_types.js";
|
2
2
|
|
3
3
|
|
4
4
|
export interface IXRRig extends Pick<IComponent, "gameObject"> {
|
@@ -0,0 +1,395 @@
|
|
1
|
+
import { Layers, Material, Matrix3, Matrix4, Mesh, Object3D, PerspectiveCamera, Quaternion, Texture, Vector2, Vector3, Vector4 } from "three";
|
2
|
+
import ThreeMeshUI from "three-mesh-ui";
|
3
|
+
import type { Options } from "three-mesh-ui/build/types/core/elements/MeshUIBaseElement";
|
4
|
+
|
5
|
+
import { ContextRegistry } from "../engine_context_registry.js";
|
6
|
+
import { OneEuroFilterXYZ } from "../engine_math.js";
|
7
|
+
import { lookAtObject } from "../engine_three_utils.js";
|
8
|
+
import type { IContext, IGameObject } from "../engine_types.js";
|
9
|
+
|
10
|
+
|
11
|
+
let _isActive = false;
|
12
|
+
|
13
|
+
/** Enable a spatial debug console that follows the camera */
|
14
|
+
export function enableSpatialConsole(active: boolean) {
|
15
|
+
if (active) {
|
16
|
+
if (_isActive) return;
|
17
|
+
_isActive = true;
|
18
|
+
onEnable();
|
19
|
+
|
20
|
+
} else {
|
21
|
+
if (!_isActive) return;
|
22
|
+
_isActive = false;
|
23
|
+
onDisable();
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
|
28
|
+
const originalConsoleMethods: { [key: string]: Function | undefined } = {
|
29
|
+
"log": undefined,
|
30
|
+
"warn": undefined,
|
31
|
+
"error": undefined,
|
32
|
+
};
|
33
|
+
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
class SpatialMessagesHandler {
|
38
|
+
|
39
|
+
private readonly familyName = "needle-xr";
|
40
|
+
private root: ThreeMeshUI.Block | null = null;
|
41
|
+
|
42
|
+
private context: IContext | null = null;
|
43
|
+
private readonly defaultFontSize = .06;
|
44
|
+
|
45
|
+
constructor() {
|
46
|
+
this.ensureFont();
|
47
|
+
}
|
48
|
+
|
49
|
+
onEnable() {
|
50
|
+
this.context = ContextRegistry.Current || ContextRegistry.All[0];
|
51
|
+
this.context.pre_render_callbacks.push(this.onBeforeRender)
|
52
|
+
}
|
53
|
+
onDisable() {
|
54
|
+
this.context?.pre_render_callbacks.splice(this.context?.pre_render_callbacks.indexOf(this.onBeforeRender), 1);
|
55
|
+
}
|
56
|
+
|
57
|
+
private readonly targetObject = new Object3D();
|
58
|
+
private readonly oneEuroFilter = new OneEuroFilterXYZ(90, .1);
|
59
|
+
private _lastElementRemoveTime = 0;
|
60
|
+
|
61
|
+
private onBeforeRender = () => {
|
62
|
+
const cam = this.context?.mainCamera as any as IGameObject;
|
63
|
+
if (this.context && cam instanceof PerspectiveCamera) {
|
64
|
+
const root = this.getRoot() as any as IGameObject;
|
65
|
+
|
66
|
+
// TODO: need to figure out why this happens when entering VR (in the simulator at least)
|
67
|
+
if (Number.isNaN(root.position.x))
|
68
|
+
root.position.set(0, 0, 0);
|
69
|
+
|
70
|
+
this.context.scene.add(this.targetObject);
|
71
|
+
|
72
|
+
const dist = 1.8;
|
73
|
+
const forward = cam.worldForward;
|
74
|
+
forward.y = 0;
|
75
|
+
forward.normalize().multiplyScalar((dist * 100) / cam.fov);
|
76
|
+
this.targetObject.position.copy(cam.worldPosition).sub(forward);
|
77
|
+
lookAtObject(this.targetObject, cam, false, true);
|
78
|
+
|
79
|
+
this.oneEuroFilter.filter(this.targetObject.position, root.position, this.context.time.time);
|
80
|
+
const step = this.context.time.deltaTime * 10;
|
81
|
+
root.quaternion.slerp(this.targetObject.quaternion, step);
|
82
|
+
|
83
|
+
this.targetObject.removeFromParent();
|
84
|
+
this.context.scene.add(this.getRoot() as any);
|
85
|
+
|
86
|
+
if (this.context.time.time - this._lastElementRemoveTime > .3) {
|
87
|
+
this._lastElementRemoveTime = this.context.time.time;
|
88
|
+
const now = Date.now();
|
89
|
+
for (let i = 0; i < root.children.length; i++) {
|
90
|
+
const el = root.children[i];
|
91
|
+
if (el instanceof ThreeMeshUI.Text && now - el["_activatedTime"] > 10000) {
|
92
|
+
el.removeFromParent();
|
93
|
+
this._textBuffer.push(el);
|
94
|
+
break;
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
addLog(type: "log" | "warn" | "error", message: string) {
|
103
|
+
const root = this.getRoot();
|
104
|
+
|
105
|
+
const text = this.getText();
|
106
|
+
|
107
|
+
let backgroundColor = 0xffffff;
|
108
|
+
let fontColor = 0x000000;
|
109
|
+
switch (type) {
|
110
|
+
case "log":
|
111
|
+
backgroundColor = 0xffffff;
|
112
|
+
fontColor = 0x000000;
|
113
|
+
break;
|
114
|
+
case "warn":
|
115
|
+
backgroundColor = 0xffee99;
|
116
|
+
fontColor = 0x442200;
|
117
|
+
break;
|
118
|
+
case "error":
|
119
|
+
backgroundColor = 0xffaaaa;
|
120
|
+
fontColor = 0xaa0000;
|
121
|
+
break;
|
122
|
+
}
|
123
|
+
|
124
|
+
text.set({
|
125
|
+
backgroundColor: backgroundColor,
|
126
|
+
color: fontColor,
|
127
|
+
});
|
128
|
+
|
129
|
+
if (message.length > 1000) message = message.substring(0, 1000) + "...";
|
130
|
+
|
131
|
+
const hourMinuteSecond = new Date().toLocaleTimeString().split(" ")[0];
|
132
|
+
text.textContent = "[" + hourMinuteSecond + "] " + message;
|
133
|
+
text.visible = true;
|
134
|
+
text["_activatedTime"] = Date.now();
|
135
|
+
root.add(text as any);
|
136
|
+
if (this.context) this.context.scene.add(root as any);
|
137
|
+
|
138
|
+
ThreeMeshUI.update();
|
139
|
+
}
|
140
|
+
|
141
|
+
private ensureFont() {
|
142
|
+
let fontFamily = ThreeMeshUI.FontLibrary.getFontFamily(this.familyName);
|
143
|
+
|
144
|
+
if (!fontFamily) {
|
145
|
+
fontFamily = ThreeMeshUI.FontLibrary.addFontFamily(this.familyName);
|
146
|
+
const variant = fontFamily.addVariant("normal", "normal", "./include/needle/arial-msdf.json", "./include/needle/arial.png") as any as ThreeMeshUI.FontVariant;
|
147
|
+
variant?.addEventListener('ready', () => {
|
148
|
+
ThreeMeshUI.update();
|
149
|
+
});
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
|
154
|
+
private readonly textOptions: Options = {
|
155
|
+
fontSize: this.defaultFontSize,
|
156
|
+
fontFamily: this.familyName,
|
157
|
+
padding: .03,
|
158
|
+
margin: .005,
|
159
|
+
color: 0x000000,
|
160
|
+
backgroundColor: 0xffffff,
|
161
|
+
backgroundOpacity: .4,
|
162
|
+
borderRadius: .03,
|
163
|
+
};
|
164
|
+
private readonly _textBuffer: ThreeMeshUI.Text[] = [];
|
165
|
+
private getText() {
|
166
|
+
const root = this.getRoot();
|
167
|
+
if (this._textBuffer.length > 0) {
|
168
|
+
const text = this._textBuffer.pop() as any as ThreeMeshUI.Text;
|
169
|
+
text.visible = true;
|
170
|
+
setTimeout(() => this.disableDepthTestRecursive(text as any), 100);
|
171
|
+
return text;
|
172
|
+
}
|
173
|
+
if (root.children.length > 20) {
|
174
|
+
return root.children[0] as any as ThreeMeshUI.Text;
|
175
|
+
}
|
176
|
+
const newText = new ThreeMeshUI.Text(this.textOptions);
|
177
|
+
setTimeout(() => this.disableDepthTestRecursive(newText as any), 500);
|
178
|
+
setTimeout(() => this.disableDepthTestRecursive(newText as any), 1500);
|
179
|
+
return newText;
|
180
|
+
}
|
181
|
+
private disableDepthTestRecursive(obj: Object3D) {
|
182
|
+
obj.traverseVisible((t: Object3D) => {
|
183
|
+
t.renderOrder = 1000;
|
184
|
+
t.layers.set(2);
|
185
|
+
const mat = (t as Mesh).material as Material;
|
186
|
+
if (mat) {
|
187
|
+
mat.depthWrite = false;
|
188
|
+
mat.depthTest = false;
|
189
|
+
}
|
190
|
+
ThreeMeshUI.update();
|
191
|
+
});
|
192
|
+
}
|
193
|
+
|
194
|
+
private getRoot() {
|
195
|
+
if (this.root) {
|
196
|
+
return this.root;
|
197
|
+
}
|
198
|
+
|
199
|
+
const fontSize = this.defaultFontSize;
|
200
|
+
const defaultOptions: Options = {
|
201
|
+
boxSizing: 'border-box',
|
202
|
+
fontFamily: this.familyName,
|
203
|
+
width: "2.6",
|
204
|
+
fontSize: fontSize,
|
205
|
+
color: 0x000000,
|
206
|
+
lineHeight: 1,
|
207
|
+
backgroundColor: 0xffffff,
|
208
|
+
backgroundOpacity: 0,
|
209
|
+
// borderColor: 0xffffff,
|
210
|
+
// borderOpacity: .5,
|
211
|
+
// borderWidth: 0.01,
|
212
|
+
// padding: 0.01,
|
213
|
+
whiteSpace: 'pre-wrap',
|
214
|
+
flexDirection: 'column-reverse',
|
215
|
+
};
|
216
|
+
this.root = new ThreeMeshUI.Block(defaultOptions);
|
217
|
+
|
218
|
+
return this.root;
|
219
|
+
}
|
220
|
+
}
|
221
|
+
|
222
|
+
|
223
|
+
|
224
|
+
let messagesHandler: SpatialMessagesHandler | null = null;
|
225
|
+
|
226
|
+
function onEnable() {
|
227
|
+
// create messages handler
|
228
|
+
if (!messagesHandler) messagesHandler = new SpatialMessagesHandler();
|
229
|
+
messagesHandler.onEnable();
|
230
|
+
|
231
|
+
// save original console methods
|
232
|
+
for (const key in originalConsoleMethods) {
|
233
|
+
originalConsoleMethods[key] = console[key];
|
234
|
+
let isLogging = false;
|
235
|
+
console[key] = function () {
|
236
|
+
// call original console method
|
237
|
+
originalConsoleMethods[key]?.apply(console, arguments);
|
238
|
+
// prevent circular calls
|
239
|
+
if (isLogging) return;
|
240
|
+
try {
|
241
|
+
isLogging = true;
|
242
|
+
onLog(key as "log" | "warn" | "error", ...arguments);
|
243
|
+
} finally {
|
244
|
+
isLogging = false;
|
245
|
+
}
|
246
|
+
};
|
247
|
+
}
|
248
|
+
|
249
|
+
console.log("Enabling Spatial Console");
|
250
|
+
}
|
251
|
+
function onDisable() {
|
252
|
+
messagesHandler?.onDisable();
|
253
|
+
for (const key in originalConsoleMethods) {
|
254
|
+
console[key] = originalConsoleMethods[key];
|
255
|
+
}
|
256
|
+
}
|
257
|
+
|
258
|
+
|
259
|
+
|
260
|
+
const seen = new Map<string, any>();
|
261
|
+
|
262
|
+
function onLog(key: "log" | "warn" | "error", ...args: any[]) {
|
263
|
+
try {
|
264
|
+
seen.clear();
|
265
|
+
switch (key) {
|
266
|
+
case "log":
|
267
|
+
messagesHandler?.addLog("log", getLogString());
|
268
|
+
break;
|
269
|
+
case "warn":
|
270
|
+
messagesHandler?.addLog("warn", getLogString());
|
271
|
+
break;
|
272
|
+
case "error":
|
273
|
+
messagesHandler?.addLog("error", getLogString());
|
274
|
+
break;
|
275
|
+
}
|
276
|
+
}
|
277
|
+
catch (err) {
|
278
|
+
console.error("Error in spatial console", err);
|
279
|
+
}
|
280
|
+
finally {
|
281
|
+
seen.clear();
|
282
|
+
}
|
283
|
+
|
284
|
+
|
285
|
+
function getLogString() {
|
286
|
+
let str = "";
|
287
|
+
for (let i = 0; i < args.length; i++) {
|
288
|
+
const el = args[i];
|
289
|
+
str += serialize(el);
|
290
|
+
if (i < args.length - 1) str += ", ";
|
291
|
+
}
|
292
|
+
return str;
|
293
|
+
}
|
294
|
+
function serialize(value: any, level: number = 0): string {
|
295
|
+
|
296
|
+
if (typeof value === "string") {
|
297
|
+
return "\"" + value + "\"";
|
298
|
+
}
|
299
|
+
else if (typeof value === "number") {
|
300
|
+
const hasDecimal = value % 1 !== 0;
|
301
|
+
if (hasDecimal) {
|
302
|
+
const str = value.toFixed(5);
|
303
|
+
const dotIndex = str.indexOf(".");
|
304
|
+
let i = str.length - 1;
|
305
|
+
while (i > dotIndex && str[i] === "0") i--;
|
306
|
+
return str.substring(0, i + 1);
|
307
|
+
}
|
308
|
+
return value.toString();
|
309
|
+
}
|
310
|
+
else if (Array.isArray(value)) {
|
311
|
+
let res = "[";
|
312
|
+
for (let i = 0; i < value.length; i++) {
|
313
|
+
const val = value[i];
|
314
|
+
res += serialize(val, level + 1);
|
315
|
+
if (i < value.length - 1) res += ", ";
|
316
|
+
}
|
317
|
+
res += "]";
|
318
|
+
return res;
|
319
|
+
}
|
320
|
+
else if (value === null) {
|
321
|
+
return "null";
|
322
|
+
}
|
323
|
+
else if (value === undefined) {
|
324
|
+
return "undefined";
|
325
|
+
}
|
326
|
+
else if (typeof value === "function") {
|
327
|
+
return value.name + "()";
|
328
|
+
}
|
329
|
+
|
330
|
+
|
331
|
+
// if (value instanceof Object3D) {
|
332
|
+
// const name = value.name?.length > 0 ? value.name : ("object@" + (value["guid"] ?? value.uuid));;
|
333
|
+
// return name;
|
334
|
+
// }
|
335
|
+
if (value instanceof Vector2) return `(${serialize(value.x)}, ${serialize(value.y)})`;
|
336
|
+
if (value instanceof Vector3) return `(${serialize(value.x)}, ${serialize(value.y)}, ${serialize(value.z)})`;
|
337
|
+
if (value instanceof Vector4) return `(${serialize(value.x)}, ${serialize(value.y)}, ${serialize(value.z)}, ${serialize(value.w)})`;
|
338
|
+
if (value instanceof Quaternion) return `(${serialize(value.x)}, ${serialize(value.y)}, ${serialize(value.z)}, ${serialize(value.w)})`;
|
339
|
+
if (value instanceof Material) return value.name;
|
340
|
+
if (value instanceof Texture) return value.name;
|
341
|
+
if (value instanceof Matrix3) return `[${value.elements.join(", ")}]`;
|
342
|
+
if (value instanceof Matrix4) return `[${value.elements.join(", ")}]`;
|
343
|
+
if (value instanceof Layers) return value.mask.toString();
|
344
|
+
|
345
|
+
if (typeof value === "object") {
|
346
|
+
if (seen.has(value)) return "*";
|
347
|
+
let res = "{\n";
|
348
|
+
res += pad(level);
|
349
|
+
const keys = Object.keys(value);
|
350
|
+
let line = "";
|
351
|
+
for (let i = 0; i < keys.length; i++) {
|
352
|
+
const key = keys[i];
|
353
|
+
const val = value[key];
|
354
|
+
if (seen.has(val)) {
|
355
|
+
line += ""
|
356
|
+
continue;
|
357
|
+
}
|
358
|
+
seen.set(val, true);
|
359
|
+
line += key + ":" + serialize(val, level + 1);
|
360
|
+
if (i < keys.length - 1) line += ", ";
|
361
|
+
if (line.length >= 60) {
|
362
|
+
line += "\n";
|
363
|
+
line += pad(level);
|
364
|
+
res += line;
|
365
|
+
line = "";
|
366
|
+
}
|
367
|
+
}
|
368
|
+
res += line;
|
369
|
+
res += "\n}";
|
370
|
+
return res;
|
371
|
+
// return JSON.stringify(value, (_key, value) => {
|
372
|
+
// if (seen.has(value)) return seen.get(value);
|
373
|
+
// seen.set(value, "-");
|
374
|
+
// const res = serialize(value);
|
375
|
+
// seen.set(value, res);
|
376
|
+
// return _key;
|
377
|
+
// }, 1);
|
378
|
+
}
|
379
|
+
|
380
|
+
|
381
|
+
return value;
|
382
|
+
}
|
383
|
+
|
384
|
+
function pad(spaces: number) {
|
385
|
+
let res = "";
|
386
|
+
for (let i = 0; i < spaces; i++) {
|
387
|
+
res += " ";
|
388
|
+
}
|
389
|
+
return res;
|
390
|
+
}
|
391
|
+
}
|
392
|
+
|
393
|
+
// // this is just a hack - the spatial console should be enabled from the user or the NeedleXRSession
|
394
|
+
// if (getParam("debugwebxr") || getParam("console"))
|
395
|
+
// setTimeout(() => enableSpatialConsole(true), 1000);
|
@@ -0,0 +1,238 @@
|
|
1
|
+
import { getParam } from "./engine_utils.js";
|
2
|
+
|
3
|
+
const showProgressLogs = getParam("debugprogress");
|
4
|
+
|
5
|
+
/** Gets the date formatted as 20240220-161993. When no Date is passed in, the current local date is used. */
|
6
|
+
export function getFormattedDate(date?: Date) {
|
7
|
+
date = date || new Date();
|
8
|
+
|
9
|
+
const month = date.getMonth() + 1;
|
10
|
+
const day = date.getDate();
|
11
|
+
const hour = date.getHours();
|
12
|
+
const min = date.getMinutes();
|
13
|
+
const sec = date.getSeconds();
|
14
|
+
|
15
|
+
const s_month = (month < 10 ? "0" : "") + month;
|
16
|
+
const s_day = (day < 10 ? "0" : "") + day;
|
17
|
+
const s_hour = (hour < 10 ? "0" : "") + hour;
|
18
|
+
const s_min = (min < 10 ? "0" : "") + min;
|
19
|
+
const s_sec = (sec < 10 ? "0" : "") + sec;
|
20
|
+
|
21
|
+
return date.getFullYear() + s_month + s_day + "-" + s_hour + s_min + s_sec;
|
22
|
+
}
|
23
|
+
|
24
|
+
declare type ProgressOptions = {
|
25
|
+
message?: string,
|
26
|
+
progress?: number,
|
27
|
+
autoStep?: boolean | number;
|
28
|
+
currentStep?: number,
|
29
|
+
totalSteps?: number
|
30
|
+
};
|
31
|
+
|
32
|
+
declare type ProgressStartOptions = {
|
33
|
+
/** This progress scope will be nested below parentScope */
|
34
|
+
parentScope?: string,
|
35
|
+
/** Callback with progress in 0..1 range. */
|
36
|
+
onProgress?: (progress: number) => void,
|
37
|
+
/** Log timings using console.time() and console.timeLog(). */
|
38
|
+
logTimings?: boolean,
|
39
|
+
};
|
40
|
+
|
41
|
+
/** Progress reporting utility.
|
42
|
+
* See `Progress.start` for usage examples.
|
43
|
+
*/
|
44
|
+
export class Progress {
|
45
|
+
|
46
|
+
/** Start a new progress reporting scope. Make sure to close it with Progress.end.
|
47
|
+
* @param scope The scope to start progress reporting for.
|
48
|
+
* @param options Parent scope, onProgress callback and logging. If only a string is provided, it's used as parentScope.
|
49
|
+
* @example
|
50
|
+
* // Manual usage:
|
51
|
+
* Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
|
52
|
+
* Progress.report("export-usdz", { message: "Exporting object 1", currentStep: 1, totalSteps: 3 });
|
53
|
+
* Progress.report("export-usdz", { message: "Exporting object 2", currentStep: 2, totalSteps: 3 });
|
54
|
+
* Progress.report("export-usdz", { message: "Exporting object 3", currentStep: 3, totalSteps: 3 });
|
55
|
+
*
|
56
|
+
* // Auto step usage:
|
57
|
+
* Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
|
58
|
+
* Progress.report("export-usdz", { message: "Exporting objects", autoStep: true, totalSteps: 3 });
|
59
|
+
* Progress.report("export-usdz", "Exporting object 1");
|
60
|
+
* Progress.report("export-usdz", "Exporting object 2");
|
61
|
+
* Progress.report("export-usdz", "Exporting object 3");
|
62
|
+
* Progress.end("export-usdz");
|
63
|
+
*
|
64
|
+
* // Auto step with weights:
|
65
|
+
* Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
|
66
|
+
* Progress.report("export-usdz", { message: "Exporting objects", autoStep: true, totalSteps: 10 });
|
67
|
+
* Progress.report("export-usdz", { message: "Exporting object 1", autoStep: 8 }); // will advance to 80% progress
|
68
|
+
* Progress.report("export-usdz", "Exporting object 2"); // 90%
|
69
|
+
* Progress.report("export-usdz", "Exporting object 3"); // 100%
|
70
|
+
*
|
71
|
+
* // Child scopes:
|
72
|
+
* Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
|
73
|
+
* Progress.report("export-usdz", { message: "Overall export", autoStep: true, totalSteps: 2 });
|
74
|
+
* Progress.start("export-usdz-objects", "export-usdz");
|
75
|
+
* Progress.report("export-usdz-objects", { message: "Exporting objects", autoStep: true, totalSteps: 3 });
|
76
|
+
* Progress.report("export-usdz-objects", "Exporting object 1");
|
77
|
+
* Progress.report("export-usdz-objects", "Exporting object 2");
|
78
|
+
* Progress.report("export-usdz-objects", "Exporting object 3");
|
79
|
+
* Progress.end("export-usdz-objects");
|
80
|
+
* Progress.report("export-usdz", "Exporting materials");
|
81
|
+
* Progress.end("export-usdz");
|
82
|
+
*
|
83
|
+
* // Enable console logging:
|
84
|
+
* Progress.start("export-usdz", { logTimings: true });
|
85
|
+
*/
|
86
|
+
static start(scope: string, options?: ProgressStartOptions | string ) {
|
87
|
+
if (typeof options === "string") options = { parentScope: options };
|
88
|
+
const p = new ProgressEntry(scope, options);
|
89
|
+
progressCache.set(scope, p);
|
90
|
+
}
|
91
|
+
|
92
|
+
/** Report progress for a formerly started scope.
|
93
|
+
* @param scope The scope to report progress for.
|
94
|
+
* @param options Options for the progress report. If a string is passed, it will be used as the message.
|
95
|
+
* @example
|
96
|
+
* // auto step and show a message
|
97
|
+
* Progress.report("export-usdz", "Exporting object 1");
|
98
|
+
* // same as above
|
99
|
+
* Progress.report("export-usdz", { message: "Exporting object 1", autoStep: true });
|
100
|
+
* // show the current step and total steps and implicitly calculate progress as 10%
|
101
|
+
* Progress.report("export-usdz", { currentStep: 1, totalSteps: 10 });
|
102
|
+
* // enable auto step mode, following calls that have autoStep true will increase currentStep automatically.
|
103
|
+
* Progress.report("export-usdz", { totalSteps: 20, autoStep: true });
|
104
|
+
* // show the progress as 50%
|
105
|
+
* Progress.report("export-usdz", { progress: 0.5 });
|
106
|
+
* // give this step a weight of 20, which changes how progress is calculated. Useful for steps that take longer and/or have child scopes.
|
107
|
+
* Progress.report("export-usdz", { message. "Long process", autoStep: 20 });
|
108
|
+
* // show the current step and total steps and implicitly calculate progress as 10%
|
109
|
+
* Progress.report("export-usdz", { currentStep: 1, totalSteps: 10 });
|
110
|
+
*/
|
111
|
+
static report(scope: string, options?: ProgressOptions | string) {
|
112
|
+
const p = progressCache.get(scope);
|
113
|
+
if (!p) {
|
114
|
+
console.warn("Reporting progress for non-existing scope", scope);
|
115
|
+
return;
|
116
|
+
}
|
117
|
+
if (typeof options === "string") options = { message: options, autoStep: true };
|
118
|
+
p.report(options);
|
119
|
+
}
|
120
|
+
|
121
|
+
/** End a formerly started scope. This will also report the progress as 100%.
|
122
|
+
* @remarks Will warn if any child scope is still running (progress < 1).
|
123
|
+
*/
|
124
|
+
static end(scope: string) {
|
125
|
+
const p = progressCache.get(scope);
|
126
|
+
if (!p) return;
|
127
|
+
p.end();
|
128
|
+
progressCache.delete(scope);
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
const progressCache: Map<string, ProgressEntry> = new Map<string, ProgressEntry>();
|
133
|
+
|
134
|
+
/** Internal class that handles Progress instances and their parent/child relationship. */
|
135
|
+
class ProgressEntry {
|
136
|
+
private scopeLabel: string;
|
137
|
+
private parentScope?: ProgressEntry;
|
138
|
+
private childScopes: Array<ProgressEntry> = [];
|
139
|
+
private parentDepth = 0;
|
140
|
+
private lastStep? = 0;
|
141
|
+
private lastAutoStepWeight = 1;
|
142
|
+
private lastTotalSteps? = 0;
|
143
|
+
private onProgress?: (progress: number) => void;
|
144
|
+
private showLogs: boolean = false;
|
145
|
+
|
146
|
+
selfProgress: number = 0;
|
147
|
+
totalProgress: number = 0;
|
148
|
+
selfReports: number = 0;
|
149
|
+
totalReports: number = 0;
|
150
|
+
|
151
|
+
constructor(scope: string, options?: ProgressStartOptions) {
|
152
|
+
this.parentScope = options?.parentScope ? progressCache.get(options.parentScope) : undefined;
|
153
|
+
if (this.parentScope) {
|
154
|
+
this.parentScope.childScopes.push(this);
|
155
|
+
this.parentDepth = this.parentScope.parentDepth + 1;
|
156
|
+
}
|
157
|
+
this.scopeLabel = " ".repeat(this.parentDepth * 2) + scope;
|
158
|
+
this.showLogs = options?.logTimings ?? showProgressLogs !== undefined;
|
159
|
+
if (this.showLogs) console.time(this.scopeLabel);
|
160
|
+
this.onProgress = options?.onProgress;
|
161
|
+
}
|
162
|
+
|
163
|
+
report(options?: ProgressOptions, indirect: boolean = false) {
|
164
|
+
if (options) {
|
165
|
+
if (options.totalSteps !== undefined)
|
166
|
+
this.lastTotalSteps = options.totalSteps;
|
167
|
+
if (options.currentStep !== undefined)
|
168
|
+
this.lastStep = options.currentStep;
|
169
|
+
if (options.autoStep !== undefined) {
|
170
|
+
if (options.currentStep === undefined) {
|
171
|
+
if (this.lastStep === undefined) this.lastStep = 0;
|
172
|
+
const stepIncrease = typeof options.autoStep === "number" ? options.autoStep : 1;
|
173
|
+
this.lastStep += this.lastAutoStepWeight;
|
174
|
+
this.lastAutoStepWeight = stepIncrease;
|
175
|
+
options.currentStep = this.lastStep;
|
176
|
+
}
|
177
|
+
options.totalSteps = this.lastTotalSteps;
|
178
|
+
}
|
179
|
+
if (options.progress !== undefined)
|
180
|
+
this.selfProgress = options.progress;
|
181
|
+
else if (options.currentStep !== undefined && options.totalSteps !== undefined) {
|
182
|
+
this.selfProgress = options.currentStep / options.totalSteps;
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|