@@ -180,10 +180,9 @@
|
|
180
180
|
set actions(val: Array<AnimationAction>) {
|
181
181
|
this._actions = val;
|
182
182
|
}
|
183
|
-
private _actions
|
183
|
+
private _actions!: Array<AnimationAction>;
|
184
|
+
private _handles!: AnimationHandle[];
|
184
185
|
|
185
|
-
private _handles: AnimationHandle[] = [];
|
186
|
-
|
187
186
|
/** @internal */
|
188
187
|
awake() {
|
189
188
|
if (debug) console.log("Animation Awake", this.name, this);
|
@@ -195,9 +194,9 @@
|
|
195
194
|
this.clip = this._tempAnimationClipBeforeGameObjectExisted;
|
196
195
|
this._tempAnimationClipBeforeGameObjectExisted = null;
|
197
196
|
}
|
198
|
-
//
|
199
|
-
|
200
|
-
|
197
|
+
// actions need to reset (e.g. if the animation component was duplicated this array must not contain previous content)
|
198
|
+
this.actions = [];
|
199
|
+
this._handles = [];
|
201
200
|
}
|
202
201
|
/** @internal */
|
203
202
|
onEnable(): void {
|
@@ -238,22 +237,26 @@
|
|
238
237
|
|
239
238
|
/** Is any animation playing? */
|
240
239
|
get isPlaying() {
|
241
|
-
|
242
|
-
|
243
|
-
|
240
|
+
if (this.actions) {
|
241
|
+
for (let i = 0; i < this.actions.length; i++) {
|
242
|
+
if (this.actions[i].isRunning())
|
243
|
+
return true;
|
244
|
+
}
|
244
245
|
}
|
245
246
|
return false;
|
246
247
|
}
|
247
248
|
|
248
249
|
/** Stops all currently playing animations */
|
249
250
|
stopAll(opts?: Pick<PlayOptions, "fadeDuration">): void {
|
250
|
-
|
251
|
-
|
252
|
-
|
251
|
+
if (this.actions) {
|
252
|
+
for (const act of this.actions) {
|
253
|
+
if (opts?.fadeDuration) {
|
254
|
+
act.fadeOut(opts.fadeDuration);
|
255
|
+
}
|
256
|
+
else {
|
257
|
+
act.stop();
|
258
|
+
}
|
253
259
|
}
|
254
|
-
else {
|
255
|
-
act.stop();
|
256
|
-
}
|
257
260
|
}
|
258
261
|
}
|
259
262
|
|
@@ -325,7 +328,7 @@
|
|
325
328
|
}
|
326
329
|
act.paused = !unpause;
|
327
330
|
}
|
328
|
-
|
331
|
+
|
329
332
|
/**
|
330
333
|
* Play an animation clip or an clip at the specified index.
|
331
334
|
* @param clipOrNumber the animation clip, index or name to play. If undefined, the first animation in the animations array will be played
|
@@ -334,7 +337,7 @@
|
|
334
337
|
*/
|
335
338
|
play(clipOrNumber: AnimationIdentifier = 0, options?: PlayOptions): Promise<AnimationAction> | void {
|
336
339
|
if (debug) console.log("PLAY", clipOrNumber)
|
337
|
-
this.
|
340
|
+
this.ensureMixer();
|
338
341
|
if (!this.mixer) {
|
339
342
|
if (debug) console.warn("Missing mixer", this);
|
340
343
|
return;
|
@@ -376,7 +379,7 @@
|
|
376
379
|
var prev = this.actions.find(a => a === action);
|
377
380
|
if (prev === action && prev.isRunning() && prev.time < prev.getClip().duration) {
|
378
381
|
const handle = this.tryFindHandle(action);
|
379
|
-
if(prev.paused) {
|
382
|
+
if (prev.paused) {
|
380
383
|
prev.paused = false;
|
381
384
|
}
|
382
385
|
if (handle) return handle.waitForFinish();
|
@@ -444,22 +447,18 @@
|
|
444
447
|
}
|
445
448
|
|
446
449
|
|
447
|
-
private
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
450
|
+
private ensureMixer() {
|
451
|
+
if (!this.mixer) {
|
452
|
+
// try getting the animation mixer from the assigned gameobject
|
453
|
+
const key = "animationMixer";
|
454
|
+
if (this.gameObject[key]) {
|
455
|
+
this.mixer = this.gameObject[key];
|
456
|
+
}
|
457
|
+
if (!this.mixer || !this.mixer.clipAction) {
|
458
|
+
this.mixer = new AnimationMixer(this.gameObject);
|
459
|
+
this.gameObject[key] = this.mixer;
|
460
|
+
}
|
458
461
|
}
|
459
|
-
if (!this.mixer || !this.mixer.clipAction) {
|
460
|
-
this.mixer = new AnimationMixer(this.gameObject);
|
461
|
-
this.gameObject[key] = this.mixer;
|
462
|
-
}
|
463
462
|
this.context.animations.registerAnimationMixer(this.mixer);
|
464
463
|
}
|
465
464
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { BackSide, 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, Material, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshStandardMaterial, 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
|
|
@@ -8,7 +8,7 @@
|
|
8
8
|
import { Gizmos } from "../engine/engine_gizmos.js";
|
9
9
|
import { onStart } from "../engine/engine_lifecycle_api.js";
|
10
10
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
11
|
-
import { getBoundingBox } from "../engine/engine_three_utils.js";
|
11
|
+
import { getBoundingBox, getVisibleInCustomShadowRendering } from "../engine/engine_three_utils.js";
|
12
12
|
import { IGameObject } from "../engine/engine_types.js";
|
13
13
|
import { getParam } from "../engine/engine_utils.js"
|
14
14
|
import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
|
@@ -141,6 +141,10 @@
|
|
141
141
|
const min = box.min;
|
142
142
|
const offset = Math.max(0.00001, (box.max.y - min.y) * .002);
|
143
143
|
box.max.y += offset;
|
144
|
+
// This is for cases where GroundProjection with autoFit is used
|
145
|
+
// Since contact shadows can currently not ignore certain objects from rendering
|
146
|
+
// we need to make sure the GroundProjection is not exactly on the same level as ContactShadows
|
147
|
+
// We can't move GroundProjection down because of immersive-ar mesh/plane tracking where occlusion would otherwise hide GroundProjection
|
144
148
|
this.shadowsRoot.position.set((min.x + box.max.x) / 2, min.y - offset, (min.z + box.max.z) / 2);
|
145
149
|
this.shadowsRoot.scale.set(box.max.x - min.x, box.max.y - min.y, box.max.z - min.z);
|
146
150
|
this.shadowsRoot.matrixWorldNeedsUpdate = true;
|
@@ -221,7 +225,9 @@
|
|
221
225
|
this.shadowGroup.add(this.blurPlane);
|
222
226
|
|
223
227
|
// max. ground distance is controlled via object scale
|
224
|
-
|
228
|
+
const near = 0;
|
229
|
+
const far = 1.0;
|
230
|
+
this.shadowCamera = new OrthographicCamera(-1 / 2, 1 / 2, 1 / 2, -1 / 2, near, far);
|
225
231
|
this.shadowCamera.layers.enableAll();
|
226
232
|
this.shadowCamera.rotation.x = Math.PI / 2; // get the camera to look up
|
227
233
|
this.shadowGroup.add(this.shadowCamera);
|
@@ -329,16 +335,36 @@
|
|
329
335
|
renderer.xr.enabled = false;
|
330
336
|
|
331
337
|
const list = renderer.renderLists.get(scene, 0);
|
332
|
-
const
|
333
|
-
|
338
|
+
const prevTransparent = list.transparent;
|
339
|
+
empty_buffer.length = 0;
|
340
|
+
list.transparent = empty_buffer;
|
334
341
|
|
342
|
+
// we need to hide objects that don't render color or that are wireframes
|
343
|
+
objects_hidden.length = 0;
|
344
|
+
for (const entry of list.opaque) {
|
345
|
+
if (!entry.object.visible) continue;
|
346
|
+
const mat = entry.material as MeshStandardMaterial;
|
347
|
+
// Ignore objects that don't render color
|
348
|
+
const hide = entry.material.colorWrite == false || mat.wireframe === true || getVisibleInCustomShadowRendering(entry.object) === false;
|
349
|
+
if (hide) {
|
350
|
+
objects_hidden.push(entry.object);
|
351
|
+
entry.object["needle:visible"] = entry.object.visible;
|
352
|
+
entry.object.visible = false;
|
353
|
+
}
|
354
|
+
}
|
355
|
+
|
335
356
|
// render to the render target to get the depths
|
336
357
|
renderer.setRenderTarget(this.renderTarget);
|
337
358
|
renderer.clear();
|
338
|
-
|
339
359
|
renderer.render(scene, this.shadowCamera);
|
360
|
+
list.transparent = prevTransparent;
|
340
361
|
|
341
|
-
|
362
|
+
// reset previously hidden objects
|
363
|
+
for (const object of objects_hidden) {
|
364
|
+
if (object["needle:visible"] != undefined) {
|
365
|
+
object.visible = object["needle:visible"];
|
366
|
+
}
|
367
|
+
}
|
342
368
|
|
343
369
|
// for the shearing idea
|
344
370
|
// this.shadowCamera.projectionMatrix.copy(mat);
|
@@ -402,4 +428,8 @@
|
|
402
428
|
|
403
429
|
renderer.setRenderTarget(currentRt);
|
404
430
|
}
|
405
|
-
}
|
431
|
+
}
|
432
|
+
|
433
|
+
const empty_buffer = [];
|
434
|
+
const objects_hidden = new Array<Object3D>();
|
435
|
+
|
@@ -195,6 +195,13 @@
|
|
195
195
|
mesh.material["depthTest"] = options.depthTest ?? true;
|
196
196
|
mesh.material["wireframe"] = true;
|
197
197
|
}
|
198
|
+
|
199
|
+
/** Set visibility of all currently rendered gizmos */
|
200
|
+
static setVisible(visible: boolean) {
|
201
|
+
for (const obj of Internal.timedObjectsBuffer) {
|
202
|
+
obj.visible = visible;
|
203
|
+
}
|
204
|
+
}
|
198
205
|
}
|
199
206
|
|
200
207
|
const box: BoxGeometry = new BoxGeometry(1, 1, 1);
|
@@ -379,6 +386,8 @@
|
|
379
386
|
|
380
387
|
object.renderOrder = 999999;
|
381
388
|
object[$cacheSymbol] = cache;
|
389
|
+
object.castShadow = false;
|
390
|
+
object.receiveShadow = false;
|
382
391
|
object["isGizmo"] = true;
|
383
392
|
this.timedObjectsBuffer.push(object);
|
384
393
|
|
@@ -393,7 +402,7 @@
|
|
393
402
|
}
|
394
403
|
|
395
404
|
|
396
|
-
|
405
|
+
public static readonly timedObjectsBuffer = new Array<Object3D>();
|
397
406
|
private static readonly timesBuffer = new Array<number>();
|
398
407
|
private static readonly contextPostRenderCallbacks = new Map<Context, () => void>();
|
399
408
|
private static readonly contextBeforeRenderCallbacks = new Map<Context, () => void>();
|
@@ -497,13 +497,33 @@
|
|
497
497
|
return type === "Mesh" || type === "SkinnedMesh";
|
498
498
|
}
|
499
499
|
|
500
|
+
// for contact shadows
|
501
|
+
export function setVisibleInCustomShadowRendering(obj: Object3D, enabled: boolean) {
|
502
|
+
if (enabled)
|
503
|
+
obj["needle:rendercustomshadow"] = true;
|
504
|
+
else {
|
505
|
+
obj["needle:rendercustomshadow"] = false;
|
506
|
+
}
|
507
|
+
}
|
508
|
+
export function getVisibleInCustomShadowRendering(obj: Object3D): boolean {
|
509
|
+
if (obj) {
|
510
|
+
if (obj["needle:rendercustomshadow"] === true) {
|
511
|
+
return true;
|
512
|
+
}
|
513
|
+
else if(obj["needle:rendercustomshadow"] == undefined) {
|
514
|
+
return true;
|
515
|
+
}
|
516
|
+
}
|
517
|
+
return false;
|
518
|
+
}
|
519
|
+
|
500
520
|
/**
|
501
521
|
* Get the bounding box of a list of objects
|
502
522
|
* @param objects the objects to get the bounding box from
|
503
523
|
* @param ignore objects to ignore when calculating the bounding box
|
504
524
|
* @param output an optional output object to store the result in
|
505
525
|
*/
|
506
|
-
export function getBoundingBox(objects: Object3D[], ignore: Object3D
|
526
|
+
export function getBoundingBox(objects: Object3D[], ignore: ((obj: Object3D) => void | boolean) | Array<Object3D | null | undefined> | undefined = undefined): Box3 {
|
507
527
|
const box = new Box3();
|
508
528
|
box.makeEmpty();
|
509
529
|
|
@@ -526,8 +546,13 @@
|
|
526
546
|
if (!(isMesh(obj))) {
|
527
547
|
allowExpanding = false;
|
528
548
|
}
|
529
|
-
|
530
|
-
|
549
|
+
if (allowExpanding) {
|
550
|
+
// Ignore things parented to the camera + ignore the camera
|
551
|
+
if (ignore && Array.isArray(ignore) && ignore?.includes(obj)) return;
|
552
|
+
else if (typeof ignore === "function") {
|
553
|
+
if (ignore(obj) === true) return;
|
554
|
+
}
|
555
|
+
}
|
531
556
|
// We don't want to fit UI objects
|
532
557
|
if (obj["isUI"] === true) return;
|
533
558
|
// If we encountered some geometry that should be ignored
|
@@ -2,11 +2,9 @@
|
|
2
2
|
import { GroundedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundedSkybox.js';
|
3
3
|
|
4
4
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
-
import { getBoundingBox, getWorldPosition, Graphics, setWorldPosition } from "../engine/engine_three_utils.js";
|
6
|
-
import { getParam, Watch as Watch } from "../engine/engine_utils.js";
|
5
|
+
import { getBoundingBox, getWorldPosition, Graphics, setVisibleInCustomShadowRendering,setWorldPosition } from "../engine/engine_three_utils.js";
|
6
|
+
import { delayForFrames, getParam, Watch as Watch } from "../engine/engine_utils.js";
|
7
7
|
import { Behaviour } from "./Component.js";
|
8
|
-
import { buildMatrix } from "./export/usdz/ThreeUSDZExporter.js";
|
9
|
-
import { LUTOperation } from "postprocessing";
|
10
8
|
|
11
9
|
const debug = getParam("debuggroundprojection");
|
12
10
|
|
@@ -100,11 +98,14 @@
|
|
100
98
|
}
|
101
99
|
/** @internal */
|
102
100
|
onEnterXR(): void {
|
101
|
+
if(!this.activeAndEnabled) return;
|
103
102
|
this._needsTextureUpdate = true;
|
104
103
|
this.updateProjection();
|
105
104
|
}
|
106
105
|
/** @internal */
|
107
|
-
onLeaveXR()
|
106
|
+
async onLeaveXR() {
|
107
|
+
if(!this.activeAndEnabled) return;
|
108
|
+
await delayForFrames(1);
|
108
109
|
this.updateProjection();
|
109
110
|
}
|
110
111
|
/** @internal */
|
@@ -144,13 +145,15 @@
|
|
144
145
|
return;
|
145
146
|
}
|
146
147
|
}
|
147
|
-
|
148
|
+
// offset here must be zero (and not .01) because the plane occlusion (when mesh tracking is active) is otherwise not correct
|
149
|
+
const offset = 0;
|
148
150
|
if (!this._projection || this.context.scene.environment !== this._lastEnvironment || this._height !== this._lastHeight || this._radius !== this._lastRadius) {
|
149
151
|
if (debug)
|
150
152
|
console.log("Create/Update Ground Projection", this.context.scene.environment.name);
|
151
153
|
this._projection?.removeFromParent();
|
152
154
|
this._projection = new GroundProjection(this.context.scene.environment, this._height, this.radius, 64);
|
153
155
|
this._projection.position.y = this._height - offset;
|
156
|
+
setVisibleInCustomShadowRendering(this._projection, false);
|
154
157
|
}
|
155
158
|
|
156
159
|
this._lastEnvironment = this.context.scene.environment;
|
@@ -231,6 +234,8 @@
|
|
231
234
|
|
232
235
|
// Update the texture
|
233
236
|
this._projection.material.map = Graphics.copyTexture(this.context.scene.environment, this._blurrynessShader);
|
237
|
+
this._projection.material.depthTest = true;
|
238
|
+
this._projection.material.depthWrite = false;
|
234
239
|
}
|
235
240
|
|
236
241
|
}
|
@@ -852,15 +852,17 @@
|
|
852
852
|
// handle controller and input source changes changes
|
853
853
|
this.session.addEventListener('end', this.onEnd);
|
854
854
|
// handle input sources change
|
855
|
-
this.session.addEventListener("inputsourceschange",
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
855
|
+
this.session.addEventListener("inputsourceschange",
|
856
|
+
/* @ts-ignore (ignore CI XRInputSourceChangeEvent mismatch) */
|
857
|
+
(evt: XRInputSourcesChangeEvent) => {
|
858
|
+
// handle removed controllers
|
859
|
+
for (const removedInputSource of evt.removed) {
|
860
|
+
this.disconnectInputSource(removedInputSource);
|
861
|
+
}
|
862
|
+
for (const newInputSource of evt.added) {
|
863
|
+
this.onInputSourceAdded(newInputSource);
|
864
|
+
}
|
865
|
+
});
|
864
866
|
|
865
867
|
// Unfortunately the code below doesnt work: the session never receives any input sources sometimes
|
866
868
|
// https://developer.mozilla.org/en-US/docs/Web/API/XRSession/visibilitychange_event
|
@@ -1262,11 +1264,14 @@
|
|
1262
1264
|
if (renderer.xr.isPresenting && this.context.mainCamera) {
|
1263
1265
|
const wasXr = renderer.xr.enabled;
|
1264
1266
|
const previousRenderTarget = renderer.getRenderTarget();
|
1267
|
+
const previousBackground = this.context.scene.background;
|
1265
1268
|
renderer.xr.enabled = false;
|
1266
1269
|
renderer.setRenderTarget(null);
|
1270
|
+
if(this.isPassThrough) this.context.scene.background = null;
|
1267
1271
|
renderer.render(this.context.scene, this.context.mainCamera);
|
1268
1272
|
renderer.xr.enabled = wasXr;
|
1269
1273
|
renderer.setRenderTarget(previousRenderTarget);
|
1274
|
+
this.context.scene.background = previousBackground;
|
1270
1275
|
}
|
1271
1276
|
}
|
1272
1277
|
}
|
@@ -267,8 +267,8 @@
|
|
267
267
|
/**
|
268
268
|
* Update the sprite. Modified properties will be applied to the sprite mesh. This method is called automatically when the sprite is changed.
|
269
269
|
*/
|
270
|
-
updateSprite() {
|
271
|
-
if (!this.__didAwake) return;
|
270
|
+
updateSprite(force: boolean = false) {
|
271
|
+
if (!this.__didAwake && !force) return;
|
272
272
|
if (!this.sprite?.spriteSheet?.sprites) return;
|
273
273
|
const sprite = this.sprite.spriteSheet.sprites[this.spriteIndex];
|
274
274
|
if (!sprite) {
|
@@ -299,8 +299,9 @@
|
|
299
299
|
// force Sprites to be created
|
300
300
|
const sprites = GameObject.getComponentsInChildren(objectToExport, SpriteRenderer);
|
301
301
|
for (const sprite of sprites) {
|
302
|
-
if (sprite && sprite.enabled
|
303
|
-
sprite.
|
302
|
+
if (sprite && sprite.enabled) {
|
303
|
+
sprite.updateSprite(true); // force create
|
304
|
+
}
|
304
305
|
}
|
305
306
|
|
306
307
|
// trigger progressive textures to be loaded:
|
@@ -590,7 +591,7 @@
|
|
590
591
|
|
591
592
|
if (!sessionRoot) {
|
592
593
|
const xr = GameObject.findObjectOfType(WebXR);
|
593
|
-
if (xr) arScale = xr.
|
594
|
+
if (xr) arScale = xr.arScale;
|
594
595
|
}
|
595
596
|
else {
|
596
597
|
arScale = sessionRoot.arScale;
|
@@ -4,8 +4,10 @@
|
|
4
4
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
5
5
|
import { Context } from "../../engine/engine_context.js";
|
6
6
|
import { destroy, instantiate } from "../../engine/engine_gameobject.js";
|
7
|
+
import { Gizmos } from "../../engine/engine_gizmos.js";
|
7
8
|
import { InputEventQueue, NEPointerEvent } from "../../engine/engine_input.js";
|
8
9
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
10
|
+
import { getBoundingBox } from "../../engine/engine_three_utils.js";
|
9
11
|
import type { IComponent, IGameObject } from "../../engine/engine_types.js";
|
10
12
|
import { getParam, isAndroidDevice } from "../../engine/engine_utils.js";
|
11
13
|
import { NeedleXRController, type NeedleXREventArgs, type NeedleXRHitTestResult, NeedleXRSession } from "../../engine/engine_xr.js";
|
@@ -51,7 +53,6 @@
|
|
51
53
|
* Note: a large value makes the scene appear smaller
|
52
54
|
* @default 1
|
53
55
|
*/
|
54
|
-
@serializable()
|
55
56
|
get arScale(): number {
|
56
57
|
return this._arScale;
|
57
58
|
}
|
@@ -63,36 +64,35 @@
|
|
63
64
|
private _arScale: number = 1;
|
64
65
|
|
65
66
|
/** When enabled the placed scene forward direction will towards the XRRig
|
67
|
+
* @deprecated
|
66
68
|
* @default false
|
67
69
|
*/
|
68
|
-
@serializable()
|
69
70
|
invertForward: boolean = false;
|
70
71
|
|
71
72
|
/** When assigned this asset will be loaded and visualize the placement while in AR
|
72
73
|
* @default null
|
73
74
|
*/
|
74
|
-
@serializable(AssetReference)
|
75
75
|
customReticle?: AssetReference;
|
76
76
|
|
77
|
-
/**
|
78
|
-
*
|
79
|
-
* @default false
|
80
|
-
**/
|
81
|
-
@serializable()
|
82
|
-
useXRAnchor: boolean = false;
|
83
|
-
|
84
|
-
/** Preview feature: enable touch transform
|
85
|
-
* @default false
|
77
|
+
/** Enable touch transform to translate, rotate and scale the scene in AR with multitouch
|
78
|
+
* @default true
|
86
79
|
*/
|
87
|
-
|
88
|
-
arTouchTransform: boolean = false;
|
80
|
+
arTouchTransform: boolean = true;
|
89
81
|
|
90
82
|
/** When enabled the scene will be placed automatically when a point in the real world is found
|
91
83
|
* @default false
|
92
84
|
*/
|
93
|
-
@serializable()
|
94
85
|
autoPlace: boolean = false;
|
95
86
|
|
87
|
+
/** When enabled the scene center will be automatically calculated from the content in the scene */
|
88
|
+
autoCenter: boolean = false;
|
89
|
+
|
90
|
+
/** Experimental: When enabled we will create a XR anchor for the scene placement
|
91
|
+
* and make sure the scene is at that anchored point during a XR session
|
92
|
+
* @default false
|
93
|
+
**/
|
94
|
+
useXRAnchor: boolean = false;
|
95
|
+
|
96
96
|
/** true if we're currently placing the scene */
|
97
97
|
private _isPlacing = true;
|
98
98
|
|
@@ -156,6 +156,15 @@
|
|
156
156
|
}
|
157
157
|
this.context.scene.add(rootObject);
|
158
158
|
|
159
|
+
if (this.autoCenter) {
|
160
|
+
const bounds = getBoundingBox(this._placementScene.children);
|
161
|
+
const center = bounds.getCenter(new Vector3());
|
162
|
+
const size = bounds.getSize(new Vector3());
|
163
|
+
const matrix = new Matrix4();
|
164
|
+
matrix.makeTranslation(center.x, center.y - size.y * .5, center.z);
|
165
|
+
this._startOffset.multiply(matrix);
|
166
|
+
}
|
167
|
+
|
159
168
|
// reparent components
|
160
169
|
// save which gameobject the sessionroot component was previously attached to
|
161
170
|
this._reparentedComponents.length = 0;
|
@@ -278,7 +287,7 @@
|
|
278
287
|
else {
|
279
288
|
reticle = new Mesh(
|
280
289
|
new RingGeometry(0.07, 0.09, 32).rotateX(- Math.PI / 2),
|
281
|
-
new MeshBasicMaterial({ side: DoubleSide, depthTest: false, depthWrite: false, transparent: true, opacity: 1, color:
|
290
|
+
new MeshBasicMaterial({ side: DoubleSide, depthTest: false, depthWrite: false, transparent: true, opacity: 1, color: 0xeeeeee })
|
282
291
|
) as any as IGameObject;
|
283
292
|
reticle.name = "AR Placement Reticle";
|
284
293
|
}
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
5
5
|
import { findObjectOfType } from "../../engine/engine_components.js";
|
6
6
|
import { serializable } from "../../engine/engine_serialization.js";
|
7
|
-
import { getParam, isDesktop, isiOS, isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
|
7
|
+
import { delayForFrames, getParam, isDesktop, isiOS, isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
|
8
8
|
import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
9
9
|
import { ButtonsFactory } from "../../engine/webcomponents/buttons.js";
|
10
10
|
import { getIconElement } from "../../engine/webcomponents/icons.js";
|
@@ -58,15 +58,26 @@
|
|
58
58
|
/** When enabled the scene must be placed in AR */
|
59
59
|
@serializable()
|
60
60
|
usePlacementReticle: boolean = true;
|
61
|
+
/** When assigned this object will be used as the AR placement reticle */
|
62
|
+
@serializable(AssetReference)
|
63
|
+
customARPlacementReticle?: AssetReference;
|
61
64
|
/** When enabled you can position, rotate or scale your AR scene with one or two fingers */
|
62
65
|
@serializable()
|
63
66
|
usePlacementAdjustment: boolean = true;
|
64
|
-
/** Used when `usePlacementReticle` is enabled */
|
67
|
+
/** Used when `usePlacementReticle` is enabled. This is the scale of the user in the scene in AR. Larger values make the 3D content appear smaller */
|
65
68
|
@serializable()
|
66
|
-
|
69
|
+
arScale: number = 1;
|
67
70
|
/** Experimental: When enabled an XRAnchor will be created for the AR scene and the position will be updated to the anchor position every few frames */
|
68
71
|
@serializable()
|
69
72
|
useXRAnchor: boolean = false;
|
73
|
+
/**
|
74
|
+
* When enabled the scene will be placed automatically when a point in the real world is found
|
75
|
+
*/
|
76
|
+
@serializable()
|
77
|
+
autoPlace: boolean = true;
|
78
|
+
/** When enabled the AR session root center will be automatically adjusted to place the center of the scene */
|
79
|
+
@serializable()
|
80
|
+
autoCenter: boolean = true;
|
70
81
|
|
71
82
|
/** When enabled a USDZExporter component will be added to the scene (if none is found) */
|
72
83
|
@serializable()
|
@@ -90,6 +101,8 @@
|
|
90
101
|
|
91
102
|
private _usdzExporter?: USDZExporter;
|
92
103
|
|
104
|
+
static activeWebXRComponent: WebXR | null = null;
|
105
|
+
|
93
106
|
awake() {
|
94
107
|
NeedleXRSession.getXRSync(this.context);
|
95
108
|
}
|
@@ -176,7 +189,17 @@
|
|
176
189
|
|
177
190
|
private _previousXRState: number = 0;
|
178
191
|
|
192
|
+
private get isActiveWebXR() {
|
193
|
+
return !WebXR.activeWebXRComponent || WebXR.activeWebXRComponent === this;
|
194
|
+
}
|
195
|
+
|
179
196
|
onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
|
197
|
+
if (!this.isActiveWebXR) {
|
198
|
+
console.warn(`WebXR: another WebXR component is already active (${WebXR.activeWebXRComponent?.name}). This is ignored: ${this.name}`);
|
199
|
+
return;
|
200
|
+
}
|
201
|
+
WebXR.activeWebXRComponent = this;
|
202
|
+
|
180
203
|
if (_mode == "immersive-ar" && this.useDepthSensing) {
|
181
204
|
args.optionalFeatures = args.optionalFeatures || [];
|
182
205
|
args.optionalFeatures.push("depth-sensing");
|
@@ -184,6 +207,8 @@
|
|
184
207
|
}
|
185
208
|
|
186
209
|
async onEnterXR(args: NeedleXREventArgs) {
|
210
|
+
if (!this.isActiveWebXR) return;
|
211
|
+
|
187
212
|
if (debug) console.log("WebXR onEnterXR")
|
188
213
|
// set XR flags
|
189
214
|
this._previousXRState = XRState.Global.Mask;
|
@@ -200,11 +225,14 @@
|
|
200
225
|
this.context.scene.add(implicitSessionRoot);
|
201
226
|
sessionroot = GameObject.addComponent(implicitSessionRoot, WebARSessionRoot)!;
|
202
227
|
this._createdComponentsInSession.push(sessionroot);
|
203
|
-
sessionroot.arScale = this.arSceneScale;
|
204
|
-
sessionroot.arTouchTransform = this.usePlacementAdjustment;
|
205
|
-
sessionroot.useXRAnchor = this.useXRAnchor;
|
206
228
|
}
|
207
|
-
|
229
|
+
|
230
|
+
sessionroot.customReticle = this.customARPlacementReticle;
|
231
|
+
sessionroot.arScale = this.arScale;
|
232
|
+
sessionroot.arTouchTransform = this.usePlacementAdjustment;
|
233
|
+
sessionroot.autoPlace = this.autoPlace;
|
234
|
+
sessionroot.autoCenter = this.autoCenter;
|
235
|
+
sessionroot.useXRAnchor = this.useXRAnchor;
|
208
236
|
}
|
209
237
|
|
210
238
|
// handle VR controls
|
@@ -225,6 +253,8 @@
|
|
225
253
|
}
|
226
254
|
|
227
255
|
onLeaveXR(_: NeedleXREventArgs): void {
|
256
|
+
if (!this.isActiveWebXR) return;
|
257
|
+
|
228
258
|
// revert XR flags
|
229
259
|
XRState.Global.Set(this._previousXRState);
|
230
260
|
|
@@ -236,6 +266,8 @@
|
|
236
266
|
this._createdComponentsInSession.length = 0;
|
237
267
|
|
238
268
|
this.handleOfferSession();
|
269
|
+
|
270
|
+
delayForFrames(1).then(() => WebXR.activeWebXRComponent = null);
|
239
271
|
}
|
240
272
|
|
241
273
|
|
@@ -6,6 +6,7 @@
|
|
6
6
|
import { destroy } from "../../engine/engine_gameobject.js";
|
7
7
|
import { Gizmos } from "../../engine/engine_gizmos.js";
|
8
8
|
import { serializable } from "../../engine/engine_serialization.js";
|
9
|
+
import { setVisibleInCustomShadowRendering } from "../../engine/engine_three_utils.js";
|
9
10
|
import type { Vec3 } from "../../engine/engine_types.js";
|
10
11
|
import { getParam } from "../../engine/engine_utils.js";
|
11
12
|
import type { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
@@ -62,6 +63,7 @@
|
|
62
63
|
* If true an occluder material will be applied to the tracked planes/meshes.
|
63
64
|
* Note: this will only be applied if dataTemplate is not assigned
|
64
65
|
*/
|
66
|
+
@serializable()
|
65
67
|
occluder = true;
|
66
68
|
|
67
69
|
/**
|
@@ -113,6 +115,15 @@
|
|
113
115
|
}
|
114
116
|
}
|
115
117
|
|
118
|
+
onLeaveXR(_args: NeedleXREventArgs): void {
|
119
|
+
for (const data of this._allPlanes.keys()) {
|
120
|
+
this.removeData(data, this._allPlanes);
|
121
|
+
}
|
122
|
+
for (const data of this._allMeshes.keys()) {
|
123
|
+
this.removeData(data, this._allMeshes);
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
116
127
|
/** @internal */
|
117
128
|
onUpdateXR(args: NeedleXREventArgs): void {
|
118
129
|
|
@@ -193,14 +204,17 @@
|
|
193
204
|
if (!dataContext) return;
|
194
205
|
_all.delete(data);
|
195
206
|
if (debug) console.log("Plane no longer tracked, id=" + dataContext.id);
|
196
|
-
if (dataContext.mesh
|
197
|
-
dataContext.mesh.
|
207
|
+
if (dataContext.mesh) {
|
208
|
+
dataContext.mesh.removeFromParent();
|
198
209
|
dataContext.mesh.traverse(x => {
|
199
210
|
const nc = x.userData["normalsHelper"];
|
200
211
|
if (nc) {
|
201
|
-
this.context.scene.remove(nc);
|
202
212
|
nc.dispose();
|
213
|
+
nc.removeFromParent();
|
203
214
|
}
|
215
|
+
else if(debug) {
|
216
|
+
console.warn("No normals helper found for mesh", dataContext.mesh);
|
217
|
+
}
|
204
218
|
});
|
205
219
|
destroy(dataContext.mesh, true, true);
|
206
220
|
}
|
@@ -219,9 +233,31 @@
|
|
219
233
|
private readonly _allMeshes = new Map<XRMesh, XRPlaneContext>();
|
220
234
|
private firstTimeNoPlanesDetected = -100;
|
221
235
|
|
236
|
+
|
237
|
+
private makeOccluder = (mesh: Mesh, m: Material | Array<Material>, force: boolean = false) => {
|
238
|
+
if (!m) return;
|
239
|
+
if (m instanceof Array) {
|
240
|
+
for (const m0 of m)
|
241
|
+
this.makeOccluder(mesh, m0, force);
|
242
|
+
return;
|
243
|
+
}
|
244
|
+
if (!force && !m.name.toLowerCase().includes("occlu")) return;
|
245
|
+
m.colorWrite = false;
|
246
|
+
m.depthTest = true;
|
247
|
+
m.depthWrite = true;
|
248
|
+
m.transparent = false;
|
249
|
+
m.polygonOffset = true;
|
250
|
+
// positive values are below
|
251
|
+
m.polygonOffsetFactor = 1;
|
252
|
+
m.polygonOffsetUnits = .1;
|
253
|
+
mesh.renderOrder = -1000;
|
254
|
+
}
|
255
|
+
|
256
|
+
|
222
257
|
private processFrameData(_xr: NeedleXRSession, rig: Object3D, frame: XRFramePlanes, detected: Set<XRPlane | XRMesh>, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
|
223
258
|
const renderer = this.context.renderer;
|
224
259
|
const referenceSpace = renderer.xr.getReferenceSpace();
|
260
|
+
|
225
261
|
if (!referenceSpace) return;
|
226
262
|
|
227
263
|
for (const data of _all.keys()) {
|
@@ -238,24 +274,6 @@
|
|
238
274
|
const planePose = frame.getPose(space, referenceSpace);
|
239
275
|
|
240
276
|
let planeMesh: Object3D | undefined;
|
241
|
-
|
242
|
-
const makeBlockerMaterials = (m: Material | Array<Material>) => {
|
243
|
-
if (!m) return;
|
244
|
-
if (m instanceof Array) {
|
245
|
-
for (const m0 of m)
|
246
|
-
makeBlockerMaterials(m0);
|
247
|
-
return;
|
248
|
-
}
|
249
|
-
if (!m.name.includes("Occlu")) return;
|
250
|
-
m.colorWrite = false;
|
251
|
-
m.depthWrite = true;
|
252
|
-
m.transparent = false;
|
253
|
-
m.polygonOffset = true;
|
254
|
-
m.polygonOffsetFactor = 1;
|
255
|
-
m.polygonOffsetUnits = 0.1;
|
256
|
-
m["_renderOrder"] = -1000;
|
257
|
-
}
|
258
|
-
|
259
277
|
// If the plane already existed just update it
|
260
278
|
if (_all.has(data)) {
|
261
279
|
const planeContext = _all.get(data)!;
|
@@ -271,14 +289,14 @@
|
|
271
289
|
if (planeContext.mesh instanceof Mesh) {
|
272
290
|
planeContext.mesh.geometry.dispose();
|
273
291
|
planeContext.mesh.geometry = geometry;
|
274
|
-
|
292
|
+
this.makeOccluder(planeContext.mesh, planeContext.mesh.material);
|
275
293
|
}
|
276
294
|
else if (planeContext.mesh instanceof Group) {
|
277
295
|
for (const ch of planeContext.mesh.children) {
|
278
296
|
if (ch instanceof Mesh) {
|
279
297
|
ch.geometry.dispose();
|
280
298
|
ch.geometry = geometry;
|
281
|
-
|
299
|
+
this.makeOccluder(ch, ch.material);
|
282
300
|
}
|
283
301
|
}
|
284
302
|
}
|
@@ -317,13 +335,13 @@
|
|
317
335
|
}
|
318
336
|
// Otherwise we create a new plane instance
|
319
337
|
else {
|
320
|
-
|
321
338
|
// if we don't have any template assigned we just use a simple mesh object
|
322
339
|
if (!this.dataTemplate) {
|
323
340
|
const mesh = new Mesh();
|
324
341
|
if (debug) mesh.material = new MeshNormalMaterial();
|
325
342
|
else if (this.occluder) {
|
326
|
-
mesh.material = new MeshBasicMaterial(
|
343
|
+
mesh.material = new MeshBasicMaterial();
|
344
|
+
this.makeOccluder(mesh, mesh.material, true);
|
327
345
|
}
|
328
346
|
else {
|
329
347
|
mesh.material = new MeshBasicMaterial({ wireframe: true, opacity: .5, transparent: true, color: 0x333333 });
|
@@ -337,19 +355,21 @@
|
|
337
355
|
else {
|
338
356
|
// Create instance
|
339
357
|
const newPlane = GameObject.instantiate(this.dataTemplate.asset) as GameObject;
|
358
|
+
newPlane.name = "xr-tracked-plane";
|
340
359
|
planeMesh = newPlane;
|
360
|
+
setVisibleInCustomShadowRendering(newPlane, false);
|
341
361
|
|
342
362
|
if (newPlane instanceof Mesh) {
|
343
363
|
disposeObjectResources(newPlane.geometry);
|
344
364
|
newPlane.geometry = this.createGeometry(data);
|
345
|
-
|
365
|
+
this.makeOccluder(newPlane, newPlane.material, this.occluder);
|
346
366
|
}
|
347
367
|
else if (newPlane instanceof Group) {
|
348
368
|
for (const ch of newPlane.children) {
|
349
369
|
if (ch instanceof Mesh) {
|
350
370
|
disposeObjectResources(ch.geometry);
|
351
371
|
ch.geometry = this.createGeometry(data);
|
352
|
-
|
372
|
+
this.makeOccluder(ch, ch.material, this.occluder);
|
353
373
|
}
|
354
374
|
}
|
355
375
|
}
|
@@ -368,6 +388,9 @@
|
|
368
388
|
// newPlane.getComponent(MeshCollider)!.sharedMesh = newPlane as unknown as Mesh;
|
369
389
|
newPlane.matrixAutoUpdate = false;
|
370
390
|
newPlane.matrixWorldNeedsUpdate = true; // force update of rendering settings and so on
|
391
|
+
// TODO: in VR this has issues when the rig is moved
|
392
|
+
// newPlane.matrixWorld.multiply(rig.matrix.invert());
|
393
|
+
// this.context.scene.add(newPlane);
|
371
394
|
rig.add(newPlane);
|
372
395
|
|
373
396
|
const planeContext: XRPlaneContext = {
|
@@ -410,11 +433,17 @@
|
|
410
433
|
if (debug) {
|
411
434
|
planeMesh.traverse(x => {
|
412
435
|
if (!(x instanceof Mesh)) return;
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
436
|
+
if(x.userData["normalsHelper"]){
|
437
|
+
const helper = x.userData["normalsHelper"] as VertexNormalsHelper;
|
438
|
+
helper.update();
|
439
|
+
}
|
440
|
+
else {
|
441
|
+
const normalsHelper = new VertexNormalsHelper(x, 0.05, 0x0000ff);
|
442
|
+
normalsHelper.layers.disableAll();
|
443
|
+
normalsHelper.layers.set(2);
|
444
|
+
this.context.scene.add(normalsHelper);
|
445
|
+
x.userData["normalsHelper"] = normalsHelper;
|
446
|
+
}
|
418
447
|
});
|
419
448
|
}
|
420
449
|
}
|
@@ -14,8 +14,8 @@
|
|
14
14
|
import { NeedleXRController, type NeedleXRControllerEventArgs, type NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
|
15
15
|
import { registerExtensions } from "../../../engine/extensions/extensions.js";
|
16
16
|
import { NEEDLE_progressive } from "../../../engine/extensions/NEEDLE_progressive.js";
|
17
|
+
import { flipForwardMatrix } from "../../../engine/xr/internal.js";
|
17
18
|
import { Behaviour, GameObject } from "../../Component.js"
|
18
|
-
import { flipForwardMatrix } from "../../../engine/xr/internal.js";
|
19
19
|
|
20
20
|
const debug = getParam("debugwebxr");
|
21
21
|
|
@@ -337,6 +337,7 @@
|
|
337
337
|
mat.depthWrite = true;
|
338
338
|
mat.depthTest = true;
|
339
339
|
mat.colorWrite = false;
|
340
|
+
obj.receiveShadow = false;
|
340
341
|
obj.renderOrder = -100;
|
341
342
|
}
|
342
343
|
}
|