@@ -1334,9 +1334,6 @@
|
|
1334
1334
|
this.domElement.dispatchEvent(new CustomEvent("ready"));
|
1335
1335
|
ContextRegistry.dispatchCallback(ContextEvent.ContextFirstFrameRendered, this);
|
1336
1336
|
}
|
1337
|
-
|
1338
|
-
|
1339
|
-
this.renderer.info.reset();
|
1340
1337
|
}
|
1341
1338
|
|
1342
1339
|
renderNow(camera?: Camera) {
|
@@ -1345,6 +1342,7 @@
|
|
1345
1342
|
if (!camera) return false;
|
1346
1343
|
}
|
1347
1344
|
this._isRendering = true;
|
1345
|
+
this.renderer.info.reset();
|
1348
1346
|
this.renderRequiredTextures();
|
1349
1347
|
|
1350
1348
|
if (this.renderer.toneMapping !== NoToneMapping)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
import { Gizmos } from './engine_gizmos.js';
|
5
5
|
import { Context } from './engine_setup.js';
|
6
|
-
import { getWorldPosition } from "./engine_three_utils.js"
|
6
|
+
import { getTempVector, getWorldPosition } from "./engine_three_utils.js"
|
7
7
|
import type { Vec2, Vec3, } from './engine_types.js';
|
8
8
|
import type { IPhysicsEngine } from './engine_types.js';
|
9
9
|
import { getParam } from "./engine_utils.js"
|
@@ -31,7 +31,16 @@
|
|
31
31
|
results?: Array<Intersection>;
|
32
32
|
/** Objects to raycast against. If no target array is provided the whole scene will be raycasted */
|
33
33
|
targets?: Array<Object3D>;
|
34
|
+
/**
|
35
|
+
* If true, the raycaster will traverse the scene recursively.
|
36
|
+
* @default true
|
37
|
+
*/
|
34
38
|
recursive?: boolean;
|
39
|
+
/**
|
40
|
+
* If true, the raycaster will use a more precise method to test for intersections. This is slower but more accurate.
|
41
|
+
* @default true
|
42
|
+
*/
|
43
|
+
precise?: boolean;
|
35
44
|
/** Set the raycaster near distance:
|
36
45
|
* The near factor of the raycaster. This value indicates which objects can be discarded based on the distance. This value shouldn't be negative and should be smaller than the far property.
|
37
46
|
* @link https://threejs.org/docs/#api/en/core/Raycaster.near
|
@@ -312,7 +321,13 @@
|
|
312
321
|
const raycastMesh = getRaycastMesh(obj);
|
313
322
|
if (raycastMesh) mesh.geometry = raycastMesh;
|
314
323
|
const lastResultsCount = results.length;
|
315
|
-
|
324
|
+
if (options.precise === false && customRaycast(mesh, raycaster, results)) {
|
325
|
+
// did handle raycast
|
326
|
+
}
|
327
|
+
else {
|
328
|
+
raycaster.intersectObject(obj, false, results);
|
329
|
+
}
|
330
|
+
|
316
331
|
if (debugPhysics && results.length != lastResultsCount)
|
317
332
|
Gizmos.DrawWireMesh({ mesh: obj as Mesh, depthTest: false, duration: .2, color: raycastMesh ? 0x88dd55 : 0x770000 })
|
318
333
|
mesh.geometry = geometry;
|
@@ -326,3 +341,42 @@
|
|
326
341
|
return results;
|
327
342
|
}
|
328
343
|
}
|
344
|
+
|
345
|
+
const tempSphere = new Sphere();
|
346
|
+
/**
|
347
|
+
* @returns false if custom raycasting can not run, otherwise true
|
348
|
+
*/
|
349
|
+
function customRaycast(mesh: Mesh, raycaster: Raycaster, results: Intersection[]): boolean {
|
350
|
+
const originalComputeIntersectionsFn = mesh["_computeIntersections"];
|
351
|
+
if (!originalComputeIntersectionsFn) {
|
352
|
+
return false;
|
353
|
+
}
|
354
|
+
|
355
|
+
// compute custom intersection, check if a custom method already exists
|
356
|
+
let computeCustomIntersectionFn = mesh["_computeIntersections:Needle"];
|
357
|
+
if (!computeCustomIntersectionFn) {
|
358
|
+
// create and bind a custom method once to the mesh object
|
359
|
+
// TODO: maybe we want to add this to the prototype instead
|
360
|
+
computeCustomIntersectionFn = mesh["_computeIntersections:Needle"] = function (raycaster: Raycaster, intersects: Intersection[], _rayLocalSpace: Ray) {
|
361
|
+
const self = this as Mesh;
|
362
|
+
const boundingSphere = self.geometry.boundingSphere;
|
363
|
+
if (boundingSphere) {
|
364
|
+
tempSphere.copy(boundingSphere);
|
365
|
+
tempSphere.applyMatrix4(self.matrixWorld);
|
366
|
+
const point = raycaster.ray.intersectSphere(tempSphere, getTempVector());
|
367
|
+
if (point) {
|
368
|
+
// const dir = getTempVector(tempSphere.center).sub(raycaster.ray.origin);
|
369
|
+
// const pointOnSphere = getTempVector(dir).normalize().multiplyScalar(tempSphere.radius);
|
370
|
+
// // const pointOnBoundingSphere = raycaster.ray.closestPointToPoint(boundingSphere.center, getTempVector());
|
371
|
+
// const distance = dir.length();//pointOnBoundingSphere.distanceTo(raycaster.ray.origin);
|
372
|
+
const distance = point.distanceTo(raycaster.ray.origin);
|
373
|
+
intersects.push({ object: self, distance: distance, point: point });
|
374
|
+
}
|
375
|
+
}
|
376
|
+
}
|
377
|
+
}
|
378
|
+
mesh["_computeIntersections"] = computeCustomIntersectionFn;
|
379
|
+
raycaster.intersectObject(mesh, false, results);
|
380
|
+
mesh["_computeIntersections"] = originalComputeIntersectionsFn;
|
381
|
+
return true;
|
382
|
+
}
|
@@ -5,6 +5,8 @@
|
|
5
5
|
import { getBoundingBox, getWorldPosition, Graphics, setWorldPosition } from "../engine/engine_three_utils.js";
|
6
6
|
import { 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";
|
8
10
|
|
9
11
|
const debug = getParam("debuggroundprojection");
|
10
12
|
|
@@ -13,17 +15,23 @@
|
|
13
15
|
*/
|
14
16
|
export class GroundProjectedEnv extends Behaviour {
|
15
17
|
|
18
|
+
/**
|
19
|
+
* If true the projection will be created on awake and onEnable
|
20
|
+
* @default false
|
21
|
+
*/
|
16
22
|
@serializable()
|
17
23
|
applyOnAwake: boolean = false;
|
18
24
|
|
19
25
|
/**
|
20
26
|
* When enabled the position of the projected environment will be adjusted to be centered in the scene (and ground level).
|
27
|
+
* @default true
|
21
28
|
*/
|
22
29
|
@serializable()
|
23
30
|
autoFit: boolean = true;
|
24
31
|
|
25
32
|
/**
|
26
33
|
* Radius of the projection sphere. Set it large enough so the camera stays inside (make sure the far plane is also large enough)
|
34
|
+
* @default 50
|
27
35
|
*/
|
28
36
|
@serializable()
|
29
37
|
set radius(val: number) {
|
@@ -31,10 +39,11 @@
|
|
31
39
|
this.updateProjection();
|
32
40
|
}
|
33
41
|
get radius(): number { return this._radius; }
|
34
|
-
private _radius: number =
|
42
|
+
private _radius: number = 50;
|
35
43
|
|
36
44
|
/**
|
37
45
|
* How far the camera that took the photo was above the ground. A larger value will magnify the downward part of the image.
|
46
|
+
* @sefault 3
|
38
47
|
*/
|
39
48
|
@serializable()
|
40
49
|
set height(val: number) {
|
@@ -44,6 +53,20 @@
|
|
44
53
|
get height(): number { return this._height; }
|
45
54
|
private _height: number = 3;
|
46
55
|
|
56
|
+
/**
|
57
|
+
* Blending factor for the AR projection being blended with the scene background.
|
58
|
+
* 0 = not visible in AR - 1 = blended with real world background.
|
59
|
+
* Values between 0 and 1 control the smoothness of the blend while lower values result in smoother blending.
|
60
|
+
* @default 0
|
61
|
+
*/
|
62
|
+
@serializable()
|
63
|
+
set arBlending(val: number) {
|
64
|
+
this._arblending = val;
|
65
|
+
this._needsTextureUpdate = true;
|
66
|
+
}
|
67
|
+
get arBlending(): number { return this._arblending; }
|
68
|
+
private _arblending = 0;
|
69
|
+
|
47
70
|
private _lastEnvironment?: Texture;
|
48
71
|
private _lastRadius?: number;
|
49
72
|
private _lastHeight?: number;
|
@@ -77,6 +100,7 @@
|
|
77
100
|
}
|
78
101
|
/** @internal */
|
79
102
|
onEnterXR(): void {
|
103
|
+
this._needsTextureUpdate = true;
|
80
104
|
this.updateProjection();
|
81
105
|
}
|
82
106
|
/** @internal */
|
@@ -89,7 +113,11 @@
|
|
89
113
|
this._projection.rotation.copy(this.scene.backgroundRotation);
|
90
114
|
}
|
91
115
|
|
92
|
-
|
116
|
+
const blurrinessChanged = this.context.scene.backgroundBlurriness !== undefined && this._lastBlurriness != this.context.scene.backgroundBlurriness && this.context.scene.backgroundBlurriness > 0.001;
|
117
|
+
if (blurrinessChanged) {
|
118
|
+
this.updateProjection();
|
119
|
+
}
|
120
|
+
else if (this._needsTextureUpdate) {
|
93
121
|
this.updateBlurriness();
|
94
122
|
}
|
95
123
|
}
|
@@ -99,24 +127,30 @@
|
|
99
127
|
this._watcher?.apply();
|
100
128
|
}
|
101
129
|
|
130
|
+
|
131
|
+
private _needsTextureUpdate = false;
|
132
|
+
|
102
133
|
/**
|
103
|
-
* Updates the ground projection. This is called automatically when the environment
|
134
|
+
* Updates the ground projection. This is called automatically when the environment or settings change.
|
104
135
|
*/
|
105
136
|
updateProjection() {
|
106
|
-
if (!this.context.scene.environment
|
137
|
+
if (!this.context.scene.environment) {
|
107
138
|
this._projection?.removeFromParent();
|
108
139
|
return;
|
109
140
|
}
|
141
|
+
if (this.context.xr?.isPassThrough || this.context.xr?.isAR) {
|
142
|
+
if (this.arBlending === 0) {
|
143
|
+
this._projection?.removeFromParent();
|
144
|
+
return;
|
145
|
+
}
|
146
|
+
}
|
110
147
|
const offset = .01;
|
111
148
|
if (!this._projection || this.context.scene.environment !== this._lastEnvironment || this._height !== this._lastHeight || this._radius !== this._lastRadius) {
|
112
149
|
if (debug)
|
113
150
|
console.log("Create/Update Ground Projection", this.context.scene.environment.name);
|
114
151
|
this._projection?.removeFromParent();
|
115
|
-
this._projection = new GroundProjection(this.context.scene.environment, this._height, this.radius);
|
152
|
+
this._projection = new GroundProjection(this.context.scene.environment, this._height, this.radius, 64);
|
116
153
|
this._projection.position.y = this._height - offset;
|
117
|
-
if (this.context.scene.backgroundBlurriness > 0.001) {
|
118
|
-
this.updateBlurriness();
|
119
|
-
}
|
120
154
|
}
|
121
155
|
|
122
156
|
this._lastEnvironment = this.context.scene.environment;
|
@@ -149,6 +183,12 @@
|
|
149
183
|
if (this._projection.isObject3D === true) {
|
150
184
|
this._projection.layers.set(2);
|
151
185
|
}
|
186
|
+
|
187
|
+
if (this.context.scene.backgroundBlurriness > 0.001 || this._needsTextureUpdate) {
|
188
|
+
this.updateBlurriness();
|
189
|
+
}
|
190
|
+
|
191
|
+
this._needsTextureUpdate = false;
|
152
192
|
}
|
153
193
|
|
154
194
|
private _blurrynessShader: ShaderMaterial | null = null;
|
@@ -161,12 +201,13 @@
|
|
161
201
|
else if (!this.context.scene.environment) {
|
162
202
|
return;
|
163
203
|
}
|
164
|
-
|
204
|
+
this._needsTextureUpdate = false;
|
165
205
|
if (debug) console.log("Update Blurriness", this.context.scene.backgroundBlurriness);
|
166
206
|
this._blurrynessShader ??= new ShaderMaterial({
|
167
207
|
uniforms: {
|
168
208
|
map: { value: this.context.scene.environment },
|
169
|
-
blurriness: { value: this.context.scene.backgroundBlurriness }
|
209
|
+
blurriness: { value: this.context.scene.backgroundBlurriness },
|
210
|
+
blending: { value: 0 }
|
170
211
|
},
|
171
212
|
vertexShader: vertexShader,
|
172
213
|
fragmentShader: fragmentShader
|
@@ -174,6 +215,21 @@
|
|
174
215
|
this._blurrynessShader.depthWrite = false;
|
175
216
|
this._blurrynessShader.uniforms.blurriness.value = this.context.scene.backgroundBlurriness;
|
176
217
|
this._lastBlurriness = this.context.scene.backgroundBlurriness;
|
218
|
+
this.context.scene.environment.needsPMREMUpdate = true;
|
219
|
+
|
220
|
+
const wasTransparent = this._projection.material.transparent;
|
221
|
+
this._projection.material.transparent = (this.context.xr?.isAR === true && this.arBlending > 0.000001) ?? false;
|
222
|
+
if (this._projection.material.transparent) {
|
223
|
+
this._blurrynessShader.uniforms.blending.value = this.arBlending;
|
224
|
+
}
|
225
|
+
else { this._blurrynessShader.uniforms.blending.value = 0; }
|
226
|
+
|
227
|
+
// Make sure the material is updated if the transparency changed
|
228
|
+
if (wasTransparent !== this._projection.material.transparent) {
|
229
|
+
this._projection.material.needsUpdate = true;
|
230
|
+
}
|
231
|
+
|
232
|
+
// Update the texture
|
177
233
|
this._projection.material.map = Graphics.copyTexture(this.context.scene.environment, this._blurrynessShader);
|
178
234
|
}
|
179
235
|
|
@@ -193,6 +249,7 @@
|
|
193
249
|
const fragmentShader = `
|
194
250
|
uniform sampler2D map;
|
195
251
|
uniform float blurriness;
|
252
|
+
uniform float blending;
|
196
253
|
varying vec2 vUv;
|
197
254
|
|
198
255
|
const float PI = 3.14159265359;
|
@@ -212,10 +269,10 @@
|
|
212
269
|
vec2 center = vec2(0.0, 0.0);
|
213
270
|
vec2 pos = vUv;
|
214
271
|
pos.x = 0.0; // Only consider vertical distance
|
215
|
-
float distance = length(pos - center)
|
272
|
+
float distance = length(pos - center);
|
216
273
|
|
217
274
|
// Calculate blur amount based on custom falloff
|
218
|
-
float blurAmount = customSmoothstep(0.5, 1.0, distance);
|
275
|
+
float blurAmount = customSmoothstep(0.5, 1.0, distance * 2.0);
|
219
276
|
blurAmount = clamp(blurAmount, 0.0, 1.0); // Ensure blur amount is within valid range
|
220
277
|
|
221
278
|
// Gaussian blur
|
@@ -238,6 +295,11 @@
|
|
238
295
|
|
239
296
|
gl_FragColor = color;
|
240
297
|
|
298
|
+
float brightness = dot(gl_FragColor.rgb, vec3(0.299, 0.587, 0.114));
|
299
|
+
float stepFactor = blending - brightness * .1;
|
300
|
+
gl_FragColor.a = pow(1.0 - blending * customSmoothstep(0.35 * stepFactor, 0.45 * stepFactor, distance), 5.);
|
301
|
+
// gl_FragColor.rgb = vec3(1.0);
|
302
|
+
|
241
303
|
// #include <tonemapping_fragment>
|
242
304
|
// #include <colorspace_fragment>
|
243
305
|
|
@@ -359,6 +359,27 @@
|
|
359
359
|
flex-direction: column-reverse;
|
360
360
|
}
|
361
361
|
|
362
|
+
.compact .options {
|
363
|
+
max-height: 20ch;
|
364
|
+
overflow: auto;
|
365
|
+
flex-direction: row;
|
366
|
+
flex-wrap: wrap;
|
367
|
+
align-items: center;
|
368
|
+
}
|
369
|
+
|
370
|
+
::-webkit-scrollbar {
|
371
|
+
max-width: 7px;
|
372
|
+
background: rgba(100,100,100,.2);
|
373
|
+
border-radius: .2rem;
|
374
|
+
}
|
375
|
+
::-webkit-scrollbar-thumb {
|
376
|
+
background: rgba(255, 255, 255, .3);
|
377
|
+
border-radius: .2rem;
|
378
|
+
}
|
379
|
+
::-webkit-scrollbar-thumb:hover {
|
380
|
+
background: rgb(150,150,150);
|
381
|
+
}
|
382
|
+
|
362
383
|
.compact .options > * {
|
363
384
|
font-size: 1.2rem;
|
364
385
|
padding: .6rem .5rem;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { fetchProfile, MotionController } from "@webxr-input-profiles/motion-controllers";
|
2
|
-
import { AxesHelper, Int8BufferAttribute, Object3D, Quaternion, Ray, Vector3 } from "three";
|
2
|
+
import { AxesHelper, Int8BufferAttribute, Matrix4, Object3D, Quaternion, Ray, Vector3 } from "three";
|
3
3
|
|
4
4
|
import { RGBAColor } from "../../engine-components/js-extensions/RGBAColor.js";
|
5
5
|
import { Context } from "../engine_context.js";
|
@@ -131,9 +131,24 @@
|
|
131
131
|
|
132
132
|
/** The XRTransientInputHitTestSource can be used to perform hit tests with the controller ray against the real world.
|
133
133
|
* see https://developer.mozilla.org/en-US/docs/Web/API/XRSession/requestHitTestSourceForTransientInput for more information
|
134
|
-
* Requires the hit-test feature to be enabled in the XRSession
|
134
|
+
* Requires the hit-test feature to be enabled in the XRSession
|
135
|
+
*
|
136
|
+
* NOTE: The hit test source should be cancelled once it's not needed anymore. Call `cancelHitTestSource` to do this
|
135
137
|
*/
|
136
|
-
|
138
|
+
getHitTestSource() {
|
139
|
+
if (!this._hitTestSource) this._requestHitTestSource();
|
140
|
+
return this._hitTestSource;
|
141
|
+
}
|
142
|
+
get hasHitTestSource() {
|
143
|
+
return this._hitTestSource;
|
144
|
+
}
|
145
|
+
/** Make sure to cancel the hittest source once it's not needed anymore */
|
146
|
+
cancelHitTestSource() {
|
147
|
+
if (this._hitTestSource) {
|
148
|
+
this._hitTestSource.cancel();
|
149
|
+
this._hitTestSource = undefined;
|
150
|
+
}
|
151
|
+
}
|
137
152
|
private _hitTestSource: XRTransientInputHitTestSource | undefined = undefined;
|
138
153
|
private _hasSelectEvent = false;
|
139
154
|
get hasSelectEvent() { return this._hasSelectEvent; }
|
@@ -157,16 +172,28 @@
|
|
157
172
|
return pose;
|
158
173
|
}
|
159
174
|
|
175
|
+
/** Grip matrix in grip space */
|
176
|
+
private readonly _gripMatrix = new Matrix4();
|
177
|
+
/** Grip position in grip space */
|
160
178
|
private readonly _gripPosition = new Vector3();
|
179
|
+
/** Grip rotation in grip space */
|
161
180
|
private readonly _gripQuaternion = new Quaternion();
|
162
181
|
private readonly _linearVelocity: Vector3 = new Vector3();
|
182
|
+
|
183
|
+
private readonly _rayPositionRaw = new Vector3();
|
184
|
+
private readonly _rayRotationRaw = new Quaternion();
|
185
|
+
/** ray matrix in grip space */
|
186
|
+
private readonly _rayMatrix = new Matrix4();
|
187
|
+
/** Ray position in rig space */
|
163
188
|
private readonly _rayPosition = new Vector3();
|
189
|
+
/** Ray rotation in rig space */
|
164
190
|
private readonly _rayQuaternion = new Quaternion();
|
165
191
|
|
166
192
|
/** Grip position in rig space */
|
167
|
-
get gripPosition() { return getTempVector(this._gripPosition)
|
193
|
+
get gripPosition() { return getTempVector(this._gripPosition) }
|
168
194
|
/** Grip rotation in rig space */
|
169
|
-
get gripQuaternion() { return getTempQuaternion(this._gripQuaternion)
|
195
|
+
get gripQuaternion() { return getTempQuaternion(this._gripQuaternion) }
|
196
|
+
get gripMatrix() { return this._gripMatrix; }
|
170
197
|
/** Grip linear velocity in rig space
|
171
198
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRPose/linearVelocity
|
172
199
|
*/
|
@@ -174,14 +201,12 @@
|
|
174
201
|
return getTempVector(this._linearVelocity).applyQuaternion(flipForwardQuaternion);
|
175
202
|
}
|
176
203
|
/** Ray position in rig space */
|
177
|
-
get rayPosition() { return getTempVector(this._rayPosition)
|
204
|
+
get rayPosition() { return getTempVector(this._rayPosition) }
|
178
205
|
/** Ray rotation in rig space */
|
179
|
-
get rayQuaternion() { return getTempQuaternion(this._rayQuaternion)
|
206
|
+
get rayQuaternion() { return getTempQuaternion(this._rayQuaternion) }
|
180
207
|
|
181
208
|
/** Controller grip position in worldspace */
|
182
|
-
get gripWorldPosition() {
|
183
|
-
return getTempVector(this._gripWorldPosition);
|
184
|
-
}
|
209
|
+
get gripWorldPosition() { return getTempVector(this._gripWorldPosition); }
|
185
210
|
private readonly _gripWorldPosition: Vector3 = new Vector3();
|
186
211
|
|
187
212
|
/** Controller grip rotation in wordspace */
|
@@ -198,7 +223,7 @@
|
|
198
223
|
/** Recalculates the ray world position */
|
199
224
|
updateRayWorldPosition() {
|
200
225
|
const parent = this.xr.context.mainCamera?.parent;
|
201
|
-
this._rayWorldPosition.copy(this.
|
226
|
+
this._rayWorldPosition.copy(this._rayPositionRaw);
|
202
227
|
if (parent) this._rayWorldPosition.applyMatrix4(parent.matrixWorld);
|
203
228
|
}
|
204
229
|
|
@@ -211,7 +236,7 @@
|
|
211
236
|
updateRayWorldQuaternion() {
|
212
237
|
const parent = this.xr.context.mainCamera?.parent;
|
213
238
|
const parentWorldQuaternion = parent ? getWorldQuaternion(parent) : undefined;
|
214
|
-
this._rayWorldQuaternion.copy(this.
|
239
|
+
this._rayWorldQuaternion.copy(this._rayRotationRaw)
|
215
240
|
// flip forward because we want +Z to be forward
|
216
241
|
.multiply(flipForwardQuaternion);
|
217
242
|
if (parentWorldQuaternion) this._rayWorldQuaternion.premultiply(parentWorldQuaternion)
|
@@ -268,16 +293,30 @@
|
|
268
293
|
this.initialize();
|
269
294
|
this.subscribeEvents();
|
270
295
|
|
271
|
-
|
272
|
-
|
296
|
+
}
|
297
|
+
|
298
|
+
private _hitTestSourcePromise: Promise<XRTransientInputHitTestSource | null> | null = null;
|
299
|
+
private _requestHitTestSource(): Promise<XRTransientInputHitTestSource | null> | null {
|
300
|
+
if (this._hitTestSourcePromise) return this._hitTestSourcePromise;
|
301
|
+
// We only request a hit test source when we need it - meaning e.g. when we want to place the scene in AR
|
302
|
+
// Make sure to cancel the hittest source when we don't need it anymore for performance reasons
|
303
|
+
|
304
|
+
// // TODO: change this to check if we have hit-testing enabled instead of pass through.
|
305
|
+
if (this.xr.mode === "immersive-ar" && this.inputSource.targetRayMode === "tracked-pointer" && this.xr.session.requestHitTestSourceForTransientInput) {
|
273
306
|
// request hittest source
|
274
|
-
this.xr.session.requestHitTestSourceForTransientInput
|
307
|
+
return this._hitTestSourcePromise = this.xr.session.requestHitTestSourceForTransientInput({
|
275
308
|
profile: this.inputSource.profiles[0],
|
276
309
|
offsetRay: new XRRay(),
|
277
310
|
})?.then(hitTestSource => {
|
311
|
+
this._hitTestSourcePromise = null;
|
312
|
+
if (!this.connected) {
|
313
|
+
hitTestSource.cancel();
|
314
|
+
return null;
|
315
|
+
}
|
278
316
|
return this._hitTestSource = hitTestSource;
|
279
|
-
});
|
317
|
+
}) ?? null;
|
280
318
|
}
|
319
|
+
return null;
|
281
320
|
}
|
282
321
|
|
283
322
|
onPointerHits = _evt => {
|
@@ -312,19 +351,34 @@
|
|
312
351
|
|
313
352
|
const rayPose = frame.getPose(this.inputSource.targetRaySpace, this.xr.referenceSpace);
|
314
353
|
this._isTracking = rayPose != null;
|
354
|
+
let gripPositionRaw: Vector3 | null = null;
|
355
|
+
let gripQuaternionRaw: Quaternion | null = null;
|
356
|
+
let rayPositionRaw: Vector3 | null = null;
|
357
|
+
let rayQuaternionRaw: Quaternion | null = null;
|
315
358
|
|
316
359
|
if (rayPose) {
|
317
360
|
const t = rayPose.transform;
|
318
|
-
this.
|
319
|
-
|
361
|
+
this._rayMatrix
|
362
|
+
.fromArray(t.matrix)
|
363
|
+
.premultiply(flipForwardMatrix);
|
364
|
+
this._rayMatrix.decompose(this._rayPosition, this._rayQuaternion, getTempVector(1, 1, 1));
|
365
|
+
rayPositionRaw = getTempVector(t.position);
|
366
|
+
rayQuaternionRaw = getTempQuaternion(t.orientation);
|
367
|
+
this._rayPositionRaw.copy(t.position);
|
368
|
+
this._rayRotationRaw.copy(t.orientation);
|
320
369
|
}
|
321
370
|
|
322
371
|
if (this.inputSource.gripSpace) {
|
323
372
|
const gripPose = frame.getPose(this.inputSource.gripSpace, this.xr.referenceSpace!);
|
324
373
|
if (gripPose) {
|
325
374
|
const t = gripPose.transform;
|
326
|
-
|
327
|
-
|
375
|
+
gripPositionRaw = getTempVector(t.position);
|
376
|
+
gripQuaternionRaw = getTempQuaternion(t.orientation);
|
377
|
+
this._gripMatrix
|
378
|
+
.fromArray(t.matrix)
|
379
|
+
.premultiply(flipForwardMatrix);
|
380
|
+
this._gripMatrix.decompose(this._gripPosition, this._gripQuaternion, getTempVector(1, 1, 1));
|
381
|
+
|
328
382
|
if ("linearVelocity" in gripPose && gripPose.linearVelocity) {
|
329
383
|
const p = gripPose.linearVelocity as DOMPointReadOnly;
|
330
384
|
this._linearVelocity.set(p.x, p.y, p.z);
|
@@ -362,22 +416,22 @@
|
|
362
416
|
const middle = hand.get("middle-finger-metacarpal");
|
363
417
|
const middlePose = middle && this.getHandJointPose(middle);
|
364
418
|
if (middlePose) {
|
365
|
-
const p = middlePose.transform.position;
|
366
|
-
const q = middlePose.transform.orientation;
|
367
419
|
// for some reason the grip rotation is different from the wrist rotation
|
368
420
|
// but we want to use the wrist rotation for the grip
|
369
|
-
this.
|
370
|
-
|
421
|
+
this._gripMatrix
|
422
|
+
.fromArray(middlePose.transform.matrix)
|
423
|
+
.premultiply(flipForwardMatrix);
|
424
|
+
this._gripMatrix.decompose(this._gripPosition, this._gripQuaternion, getTempVector(1, 1, 1));
|
371
425
|
}
|
372
426
|
}
|
373
427
|
// on VisionOS we get a gripSpace that matches where the controller is for transient input sources
|
374
|
-
else if (this.inputSource.gripSpace && this.targetRayMode === "transient-pointer") {
|
375
|
-
this._object.position.copy(
|
376
|
-
this._object.quaternion.copy(
|
428
|
+
else if (this.inputSource.gripSpace && this.targetRayMode === "transient-pointer" && gripPositionRaw && gripQuaternionRaw) {
|
429
|
+
this._object.position.copy(gripPositionRaw);
|
430
|
+
this._object.quaternion.copy(gripQuaternionRaw).multiply(flipForwardQuaternion);
|
377
431
|
}
|
378
|
-
else {
|
379
|
-
this._object.position.copy(
|
380
|
-
this._object.quaternion.copy(
|
432
|
+
else if (rayPositionRaw && rayQuaternionRaw) {
|
433
|
+
this._object.position.copy(rayPositionRaw);
|
434
|
+
this._object.quaternion.copy(rayQuaternionRaw).multiply(flipForwardQuaternion);
|
381
435
|
}
|
382
436
|
|
383
437
|
|
@@ -386,12 +440,15 @@
|
|
386
440
|
const parentWorldQuaternion = parent ? getWorldQuaternion(parent) : undefined;
|
387
441
|
|
388
442
|
// GRIP
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
443
|
+
if (gripPositionRaw && gripQuaternionRaw) {
|
444
|
+
this._gripWorldPosition.copy(gripPositionRaw);
|
445
|
+
if (parent) this._gripWorldPosition.applyMatrix4(parent.matrixWorld);
|
446
|
+
|
447
|
+
this._gripWorldQuaternion.copy(gripQuaternionRaw);
|
448
|
+
// flip forward because we want +Z to be forward
|
449
|
+
this._gripWorldQuaternion.multiply(flipForwardQuaternion);
|
450
|
+
if (parentWorldQuaternion) this._gripWorldQuaternion.premultiply(parentWorldQuaternion)
|
451
|
+
}
|
395
452
|
|
396
453
|
// RAY
|
397
454
|
this.updateRayWorldPosition();
|
@@ -409,6 +466,10 @@
|
|
409
466
|
this._object.removeFromParent();
|
410
467
|
this._debugAxesHelper.removeFromParent();
|
411
468
|
this.unsubscribeEvents();
|
469
|
+
if (this._hitTestSource) {
|
470
|
+
this._hitTestSource.cancel();
|
471
|
+
this._hitTestSource = undefined;
|
472
|
+
}
|
412
473
|
}
|
413
474
|
|
414
475
|
/**
|
@@ -471,7 +532,7 @@
|
|
471
532
|
private readonly _needleGamepadButtons: { [key: number | string]: NeedleGamepadButton } = {};
|
472
533
|
/** combine the InputState information + the GamepadButton information (since GamepadButtons can not be extended) */
|
473
534
|
private toNeedleGamepadButton(index: number): NeedleGamepadButton | undefined {
|
474
|
-
if(!this.inputSource.gamepad?.buttons) return undefined
|
535
|
+
if (!this.inputSource.gamepad?.buttons) return undefined
|
475
536
|
const button = this.inputSource.gamepad?.buttons[index];
|
476
537
|
const state = this.states[index];
|
477
538
|
const needleButton = this._needleGamepadButtons[index] || new NeedleGamepadButton();
|
@@ -534,12 +534,22 @@
|
|
534
534
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession/visibilityState
|
535
535
|
* @returns {XRVisibilityState} The visibility state of the XRSession
|
536
536
|
*/
|
537
|
-
get visibilityState() { return this.session.visibilityState; }
|
537
|
+
get visibilityState(): XRVisibilityState { return this.session.visibilityState; }
|
538
538
|
|
539
539
|
/**
|
540
|
+
* Check if the session is `visible-blurred` - this means e.g. the keyboard is shown
|
541
|
+
*/
|
542
|
+
get isVisibleBlurred(): boolean { return this.session.visibilityState === 'visible-blurred' }
|
543
|
+
|
544
|
+
/**
|
545
|
+
* Check if the session has system keyboard support
|
546
|
+
*/
|
547
|
+
get isSystemKeyboardSupported(): boolean { return this.session.isSystemKeyboardSupported; }
|
548
|
+
|
549
|
+
/**
|
540
550
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession/environmentBlendMode
|
541
551
|
*/
|
542
|
-
get environmentBlendMode() { return this.session.environmentBlendMode; }
|
552
|
+
get environmentBlendMode(): XREnvironmentBlendMode { return this.session.environmentBlendMode; }
|
543
553
|
|
544
554
|
/**
|
545
555
|
* The current XR frame
|
@@ -718,7 +728,7 @@
|
|
718
728
|
return null;
|
719
729
|
}
|
720
730
|
private getControllerHitTest(controller: NeedleXRController): NeedleXRHitTestResult | null {
|
721
|
-
const hitTestSource = controller.
|
731
|
+
const hitTestSource = controller.getHitTestSource();
|
722
732
|
if (!hitTestSource) return null;
|
723
733
|
const res = this.frame.getHitTestResultsForTransientInput(hitTestSource);
|
724
734
|
for (const result of res) {
|
@@ -852,10 +862,31 @@
|
|
852
862
|
}
|
853
863
|
});
|
854
864
|
|
865
|
+
// Unfortunately the code below doesnt work: the session never receives any input sources sometimes
|
866
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/XRSession/visibilitychange_event
|
867
|
+
// this.session.addEventListener("visibilitychange", (evt: XRSessionEvent) => {
|
868
|
+
// // sometimes when entering an XR session the controllers are not added/not in the list and we don't receive an event
|
869
|
+
// // this is a workaround trying to add controllers when the scene visibility changes to "visible"
|
870
|
+
// // e.g. due to a user opening and closing the menu
|
871
|
+
// if (this.controllers.length === 0 && evt.session.visibilityState === "visible") {
|
872
|
+
// for (const controller of evt.session.inputSources) {
|
873
|
+
// this.onInputSourceAdded(controller);
|
874
|
+
// }
|
875
|
+
// }
|
876
|
+
// })
|
877
|
+
|
855
878
|
// we set the session on the webxr manager at the end because we want to receive inputsource events first
|
856
879
|
// e.g. in case there's a bug in the threejs codebase
|
857
880
|
this.context.xr = this;
|
858
881
|
this.context.renderer.xr.setSession(this.session).then(this.onRendererSessionSet);
|
882
|
+
// disable three.js renderer controller autoUpdate (added in ac67b31e3548386f8a93e23a4176554c92bbd0d9)
|
883
|
+
if ("controllerAutoUpdate" in this.context.renderer.xr) {
|
884
|
+
console.debug("Disabling three.js controllerAutoUpdate");
|
885
|
+
this.context.renderer.xr.controllerAutoUpdate = false;
|
886
|
+
}
|
887
|
+
else if (debug) {
|
888
|
+
console.warn("controllerAutoUpdate is not available in three.js - cannot disable it");
|
889
|
+
}
|
859
890
|
}
|
860
891
|
|
861
892
|
/** called when renderer.setSession is fulfilled */
|
@@ -1133,7 +1164,10 @@
|
|
1133
1164
|
const copy = [...this._newControllers];
|
1134
1165
|
this._newControllers.length = 0;
|
1135
1166
|
for (const controller of copy) {
|
1136
|
-
if (!controller.connected)
|
1167
|
+
if (!controller.connected) {
|
1168
|
+
console.warn("New controller is not connected", controller);
|
1169
|
+
continue;
|
1170
|
+
}
|
1137
1171
|
this.controllers.push(controller);
|
1138
1172
|
for (const script of this._xr_scripts) {
|
1139
1173
|
if (script.destroyed) {
|
@@ -1151,6 +1185,11 @@
|
|
1151
1185
|
this.controllers.sort((a, b) => a.index - b.index);
|
1152
1186
|
}
|
1153
1187
|
|
1188
|
+
if (debug && this.context.time.frame % 30 === 0 && this.controllers.length <= 0 && this.session.inputSources.length > 0) {
|
1189
|
+
enableSpatialConsole(true)
|
1190
|
+
console.error("XRControllers are not added but inputSources are present");
|
1191
|
+
}
|
1192
|
+
|
1154
1193
|
// invoke update on all scripts
|
1155
1194
|
for (const script of this._xr_update_scripts) {
|
1156
1195
|
if (script.destroyed === true) {
|
@@ -1198,10 +1237,11 @@
|
|
1198
1237
|
const upwards = this.rig.gameObject.worldUp;
|
1199
1238
|
pos.add(upwards.multiplyScalar(2.5));
|
1200
1239
|
let debugLabel = "";
|
1201
|
-
debugLabel += this.context.time.smoothedFps.toFixed(
|
1202
|
-
|
1240
|
+
debugLabel += `${this.context.time.smoothedFps.toFixed(0)} FPS`;
|
1241
|
+
debugLabel += `, calls: ${this.context.renderer.info.render.calls}, tris: ${this.context.renderer.info.render.triangles.toLocaleString()}`;
|
1242
|
+
if (debug || debugFPS) {
|
1203
1243
|
for (const ctrl of this.controllers) {
|
1204
|
-
debugLabel += `\n${ctrl.hand ? "hand" : "ctrl"} ${ctrl.inputSource.handedness}[${ctrl.index}] con:${ctrl.connected} tr:${ctrl.isTracking}`;
|
1244
|
+
debugLabel += `\n${ctrl.hand ? "hand" : "ctrl"} ${ctrl.inputSource.handedness}[${ctrl.index}] con:${ctrl.connected} tr:${ctrl.isTracking} hts:${ctrl.hasHitTestSource ? "yes" : "no"}`;
|
1205
1245
|
}
|
1206
1246
|
}
|
1207
1247
|
Gizmos.DrawLabel(pos, debugLabel);
|
@@ -385,6 +385,12 @@
|
|
385
385
|
if (this.useXRAnchor) {
|
386
386
|
this.onCreateAnchor(NeedleXRSession.active!, hit);
|
387
387
|
}
|
388
|
+
|
389
|
+
if (this.context.xr) {
|
390
|
+
for (const ctrl of this.context.xr.controllers) {
|
391
|
+
ctrl.cancelHitTestSource();
|
392
|
+
}
|
393
|
+
}
|
388
394
|
}
|
389
395
|
|
390
396
|
private onScaleChanged() {
|
@@ -15,6 +15,7 @@
|
|
15
15
|
import { registerExtensions } from "../../../engine/extensions/extensions.js";
|
16
16
|
import { NEEDLE_progressive } from "../../../engine/extensions/NEEDLE_progressive.js";
|
17
17
|
import { Behaviour, GameObject } from "../../Component.js"
|
18
|
+
import { flipForwardMatrix } from "../../../engine/xr/internal.js";
|
18
19
|
|
19
20
|
const debug = getParam("debugwebxr");
|
20
21
|
|
@@ -97,6 +98,10 @@
|
|
97
98
|
// The controller mesh should by default inherit layers.
|
98
99
|
model.traverse(child => {
|
99
100
|
child.layers.set(2);
|
101
|
+
// disable auto update on controller objects. No need to do this every frame
|
102
|
+
child.matrixWorldAutoUpdate = false;
|
103
|
+
child.matrixAutoUpdate = false;
|
104
|
+
child.updateMatrix();
|
100
105
|
});
|
101
106
|
controller.model = model;
|
102
107
|
}
|
@@ -168,11 +173,9 @@
|
|
168
173
|
|
169
174
|
// do we have a controller model?
|
170
175
|
if (entry.model && !entry.handmesh) {
|
171
|
-
|
172
|
-
|
173
|
-
entry.model.
|
174
|
-
// entry.model.quaternion.copy(ctrl.gripWorldQuaternion);
|
175
|
-
entry.model.quaternion.copy(ctrl.gripQuaternion);
|
176
|
+
entry.model.matrixAutoUpdate = false;
|
177
|
+
entry.model.matrixWorldAutoUpdate = false;
|
178
|
+
entry.model.matrix.copy(ctrl.gripMatrix);
|
176
179
|
entry.model.visible = ctrl.isTracking;
|
177
180
|
// ensure that controller models are in rig space
|
178
181
|
xr.rig?.gameObject.add(entry.model);
|
@@ -203,12 +206,11 @@
|
|
203
206
|
// Update the joints groups with the XRJoint poses
|
204
207
|
const jointPose = ctrl.getHandJointPose(inputjoint);
|
205
208
|
if (jointPose) {
|
206
|
-
|
207
|
-
|
208
|
-
// joint.matrix.decompose(joint.position, joint.quaternion, joint.scale);
|
209
|
-
const { position, quaternion } = xr.convertSpace(jointPose.transform);
|
209
|
+
const position = jointPose.transform.position;
|
210
|
+
const quaternion = jointPose.transform.orientation;
|
210
211
|
joint.position.copy(position);
|
211
212
|
joint.quaternion.copy(quaternion);
|
213
|
+
joint.matrixAutoUpdate = false;
|
212
214
|
joint.matrixWorldAutoUpdate = false;
|
213
215
|
}
|
214
216
|
joint.visible = jointPose != null;
|
@@ -220,10 +222,15 @@
|
|
220
222
|
if (entry.model.visible && entry.model.parent !== xr.rig?.gameObject) {
|
221
223
|
xr.rig?.gameObject.add(entry.model);
|
222
224
|
}
|
223
|
-
entry.model.position.set(0, 0, 0);
|
224
225
|
}
|
225
226
|
|
226
|
-
if (entry.model?.visible)
|
227
|
+
if (entry.model?.visible) {
|
228
|
+
entry.handmesh?.updateMesh();
|
229
|
+
entry.model.matrixWorldAutoUpdate = false;
|
230
|
+
entry.model.matrixAutoUpdate = false;
|
231
|
+
entry.model.matrix.identity();
|
232
|
+
entry.model.applyMatrix4(flipForwardMatrix);
|
233
|
+
}
|
227
234
|
}
|
228
235
|
}
|
229
236
|
}
|
@@ -62,7 +62,7 @@
|
|
62
62
|
* @default false
|
63
63
|
*/
|
64
64
|
@serializable()
|
65
|
-
showHits: boolean =
|
65
|
+
showHits: boolean = true;
|
66
66
|
|
67
67
|
readonly isXRMovementHandler: true = true;
|
68
68
|
readonly xrSessionMode = "immersive-vr";
|
@@ -257,7 +257,7 @@
|
|
257
257
|
continue;
|
258
258
|
}
|
259
259
|
|
260
|
-
const hit = this.context.physics.raycastFromRay(ctrl.ray, { testObject: this.hitPointRaycastFilter })[0];
|
260
|
+
const hit = this.context.physics.raycastFromRay(ctrl.ray, { testObject: this.hitPointRaycastFilter, precise: false })[0];
|
261
261
|
this._hitDistances[i] = hit?.distance;
|
262
262
|
|
263
263
|
let disc = this._hitDiscs[i];
|