@@ -34,18 +34,24 @@
|
|
34
34
|
return;
|
35
35
|
}
|
36
36
|
}
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
fs.
|
37
|
+
try {
|
38
|
+
const targetPath = "./" + getPosterPath();
|
39
|
+
console.debug(`Received poster, saving to ${targetPath}`);
|
40
|
+
// remove data:image/png;base64, from the beginning of the string
|
41
|
+
if (targetPath.endsWith(".webp"))
|
42
|
+
data.data = data.data.replace(/^data:image\/webp;base64,/, "");
|
43
|
+
else
|
44
|
+
data.data = data.data.replace(/^data:image\/png;base64,/, "");
|
45
|
+
const dir = path.dirname(targetPath);
|
46
|
+
if (!fs.existsSync(dir)) {
|
47
|
+
fs.mkdirSync(dir, { recursive: true })
|
48
|
+
}
|
49
|
+
fs.writeFileSync(targetPath, Buffer.from(data.data, "base64"));
|
50
|
+
console.debug("Saved poster to file");
|
47
51
|
}
|
48
|
-
|
52
|
+
catch (err) {
|
53
|
+
console.error("Failed to save poster", err.message);
|
54
|
+
}
|
49
55
|
});
|
50
56
|
},
|
51
57
|
transformIndexHtml: {
|
@@ -47,7 +47,7 @@
|
|
47
47
|
USDObject.createEmptyParent(model);
|
48
48
|
}
|
49
49
|
const clone = model.clone();
|
50
|
-
if (this.matrix) clone.
|
50
|
+
if (this.matrix) clone.setMatrix(this.matrix);
|
51
51
|
if (this.material) clone.material = this.material;
|
52
52
|
if (this.geometry) clone.geometry = this.geometry;
|
53
53
|
model.parent?.add(clone);
|
@@ -812,11 +812,11 @@
|
|
812
812
|
writer.appendLine( `uniform token[] joints = [${bonesArray}]` );
|
813
813
|
writer.appendLine( `uniform token purpose = "guide"` );
|
814
814
|
writer.appendLine( `uniform matrix4d[] restTransforms = [${rest.map( m => buildMatrix( m ) ).join( ', ' )}]` );
|
815
|
+
|
815
816
|
// In glTF, transformations on the Skeleton are ignored (NODE_SKINNED_MESH_LOCAL_TRANSFORMS validator warning)
|
816
|
-
// So here we
|
817
|
-
writer.appendLine( `matrix4d xformOp:transform = ${buildMatrix( new Matrix4() )}` );
|
818
|
-
// writer.appendLine( `
|
819
|
-
writer.appendLine( `uniform token[] xformOpOrder = ["xformOp:transform"]` );
|
817
|
+
// So here we don't write anything to get an identity transform.
|
818
|
+
// writer.appendLine( `matrix4d xformOp:transform = ${buildMatrix( new Matrix4() )}` );
|
819
|
+
// writer.appendLine( `uniform token[] xformOpOrder = ["xformOp:transform"]` );
|
820
820
|
|
821
821
|
const timeSampleObjects = createAllTimeSampleObjects ( boneAndInverse.map( x => x.bone ) );
|
822
822
|
|
@@ -902,6 +902,7 @@
|
|
902
902
|
this.skinnedMeshExport(writer, _context);
|
903
903
|
|
904
904
|
const object = this.object;
|
905
|
+
const model = this.model;
|
905
906
|
|
906
907
|
// do we have animation data for this node? if not, return
|
907
908
|
const arr = this.dict.get(object);
|
@@ -951,9 +952,6 @@
|
|
951
952
|
useGrouping: false,
|
952
953
|
});
|
953
954
|
|
954
|
-
|
955
|
-
const xFormOrder: Array<"xformOp:translate" | "xformOp:orient" | "xformOp:scale"> = [];
|
956
|
-
|
957
955
|
function writeAnimationTimesamples(arr: TransformData[], type: "position" | "rotation" | "scale") {
|
958
956
|
|
959
957
|
const hasAnimationData = arr.some(x => x && {
|
@@ -967,15 +965,15 @@
|
|
967
965
|
|
968
966
|
switch (type) {
|
969
967
|
case "position":
|
970
|
-
|
968
|
+
model.needsTranslate = true;
|
971
969
|
writer.beginBlock(`double3 xformOp:translate.timeSamples = {`, '');
|
972
970
|
break;
|
973
971
|
case "rotation":
|
974
|
-
|
972
|
+
model.needsOrient = true;
|
975
973
|
writer.beginBlock(`quatf xformOp:orient.timeSamples = {`, '');
|
976
974
|
break;
|
977
975
|
case "scale":
|
978
|
-
|
976
|
+
model.needsScale = true;
|
979
977
|
writer.beginBlock(`double3 xformOp:scale.timeSamples = {`, '');
|
980
978
|
break;
|
981
979
|
}
|
@@ -1027,10 +1025,16 @@
|
|
1027
1025
|
writeAnimationTimesamples(arr, "rotation");
|
1028
1026
|
writeAnimationTimesamples(arr, "scale");
|
1029
1027
|
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1028
|
+
// We must be careful here that we don't overwrite the xformOpOrder that the object already had.
|
1029
|
+
// it _might_ not even have any (if the position/quaternion/scale are all indentity values)
|
1030
|
+
// so in some cases we end up writing the same xformOpOrder multiple times here...
|
1031
|
+
// So right now, we just always write the xformOpOrder when traversing the hierarchy, in case something is animated.
|
1032
|
+
|
1033
|
+
// if (xFormOrder.length > 0) {
|
1034
|
+
// const xformUnique = [...new Set(xFormOrder)];
|
1035
|
+
// writer.appendLine(`uniform token[] xformOpOrder = [${xformUnique.map(x => `"${x}"`).join(', ')}]`);
|
1036
|
+
// writer.appendLine('uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"]');
|
1037
|
+
// }
|
1034
1038
|
|
1035
1039
|
// for (let i = 0; i < arr.length; i++) {
|
1036
1040
|
// const transformData = arr[i];
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import type { IUSDExporterExtension } from "../../Extension.js";
|
4
4
|
import type { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
5
5
|
import { AudioExtension } from "./AudioExtension.js";
|
6
|
-
import type
|
6
|
+
import { ActionModel, type BehaviorModel, GroupActionModel, IBehaviorElement, type Target, TriggerModel } from "./BehavioursBuilder.js";
|
7
7
|
|
8
8
|
const debug = getParam("debugusdzbehaviours");
|
9
9
|
|
@@ -44,6 +44,11 @@
|
|
44
44
|
private behaviourComponentsCopy: Array<UsdzBehaviour> = [];
|
45
45
|
private audioClips: Array<{clipUrl: string, filesKey: string}> = [];
|
46
46
|
private audioClipsCopy: Array<{clipUrl: string, filesKey: string}> = [];
|
47
|
+
private targetUuids: Set<string> = new Set();
|
48
|
+
|
49
|
+
getAllTargetUuids() {
|
50
|
+
return this.targetUuids;
|
51
|
+
}
|
47
52
|
|
48
53
|
onBeforeBuildDocument(context: USDZExporterContext) {
|
49
54
|
if (!context.root) return Promise.resolve();
|
@@ -88,6 +93,58 @@
|
|
88
93
|
this.behaviourComponents.length = 0;
|
89
94
|
this.audioClipsCopy = this.audioClips.slice();
|
90
95
|
this.audioClips.length = 0;
|
96
|
+
|
97
|
+
// We want to know all trigger sources and action targets.
|
98
|
+
// These can be nested in Group Actions.
|
99
|
+
|
100
|
+
const triggerSources = new Set<Target>();
|
101
|
+
const actionTargets = new Set<Target>();
|
102
|
+
const targetUuids = new Set<string>();
|
103
|
+
|
104
|
+
function collect (actionModel: IBehaviorElement) {
|
105
|
+
if (actionModel instanceof GroupActionModel) {
|
106
|
+
for (const action of actionModel.actions) {
|
107
|
+
collect(action);
|
108
|
+
}
|
109
|
+
}
|
110
|
+
else if (actionModel instanceof ActionModel) {
|
111
|
+
const affected = actionModel.affectedObjects;
|
112
|
+
if (affected) {
|
113
|
+
if (typeof affected === "object")
|
114
|
+
actionTargets.add(affected as Target);
|
115
|
+
else if (typeof affected === "string")
|
116
|
+
actionTargets.add({uuid: affected} as any as Target);
|
117
|
+
}
|
118
|
+
|
119
|
+
const xform = actionModel.xFormTarget;
|
120
|
+
if (xform) {
|
121
|
+
if (typeof xform === "object")
|
122
|
+
actionTargets.add(xform as Target);
|
123
|
+
else if (typeof xform === "string")
|
124
|
+
actionTargets.add({uuid: xform} as any as Target);
|
125
|
+
}
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
// collect all targets of all triggers and actions
|
130
|
+
for (const beh of this.behaviours) {
|
131
|
+
if (beh.trigger instanceof TriggerModel && typeof beh.trigger.targetId === "object" )
|
132
|
+
triggerSources.add(beh.trigger.targetId as Target);
|
133
|
+
collect(beh.action);
|
134
|
+
}
|
135
|
+
|
136
|
+
for (const source of new Set([...triggerSources, ...actionTargets])) {
|
137
|
+
// shouldn't happen but strictly speaking a trigger source could be set to an array
|
138
|
+
if (Array.isArray(source)) {
|
139
|
+
for (const s of source)
|
140
|
+
targetUuids.add(s.uuid);
|
141
|
+
}
|
142
|
+
else
|
143
|
+
targetUuids.add(source.uuid);
|
144
|
+
}
|
145
|
+
|
146
|
+
if (debug) console.log("All Behavior trigger sources and action targets", triggerSources, actionTargets, targetUuids);
|
147
|
+
this.targetUuids = new Set(targetUuids);
|
91
148
|
}
|
92
149
|
|
93
150
|
onAfterHierarchy(context: USDZExporterContext, writer: USDWriter) {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Material, Mesh, Object3D, Quaternion, Vector3 } from "three";
|
1
|
+
import { Material, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { isDevEnvironment, showBalloonWarning } from "../../../../../engine/debug/index.js";
|
4
4
|
import { serializable } from "../../../../../engine/engine_serialization_decorator.js";
|
@@ -334,7 +334,7 @@
|
|
334
334
|
variant.displayName = variant.displayName + ": Variant with material " + this.variantMaterial.name;
|
335
335
|
variant.material = this.variantMaterial;
|
336
336
|
variant.geometry = target.geometry;
|
337
|
-
variant.
|
337
|
+
variant.transform = target.transform;
|
338
338
|
|
339
339
|
if (!target.parent || !target.parent.isEmpty()) {
|
340
340
|
USDObject.createEmptyParent(target);
|
@@ -471,7 +471,7 @@
|
|
471
471
|
// We create clones exactly once for this gameObject, so that all SetActiveOnClick on the same object use the same trigger.
|
472
472
|
if (!this.gameObject[SetActiveOnClick.toggleClone]) {
|
473
473
|
const clone = this.selfModelClone.clone();
|
474
|
-
clone.
|
474
|
+
clone.setMatrix(new Matrix4());
|
475
475
|
clone.name += "_toggle" + (SetActiveOnClick.clonedToggleIndex++);
|
476
476
|
originalModel.add(clone);
|
477
477
|
this.gameObject[SetActiveOnClick.toggleClone] = clone;
|
@@ -482,7 +482,7 @@
|
|
482
482
|
|
483
483
|
if (!this.gameObject[SetActiveOnClick.reverseToggleClone]) {
|
484
484
|
const clone = this.selfModelClone.clone();
|
485
|
-
clone.
|
485
|
+
clone.setMatrix(new Matrix4());
|
486
486
|
clone.name += "_toggleReverse" + (SetActiveOnClick.clonedToggleIndex++);
|
487
487
|
originalModel.add(clone);
|
488
488
|
this.gameObject[SetActiveOnClick.reverseToggleClone] = clone;
|
@@ -91,7 +91,7 @@
|
|
91
91
|
}
|
92
92
|
|
93
93
|
|
94
|
-
type Target = USDObject | USDObject[] | Object3D | Object3D[];
|
94
|
+
export type Target = USDObject | USDObject[] | Object3D | Object3D[];
|
95
95
|
|
96
96
|
const addedStrings = new Set<string>();
|
97
97
|
/** called to resolve target objects to usdz paths */
|
@@ -134,11 +134,16 @@
|
|
134
134
|
addedStrings.clear();
|
135
135
|
}
|
136
136
|
else if (typeof targetObject === "object") {
|
137
|
+
const sourceObject = targetObject;
|
137
138
|
//@ts-ignore
|
138
|
-
if (
|
139
|
+
if (sourceObject.isObject3D) {
|
139
140
|
//@ts-ignore
|
140
|
-
targetObject = document.findById(
|
141
|
+
targetObject = document.findById(sourceObject.uuid);
|
141
142
|
}
|
143
|
+
if (!targetObject) {
|
144
|
+
console.error("Invalid target object in behavior, the target object is likely missing from USDZ export. Is the object exported?", sourceObject);
|
145
|
+
throw new Error(`Invalid target object in behavior, the target object is likely missing from USDZ export. Please report a bug. uuid: ${sourceObject.uuid}.`);
|
146
|
+
}
|
142
147
|
result = (targetObject as any).getPath?.call(targetObject) as string;
|
143
148
|
}
|
144
149
|
|
@@ -349,8 +349,8 @@
|
|
349
349
|
* const myComponents = findObjectsOfType(MyComponent);
|
350
350
|
* ```
|
351
351
|
*/
|
352
|
-
export function findObjectsOfType<T extends IComponent>(type: Constructor<T>, array
|
353
|
-
if (!type) return array;
|
352
|
+
export function findObjectsOfType<T extends IComponent>(type: Constructor<T>, array?: T[], contextOrScene: undefined | Object3D | { scene: Scene } = undefined): T[] {
|
353
|
+
if (!type) return array ?? [];
|
354
354
|
if (!array) array = [];
|
355
355
|
array.length = 0;
|
356
356
|
|
@@ -134,7 +134,7 @@
|
|
134
134
|
this._used = true;
|
135
135
|
}
|
136
136
|
|
137
|
-
/** Unique identifier for this input: a combination of the deviceIndex + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 =
|
137
|
+
/** Unique identifier for this input: a combination of the deviceIndex + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 11) */
|
138
138
|
override get pointerId(): number { return this._pointerid; }
|
139
139
|
private readonly _pointerid;
|
140
140
|
|
@@ -419,6 +419,11 @@
|
|
419
419
|
return count;
|
420
420
|
}
|
421
421
|
|
422
|
+
/**
|
423
|
+
* Gets the position of the given pointer index in pixel
|
424
|
+
* @param i The pointer index
|
425
|
+
* @returns The position of the pointer in pixel
|
426
|
+
*/
|
422
427
|
getPointerPosition(i: number): Vector2 | null {
|
423
428
|
if (i >= this._pointerPositions.length) return null;
|
424
429
|
return this._pointerPositions[i];
|
@@ -5,23 +5,11 @@
|
|
5
5
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
6
6
|
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
7
7
|
|
8
|
-
import { isDevEnvironment } from './debug/index.js';
|
9
8
|
import { Context } from "./engine_setup.js"
|
10
9
|
import { getParam } from "./engine_utils.js";
|
11
10
|
|
12
11
|
const debug = getParam("debugdecoders");
|
13
12
|
|
14
|
-
// NOTE: keep in sync with gltf-progressive
|
15
|
-
let DEFAULT_DRACO_DECODER_LOCATION = 'https://www.gstatic.com/draco/versioned/decoders/1.5.7/';
|
16
|
-
let DEFAULT_KTX2_TRANSCODER_LOCATION = 'https://www.gstatic.com/basis-universal/versioned/2021-04-15-ba1c3e4/';
|
17
|
-
fetch(DEFAULT_DRACO_DECODER_LOCATION + "draco_decoder.js", { method: "head" })
|
18
|
-
.catch(_ => {
|
19
|
-
if (isDevEnvironment()) console.warn("Failed to load draco decoder from \"" + DEFAULT_DRACO_DECODER_LOCATION + "\".\nFalling back to local version at \"./include/draco\"");
|
20
|
-
DEFAULT_DRACO_DECODER_LOCATION = "./include/draco/";
|
21
|
-
DEFAULT_KTX2_TRANSCODER_LOCATION = "./include/ktx2/";
|
22
|
-
});
|
23
|
-
|
24
|
-
|
25
13
|
let meshoptDecoder: typeof MeshoptDecoder;
|
26
14
|
|
27
15
|
let loaders: null | { dracoLoader: DRACOLoader, ktx2Loader: KTX2Loader, meshoptDecoder: typeof MeshoptDecoder } = null;
|
@@ -36,10 +24,10 @@
|
|
36
24
|
|
37
25
|
export function setDracoDecoderPath(path: string | undefined) {
|
38
26
|
if (path !== undefined && typeof path === "string") {
|
27
|
+
setDracoDecoderLocation(path);
|
39
28
|
const loaders = ensureLoaders();
|
40
29
|
if (debug) console.log("Setting draco decoder path to", path);
|
41
30
|
loaders.dracoLoader.setDecoderPath(path);
|
42
|
-
setDracoDecoderLocation(path);
|
43
31
|
}
|
44
32
|
}
|
45
33
|
|
@@ -53,10 +41,10 @@
|
|
53
41
|
|
54
42
|
export function setKtx2TranscoderPath(path: string) {
|
55
43
|
if (path !== undefined && typeof path === "string") {
|
44
|
+
setKTX2TranscoderLocation(path);
|
56
45
|
const loaders = ensureLoaders();
|
57
46
|
if (debug) console.log("Setting ktx2 transcoder path to", path);
|
58
47
|
loaders.ktx2Loader.setTranscoderPath(path);
|
59
|
-
setKTX2TranscoderLocation(path);
|
60
48
|
}
|
61
49
|
}
|
62
50
|
|
@@ -167,13 +167,13 @@
|
|
167
167
|
this.validate();
|
168
168
|
const body = this.internal_getRigidbody(rigidbody);
|
169
169
|
if (body) body.addForce(force, wakeup)
|
170
|
-
else console.warn("Rigidbody doesn't exist: can not apply force");
|
170
|
+
else console.warn("Rigidbody doesn't exist: can not apply force (does your object with the Rigidbody have a collider?)");
|
171
171
|
}
|
172
172
|
addImpulse(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
|
173
173
|
this.validate();
|
174
174
|
const body = this.internal_getRigidbody(rigidbody);
|
175
175
|
if (body) body.applyImpulse(force, wakeup);
|
176
|
-
else console.warn("Rigidbody doesn't exist: can not apply impulse");
|
176
|
+
else console.warn("Rigidbody doesn't exist: can not apply impulse (does your object with the Rigidbody have a collider?)");
|
177
177
|
}
|
178
178
|
getLinearVelocity(comp: IRigidbody | ICollider): Vec3 | null {
|
179
179
|
this.validate();
|
@@ -182,6 +182,7 @@
|
|
182
182
|
const vel = body.linvel();
|
183
183
|
return vel;
|
184
184
|
}
|
185
|
+
// else console.warn("Rigidbody doesn't exist: can not get linear velocity (does your object with the Rigidbody have a collider?)");
|
185
186
|
return null;
|
186
187
|
}
|
187
188
|
getAngularVelocity(rb: IRigidbody): Vec3 | null {
|
@@ -191,6 +192,7 @@
|
|
191
192
|
const vel = body.angvel();
|
192
193
|
return vel;
|
193
194
|
}
|
195
|
+
// else console.warn("Rigidbody doesn't exist: can not get angular velocity (does your object with the Rigidbody have a collider?)");
|
194
196
|
return null;
|
195
197
|
}
|
196
198
|
resetForces(rb: IRigidbody, wakeup: boolean) {
|
@@ -207,14 +209,14 @@
|
|
207
209
|
this.validate();
|
208
210
|
const body = this.internal_getRigidbody(rb);
|
209
211
|
if (body) body.applyImpulse(vec, wakeup);
|
210
|
-
else console.warn("Rigidbody doesn't exist: can not apply impulse");
|
212
|
+
else console.warn("Rigidbody doesn't exist: can not apply impulse (does your object with the Rigidbody have a collider?)");
|
211
213
|
}
|
212
214
|
|
213
215
|
wakeup(rb: IRigidbody) {
|
214
216
|
this.validate();
|
215
217
|
const body = this.internal_getRigidbody(rb);
|
216
218
|
if (body) body.wakeUp();
|
217
|
-
else console.warn("Rigidbody doesn't exist: can not wake up");
|
219
|
+
else console.warn("Rigidbody doesn't exist: can not wake up (does your object with the Rigidbody have a collider?)");
|
218
220
|
}
|
219
221
|
isSleeping(rb: IRigidbody) {
|
220
222
|
this.validate();
|
@@ -225,13 +227,13 @@
|
|
225
227
|
this.validate();
|
226
228
|
const body = this.internal_getRigidbody(rb);
|
227
229
|
if (body) body.setAngvel(vec, wakeup);
|
228
|
-
else console.warn("Rigidbody doesn't exist: can not set angular velocity");
|
230
|
+
else console.warn("Rigidbody doesn't exist: can not set angular velocity (does your object with the Rigidbody have a collider?)");
|
229
231
|
}
|
230
232
|
setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
|
231
233
|
this.validate();
|
232
234
|
const body = this.internal_getRigidbody(rb);
|
233
235
|
if (body) body.setLinvel(vec, wakeup);
|
234
|
-
else console.warn("Rigidbody doesn't exist: can not set linear velocity");
|
236
|
+
else console.warn("Rigidbody doesn't exist: can not set linear velocity (does your object with the Rigidbody have a collider?)");
|
235
237
|
}
|
236
238
|
|
237
239
|
private context?: IContext;
|
@@ -4,6 +4,7 @@
|
|
4
4
|
|
5
5
|
import { useForAutoFit } from "./engine_camera.js";
|
6
6
|
import { Mathf } from "./engine_math.js"
|
7
|
+
import { Vec3 } from "./engine_types.js";
|
7
8
|
import { CircularBuffer } from "./engine_utils.js";
|
8
9
|
|
9
10
|
/**
|
@@ -91,10 +92,11 @@
|
|
91
92
|
export function getTempVector(): Vector3;
|
92
93
|
export function getTempVector(vec3: Vector3): Vector3;
|
93
94
|
export function getTempVector(vec3: [number, number, number]): Vector3;
|
95
|
+
export function getTempVector(vec3: Vec3): Vector3;
|
94
96
|
export function getTempVector(dom: DOMPointReadOnly): Vector3;
|
95
97
|
export function getTempVector(x: number): Vector3;
|
96
98
|
export function getTempVector(x: number, y: number, z: number): Vector3;
|
97
|
-
export function getTempVector(vecOrX?: Vector3 | [number, number, number] | DOMPointReadOnly | number, y?: number, z?: number): Vector3 {
|
99
|
+
export function getTempVector(vecOrX?: Vec3 | Vector3 | [number, number, number] | DOMPointReadOnly | number, y?: number, z?: number): Vector3 {
|
98
100
|
const vec = _tempVecs.get();
|
99
101
|
vec.set(0, 0, 0); // initialize with default values
|
100
102
|
if (vecOrX instanceof Vector3) vec.copy(vecOrX);
|
@@ -106,6 +108,11 @@
|
|
106
108
|
vec.y = y !== undefined ? y : vec.x;
|
107
109
|
vec.z = z !== undefined ? z : vec.x;
|
108
110
|
}
|
111
|
+
else if (typeof vecOrX === "object") {
|
112
|
+
vec.x = vecOrX.x;
|
113
|
+
vec.y = vecOrX.y;
|
114
|
+
vec.z = vecOrX.z;
|
115
|
+
}
|
109
116
|
}
|
110
117
|
return vec;
|
111
118
|
}
|
@@ -523,10 +523,13 @@
|
|
523
523
|
* https://w3c.github.io/gamepad/#remapping
|
524
524
|
*/
|
525
525
|
export type GamepadButtonName = "a-button" | "b-button" | "x-button" | "y-button";
|
526
|
+
|
527
|
+
/** Needle-defined names for stylus (MX Ink) */
|
528
|
+
export type StylusButtonName = "stylus-touch" | "stylus-tip";
|
529
|
+
|
526
530
|
/** Button names as used in the xr profile */
|
531
|
+
export type XRControllerButtonName = "thumbrest" | "xr-standard-trigger" | "xr-standard-squeeze" | "xr-standard-thumbstick" | "xr-standard-touchpad" | "menu" | GamepadButtonName | StylusButtonName;
|
527
532
|
|
528
|
-
export type XRControllerButtonName = "thumbrest" | "xr-standard-trigger" | "xr-standard-squeeze" | "xr-standard-thumbstick" | "xr-standard-touchpad" | "menu" | GamepadButtonName;
|
529
|
-
|
530
533
|
export type XRGestureName = "pinch";
|
531
534
|
|
532
535
|
/** All known (types) button names for various devices and cases combined. You should use the device specific names if you e.g. know you only deal with a mouse use MouseButtonName */
|
@@ -93,7 +93,7 @@
|
|
93
93
|
if (this.raycaster.length <= 0) {
|
94
94
|
const res = GameObject.findObjectOfType(Raycaster, this.context);
|
95
95
|
if (!res) {
|
96
|
-
const rc = GameObject.
|
96
|
+
const rc = GameObject.addComponent(this.context.scene, ObjectRaycaster);
|
97
97
|
this.raycaster.push(rc);
|
98
98
|
if (isDevEnvironment() || debug)
|
99
99
|
console.warn("Added an ObjectRaycaster to the scene because no raycaster was found.");
|
@@ -70,8 +70,8 @@
|
|
70
70
|
// rotate by 90° - counter-rotation on the parent makes sure
|
71
71
|
// that without Preliminary Behaviours it still looks right
|
72
72
|
const flip = this.invertForward ? -1 : 1;
|
73
|
-
parent.
|
74
|
-
model.
|
73
|
+
parent.setMatrix(parent.getMatrix().multiply(new Matrix4().makeRotationZ(Math.PI / 2 * flip)));
|
74
|
+
model.setMatrix(model.getMatrix().multiply(new Matrix4().makeRotationZ(-Math.PI / 2 * flip)));
|
75
75
|
}
|
76
76
|
|
77
77
|
const lookAt = new BehaviorModel("lookat " + this.name,
|
@@ -62,6 +62,18 @@
|
|
62
62
|
this.menu?.removeFromParent();
|
63
63
|
}
|
64
64
|
|
65
|
+
private userRequestedMenu = false;
|
66
|
+
/** Bring up the spatial menu. This is typically invoked from a button click.
|
67
|
+
* The menu will show at a lower height to be easily accessible.
|
68
|
+
* @returns true if the menu was shown, false if it can't be shown because the menu has been disabled.
|
69
|
+
*/
|
70
|
+
setDisplay(display: boolean) {
|
71
|
+
if (!this.enabled) return false;
|
72
|
+
|
73
|
+
this.userRequestedMenu = display;
|
74
|
+
return true;
|
75
|
+
}
|
76
|
+
|
65
77
|
onDestroy() {
|
66
78
|
const index = this._context.pre_render_callbacks.indexOf(this.preRender);
|
67
79
|
if (index > -1) {
|
@@ -156,7 +168,7 @@
|
|
156
168
|
|
157
169
|
const showMenuThreshold = fwd.y > .6;
|
158
170
|
const hideMenuThreshold = fwd.y > .4;
|
159
|
-
const newVisibleState = menu.visible ? hideMenuThreshold : showMenuThreshold;
|
171
|
+
const newVisibleState = (menu.visible ? hideMenuThreshold : showMenuThreshold) || this.userRequestedMenu;
|
160
172
|
const becomesVisible = !menu.visible && newVisibleState;
|
161
173
|
menu.visible = newVisibleState || (isDesktop() && debug as boolean);
|
162
174
|
|
@@ -167,7 +179,7 @@
|
|
167
179
|
|
168
180
|
if (becomesVisible || testBecomesVisible) {
|
169
181
|
menu.position.copy(this._menuTarget.position);
|
170
|
-
menu.position.y +=
|
182
|
+
menu.position.y += 0.25;
|
171
183
|
this._menuTarget.position.copy(menu.position);
|
172
184
|
this.positionFilter.reset(menu.position);
|
173
185
|
menu.quaternion.copy(this._menuTarget.quaternion);
|
@@ -178,7 +190,7 @@
|
|
178
190
|
this.ensureRenderOnTop(this.menu as any as Object3D);
|
179
191
|
this._menuTarget.position.copy(menuTargetPosition);
|
180
192
|
this._context.scene.add(this._menuTarget);
|
181
|
-
lookAtObject(this._menuTarget, this._context.mainCamera!,
|
193
|
+
lookAtObject(this._menuTarget, this._context.mainCamera!, true, true);
|
182
194
|
this._menuTarget.removeFromParent();
|
183
195
|
}
|
184
196
|
this.positionFilter.filter(this._menuTarget.position, menu.position, this._context.time.time);
|
@@ -212,6 +224,10 @@
|
|
212
224
|
private familyName = "Needle Spatial Menu";
|
213
225
|
private menu?: ThreeMeshUI.Block;
|
214
226
|
|
227
|
+
get isVisible() {
|
228
|
+
return this.menu?.visible;
|
229
|
+
}
|
230
|
+
|
215
231
|
private getMenu() {
|
216
232
|
if (this.menu) {
|
217
233
|
return this.menu;
|
@@ -178,6 +178,14 @@
|
|
178
178
|
this._spatialMenu.setEnabled(enabled);
|
179
179
|
}
|
180
180
|
|
181
|
+
setSpatialMenuVisible(display: boolean) {
|
182
|
+
this._spatialMenu.setDisplay(display);
|
183
|
+
}
|
184
|
+
|
185
|
+
get spatialMenuIsVisible() {
|
186
|
+
return this._spatialMenu.isVisible;
|
187
|
+
}
|
188
|
+
|
181
189
|
/**
|
182
190
|
* Call to add or remove a button to the menu to show a QR code for the current page
|
183
191
|
* If enabled=true then a button will be added to the menu that will show a QR code for the current page when clicked.
|
@@ -134,6 +134,9 @@
|
|
134
134
|
/** is left side. shorthand for `side === 'left'` */
|
135
135
|
get isLeft() { return this.side === 'left'; }
|
136
136
|
|
137
|
+
/** is XR stylus, e.g. Logitech MX Ink */
|
138
|
+
get isStylus() { return this._isMxInk; }
|
139
|
+
|
137
140
|
/** The XRTransientInputHitTestSource can be used to perform hit tests with the controller ray against the real world.
|
138
141
|
* see https://developer.mozilla.org/en-US/docs/Web/API/XRSession/requestHitTestSourceForTransientInput for more information
|
139
142
|
* Requires the hit-test feature to be enabled in the XRSession
|
@@ -158,6 +161,7 @@
|
|
158
161
|
private _hasSelectEvent = false;
|
159
162
|
get hasSelectEvent() { return this._hasSelectEvent; }
|
160
163
|
private _isMxInk = false;
|
164
|
+
private _isMetaQuestTouchController = false;
|
161
165
|
|
162
166
|
/** Perform a hit test against the XR planes or meshes. shorthand for `xr.getHitTest(controller)`
|
163
167
|
* @returns the hit test result (with position and rotation in worldspace) or null if no hit was found
|
@@ -736,8 +740,15 @@
|
|
736
740
|
private initialize() {
|
737
741
|
// WORKAROUND for hand controllers that don't have a select event
|
738
742
|
this._hasSelectEvent = this.profiles.includes("generic-hand-select") || this.profiles.some(p => p.startsWith("generic-trigger"));
|
739
|
-
|
740
|
-
|
743
|
+
|
744
|
+
// Used to determine special layout for Quest controllers, e.g. last button is menu button
|
745
|
+
this._isMetaQuestTouchController = this.profiles.includes("meta-quest-touch-plus") || this.profiles.includes("oculus-touch-v3");
|
746
|
+
|
747
|
+
this._isMxInk =
|
748
|
+
// Proper profile starting with v69 and browser 35.1
|
749
|
+
this.profiles.includes("logitech-mx-ink") ||
|
750
|
+
// Workaround for Logitech MX Ink not having a proper profile on QuestOS v68
|
751
|
+
this.profiles.includes("meta-quest-touch-plus") && this.inputSource.gamepad?.buttons.length === 7;
|
741
752
|
|
742
753
|
if (!this._layout) {
|
743
754
|
// Ignore transient-pointer since we likely don't want to spawn a controller visual just for a temporary pointer.
|
@@ -893,36 +904,76 @@
|
|
893
904
|
const state = this.states[index] || new InputState();
|
894
905
|
let eventName: InputEventNames | null = null;
|
895
906
|
|
896
|
-
//
|
897
|
-
|
898
|
-
|
899
|
-
state.
|
900
|
-
|
907
|
+
// Special handling for MX Ink stylus on Quest OS v69+.
|
908
|
+
// We're never getting a "pressed" state here, so we determine pressed state based on the value.
|
909
|
+
if (this._isMxInk && (index === 4 || index === 5)) {
|
910
|
+
if (button.value > 0 && !state.pressed) {
|
911
|
+
eventName = "pointerdown";
|
912
|
+
state.isDown = true;
|
913
|
+
state.isUp = false;
|
914
|
+
}
|
915
|
+
else if (button.value === 0 && state.pressed) {
|
916
|
+
eventName = "pointerup";
|
917
|
+
state.isDown = false;
|
918
|
+
state.isUp = true;
|
919
|
+
}
|
920
|
+
else if (state.pressed) {
|
921
|
+
eventName = "pointermove";
|
922
|
+
state.isDown = false;
|
923
|
+
state.isUp = false;
|
924
|
+
}
|
925
|
+
state.pressed = button.value > 0;
|
926
|
+
state.value = button.value;
|
901
927
|
}
|
902
|
-
//
|
903
|
-
else if (!button.pressed && state.pressed) {
|
904
|
-
eventName = "pointerup"
|
905
|
-
state.isDown = false;
|
906
|
-
state.isUp = true;
|
907
|
-
}
|
928
|
+
// Regular controller handling.
|
908
929
|
else {
|
909
|
-
|
910
|
-
state.
|
930
|
+
// is down
|
931
|
+
if (button.pressed && !state.pressed) {
|
932
|
+
eventName = "pointerdown";
|
933
|
+
state.isDown = true;
|
934
|
+
state.isUp = false;
|
935
|
+
}
|
936
|
+
// is up
|
937
|
+
else if (!button.pressed && state.pressed) {
|
938
|
+
eventName = "pointerup"
|
939
|
+
state.isDown = false;
|
940
|
+
state.isUp = true;
|
941
|
+
}
|
942
|
+
else {
|
943
|
+
state.isDown = false;
|
944
|
+
state.isUp = false;
|
945
|
+
}
|
946
|
+
state.pressed = button.pressed;
|
947
|
+
state.value = button.value;
|
911
948
|
}
|
912
|
-
|
913
|
-
state.value = button.value;
|
914
|
-
state.pressed = button.pressed;
|
915
949
|
this.states[index] = state;
|
916
950
|
|
917
951
|
// the selection event is handled in the "selectstart" callback
|
918
952
|
const emitEvent = index !== this._selectButtonIndex && index !== this._squeezeButtonIndex;
|
919
953
|
|
920
954
|
if (eventName != null && emitEvent) {
|
921
|
-
|
955
|
+
let name = this._layout?.gamepad[index];
|
956
|
+
if (this._isMxInk && index === 4) name = "stylus-touch";
|
957
|
+
if (this._isMxInk && index === 5) name = "stylus-tip";
|
922
958
|
if (debug || debugCustomGesture) console.log("Emitting pointer event", eventName, index, name, button.value, this.gamepad, this._layout);
|
923
959
|
this.emitPointerEvent(eventName, index, name ?? "none", false, null, button.value);
|
924
960
|
}
|
925
961
|
}
|
962
|
+
|
963
|
+
// For Quest controllers, the last button is the menu button
|
964
|
+
if (this._isMetaQuestTouchController) {
|
965
|
+
const menuButtonIndex = this.gamepad.buttons.length - 1;
|
966
|
+
const menuButtonState = this.states[menuButtonIndex];
|
967
|
+
if (menuButtonState) {
|
968
|
+
if (menuButtonState.isDown) {
|
969
|
+
const menu = this.context.menu;
|
970
|
+
if (menu.spatialMenuIsVisible)
|
971
|
+
menu.setSpatialMenuVisible(false);
|
972
|
+
else
|
973
|
+
this.context.menu.setSpatialMenuVisible(true);
|
974
|
+
}
|
975
|
+
}
|
976
|
+
}
|
926
977
|
}
|
927
978
|
|
928
979
|
// update hand gesture states
|
@@ -373,7 +373,8 @@
|
|
373
373
|
// handle iOS platform where "immersive-ar" is not supported
|
374
374
|
// TODO: should we add a separate mode (e.g. "AR")? https://linear.app/needle/issue/NE-5303
|
375
375
|
if (mode === "immersive-ar" && isiOS()) {
|
376
|
-
|
376
|
+
const arSupported = await this.isARSupported();
|
377
|
+
if (!arSupported && InternalUSDZRegistry.exportAndOpen()) {
|
377
378
|
return null;
|
378
379
|
}
|
379
380
|
}
|
@@ -78,6 +78,7 @@
|
|
78
78
|
private _manager: IPostProcessingManager | null = null;
|
79
79
|
|
80
80
|
onEnable(): void {
|
81
|
+
super.onEnable();
|
81
82
|
this.onEffectEnabled();
|
82
83
|
// Dont override the serialized value by enabling (we could also just disable this component / map enabled to active)
|
83
84
|
if (this.__internalDidAwakeAndStart)
|
@@ -85,6 +86,7 @@
|
|
85
86
|
}
|
86
87
|
|
87
88
|
onDisable(): void {
|
89
|
+
super.onDisable();
|
88
90
|
this._manager?.removeEffect(this);
|
89
91
|
this.active = false;
|
90
92
|
}
|
@@ -91,7 +91,7 @@
|
|
91
91
|
|
92
92
|
context[activeKey] = this;
|
93
93
|
|
94
|
-
if (debug) console.log("
|
94
|
+
if (debug) console.log("Apply Postprocessing Effects", components);
|
95
95
|
|
96
96
|
this._lastVolumeComponents = [...components];
|
97
97
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { Matrix4, Object3D, Quaternion, Vector3, Vector3Like } from "three";
|
2
2
|
|
3
|
+
import { isDevEnvironment } from "../engine/debug/index.js";
|
3
4
|
import { CollisionDetectionMode, RigidbodyConstraints } from "../engine/engine_physics.types.js";
|
4
5
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
6
|
import { Context, FrameEvent } from "../engine/engine_setup.js";
|
@@ -313,6 +314,9 @@
|
|
313
314
|
}
|
314
315
|
this._watch.start(true, true);
|
315
316
|
this.startCoroutine(this.beforePhysics(), FrameEvent.LateUpdate);
|
317
|
+
if (isDevEnvironment() && !this.context.physics.engine?.getBody(this)) {
|
318
|
+
console.warn(`Rigidbody was not created: Does your object (${this.name}) have a collider?`);
|
319
|
+
}
|
316
320
|
}
|
317
321
|
|
318
322
|
onDisable() {
|
@@ -65,12 +65,13 @@
|
|
65
65
|
**/
|
66
66
|
export interface ISceneEventListener {
|
67
67
|
/** Called when the scene is loaded and added */
|
68
|
-
sceneOpened
|
68
|
+
sceneOpened(sceneSwitcher: SceneSwitcher): Promise<void>
|
69
69
|
/** Called before the scene is being removed (due to another scene being loaded) */
|
70
|
-
sceneClosing
|
70
|
+
sceneClosing(): Promise<void>
|
71
71
|
}
|
72
72
|
|
73
73
|
|
74
|
+
|
74
75
|
/** The SceneSwitcher can be used to dynamically load and unload extra content
|
75
76
|
* Available scenes are defined in the `scenes` array.
|
76
77
|
* Loaded scenes will be added to the SceneSwitcher's GameObject as a child and removed when another scene is loaded by the same SceneSwitcher.
|
@@ -783,7 +784,11 @@
|
|
783
784
|
}
|
784
785
|
|
785
786
|
private tryGetSceneEventListener(obj: Object3D, level: number = 0): ISceneEventListener | null {
|
786
|
-
const sceneListener = GameObject.foreachComponent(obj, c =>
|
787
|
+
const sceneListener = GameObject.foreachComponent(obj, c => {
|
788
|
+
const i = c as any as ISceneEventListener;
|
789
|
+
if (i.sceneClosing! || i.sceneOpened!) return i;
|
790
|
+
else return undefined;
|
791
|
+
});
|
787
792
|
// if we didnt find any component with the listener on the root object
|
788
793
|
// we also check the first level of its children because a scene might be a group
|
789
794
|
if (level === 0 && !sceneListener && obj.children.length) {
|
@@ -7,6 +7,7 @@
|
|
7
7
|
BufferGeometry,
|
8
8
|
Color,
|
9
9
|
DoubleSide,
|
10
|
+
InterleavedBufferAttribute,
|
10
11
|
LinearFilter,
|
11
12
|
Material,
|
12
13
|
MathUtils,
|
@@ -19,6 +20,7 @@
|
|
19
20
|
OrthographicCamera,
|
20
21
|
PerspectiveCamera,
|
21
22
|
PlaneGeometry,
|
23
|
+
Quaternion,
|
22
24
|
RGBAFormat,
|
23
25
|
Scene,
|
24
26
|
ShaderMaterial,
|
@@ -27,6 +29,7 @@
|
|
27
29
|
Texture,
|
28
30
|
Uniform,
|
29
31
|
UnsignedByteType,
|
32
|
+
Vector3,
|
30
33
|
Vector4,
|
31
34
|
WebGLRenderer,
|
32
35
|
WebGLRenderTarget} from 'three';
|
@@ -89,6 +92,16 @@
|
|
89
92
|
return structuralNodes;
|
90
93
|
}
|
91
94
|
|
95
|
+
declare type USDObjectTransform = {
|
96
|
+
position: Vector3 | null;
|
97
|
+
quaternion: Quaternion | null;
|
98
|
+
scale: Vector3 | null;
|
99
|
+
}
|
100
|
+
|
101
|
+
const PositionIdentity = new Vector3();
|
102
|
+
const QuaternionIdentity = new Quaternion();
|
103
|
+
const ScaleIdentity = new Vector3(1,1,1);
|
104
|
+
|
92
105
|
class USDObject {
|
93
106
|
|
94
107
|
static USDObject_export_id = 0;
|
@@ -101,7 +114,30 @@
|
|
101
114
|
extraSchemas: string[] = [];
|
102
115
|
displayName?: string;
|
103
116
|
visibility?: "inherited" | "invisible"; // defaults to "inherited" in USD
|
104
|
-
|
117
|
+
getMatrix(): Matrix4 {
|
118
|
+
if (!this.transform) return new Matrix4();
|
119
|
+
const { position, quaternion, scale } = this.transform;
|
120
|
+
const matrix = new Matrix4();
|
121
|
+
matrix.compose(position || PositionIdentity, quaternion || QuaternionIdentity, scale || ScaleIdentity);
|
122
|
+
return matrix;
|
123
|
+
}
|
124
|
+
setMatrix( value ) {
|
125
|
+
if (!value || !(value instanceof Matrix4)) {
|
126
|
+
this.transform = null;
|
127
|
+
return;
|
128
|
+
}
|
129
|
+
const position = new Vector3();
|
130
|
+
const quaternion = new Quaternion();
|
131
|
+
const scale = new Vector3();
|
132
|
+
value.decompose(position, quaternion, scale);
|
133
|
+
this.transform = { position, quaternion, scale };
|
134
|
+
}
|
135
|
+
/** @deprecated Use `transform`, or `getMatrix()` if you really need the matrix */
|
136
|
+
get matrix() { return this.getMatrix(); }
|
137
|
+
/** @deprecated Use `transform`, or `setMatrix()` if you really need the matrix */
|
138
|
+
set matrix( value ) { this.setMatrix( value ); }
|
139
|
+
|
140
|
+
transform: USDObjectTransform | null = null;
|
105
141
|
private _isDynamic: boolean;
|
106
142
|
get isDynamic() { return this._isDynamic; }
|
107
143
|
private set isDynamic( value ) { this._isDynamic = value; }
|
@@ -114,31 +150,42 @@
|
|
114
150
|
animations: AnimationClip[] | null;
|
115
151
|
_eventListeners: {};
|
116
152
|
|
153
|
+
// these are for tracking which xformops are needed
|
154
|
+
needsTranslate: boolean = false;
|
155
|
+
needsOrient: boolean = false;
|
156
|
+
needsScale: boolean = false;
|
157
|
+
|
117
158
|
static createEmptyParent( object: USDObject ) {
|
118
159
|
|
119
|
-
const emptyParent = new USDObject( MathUtils.generateUUID(), object.name + '_empty_' + ( USDObject.USDObject_export_id ++ ), object.
|
160
|
+
const emptyParent = new USDObject( MathUtils.generateUUID(), object.name + '_empty_' + ( USDObject.USDObject_export_id ++ ), object.transform );
|
120
161
|
const parent = object.parent;
|
121
162
|
if (parent) parent.add( emptyParent );
|
122
163
|
emptyParent.add( object );
|
123
164
|
emptyParent.isDynamic = true;
|
124
|
-
object.
|
165
|
+
object.transform = null;
|
125
166
|
return emptyParent;
|
126
167
|
|
127
168
|
}
|
128
169
|
|
129
170
|
static createEmpty() {
|
130
171
|
|
131
|
-
const empty = new USDObject( MathUtils.generateUUID(), 'Empty_' + ( USDObject.USDObject_export_id ++ )
|
172
|
+
const empty = new USDObject( MathUtils.generateUUID(), 'Empty_' + ( USDObject.USDObject_export_id ++ ) );
|
132
173
|
empty.isDynamic = true;
|
133
174
|
return empty;
|
134
175
|
}
|
135
176
|
|
136
|
-
constructor( id, name,
|
177
|
+
constructor( id, name, transform: USDObjectTransform | null = null, mesh: BufferGeometry | null = null, material: MeshStandardMaterial | MeshBasicMaterial | Material | null = null, camera: PerspectiveCamera | OrthographicCamera | null = null, skinnedMesh: SkinnedMesh | null = null, animations: AnimationClip[] | null = null ) {
|
137
178
|
|
138
179
|
this.uuid = id;
|
139
180
|
this.name = makeNameSafe( name );
|
140
181
|
this.displayName = name;
|
141
|
-
|
182
|
+
|
183
|
+
if (!transform) this.transform = null;
|
184
|
+
else this.transform = {
|
185
|
+
position: transform.position?.clone() || null,
|
186
|
+
quaternion: transform.quaternion?.clone() || null,
|
187
|
+
scale: transform.scale?.clone() || null
|
188
|
+
};
|
142
189
|
this.geometry = mesh;
|
143
190
|
this.material = material;
|
144
191
|
this.camera = camera;
|
@@ -166,7 +213,7 @@
|
|
166
213
|
|
167
214
|
clone() {
|
168
215
|
|
169
|
-
const clone = new USDObject( MathUtils.generateUUID(), this.name, this.
|
216
|
+
const clone = new USDObject( MathUtils.generateUUID(), this.name, this.transform, this.geometry, this.material );
|
170
217
|
clone.isDynamic = this.isDynamic;
|
171
218
|
return clone;
|
172
219
|
|
@@ -276,7 +323,7 @@
|
|
276
323
|
|
277
324
|
constructor() {
|
278
325
|
|
279
|
-
super(undefined, 'StageRoot',
|
326
|
+
super(undefined, 'StageRoot', null, null, null, null);
|
280
327
|
this.children = [];
|
281
328
|
this.stageLength = 200;
|
282
329
|
|
@@ -320,9 +367,9 @@
|
|
320
367
|
findById( uuid: string ) {
|
321
368
|
|
322
369
|
let found = false;
|
323
|
-
function search( current: USDObject ) {
|
370
|
+
function search( current: USDObject ): USDObject | undefined {
|
324
371
|
|
325
|
-
if ( found ) return;
|
372
|
+
if ( found ) return undefined;
|
326
373
|
if ( current.uuid === uuid ) {
|
327
374
|
|
328
375
|
found = true;
|
@@ -341,7 +388,7 @@
|
|
341
388
|
}
|
342
389
|
|
343
390
|
}
|
344
|
-
|
391
|
+
return undefined;
|
345
392
|
}
|
346
393
|
|
347
394
|
return search( this );
|
@@ -531,6 +578,7 @@
|
|
531
578
|
|
532
579
|
class USDZExporter {
|
533
580
|
debug: boolean;
|
581
|
+
pruneUnusedNodes: boolean;
|
534
582
|
sceneAnchoringOptions: USDZExporterOptions = new USDZExporterOptions();
|
535
583
|
extensions: Array<IUSDExporterExtension> = [];
|
536
584
|
keepObject?: (object: Object3D) => boolean;
|
@@ -538,6 +586,7 @@
|
|
538
586
|
constructor() {
|
539
587
|
|
540
588
|
this.debug = false;
|
589
|
+
this.pruneUnusedNodes = true;
|
541
590
|
|
542
591
|
}
|
543
592
|
|
@@ -563,8 +612,11 @@
|
|
563
612
|
Progress.report('export-usdz', "Done onBeforeBuildDocument");
|
564
613
|
|
565
614
|
Progress.report('export-usdz', "Reparent bones to common ancestor");
|
566
|
-
|
615
|
+
|
616
|
+
// Find all skeletons and reparent them to their skelroot / armature / uppermost bone parent.
|
617
|
+
// This may not be correct in all cases.
|
567
618
|
const reparentings: Array<{ object: Object3D, originalParent: Object3D | null, newParent: Object3D }> = [];
|
619
|
+
const allReparentingObjects = new Set<string>();
|
568
620
|
scene?.traverse(object => {
|
569
621
|
if (!options.exportInvisible && !object.visible) return;
|
570
622
|
|
@@ -573,14 +625,19 @@
|
|
573
625
|
|
574
626
|
const commonAncestor = findCommonAncestor(bones);
|
575
627
|
if (commonAncestor) {
|
576
|
-
|
628
|
+
const newReparenting = { object, originalParent: object.parent, newParent: commonAncestor };
|
629
|
+
reparentings.push( newReparenting );
|
630
|
+
|
631
|
+
// keep track of which nodes are important for skeletal export consistency
|
632
|
+
allReparentingObjects.add(newReparenting.object.uuid);
|
633
|
+
if (newReparenting.newParent) allReparentingObjects.add(newReparenting.newParent.uuid);
|
634
|
+
if (newReparenting.originalParent) allReparentingObjects.add(newReparenting.originalParent.uuid);
|
577
635
|
}
|
578
636
|
}
|
579
637
|
});
|
580
638
|
|
581
639
|
for ( const reparenting of reparentings ) {
|
582
640
|
const { object, originalParent, newParent } = reparenting;
|
583
|
-
// if (this.debug) console.log("REPARENTING", object, "from", originalParent, "to", newParent);
|
584
641
|
newParent.add( object );
|
585
642
|
}
|
586
643
|
|
@@ -595,6 +652,28 @@
|
|
595
652
|
Progress.report('export-usdz', "Invoking onAfterBuildDocument");
|
596
653
|
await invokeAll( context, 'onAfterBuildDocument' );
|
597
654
|
|
655
|
+
// At this point, we know all animated objects, all skinned mesh objects, and all objects targeted by behaviors.
|
656
|
+
// We can prune all empty nodes (no geometry or material) depth-first.
|
657
|
+
// This avoids unnecessary export of e.g. animated bones as nodes when they have no children
|
658
|
+
// (for example, a sword attached to a hand still needs that entire hierarchy exported)
|
659
|
+
const behaviorExt = context.extensions.find( ext => ext.extensionName === 'Behaviour' ) as BehaviorExtension | undefined;
|
660
|
+
const allBehaviorTargets = behaviorExt?.getAllTargetUuids() ?? new Set<string>();
|
661
|
+
|
662
|
+
// Prune pass. Depth-first removal of nodes that don't affect the outcome of the scene.
|
663
|
+
if (this.pruneUnusedNodes) {
|
664
|
+
const options = {
|
665
|
+
allBehaviorTargets,
|
666
|
+
debug: false,
|
667
|
+
boneReparentings: allReparentingObjects
|
668
|
+
};
|
669
|
+
if (this.debug) logUsdHierarchy(context.document, "Hierarchy BEFORE pruning", options);
|
670
|
+
prune( context.document, options );
|
671
|
+
if (this.debug) logUsdHierarchy(context.document, "Hierarchy AFTER pruning");
|
672
|
+
}
|
673
|
+
else if (this.debug) {
|
674
|
+
console.log("Pruning of empty nodes is disabled. This may result in a larger USDZ file.");
|
675
|
+
}
|
676
|
+
|
598
677
|
Progress.report('export-usdz', { message: "Parsing document", autoStep: 10 });
|
599
678
|
await parseDocument( context, () => {
|
600
679
|
// injected after stageRoot.
|
@@ -724,6 +803,14 @@
|
|
724
803
|
let model: USDObject | undefined = undefined;
|
725
804
|
let geometry: BufferGeometry | undefined = undefined;
|
726
805
|
let material: Material | Material[] | undefined = undefined;
|
806
|
+
|
807
|
+
const transform: USDObjectTransform = { position: object.position, quaternion: object.quaternion, scale: object.scale };
|
808
|
+
if (object.position.x === 0 && object.position.y === 0 && object.position.z === 0)
|
809
|
+
transform.position = null;
|
810
|
+
if (object.quaternion.x === 0 && object.quaternion.y === 0 && object.quaternion.z === 0 && object.quaternion.w === 1)
|
811
|
+
transform.quaternion = null;
|
812
|
+
if (object.scale.x === 1 && object.scale.y === 1 && object.scale.z === 1)
|
813
|
+
transform.scale = null;
|
727
814
|
|
728
815
|
if (object instanceof Mesh || object instanceof SkinnedMesh) {
|
729
816
|
geometry = object.geometry;
|
@@ -745,17 +832,17 @@
|
|
745
832
|
|
746
833
|
const name = getObjectId( object );
|
747
834
|
const skinnedMeshObject = object instanceof SkinnedMesh ? object : null;
|
748
|
-
model = new USDObject( object.uuid, name,
|
835
|
+
model = new USDObject( object.uuid, name, transform, geometry, material, undefined, skinnedMeshObject, object.animations );
|
749
836
|
|
750
837
|
} else if ( object instanceof PerspectiveCamera || object instanceof OrthographicCamera ) {
|
751
838
|
|
752
839
|
const name = getObjectId( object );
|
753
|
-
model = new USDObject( object.uuid, name,
|
840
|
+
model = new USDObject( object.uuid, name, transform, undefined, undefined, object );
|
754
841
|
|
755
842
|
} else {
|
756
843
|
|
757
844
|
const name = getObjectId( object );
|
758
|
-
model = new USDObject( object.uuid, name,
|
845
|
+
model = new USDObject( object.uuid, name, transform, undefined, undefined, undefined, undefined, object.animations );
|
759
846
|
|
760
847
|
}
|
761
848
|
|
@@ -785,7 +872,7 @@
|
|
785
872
|
} else {
|
786
873
|
|
787
874
|
const name = getObjectId( object );
|
788
|
-
const empty = new USDObject( object.uuid, name, object.
|
875
|
+
const empty = new USDObject( object.uuid, name, { position: object.position, quaternion: object.quaternion, scale: object.scale } );
|
789
876
|
if ( parentModel ) {
|
790
877
|
|
791
878
|
parentModel.add( empty );
|
@@ -804,6 +891,106 @@
|
|
804
891
|
|
805
892
|
}
|
806
893
|
|
894
|
+
function logUsdHierarchy( object: USDObject, prefix: string, ...extraLogObjects: any[] ) {
|
895
|
+
|
896
|
+
const item = {};
|
897
|
+
let itemCount = 0;
|
898
|
+
|
899
|
+
function collectItem( object: USDObject, current) {
|
900
|
+
itemCount++;
|
901
|
+
let name = object.displayName || object.name;
|
902
|
+
name += " (" + object.uuid + ")";
|
903
|
+
const hasAny = object.geometry || object.material || object.camera || object.skinnedMesh;
|
904
|
+
if (hasAny) {
|
905
|
+
name += " (" + (object.geometry ? "geo, " : "") + (object.material ? "mat, " : "") + (object.camera ? "cam, " : "") + (object.skinnedMesh ? "skin, " : "") + ")";
|
906
|
+
}
|
907
|
+
current[name] = {};
|
908
|
+
const props = { object };
|
909
|
+
if (object.material) props['mat'] = true;
|
910
|
+
if (object.geometry) props['geo'] = true;
|
911
|
+
if (object.camera) props['cam'] = true;
|
912
|
+
if (object.skinnedMesh) props['skin'] = true;
|
913
|
+
|
914
|
+
current[name]._self = props;
|
915
|
+
for ( const child of object.children ) {
|
916
|
+
if (child) {
|
917
|
+
collectItem(child, current[name]);
|
918
|
+
}
|
919
|
+
}
|
920
|
+
}
|
921
|
+
|
922
|
+
collectItem(object, item);
|
923
|
+
|
924
|
+
console.log(prefix + " (" + itemCount + " nodes)", item, ...extraLogObjects);
|
925
|
+
}
|
926
|
+
|
927
|
+
function prune ( object: USDObject, options : {
|
928
|
+
allBehaviorTargets: Set<string>,
|
929
|
+
debug: boolean,
|
930
|
+
boneReparentings: Set<string>
|
931
|
+
} ) {
|
932
|
+
|
933
|
+
let allChildsWerePruned = true;
|
934
|
+
|
935
|
+
const prunedChilds = new Array<USDObject>();
|
936
|
+
const keptChilds = new Array<USDObject>();
|
937
|
+
|
938
|
+
if (object.children.length === 0) {
|
939
|
+
allChildsWerePruned = true;
|
940
|
+
}
|
941
|
+
else {
|
942
|
+
const childs = [...object.children];
|
943
|
+
for ( const child of childs ) {
|
944
|
+
if (child) {
|
945
|
+
const childWasPruned = prune(child, options);
|
946
|
+
if (options.debug) {
|
947
|
+
if (childWasPruned) prunedChilds.push(child);
|
948
|
+
else keptChilds.push(child);
|
949
|
+
}
|
950
|
+
allChildsWerePruned = allChildsWerePruned && childWasPruned;
|
951
|
+
}
|
952
|
+
}
|
953
|
+
}
|
954
|
+
|
955
|
+
// check if this object is referenced by any behavior
|
956
|
+
const isBehaviorSourceOrTarget = options.allBehaviorTargets.has(object.uuid);
|
957
|
+
|
958
|
+
// check if this object has any material or geometry
|
959
|
+
const isVisible = object.geometry || object.material || object.camera || object.skinnedMesh || false;
|
960
|
+
|
961
|
+
// check if this object is part of any reparenting
|
962
|
+
const isBoneReparenting = options.boneReparentings.has(object.uuid);
|
963
|
+
|
964
|
+
const canBePruned = allChildsWerePruned && !isBehaviorSourceOrTarget && !isVisible && !isBoneReparenting;
|
965
|
+
|
966
|
+
if (canBePruned) {
|
967
|
+
if (options.debug) console.log("Pruned object:", (object.displayName || object.name) + " (" + object.uuid + ")", {
|
968
|
+
isVisible,
|
969
|
+
isBehaviorSourceOrTarget,
|
970
|
+
allChildsWerePruned,
|
971
|
+
isBoneReparenting,
|
972
|
+
object,
|
973
|
+
prunedChilds,
|
974
|
+
keptChilds
|
975
|
+
});
|
976
|
+
object.parent?.remove(object);
|
977
|
+
}
|
978
|
+
else {
|
979
|
+
if (options.debug) console.log("Kept object:", (object.displayName || object.name) + " (" + object.uuid + ")", {
|
980
|
+
isVisible,
|
981
|
+
isBehaviorSourceOrTarget,
|
982
|
+
allChildsWerePruned,
|
983
|
+
isBoneReparenting,
|
984
|
+
object,
|
985
|
+
prunedChilds,
|
986
|
+
keptChilds
|
987
|
+
});
|
988
|
+
}
|
989
|
+
|
990
|
+
// if it has no children and is not a behavior source or target, and is not visible, prune it
|
991
|
+
return canBePruned;
|
992
|
+
}
|
993
|
+
|
807
994
|
async function parseDocument( context: USDZExporterContext, afterStageRoot: () => string ) {
|
808
995
|
|
809
996
|
Progress.start("export-usdz-resources", "export-usdz");
|
@@ -1224,7 +1411,8 @@
|
|
1224
1411
|
|
1225
1412
|
Progress.report("export-usdz-xforms", { message: "buildXform " + model.displayName || model.name, autoStep: true });
|
1226
1413
|
|
1227
|
-
const matrix = model.matrix;
|
1414
|
+
// const matrix = model.matrix;
|
1415
|
+
const transform = model.transform;
|
1228
1416
|
const geometry = model.geometry;
|
1229
1417
|
const material = model.material;
|
1230
1418
|
const camera = model.camera;
|
@@ -1236,13 +1424,15 @@
|
|
1236
1424
|
}
|
1237
1425
|
}
|
1238
1426
|
|
1239
|
-
const transform = buildMatrix( matrix );
|
1427
|
+
// const transform = buildMatrix( matrix );
|
1240
1428
|
|
1429
|
+
/*
|
1241
1430
|
if ( matrix.determinant() < 0 ) {
|
1242
1431
|
|
1243
1432
|
console.warn( 'NeedleUSDZExporter: USDZ does not support negative scales', name );
|
1244
1433
|
|
1245
1434
|
}
|
1435
|
+
*/
|
1246
1436
|
|
1247
1437
|
const isSkinnedMesh = geometry && geometry.isBufferGeometry && geometry.attributes.skinIndex !== undefined && geometry.attributes.skinIndex.count > 0;
|
1248
1438
|
const objType = isSkinnedMesh ? 'SkelRoot' : 'Xform';
|
@@ -1264,16 +1454,20 @@
|
|
1264
1454
|
writer.beginBlock( `def Camera "${name}"`, "(", false );
|
1265
1455
|
else if ( model.type !== undefined)
|
1266
1456
|
writer.beginBlock( `def ${model.type} "${name}"` );
|
1267
|
-
else
|
1457
|
+
else // if (model.type === undefined)
|
1268
1458
|
writer.beginBlock( `def Xform "${name}"`, "(", false);
|
1269
1459
|
|
1270
|
-
if (model.displayName)
|
1271
|
-
writer.appendLine(`displayName = "${model.displayName}"`);
|
1272
1460
|
if (model.type === undefined) {
|
1273
1461
|
if (model.extraSchemas?.length)
|
1274
1462
|
_apiSchemas.push(...model.extraSchemas);
|
1275
1463
|
if (_apiSchemas.length)
|
1276
1464
|
writer.appendLine(`prepend apiSchemas = [${_apiSchemas.map(s => `"${s}"`).join(', ')}]`);
|
1465
|
+
}
|
1466
|
+
|
1467
|
+
if (model.displayName)
|
1468
|
+
writer.appendLine(`displayName = "${model.displayName}"`);
|
1469
|
+
|
1470
|
+
if ( camera || model.type === undefined) {
|
1277
1471
|
writer.closeBlock( ")" );
|
1278
1472
|
writer.beginBlock();
|
1279
1473
|
}
|
@@ -1292,16 +1486,30 @@
|
|
1292
1486
|
writer.closeBlock();
|
1293
1487
|
}
|
1294
1488
|
}
|
1489
|
+
let haveWrittenAnyXformOps = false;
|
1295
1490
|
if ( isSkinnedMesh ) {
|
1296
1491
|
writer.appendLine( `rel skel:skeleton = <Rig>` );
|
1297
1492
|
writer.appendLine( `rel skel:animationSource = <Rig/_anim>`);
|
1298
|
-
|
1493
|
+
haveWrittenAnyXformOps = false;
|
1494
|
+
// writer.appendLine( `matrix4d xformOp:transform = ${buildMatrix(new Matrix4())}` ); // always identity / in world space
|
1299
1495
|
}
|
1300
1496
|
else if (model.type === undefined) {
|
1301
|
-
|
1497
|
+
if (transform) {
|
1498
|
+
haveWrittenAnyXformOps = haveWrittenAnyXformOps || (transform.position !== null || transform.quaternion !== null || transform.scale !== null);
|
1499
|
+
if (transform.position) {
|
1500
|
+
model.needsTranslate = true;
|
1501
|
+
writer.appendLine( `double3 xformOp:translate = (${fn(transform.position.x)}, ${fn(transform.position.y)}, ${fn(transform.position.z)})` );
|
1502
|
+
}
|
1503
|
+
if (transform.quaternion) {
|
1504
|
+
model.needsOrient = true;
|
1505
|
+
writer.appendLine( `quatf xformOp:orient = (${fn(transform.quaternion.w)}, ${fn(transform.quaternion.x)}, ${fn(transform.quaternion.y)}, ${fn(transform.quaternion.z)})` );
|
1506
|
+
}
|
1507
|
+
if (transform.scale) {
|
1508
|
+
model.needsScale = true;
|
1509
|
+
writer.appendLine( `double3 xformOp:scale = (${fn(transform.scale.x)}, ${fn(transform.scale.y)}, ${fn(transform.scale.z)})` );
|
1510
|
+
}
|
1511
|
+
}
|
1302
1512
|
}
|
1303
|
-
if (model.type === undefined)
|
1304
|
-
writer.appendLine( 'uniform token[] xformOpOrder = ["xformOp:transform"]' );
|
1305
1513
|
|
1306
1514
|
if (model.visibility !== undefined)
|
1307
1515
|
writer.appendLine(`token visibility = "${model.visibility}"`);
|
@@ -1333,6 +1541,20 @@
|
|
1333
1541
|
|
1334
1542
|
}
|
1335
1543
|
|
1544
|
+
// after serialization, we know which xformops to actually define here:
|
1545
|
+
if (model.type === undefined) {
|
1546
|
+
// TODO only write the necessary ones – this isn't trivial though because we need to know
|
1547
|
+
// if some of them are animated, and then we need to include those.
|
1548
|
+
// Best approach would likely be to write xformOpOrder _after_ onSerialize
|
1549
|
+
// and keep track of what was written in onSerialize (e.g. model.needsTranslate = true)
|
1550
|
+
const ops = new Array<string>();
|
1551
|
+
if (model.needsTranslate) ops.push('"xformOp:translate"');
|
1552
|
+
if (model.needsOrient) ops.push('"xformOp:orient"');
|
1553
|
+
if (model.needsScale) ops.push('"xformOp:scale"');
|
1554
|
+
if (ops.length)
|
1555
|
+
writer.appendLine( `uniform token[] xformOpOrder = [${ops.join(', ')}]` );
|
1556
|
+
}
|
1557
|
+
|
1336
1558
|
if ( model.children ) {
|
1337
1559
|
|
1338
1560
|
writer.appendLine();
|
@@ -1380,7 +1602,7 @@
|
|
1380
1602
|
|
1381
1603
|
}
|
1382
1604
|
|
1383
|
-
function buildMesh( geometry, bones: Bone[] = [], quickLookCompatible: boolean = true) {
|
1605
|
+
function buildMesh( geometry: BufferGeometry, bones: Bone[] = [], quickLookCompatible: boolean = true) {
|
1384
1606
|
|
1385
1607
|
const name = 'Geometry';
|
1386
1608
|
const attributes = geometry.attributes;
|
@@ -1394,7 +1616,7 @@
|
|
1394
1616
|
const sortedBones: Array<{bone: Object3D, index: number}> = [];
|
1395
1617
|
const indexMapping: number[] = [];
|
1396
1618
|
let sortedSkinIndex = new Array<number>();
|
1397
|
-
let sortedSkinIndexAttribute: BufferAttribute | null = attributes.skinIndex;
|
1619
|
+
let sortedSkinIndexAttribute: BufferAttribute | InterleavedBufferAttribute | null = attributes.skinIndex;
|
1398
1620
|
let bonesArray = "";
|
1399
1621
|
if (hasBones) {
|
1400
1622
|
const uuidsFound:string[] = [];
|
@@ -1468,13 +1690,12 @@
|
|
1468
1690
|
)` : '' }
|
1469
1691
|
point3f[] points = [${buildVector3Array( attributes.position, count )}]
|
1470
1692
|
${attributes.uv ?
|
1471
|
-
`texCoord2f[] primvars:st = [${buildVector2Array( attributes.uv, count )}] (
|
1693
|
+
`texCoord2f[] primvars:st = [${buildVector2Array( attributes.uv, count, true )}] (
|
1472
1694
|
interpolation = "vertex"
|
1473
1695
|
)` : '' }
|
1474
|
-
${attributes.
|
1475
|
-
|
1476
|
-
|
1477
|
-
)` : '' }
|
1696
|
+
${attributes.uv1 ? buildCustomAttributeAccessor('st1', attributes.uv1) : '' }
|
1697
|
+
${attributes.uv2 ? buildCustomAttributeAccessor('st2', attributes.uv2) : '' }
|
1698
|
+
${attributes.uv3 ? buildCustomAttributeAccessor('st3', attributes.uv3) : '' }
|
1478
1699
|
${isSkinnedMesh ?
|
1479
1700
|
`matrix4d primvars:skel:geomBindTransform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) (
|
1480
1701
|
elementSize = 1
|
@@ -1490,6 +1711,10 @@
|
|
1490
1711
|
elementSize = 4
|
1491
1712
|
interpolation = "vertex"
|
1492
1713
|
)` : '' }
|
1714
|
+
${attributes.color ?
|
1715
|
+
`color3f[] primvars:displayColor = [${buildVector3Array( attributes.color, count )}] (
|
1716
|
+
interpolation = "vertex"
|
1717
|
+
)` : '' }
|
1493
1718
|
uniform token subdivisionScheme = "none"
|
1494
1719
|
}
|
1495
1720
|
}
|
@@ -1510,7 +1735,7 @@
|
|
1510
1735
|
`;
|
1511
1736
|
}
|
1512
1737
|
|
1513
|
-
function buildMeshVertexCount( geometry ) {
|
1738
|
+
function buildMeshVertexCount( geometry: BufferGeometry ) {
|
1514
1739
|
|
1515
1740
|
const count = geometry.index !== null ? geometry.index.count : geometry.attributes.position.count;
|
1516
1741
|
|
@@ -1553,6 +1778,29 @@
|
|
1553
1778
|
|
1554
1779
|
}
|
1555
1780
|
|
1781
|
+
/** Returns a string with the correct attribute declaration for the given attribute. Could have 2,3,4 components. */
|
1782
|
+
function buildCustomAttributeAccessor( primvarName: string, attribute: BufferAttribute | InterleavedBufferAttribute ) {
|
1783
|
+
const count = attribute.itemSize;
|
1784
|
+
switch (count) {
|
1785
|
+
case 2:
|
1786
|
+
// TODO: Check if we want to flip Y here as well. We do that for texcoords, but the data in UV1..N could be intended for other purposes.
|
1787
|
+
return `texCoord2f[] primvars:${primvarName} = [${buildVector2Array( attribute, count, true )}] (
|
1788
|
+
interpolation = "vertex"
|
1789
|
+
)`;
|
1790
|
+
case 3:
|
1791
|
+
return `texCoord3f[] primvars:${primvarName} = [${buildVector3Array( attribute, count )}] (
|
1792
|
+
interpolation = "vertex"
|
1793
|
+
)`;
|
1794
|
+
case 4:
|
1795
|
+
return `double4[] primvars:${primvarName} = [${buildVector4Array2( attribute, count )}] (
|
1796
|
+
interpolation = "vertex"
|
1797
|
+
)`;
|
1798
|
+
default:
|
1799
|
+
console.warn('USDZExporter: Attribute with ' + count + ' components are currently not supported. Results may be undefined for ' + primvarName + '.');
|
1800
|
+
return '';
|
1801
|
+
}
|
1802
|
+
}
|
1803
|
+
|
1556
1804
|
function buildVector3Array( attribute, count ) {
|
1557
1805
|
|
1558
1806
|
if ( attribute === undefined ) {
|
@@ -1578,6 +1826,33 @@
|
|
1578
1826
|
|
1579
1827
|
}
|
1580
1828
|
|
1829
|
+
|
1830
|
+
function buildVector4Array2( attribute, count ) {
|
1831
|
+
|
1832
|
+
if ( attribute === undefined ) {
|
1833
|
+
|
1834
|
+
console.warn( 'USDZExporter: Attribute is missing. Results may be undefined.' );
|
1835
|
+
return Array( count ).fill( '(0, 0, 0, 0)' ).join( ', ' );
|
1836
|
+
|
1837
|
+
}
|
1838
|
+
|
1839
|
+
const array: Array<string> = [];
|
1840
|
+
|
1841
|
+
for ( let i = 0; i < attribute.count; i ++ ) {
|
1842
|
+
|
1843
|
+
const x = attribute.getX( i );
|
1844
|
+
const y = attribute.getY( i );
|
1845
|
+
const z = attribute.getZ( i ) || 0;
|
1846
|
+
const w = attribute.getW( i ) || 0;
|
1847
|
+
|
1848
|
+
array.push( `(${x.toPrecision( PRECISION )}, ${y.toPrecision( PRECISION )}, ${z.toPrecision( PRECISION )}, ${w.toPrecision( PRECISION )})` );
|
1849
|
+
|
1850
|
+
}
|
1851
|
+
|
1852
|
+
return array.join( ', ' );
|
1853
|
+
|
1854
|
+
}
|
1855
|
+
|
1581
1856
|
function buildVector4Array( attribute, ints = false ) {
|
1582
1857
|
const array: Array<string> = [];
|
1583
1858
|
|
@@ -1599,8 +1874,8 @@
|
|
1599
1874
|
|
1600
1875
|
}
|
1601
1876
|
|
1602
|
-
function buildVector2Array( attribute, count ) {
|
1603
|
-
|
1877
|
+
function buildVector2Array( attribute: BufferAttribute | InterleavedBufferAttribute, count: number, flipY: boolean = false ) {
|
1878
|
+
|
1604
1879
|
if ( attribute === undefined ) {
|
1605
1880
|
|
1606
1881
|
console.warn( 'USDZExporter: UVs missing.' );
|
@@ -1613,10 +1888,11 @@
|
|
1613
1888
|
for ( let i = 0; i < attribute.count; i ++ ) {
|
1614
1889
|
|
1615
1890
|
const x = attribute.getX( i );
|
1616
|
-
|
1891
|
+
let y = attribute.getY( i );
|
1892
|
+
if (flipY)
|
1893
|
+
y = 1 - y;
|
1894
|
+
array.push( `(${x.toPrecision( PRECISION )}, ${y.toPrecision( PRECISION )})` );
|
1617
1895
|
|
1618
|
-
array.push( `(${x.toPrecision( PRECISION )}, ${1 - y.toPrecision( PRECISION )})` );
|
1619
|
-
|
1620
1896
|
}
|
1621
1897
|
|
1622
1898
|
return array.join( ', ' );
|
@@ -1993,10 +2269,10 @@
|
|
1993
2269
|
}
|
1994
2270
|
` : ''}
|
1995
2271
|
${usedUVChannels.has(1) ? `
|
1996
|
-
def Shader "
|
2272
|
+
def Shader "uvReader_st1"
|
1997
2273
|
{
|
1998
2274
|
uniform token info:id = "UsdPrimvarReader_float2"
|
1999
|
-
token inputs:varname = "
|
2275
|
+
token inputs:varname = "st1"
|
2000
2276
|
float2 inputs:fallback = (0.0, 0.0)
|
2001
2277
|
float2 outputs:result
|
2002
2278
|
}
|
@@ -26,6 +26,7 @@
|
|
26
26
|
import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js";
|
27
27
|
|
28
28
|
const debug = getParam("debugusdz");
|
29
|
+
const debugUsdzPruning = getParam("debugusdzpruning");
|
29
30
|
|
30
31
|
/**
|
31
32
|
* Custom branding for the QuickLook overlay, used by {@link USDZExporter}.
|
@@ -384,6 +385,7 @@
|
|
384
385
|
|
385
386
|
//@ts-ignore
|
386
387
|
exporter.debug = debug;
|
388
|
+
exporter.pruneUnusedNodes = !debugUsdzPruning;
|
387
389
|
exporter.keepObject = (object) => {
|
388
390
|
// This explicitly removes geometry and material data from disabled renderers.
|
389
391
|
// Note that this is different to the object itself being active –
|
@@ -161,9 +161,10 @@
|
|
161
161
|
height = rt.height;
|
162
162
|
}
|
163
163
|
|
164
|
-
|
164
|
+
const mat = rotateYAxisMatrix.clone();
|
165
165
|
if (rt) // Not ideal but works for now:
|
166
|
-
|
166
|
+
mat.premultiply(invertX);
|
167
|
+
newModel.setMatrix(mat);
|
167
168
|
|
168
169
|
const color = text.color.clone();
|
169
170
|
newModel.material = new MeshStandardMaterial({ color: color, emissive: color });
|
@@ -99,7 +99,7 @@
|
|
99
99
|
|
100
100
|
if (shadowComponent) {
|
101
101
|
const mat = shadowComponent.matrix;
|
102
|
-
shadowRootModel.
|
102
|
+
shadowRootModel.setMatrix(mat);
|
103
103
|
|
104
104
|
const usdObjectMap = new Map<Object3D, USDObject>();
|
105
105
|
const opacityMap = new Map<Object3D, number>();
|
@@ -111,7 +111,7 @@
|
|
111
111
|
if (child === shadowComponent) return;
|
112
112
|
|
113
113
|
const childModel = USDObject.createEmpty();
|
114
|
-
childModel.
|
114
|
+
childModel.setMatrix(child.matrix);
|
115
115
|
|
116
116
|
const childParent = child.parent;
|
117
117
|
const isText = !!childParent && typeof childParent["textContent"] === "string" && childParent["textContent"].length > 0;
|
@@ -82,6 +82,15 @@
|
|
82
82
|
this._isDirty = true;
|
83
83
|
return effect;
|
84
84
|
}
|
85
|
+
else if (effect instanceof PostProcessingEffect) {
|
86
|
+
// if the effect is part of the shared profile remove it from there
|
87
|
+
const si = this.sharedProfile?.components?.indexOf(effect);
|
88
|
+
if (si !== undefined && si !== -1) {
|
89
|
+
this._isDirty = true;
|
90
|
+
this.sharedProfile?.components?.splice(si, 1);
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
85
94
|
return effect;
|
86
95
|
}
|
87
96
|
|
@@ -191,6 +200,8 @@
|
|
191
200
|
this._activeEffects.push(effect);
|
192
201
|
}
|
193
202
|
|
203
|
+
if(debug) console.log("Apply PostProcessing", this._activeEffects);
|
204
|
+
|
194
205
|
if (this._activeEffects.length > 0) {
|
195
206
|
if (!this._postprocessing)
|
196
207
|
this._postprocessing = new PostProcessingHandler(this.context);
|
@@ -206,9 +206,8 @@
|
|
206
206
|
const mat = relativeMatrix
|
207
207
|
.clone()
|
208
208
|
.invert()
|
209
|
-
|
210
|
-
|
211
|
-
.scale(new Vector3(scale, scale, scale));
|
209
|
+
// apply session root scale again after undoing the world transformation
|
210
|
+
model.setMatrix(mat.scale(new Vector3(scale, scale, scale)));
|
212
211
|
|
213
212
|
// Unfortunately looks like Apple's docs are incomplete:
|
214
213
|
// https://developer.apple.com/documentation/realitykit/preliminary_anchoringapi#Nest-and-Layer-Anchorable-Prims
|