@@ -30,6 +30,9 @@
|
|
30
30
|
private root: Object3D;
|
31
31
|
private clip: AnimationClip | null;
|
32
32
|
|
33
|
+
// Playback speed. Does not affect how the animation is written, just how fast actions play it back.
|
34
|
+
speed?: number;
|
35
|
+
|
33
36
|
constructor(ext: AnimationExtension, root: Object3D, clip: AnimationClip | null) {
|
34
37
|
this.ext = ext;
|
35
38
|
this.root = root;
|
@@ -498,12 +501,32 @@
|
|
498
501
|
const skeleton = model.skinnedMesh.skeleton;
|
499
502
|
|
500
503
|
const boneAndInverse = new Array<{bone: Object3D, inverse: Matrix4}>();
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
504
|
+
|
505
|
+
const sortedBones:Bone[] = [];
|
506
|
+
const uuidsFound:string[] = [];
|
507
|
+
for(const bone of skeleton.bones ){
|
508
|
+
if (bone.parent!.type !== 'Bone'){
|
509
|
+
sortedBones.push(bone);
|
510
|
+
const inverse = skeleton.boneInverses[skeleton.bones.indexOf(bone)];
|
511
|
+
boneAndInverse.push({bone, inverse});
|
512
|
+
uuidsFound.push(bone.uuid);
|
513
|
+
}
|
505
514
|
}
|
506
515
|
|
516
|
+
while (uuidsFound.length < skeleton.bones.length){
|
517
|
+
for(const sortedBone of sortedBones){
|
518
|
+
const children = sortedBone.children as Bone[];
|
519
|
+
for (const childBone of children){
|
520
|
+
if (uuidsFound.indexOf(childBone.uuid) === -1 && skeleton.bones.indexOf(childBone) !== -1){
|
521
|
+
sortedBones.push(childBone);
|
522
|
+
const childInverse = skeleton.boneInverses[skeleton.bones.indexOf(childBone)];
|
523
|
+
boneAndInverse.push({bone: childBone, inverse: childInverse});
|
524
|
+
uuidsFound.push(childBone.uuid);
|
525
|
+
}
|
526
|
+
}
|
527
|
+
}
|
528
|
+
}
|
529
|
+
|
507
530
|
for (const bone of findStructuralNodesInBoneHierarchy(skeleton.bones)) {
|
508
531
|
boneAndInverse.push({bone, inverse: bone.matrixWorld.clone().invert()});
|
509
532
|
}
|
@@ -8,6 +8,7 @@
|
|
8
8
|
import type { AnimationExtension } from "../extensions/Animation.js";
|
9
9
|
import type { AudioExtension } from "../extensions/behavior/AudioExtension.js";
|
10
10
|
import { PlayAnimationOnClick, PlayAudioOnClick } from "../extensions/behavior/BehaviourComponents.js";
|
11
|
+
import { ActionBuilder,BehaviorModel, TriggerBuilder } from "../extensions/behavior/BehavioursBuilder.js";
|
11
12
|
|
12
13
|
const debug = getParam("debugusdz");
|
13
14
|
|
@@ -148,6 +149,10 @@
|
|
148
149
|
|
149
150
|
const constructedObjects = new Array<Object3D>();
|
150
151
|
|
152
|
+
if (debug) {
|
153
|
+
console.log({ audioSources, playAudioOnClicks });
|
154
|
+
}
|
155
|
+
|
151
156
|
// Remove all audio sources that are already referenced from existing PlayAudioOnClick components
|
152
157
|
for (const player of playAudioOnClicks) {
|
153
158
|
if (!player.target) continue;
|
@@ -164,6 +169,7 @@
|
|
164
169
|
const newComponent = new PlayAudioOnClick();
|
165
170
|
newComponent.target = audioSource;
|
166
171
|
newComponent.name = "PlayAudioOnClick_implicitAtStart_";
|
172
|
+
newComponent.trigger = "start";
|
167
173
|
const go = new Object3D();
|
168
174
|
GameObject.addComponent(go, newComponent);
|
169
175
|
console.log("implicit PlayAudioOnStart", go, newComponent);
|
@@ -175,4 +181,12 @@
|
|
175
181
|
}
|
176
182
|
|
177
183
|
return constructedObjects;
|
184
|
+
}
|
185
|
+
|
186
|
+
export function disableObjectsAtStart(objects: Array<Object3D>) {
|
187
|
+
const newComponent = new BehaviorModel("DisableAtStart",
|
188
|
+
TriggerBuilder.sceneStartTrigger(),
|
189
|
+
ActionBuilder.fadeAction(objects, 0, false),
|
190
|
+
)
|
191
|
+
return newComponent;
|
178
192
|
}
|
@@ -46,6 +46,16 @@
|
|
46
46
|
|
47
47
|
if (!this.files.some(f => f.path === audioSource.clip)) {
|
48
48
|
this.files.push({ path: audioSource.clip, name: safeClipNameWithExt });
|
49
|
+
|
50
|
+
const lowerCase = safeClipNameWithExt.toLowerCase();
|
51
|
+
// Warn for non-supported audio formats.
|
52
|
+
// See https://openusd.org/release/wp_usdaudio.html#id9
|
53
|
+
if (_context.quickLookCompatible &&
|
54
|
+
!lowerCase.endsWith(".mp3") &&
|
55
|
+
!lowerCase.endsWith(".wav") &&
|
56
|
+
!lowerCase.endsWith(".m4a")) {
|
57
|
+
console.error("Audio file " + audioSource.clip + " from " + audioSource.name + " is not an MP3 or WAV file. QuickLook may not support playing it.");
|
58
|
+
}
|
49
59
|
}
|
50
60
|
|
51
61
|
// Turns out that in visionOS versions, using UsdAudio together with preliminary behaviours
|
@@ -148,7 +148,7 @@
|
|
148
148
|
if (needsUpdate)
|
149
149
|
ThreeMeshUI.update();
|
150
150
|
|
151
|
-
if (debug) console.
|
151
|
+
if (debug) console.warn("Added shadow component", this.shadowComponent);
|
152
152
|
}
|
153
153
|
|
154
154
|
protected setShadowComponentOwner(current: ThreeMeshUI.MeshUIBaseElement | Object3D | null | undefined) {
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import { getParam } from "../../../../../engine/engine_utils.js";
|
2
2
|
import { GameObject } from "../../../../Component.js";
|
3
3
|
import type { IUSDExporterExtension } from "../../Extension.js";
|
4
|
-
import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
5
|
-
import {
|
4
|
+
import type { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
5
|
+
import { AudioExtension } from "./AudioExtension.js";
|
6
|
+
import type { BehaviorModel } from "./BehavioursBuilder.js";
|
6
7
|
|
7
8
|
const debug = getParam("debugusdzbehaviours");
|
8
9
|
|
@@ -26,11 +27,26 @@
|
|
26
27
|
this.behaviours.push(beh);
|
27
28
|
}
|
28
29
|
|
30
|
+
/** Register audio clip for USDZ export. The clip will be embedded in the resulting file. */
|
31
|
+
addAudioClip(clipUrl: string) {
|
32
|
+
if (!clipUrl) return "";
|
33
|
+
if (typeof clipUrl !== "string") return "";
|
34
|
+
|
35
|
+
const clipName = AudioExtension.getName(clipUrl);
|
36
|
+
const filesKey = "audio/" + clipName;
|
37
|
+
|
38
|
+
this.audioClips.push({clipUrl, filesKey});
|
39
|
+
|
40
|
+
return filesKey;
|
41
|
+
}
|
42
|
+
|
29
43
|
behaviourComponents: Array<UsdzBehaviour> = [];
|
30
44
|
private behaviourComponentsCopy: Array<UsdzBehaviour> = [];
|
45
|
+
private audioClips: Array<{clipUrl: string, filesKey: string}> = [];
|
46
|
+
private audioClipsCopy: Array<{clipUrl: string, filesKey: string}> = [];
|
31
47
|
|
32
|
-
|
33
|
-
|
48
|
+
onBeforeBuildDocument(context: USDZExporterContext) {
|
49
|
+
if (!context.root) return Promise.resolve();
|
34
50
|
const beforeCreateDocumentPromises : Array<Promise<any>> = [];
|
35
51
|
context.root.traverse(e => {
|
36
52
|
GameObject.foreachComponent(e, (comp) => {
|
@@ -63,16 +79,18 @@
|
|
63
79
|
}
|
64
80
|
}
|
65
81
|
|
66
|
-
onAfterBuildDocument(context) {
|
82
|
+
onAfterBuildDocument(context: USDZExporterContext) {
|
67
83
|
for (const beh of this.behaviourComponents) {
|
68
84
|
if (typeof beh.afterCreateDocument === "function")
|
69
85
|
beh.afterCreateDocument(this, context);
|
70
86
|
}
|
71
87
|
this.behaviourComponentsCopy = this.behaviourComponents.slice();
|
72
88
|
this.behaviourComponents.length = 0;
|
89
|
+
this.audioClipsCopy = this.audioClips.slice();
|
90
|
+
this.audioClips.length = 0;
|
73
91
|
}
|
74
92
|
|
75
|
-
onAfterHierarchy(context, writer: USDWriter) {
|
93
|
+
onAfterHierarchy(context: USDZExporterContext, writer: USDWriter) {
|
76
94
|
if (this.behaviours?.length) {
|
77
95
|
writer.beginBlock('def Scope "Behaviors"');
|
78
96
|
|
@@ -83,7 +101,7 @@
|
|
83
101
|
}
|
84
102
|
}
|
85
103
|
|
86
|
-
async onAfterSerialize(context) {
|
104
|
+
async onAfterSerialize(context: USDZExporterContext) {
|
87
105
|
if (debug) console.log("onAfterSerialize behaviours", this.behaviourComponentsCopy)
|
88
106
|
|
89
107
|
for (const beh of this.behaviourComponentsCopy) {
|
@@ -100,8 +118,21 @@
|
|
100
118
|
}
|
101
119
|
}
|
102
120
|
|
121
|
+
for (const { clipUrl, filesKey } of this.audioClipsCopy) {
|
122
|
+
|
123
|
+
// if the clip was already added, don't add it again
|
124
|
+
if (context.files[filesKey]) return;
|
125
|
+
|
126
|
+
const audio = await fetch(clipUrl);
|
127
|
+
const audioBlob = await audio.blob();
|
128
|
+
const arrayBuffer = await audioBlob.arrayBuffer();
|
129
|
+
const audioData: Uint8Array = new Uint8Array(arrayBuffer)
|
130
|
+
context.files[filesKey] = audioData;
|
131
|
+
}
|
132
|
+
|
103
133
|
// cleanup
|
104
|
-
this.
|
134
|
+
this.behaviourComponentsCopy.length = 0;
|
135
|
+
this.audioClipsCopy.length = 0;
|
105
136
|
}
|
106
137
|
|
107
138
|
// combine behaviours that have tap triggers on the same object
|
@@ -524,7 +524,7 @@
|
|
524
524
|
// add InputTargetComponent for VisionOS direct/indirect interactions
|
525
525
|
const addInputTargetComponent = (model: USDObject) => {
|
526
526
|
const empty = USDObject.createEmpty();
|
527
|
-
empty.name = "
|
527
|
+
empty.name = "InputTarget_" + empty.name;
|
528
528
|
empty.displayName = undefined;
|
529
529
|
empty.type = "RealityKitComponent";
|
530
530
|
empty.onSerialize = (writer: USDWriter) => {
|
@@ -574,24 +574,26 @@
|
|
574
574
|
export class HideOnStart extends Behaviour implements UsdzBehaviour {
|
575
575
|
|
576
576
|
start() {
|
577
|
-
this.gameObject
|
577
|
+
GameObject.setActive(this.gameObject, false);
|
578
578
|
}
|
579
579
|
|
580
580
|
createBehaviours(ext, model, _context) {
|
581
|
-
if (model.uuid === this.gameObject.uuid)
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
581
|
+
if (model.uuid === this.gameObject.uuid) {
|
582
|
+
// we only want to mark the object as HideOnStart if it's still hidden
|
583
|
+
if (!this.wasVisible) {
|
584
|
+
ext.addBehavior(new BehaviorModel("HideOnStart_" + this.gameObject.name,
|
585
|
+
TriggerBuilder.sceneStartTrigger(),
|
586
|
+
ActionBuilder.fadeAction(model, 0, false)
|
587
|
+
));
|
588
|
+
}
|
589
|
+
}
|
586
590
|
}
|
587
591
|
|
592
|
+
private wasVisible: boolean = false;
|
593
|
+
|
588
594
|
beforeCreateDocument() {
|
589
|
-
this.
|
595
|
+
this.wasVisible = GameObject.isActiveSelf(this.gameObject);
|
590
596
|
}
|
591
|
-
|
592
|
-
afterCreateDocument() {
|
593
|
-
this.gameObject.visible = false;
|
594
|
-
}
|
595
597
|
}
|
596
598
|
|
597
599
|
export class EmphasizeOnClick extends Behaviour implements UsdzBehaviour {
|
@@ -633,6 +635,9 @@
|
|
633
635
|
@serializable()
|
634
636
|
toggleOnClick: boolean = false;
|
635
637
|
|
638
|
+
// Not exposed, but used for implicit playback of PlayOnAwake audio sources
|
639
|
+
trigger: "tap" | "start" = "tap";
|
640
|
+
|
636
641
|
start(): void {
|
637
642
|
ensureRaycaster(this.gameObject);
|
638
643
|
}
|
@@ -672,7 +677,7 @@
|
|
672
677
|
}
|
673
678
|
}
|
674
679
|
|
675
|
-
createBehaviours(ext, model, _context) {
|
680
|
+
createBehaviours(ext: BehaviorExtension, model: USDObject, _context: USDZExporterContext) {
|
676
681
|
if (!this.target && !this.clip) return;
|
677
682
|
if (model.uuid === this.gameObject.uuid) {
|
678
683
|
|
@@ -694,16 +699,20 @@
|
|
694
699
|
// unclear why, but having a useless tap action (nothing to tap on) "fixes" it.
|
695
700
|
anyChildHasGeometry = true;
|
696
701
|
|
697
|
-
|
702
|
+
const audioClip = ext.addAudioClip(clipUrl);
|
703
|
+
// const stopAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, audioClip, "stop", volume, auralMode);
|
704
|
+
let playAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, audioClip, "play", volume, auralMode);
|
698
705
|
if (this.target && this.target.loop)
|
699
706
|
playAction = ActionBuilder.sequence(playAction).makeLooping();
|
700
707
|
|
701
|
-
|
708
|
+
const behaviorName = (this.name ? "_" + this.name : "");
|
709
|
+
|
710
|
+
if (anyChildHasGeometry && this.trigger === "tap")
|
702
711
|
{
|
703
712
|
// does not seem to work in iOS / QuickLook...
|
704
713
|
// TODO use play "type" which can be start/stop/pause
|
705
714
|
if (this.toggleOnClick) (playAction as ActionModel).multiplePerformOperation = "stop";
|
706
|
-
const playClipOnTap = new BehaviorModel("playAudio
|
715
|
+
const playClipOnTap = new BehaviorModel("playAudio" + behaviorName,
|
707
716
|
TriggerBuilder.tapTrigger(this.gameObject),
|
708
717
|
playAction,
|
709
718
|
);
|
@@ -712,13 +721,13 @@
|
|
712
721
|
|
713
722
|
// automatically play audio on start too if the referenced AudioSource has playOnAwake enabled
|
714
723
|
if (this.target && this.target.playOnAwake && this.target.enabled) {
|
715
|
-
if (anyChildHasGeometry) {
|
724
|
+
if (anyChildHasGeometry && this.trigger === "tap") {
|
716
725
|
// HACK Currently (20240509) we MUST not emit this behaviour if we're also expecting the tap trigger to work.
|
717
|
-
// Seems to be a regression in QuickLook...
|
726
|
+
// Seems to be a regression in QuickLook... audio clips can't be stopped anymore as soon as they start playing.
|
718
727
|
console.warn("USDZExport: Audio sources that are played on tap can't also auto-play at scene start due to a QuickLook bug.");
|
719
728
|
}
|
720
729
|
else {
|
721
|
-
const playClipOnStart = new BehaviorModel("playAudioOnStart" +
|
730
|
+
const playClipOnStart = new BehaviorModel("playAudioOnStart" + behaviorName,
|
722
731
|
TriggerBuilder.sceneStartTrigger(),
|
723
732
|
playAction,
|
724
733
|
);
|
@@ -727,24 +736,6 @@
|
|
727
736
|
}
|
728
737
|
}
|
729
738
|
}
|
730
|
-
|
731
|
-
async afterSerialize(_ext, context) {
|
732
|
-
if (!this.target && !this.clip) return;
|
733
|
-
const clipUrl = this.clip ? this.clip : this.target ? this.target.clip : undefined;
|
734
|
-
if (!clipUrl) return;
|
735
|
-
if (typeof clipUrl !== "string") return;
|
736
|
-
|
737
|
-
const clipName = AudioExtension.getName(clipUrl);
|
738
|
-
const filesKey = "audio/" + clipName;
|
739
|
-
// if the clip was already added, don't add it again
|
740
|
-
if (context.files[filesKey]) return;
|
741
|
-
|
742
|
-
const audio = await fetch(clipUrl);
|
743
|
-
const audioBlob = await audio.blob();
|
744
|
-
const arrayBuffer = await audioBlob.arrayBuffer();
|
745
|
-
const audioData: Uint8Array = new Uint8Array(arrayBuffer)
|
746
|
-
context.files[filesKey] = audioData;
|
747
|
-
}
|
748
739
|
}
|
749
740
|
|
750
741
|
export class PlayAnimationOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour, UsdzAnimation {
|
@@ -785,7 +776,8 @@
|
|
785
776
|
private animationSequence? = new Array<RegisteredAnimationInfo>();
|
786
777
|
private animationLoopAfterSequence? = new Array<RegisteredAnimationInfo>();
|
787
778
|
|
788
|
-
createBehaviours(_ext, model, _context) {
|
779
|
+
createBehaviours(_ext: BehaviorExtension, model: USDObject, _context: USDZExporterContext) {
|
780
|
+
|
789
781
|
if (model.uuid === this.gameObject.uuid)
|
790
782
|
this.selfModel = model;
|
791
783
|
}
|
@@ -827,38 +819,17 @@
|
|
827
819
|
PlayAnimationOnClick.rootsWithExclusivePlayback.add(this.target);
|
828
820
|
}
|
829
821
|
|
830
|
-
const
|
831
|
-
let action = PlayAnimationOnClick.animationActions.find(a => a.affectedObjects == model && a.start == anim.start && a.duration == anim.duration);
|
832
|
-
if (!action) {
|
833
|
-
action = ActionBuilder.startAnimationAction(model, anim.start, anim.duration) as ActionModel;
|
834
|
-
PlayAnimationOnClick.animationActions.push(action);
|
835
|
-
}
|
836
|
-
return action;
|
837
|
-
}
|
822
|
+
const behaviorName = (this.name ? this.name : "");
|
838
823
|
|
839
824
|
document.traverse(model => {
|
840
825
|
if (model.uuid === this.target?.uuid) {
|
841
|
-
const sequence =
|
826
|
+
const sequence = PlayAnimationOnClick.getActionForSequences(
|
827
|
+
model,
|
828
|
+
this.animationSequence,
|
829
|
+
this.animationLoopAfterSequence
|
830
|
+
);
|
842
831
|
|
843
|
-
|
844
|
-
{
|
845
|
-
for (const anim of this.animationSequence) {
|
846
|
-
sequence.addAction(getOrCacheAction(model, anim));
|
847
|
-
}
|
848
|
-
}
|
849
|
-
|
850
|
-
if (this.animationLoopAfterSequence && this.animationLoopAfterSequence.length > 0) {
|
851
|
-
// only make a new action group if there's already stuff in the existing one
|
852
|
-
const loopSequence = sequence.actions.length == 0 ? sequence : ActionBuilder.sequence();
|
853
|
-
for (const anim of this.animationLoopAfterSequence) {
|
854
|
-
loopSequence.addAction(getOrCacheAction(model, anim));
|
855
|
-
}
|
856
|
-
loopSequence.makeLooping();
|
857
|
-
if (sequence !== loopSequence)
|
858
|
-
sequence.addAction(loopSequence);
|
859
|
-
}
|
860
|
-
|
861
|
-
const playAnimationOnTap = new BehaviorModel("_tap_" + this.name + "_toPlayClip_" + this.stateName + "_on_" + this.target?.name,
|
832
|
+
const playAnimationOnTap = new BehaviorModel(this.trigger + "_" + behaviorName + "_toPlayAnimation_" + this.stateName + "_on_" + this.target?.name,
|
862
833
|
this.trigger == "tap" ? TriggerBuilder.tapTrigger(this.selfModel) : TriggerBuilder.sceneStartTrigger(),
|
863
834
|
sequence
|
864
835
|
);
|
@@ -871,19 +842,66 @@
|
|
871
842
|
});
|
872
843
|
}
|
873
844
|
|
874
|
-
|
875
|
-
if (!this.target || (!this.animator && !this.animation)) return;
|
845
|
+
static getActionForSequences(model: USDObject, animationSequence?: Array<RegisteredAnimationInfo>, animationLoopAfterSequence?: Array<RegisteredAnimationInfo>) {
|
876
846
|
|
877
|
-
|
847
|
+
const getOrCacheAction = (model: USDObject, anim: RegisteredAnimationInfo) => {
|
848
|
+
let action = PlayAnimationOnClick.animationActions.find(a => a.affectedObjects == model && a.start == anim.start && a.duration == anim.duration && a.animationSpeed == anim.speed);
|
849
|
+
if (!action) {
|
850
|
+
action = ActionBuilder.startAnimationAction(model, anim.start, anim.duration, anim.speed) as ActionModel;
|
851
|
+
PlayAnimationOnClick.animationActions.push(action);
|
852
|
+
}
|
853
|
+
return action;
|
854
|
+
}
|
878
855
|
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
856
|
+
const sequence = ActionBuilder.sequence();
|
857
|
+
|
858
|
+
if (animationSequence && animationSequence.length > 0)
|
859
|
+
{
|
860
|
+
for (const anim of animationSequence) {
|
861
|
+
sequence.addAction(getOrCacheAction(model, anim));
|
862
|
+
}
|
885
863
|
}
|
886
864
|
|
865
|
+
if (animationLoopAfterSequence && animationLoopAfterSequence.length > 0) {
|
866
|
+
// only make a new action group if there's already stuff in the existing one
|
867
|
+
const loopSequence = sequence.actions.length == 0 ? sequence : ActionBuilder.sequence();
|
868
|
+
for (const anim of animationLoopAfterSequence) {
|
869
|
+
loopSequence.addAction(getOrCacheAction(model, anim));
|
870
|
+
}
|
871
|
+
loopSequence.makeLooping();
|
872
|
+
if (sequence !== loopSequence)
|
873
|
+
sequence.addAction(loopSequence);
|
874
|
+
}
|
875
|
+
|
876
|
+
return sequence;
|
877
|
+
}
|
878
|
+
|
879
|
+
static getAndRegisterAnimationSequences(ext: AnimationExtension, target: GameObject, stateName?: string):
|
880
|
+
{ animationSequence: Array<RegisteredAnimationInfo>, animationLoopAfterSequence: Array<RegisteredAnimationInfo> } | undefined {
|
881
|
+
|
882
|
+
if (!target) return undefined;
|
883
|
+
|
884
|
+
const animator = target.getComponent(Animator);
|
885
|
+
const animation = target.getComponent(Animation);
|
886
|
+
|
887
|
+
if (!animator && !animation) return undefined;
|
888
|
+
|
889
|
+
if (animator && !stateName) {
|
890
|
+
throw new Error("PlayAnimationOnClick: No stateName specified for animator " + animator.name + " on " + target.name);
|
891
|
+
}
|
892
|
+
|
893
|
+
let animationSequence: Array<RegisteredAnimationInfo> = [];
|
894
|
+
let animationLoopAfterSequence: Array<RegisteredAnimationInfo> = [];
|
895
|
+
|
896
|
+
if (animation) {
|
897
|
+
const anim = ext.registerAnimation(target, animation.clip);
|
898
|
+
if (anim) animationLoopAfterSequence.push(anim);
|
899
|
+
return {
|
900
|
+
animationSequence,
|
901
|
+
animationLoopAfterSequence
|
902
|
+
}
|
903
|
+
}
|
904
|
+
|
887
905
|
// If there's a separate state specified to play after this one, we
|
888
906
|
// play it automatically. Theoretically an animator state machine flow could be encoded here.
|
889
907
|
|
@@ -893,8 +911,8 @@
|
|
893
911
|
// - (0 > 1 > 1) should keep looping (1).
|
894
912
|
// - (0 > 1 > 2 > 3 > 2) should keep looping (2,3).
|
895
913
|
// - (0 > 1 > 2 > 3 > 0) should keep looping (0,1,2,3).
|
896
|
-
const runtimeController =
|
897
|
-
let currentState = runtimeController?.findState(
|
914
|
+
const runtimeController = animator?.runtimeAnimatorController;
|
915
|
+
let currentState = runtimeController?.findState(stateName);
|
898
916
|
let statesUntilLoop: State[] = [];
|
899
917
|
let statesLooping: State[] = [];
|
900
918
|
|
@@ -940,22 +958,22 @@
|
|
940
958
|
const firstStateInLoop = visitedStates.indexOf(currentState);
|
941
959
|
statesUntilLoop = visitedStates.slice(0, firstStateInLoop); // can be empty, which means we're looping all
|
942
960
|
statesLooping = visitedStates.slice(firstStateInLoop); // can be empty, which means nothing is looping
|
943
|
-
if (debug) console.log("found loop from " +
|
961
|
+
if (debug) console.log("found loop from " + stateName, "states until loop", statesUntilLoop, "states looping", statesLooping);
|
944
962
|
}
|
945
963
|
else {
|
946
964
|
statesUntilLoop = visitedStates;
|
947
965
|
statesLooping = [];
|
948
|
-
if (debug) console.log("found no loop from " +
|
966
|
+
if (debug) console.log("found no loop from " + stateName, "states", statesUntilLoop);
|
949
967
|
}
|
950
968
|
}
|
951
969
|
|
952
970
|
// Special case: someone's trying to play an empty clip without any motion data, no loops or anything.
|
953
971
|
// In that case, we simply go to the rest clip – this is a common case for "idle" states.
|
954
972
|
if (statesUntilLoop.length === 1 && (!statesUntilLoop[0].motion?.clip || statesUntilLoop[0].motion?.clip.tracks?.length === 0)) {
|
955
|
-
|
956
|
-
const anim = ext.registerAnimation(
|
957
|
-
if (anim)
|
958
|
-
return;
|
973
|
+
animationSequence = new Array<RegisteredAnimationInfo>();
|
974
|
+
const anim = ext.registerAnimation(target, null);
|
975
|
+
if (anim) animationSequence.push(anim);
|
976
|
+
return undefined;
|
959
977
|
}
|
960
978
|
|
961
979
|
// filter out any states that don't have motion data
|
@@ -964,32 +982,53 @@
|
|
964
982
|
|
965
983
|
// If none of the found states have motion, we need to warn
|
966
984
|
if (statesUntilLoop.length === 0 && statesLooping.length === 0) {
|
967
|
-
console.warn("No clips found for state " +
|
968
|
-
return;
|
985
|
+
console.warn("No clips found for state " + stateName + " on " + animator?.name + ", can't export animation data");
|
986
|
+
return undefined;
|
969
987
|
}
|
970
988
|
|
971
989
|
const addStateToSequence = (state: State, sequence: Array<RegisteredAnimationInfo>) => {
|
972
|
-
if (!
|
973
|
-
const anim = ext.registerAnimation(
|
974
|
-
if (anim)
|
975
|
-
|
990
|
+
if (!target) return;
|
991
|
+
const anim = ext.registerAnimation(target, state.motion.clip ?? null);
|
992
|
+
if (anim) {
|
993
|
+
anim.speed = state.speed;
|
994
|
+
sequence.push(anim);
|
995
|
+
}
|
996
|
+
else console.warn("Couldn't register animation for state " + state.name + " on " + animator?.name);
|
976
997
|
};
|
977
998
|
|
978
999
|
// Register all the animation states we found.
|
979
1000
|
if (statesUntilLoop.length > 0) {
|
980
|
-
|
1001
|
+
animationSequence = new Array<RegisteredAnimationInfo>();
|
981
1002
|
for (const state of statesUntilLoop) {
|
982
|
-
addStateToSequence(state,
|
1003
|
+
addStateToSequence(state, animationSequence);
|
983
1004
|
}
|
984
1005
|
}
|
985
1006
|
if (statesLooping.length > 0) {
|
986
|
-
|
1007
|
+
animationLoopAfterSequence = new Array<RegisteredAnimationInfo>();
|
987
1008
|
for (const state of statesLooping) {
|
988
|
-
addStateToSequence(state,
|
1009
|
+
addStateToSequence(state, animationLoopAfterSequence);
|
989
1010
|
}
|
990
1011
|
}
|
1012
|
+
|
1013
|
+
return {
|
1014
|
+
animationSequence,
|
1015
|
+
animationLoopAfterSequence
|
1016
|
+
}
|
991
1017
|
}
|
992
1018
|
|
1019
|
+
createAnimation(ext: AnimationExtension, model: USDObject, _context: USDZExporterContext) {
|
1020
|
+
|
1021
|
+
if (!this.target || (!this.animator && !this.animation)) return;
|
1022
|
+
|
1023
|
+
const result = PlayAnimationOnClick.getAndRegisterAnimationSequences(ext, this.target, this.stateName);
|
1024
|
+
if (!result) return;
|
1025
|
+
|
1026
|
+
this.animationSequence = result.animationSequence;
|
1027
|
+
this.animationLoopAfterSequence = result.animationLoopAfterSequence;
|
1028
|
+
|
1029
|
+
this.stateAnimationModel = model;
|
1030
|
+
}
|
1031
|
+
|
993
1032
|
}
|
994
1033
|
|
995
1034
|
export class PreliminaryAction extends Behaviour {
|
@@ -75,6 +75,10 @@
|
|
75
75
|
let str = "[ ";
|
76
76
|
for (let i = 0; i < targetObject.length; i++) {
|
77
77
|
let obj = targetObject[i];
|
78
|
+
if (!obj) {
|
79
|
+
console.warn("Invalid target object in behavior", targetObject + ". Is the object exported?");
|
80
|
+
continue;
|
81
|
+
}
|
78
82
|
if (typeof obj === "string")
|
79
83
|
str += obj;
|
80
84
|
else if (typeof obj === "object") {
|
@@ -82,6 +86,10 @@
|
|
82
86
|
if (obj.isObject3D) {
|
83
87
|
//@ts-ignore
|
84
88
|
obj = document.findById(obj.uuid);
|
89
|
+
if (!obj) {
|
90
|
+
console.warn("Invalid target object in behavior", targetObject + ". Is the object exported?");
|
91
|
+
continue;
|
92
|
+
}
|
85
93
|
}
|
86
94
|
const res = (obj as any).getPath?.call(obj) as string;
|
87
95
|
str += res;
|
@@ -313,8 +321,14 @@
|
|
313
321
|
if (typeof this.affectedObjects !== "string") this.affectedObjects = resolve(this.affectedObjects, document);
|
314
322
|
writer.appendLine('rel affectedObjects = ' + this.affectedObjects);
|
315
323
|
}
|
316
|
-
if (typeof this.duration === "number")
|
317
|
-
|
324
|
+
if (typeof this.duration === "number") {
|
325
|
+
if (typeof this.animationSpeed === "number" && this.animationSpeed !== 1) {
|
326
|
+
writer.appendLine(`double duration = ${this.duration / this.animationSpeed} `);
|
327
|
+
}
|
328
|
+
else {
|
329
|
+
writer.appendLine(`double duration = ${this.duration} `);
|
330
|
+
}
|
331
|
+
}
|
318
332
|
if (this.easeType)
|
319
333
|
writer.appendLine(`token easeType = "${this.easeType}"`);
|
320
334
|
if (this.tokenId)
|
@@ -337,7 +351,7 @@
|
|
337
351
|
writer.appendLine(`double start = ${this.start} `);
|
338
352
|
}
|
339
353
|
if (typeof this.animationSpeed === "number") {
|
340
|
-
writer.appendLine(`double animationSpeed = ${this.animationSpeed} `);
|
354
|
+
writer.appendLine(`double animationSpeed = ${this.animationSpeed.toFixed(2)} `);
|
341
355
|
}
|
342
356
|
if (typeof this.reversed === "boolean") {
|
343
357
|
writer.appendLine(`bool reversed = ${this.reversed}`)
|
@@ -469,10 +483,10 @@
|
|
469
483
|
return act;
|
470
484
|
}
|
471
485
|
|
472
|
-
static lookAtCameraAction(targets: Target, duration
|
486
|
+
static lookAtCameraAction(targets: Target, duration?: number, front?: Vec3, up?: Vec3): ActionModel {
|
473
487
|
const act = new ActionModel(targets);
|
474
488
|
act.tokenId = "LookAtCamera";
|
475
|
-
act.duration = duration;
|
489
|
+
act.duration = duration === undefined ? 9999999999999 : duration;
|
476
490
|
act.front = front ?? Vec3.forward;
|
477
491
|
// 0,0,0 is a special case for "free look"
|
478
492
|
// 0,1,0 is for "y-locked look-at"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { CustomBlending, DoubleSide, FrontSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, Object3D, OrthographicCamera, PlaneGeometry, RenderItem, ShaderMaterial, Vector3, WebGLRenderTarget } from "three";
|
1
|
+
import { BackSide, CustomBlending, DoubleSide, FrontSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, Object3D, OrthographicCamera, PlaneGeometry, RenderItem, ShaderMaterial, Vector3, WebGLRenderTarget } from "three";
|
2
2
|
import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js';
|
3
3
|
import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js';
|
4
4
|
|
@@ -10,7 +10,7 @@
|
|
10
10
|
import { getBoundingBox } from "../engine/engine_three_utils.js";
|
11
11
|
import { getParam } from "../engine/engine_utils.js"
|
12
12
|
import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
|
13
|
-
import { Behaviour } from "./Component.js";
|
13
|
+
import { Behaviour, GameObject } from "./Component.js";
|
14
14
|
|
15
15
|
const debug = getParam("debugcontactshadows");
|
16
16
|
|
@@ -26,7 +26,7 @@
|
|
26
26
|
*/
|
27
27
|
export class ContactShadows extends Behaviour {
|
28
28
|
|
29
|
-
private static
|
29
|
+
private static _instances: Map<Context, ContactShadows> = new Map();
|
30
30
|
/**
|
31
31
|
* Create contact shadows for the scene. Automatically fits the shadows to the scene.
|
32
32
|
* The instance of contact shadows will be created only once.
|
@@ -34,17 +34,22 @@
|
|
34
34
|
* @returns The instance of the contact shadows.
|
35
35
|
*/
|
36
36
|
static auto(context?: Context): ContactShadows {
|
37
|
-
if (!
|
37
|
+
if (!context) context = Context.Current;
|
38
|
+
if (!context) {
|
39
|
+
throw new Error("No context provided and no current context set.");
|
40
|
+
}
|
41
|
+
let instance = this._instances.get(context);
|
42
|
+
if (!instance || instance.destroyed) {
|
38
43
|
const obj = new Object3D();
|
39
|
-
|
44
|
+
instance = addComponent(obj, ContactShadows, {
|
40
45
|
autoFit: false,
|
41
46
|
occludeBelowGround: false
|
42
47
|
});
|
48
|
+
this._instances.set(context, instance);
|
43
49
|
}
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
return this._instance;
|
50
|
+
context.scene.add(instance.gameObject);
|
51
|
+
instance.fitShadows();
|
52
|
+
return instance;
|
48
53
|
}
|
49
54
|
|
50
55
|
/**
|
@@ -59,7 +64,7 @@
|
|
59
64
|
@serializable()
|
60
65
|
blur: number = 4.0;
|
61
66
|
@serializable()
|
62
|
-
occludeBelowGround: boolean =
|
67
|
+
occludeBelowGround: boolean = false;
|
63
68
|
@serializable()
|
64
69
|
backfaceShadows: boolean = true;
|
65
70
|
|
@@ -77,6 +82,8 @@
|
|
77
82
|
private horizontalBlurMaterial?: ShaderMaterial;
|
78
83
|
private verticalBlurMaterial?: ShaderMaterial;
|
79
84
|
|
85
|
+
private textureSize = 512;
|
86
|
+
|
80
87
|
/**
|
81
88
|
* Call to fit the shadows to the scene.
|
82
89
|
*/
|
@@ -85,15 +92,23 @@
|
|
85
92
|
setAutoFitEnabled(this.gameObject, false);
|
86
93
|
const box = getBoundingBox(this.context.scene.children, [this.gameObject]);
|
87
94
|
// expand box in all directions (except below ground)
|
88
|
-
// 0.
|
89
|
-
|
90
|
-
|
95
|
+
// 0.75 expands by 75% in each direction
|
96
|
+
// The "32" is pretty much heuristically determined – adjusting the value until we don't get a visible border anymore.
|
97
|
+
const expandFactor = Math.max(0.5, this.blur / 32);
|
98
|
+
const sizeX = box.max.x - box.min.x;
|
99
|
+
const sizeZ = box.max.z - box.min.z;
|
100
|
+
box.expandByVector(new Vector3(expandFactor * sizeX, 0, expandFactor * sizeZ));
|
101
|
+
if (debug) Gizmos.DrawWireBox3(box, 0xffff00, 60);
|
102
|
+
if (this.gameObject.parent) {
|
103
|
+
// transform box from world space into parent space
|
104
|
+
box.applyMatrix4((this.gameObject.parent as GameObject).matrixWorld.clone().invert());
|
105
|
+
}
|
91
106
|
const min = box.min;
|
92
107
|
const offset = Math.max(0.00001, (box.max.y - min.y) * .002);
|
93
108
|
box.max.y += offset;
|
94
|
-
if (debug) Gizmos.DrawWireBox3(box, 0xffff00, 60);
|
95
109
|
this.gameObject.position.set((min.x + box.max.x) / 2, min.y - offset, (min.z + box.max.z) / 2);
|
96
110
|
this.gameObject.scale.set(box.max.x - min.x, box.max.y - min.y, box.max.z - min.z);
|
111
|
+
this.gameObject.matrixWorldNeedsUpdate = true;
|
97
112
|
}
|
98
113
|
|
99
114
|
awake() {
|
@@ -104,17 +119,16 @@
|
|
104
119
|
/** @internal */
|
105
120
|
start(): void {
|
106
121
|
if (debug) console.log("Create ContactShadows on " + this.gameObject.name, this)
|
107
|
-
const textureSize = 512;
|
108
122
|
|
109
123
|
this.shadowGroup = new Group();
|
110
124
|
this.gameObject.add(this.shadowGroup);
|
111
125
|
|
112
126
|
// the render target that will show the shadows in the plane texture
|
113
|
-
this.renderTarget = new WebGLRenderTarget(textureSize, textureSize);
|
127
|
+
this.renderTarget = new WebGLRenderTarget(this.textureSize, this.textureSize);
|
114
128
|
this.renderTarget.texture.generateMipmaps = false;
|
115
129
|
|
116
130
|
// the render target that we will use to blur the first render target
|
117
|
-
this.renderTargetBlur = new WebGLRenderTarget(textureSize, textureSize);
|
131
|
+
this.renderTargetBlur = new WebGLRenderTarget(this.textureSize, this.textureSize);
|
118
132
|
this.renderTargetBlur.texture.generateMipmaps = false;
|
119
133
|
|
120
134
|
// make a plane and make it face up
|
@@ -144,6 +158,7 @@
|
|
144
158
|
color: 0x000000,
|
145
159
|
transparent: true,
|
146
160
|
depthWrite: false,
|
161
|
+
side: FrontSide,
|
147
162
|
});
|
148
163
|
this.plane = new Mesh(planeGeometry, planeMaterial);
|
149
164
|
this.plane.scale.y = - 1;
|
@@ -155,6 +170,7 @@
|
|
155
170
|
depthWrite: true,
|
156
171
|
stencilWrite: true,
|
157
172
|
colorWrite: false,
|
173
|
+
side: BackSide,
|
158
174
|
}))
|
159
175
|
// .rotateX(Math.PI)
|
160
176
|
.translateY(-0.0001);
|
@@ -291,14 +307,11 @@
|
|
291
307
|
// and reset the override material
|
292
308
|
scene.overrideMaterial = null;
|
293
309
|
|
294
|
-
const
|
295
|
-
|
296
|
-
// take the size of the scene into account - for larger scenes we want less blur to keep it somewhat consistent
|
297
|
-
const blurAmount = Math.min(this.blur, this.blur / size);
|
310
|
+
const blurAmount = Math.max(this.blur, 0.05);
|
298
311
|
|
299
|
-
//
|
300
|
-
|
301
|
-
this.blurShadow(blurAmount * 0.
|
312
|
+
// two-pass blur to reduce the artifacts
|
313
|
+
this.blurShadow(blurAmount * 2);
|
314
|
+
this.blurShadow(blurAmount * 0.5);
|
302
315
|
|
303
316
|
this.shadowGroup.visible = false;
|
304
317
|
if (this.occluderMesh) this.occluderMesh.visible = this.occludeBelowGround;
|
@@ -312,7 +325,7 @@
|
|
312
325
|
}
|
313
326
|
|
314
327
|
// renderTarget --> blurPlane (horizontalBlur) --> renderTargetBlur --> blurPlane (verticalBlur) --> renderTarget
|
315
|
-
private blurShadow(amount) {
|
328
|
+
private blurShadow(amount: number) {
|
316
329
|
if (!this.blurPlane || !this.shadowCamera ||
|
317
330
|
!this.renderTarget || !this.renderTargetBlur ||
|
318
331
|
!this.horizontalBlurMaterial || !this.verticalBlurMaterial)
|
@@ -320,10 +333,17 @@
|
|
320
333
|
|
321
334
|
this.blurPlane.visible = true;
|
322
335
|
|
336
|
+
// Correct for contact shadow plane aspect ratio.
|
337
|
+
// since we have a separable blur, we can just adjust the blur amount for X and Z individually
|
338
|
+
const ws = this.gameObject.worldScale;
|
339
|
+
const avg = (ws.x + ws.z) / 2;
|
340
|
+
const aspectX = ws.z / avg;
|
341
|
+
const aspectZ = ws.x / avg;
|
342
|
+
|
323
343
|
// blur horizontally and draw in the renderTargetBlur
|
324
344
|
this.blurPlane.material = this.horizontalBlurMaterial;
|
325
345
|
(this.blurPlane.material as ShaderMaterial).uniforms.tDiffuse.value = this.renderTarget.texture;
|
326
|
-
this.horizontalBlurMaterial.uniforms.h.value = amount * 1 /
|
346
|
+
this.horizontalBlurMaterial.uniforms.h.value = amount * 1 / this.textureSize * aspectX;
|
327
347
|
|
328
348
|
const renderer = this.context.renderer;
|
329
349
|
|
@@ -334,7 +354,7 @@
|
|
334
354
|
// blur vertically and draw in the main renderTarget
|
335
355
|
this.blurPlane.material = this.verticalBlurMaterial;
|
336
356
|
(this.blurPlane.material as ShaderMaterial).uniforms.tDiffuse.value = this.renderTargetBlur.texture;
|
337
|
-
this.verticalBlurMaterial.uniforms.v.value = amount * 1 /
|
357
|
+
this.verticalBlurMaterial.uniforms.v.value = amount * 1 / this.textureSize * aspectZ;
|
338
358
|
|
339
359
|
renderer.setRenderTarget(this.renderTarget);
|
340
360
|
renderer.render(this.blurPlane, this.shadowCamera);
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { isLocalNetwork } from "../engine_networking_utils.js";
|
2
2
|
import { getParam } from "../engine_utils.js";
|
3
3
|
import { showDebugConsole } from "./debug_console.js";
|
4
|
-
import { addLog, clearMessages, LogType,
|
4
|
+
import { addLog, clearMessages, LogType, setAllowBalloonMessages, setAllowOverlayMessages } from "./debug_overlay.js";
|
5
5
|
|
6
6
|
export { showDebugConsole }
|
7
7
|
export {
|
@@ -9,8 +9,8 @@
|
|
9
9
|
/** @deprecated use clearBalloonMessages instead */
|
10
10
|
clearMessages as clearOverlayMessages,
|
11
11
|
LogType,
|
12
|
+
setAllowBalloonMessages,
|
12
13
|
setAllowOverlayMessages,
|
13
|
-
setAllowBalloonMessages,
|
14
14
|
};
|
15
15
|
export { enableSpatialConsole } from "./debug_spatial_console.js";
|
16
16
|
|
@@ -59,8 +59,8 @@
|
|
59
59
|
componentInstance.guid = idProvider.generateUUID();
|
60
60
|
}
|
61
61
|
apply(obj);
|
62
|
-
//
|
63
|
-
registerComponent(componentInstance);
|
62
|
+
// register the component - make sure to provide the component instance context (if assigned)
|
63
|
+
registerComponent(componentInstance, componentInstance.context);
|
64
64
|
try {
|
65
65
|
if (callAwake && componentInstance.__internalAwake) {
|
66
66
|
updateActiveInHierarchyWithoutEventCall(obj);
|
@@ -108,7 +108,8 @@
|
|
108
108
|
componentInstance.guid = idProvider.generateUUID();
|
109
109
|
}
|
110
110
|
if(init) componentInstance._internalInit(init);
|
111
|
-
|
111
|
+
// Register the component - make sure to provide the component instance context (if assigned)
|
112
|
+
registerComponent(componentInstance, componentInstance.context);
|
112
113
|
return componentInstance;
|
113
114
|
}
|
114
115
|
|
@@ -109,7 +109,11 @@
|
|
109
109
|
console.error("Registered script is not a Needle Engine component. \nThe script will be ignored. Please make sure your component extends \"Behaviour\" imported from \"@needle-tools/engine\"\n", script);
|
110
110
|
return;
|
111
111
|
}
|
112
|
-
|
112
|
+
if (!context) {
|
113
|
+
context = Context.Current;
|
114
|
+
if (debug) console.warn("> Registering component without context");
|
115
|
+
}
|
116
|
+
const new_scripts = context?.new_scripts;
|
113
117
|
if (!new_scripts.includes(script)) {
|
114
118
|
new_scripts.push(script);
|
115
119
|
}
|
@@ -241,14 +241,16 @@
|
|
241
241
|
const logoSize = 120;
|
242
242
|
logo.style.width = `${logoSize}px`;
|
243
243
|
logo.style.height = `${logoSize}px`;
|
244
|
-
logo.style.
|
245
|
-
logo.style.
|
246
|
-
logo.style.
|
244
|
+
logo.style.paddingTop = "20px";
|
245
|
+
logo.style.paddingBottom = "10px";
|
246
|
+
logo.style.margin = "0px";
|
247
247
|
logo.style.userSelect = "none";
|
248
248
|
logo.style.objectFit = "contain";
|
249
|
-
logo.style.transition = "transform
|
250
|
-
logo.style.transform = "translateY(
|
249
|
+
logo.style.transition = "transform 1.5s ease-out, opacity .3s ease-in-out";
|
250
|
+
logo.style.transform = "translateY(30px)";
|
251
|
+
logo.style.opacity = "0.05";
|
251
252
|
setTimeout(() => {
|
253
|
+
logo.style.opacity = "1";
|
252
254
|
logo.style.transform = "translateY(0px)";
|
253
255
|
}, 1);
|
254
256
|
logo.src = needleLogoOnlySVG;
|
@@ -334,7 +336,7 @@
|
|
334
336
|
this._messageContainer = messageContainer;
|
335
337
|
messageContainer.style.display = "flex";
|
336
338
|
messageContainer.style.fontSize = ".8rem";
|
337
|
-
messageContainer.style.paddingTop = ".
|
339
|
+
messageContainer.style.paddingTop = ".1rem";
|
338
340
|
// messageContainer.style.border = "1px solid rgba(255,255,255,.1)";
|
339
341
|
messageContainer.style.justifyContent = "center";
|
340
342
|
details.appendChild(messageContainer);
|
@@ -220,6 +220,9 @@
|
|
220
220
|
// assign basic fields
|
221
221
|
assign(instance, compData, context.implementationInformation);
|
222
222
|
|
223
|
+
// make sure we assign the Needle Engine Context because Context.Current is unreliable when loading multiple <needle-engine> elements at the same time due to async processes
|
224
|
+
instance.context = context.context;
|
225
|
+
|
223
226
|
// assign the guid of the original instance
|
224
227
|
if ("guid" in compData)
|
225
228
|
instance[editorGuidKeyName] = compData.guid;
|
@@ -1,11 +1,12 @@
|
|
1
1
|
import { type Context, FrameEvent } from "./engine_context.js";
|
2
|
-
import
|
2
|
+
import { ContextEvent } from "./engine_context_registry.js";
|
3
3
|
import { safeInvoke } from "./engine_generic_utils.js";
|
4
4
|
|
5
5
|
export declare type LifecycleMethod = (ctx: Context) => void;
|
6
6
|
export declare type Event = ContextEvent | FrameEvent;
|
7
7
|
|
8
8
|
const allMethods = new Map<Event, Array<LifecycleMethod>>();
|
9
|
+
const _started = new WeakSet<Context>();
|
9
10
|
|
10
11
|
/** register a function to be called during the Needle Engine frame event at a specific point
|
11
12
|
* @param cb the function to call
|
@@ -35,10 +36,14 @@
|
|
35
36
|
const methods = allMethods.get(evt);
|
36
37
|
if (methods) {
|
37
38
|
if (methods.length > 0) {
|
38
|
-
|
39
|
+
const array = methods;
|
39
40
|
if (evt === FrameEvent.Start) {
|
40
|
-
|
41
|
-
|
41
|
+
if (_started.has(ctx)) {
|
42
|
+
return;
|
43
|
+
}
|
44
|
+
else {
|
45
|
+
_started.add(ctx);
|
46
|
+
}
|
42
47
|
}
|
43
48
|
invoke(ctx, array);
|
44
49
|
}
|
@@ -21,7 +21,7 @@
|
|
21
21
|
Vec2,
|
22
22
|
Vec3,
|
23
23
|
} from './engine_types.js';
|
24
|
-
import { Collision,ContactPoint } from './engine_types.js';
|
24
|
+
import { Collision, ContactPoint } from './engine_types.js';
|
25
25
|
import { SphereOverlapResult } from './engine_types.js';
|
26
26
|
import { CircularBuffer, getParam } from "./engine_utils.js"
|
27
27
|
|
@@ -166,7 +166,7 @@
|
|
166
166
|
addForce(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
|
167
167
|
this.validate();
|
168
168
|
const body = this.internal_getRigidbody(rigidbody);
|
169
|
-
if(body) body.addForce(force, wakeup)
|
169
|
+
if (body) body.addForce(force, wakeup)
|
170
170
|
else console.warn("Rigidbody doesn't exist: can not apply force");
|
171
171
|
}
|
172
172
|
addImpulse(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
|
@@ -206,14 +206,14 @@
|
|
206
206
|
applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
|
207
207
|
this.validate();
|
208
208
|
const body = this.internal_getRigidbody(rb);
|
209
|
-
if(body) body.applyImpulse(vec, wakeup);
|
209
|
+
if (body) body.applyImpulse(vec, wakeup);
|
210
210
|
else console.warn("Rigidbody doesn't exist: can not apply impulse");
|
211
211
|
}
|
212
212
|
|
213
213
|
wakeup(rb: IRigidbody) {
|
214
214
|
this.validate();
|
215
215
|
const body = this.internal_getRigidbody(rb);
|
216
|
-
if(body) body.wakeUp();
|
216
|
+
if (body) body.wakeUp();
|
217
217
|
else console.warn("Rigidbody doesn't exist: can not wake up");
|
218
218
|
}
|
219
219
|
isSleeping(rb: IRigidbody) {
|
@@ -224,13 +224,13 @@
|
|
224
224
|
setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
|
225
225
|
this.validate();
|
226
226
|
const body = this.internal_getRigidbody(rb);
|
227
|
-
if(body) body.setAngvel(vec, wakeup);
|
227
|
+
if (body) body.setAngvel(vec, wakeup);
|
228
228
|
else console.warn("Rigidbody doesn't exist: can not set angular velocity");
|
229
229
|
}
|
230
230
|
setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
|
231
231
|
this.validate();
|
232
232
|
const body = this.internal_getRigidbody(rb);
|
233
|
-
if(body) body.setLinvel(vec, wakeup);
|
233
|
+
if (body) body.setLinvel(vec, wakeup);
|
234
234
|
else console.warn("Rigidbody doesn't exist: can not set linear velocity");
|
235
235
|
}
|
236
236
|
|
@@ -651,8 +651,7 @@
|
|
651
651
|
positions = this._meshCache.get(key)!;
|
652
652
|
}
|
653
653
|
else {
|
654
|
-
console.warn(`Your MeshCollider \"${collider.name}\" is scaled (${scale.x}, ${scale.y}, ${scale.z})
|
655
|
-
// showBalloonWarning("Your model is using scaled mesh colliders which is not optimal for performance: " + mesh.name + ", consider using unscaled objects");
|
654
|
+
if (debugPhysics || isDevEnvironment()) console.warn(`Your MeshCollider \"${collider.name}\" is scaled: consider applying the scale to the collider mesh instead (${scale.x}, ${scale.y}, ${scale.z})`);
|
656
655
|
const scaledPositions = new Float32Array(positions.length);
|
657
656
|
for (let i = 0; i < positions.length; i += 3) {
|
658
657
|
scaledPositions[i] = positions[i] * scale.x;
|
@@ -1213,8 +1212,7 @@
|
|
1213
1212
|
this._tempCenterPos.z = center.z;
|
1214
1213
|
getWorldScale(collider.gameObject, this._tempCenterVec);
|
1215
1214
|
this._tempCenterPos.multiply(this._tempCenterVec);
|
1216
|
-
if (!collider.attachedRigidbody)
|
1217
|
-
{
|
1215
|
+
if (!collider.attachedRigidbody) {
|
1218
1216
|
getWorldQuaternion(collider.gameObject, this._tempCenterQuaternion);
|
1219
1217
|
this._tempCenterPos.applyQuaternion(this._tempCenterQuaternion);
|
1220
1218
|
}
|
@@ -75,7 +75,8 @@
|
|
75
75
|
|
76
76
|
if (!opts) opts = {}
|
77
77
|
|
78
|
-
let { context, width, height,
|
78
|
+
let { context, width, height, camera } = opts;
|
79
|
+
const { mimeType } = opts;
|
79
80
|
|
80
81
|
if (!context) {
|
81
82
|
context = ContextRegistry.Current as Context;
|
@@ -126,7 +127,6 @@
|
|
126
127
|
// render now
|
127
128
|
context.renderNow();
|
128
129
|
|
129
|
-
console.log("Screenshot taken", { width, height, mimeType, camera });
|
130
130
|
const dataUrl = canvas.toDataURL(mimeType);
|
131
131
|
return dataUrl;
|
132
132
|
}
|
@@ -135,14 +135,14 @@
|
|
135
135
|
super.onEnable();
|
136
136
|
this.addShadowComponent(this.rectBlock);
|
137
137
|
this._transformNeedsUpdate = true;
|
138
|
-
this.
|
138
|
+
this.canvas?.registerTransform(this);
|
139
139
|
// this.onApplyTransform("enable");
|
140
140
|
}
|
141
141
|
|
142
142
|
onDisable() {
|
143
143
|
super.onDisable();
|
144
144
|
this.removeShadowComponent();
|
145
|
-
this.
|
145
|
+
this.canvas?.unregisterTransform(this);
|
146
146
|
}
|
147
147
|
|
148
148
|
onParentRectTransformChanged(comp: IRectTransform) {
|
@@ -218,7 +218,7 @@
|
|
218
218
|
// calc anchors and offset and apply
|
219
219
|
tempVec.set(0, 0, 0);
|
220
220
|
this.applyAnchoring(tempVec);
|
221
|
-
if(this.
|
221
|
+
if(this.canvas?.screenspace) tempVec.z += .1;
|
222
222
|
else tempVec.z += .01;
|
223
223
|
tempMatrix.identity();
|
224
224
|
tempMatrix.setPosition(tempVec.x, tempVec.y, tempVec.z);
|
@@ -1,6 +1,6 @@
|
|
1
1
|
/* eslint-disable */
|
2
2
|
import { TypeStore } from "./../engine_typestore.js"
|
3
|
-
|
3
|
+
|
4
4
|
// Import types
|
5
5
|
import { __Ignore } from "../../engine-components/codegen/components.js";
|
6
6
|
import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
@@ -220,7 +220,7 @@
|
|
220
220
|
import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
|
221
221
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
222
222
|
import { XRState } from "../../engine-components/webxr/XRFlag.js";
|
223
|
-
|
223
|
+
|
224
224
|
// Register types
|
225
225
|
TypeStore.add("__Ignore", __Ignore);
|
226
226
|
TypeStore.add("ActionBuilder", ActionBuilder);
|
@@ -325,6 +325,8 @@
|
|
325
325
|
private _sharedMaterials!: SharedMaterialArray;
|
326
326
|
private _originalMaterials?: Material[];
|
327
327
|
|
328
|
+
private _probeAnchorLastFrame?: Object3D;
|
329
|
+
|
328
330
|
// this is just available during deserialization
|
329
331
|
private set sharedMaterials(_val: Array<Material | null>) {
|
330
332
|
// TODO: elements in the array might be missing at the moment which leads to problems if an index is serialized
|
@@ -630,6 +632,11 @@
|
|
630
632
|
return;
|
631
633
|
}
|
632
634
|
|
635
|
+
if(this._probeAnchorLastFrame !== this.probeAnchor) {
|
636
|
+
this._reflectionProbe?.onUnset(this);
|
637
|
+
this.updateReflectionProbe();
|
638
|
+
}
|
639
|
+
|
633
640
|
if (debugRenderer == this.name && this.gameObject instanceof Mesh) {
|
634
641
|
this.gameObject.geometry.computeBoundingSphere();
|
635
642
|
const tempCenter = getTempVector(this.gameObject.geometry.boundingSphere.center).applyMatrix4(this.gameObject.matrixWorld);
|
@@ -749,7 +756,7 @@
|
|
749
756
|
}
|
750
757
|
|
751
758
|
if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
|
752
|
-
this._reflectionProbe.onUnset(this)
|
759
|
+
this._reflectionProbe.onUnset(this);
|
753
760
|
}
|
754
761
|
}
|
755
762
|
|
@@ -777,11 +784,12 @@
|
|
777
784
|
// handle reflection probe
|
778
785
|
this._reflectionProbe = null;
|
779
786
|
if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off) {
|
780
|
-
if (!this.probeAnchor) return;
|
781
787
|
// update the reflection probe right before rendering
|
782
788
|
// if we do it immediately the reflection probe might not be enabled yet
|
783
789
|
// (since this method is called from onEnable)
|
784
790
|
this.startCoroutine(this._updateReflectionProbe(), FrameEvent.LateUpdate);
|
791
|
+
|
792
|
+
this._probeAnchorLastFrame = this.probeAnchor;
|
785
793
|
}
|
786
794
|
}
|
787
795
|
private *_updateReflectionProbe() {
|
@@ -111,10 +111,10 @@
|
|
111
111
|
* The scenes that can be loaded by the SceneSwitcher.
|
112
112
|
*/
|
113
113
|
@serializable(AssetReference)
|
114
|
-
scenes
|
114
|
+
scenes: AssetReference[] = [];
|
115
115
|
|
116
116
|
@serializable(AssetReference)
|
117
|
-
loadingScene
|
117
|
+
loadingScene?: AssetReference;
|
118
118
|
|
119
119
|
/** the url parameter that is set/used to store the currently loaded scene in, set to "" to disable */
|
120
120
|
@serializable()
|
@@ -289,8 +289,8 @@
|
|
289
289
|
|
290
290
|
if (sprite.texture && !mat.wireframe) {
|
291
291
|
let tex = sprite.texture;
|
292
|
-
// the sprite renderer modifies the
|
293
|
-
// so we need to clone the texture
|
292
|
+
// the sprite renderer modifies the texture offset and scale
|
293
|
+
// so we need to clone the texture
|
294
294
|
// if the same texture is used multiple times
|
295
295
|
if (tex[$spriteTexOwner] !== undefined && tex[$spriteTexOwner] !== this && this.spriteFrames > 1) {
|
296
296
|
tex = sprite!.texture = tex.clone();
|
@@ -32,8 +32,10 @@
|
|
32
32
|
WebGLRenderTarget} from 'three';
|
33
33
|
import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
|
34
34
|
|
35
|
+
import { VERSION } from "../../../engine/engine_constants.js";
|
35
36
|
import type { OffscreenCanvasExt } from '../../../engine/engine_shims.js';
|
36
37
|
import { Progress } from '../../../engine/engine_time_utils.js';
|
38
|
+
import { BehaviorExtension } from '../../api.js';
|
37
39
|
import type { IUSDExporterExtension } from './Extension.js';
|
38
40
|
import type { AnimationExtension } from './extensions/Animation.js';
|
39
41
|
|
@@ -95,6 +97,7 @@
|
|
95
97
|
name: string;
|
96
98
|
type?: string; // by default, Xform is used
|
97
99
|
displayName?: string;
|
100
|
+
visibility?: "inherited" | "invisible"; // defaults to "inherited" in USD
|
98
101
|
matrix: Matrix4;
|
99
102
|
private _isDynamic: boolean;
|
100
103
|
get isDynamic() { return this._isDynamic; }
|
@@ -314,7 +317,7 @@
|
|
314
317
|
findById( uuid: string ) {
|
315
318
|
|
316
319
|
let found = false;
|
317
|
-
function search( current ) {
|
320
|
+
function search( current: USDObject ) {
|
318
321
|
|
319
322
|
if ( found ) return;
|
320
323
|
if ( current.uuid === uuid ) {
|
@@ -328,6 +331,7 @@
|
|
328
331
|
|
329
332
|
for ( const child of current.children ) {
|
330
333
|
|
334
|
+
if (!child) continue;
|
331
335
|
const res = search( child );
|
332
336
|
if ( res ) return res;
|
333
337
|
|
@@ -342,12 +346,21 @@
|
|
342
346
|
}
|
343
347
|
|
344
348
|
|
345
|
-
buildHeader(
|
346
|
-
|
349
|
+
buildHeader( _context: USDZExporterContext ) {
|
350
|
+
const animationExtension = _context.extensions?.find( ext => ext?.extensionName === 'animation' ) as AnimationExtension | undefined;
|
351
|
+
const behaviorExtension = _context.extensions?.find( ext => ext?.extensionName === 'Behaviour' ) as BehaviorExtension | undefined;
|
352
|
+
const startTimeCode = animationExtension?.getStartTimeCode() ?? 0;
|
353
|
+
const endTimeCode = animationExtension?.getEndTimeCode() ?? 0;
|
354
|
+
|
347
355
|
return `#usda 1.0
|
348
356
|
(
|
349
357
|
customLayerData = {
|
350
|
-
string creator = "Needle Engine
|
358
|
+
string creator = "Needle Engine ${VERSION}"
|
359
|
+
dictionary Needle = {
|
360
|
+
bool animations = ${animationExtension ? 1 : 0}
|
361
|
+
bool interactive = ${behaviorExtension ? 1 : 0}
|
362
|
+
bool quickLookCompatible = ${_context.quickLookCompatible ? 1 : 0}
|
363
|
+
}
|
351
364
|
}
|
352
365
|
defaultPrim = "${makeNameSafe( this.name )}"
|
353
366
|
metersPerUnit = 1
|
@@ -356,6 +369,7 @@
|
|
356
369
|
endTimeCode = ${endTimeCode}
|
357
370
|
timeCodesPerSecond = 60
|
358
371
|
framesPerSecond = 60
|
372
|
+
doc = """Generated by Needle Engine USDZ Exporter ${VERSION}"""
|
359
373
|
)
|
360
374
|
`;
|
361
375
|
|
@@ -460,6 +474,7 @@
|
|
460
474
|
exporter: USDZExporter;
|
461
475
|
extensions: Array<IUSDExporterExtension> = [];
|
462
476
|
quickLookCompatible: boolean;
|
477
|
+
exportInvisible: boolean;
|
463
478
|
materials: Map<string, Material>;
|
464
479
|
textures: TextureMap;
|
465
480
|
files: { [path: string]: Uint8Array | [Uint8Array, fflate.ZipOptions] | null | any }
|
@@ -467,14 +482,19 @@
|
|
467
482
|
output: string;
|
468
483
|
animations: AnimationClip[];
|
469
484
|
|
470
|
-
constructor( root: Object3D | undefined, exporter: USDZExporter,
|
485
|
+
constructor( root: Object3D | undefined, exporter: USDZExporter, options: {
|
486
|
+
extensions?: Array<IUSDExporterExtension>,
|
487
|
+
quickLookCompatible: boolean,
|
488
|
+
exportInvisible: boolean,
|
489
|
+
} ) {
|
471
490
|
|
472
491
|
this.root = root;
|
473
492
|
this.exporter = exporter;
|
474
|
-
this.quickLookCompatible = quickLookCompatible;
|
493
|
+
this.quickLookCompatible = options.quickLookCompatible;
|
494
|
+
this.exportInvisible = options.exportInvisible;
|
475
495
|
|
476
|
-
if ( extensions )
|
477
|
-
this.extensions = extensions;
|
496
|
+
if ( options.extensions )
|
497
|
+
this.extensions = options.extensions;
|
478
498
|
|
479
499
|
this.materials = new Map();
|
480
500
|
this.textures = {};
|
@@ -503,6 +523,7 @@
|
|
503
523
|
quickLookCompatible: boolean = false;
|
504
524
|
extensions: any[] = [];
|
505
525
|
maxTextureSize: number = 4096;
|
526
|
+
exportInvisible: boolean = false;
|
506
527
|
}
|
507
528
|
|
508
529
|
class USDZExporter {
|
@@ -522,7 +543,7 @@
|
|
522
543
|
options = Object.assign( new USDZExporterOptions(), options );
|
523
544
|
|
524
545
|
this.sceneAnchoringOptions = options;
|
525
|
-
const context = new USDZExporterContext( scene, this, options
|
546
|
+
const context = new USDZExporterContext( scene, this, options );
|
526
547
|
this.extensions = context.extensions;
|
527
548
|
|
528
549
|
const files = context.files;
|
@@ -541,7 +562,9 @@
|
|
541
562
|
Progress.report('export-usdz', "Reparent bones to common ancestor");
|
542
563
|
// HACK let's find all skeletons and reparent them to their skelroot / armature / uppermost bone parent
|
543
564
|
const reparentings: Array<{ object: Object3D, originalParent: Object3D | null, newParent: Object3D }> = [];
|
544
|
-
scene?.
|
565
|
+
scene?.traverse(object => {
|
566
|
+
if (!options.exportInvisible && !object.visible) return;
|
567
|
+
|
545
568
|
if (object instanceof SkinnedMesh) {
|
546
569
|
const bones = object.skeleton.bones as Bone[];
|
547
570
|
|
@@ -559,7 +582,7 @@
|
|
559
582
|
}
|
560
583
|
|
561
584
|
Progress.report('export-usdz', "Traversing hierarchy");
|
562
|
-
if(scene)
|
585
|
+
if(scene) traverse( scene, context.document, context, this.keepObject);
|
563
586
|
|
564
587
|
Progress.report('export-usdz', "Invoking onAfterBuildDocument");
|
565
588
|
await invokeAll( context, 'onAfterBuildDocument' );
|
@@ -585,12 +608,8 @@
|
|
585
608
|
|
586
609
|
// Moved into parseDocument callback for proper defaultPrim encapsulation
|
587
610
|
// context.output += buildMaterials( materials, textures, options.quickLookCompatible );
|
588
|
-
|
589
|
-
const animationExtension: AnimationExtension | undefined = context.extensions.find( ext => ext.extensionName === 'animation' ) as AnimationExtension;
|
590
|
-
const startTimeCode = animationExtension?.getStartTimeCode() ?? 0;
|
591
|
-
const endTimeCode = animationExtension?.getEndTimeCode() ?? 0;
|
592
611
|
|
593
|
-
const header = context.document.buildHeader(
|
612
|
+
const header = context.document.buildHeader( context );
|
594
613
|
const final = header + '\n' + context.output;
|
595
614
|
|
596
615
|
// full output file
|
@@ -690,10 +709,10 @@
|
|
690
709
|
|
691
710
|
}
|
692
711
|
|
693
|
-
function
|
712
|
+
function traverse( object: Object3D, parentModel: USDObject, context: USDZExporterContext, keepObject?: (object: Object3D) => boolean ) {
|
694
713
|
|
695
|
-
if ( !
|
696
|
-
|
714
|
+
if (!context.exportInvisible && !object.visible) return;
|
715
|
+
|
697
716
|
let model: USDObject | undefined = undefined;
|
698
717
|
let geometry: BufferGeometry | undefined = undefined;
|
699
718
|
let material: Material | Material[] | undefined = undefined;
|
@@ -703,6 +722,8 @@
|
|
703
722
|
material = object.material;
|
704
723
|
}
|
705
724
|
|
725
|
+
// API for an explicit choice to discard this object – for example, a geometry that should not be exported,
|
726
|
+
// but childs should still be exported.
|
706
727
|
if (keepObject && !keepObject(object)) {
|
707
728
|
geometry = undefined;
|
708
729
|
material = undefined;
|
@@ -729,6 +750,7 @@
|
|
729
750
|
if ( model ) {
|
730
751
|
|
731
752
|
model.displayName = object.name;
|
753
|
+
model.visibility = object.visible ? undefined : "invisible";
|
732
754
|
|
733
755
|
if ( parentModel ) {
|
734
756
|
|
@@ -764,7 +786,7 @@
|
|
764
786
|
|
765
787
|
for ( const ch of object.children ) {
|
766
788
|
|
767
|
-
|
789
|
+
traverse( ch, parentModel, context, keepObject );
|
768
790
|
|
769
791
|
}
|
770
792
|
|
@@ -858,7 +880,7 @@
|
|
858
880
|
if ( ! ( geometryFileName in context.files ) ) {
|
859
881
|
|
860
882
|
const action = () => {
|
861
|
-
const meshObject = buildMeshObject( geometry, object.skinnedMesh?.skeleton?.bones );
|
883
|
+
const meshObject = buildMeshObject( geometry, object.skinnedMesh?.skeleton?.bones, context.quickLookCompatible );
|
862
884
|
context.files[ geometryFileName ] = buildUSDFileAsString( meshObject, context);
|
863
885
|
};
|
864
886
|
|
@@ -1208,6 +1230,7 @@
|
|
1208
1230
|
const objType = isSkinnedMesh ? 'SkelRoot' : 'Xform';
|
1209
1231
|
const apiSchemas = isSkinnedMesh ? '"MaterialBindingAPI", "SkelBindingAPI"' : '"MaterialBindingAPI"';
|
1210
1232
|
|
1233
|
+
writer.appendLine();
|
1211
1234
|
if ( geometry ) {
|
1212
1235
|
writer.beginBlock( `def ${objType} "${name}"`, "(", false );
|
1213
1236
|
// NE-4084: To use the doubleSided workaround with skeletal meshes we'd have to
|
@@ -1257,6 +1280,9 @@
|
|
1257
1280
|
if (model.type === undefined)
|
1258
1281
|
writer.appendLine( 'uniform token[] xformOpOrder = ["xformOp:transform"]' );
|
1259
1282
|
|
1283
|
+
if (model.visibility !== undefined)
|
1284
|
+
writer.appendLine(`token visibility = "${model.visibility}"`);
|
1285
|
+
|
1260
1286
|
if ( camera ) {
|
1261
1287
|
|
1262
1288
|
if ( 'isOrthographicCamera' in camera && camera.isOrthographicCamera ) {
|
@@ -1321,9 +1347,9 @@
|
|
1321
1347
|
|
1322
1348
|
// Mesh
|
1323
1349
|
|
1324
|
-
function buildMeshObject( geometry: BufferGeometry, bonesArray: Bone[] = [] ) {
|
1350
|
+
function buildMeshObject( geometry: BufferGeometry, bonesArray: Bone[] = [], quickLookCompatible: boolean = true) {
|
1325
1351
|
|
1326
|
-
const mesh = buildMesh( geometry, bonesArray );
|
1352
|
+
const mesh = buildMesh( geometry, bonesArray, quickLookCompatible );
|
1327
1353
|
return `
|
1328
1354
|
def "Geometry"
|
1329
1355
|
${mesh}
|
@@ -1331,7 +1357,7 @@
|
|
1331
1357
|
|
1332
1358
|
}
|
1333
1359
|
|
1334
|
-
function buildMesh( geometry, bones: Bone[] = [] ) {
|
1360
|
+
function buildMesh( geometry, bones: Bone[] = [], quickLookCompatible: boolean = true) {
|
1335
1361
|
|
1336
1362
|
const name = 'Geometry';
|
1337
1363
|
const attributes = geometry.attributes;
|
@@ -1348,9 +1374,24 @@
|
|
1348
1374
|
let sortedSkinIndexAttribute: BufferAttribute | null = attributes.skinIndex;
|
1349
1375
|
let bonesArray = "";
|
1350
1376
|
if (hasBones) {
|
1377
|
+
const uuidsFound:string[] = [];
|
1378
|
+
for(const bone of bones ){
|
1379
|
+
if (bone.parent!.type !== 'Bone'){
|
1380
|
+
sortedBones.push({bone: bone, index: bones.indexOf(bone)});
|
1381
|
+
uuidsFound.push(bone.uuid);
|
1382
|
+
}
|
1383
|
+
}
|
1351
1384
|
|
1352
|
-
|
1353
|
-
|
1385
|
+
while (uuidsFound.length < bones.length){
|
1386
|
+
for(const sortedBone of sortedBones){
|
1387
|
+
const children = sortedBone.bone.children as Bone[];
|
1388
|
+
for (const childBone of children){
|
1389
|
+
if (uuidsFound.indexOf(childBone.uuid) === -1 && bones.indexOf(childBone) !== -1){
|
1390
|
+
sortedBones.push({bone: childBone, index: bones.indexOf(childBone)});
|
1391
|
+
uuidsFound.push(childBone.uuid);
|
1392
|
+
}
|
1393
|
+
}
|
1394
|
+
}
|
1354
1395
|
}
|
1355
1396
|
|
1356
1397
|
// add structural nodes to the list of bones
|
@@ -1359,7 +1400,7 @@
|
|
1359
1400
|
}
|
1360
1401
|
|
1361
1402
|
// sort bones by path – need to be sorted in the same order as during mesh export
|
1362
|
-
const assumedRoot =
|
1403
|
+
const assumedRoot = sortedBones[0].bone.parent!;
|
1363
1404
|
sortedBones.sort((a, b) => getPathToSkeleton(a.bone, assumedRoot) > getPathToSkeleton(b.bone, assumedRoot) ? 1 : -1);
|
1364
1405
|
bonesArray = sortedBones.map( x => "\"" + getPathToSkeleton(x.bone, assumedRoot) + "\"" ).join( ', ' );
|
1365
1406
|
|
@@ -1398,9 +1439,10 @@
|
|
1398
1439
|
{
|
1399
1440
|
int[] faceVertexCounts = [${buildMeshVertexCount( geometry )}]
|
1400
1441
|
int[] faceVertexIndices = [${buildMeshVertexIndices( geometry )}]
|
1401
|
-
|
1442
|
+
${attributes.normal || quickLookCompatible ? // in QuickLook, normals are required, otherwise double-sided rendering doesn't work.
|
1443
|
+
`normal3f[] normals = [${buildVector3Array( attributes.normal, count )}] (
|
1402
1444
|
interpolation = "vertex"
|
1403
|
-
)
|
1445
|
+
)` : '' }
|
1404
1446
|
point3f[] points = [${buildVector3Array( attributes.position, count )}]
|
1405
1447
|
${attributes.uv ?
|
1406
1448
|
`texCoord2f[] primvars:st = [${buildVector2Array( attributes.uv, count )}] (
|
@@ -1431,7 +1473,7 @@
|
|
1431
1473
|
uniform token subdivisionScheme = "none"
|
1432
1474
|
}
|
1433
1475
|
}
|
1434
|
-
|
1476
|
+
${quickLookCompatible ? `
|
1435
1477
|
# This is a workaround for QuickLook/RealityKit not supporting the doubleSided attribute. We're adding a second
|
1436
1478
|
# geometry definition here, that uses the same mesh data but appends extra faces with reversed winding order.
|
1437
1479
|
def "${name}_doubleSided" (
|
@@ -1444,8 +1486,8 @@
|
|
1444
1486
|
int[] faceVertexIndices = [${buildMeshVertexIndices( geometry ) + ", " + buildMeshVertexIndices( geometry, true )}]
|
1445
1487
|
}
|
1446
1488
|
}
|
1489
|
+
` : '' }
|
1447
1490
|
`;
|
1448
|
-
|
1449
1491
|
}
|
1450
1492
|
|
1451
1493
|
function buildMeshVertexCount( geometry ) {
|
@@ -1495,8 +1537,8 @@
|
|
1495
1537
|
|
1496
1538
|
if ( attribute === undefined ) {
|
1497
1539
|
|
1498
|
-
console.warn( 'USDZExporter:
|
1499
|
-
return Array( count ).fill( '(0, 0,
|
1540
|
+
console.warn( 'USDZExporter: Attribute is missing. Results may be undefined.' );
|
1541
|
+
return Array( count ).fill( '(0, 0, 1)' ).join( ', ' );
|
1500
1542
|
|
1501
1543
|
}
|
1502
1544
|
|
@@ -1575,13 +1617,11 @@
|
|
1575
1617
|
|
1576
1618
|
}
|
1577
1619
|
|
1578
|
-
return `
|
1620
|
+
return `
|
1621
|
+
def "Materials"
|
1579
1622
|
{
|
1580
1623
|
${array.join( '' )}
|
1581
|
-
}
|
1582
|
-
|
1583
|
-
`;
|
1584
|
-
|
1624
|
+
}`;
|
1585
1625
|
}
|
1586
1626
|
|
1587
1627
|
/** Slot of the exported texture. Some slots (e.g. normal, opacity) require additional processing. */
|
@@ -1595,9 +1635,12 @@
|
|
1595
1635
|
const inputs: Array<string> = [];
|
1596
1636
|
const samplers: Array<string> = [];
|
1597
1637
|
const materialName = getMaterialName(material);
|
1638
|
+
const usedUVChannels: Set<number> = new Set();
|
1598
1639
|
|
1599
1640
|
function texName(tex: Texture) {
|
1600
|
-
|
1641
|
+
// If we have a source, we only want to use the source's id, not the texture's id
|
1642
|
+
// to avoid exporting the same underlying data multiple times.
|
1643
|
+
return makeNameSafe(tex.name) + '_' + (tex.source?.id ?? tex.id);
|
1601
1644
|
}
|
1602
1645
|
|
1603
1646
|
function buildTexture( texture: Texture, mapType: MapType, color: Color | undefined = undefined, opacity: number | undefined = undefined ) {
|
@@ -1628,6 +1671,7 @@
|
|
1628
1671
|
textures[ id ] = { texture, scale: scaleToApply };
|
1629
1672
|
|
1630
1673
|
const uv = texture.channel > 0 ? 'st' + texture.channel : 'st';
|
1674
|
+
usedUVChannels.add(texture.channel);
|
1631
1675
|
|
1632
1676
|
const isRGBA = formatsWithAlphaChannel.includes( texture.format );
|
1633
1677
|
|
@@ -1655,6 +1699,10 @@
|
|
1655
1699
|
// This is NOT correct yet in QuickLook, but comes close for a range of models.
|
1656
1700
|
// It becomes more incorrect the bigger the offset is
|
1657
1701
|
|
1702
|
+
// sanitize repeat values to avoid NaNs
|
1703
|
+
if (repeat.x === 0) repeat.x = 0.0001;
|
1704
|
+
if (repeat.y === 0) repeat.y = 0.0001;
|
1705
|
+
|
1658
1706
|
offset.x = offset.x / repeat.x;
|
1659
1707
|
offset.y = offset.y / repeat.y;
|
1660
1708
|
|
@@ -1681,42 +1729,41 @@
|
|
1681
1729
|
const normalBiasZString = (1 - normalScale).toFixed( PRECISION );
|
1682
1730
|
|
1683
1731
|
return `
|
1684
|
-
|
1685
|
-
|
1686
|
-
|
1687
|
-
|
1688
|
-
|
1689
|
-
|
1690
|
-
|
1691
|
-
|
1692
|
-
|
1693
|
-
|
1694
|
-
|
1695
|
-
|
1696
|
-
|
1697
|
-
|
1698
|
-
|
1699
|
-
|
1700
|
-
|
1701
|
-
|
1702
|
-
|
1703
|
-
|
1704
|
-
|
1705
|
-
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1712
|
-
|
1713
|
-
|
1714
|
-
|
1715
|
-
|
1716
|
-
|
1717
|
-
|
1718
|
-
|
1719
|
-
|
1732
|
+
${needsTextureTransform ? `def Shader "Transform2d_${mapType}" (
|
1733
|
+
sdrMetadata = {
|
1734
|
+
string role = "math"
|
1735
|
+
}
|
1736
|
+
)
|
1737
|
+
{
|
1738
|
+
uniform token info:id = "UsdTransform2d"
|
1739
|
+
float2 inputs:in.connect = ${textureTransformInput}
|
1740
|
+
float2 inputs:scale = ${buildVector2( repeat )}
|
1741
|
+
float2 inputs:translation = ${buildVector2( offset )}
|
1742
|
+
float inputs:rotation = ${(rotation / Math.PI * 180).toFixed( PRECISION )}
|
1743
|
+
float2 outputs:result
|
1744
|
+
}
|
1745
|
+
` : '' }
|
1746
|
+
def Shader "${name}_${mapType}"
|
1747
|
+
{
|
1748
|
+
uniform token info:id = "UsdUVTexture"
|
1749
|
+
asset inputs:file = @textures/${id}.${isRGBA ? 'png' : 'jpg'}@
|
1750
|
+
token inputs:sourceColorSpace = "${ texture.colorSpace === 'srgb' ? 'sRGB' : 'raw' }"
|
1751
|
+
float2 inputs:st.connect = ${needsTextureTransform ? textureTransformOutput : textureTransformInput}
|
1752
|
+
${needsTextureScale ? `
|
1753
|
+
float4 inputs:scale = (${color ? color.r + ', ' + color.g + ', ' + color.b : '1, 1, 1'}, ${opacity})
|
1754
|
+
` : `` }
|
1755
|
+
${needsNormalScaleAndBias ? `
|
1756
|
+
float4 inputs:scale = (${normalScaleValueString}, ${normalScaleValueString}, ${normalScaleValueString}, 1)
|
1757
|
+
float4 inputs:bias = (${normalBiasString}, ${normalBiasString}, ${normalBiasZString}, 0)
|
1758
|
+
` : `` }
|
1759
|
+
token inputs:wrapS = "${ WRAPPINGS[ texture.wrapS ] }"
|
1760
|
+
token inputs:wrapT = "${ WRAPPINGS[ texture.wrapT ] }"
|
1761
|
+
float outputs:r
|
1762
|
+
float outputs:g
|
1763
|
+
float outputs:b
|
1764
|
+
float3 outputs:rgb
|
1765
|
+
${material.transparent || material.alphaTest > 0.0 ? 'float outputs:a' : ''}
|
1766
|
+
}`;
|
1720
1767
|
}
|
1721
1768
|
|
1722
1769
|
let effectiveOpacity = ( material.transparent || material.alphaTest ) ? material.opacity : 1;
|
@@ -1734,7 +1781,16 @@
|
|
1734
1781
|
|
1735
1782
|
inputs.push( `${pad}color3f inputs:diffuseColor.connect = ${materialRoot}/${materialName}/${texName(material.map)}_diffuse.outputs:rgb>` );
|
1736
1783
|
|
1737
|
-
|
1784
|
+
// Enforce alpha hashing in QuickLook for unlit materials
|
1785
|
+
if (material instanceof MeshBasicMaterial && material.transparent && material.alphaTest == 0.0 && quickLookCompatible) {
|
1786
|
+
inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/${materialName}/${texName(material.map)}_diffuse.outputs:a>` );
|
1787
|
+
haveConnectedOpacity = true;
|
1788
|
+
// see below – QuickLook applies alpha hashing instead of pure blending when
|
1789
|
+
// both opacity and opacityThreshold are connected
|
1790
|
+
inputs.push( `${pad}float inputs:opacityThreshold = ${0.0000000001}` );
|
1791
|
+
haveConnectedOpacityThreshold = true;
|
1792
|
+
}
|
1793
|
+
else if ( material.transparent ) {
|
1738
1794
|
|
1739
1795
|
inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/${materialName}/${texName(material.map)}_diffuse.outputs:a>` );
|
1740
1796
|
haveConnectedOpacity = true;
|
@@ -1756,7 +1812,7 @@
|
|
1756
1812
|
|
1757
1813
|
}
|
1758
1814
|
|
1759
|
-
if ( material.alphaHash ) {
|
1815
|
+
if ( material.alphaHash && quickLookCompatible) {
|
1760
1816
|
|
1761
1817
|
// Seems we can do this to basically enforce alpha hashing / dithered transparency in QuickLook –
|
1762
1818
|
// works completely different in usdview though...
|
@@ -1880,21 +1936,34 @@
|
|
1880
1936
|
|
1881
1937
|
}
|
1882
1938
|
|
1939
|
+
// check if usedUVChannels contains any data besides exactly 0 or 1
|
1940
|
+
if (usedUVChannels.size > 2) {
|
1941
|
+
console.warn('USDZExporter: Material ' + material.name + ' uses more than 2 UV channels. Currently, only UV0 and UV1 are supported.');
|
1942
|
+
}
|
1943
|
+
else if (usedUVChannels.size === 2) {
|
1944
|
+
// check if it's exactly 0 and 1
|
1945
|
+
if (!usedUVChannels.has(0) || !usedUVChannels.has(1)) {
|
1946
|
+
console.warn('USDZExporter: Material ' + material.name + ' uses UV channels other than 0 and 1. Currently, only UV0 and UV1 are supported.');
|
1947
|
+
}
|
1948
|
+
}
|
1949
|
+
|
1883
1950
|
return `
|
1884
|
-
|
1885
|
-
|
1886
|
-
|
1951
|
+
|
1952
|
+
def Material "${materialName}" ${material.name ?`(
|
1953
|
+
displayName = "${material.name}"
|
1954
|
+
)` : ''}
|
1887
1955
|
{
|
1956
|
+
token outputs:surface.connect = ${materialRoot}/${materialName}/PreviewSurface.outputs:surface>
|
1957
|
+
|
1888
1958
|
def Shader "PreviewSurface"
|
1889
1959
|
{
|
1890
1960
|
uniform token info:id = "UsdPreviewSurface"
|
1891
1961
|
${inputs.join( '\n' )}
|
1892
|
-
int inputs:useSpecularWorkflow = 0
|
1962
|
+
int inputs:useSpecularWorkflow = ${material instanceof MeshBasicMaterial ? '1' : '0'}
|
1893
1963
|
token outputs:surface
|
1894
1964
|
}
|
1895
|
-
|
1896
|
-
|
1897
|
-
|
1965
|
+
${samplers.length > 0 ? `
|
1966
|
+
${usedUVChannels.has(0) ? `
|
1898
1967
|
def Shader "uvReader_st"
|
1899
1968
|
{
|
1900
1969
|
uniform token info:id = "UsdPrimvarReader_float2"
|
@@ -1902,7 +1971,8 @@
|
|
1902
1971
|
float2 inputs:fallback = (0.0, 0.0)
|
1903
1972
|
float2 outputs:result
|
1904
1973
|
}
|
1905
|
-
|
1974
|
+
` : ''}
|
1975
|
+
${usedUVChannels.has(1) ? `
|
1906
1976
|
def Shader "uvReader_st2"
|
1907
1977
|
{
|
1908
1978
|
uniform token info:id = "UsdPrimvarReader_float2"
|
@@ -1910,12 +1980,9 @@
|
|
1910
1980
|
float2 inputs:fallback = (0.0, 0.0)
|
1911
1981
|
float2 outputs:result
|
1912
1982
|
}
|
1913
|
-
|
1914
|
-
${samplers.join( '\n' )}
|
1915
|
-
|
1916
|
-
}
|
1917
|
-
`;
|
1918
|
-
|
1983
|
+
` : ''}
|
1984
|
+
${samplers.join( '\n' )}` : ''}
|
1985
|
+
}`;
|
1919
1986
|
}
|
1920
1987
|
|
1921
1988
|
function buildColor( color ) {
|
@@ -11,7 +11,7 @@
|
|
11
11
|
import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
|
12
12
|
import { WebXR } from "../../webxr/WebXR.js";
|
13
13
|
import { WebXRButtonFactory } from "../../webxr/WebXRButtons.js";
|
14
|
-
import {
|
14
|
+
import { XRState, XRStateFlag } from "../../webxr/XRFlag.js";
|
15
15
|
import type { IUSDExporterExtension } from "./Extension.js";
|
16
16
|
import { AnimationExtension } from "./extensions/Animation.js"
|
17
17
|
import { AudioExtension } from "./extensions/behavior/AudioExtension.js";
|
@@ -20,7 +20,7 @@
|
|
20
20
|
import { TextExtension } from "./extensions/USDZText.js";
|
21
21
|
import { USDZUIExtension } from "./extensions/USDZUI.js";
|
22
22
|
import { USDZExporter as ThreeUSDZExporter } from "./ThreeUSDZExporter.js";
|
23
|
-
import { registerAnimatorsImplictly, registerAudioSourcesImplictly } from "./utils/animationutils.js";
|
23
|
+
import { disableObjectsAtStart, registerAnimatorsImplictly, registerAudioSourcesImplictly } from "./utils/animationutils.js";
|
24
24
|
import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js";
|
25
25
|
|
26
26
|
const debug = getParam("debugusdz");
|
@@ -337,15 +337,15 @@
|
|
337
337
|
XRState.Global.Set(XRStateFlag.AR);
|
338
338
|
|
339
339
|
const exporter = new ThreeUSDZExporter();
|
340
|
-
|
341
|
-
|
342
|
-
//
|
340
|
+
// We're creating a new animation extension on each export to avoid issues with multiple exports.
|
341
|
+
// TODO we probably want to do that with all the extensions...
|
342
|
+
// Ordering of extensions is important
|
343
343
|
const animExt = new AnimationExtension(this.quickLookCompatible);
|
344
|
-
extensions.
|
344
|
+
const extensions: any = [animExt, ...this.extensions]
|
345
345
|
|
346
346
|
const eventArgs = { self: this, exporter: exporter, extensions: extensions, object: objectToExport };
|
347
347
|
Progress.report("export-usdz", "Invoking before-export");
|
348
|
-
this.dispatchEvent(new CustomEvent("before-export", { detail: eventArgs }))
|
348
|
+
this.dispatchEvent(new CustomEvent("before-export", { detail: eventArgs }));
|
349
349
|
|
350
350
|
// make sure we apply the AR scale
|
351
351
|
this.applyWebARSessionRoot();
|
@@ -369,14 +369,40 @@
|
|
369
369
|
//@ts-ignore
|
370
370
|
exporter.debug = debug;
|
371
371
|
exporter.keepObject = (object) => {
|
372
|
-
//
|
373
|
-
//
|
374
|
-
//
|
372
|
+
// This explicitly removes geometry and material data from disabled renderers.
|
373
|
+
// Note that this is different to the object itself being active –
|
374
|
+
// here, we have an active object with a disabled renderer.
|
375
375
|
const renderer = GameObject.getComponent(object, Renderer)
|
376
376
|
if (renderer && !renderer.enabled) return false;
|
377
377
|
return true;
|
378
378
|
}
|
379
379
|
|
380
|
+
// Collect invisible objects so that we can disable them if
|
381
|
+
// - we're exporting for QuickLook
|
382
|
+
// - and interactive behaviors are allowed.
|
383
|
+
// When exporting for regular USD, we're supporting the "visibility" attribute anyways.
|
384
|
+
const objectsToDisableAtSceneStart = new Array<Object3D>();
|
385
|
+
if (this.objectToExport && this.quickLookCompatible && this.interactive) {
|
386
|
+
this.objectToExport.traverse((obj) => {
|
387
|
+
if (!obj.visible) {
|
388
|
+
objectsToDisableAtSceneStart.push(obj);
|
389
|
+
}
|
390
|
+
});
|
391
|
+
}
|
392
|
+
|
393
|
+
const behaviorExt = this.extensions.find(ext => ext.extensionName === "Behaviour");
|
394
|
+
if (this.interactive && behaviorExt) {
|
395
|
+
//@ts-ignore
|
396
|
+
behaviorExt.addBehavior(disableObjectsAtStart(objectsToDisableAtSceneStart));
|
397
|
+
}
|
398
|
+
|
399
|
+
let exportInvisible = true;
|
400
|
+
// The only case where we want to strip out invisible objects is
|
401
|
+
// when we're exporting for QuickLook and we're NOT adding interactive behaviors,
|
402
|
+
// since QuickLook on iOS does not support "visibility" tokens.
|
403
|
+
if (this.quickLookCompatible && !this.interactive)
|
404
|
+
exportInvisible = false;
|
405
|
+
|
380
406
|
// sanitize anchoring types
|
381
407
|
if (this.anchoringType !== "plane" && this.anchoringType !== "none" && this.anchoringType !== "image" && this.anchoringType !== "face")
|
382
408
|
this.anchoringType = "plane";
|
@@ -384,6 +410,7 @@
|
|
384
410
|
this.planeAnchoringAlignment = "horizontal";
|
385
411
|
|
386
412
|
Progress.report("export-usdz", "Invoking exporter.parse");
|
413
|
+
|
387
414
|
//@ts-ignore
|
388
415
|
const arraybuffer = await exporter.parse(this.objectToExport, {
|
389
416
|
ar: {
|
@@ -397,6 +424,7 @@
|
|
397
424
|
extensions: extensions,
|
398
425
|
quickLookCompatible: this.quickLookCompatible,
|
399
426
|
maxTextureSize: this.maxTextureSize,
|
427
|
+
exportInvisible: exportInvisible,
|
400
428
|
});
|
401
429
|
|
402
430
|
const blob = new Blob([arraybuffer], { type: 'model/vnd.usdz+zip' });
|
@@ -148,54 +148,6 @@
|
|
148
148
|
return "text";
|
149
149
|
}
|
150
150
|
|
151
|
-
// HACK we should clean this up, text export has moved to USDZUI.ts and is
|
152
|
-
// integrated into the hierarchy now
|
153
|
-
onExportObject(_object: Object3D, _model: USDObject, _context: USDZExporterContext) {
|
154
|
-
|
155
|
-
return;
|
156
|
-
|
157
|
-
/*
|
158
|
-
const text = GameObject.getComponent(object, Text);
|
159
|
-
if (text) {
|
160
|
-
const rt = GameObject.getComponent(object, RectTransform);
|
161
|
-
let width = 100;
|
162
|
-
let height = 100;
|
163
|
-
if (rt) {
|
164
|
-
width = rt.width;
|
165
|
-
height = rt.height;
|
166
|
-
|
167
|
-
// Take the matrix from the three mesh ui object:
|
168
|
-
const mat = rt.shadowComponent!.matrix;
|
169
|
-
model.matrix.copy(mat);
|
170
|
-
// model.matrix.premultiply(rotateYAxisMatrix);
|
171
|
-
model.matrix.premultiply(invertX);
|
172
|
-
}
|
173
|
-
const newModel = model.clone();
|
174
|
-
newModel.matrix = rotateYAxisMatrix.clone();
|
175
|
-
if (rt) // Not ideal but works for now:
|
176
|
-
newModel.matrix.premultiply(invertX);
|
177
|
-
|
178
|
-
const color = new Color().copySRGBToLinear(text.color);
|
179
|
-
newModel.material = new MeshStandardMaterial({ color: color, emissive: color });
|
180
|
-
model.add(newModel);
|
181
|
-
|
182
|
-
// model.matrix.scale(new Vector3(100, 100, 100));
|
183
|
-
newModel.addEventListener("serialize", (writer: USDWriter, _context: USDZExporterContext) => {
|
184
|
-
let txt = text.text;
|
185
|
-
txt = txt.replace(/\n/g, "\\n");
|
186
|
-
const textObj = TextBuilder.multiLine(txt, width, height, HorizontalAlignment.center, VerticalAlignment.bottom, TextWrapMode.flowing);
|
187
|
-
this.setTextAlignment(textObj, text.alignment);
|
188
|
-
this.setOverflow(textObj, text);
|
189
|
-
if (newModel.material)
|
190
|
-
textObj.material = newModel.material;
|
191
|
-
textObj.pointSize = this.convertToTextSize(text.fontSize);
|
192
|
-
textObj.depth = .001;
|
193
|
-
textObj.writeTo(undefined, writer);
|
194
|
-
});
|
195
|
-
}
|
196
|
-
*/
|
197
|
-
}
|
198
|
-
|
199
151
|
exportText(object: Object3D, newModel: USDObject, _context: USDZExporterContext) {
|
200
152
|
|
201
153
|
const text = GameObject.getComponent(object, Text);
|
@@ -1,11 +1,12 @@
|
|
1
1
|
import { Color, Mesh, MeshBasicMaterial, Object3D } from "three";
|
2
2
|
|
3
3
|
import { GameObject } from "../../../Component.js";
|
4
|
-
import { $shadowDomOwner } from "../../../ui/BaseUIComponent.js";
|
4
|
+
import { $shadowDomOwner, BaseUIComponent } from "../../../ui/BaseUIComponent.js";
|
5
5
|
import { Canvas } from "../../../ui/Canvas.js";
|
6
6
|
import { RenderMode } from "../../../ui/Canvas.js";
|
7
7
|
import { CanvasGroup } from "../../../ui/CanvasGroup.js";
|
8
8
|
import { RectTransform } from "../../../ui/RectTransform.js";
|
9
|
+
import { Text } from "../../../ui/Text.js";
|
9
10
|
import type { IUSDExporterExtension } from "../Extension.js";
|
10
11
|
import { USDObject, USDZExporterContext } from "../ThreeUSDZExporter.js";
|
11
12
|
import { TextExtension } from "./USDZText.js";
|
@@ -20,14 +21,73 @@
|
|
20
21
|
onExportObject(object: Object3D, model: USDObject, _context: USDZExporterContext) {
|
21
22
|
const canvas = GameObject.getComponent(object, Canvas);
|
22
23
|
|
23
|
-
if (canvas && canvas.
|
24
|
+
if (canvas && canvas.enabled && canvas.renderMode === RenderMode.WorldSpace) {
|
25
|
+
|
24
26
|
const textExt = new TextExtension();
|
25
27
|
const rt = GameObject.getComponent(object, RectTransform);
|
26
28
|
const canvasGroup = GameObject.getComponent(object, CanvasGroup);
|
27
29
|
|
30
|
+
// we have to do some temporary changes (enable UI component so that they're building
|
31
|
+
// their shadow hierarchy, then revert them back to the original state)
|
32
|
+
const revertActions = new Array<() => void>();
|
33
|
+
|
28
34
|
let width = 100;
|
29
35
|
let height = 100;
|
30
36
|
if (rt) {
|
37
|
+
|
38
|
+
// Workaround: since UI components are only present in the shadow hierarchy when objects are on,
|
39
|
+
// we need to enable them temporarily to export them including their potential child components.
|
40
|
+
// For example, Text can have multiple child objects (e.g. rich text, panels, ...)
|
41
|
+
if (!GameObject.isActiveSelf(object)) {
|
42
|
+
const wasActive = GameObject.isActiveSelf(object);
|
43
|
+
GameObject.setActive(object, true);
|
44
|
+
rt.onEnable();
|
45
|
+
rt.updateTransform();
|
46
|
+
|
47
|
+
revertActions.push(() => {
|
48
|
+
rt.onDisable();
|
49
|
+
GameObject.setActive(object, wasActive);
|
50
|
+
});
|
51
|
+
}
|
52
|
+
|
53
|
+
object.traverse((child) => {
|
54
|
+
if (!GameObject.isActiveInHierarchy(child)) {
|
55
|
+
const wasActive = GameObject.isActiveSelf(child);
|
56
|
+
GameObject.setActive(child, true);
|
57
|
+
const baseUIComponent = GameObject.getComponent(child, BaseUIComponent);
|
58
|
+
if (baseUIComponent) {
|
59
|
+
baseUIComponent.onEnable();
|
60
|
+
revertActions.push(() => {
|
61
|
+
baseUIComponent.onDisable();
|
62
|
+
});
|
63
|
+
}
|
64
|
+
|
65
|
+
const rectTransform = GameObject.getComponent(child, RectTransform);
|
66
|
+
if (rectTransform) {
|
67
|
+
rectTransform.onEnable();
|
68
|
+
rectTransform.updateTransform();
|
69
|
+
// This method bypasses the checks for whether the object is enabled etc.
|
70
|
+
// so that we can ensure even a disabled object has the correct layout.
|
71
|
+
rectTransform["onApplyTransform"]();
|
72
|
+
revertActions.push(() => {
|
73
|
+
rectTransform.onDisable();
|
74
|
+
});
|
75
|
+
}
|
76
|
+
|
77
|
+
const text = GameObject.getComponent(child, Text);
|
78
|
+
if (text) {
|
79
|
+
text.onEnable();
|
80
|
+
revertActions.push(() => {
|
81
|
+
text.onDisable();
|
82
|
+
});
|
83
|
+
}
|
84
|
+
|
85
|
+
revertActions.push(() => {
|
86
|
+
GameObject.setActive(child, wasActive);
|
87
|
+
});
|
88
|
+
}
|
89
|
+
});
|
90
|
+
|
31
91
|
width = rt.width;
|
32
92
|
height = rt.height;
|
33
93
|
|
@@ -38,15 +98,14 @@
|
|
38
98
|
if (shadowComponent) {
|
39
99
|
const mat = shadowComponent.matrix;
|
40
100
|
shadowRootModel.matrix.copy(mat);
|
41
|
-
// shadowRootModel.matrix.premultiply(invertX);
|
42
101
|
|
43
|
-
// TODO build map of parent GOs to USDObjects so we can reparent while traversing
|
44
102
|
const usdObjectMap = new Map<Object3D, USDObject>();
|
45
103
|
const opacityMap = new Map<Object3D, number>();
|
46
104
|
usdObjectMap.set(shadowComponent, shadowRootModel);
|
47
105
|
opacityMap.set(shadowComponent, canvasGroup ? canvasGroup.alpha : 1);
|
48
106
|
|
49
107
|
shadowComponent.traverse((child) => {
|
108
|
+
// console.log("traversing UI shadow components", shadowComponent.name + " ->" + child.name);
|
50
109
|
if (child === shadowComponent) return;
|
51
110
|
|
52
111
|
const childModel = USDObject.createEmpty();
|
@@ -109,6 +168,11 @@
|
|
109
168
|
});
|
110
169
|
}
|
111
170
|
}
|
171
|
+
|
172
|
+
// revert temporary changes that we did here
|
173
|
+
for (const revert of revertActions) {
|
174
|
+
revert();
|
175
|
+
}
|
112
176
|
}
|
113
177
|
}
|