@terraware/web-components 5.0.0 → 5.0.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/VirtualWalkthrough/BoundaryWall.d.ts +0 -1
- package/components/VirtualWalkthrough/BoundaryWall.d.ts.map +1 -1
- package/components/VirtualWalkthrough/SplatControls.js +4 -3
- package/components/VirtualWalkthrough/SplatModel.d.ts +16 -1
- package/components/VirtualWalkthrough/SplatModel.d.ts.map +1 -1
- package/components/VirtualWalkthrough/SplatModel.js +18 -1
- package/components/VirtualWalkthrough/TfAnnotationManager.d.ts +2 -9
- package/components/VirtualWalkthrough/TfAnnotationManager.d.ts.map +1 -1
- package/components/VirtualWalkthrough/TfAnnotationManager.js +11 -32
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts +22 -1
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts.map +1 -1
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +54 -26
- package/components/VirtualWalkthrough/VrAnnotationPanel.d.ts +1 -2
- package/components/VirtualWalkthrough/VrAnnotationPanel.d.ts.map +1 -1
- package/components/VirtualWalkthrough/VrAnnotationPanel.js +2 -4
- package/components/VirtualWalkthrough/XrStartPose.d.ts +52 -0
- package/components/VirtualWalkthrough/XrStartPose.d.ts.map +1 -0
- package/components/VirtualWalkthrough/XrStartPose.js +128 -0
- package/components/VirtualWalkthrough/boundary-wall.d.ts +0 -2
- package/components/VirtualWalkthrough/boundary-wall.d.ts.map +1 -1
- package/components/VirtualWalkthrough/boundary-wall.js +0 -2
- package/components/VirtualWalkthrough/useXrRenderTuning.d.ts +10 -0
- package/components/VirtualWalkthrough/useXrRenderTuning.d.ts.map +1 -0
- package/components/VirtualWalkthrough/useXrRenderTuning.js +46 -0
- package/components/VirtualWalkthrough/vr-annotation-panel.d.ts +0 -1
- package/components/VirtualWalkthrough/vr-annotation-panel.d.ts.map +1 -1
- package/components/VirtualWalkthrough/vr-annotation-panel.js +6 -9
- package/components/VirtualWalkthrough/walkthrough-camera.d.ts +0 -1
- package/components/VirtualWalkthrough/walkthrough-camera.d.ts.map +1 -1
- package/components/VirtualWalkthrough/walkthrough-camera.js +3 -5
- package/components/VirtualWalkthrough/xr-render-tuning.d.ts +36 -0
- package/components/VirtualWalkthrough/xr-render-tuning.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-render-tuning.js +54 -0
- package/components/VirtualWalkthrough/xr-start-pose.d.ts +57 -0
- package/components/VirtualWalkthrough/xr-start-pose.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-start-pose.js +80 -0
- package/hooks/useXr.d.ts.map +1 -1
- package/hooks/useXr.js +10 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BoundaryWall.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/BoundaryWall.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAIlC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,
|
|
1
|
+
{"version":3,"file":"BoundaryWall.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/BoundaryWall.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAIlC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,QAAA,MAAM,YAAY,GAAI,wCAAwC,iBAAiB,4CAuB9E,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
|
@@ -47,7 +47,8 @@ const SplatControls = ({
|
|
|
47
47
|
const theme = useTheme();
|
|
48
48
|
const strings = useStrings();
|
|
49
49
|
const {
|
|
50
|
-
isDesktop
|
|
50
|
+
isDesktop,
|
|
51
|
+
isMobile
|
|
51
52
|
} = useDeviceInfo();
|
|
52
53
|
const {
|
|
53
54
|
setCamera,
|
|
@@ -140,10 +141,10 @@ const SplatControls = ({
|
|
|
140
141
|
gap: 1,
|
|
141
142
|
pointerEvents: 'auto'
|
|
142
143
|
},
|
|
143
|
-
children: [isXrAvailable('AR') && !isEdit && /*#__PURE__*/jsx(Button, {
|
|
144
|
+
children: [isMobile && isXrAvailable('AR') && !isEdit && /*#__PURE__*/jsx(Button, {
|
|
144
145
|
label: strings.AR,
|
|
145
146
|
onClick: () => startXr('AR')
|
|
146
|
-
}), isXrAvailable('VR') && !isEdit && /*#__PURE__*/jsx(Button, {
|
|
147
|
+
}), !isMobile && isXrAvailable('VR') && !isEdit && /*#__PURE__*/jsx(Button, {
|
|
147
148
|
label: strings.VR,
|
|
148
149
|
onClick: () => startXr('VR')
|
|
149
150
|
}), isDesktop && editable && !isEdit && onToggleEdit && /*#__PURE__*/jsx(Button, {
|
|
@@ -9,14 +9,29 @@ export interface SplatModelProps {
|
|
|
9
9
|
*/
|
|
10
10
|
splatFormat?: SplatFormat;
|
|
11
11
|
rotation?: [number, number, number];
|
|
12
|
+
/**
|
|
13
|
+
* Scale applied to the splat entity. Splat models come out of reconstruction in their own
|
|
14
|
+
* arbitrary units, so this is what converts them world units.
|
|
15
|
+
*/
|
|
16
|
+
scaleFactor?: number;
|
|
12
17
|
cropAabbMin?: [number, number, number];
|
|
13
18
|
cropAabbMax?: [number, number, number];
|
|
14
19
|
cropEdgeScaleFactor?: number;
|
|
15
20
|
cropFade?: boolean;
|
|
16
21
|
cropFadeDistance?: number;
|
|
17
22
|
revealRain?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Maximum allowable budget for splat rendering operations. Only applies to streamed sog models.
|
|
25
|
+
* A value of 0 means no limit. Left at the engine's default when omitted.
|
|
26
|
+
*/
|
|
27
|
+
splatBudget?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Caps the device pixel ratio the scene renders at, trading sharpness for fill rate on high-DPI
|
|
30
|
+
* displays. Left at the engine's default when omitted.
|
|
31
|
+
*/
|
|
32
|
+
maxPixelRatio?: number;
|
|
18
33
|
onError?: (error: Error) => void;
|
|
19
34
|
}
|
|
20
|
-
declare const _default: React.MemoExoticComponent<({ splatSrc, splatFormat, rotation, cropAabbMin, cropAabbMax, cropEdgeScaleFactor, cropFade, cropFadeDistance, revealRain, onError, }: SplatModelProps) => import("react/jsx-runtime").JSX.Element | null>;
|
|
35
|
+
declare const _default: React.MemoExoticComponent<({ splatSrc, splatFormat, rotation, scaleFactor, cropAabbMin, cropAabbMax, cropEdgeScaleFactor, cropFade, cropFadeDistance, revealRain, splatBudget, maxPixelRatio, onError, }: SplatModelProps) => import("react/jsx-runtime").JSX.Element | null>;
|
|
21
36
|
export default _default;
|
|
22
37
|
//# sourceMappingURL=SplatModel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SplatModel.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/SplatModel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0B,MAAM,OAAO,CAAC;AAU/C,OAAO,EAAE,WAAW,EAA0C,MAAM,eAAe,CAAC;AAEpF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;
|
|
1
|
+
{"version":3,"file":"SplatModel.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/SplatModel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0B,MAAM,OAAO,CAAC;AAU/C,OAAO,EAAE,WAAW,EAA0C,MAAM,eAAe,CAAC;AAEpF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;kOAgBE,eAAe;AAgDlB,wBAAgC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { memo, useEffect } from 'react';
|
|
2
2
|
import { Entity } from '@playcanvas/react';
|
|
3
3
|
import { GSplat } from '@playcanvas/react/components';
|
|
4
|
-
import { useSplat } from '@playcanvas/react/hooks';
|
|
4
|
+
import { useApp, useSplat } from '@playcanvas/react/hooks';
|
|
5
5
|
import BlockingSpinner from './BlockingSpinner.js';
|
|
6
6
|
import SplatCrop from './SplatCrop.js';
|
|
7
7
|
import SplatFadeCrop from './SplatFadeCrop.js';
|
|
@@ -13,14 +13,19 @@ const SplatModel = ({
|
|
|
13
13
|
splatSrc,
|
|
14
14
|
splatFormat,
|
|
15
15
|
rotation,
|
|
16
|
+
scaleFactor = 1,
|
|
16
17
|
cropAabbMin,
|
|
17
18
|
cropAabbMax,
|
|
18
19
|
cropEdgeScaleFactor,
|
|
19
20
|
cropFade = false,
|
|
20
21
|
cropFadeDistance = 0.5,
|
|
21
22
|
revealRain = false,
|
|
23
|
+
splatBudget,
|
|
24
|
+
maxPixelRatio,
|
|
22
25
|
onError
|
|
23
26
|
}) => {
|
|
27
|
+
const app = useApp();
|
|
28
|
+
|
|
24
29
|
// The loader selects its parser from this filename rather than from splatSrc, so it has to name the
|
|
25
30
|
// format we are actually loading. See splatFormat.ts.
|
|
26
31
|
const filename = splatLoaderFilename(splatFormat ?? detectSplatFormat(splatSrc));
|
|
@@ -33,6 +38,17 @@ const SplatModel = ({
|
|
|
33
38
|
filename
|
|
34
39
|
}
|
|
35
40
|
});
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!app) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (splatBudget !== undefined) {
|
|
46
|
+
app.scene.gsplat.splatBudget = splatBudget;
|
|
47
|
+
}
|
|
48
|
+
if (maxPixelRatio !== undefined) {
|
|
49
|
+
app.graphicsDevice.maxPixelRatio = maxPixelRatio;
|
|
50
|
+
}
|
|
51
|
+
}, [app, splatBudget, maxPixelRatio]);
|
|
36
52
|
useEffect(() => {
|
|
37
53
|
if (error) {
|
|
38
54
|
onError?.(new Error(error));
|
|
@@ -47,6 +63,7 @@ const SplatModel = ({
|
|
|
47
63
|
return /*#__PURE__*/jsxs(Entity, {
|
|
48
64
|
name: "splat",
|
|
49
65
|
rotation: rotation,
|
|
66
|
+
scale: [scaleFactor, scaleFactor, scaleFactor],
|
|
50
67
|
children: [/*#__PURE__*/jsx(GSplat, {
|
|
51
68
|
asset: asset,
|
|
52
69
|
unified: true
|
|
@@ -8,7 +8,6 @@ import { AnnotationIconType } from './Annotation';
|
|
|
8
8
|
export declare class TfAnnotationManager extends PcAnnotationManager {
|
|
9
9
|
static scriptName: string;
|
|
10
10
|
private _maxWorldSize;
|
|
11
|
-
private _scratchScale;
|
|
12
11
|
private _customParentDom;
|
|
13
12
|
private _clickHandlersAttached;
|
|
14
13
|
private _hotspotBackgroundColor;
|
|
@@ -98,8 +97,8 @@ export declare class TfAnnotationManager extends PcAnnotationManager {
|
|
|
98
97
|
_updateAnnotationPositions(annotation: any, resources: any, screenPos: any): void;
|
|
99
98
|
/**
|
|
100
99
|
* Override to also disable the hotspot mesh (not just the DOM) when the annotation is behind the
|
|
101
|
-
* camera. The base implementation leaves the mesh enabled, so a not-yet-scaled plane
|
|
102
|
-
*
|
|
100
|
+
* camera. The base implementation leaves the mesh enabled, so a not-yet-scaled plane would stay
|
|
101
|
+
* in view at its full unit size until the camera moves the anchor in front.
|
|
103
102
|
* @private
|
|
104
103
|
*/
|
|
105
104
|
_hideAnnotationElements(annotation: any, resources: any): void;
|
|
@@ -109,12 +108,6 @@ export declare class TfAnnotationManager extends PcAnnotationManager {
|
|
|
109
108
|
* @private
|
|
110
109
|
*/
|
|
111
110
|
_updateAnnotationRotationAndScale(annotation: any): void;
|
|
112
|
-
/**
|
|
113
|
-
* World-space uniform scale of the annotation entity's parent (e.g. a scaled
|
|
114
|
-
* content-root). Returns 1 when there is no parent or it has no scale.
|
|
115
|
-
* @private
|
|
116
|
-
*/
|
|
117
|
-
_getParentWorldScale(entity: any): number;
|
|
118
111
|
/**
|
|
119
112
|
* Override to create hotspot texture with custom background color.
|
|
120
113
|
* @private
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TfAnnotationManager.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/TfAnnotationManager.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,OAAO,
|
|
1
|
+
{"version":3,"file":"TfAnnotationManager.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/TfAnnotationManager.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,OAAO,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,iBAAiB,IAAI,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAIlG,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAelD;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,mBAAmB;IAC1D,MAAM,CAAC,UAAU,SAAyB;IAE1C,OAAO,CAAC,aAAa,CAAO;IAC5B,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,sBAAsB,CAAkB;IAChD,OAAO,CAAC,uBAAuB,CAAa;IAC5C,OAAO,CAAC,WAAW,CAAmD;IAEtE;;;;;;;;OAQG;IACH,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAE7B;IAED,IAAI,YAAY,IAJQ,MAAM,CAM7B;IAED;;;;;;;;;;OAUG;IACH,IAAI,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAEvC;IAED,IAAI,sBAAsB,IAJQ,MAAM,CAMvC;IAED;;OAEG;IACH,UAAU;IAgBV;;;;OAIG;IACH,eAAe;IAiBf;;;;OAIG;IACH,YAAY,CAAC,QAAQ,EAAE,kBAAkB;IAazC;;OAEG;IACH,MAAM;IAmCN;;;;OAIG;IACH,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAcnC;;;OAGG;IACH,oBAAoB,CAAC,UAAU,EAAE,GAAG;IAiBpC;;;;;OAKG;IACH,SAAS,CAAC,GAAG,EAAE,wBAAwB,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM;IAW/E;;;;OAIG;IACH,cAAc,CAAC,UAAU,EAAE,GAAG;IAI9B;;;;OAIG;IACH,sBAAsB,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG;IASlD,mBAAmB,CAAC,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO;IAKrD,0BAA0B;IAK1B,YAAY,CAAC,WAAW,EAAE,GAAG;IAK7B,aAAa,CAAC,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM;IAI7C;;;OAGG;IACH,0BAA0B,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG;IA6B1E;;;;;OAKG;IACH,uBAAuB,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG;IAMvD;;;;OAIG;IACH,iCAAiC,CAAC,UAAU,EAAE,GAAG;IA2CjD;;;OAGG;IACH,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,SAAuB,EAAE,WAAW,SAAI,EAAE,IAAI,CAAC,EAAE,kBAAkB;CAyD7G"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement } from 'react';
|
|
2
2
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
-
import {
|
|
3
|
+
import { Texture, FILTER_LINEAR, PIXELFORMAT_RGBA8 } from 'playcanvas';
|
|
4
4
|
import { AnnotationManager } from 'playcanvas/scripts/esm/annotations.mjs';
|
|
5
5
|
import Icon from '../Icon/Icon.js';
|
|
6
6
|
|
|
@@ -24,7 +24,6 @@ const HOTSPOT_HOVER_SCALE = 1.3;
|
|
|
24
24
|
class TfAnnotationManager extends AnnotationManager {
|
|
25
25
|
static scriptName = 'tfAnnotationManager';
|
|
26
26
|
_maxWorldSize = 1.0;
|
|
27
|
-
_scratchScale = new Vec3();
|
|
28
27
|
_customParentDom = null;
|
|
29
28
|
_clickHandlersAttached = new Set();
|
|
30
29
|
_hotspotBackgroundColor = '#2C8658';
|
|
@@ -163,9 +162,9 @@ class TfAnnotationManager extends AnnotationManager {
|
|
|
163
162
|
super._registerAnnotation(annotation);
|
|
164
163
|
this._applyAnnotationIcon(annotation);
|
|
165
164
|
|
|
166
|
-
// Keep the hotspot mesh hidden until the first scale pass
|
|
167
|
-
// starts at unit
|
|
168
|
-
//
|
|
165
|
+
// Keep the hotspot mesh hidden until the first scale pass sizes it. The annotation entity
|
|
166
|
+
// starts at unit scale, so the plane would render a metre across for the frames before
|
|
167
|
+
// _updateAnnotationRotationAndScale runs.
|
|
169
168
|
const resources = this._annotationResources.get(annotation);
|
|
170
169
|
if (resources) {
|
|
171
170
|
resources.baseEntity.enabled = false;
|
|
@@ -281,8 +280,8 @@ class TfAnnotationManager extends AnnotationManager {
|
|
|
281
280
|
|
|
282
281
|
/**
|
|
283
282
|
* Override to also disable the hotspot mesh (not just the DOM) when the annotation is behind the
|
|
284
|
-
* camera. The base implementation leaves the mesh enabled, so a not-yet-scaled plane
|
|
285
|
-
*
|
|
283
|
+
* camera. The base implementation leaves the mesh enabled, so a not-yet-scaled plane would stay
|
|
284
|
+
* in view at its full unit size until the camera moves the anchor in front.
|
|
286
285
|
* @private
|
|
287
286
|
*/
|
|
288
287
|
_hideAnnotationElements(annotation, resources) {
|
|
@@ -309,14 +308,9 @@ class TfAnnotationManager extends AnnotationManager {
|
|
|
309
308
|
// The world size that projects to _hotspotSize screen pixels at this distance.
|
|
310
309
|
const targetWorldSize = this._hotspotSize / screenHeight * (2 * distance / projMatrix.data[5]);
|
|
311
310
|
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
const
|
|
315
|
-
const unclampedScale = targetWorldSize / parentScale;
|
|
316
|
-
|
|
317
|
-
// Clamp in the annotation's own (parent-local) space so maxWorldSize keeps the
|
|
318
|
-
// same size relative to the model regardless of the content-root scale.
|
|
319
|
-
const clampedScale = Math.min(unclampedScale, this._maxWorldSize);
|
|
311
|
+
// Annotations hang off an unscaled parent, so local scale is world scale and maxWorldSize
|
|
312
|
+
// clamps the hotspot at a size in metres.
|
|
313
|
+
const clampedScale = Math.min(targetWorldSize, this._maxWorldSize);
|
|
320
314
|
const hovered = this._hoverAnnotation === annotation;
|
|
321
315
|
const hoverScale = hovered ? HOTSPOT_HOVER_SCALE : 1;
|
|
322
316
|
const entityScale = clampedScale * hoverScale;
|
|
@@ -324,8 +318,7 @@ class TfAnnotationManager extends AnnotationManager {
|
|
|
324
318
|
|
|
325
319
|
// Scale the hotspot DOM element proportionally when clamped, including the
|
|
326
320
|
// hover growth so it tracks the visible circle. This is a pure screen-space
|
|
327
|
-
// size, so it uses the true _hotspotSize and the clamp ratio only
|
|
328
|
-
// parent scale, which the DOM overlay does not inherit).
|
|
321
|
+
// size, so it uses the true _hotspotSize and the clamp ratio only.
|
|
329
322
|
const resources = this._annotationResources.get(annotation);
|
|
330
323
|
if (resources) {
|
|
331
324
|
// This branch only runs for annotations in front of the camera, and the scale above is now
|
|
@@ -334,7 +327,7 @@ class TfAnnotationManager extends AnnotationManager {
|
|
|
334
327
|
resources.overlayEntity.enabled = true;
|
|
335
328
|
}
|
|
336
329
|
if (resources && resources.hotspotDom) {
|
|
337
|
-
const clampRatio = clampedScale /
|
|
330
|
+
const clampRatio = clampedScale / targetWorldSize;
|
|
338
331
|
const baseSize = this._hotspotSize + 5; // Match the +5 from the stylesheet
|
|
339
332
|
const scaledSize = baseSize * clampRatio * hoverScale;
|
|
340
333
|
resources.hotspotDom.style.width = `${scaledSize}px`;
|
|
@@ -342,20 +335,6 @@ class TfAnnotationManager extends AnnotationManager {
|
|
|
342
335
|
}
|
|
343
336
|
}
|
|
344
337
|
|
|
345
|
-
/**
|
|
346
|
-
* World-space uniform scale of the annotation entity's parent (e.g. a scaled
|
|
347
|
-
* content-root). Returns 1 when there is no parent or it has no scale.
|
|
348
|
-
* @private
|
|
349
|
-
*/
|
|
350
|
-
_getParentWorldScale(entity) {
|
|
351
|
-
const parent = entity.parent;
|
|
352
|
-
if (!parent) {
|
|
353
|
-
return 1;
|
|
354
|
-
}
|
|
355
|
-
parent.getWorldTransform().getScale(this._scratchScale);
|
|
356
|
-
return this._scratchScale.x || 1;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
338
|
/**
|
|
360
339
|
* Override to create hotspot texture with custom background color.
|
|
361
340
|
* @private
|
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
import { AnnotationProps } from './Annotation';
|
|
2
|
+
import { SplatModelProps } from './SplatModel';
|
|
2
3
|
import { SplatFormat } from './splatFormat';
|
|
4
|
+
/**
|
|
5
|
+
* Every coordinate, distance and size in this component is world-space, in the metres an XR
|
|
6
|
+
* headset imposes on the scene: `scaleFactor` converts the splat's own arbitrary units to them,
|
|
7
|
+
* and is applied to nothing but the splat itself.
|
|
8
|
+
*/
|
|
3
9
|
export interface VirtualWalkthroughViewerProps {
|
|
4
10
|
splatSrc: string;
|
|
5
11
|
splatFormat?: SplatFormat;
|
|
12
|
+
/** World-space point the camera looks at. */
|
|
6
13
|
origin?: [number, number, number];
|
|
14
|
+
/** World-space point the camera starts at. */
|
|
7
15
|
cameraPosition?: [number, number, number];
|
|
16
|
+
/** World-space centre (x, y, z) and radius (m) of the circle the camera is held inside. */
|
|
8
17
|
sceneBounds?: {
|
|
9
18
|
x: number;
|
|
10
19
|
y: number;
|
|
11
20
|
z: number;
|
|
12
21
|
m: number;
|
|
13
22
|
};
|
|
23
|
+
/** Three world-space points defining the plane the camera walks on. */
|
|
14
24
|
groundPlane?: [number, number, number][];
|
|
15
25
|
skyColor?: string;
|
|
16
26
|
groundColor?: string;
|
|
27
|
+
/** Eye height (m) above the ground plane. */
|
|
17
28
|
averageCameraHeight?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Scale that converts the splat model's units to the world's metres. Applied to the splat
|
|
31
|
+
* entity alone — every other coordinate this component takes is already world-space.
|
|
32
|
+
*/
|
|
18
33
|
scaleFactor?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Overrides passed through to the splat model, for tuning things like `splatBudget` and
|
|
36
|
+
* `maxPixelRatio`. The props this component drives itself — `splatSrc`, `splatFormat`,
|
|
37
|
+
* `rotation`, `scaleFactor` and `revealRain` — take precedence over anything set here.
|
|
38
|
+
*/
|
|
39
|
+
splatModelProps?: Partial<SplatModelProps>;
|
|
19
40
|
annotations: AnnotationProps[];
|
|
20
41
|
onSaveAnnotations: (annotations: AnnotationProps[]) => void | Promise<void>;
|
|
21
42
|
editable?: boolean;
|
|
@@ -24,6 +45,6 @@ export interface VirtualWalkthroughViewerProps {
|
|
|
24
45
|
onToggleFullScreen?: () => void;
|
|
25
46
|
maxImagesPerAnnotation?: number;
|
|
26
47
|
}
|
|
27
|
-
declare const VirtualWalkthroughViewer: ({ splatSrc, splatFormat, origin, cameraPosition, sceneBounds, groundPlane: groundPlaneProp, skyColor, groundColor, averageCameraHeight, scaleFactor, annotations, onSaveAnnotations, editable, showFreeFly, isFullScreen, onToggleFullScreen, maxImagesPerAnnotation, }: VirtualWalkthroughViewerProps) => import("react/jsx-runtime").JSX.Element;
|
|
48
|
+
declare const VirtualWalkthroughViewer: ({ splatSrc, splatFormat, origin, cameraPosition, sceneBounds, groundPlane: groundPlaneProp, skyColor, groundColor, averageCameraHeight, scaleFactor, splatModelProps, annotations, onSaveAnnotations, editable, showFreeFly, isFullScreen, onToggleFullScreen, maxImagesPerAnnotation, }: VirtualWalkthroughViewerProps) => import("react/jsx-runtime").JSX.Element;
|
|
28
49
|
export default VirtualWalkthroughViewer;
|
|
29
50
|
//# sourceMappingURL=VirtualWalkthroughViewer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VirtualWalkthroughViewer.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/VirtualWalkthroughViewer.tsx"],"names":[],"mappings":"AAYA,OAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"VirtualWalkthroughViewer.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/VirtualWalkthroughViewer.tsx"],"names":[],"mappings":"AAYA,OAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAO3D,OAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAS3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAmB5C;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,6CAA6C;IAC7C,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,8CAA8C;IAC9C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,2FAA2F;IAC3F,WAAW,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,uEAAuE;IACvE,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAC3C,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,iBAAiB,EAAE,CAAC,WAAW,EAAE,eAAe,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,QAAA,MAAM,wBAAwB,GAAI,0RAmB/B,6BAA6B,4CA+X/B,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
|
|
@@ -23,6 +23,8 @@ import XrAnnotationInteraction from './XrAnnotationInteraction.js';
|
|
|
23
23
|
import XrExitButton from './XrExitButton.js';
|
|
24
24
|
import XrGazeDwell from './XrGazeDwell.js';
|
|
25
25
|
import XrPointerRay from './XrPointerRay.js';
|
|
26
|
+
import { XrStartPose } from './XrStartPose.js';
|
|
27
|
+
import useXrRenderTuning from './useXrRenderTuning.js';
|
|
26
28
|
import { WalkthroughCamera } from './walkthrough-camera.js';
|
|
27
29
|
import { rayHitsInteractiveUi } from './xr-interactive-ui.js';
|
|
28
30
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
@@ -32,9 +34,21 @@ const DEFAULT_POSITION = [1, 0.1, 0];
|
|
|
32
34
|
|
|
33
35
|
// How far outside the clamp radius the boundary wall stands. Without it the wall would be coplanar
|
|
34
36
|
// with the surface the head is clamped to, so a pinned user would have it in their face and through
|
|
35
|
-
// their near clip plane.
|
|
36
|
-
// scaleFactor: it is about the size of the user's body, not about the size of the scene.
|
|
37
|
+
// their near clip plane.
|
|
37
38
|
const WALL_INSET = 0.25;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Largest world size (metres) a hotspot is allowed to reach as the camera approaches it, so it
|
|
42
|
+
* stops growing rather than filling the view from close up.
|
|
43
|
+
*/
|
|
44
|
+
const MAX_HOTSPOT_WORLD_SIZE = 0.75;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Every coordinate, distance and size in this component is world-space, in the metres an XR
|
|
48
|
+
* headset imposes on the scene: `scaleFactor` converts the splat's own arbitrary units to them,
|
|
49
|
+
* and is applied to nothing but the splat itself.
|
|
50
|
+
*/
|
|
51
|
+
|
|
38
52
|
const VirtualWalkthroughViewer = ({
|
|
39
53
|
splatSrc,
|
|
40
54
|
splatFormat,
|
|
@@ -46,6 +60,7 @@ const VirtualWalkthroughViewer = ({
|
|
|
46
60
|
groundColor,
|
|
47
61
|
averageCameraHeight = 0,
|
|
48
62
|
scaleFactor = 1,
|
|
63
|
+
splatModelProps,
|
|
49
64
|
annotations,
|
|
50
65
|
onSaveAnnotations,
|
|
51
66
|
editable = false,
|
|
@@ -77,6 +92,7 @@ const VirtualWalkthroughViewer = ({
|
|
|
77
92
|
isCurrentlyInVr,
|
|
78
93
|
isCurrentlyInAr
|
|
79
94
|
} = useXr();
|
|
95
|
+
useXrRenderTuning();
|
|
80
96
|
const sceneBoundsRadius = useMemo(() => {
|
|
81
97
|
if (sceneBounds?.m !== undefined) {
|
|
82
98
|
return sceneBounds.m;
|
|
@@ -84,36 +100,45 @@ const VirtualWalkthroughViewer = ({
|
|
|
84
100
|
const dx = cameraPosition[0] - origin[0];
|
|
85
101
|
const dy = cameraPosition[1] - origin[1];
|
|
86
102
|
const dz = cameraPosition[2] - origin[2];
|
|
87
|
-
return Math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
103
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
88
104
|
}, [cameraPosition, sceneBounds, origin]);
|
|
89
105
|
const sceneBoundsCenter = useMemo(() => sceneBounds ? new Vec3(sceneBounds.x, sceneBounds.y, sceneBounds.z) : new Vec3(origin[0], cameraPosition[1], origin[2]), [sceneBounds, origin, cameraPosition]);
|
|
90
|
-
const cameraBoundsCenter = useMemo(() => sceneBoundsCenter.clone().mulScalar(scaleFactor), [sceneBoundsCenter, scaleFactor]);
|
|
91
106
|
const groundPlane = useMemo(() => groundPlaneProp?.length === 3 ? groundPlaneProp.map(p => new Vec3(p[0], p[1], p[2])) : [], [groundPlaneProp]);
|
|
92
|
-
|
|
107
|
+
|
|
108
|
+
// Also runs when a session ends. WalkthroughCamera is unmounted for the duration of a session, so
|
|
109
|
+
// the camera entity is left holding the last head pose — an eye height above wherever in the rig
|
|
110
|
+
// the user was standing — which is neither where nor how high the walkthrough left off.
|
|
93
111
|
useEffect(() => {
|
|
112
|
+
if (isCurrentlyInXr) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
94
115
|
setCamera(origin, cameraPosition);
|
|
95
|
-
}, [origin, cameraPosition, setCamera]);
|
|
116
|
+
}, [origin, cameraPosition, setCamera, isCurrentlyInXr]);
|
|
117
|
+
|
|
118
|
+
// Re-applied when a session ends for the same reason: the script that comes back is a new
|
|
119
|
+
// instance, and without the ground plane it walks the camera at the bounds centre's height
|
|
120
|
+
// instead of an eye height above the ground.
|
|
96
121
|
useEffect(() => {
|
|
97
|
-
if (!
|
|
122
|
+
if (!groundPlane.length || isCurrentlyInXr) {
|
|
98
123
|
return;
|
|
99
124
|
}
|
|
100
125
|
// @ts-expect-error - scripts are added dynamically to the camera entity
|
|
101
126
|
const walkthroughCam = app.root.findByName('camera')?.script?.walkthroughCamera;
|
|
102
127
|
if (walkthroughCam) {
|
|
103
128
|
// Should be changed to a react prop if shallowEquals in playcanvas/react is fixed (see https://github.com/playcanvas/react/pull/298)
|
|
104
|
-
walkthroughCam.groundPlane =
|
|
129
|
+
walkthroughCam.groundPlane = groundPlane;
|
|
105
130
|
}
|
|
106
|
-
}, [
|
|
131
|
+
}, [groundPlane, app, isCurrentlyInXr]);
|
|
107
132
|
useEffect(() => {
|
|
108
133
|
// Set imperatively for the same reason as BoundaryRing: boundsCenter is a Vec3, and the
|
|
109
134
|
// @playcanvas/react memo() comparator stops at the first prop with an .equals() method.
|
|
110
135
|
// @ts-expect-error - scripts are added dynamically to the entity
|
|
111
136
|
const navigation = app.root.findByName('camera-root')?.script?.tfXrNavigation;
|
|
112
137
|
if (navigation) {
|
|
113
|
-
navigation.boundsCenter =
|
|
114
|
-
navigation.boundsRadius = sceneBoundsRadius
|
|
138
|
+
navigation.boundsCenter = sceneBoundsCenter;
|
|
139
|
+
navigation.boundsRadius = sceneBoundsRadius;
|
|
115
140
|
}
|
|
116
|
-
}, [app,
|
|
141
|
+
}, [app, sceneBoundsCenter, sceneBoundsRadius]);
|
|
117
142
|
const handleToggleFreeFly = useCallback(() => {
|
|
118
143
|
const newFreeFly = !isFreeFly;
|
|
119
144
|
// @ts-expect-error - scripts are added dynamically to the camera entity
|
|
@@ -228,11 +253,13 @@ const VirtualWalkthroughViewer = ({
|
|
|
228
253
|
}), [handleAnnotationUpdate]);
|
|
229
254
|
const canSave = useMemo(() => localAnnotations.every(annotation => annotation.title && annotation.title.trim() !== ''), [localAnnotations]);
|
|
230
255
|
const splatModel = useMemo(() => /*#__PURE__*/jsx(SplatModel, {
|
|
256
|
+
...splatModelProps,
|
|
231
257
|
splatSrc: splatSrc,
|
|
232
258
|
splatFormat: splatFormat,
|
|
233
259
|
rotation: [-180, 0, 0],
|
|
260
|
+
scaleFactor: scaleFactor,
|
|
234
261
|
revealRain: isHighPerformance
|
|
235
|
-
}, 'splat'), [isHighPerformance, splatSrc, splatFormat]);
|
|
262
|
+
}, 'splat'), [isHighPerformance, splatSrc, splatFormat, scaleFactor, splatModelProps]);
|
|
236
263
|
return /*#__PURE__*/jsxs(Fragment, {
|
|
237
264
|
children: [/*#__PURE__*/jsx(GradientSky, {
|
|
238
265
|
topColor: skyColor || '#FFFFFF',
|
|
@@ -247,13 +274,10 @@ const VirtualWalkthroughViewer = ({
|
|
|
247
274
|
fov: 60
|
|
248
275
|
}), !isCurrentlyInXr && /*#__PURE__*/jsx(Script, {
|
|
249
276
|
script: WalkthroughCamera,
|
|
250
|
-
boundsCenter:
|
|
251
|
-
boundsRadius: sceneBoundsRadius
|
|
277
|
+
boundsCenter: sceneBoundsCenter,
|
|
278
|
+
boundsRadius: sceneBoundsRadius,
|
|
252
279
|
enableFly: !isTextFieldFocused,
|
|
253
|
-
averageCameraHeight:
|
|
254
|
-
moveSpeed: 0.3 * scaleFactor,
|
|
255
|
-
moveFastSpeed: 0.5 * scaleFactor,
|
|
256
|
-
moveSlowSpeed: 0.15 * scaleFactor
|
|
280
|
+
averageCameraHeight: averageCameraHeight
|
|
257
281
|
})]
|
|
258
282
|
}), /*#__PURE__*/jsx(XrExitButton, {}), /*#__PURE__*/jsx(Script, {
|
|
259
283
|
script: XrControllers,
|
|
@@ -262,10 +286,15 @@ const VirtualWalkthroughViewer = ({
|
|
|
262
286
|
onDismiss: handleCloseAnnotation
|
|
263
287
|
}), /*#__PURE__*/jsx(XrPointerRay, {}), isCurrentlyInVr && viewingAnnotation && /*#__PURE__*/jsx(VrAnnotationPanel, {
|
|
264
288
|
annotation: viewingAnnotation,
|
|
265
|
-
annotationIndex: viewingAnnotationIndex
|
|
266
|
-
scaleFactor: scaleFactor
|
|
289
|
+
annotationIndex: viewingAnnotationIndex
|
|
267
290
|
}, viewingAnnotationIndex), isCurrentlyInXr && /*#__PURE__*/jsx(XrGazeDwell, {
|
|
268
291
|
activeIndex: viewingAnnotationIndex
|
|
292
|
+
}), /*#__PURE__*/jsx(Script, {
|
|
293
|
+
script: XrStartPose,
|
|
294
|
+
targetX: cameraPosition[0],
|
|
295
|
+
targetZ: cameraPosition[2],
|
|
296
|
+
focusX: origin[0],
|
|
297
|
+
focusZ: origin[2]
|
|
269
298
|
}), /*#__PURE__*/jsx(Script, {
|
|
270
299
|
script: TfXrNavigation,
|
|
271
300
|
enabled: !isEdit,
|
|
@@ -280,7 +309,6 @@ const VirtualWalkthroughViewer = ({
|
|
|
280
309
|
})]
|
|
281
310
|
}), /*#__PURE__*/jsxs(Entity, {
|
|
282
311
|
name: "content-root",
|
|
283
|
-
scale: [scaleFactor, scaleFactor, scaleFactor],
|
|
284
312
|
children: [splatModel, sceneBounds?.m !== undefined && groundPlane.length === 3 && /*#__PURE__*/jsx(BoundaryRing, {
|
|
285
313
|
center: sceneBoundsCenter,
|
|
286
314
|
radius: sceneBoundsRadius,
|
|
@@ -291,7 +319,7 @@ const VirtualWalkthroughViewer = ({
|
|
|
291
319
|
script: TfAnnotationManager,
|
|
292
320
|
enabled: true,
|
|
293
321
|
hotspotSize: 30,
|
|
294
|
-
maxWorldSize:
|
|
322
|
+
maxWorldSize: MAX_HOTSPOT_WORLD_SIZE,
|
|
295
323
|
opacity: 1,
|
|
296
324
|
hotspotColor: new Color().fromString(theme.palette.TwClrIcnBrand),
|
|
297
325
|
hoverColor: new Color().fromString('#ffffff'),
|
|
@@ -310,9 +338,9 @@ const VirtualWalkthroughViewer = ({
|
|
|
310
338
|
}, `annotation-${index}`))]
|
|
311
339
|
})]
|
|
312
340
|
}), isCurrentlyInXr && sceneBoundsRadius > 0 && /*#__PURE__*/jsx(BoundaryWall, {
|
|
313
|
-
center:
|
|
314
|
-
radius: sceneBoundsRadius
|
|
315
|
-
groundPlane:
|
|
341
|
+
center: sceneBoundsCenter,
|
|
342
|
+
radius: sceneBoundsRadius + WALL_INSET,
|
|
343
|
+
groundPlane: groundPlane,
|
|
316
344
|
baseY: 0
|
|
317
345
|
}), /*#__PURE__*/jsx(SplatControls, {
|
|
318
346
|
defaultCameraFocus: origin,
|
|
@@ -2,8 +2,7 @@ import type { AnnotationProps } from './Annotation';
|
|
|
2
2
|
interface VrAnnotationPanelProps {
|
|
3
3
|
annotation: AnnotationProps;
|
|
4
4
|
annotationIndex: number;
|
|
5
|
-
scaleFactor: number;
|
|
6
5
|
}
|
|
7
|
-
declare const VrAnnotationPanel: ({ annotation, annotationIndex
|
|
6
|
+
declare const VrAnnotationPanel: ({ annotation, annotationIndex }: VrAnnotationPanelProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
7
|
export default VrAnnotationPanel;
|
|
9
8
|
//# sourceMappingURL=VrAnnotationPanel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VrAnnotationPanel.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/VrAnnotationPanel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"VrAnnotationPanel.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/VrAnnotationPanel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,QAAA,MAAM,iBAAiB,GAAI,iCAAiC,sBAAsB,4CAWjF,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
|
@@ -6,8 +6,7 @@ import { jsx } from 'react/jsx-runtime';
|
|
|
6
6
|
|
|
7
7
|
const VrAnnotationPanel = ({
|
|
8
8
|
annotation,
|
|
9
|
-
annotationIndex
|
|
10
|
-
scaleFactor
|
|
9
|
+
annotationIndex
|
|
11
10
|
}) => /*#__PURE__*/jsx(Entity, {
|
|
12
11
|
name: "vr-annotation-panel",
|
|
13
12
|
children: /*#__PURE__*/jsx(Script, {
|
|
@@ -16,8 +15,7 @@ const VrAnnotationPanel = ({
|
|
|
16
15
|
label: annotation.label,
|
|
17
16
|
bodyText: annotation.bodyText,
|
|
18
17
|
imageUrls: annotation.imageUrls,
|
|
19
|
-
annotationIndex: annotationIndex
|
|
20
|
-
scaleFactor: scaleFactor
|
|
18
|
+
annotationIndex: annotationIndex
|
|
21
19
|
})
|
|
22
20
|
});
|
|
23
21
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Script } from 'playcanvas';
|
|
2
|
+
/**
|
|
3
|
+
* Starts an immersive session at the scene's camera position rather than at the world origin.
|
|
4
|
+
*
|
|
5
|
+
* PlayCanvas overwrites the camera entity's local transform with the head pose every frame, so the
|
|
6
|
+
* head lands wherever the headset's reference space puts it — near (0, 0) for a rig that has never
|
|
7
|
+
* been moved. This script moves the rig on the first frame of a session so the head instead starts
|
|
8
|
+
* at `targetX`/`targetZ` facing `focusX`/`focusZ`.
|
|
9
|
+
*
|
|
10
|
+
* Only XZ is touched: the user stands on the rig floor, which the boundary wall is also built
|
|
11
|
+
* against, so lifting the rig onto the splat's ground plane would put them through it.
|
|
12
|
+
*
|
|
13
|
+
* Attach to the rig entity — the camera's parent, alongside TfXrNavigation, whose bounds clamp
|
|
14
|
+
* pulls the start point back inside the scene bounds when the camera position sits outside them.
|
|
15
|
+
*/
|
|
16
|
+
export declare class XrStartPose extends Script {
|
|
17
|
+
static scriptName: string;
|
|
18
|
+
/** World-space X the head should start at, i.e. the viewer's `cameraPosition`. */
|
|
19
|
+
targetX: number;
|
|
20
|
+
/** World-space Z the head should start at. */
|
|
21
|
+
targetZ: number;
|
|
22
|
+
/** World-space X the head should face from there, i.e. the viewer's `origin`. */
|
|
23
|
+
focusX: number;
|
|
24
|
+
/** World-space Z the head should face from there. */
|
|
25
|
+
focusZ: number;
|
|
26
|
+
/** Set when a session starts, cleared once the rig has been placed. */
|
|
27
|
+
private _pending;
|
|
28
|
+
/**
|
|
29
|
+
* Whether the engine has written a head pose for this session yet.
|
|
30
|
+
*/
|
|
31
|
+
private _posed;
|
|
32
|
+
private _onXrStart;
|
|
33
|
+
/** Fired from XrManager.update only after the head pose has been written to the camera. */
|
|
34
|
+
private _onXrUpdate;
|
|
35
|
+
private _onXrEnd;
|
|
36
|
+
initialize(): void;
|
|
37
|
+
private _isXrActive;
|
|
38
|
+
/**
|
|
39
|
+
* The bounds circle to place the start point in, or undefined when nothing is clamping the head.
|
|
40
|
+
* Resolved per call rather than cached: the React wrapper assigns the bounds to the navigation
|
|
41
|
+
* script imperatively, so they can land after this script initializes.
|
|
42
|
+
*/
|
|
43
|
+
private _bounds;
|
|
44
|
+
/**
|
|
45
|
+
* Runs in update rather than on the 'start' event: the engine writes the head pose during
|
|
46
|
+
* xr.update, which precedes the script update in the same tick, so a frame that has posed is the
|
|
47
|
+
* first point at which there is a real head to solve the rig pose from.
|
|
48
|
+
*/
|
|
49
|
+
update(): void;
|
|
50
|
+
private _teardown;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=XrStartPose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"XrStartPose.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/XrStartPose.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAwB,MAAM,YAAY,CAAC;AAM1D;;;;;;;;;;;;;GAaG;AACH,qBAAa,WAAY,SAAQ,MAAM;IACrC,MAAM,CAAC,UAAU,SAAiB;IAElC,kFAAkF;IAClF,OAAO,SAAK;IAEZ,8CAA8C;IAC9C,OAAO,SAAK;IAEZ,iFAAiF;IACjF,MAAM,SAAK;IAEX,qDAAqD;IACrD,MAAM,SAAK;IAEX,uEAAuE;IACvE,OAAO,CAAC,QAAQ,CAAS;IAEzB;;OAEG;IACH,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,CAAC,UAAU,CAEhB;IAEF,2FAA2F;IAC3F,OAAO,CAAC,WAAW,CAEjB;IAEF,OAAO,CAAC,QAAQ,CAEd;IAEF,UAAU;IAWV,OAAO,CAAC,WAAW,CACoF;IAEvG;;;;OAIG;IACH,OAAO,CAAC,OAAO,CAQb;IAEF;;;;OAIG;IACH,MAAM;IA2BN,OAAO,CAAC,SAAS;CAKlB"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Script, XRTYPE_VR, XRTYPE_AR } from 'playcanvas';
|
|
2
|
+
import { xrStartRigPose, yawFromBasis } from './xr-start-pose.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Starts an immersive session at the scene's camera position rather than at the world origin.
|
|
6
|
+
*
|
|
7
|
+
* PlayCanvas overwrites the camera entity's local transform with the head pose every frame, so the
|
|
8
|
+
* head lands wherever the headset's reference space puts it — near (0, 0) for a rig that has never
|
|
9
|
+
* been moved. This script moves the rig on the first frame of a session so the head instead starts
|
|
10
|
+
* at `targetX`/`targetZ` facing `focusX`/`focusZ`.
|
|
11
|
+
*
|
|
12
|
+
* Only XZ is touched: the user stands on the rig floor, which the boundary wall is also built
|
|
13
|
+
* against, so lifting the rig onto the splat's ground plane would put them through it.
|
|
14
|
+
*
|
|
15
|
+
* Attach to the rig entity — the camera's parent, alongside TfXrNavigation, whose bounds clamp
|
|
16
|
+
* pulls the start point back inside the scene bounds when the camera position sits outside them.
|
|
17
|
+
*/
|
|
18
|
+
class XrStartPose extends Script {
|
|
19
|
+
static scriptName = 'xrStartPose';
|
|
20
|
+
|
|
21
|
+
/** World-space X the head should start at, i.e. the viewer's `cameraPosition`. */
|
|
22
|
+
targetX = 0;
|
|
23
|
+
|
|
24
|
+
/** World-space Z the head should start at. */
|
|
25
|
+
targetZ = 0;
|
|
26
|
+
|
|
27
|
+
/** World-space X the head should face from there, i.e. the viewer's `origin`. */
|
|
28
|
+
focusX = 0;
|
|
29
|
+
|
|
30
|
+
/** World-space Z the head should face from there. */
|
|
31
|
+
focusZ = 0;
|
|
32
|
+
|
|
33
|
+
/** Set when a session starts, cleared once the rig has been placed. */
|
|
34
|
+
_pending = false;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether the engine has written a head pose for this session yet.
|
|
38
|
+
*/
|
|
39
|
+
_posed = false;
|
|
40
|
+
_onXrStart = () => {
|
|
41
|
+
this._pending = true;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Fired from XrManager.update only after the head pose has been written to the camera. */
|
|
45
|
+
_onXrUpdate = () => {
|
|
46
|
+
this._posed = true;
|
|
47
|
+
};
|
|
48
|
+
_onXrEnd = () => {
|
|
49
|
+
this._posed = false;
|
|
50
|
+
};
|
|
51
|
+
initialize() {
|
|
52
|
+
// Covers mounting into a session that is already running as well as the usual mount before one.
|
|
53
|
+
this._pending = this._isXrActive();
|
|
54
|
+
this.app.xr?.on('start', this._onXrStart);
|
|
55
|
+
this.app.xr?.on('update', this._onXrUpdate);
|
|
56
|
+
this.app.xr?.on('end', this._onXrEnd);
|
|
57
|
+
// Script removal fires a 'destroy' event rather than calling a destroy() method, so the
|
|
58
|
+
// teardown has to be registered as a listener or these handlers outlive the script.
|
|
59
|
+
this.once('destroy', () => this._teardown());
|
|
60
|
+
}
|
|
61
|
+
_isXrActive = () => this.app.xr?.active === true && (this.app.xr?.type === XRTYPE_VR || this.app.xr?.type === XRTYPE_AR);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The bounds circle to place the start point in, or undefined when nothing is clamping the head.
|
|
65
|
+
* Resolved per call rather than cached: the React wrapper assigns the bounds to the navigation
|
|
66
|
+
* script imperatively, so they can land after this script initializes.
|
|
67
|
+
*/
|
|
68
|
+
_bounds = () => {
|
|
69
|
+
// @ts-expect-error - scripts are added dynamically to the entity
|
|
70
|
+
const navigation = this.entity.script?.tfXrNavigation;
|
|
71
|
+
if (!navigation || navigation.boundsRadius <= 0) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
x: navigation.boundsCenter.x,
|
|
76
|
+
z: navigation.boundsCenter.z,
|
|
77
|
+
radius: navigation.boundsRadius
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Runs in update rather than on the 'start' event: the engine writes the head pose during
|
|
83
|
+
* xr.update, which precedes the script update in the same tick, so a frame that has posed is the
|
|
84
|
+
* first point at which there is a real head to solve the rig pose from.
|
|
85
|
+
*/
|
|
86
|
+
update() {
|
|
87
|
+
if (!this._pending || !this._posed || !this._isXrActive()) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const camera = this.entity.findComponent('camera')?.entity;
|
|
91
|
+
if (!camera) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const head = camera.getPosition();
|
|
95
|
+
const rig = this.entity.getPosition();
|
|
96
|
+
const rigY = rig.y;
|
|
97
|
+
const pose = xrStartRigPose({
|
|
98
|
+
head: {
|
|
99
|
+
x: head.x,
|
|
100
|
+
z: head.z
|
|
101
|
+
},
|
|
102
|
+
headYaw: yawFromBasis(camera.forward, camera.right),
|
|
103
|
+
rig: {
|
|
104
|
+
x: rig.x,
|
|
105
|
+
z: rig.z
|
|
106
|
+
},
|
|
107
|
+
target: {
|
|
108
|
+
x: this.targetX,
|
|
109
|
+
z: this.targetZ
|
|
110
|
+
},
|
|
111
|
+
focus: {
|
|
112
|
+
x: this.focusX,
|
|
113
|
+
z: this.focusZ
|
|
114
|
+
},
|
|
115
|
+
bounds: this._bounds()
|
|
116
|
+
});
|
|
117
|
+
this.entity.rotate(0, pose.yawDelta, 0);
|
|
118
|
+
this.entity.setPosition(pose.x, rigY, pose.z);
|
|
119
|
+
this._pending = false;
|
|
120
|
+
}
|
|
121
|
+
_teardown() {
|
|
122
|
+
this.app.xr?.off('start', this._onXrStart);
|
|
123
|
+
this.app.xr?.off('update', this._onXrUpdate);
|
|
124
|
+
this.app.xr?.off('end', this._onXrEnd);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { XrStartPose };
|
|
@@ -42,8 +42,6 @@ export declare const boundaryWallMesh: ({ center, radius, groundPlane, baseY, to
|
|
|
42
42
|
*
|
|
43
43
|
* center / radius / groundPlane / baseY are set imperatively by the BoundaryWall component (see
|
|
44
44
|
* BoundaryWall.tsx for why they are not reactive Script props), which calls `rebuild()` afterwards.
|
|
45
|
-
* All of them are world-space: this entity is deliberately mounted outside `content-root`, which
|
|
46
|
-
* carries the scene's scaleFactor.
|
|
47
45
|
*/
|
|
48
46
|
export declare class BoundaryWallScript extends Script {
|
|
49
47
|
static scriptName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boundary-wall.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/boundary-wall.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,EAOL,MAAM,EAEN,IAAI,EACL,MAAM,YAAY,CAAC;AAKpB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,gHAAgH;IAChH,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,IAAI,EAAE,MAAM,CAAC;IACb,yGAAyG;IACzG,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,sEAQ9B,0BAA0B,KAAG,oBAqD/B,CAAC;AA2EF
|
|
1
|
+
{"version":3,"file":"boundary-wall.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/boundary-wall.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,EAOL,MAAM,EAEN,IAAI,EACL,MAAM,YAAY,CAAC;AAKpB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,gHAAgH;IAChH,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,IAAI,EAAE,MAAM,CAAC;IACb,yGAAyG;IACzG,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,sEAQ9B,0BAA0B,KAAG,oBAqD/B,CAAC;AA2EF;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,MAAM;IAC5C,MAAM,CAAC,UAAU,SAAkB;IAEnC,MAAM,OAAc;IACpB,MAAM,SAAK;IACX,WAAW,EAAE,IAAI,EAAE,CAAM;IACzB,uEAAuE;IACvE,KAAK,SAAK;IACV;;;OAGG;IACH,IAAI,SAAO;IACX,QAAQ,SAAM;IACd,WAAW,SAAO;IAClB,2CAA2C;IAC3C,SAAS,SAAS;IAClB,0EAA0E;IAC1E,YAAY,SAAO;IACnB,0EAA0E;IAC1E,gBAAgB,SAAO;IACvB,SAAS,QAA2B;IACpC,SAAS,QAA2B;IAEpC,OAAO,CAAC,SAAS,CAAC,CAAiB;IACnC,OAAO,CAAC,KAAK,CAAC,CAAO;IACrB,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,UAAU,CAAuB;IAEzC,UAAU;IAwBV;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAW5B,OAAO;IA6CP,MAAM;IAgCN,OAAO,CAAC,UAAU;CASnB"}
|
|
@@ -179,8 +179,6 @@ const wallFragmentGLSL = /* glsl */`
|
|
|
179
179
|
*
|
|
180
180
|
* center / radius / groundPlane / baseY are set imperatively by the BoundaryWall component (see
|
|
181
181
|
* BoundaryWall.tsx for why they are not reactive Script props), which calls `rebuild()` afterwards.
|
|
182
|
-
* All of them are world-space: this entity is deliberately mounted outside `content-root`, which
|
|
183
|
-
* carries the scene's scaleFactor.
|
|
184
182
|
*/
|
|
185
183
|
class BoundaryWallScript extends Script {
|
|
186
184
|
static scriptName = 'boundaryWall';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds the XR render settings in place for the duration of a session.
|
|
3
|
+
*
|
|
4
|
+
* Call this once per application. The gsplat settings it writes are scene-global, so a second
|
|
5
|
+
* caller would capture the first caller's tuned values as the state to restore, and the scene would
|
|
6
|
+
* come out of the session still tuned.
|
|
7
|
+
*/
|
|
8
|
+
export declare const useXrRenderTuning: () => void;
|
|
9
|
+
export default useXrRenderTuning;
|
|
10
|
+
//# sourceMappingURL=useXrRenderTuning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useXrRenderTuning.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/useXrRenderTuning.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,YAsC7B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useApp } from '@playcanvas/react/hooks';
|
|
3
|
+
import { applyGsplatTuning, XR_FIXED_FOVEATION, restoreGsplatTuning } from './xr-render-tuning.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Holds the XR render settings in place for the duration of a session.
|
|
7
|
+
*
|
|
8
|
+
* Call this once per application. The gsplat settings it writes are scene-global, so a second
|
|
9
|
+
* caller would capture the first caller's tuned values as the state to restore, and the scene would
|
|
10
|
+
* come out of the session still tuned.
|
|
11
|
+
*/
|
|
12
|
+
const useXrRenderTuning = () => {
|
|
13
|
+
const app = useApp();
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const xr = app.xr;
|
|
16
|
+
if (!xr) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let previous = null;
|
|
20
|
+
const restore = () => {
|
|
21
|
+
if (previous) {
|
|
22
|
+
restoreGsplatTuning(app.scene.gsplat, previous);
|
|
23
|
+
previous = null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const handleStart = () => {
|
|
27
|
+
// Guards against capturing already-tuned values as the restore target if `start` ever
|
|
28
|
+
// fires again before a matching `end`.
|
|
29
|
+
if (!previous) {
|
|
30
|
+
previous = applyGsplatTuning(app.scene.gsplat);
|
|
31
|
+
}
|
|
32
|
+
// Only settable on an active session, so it cannot go in the start options alongside
|
|
33
|
+
// framebufferScaleFactor. A no-op when the presentation layer doesn't support it.
|
|
34
|
+
xr.fixedFoveation = XR_FIXED_FOVEATION;
|
|
35
|
+
};
|
|
36
|
+
xr.on('start', handleStart);
|
|
37
|
+
xr.on('end', restore);
|
|
38
|
+
return () => {
|
|
39
|
+
xr.off('start', handleStart);
|
|
40
|
+
xr.off('end', restore);
|
|
41
|
+
restore();
|
|
42
|
+
};
|
|
43
|
+
}, [app]);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export { useXrRenderTuning as default, useXrRenderTuning };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vr-annotation-panel.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/vr-annotation-panel.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,MAAM,EAGN,IAAI,EAEL,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"vr-annotation-panel.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/vr-annotation-panel.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,MAAM,EAGN,IAAI,EAEL,MAAM,YAAY,CAAC;AAmCpB,qBAAa,iBAAkB,SAAQ,MAAM;IAC3C,MAAM,CAAC,UAAU,SAAuB;IAGxC,KAAK,SAAM;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,eAAe,SAAM;IAErB,OAAO,CAAC,SAAS,CAAC,CAAmB;IACrC,OAAO,CAAC,QAAQ,CAAC,CAAU;IAC3B,OAAO,CAAC,KAAK,CAAC,CAAO;IACrB,OAAO,CAAC,OAAO,CAAC,CAAoB;IACpC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,aAAa,CAAc;IAEnC,OAAO,CAAC,SAAS,CAsBf;IAEF,UAAU;IAwCV,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,UAAU;IAkBlB,OAAO,CAAC,SAAS;IAWjB,OAAO,CAAC,YAAY;IAqFpB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,WAAW;IAmBnB,8FAA8F;IAC9F,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,OAAO;IAIpD,MAAM;IAmCN,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,SAAS;CAYlB"}
|
|
@@ -4,14 +4,15 @@ import { rayQuadHit } from './xr-ray-quad.js';
|
|
|
4
4
|
|
|
5
5
|
const CANVAS_WIDTH = 1024;
|
|
6
6
|
const CANVAS_HEIGHT = 768;
|
|
7
|
-
|
|
8
|
-
/** Panel width in world meters; height derived from the canvas aspect ratio. */
|
|
9
|
-
const PANEL_WIDTH = 0.5;
|
|
7
|
+
const PANEL_WIDTH = 7.5;
|
|
10
8
|
const PANEL_HEIGHT = PANEL_WIDTH * CANVAS_HEIGHT / CANVAS_WIDTH;
|
|
11
9
|
|
|
10
|
+
/** Gap (m) between the top of the hotspot and the bottom edge of the panel. */
|
|
11
|
+
const PANEL_GAP = 0.75;
|
|
12
|
+
|
|
12
13
|
/** World-space offset from the anchored hotspot: raise the panel above it so the
|
|
13
14
|
* (tall) panel clears the hotspot rather than covering it. */
|
|
14
|
-
const PANEL_OFFSET = new Vec3(0, PANEL_HEIGHT / 2 +
|
|
15
|
+
const PANEL_OFFSET = new Vec3(0, PANEL_HEIGHT / 2 + PANEL_GAP, 0);
|
|
15
16
|
const PAD_X = 48;
|
|
16
17
|
|
|
17
18
|
/** Local +z (the quad's front face) — used to derive the panel's yaw toward the viewer. */
|
|
@@ -33,7 +34,6 @@ class VrAnnotationPanel extends Script {
|
|
|
33
34
|
// Props assigned by the React wrapper.
|
|
34
35
|
title = '';
|
|
35
36
|
annotationIndex = -1;
|
|
36
|
-
scaleFactor = 1;
|
|
37
37
|
_drawnSignature = null;
|
|
38
38
|
_currentImage = 0;
|
|
39
39
|
_loadToken = 0;
|
|
@@ -278,10 +278,7 @@ class VrAnnotationPanel extends Script {
|
|
|
278
278
|
return;
|
|
279
279
|
}
|
|
280
280
|
this._anchor.copy(anchor.getPosition());
|
|
281
|
-
|
|
282
|
-
// proportions relative to the (content-root-scaled) annotations across scenes.
|
|
283
|
-
this.entity.setLocalScale(this.scaleFactor, this.scaleFactor, this.scaleFactor);
|
|
284
|
-
this.entity.setPosition(this._anchor.x + PANEL_OFFSET.x * this.scaleFactor, this._anchor.y + PANEL_OFFSET.y * this.scaleFactor, this._anchor.z + PANEL_OFFSET.z * this.scaleFactor);
|
|
281
|
+
this.entity.setPosition(this._anchor.x + PANEL_OFFSET.x, this._anchor.y + PANEL_OFFSET.y, this._anchor.z + PANEL_OFFSET.z);
|
|
285
282
|
|
|
286
283
|
// Billboard on yaw only: face the viewer horizontally but stay upright and level, so
|
|
287
284
|
// tilting/rolling the headset does not tilt the panel. Derived from the head's forward
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walkthrough-camera.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/walkthrough-camera.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,IAAI,EAAQ,MAAM,YAAY,CAAC;AA8BtD,qBAAa,iBAAkB,SAAQ,MAAM;IAC3C,MAAM,CAAC,UAAU,SAAuB;IAExC,
|
|
1
|
+
{"version":3,"file":"walkthrough-camera.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/walkthrough-camera.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,IAAI,EAAQ,MAAM,YAAY,CAAC;AA8BtD,qBAAa,iBAAkB,SAAQ,MAAM;IAC3C,MAAM,CAAC,UAAU,SAAuB;IAExC,SAAS,SAAO;IAEhB,iBAAiB;IACjB,aAAa,SAAO;IAEpB,iBAAiB;IACjB,aAAa,SAAQ;IAErB,iBAAiB;IACjB,eAAe,SAAO;IAEtB,iBAAiB;IACjB,aAAa,SAAQ;IAErB,iBAAiB;IACjB,QAAQ,SAAO;IAEf,iBAAiB;IACjB,QAAQ,SAAM;IAEd,iBAAiB;IACjB,WAAW,SAAS;IAEpB,iBAAiB;IACjB,YAAY,OAAqB;IAEjC,iBAAiB;IACjB,YAAY,SAAM;IAElB,iBAAiB;IACjB,OAAO,UAAS;IAEhB,iBAAiB;IACjB,SAAS,UAAQ;IAEjB,iBAAiB;IACjB,mBAAmB,SAAK;IAExB;;;;;;;;;;OAUG;IACH,WAAW,EAAE,IAAI,EAAE,CAAM;IAGzB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,IAAI,CAAK;IAGjB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,KAAK,CAAwC;IACrD,OAAO,CAAC,gBAAgB,CAAsB;IAG9C,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,eAAe,CAAS;IAEhC,OAAO,CAAC,gBAAgB;IAaxB,UAAU;IAkFV;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI;IAsBjC,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM;IAsB1B;;;OAGG;IACH,IAAI,UAAU,IAAI,IAAI,CAMrB;IAED,MAAM,CAAC,EAAE,EAAE,MAAM;IA4FjB,OAAO;CAIR"}
|
|
@@ -28,15 +28,13 @@ const ANGLE_EPSILON = 1e-3;
|
|
|
28
28
|
const POSITION_EPSILON = 1e-5;
|
|
29
29
|
class WalkthroughCamera extends Script {
|
|
30
30
|
static scriptName = 'walkthroughCamera';
|
|
31
|
+
moveSpeed = 4.5;
|
|
31
32
|
|
|
32
33
|
/** @attribute */
|
|
33
|
-
|
|
34
|
+
moveFastSpeed = 7.5;
|
|
34
35
|
|
|
35
36
|
/** @attribute */
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/** @attribute */
|
|
39
|
-
moveSlowSpeed = 0.15;
|
|
37
|
+
moveSlowSpeed = 2.25;
|
|
40
38
|
|
|
41
39
|
/** @attribute */
|
|
42
40
|
lookSensitivity = 0.1;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render settings applied for the duration of an XR session. Splats rasterize as alpha-blended
|
|
3
|
+
* quads with no early-z rejection, which makes a headset's stereo pixel throughput the binding
|
|
4
|
+
* constraint on framerate long before splat count is.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Peripheral back-buffer resolution reduction, 0 (off) to 1 (strongest). Ineffective while MSAA is
|
|
8
|
+
* on, which is one reason the application requests `antialias: false`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const XR_FIXED_FOVEATION = 1;
|
|
11
|
+
/**
|
|
12
|
+
* The subset of `GSplatParams` this module writes. Structural rather than the PlayCanvas type so
|
|
13
|
+
* the logic can be exercised against a plain object.
|
|
14
|
+
*/
|
|
15
|
+
export interface GsplatTuningParams {
|
|
16
|
+
radialSorting: boolean;
|
|
17
|
+
alphaClipForward: number;
|
|
18
|
+
minPixelSize: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `radialSorting` orders splats by distance from the camera rather than by depth along its forward
|
|
22
|
+
* axis, which is the more accurate ordering when the camera rotates rather than translates. Sorting
|
|
23
|
+
* runs on a worker and so lags the view by at least a frame; head rotation dominates in a headset,
|
|
24
|
+
* where that lag is what reads as swimming.
|
|
25
|
+
*
|
|
26
|
+
* The other two trade fidelity for fill rate: `alphaClipForward` drops near-transparent splats from
|
|
27
|
+
* the forward pass, and `minPixelSize` drops splats that project to less than a few pixels.
|
|
28
|
+
*/
|
|
29
|
+
export declare const XR_GSPLAT_TUNING: GsplatTuningParams;
|
|
30
|
+
/**
|
|
31
|
+
* Applies the XR settings, returning the values that were replaced so they can be restored when the
|
|
32
|
+
* session ends. These live on the scene, so leaving them tuned would degrade desktop rendering.
|
|
33
|
+
*/
|
|
34
|
+
export declare const applyGsplatTuning: (params: GsplatTuningParams) => GsplatTuningParams;
|
|
35
|
+
export declare const restoreGsplatTuning: (params: GsplatTuningParams, previous: GsplatTuningParams) => void;
|
|
36
|
+
//# sourceMappingURL=xr-render-tuning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xr-render-tuning.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-render-tuning.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,kBAI9B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,kBAAkB,KAAG,kBAY9D,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,QAAQ,kBAAkB,EAAE,UAAU,kBAAkB,KAAG,IAI9F,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render settings applied for the duration of an XR session. Splats rasterize as alpha-blended
|
|
3
|
+
* quads with no early-z rejection, which makes a headset's stereo pixel throughput the binding
|
|
4
|
+
* constraint on framerate long before splat count is.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Peripheral back-buffer resolution reduction, 0 (off) to 1 (strongest). Ineffective while MSAA is
|
|
9
|
+
* on, which is one reason the application requests `antialias: false`.
|
|
10
|
+
*/
|
|
11
|
+
const XR_FIXED_FOVEATION = 1;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The subset of `GSplatParams` this module writes. Structural rather than the PlayCanvas type so
|
|
15
|
+
* the logic can be exercised against a plain object.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `radialSorting` orders splats by distance from the camera rather than by depth along its forward
|
|
20
|
+
* axis, which is the more accurate ordering when the camera rotates rather than translates. Sorting
|
|
21
|
+
* runs on a worker and so lags the view by at least a frame; head rotation dominates in a headset,
|
|
22
|
+
* where that lag is what reads as swimming.
|
|
23
|
+
*
|
|
24
|
+
* The other two trade fidelity for fill rate: `alphaClipForward` drops near-transparent splats from
|
|
25
|
+
* the forward pass, and `minPixelSize` drops splats that project to less than a few pixels.
|
|
26
|
+
*/
|
|
27
|
+
const XR_GSPLAT_TUNING = {
|
|
28
|
+
radialSorting: true,
|
|
29
|
+
alphaClipForward: 0.1,
|
|
30
|
+
minPixelSize: 1
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Applies the XR settings, returning the values that were replaced so they can be restored when the
|
|
35
|
+
* session ends. These live on the scene, so leaving them tuned would degrade desktop rendering.
|
|
36
|
+
*/
|
|
37
|
+
const applyGsplatTuning = params => {
|
|
38
|
+
const previous = {
|
|
39
|
+
radialSorting: params.radialSorting,
|
|
40
|
+
alphaClipForward: params.alphaClipForward,
|
|
41
|
+
minPixelSize: params.minPixelSize
|
|
42
|
+
};
|
|
43
|
+
params.radialSorting = XR_GSPLAT_TUNING.radialSorting;
|
|
44
|
+
params.alphaClipForward = XR_GSPLAT_TUNING.alphaClipForward;
|
|
45
|
+
params.minPixelSize = XR_GSPLAT_TUNING.minPixelSize;
|
|
46
|
+
return previous;
|
|
47
|
+
};
|
|
48
|
+
const restoreGsplatTuning = (params, previous) => {
|
|
49
|
+
params.radialSorting = previous.radialSorting;
|
|
50
|
+
params.alphaClipForward = previous.alphaClipForward;
|
|
51
|
+
params.minPixelSize = previous.minPixelSize;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { XR_FIXED_FOVEATION, XR_GSPLAT_TUNING, applyGsplatTuning, restoreGsplatTuning };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type Point2 = {
|
|
2
|
+
x: number;
|
|
3
|
+
z: number;
|
|
4
|
+
};
|
|
5
|
+
export type XrStartBounds = Point2 & {
|
|
6
|
+
radius: number;
|
|
7
|
+
};
|
|
8
|
+
export type XrStartPoseInput = {
|
|
9
|
+
/** Where the head is now. */
|
|
10
|
+
head: Point2;
|
|
11
|
+
/** Yaw of the head's forward direction now. */
|
|
12
|
+
headYaw: number;
|
|
13
|
+
/** Where the rig is now. */
|
|
14
|
+
rig: Point2;
|
|
15
|
+
/** Where the head should end up. */
|
|
16
|
+
target: Point2;
|
|
17
|
+
/** What the head should face from there. */
|
|
18
|
+
focus: Point2;
|
|
19
|
+
/**
|
|
20
|
+
* Circle to keep the target inside. Omit (or pass a non-positive radius) to place the head on the
|
|
21
|
+
* target as given.
|
|
22
|
+
*/
|
|
23
|
+
bounds?: XrStartBounds;
|
|
24
|
+
};
|
|
25
|
+
export type XrStartPose = {
|
|
26
|
+
/** Yaw to turn the rig by, about its own origin. */
|
|
27
|
+
yawDelta: number;
|
|
28
|
+
/** Where the rig ends up once turned. */
|
|
29
|
+
x: number;
|
|
30
|
+
z: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Yaw of a heading on the XZ plane, matching PlayCanvas' right-handed CCW Y rotation:
|
|
34
|
+
* forward = (-sin(yaw), 0, -cos(yaw)).
|
|
35
|
+
*/
|
|
36
|
+
export declare const yawFromDirection: (dx: number, dz: number) => number | null;
|
|
37
|
+
/**
|
|
38
|
+
* Yaw an entity is facing, from its world basis vectors. Falls back to the right vector when the
|
|
39
|
+
* forward vector is vertical — a head tipped fully up or down still has a heading, but its forward
|
|
40
|
+
* vector has no horizontal component to read it from.
|
|
41
|
+
*/
|
|
42
|
+
export declare const yawFromBasis: (forward: {
|
|
43
|
+
x: number;
|
|
44
|
+
z: number;
|
|
45
|
+
}, right: {
|
|
46
|
+
x: number;
|
|
47
|
+
z: number;
|
|
48
|
+
}) => number;
|
|
49
|
+
/**
|
|
50
|
+
* Solves for the rig pose that lands the head on `target` facing `focus`.
|
|
51
|
+
*
|
|
52
|
+
* Turning the rig swings the head around the rig's origin, so the turn is applied to the head's
|
|
53
|
+
* offset within the rig first and the rig is then placed so that swung offset lands on the target.
|
|
54
|
+
* A focus point on top of the target gives no heading, so the rig is only moved, not turned.
|
|
55
|
+
*/
|
|
56
|
+
export declare const xrStartRigPose: ({ head, headYaw, rig, target, focus, bounds }: XrStartPoseInput) => XrStartPose;
|
|
57
|
+
//# sourceMappingURL=xr-start-pose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xr-start-pose.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-start-pose.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX,CAAC;AAeF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,IAAI,MAAM,EAAE,IAAI,MAAM,KAAG,MAAM,GAAG,IAC8C,CAAC;AAElH;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,SAAS;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,MACZ,CAAC;AAWvF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,+CAA+C,gBAAgB,KAAG,WAOhG,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { clampToCircle } from './xr-scene-bounds.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where to put the XR rig so the head starts at a chosen spot facing a chosen point.
|
|
5
|
+
*
|
|
6
|
+
* PlayCanvas writes the head pose into the camera entity's *local* transform every frame, so the
|
|
7
|
+
* only thing a scene can move is the rig the camera hangs off. The head sits at an arbitrary offset
|
|
8
|
+
* within the rig — the reference space is pinned to wherever the user physically stood when the
|
|
9
|
+
* session began — so the rig pose has to be solved backwards from the head pose rather than set to
|
|
10
|
+
* the target outright.
|
|
11
|
+
*
|
|
12
|
+
* All angles are degrees and all coordinates world-space XZ; Y is left to the caller, since the
|
|
13
|
+
* user stands on the rig floor rather than on the scene's ground plane.
|
|
14
|
+
*/
|
|
15
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
16
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
17
|
+
|
|
18
|
+
/** Below this length a direction vector carries no usable heading. */
|
|
19
|
+
const DIRECTION_EPSILON = 1e-6;
|
|
20
|
+
|
|
21
|
+
/** Wraps to (-180, 180] so the rig always turns the short way round. */
|
|
22
|
+
const normalizeAngle = degrees => {
|
|
23
|
+
const wrapped = ((degrees + 180) % 360 + 360) % 360 - 180;
|
|
24
|
+
return wrapped === -180 ? 180 : wrapped;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Yaw of a heading on the XZ plane, matching PlayCanvas' right-handed CCW Y rotation:
|
|
29
|
+
* forward = (-sin(yaw), 0, -cos(yaw)).
|
|
30
|
+
*/
|
|
31
|
+
const yawFromDirection = (dx, dz) => Math.abs(dx) < DIRECTION_EPSILON && Math.abs(dz) < DIRECTION_EPSILON ? null : Math.atan2(-dx, -dz) * RAD_TO_DEG;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Yaw an entity is facing, from its world basis vectors. Falls back to the right vector when the
|
|
35
|
+
* forward vector is vertical — a head tipped fully up or down still has a heading, but its forward
|
|
36
|
+
* vector has no horizontal component to read it from.
|
|
37
|
+
*/
|
|
38
|
+
const yawFromBasis = (forward, right) => yawFromDirection(forward.x, forward.z) ?? Math.atan2(-right.z, right.x) * RAD_TO_DEG;
|
|
39
|
+
|
|
40
|
+
/** Rotates a point about the origin by `degrees` of PlayCanvas yaw. */
|
|
41
|
+
const rotateXz = (point, degrees) => {
|
|
42
|
+
const radians = degrees * DEG_TO_RAD;
|
|
43
|
+
const cos = Math.cos(radians);
|
|
44
|
+
const sin = Math.sin(radians);
|
|
45
|
+
return {
|
|
46
|
+
x: point.x * cos + point.z * sin,
|
|
47
|
+
z: -point.x * sin + point.z * cos
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Solves for the rig pose that lands the head on `target` facing `focus`.
|
|
53
|
+
*
|
|
54
|
+
* Turning the rig swings the head around the rig's origin, so the turn is applied to the head's
|
|
55
|
+
* offset within the rig first and the rig is then placed so that swung offset lands on the target.
|
|
56
|
+
* A focus point on top of the target gives no heading, so the rig is only moved, not turned.
|
|
57
|
+
*/
|
|
58
|
+
const xrStartRigPose = ({
|
|
59
|
+
head,
|
|
60
|
+
headYaw,
|
|
61
|
+
rig,
|
|
62
|
+
target,
|
|
63
|
+
focus,
|
|
64
|
+
bounds
|
|
65
|
+
}) => {
|
|
66
|
+
const landing = bounds ? clampToCircle(target.x, target.z, bounds.x, bounds.z, bounds.radius) : target;
|
|
67
|
+
const facingYaw = yawFromDirection(focus.x - landing.x, focus.z - landing.z);
|
|
68
|
+
const yawDelta = facingYaw === null ? 0 : normalizeAngle(facingYaw - headYaw);
|
|
69
|
+
const offset = rotateXz({
|
|
70
|
+
x: head.x - rig.x,
|
|
71
|
+
z: head.z - rig.z
|
|
72
|
+
}, yawDelta);
|
|
73
|
+
return {
|
|
74
|
+
yawDelta,
|
|
75
|
+
x: landing.x - offset.x,
|
|
76
|
+
z: landing.z - offset.z
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export { xrStartRigPose, yawFromBasis, yawFromDirection };
|
package/hooks/useXr.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useXr.d.ts","sourceRoot":"","sources":["../../src/hooks/useXr.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjC,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;
|
|
1
|
+
{"version":3,"file":"useXr.d.ts","sourceRoot":"","sources":["../../src/hooks/useXr.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjC,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAgBD,eAAO,MAAM,KAAK,GAAI,cAAa,YAAiB;0BA4CT,MAAM;oBAGtC,MAAM;;;;;CA8BhB,CAAC;AAEF,eAAe,KAAK,CAAC"}
|
package/hooks/useXr.js
CHANGED
|
@@ -6,6 +6,15 @@ const XR_TYPES = {
|
|
|
6
6
|
VR: XRTYPE_VR,
|
|
7
7
|
AR: XRTYPE_AR
|
|
8
8
|
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Factor PlayCanvas applies on top of the device pixel ratio to compute the headset's
|
|
12
|
+
* framebuffer resolution: `app.graphicsDevice.maxPixelRatio / window.devicePixelRatio *
|
|
13
|
+
* XR_FRAMEBUFFER_SCALE_FACTOR`. Splat rendering in stereo is fill-rate bound, so this (and
|
|
14
|
+
* `maxPixelRatio`, which also multiplies XR resolution) is the largest lever on XR framerate.
|
|
15
|
+
* Read once when the session starts and fixed for its lifetime.
|
|
16
|
+
*/
|
|
17
|
+
const XR_FRAMEBUFFER_SCALE_FACTOR = 1;
|
|
9
18
|
const useXr = ({
|
|
10
19
|
onError
|
|
11
20
|
} = {}) => {
|
|
@@ -56,6 +65,7 @@ const useXr = ({
|
|
|
56
65
|
const startXr = useCallback(type => {
|
|
57
66
|
const camera = app.root.findComponent('camera');
|
|
58
67
|
app.xr?.start(camera, XR_TYPES[type], XRSPACE_LOCALFLOOR, {
|
|
68
|
+
framebufferScaleFactor: XR_FRAMEBUFFER_SCALE_FACTOR,
|
|
59
69
|
callback: err => {
|
|
60
70
|
if (err) {
|
|
61
71
|
onError?.(err);
|