@@ -26,63 +26,33 @@
|
|
26
26
|
}
|
27
27
|
|
28
28
|
|
29
|
+
let userInteractionRegistered = false;
|
30
|
+
function onUserInteraction() {
|
31
|
+
userInteractionRegistered = true;
|
32
|
+
}
|
33
|
+
document.addEventListener('pointerdown', onUserInteraction);
|
34
|
+
document.addEventListener('click', onUserInteraction);
|
35
|
+
document.addEventListener('dragstart', onUserInteraction);
|
36
|
+
document.addEventListener('touchstart', onUserInteraction);
|
37
|
+
|
29
38
|
export class AudioSource extends Behaviour {
|
30
39
|
|
31
|
-
private static _didCallBeginWaitForUserInteraction: boolean = false;
|
32
40
|
public static get userInteractionRegistered(): boolean {
|
33
|
-
|
34
|
-
if (!AudioSource._didCallBeginWaitForUserInteraction) {
|
35
|
-
AudioSource._didCallBeginWaitForUserInteraction = true;
|
36
|
-
AudioSource._beginWaitForUserInteraction();
|
37
|
-
}
|
38
|
-
return AudioSource._userInteractionRegistered;
|
41
|
+
return userInteractionRegistered;
|
39
42
|
}
|
40
43
|
|
41
44
|
private static callbacks: Function[] = [];
|
42
45
|
public static registerWaitForAllowAudio(cb: Function) {
|
43
46
|
if (cb !== null) {
|
44
|
-
if (
|
47
|
+
if (userInteractionRegistered) {
|
45
48
|
cb();
|
46
49
|
return;
|
47
50
|
}
|
48
51
|
if (this.callbacks.indexOf(cb) === -1)
|
49
52
|
this.callbacks.push(cb);
|
50
|
-
if (!AudioSource._didCallBeginWaitForUserInteraction) {
|
51
|
-
AudioSource._didCallBeginWaitForUserInteraction = true;
|
52
|
-
AudioSource._beginWaitForUserInteraction();
|
53
|
-
}
|
54
53
|
}
|
55
54
|
}
|
56
55
|
|
57
|
-
private static _userInteractionRegistered: boolean = false;
|
58
|
-
private static _beginWaitForUserInteraction(cb: Function | null = null) {
|
59
|
-
if (this._userInteractionRegistered) {
|
60
|
-
if (cb) cb();
|
61
|
-
return;
|
62
|
-
}
|
63
|
-
if (cb !== null)
|
64
|
-
this.registerWaitForAllowAudio(cb);
|
65
|
-
const callback = () => {
|
66
|
-
if (fn == undefined) return;
|
67
|
-
if (AudioSource._userInteractionRegistered) return;
|
68
|
-
AudioSource._userInteractionRegistered = true;
|
69
|
-
if (debug) console.log("🔊 registered interaction, can play audio now");
|
70
|
-
document.removeEventListener('pointerdown', fn);
|
71
|
-
document.removeEventListener('click', fn);
|
72
|
-
document.removeEventListener('dragstart', fn);
|
73
|
-
document.removeEventListener('touchstart', fn);
|
74
|
-
for (const cb of this.callbacks) {
|
75
|
-
cb();
|
76
|
-
}
|
77
|
-
this.callbacks.length = 0;
|
78
|
-
};
|
79
|
-
const fn = callback.bind(this);
|
80
|
-
document.addEventListener('pointerdown', fn);
|
81
|
-
document.addEventListener('click', fn);
|
82
|
-
document.addEventListener('dragstart', fn);
|
83
|
-
document.addEventListener('touchstart', fn);
|
84
|
-
}
|
85
|
-
|
86
56
|
@serializable(URL)
|
87
57
|
clip: string | MediaStream = "";
|
88
58
|
|
@@ -160,7 +130,7 @@
|
|
160
130
|
private _audioElement: HTMLAudioElement | null = null;
|
161
131
|
|
162
132
|
public get Sound(): PositionalAudio | null {
|
163
|
-
if (!this.sound && AudioSource.
|
133
|
+
if (!this.sound && AudioSource.userInteractionRegistered) {
|
164
134
|
let listener = GameObject.getComponent(this.context.mainCamera, AudioListener) ?? GameObject.findObjectOfType(AudioListener, this.context);
|
165
135
|
if (!listener && this.context.mainCamera) listener = GameObject.addNewComponent(this.context.mainCamera, AudioListener);
|
166
136
|
if (listener?.listener) {
|
@@ -181,8 +151,8 @@
|
|
181
151
|
}
|
182
152
|
|
183
153
|
onEnable(): void {
|
184
|
-
if (!AudioSource.
|
185
|
-
AudioSource.
|
154
|
+
if (!AudioSource.userInteractionRegistered) {
|
155
|
+
AudioSource.registerWaitForAllowAudio(() => {
|
186
156
|
if (this.enabled && !this.destroyed && this.shouldPlay)
|
187
157
|
this.onNewClip(this.clip);
|
188
158
|
});
|
@@ -211,8 +181,8 @@
|
|
211
181
|
}
|
212
182
|
break;
|
213
183
|
case "visible":
|
214
|
-
if (debug) console.log("visible", this.enabled, this.playOnAwake, !this.isPlaying, AudioSource.
|
215
|
-
if (this.enabled && this.playOnAwake && !this.isPlaying && AudioSource.
|
184
|
+
if (debug) console.log("visible", this.enabled, this.playOnAwake, !this.isPlaying, AudioSource.userInteractionRegistered, this.wasPlaying);
|
185
|
+
if (this.enabled && this.playOnAwake && !this.isPlaying && AudioSource.userInteractionRegistered && this.wasPlaying) {
|
216
186
|
this.play();
|
217
187
|
}
|
218
188
|
break;
|
@@ -257,7 +227,7 @@
|
|
257
227
|
|
258
228
|
if (debug) console.log(this.name, this.shouldPlay, AudioSource.userInteractionRegistered, this);
|
259
229
|
|
260
|
-
if (this.shouldPlay && AudioSource.
|
230
|
+
if (this.shouldPlay && AudioSource.userInteractionRegistered)
|
261
231
|
this.play();
|
262
232
|
});
|
263
233
|
}
|
@@ -322,7 +292,7 @@
|
|
322
292
|
/** Play a mediastream */
|
323
293
|
play(clip: string | MediaStream | undefined = undefined) {
|
324
294
|
// use audio source's clip when no clip is passed in
|
325
|
-
if(!clip && this.clip)
|
295
|
+
if (!clip && this.clip)
|
326
296
|
clip = this.clip;
|
327
297
|
|
328
298
|
// We only support strings and media stream
|
@@ -17,6 +17,8 @@
|
|
17
17
|
if (this.isGizmo && !params.showGizmos) return;
|
18
18
|
if (!this._axes)
|
19
19
|
this._axes = new _AxesHelper(this.length);
|
20
|
+
this._axes.layers.disableAll();
|
21
|
+
this._axes.layers.set(this.layer);
|
20
22
|
this.gameObject.add(this._axes);
|
21
23
|
const mat: any = this._axes.material;
|
22
24
|
if (mat) {
|
@@ -478,16 +478,16 @@
|
|
478
478
|
|
479
479
|
onPointerClick(args: PointerEventData) {
|
480
480
|
args.use();
|
481
|
-
if (!this.target && !this.clip) return;
|
482
481
|
|
482
|
+
if (!this.clip) return;
|
483
|
+
|
483
484
|
if (!this.target) {
|
484
|
-
|
485
485
|
const newAudioSource = this.gameObject.addNewComponent(AudioSource);
|
486
486
|
if (newAudioSource) {
|
487
|
+
this.target = newAudioSource;
|
487
488
|
newAudioSource.spatialBlend = 1;
|
488
489
|
newAudioSource.volume = 1;
|
489
490
|
newAudioSource.loop = false;
|
490
|
-
this.target = newAudioSource;
|
491
491
|
}
|
492
492
|
}
|
493
493
|
|
@@ -7,6 +7,7 @@
|
|
7
7
|
import { Animator } from "../Animator.js";
|
8
8
|
import { getParam } from "../../engine/engine_utils.js";
|
9
9
|
import { showBalloonMessage } from "../../engine/debug/index.js";
|
10
|
+
import { GraphicRaycaster, ObjectRaycaster, Raycaster } from "./Raycaster.js";
|
10
11
|
|
11
12
|
const debug = getParam("debugbutton");
|
12
13
|
|
@@ -176,6 +177,9 @@
|
|
176
177
|
|
177
178
|
start() {
|
178
179
|
this._image?.setInteractable(this.interactable);
|
180
|
+
if (!this.gameObject.getComponentInParent(Raycaster)) {
|
181
|
+
this.gameObject.addComponent(GraphicRaycaster);
|
182
|
+
}
|
179
183
|
}
|
180
184
|
|
181
185
|
onEnable() {
|
@@ -38,6 +38,7 @@
|
|
38
38
|
export { Button } from "../ui/Button.js";
|
39
39
|
export { CallInfo } from "../EventList.js";
|
40
40
|
export { Camera } from "../Camera.js";
|
41
|
+
export { CameraTargetReachedEvent } from "../OrbitControls.js";
|
41
42
|
export { Canvas } from "../ui/Canvas.js";
|
42
43
|
export { CanvasGroup } from "../ui/CanvasGroup.js";
|
43
44
|
export { CapsuleCollider } from "../Collider.js";
|
@@ -4,7 +4,11 @@
|
|
4
4
|
import { CustomBlending, DoubleSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, OrthographicCamera, PlaneGeometry, ShaderMaterial, WebGLRenderTarget } from "three";
|
5
5
|
import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js';
|
6
6
|
import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js';
|
7
|
+
import { getParam } from "../engine/engine_utils.js"
|
8
|
+
import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
|
7
9
|
|
10
|
+
const debug = getParam("debugcontactshadows");
|
11
|
+
|
8
12
|
// Adapted from https://github.com/mrdoob/three.js/blob/master/examples/webgl_shadow_contact.html.
|
9
13
|
|
10
14
|
// Improved with
|
@@ -40,6 +44,7 @@
|
|
40
44
|
private verticalBlurMaterial?: ShaderMaterial;
|
41
45
|
|
42
46
|
awake(): void {
|
47
|
+
if(debug) console.log("Create ContactShadows on " + this.gameObject.name, this)
|
43
48
|
const textureSize = 512;
|
44
49
|
|
45
50
|
this.shadowGroup = new Group();
|
@@ -55,11 +60,17 @@
|
|
55
60
|
|
56
61
|
// make a plane and make it face up
|
57
62
|
const planeGeometry = new PlaneGeometry(1, 1).rotateX(Math.PI / 2);
|
58
|
-
|
59
|
-
if (this.gameObject
|
63
|
+
|
64
|
+
if (this.gameObject instanceof Mesh) {
|
65
|
+
if (debug) console.log("ContactShadows: use existing mesh", this.gameObject)
|
60
66
|
this.plane = this.gameObject as any as Mesh;
|
61
|
-
|
67
|
+
// Make sure we clone the material once because it might be used on another object as well
|
68
|
+
const mat = this.plane.material = (this.plane.material as MeshBasicMaterial).clone();
|
62
69
|
mat.map = this.renderTarget.texture;
|
70
|
+
mat.opacity = this.opacity;
|
71
|
+
// mat.transparent = true;
|
72
|
+
// mat.depthWrite = false;
|
73
|
+
// mat.needsUpdate = true;
|
63
74
|
// When someone makes a custom mesh, they can set these values right on the material.
|
64
75
|
// mat.opacity = this.state.plane.opacity;
|
65
76
|
// mat.transparent = true;
|
@@ -85,7 +96,9 @@
|
|
85
96
|
depthWrite: true,
|
86
97
|
stencilWrite: true,
|
87
98
|
colorWrite: false,
|
88
|
-
}))
|
99
|
+
}))
|
100
|
+
// .rotateX(Math.PI)
|
101
|
+
.translateY(-0.0001);
|
89
102
|
this.occluderMesh.renderOrder = -100;
|
90
103
|
this.gameObject.add(this.occluderMesh);
|
91
104
|
}
|
@@ -159,13 +172,12 @@
|
|
159
172
|
if (!this.renderTarget || !this.renderTargetBlur ||
|
160
173
|
!this.depthMaterial || !this.shadowCamera ||
|
161
174
|
!this.blurPlane || !this.shadowGroup || !this.plane ||
|
162
|
-
!this.horizontalBlurMaterial || !this.verticalBlurMaterial)
|
175
|
+
!this.horizontalBlurMaterial || !this.verticalBlurMaterial) {
|
176
|
+
if(debug)
|
177
|
+
console.error("ContactShadows: not initialized yet");
|
163
178
|
return;
|
179
|
+
}
|
164
180
|
|
165
|
-
//@ts-ignore
|
166
|
-
if (this.gameObject.isMesh)
|
167
|
-
this.gameObject.visible = false;
|
168
|
-
|
169
181
|
// Idea: shear the shadowCamera matrix to add some light direction to the ground shadows
|
170
182
|
/*
|
171
183
|
const mat = this.shadowCamera.projectionMatrix.clone();
|
@@ -177,6 +189,11 @@
|
|
177
189
|
const planeWasVisible = this.plane.visible;
|
178
190
|
this.plane.visible = false;
|
179
191
|
|
192
|
+
if (this.gameObject instanceof Mesh) {
|
193
|
+
this.gameObject.visible = false;
|
194
|
+
setCustomVisibility(this.gameObject, false);
|
195
|
+
}
|
196
|
+
|
180
197
|
// remove the background
|
181
198
|
const initialBackground = scene.background;
|
182
199
|
scene.background = null;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import type { ICameraController } from "./engine_types.js";
|
2
|
-
import { Camera } from "three";
|
2
|
+
import { Camera, Object3D } from "three";
|
3
3
|
|
4
4
|
|
5
5
|
const $cameraController = Symbol("cameraController");
|
@@ -11,8 +11,23 @@
|
|
11
11
|
export function setCameraController(cam: Camera, cameraController: ICameraController, active: boolean) {
|
12
12
|
if (active)
|
13
13
|
cam[$cameraController] = cameraController;
|
14
|
-
else{
|
15
|
-
if(cam[$cameraController] === cameraController)
|
14
|
+
else {
|
15
|
+
if (cam[$cameraController] === cameraController)
|
16
16
|
cam[$cameraController] = null;
|
17
17
|
}
|
18
|
+
}
|
19
|
+
|
20
|
+
|
21
|
+
const $autofit = Symbol("camera autofit");
|
22
|
+
|
23
|
+
export function useForAutoFit(obj: Object3D): boolean {
|
24
|
+
// if autofit is not defined we assume it may be included
|
25
|
+
if (obj[$autofit] === undefined) return true;
|
26
|
+
// otherwise if anything is set except false we assume it should be included
|
27
|
+
return obj[$autofit] !== false;
|
28
|
+
}
|
29
|
+
|
30
|
+
export function setAutoFitEnabled(obj: Object3D, enabled: boolean): void {
|
31
|
+
obj[$autofit] = enabled;
|
32
|
+
|
18
33
|
}
|
@@ -84,9 +84,92 @@
|
|
84
84
|
}
|
85
85
|
return true;
|
86
86
|
}
|
87
|
+
|
88
|
+
easeInOutCubic(x: number) {
|
89
|
+
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
90
|
+
}
|
87
91
|
};
|
88
92
|
|
89
93
|
const vectorKeys = ["x", "y", "z", "w"]
|
90
94
|
|
91
|
-
const Mathf = new MathHelper();
|
92
|
-
|
95
|
+
export const Mathf = new MathHelper();
|
96
|
+
|
97
|
+
|
98
|
+
class LowPassFilter {
|
99
|
+
y: number | null;
|
100
|
+
s: number | null;
|
101
|
+
alpha = 0;
|
102
|
+
|
103
|
+
constructor(alpha: number) {
|
104
|
+
this.setAlpha(alpha);
|
105
|
+
this.y = null;
|
106
|
+
this.s = null;
|
107
|
+
}
|
108
|
+
|
109
|
+
setAlpha(alpha: number) {
|
110
|
+
if (alpha <= 0 || alpha > 1.0) {
|
111
|
+
throw new Error();
|
112
|
+
}
|
113
|
+
this.alpha = alpha;
|
114
|
+
}
|
115
|
+
|
116
|
+
filter(value: number, alpha: number) {
|
117
|
+
if (alpha) {
|
118
|
+
this.setAlpha(alpha);
|
119
|
+
}
|
120
|
+
let s: number;
|
121
|
+
if (!this.y) {
|
122
|
+
s = value;
|
123
|
+
} else {
|
124
|
+
s = this.alpha * value + (1.0 - this.alpha) * this.s!;
|
125
|
+
}
|
126
|
+
this.y = value;
|
127
|
+
this.s = s;
|
128
|
+
return s;
|
129
|
+
}
|
130
|
+
|
131
|
+
lastValue() {
|
132
|
+
return this.y;
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
export class OneEuroFilter {
|
137
|
+
freq: number;
|
138
|
+
minCutOff: number;
|
139
|
+
beta: number;
|
140
|
+
dCutOff: number;
|
141
|
+
x: LowPassFilter;
|
142
|
+
dx: LowPassFilter;
|
143
|
+
lasttime: number | null;
|
144
|
+
|
145
|
+
constructor(freq: number, minCutOff = 1.0, beta = 0.0, dCutOff = 1.0) {
|
146
|
+
if (freq <= 0 || minCutOff <= 0 || dCutOff <= 0) {
|
147
|
+
throw new Error();
|
148
|
+
}
|
149
|
+
this.freq = freq;
|
150
|
+
this.minCutOff = minCutOff;
|
151
|
+
this.beta = beta;
|
152
|
+
this.dCutOff = dCutOff;
|
153
|
+
this.x = new LowPassFilter(this.alpha(this.minCutOff));
|
154
|
+
this.dx = new LowPassFilter(this.alpha(this.dCutOff));
|
155
|
+
this.lasttime = null;
|
156
|
+
}
|
157
|
+
|
158
|
+
alpha(cutOff: number) {
|
159
|
+
const te = 1.0 / this.freq;
|
160
|
+
const tau = 1.0 / (2 * Math.PI * cutOff);
|
161
|
+
return 1.0 / (1.0 + tau / te);
|
162
|
+
}
|
163
|
+
|
164
|
+
filter(x: number, time: number | null = null) {
|
165
|
+
if (this.lasttime && time) {
|
166
|
+
this.freq = 1.0 / (time - this.lasttime);
|
167
|
+
}
|
168
|
+
this.lasttime = time;
|
169
|
+
const prevX = this.x.lastValue();
|
170
|
+
const dx = !prevX ? 0.0 : (x - prevX) * this.freq;
|
171
|
+
const edx = this.dx.filter(dx, this.alpha(this.dCutOff));
|
172
|
+
const cutOff = this.minCutOff + this.beta * Math.abs(edx);
|
173
|
+
return this.x.filter(x, this.alpha(cutOff));
|
174
|
+
}
|
175
|
+
}
|
@@ -563,7 +563,7 @@
|
|
563
563
|
const finalRadius = radius * scale.x;
|
564
564
|
// half height = distance between capsule origin and top sphere origin (not the top end of the capsule)
|
565
565
|
height = Math.max(height, finalRadius * 2);
|
566
|
-
const hh = (height * .5 * scale.y) - (radius * scale.x);
|
566
|
+
const hh = Mathf.clamp((height * .5 * scale.y) - (radius * scale.x), 0, Number.MAX_SAFE_INTEGER);
|
567
567
|
const desc = ColliderDesc.capsule(hh, finalRadius);
|
568
568
|
this.createCollider(collider, desc, center);
|
569
569
|
}
|
@@ -8,6 +8,8 @@
|
|
8
8
|
const debugPhysics = getParam("debugphysics");
|
9
9
|
const layerMaskHelper: Layers = new Layers();
|
10
10
|
|
11
|
+
declare type IgnoreCallback = (obj: Object3D) => void | boolean | "continue in children";
|
12
|
+
|
11
13
|
export class RaycastOptions {
|
12
14
|
ray: Ray | undefined = undefined;
|
13
15
|
cam: Camera | undefined | null = undefined;
|
@@ -22,6 +24,10 @@
|
|
22
24
|
/** raw layer mask, use setLayer to set an individual layer active */
|
23
25
|
layerMask: Layers | number | undefined = undefined;
|
24
26
|
ignore: Object3D[] | undefined = undefined;
|
27
|
+
/** if defined it's called per object before tested for intersections.
|
28
|
+
* Return `false` to ignore the object completely or `"continue in children"` to skip the object but continue to traverse its children (if you do raycast with `recursive` enabled)
|
29
|
+
* */
|
30
|
+
testObject?: IgnoreCallback = undefined;
|
25
31
|
|
26
32
|
screenPointFromOffset(ox: number, oy: number) {
|
27
33
|
if (this.screenPoint === undefined) this.screenPoint = new Vector2();
|
@@ -212,7 +218,8 @@
|
|
212
218
|
|
213
219
|
// shoot
|
214
220
|
results.length = 0;
|
215
|
-
|
221
|
+
this.intersect(this.raycaster, targets, results, options);
|
222
|
+
results.sort((a, b) => a.distance - b.distance);
|
216
223
|
|
217
224
|
// TODO: instead of doing this we should temporerly set these objects to layer 2 during raycasting
|
218
225
|
const ignorelist = options.ignore;
|
@@ -221,4 +228,19 @@
|
|
221
228
|
}
|
222
229
|
return results;
|
223
230
|
}
|
231
|
+
|
232
|
+
private intersect(raycaster: Raycaster, objects: Object3D[], results: Intersection[], options: RaycastOptions) {
|
233
|
+
for (const obj of objects) {
|
234
|
+
const testResult = options.testObject?.(obj);
|
235
|
+
if (testResult === false) continue;
|
236
|
+
const checkObject = testResult !== "continue in children";
|
237
|
+
if (checkObject)
|
238
|
+
raycaster.intersectObject(obj, false, results);
|
239
|
+
|
240
|
+
if (options.recursive) {
|
241
|
+
this.intersect(raycaster, obj.children, results, options);
|
242
|
+
}
|
243
|
+
}
|
244
|
+
return results;
|
245
|
+
}
|
224
246
|
}
|
@@ -92,11 +92,25 @@
|
|
92
92
|
const res = GameObject.findObjectOfType(Raycaster, this.context);
|
93
93
|
if (!res) {
|
94
94
|
const rc = GameObject.addNewComponent(this.context.scene, ObjectRaycaster);
|
95
|
+
rc.ignoreSkinnedMeshes = true;
|
95
96
|
this.raycaster.push(rc);
|
96
97
|
if (isDevEnvironment() || debug)
|
97
|
-
console.warn("Added an ObjectRaycaster to the scene because no raycaster was found");
|
98
|
+
console.warn("Added an ObjectRaycaster to the scene because no raycaster was found. Skinnedmeshes will be ignored for better performance");
|
98
99
|
}
|
99
100
|
}
|
101
|
+
|
102
|
+
if (isDevEnvironment()) {
|
103
|
+
const foundProblematicObjects: string[] = [];
|
104
|
+
for (const rc of this.raycaster) {
|
105
|
+
if (rc instanceof ObjectRaycaster) {
|
106
|
+
if (rc.ignoreSkinnedMeshes === false) {
|
107
|
+
foundProblematicObjects.push(rc.gameObject.name);
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
if (foundProblematicObjects.length > 0)
|
112
|
+
console.warn("Found ObjectRaycaster that doesn't ignore skinned meshes. This might cause performance issues. Consider enabling \"ignoreSkinnedMeshes\" on the ObjectRaycaster components in your scene", foundProblematicObjects);
|
113
|
+
}
|
100
114
|
}
|
101
115
|
|
102
116
|
register(rc: Raycaster) {
|
@@ -166,7 +180,7 @@
|
|
166
180
|
const controllerRcOpts = new RaycastOptions();
|
167
181
|
this._selectUpdateFn ??= (_ctrl: WebXRController) => {
|
168
182
|
controllerRcOpts.ray = _ctrl.getRay();
|
169
|
-
const rc = this.performRaycast(controllerRcOpts);
|
183
|
+
const rc = this.performRaycast(controllerRcOpts, _ctrl.selectionClick);
|
170
184
|
if (!rc) return;
|
171
185
|
const opts = new PointerEventData(this.context.input);
|
172
186
|
opts.inputSource = _ctrl;
|
@@ -226,7 +240,7 @@
|
|
226
240
|
|
227
241
|
data.inputSource = this.context.input;
|
228
242
|
data.pointerId = pointerId;
|
229
|
-
data.isClicked =
|
243
|
+
data.isClicked = pointerEvent.type == InputEvents.PointerUp && this.context.input.getPointerClicked(pointerId)
|
230
244
|
// using the input type directly instead of input API -> otherwise onMove events can sometimes be getPointerUp() == true
|
231
245
|
data.isDown = pointerEvent.type == InputEvents.PointerDown;
|
232
246
|
data.isUp = pointerEvent.type == InputEvents.PointerUp;
|
@@ -238,7 +252,7 @@
|
|
238
252
|
const options = new RaycastOptions();
|
239
253
|
options.screenPoint = this.context.input.getPointerPositionRC(pointerId)!;
|
240
254
|
|
241
|
-
const hits = this.performRaycast(options);
|
255
|
+
const hits = this.performRaycast(options, data.isClicked);
|
242
256
|
if (!hits) return;
|
243
257
|
|
244
258
|
if (debug && data.isClicked) {
|
@@ -277,12 +291,27 @@
|
|
277
291
|
|
278
292
|
private readonly _sortedHits: THREE.Intersection[] = [];
|
279
293
|
|
280
|
-
private performRaycast(opts: RaycastOptions | null): THREE.Intersection[] | null {
|
294
|
+
private performRaycast(opts: RaycastOptions | null, isClick: boolean): THREE.Intersection[] | null {
|
281
295
|
if (!this.raycaster) return null;
|
282
296
|
this._sortedHits.length = 0;
|
283
297
|
for (const rc of this.raycaster) {
|
284
298
|
if (!rc.activeAndEnabled) continue;
|
299
|
+
|
300
|
+
// TODO: it would be better to filter out the objects that actually have components with callback methods either themselves or in their parents(?)
|
301
|
+
let didIgnoreSkinnedMeshes: boolean | undefined = undefined;;
|
302
|
+
if (rc instanceof ObjectRaycaster) {
|
303
|
+
didIgnoreSkinnedMeshes = rc.ignoreSkinnedMeshes;
|
304
|
+
if (!isClick) rc.ignoreSkinnedMeshes = true;
|
305
|
+
}
|
285
306
|
const res = rc.performRaycast(opts);
|
307
|
+
|
308
|
+
if (rc instanceof ObjectRaycaster) {
|
309
|
+
if (didIgnoreSkinnedMeshes !== undefined) {
|
310
|
+
rc.ignoreSkinnedMeshes = didIgnoreSkinnedMeshes;
|
311
|
+
}
|
312
|
+
}
|
313
|
+
|
314
|
+
|
286
315
|
if (res && res.length > 0)
|
287
316
|
this._sortedHits.push(...res);
|
288
317
|
}
|
@@ -1,19 +1,20 @@
|
|
1
1
|
import { Behaviour, GameObject } from "./Component.js";
|
2
2
|
import { Camera } from "./Camera.js";
|
3
3
|
import { LookAtConstraint } from "./LookAtConstraint.js";
|
4
|
-
import { getWorldPosition, getWorldRotation, setWorldPosition, setWorldRotation, slerp } from "../engine/engine_three_utils.js";
|
4
|
+
import { getWorldDirection, getWorldPosition, getWorldRotation, setWorldPosition, setWorldRotation, slerp } from "../engine/engine_three_utils.js";
|
5
5
|
import { RaycastOptions } from "../engine/engine_physics.js";
|
6
6
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
7
7
|
import { getParam, isMobileDevice } from "../engine/engine_utils.js";
|
8
8
|
|
9
|
-
import { Camera as ThreeCamera, Box3, Object3D, PerspectiveCamera, Vector2, Vector3, Box3Helper, GridHelper, Mesh, ShadowMaterial, RGBA_ASTC_10x10_Format } from "three";
|
9
|
+
import { Camera as ThreeCamera, Box3, Object3D, PerspectiveCamera, Vector2, Vector3, Box3Helper, GridHelper, Mesh, ShadowMaterial, RGBA_ASTC_10x10_Format, Ray } from "three";
|
10
10
|
import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
11
11
|
import { type AfterHandleInputEvent, EventSystem, EventSystemEvents } from "./ui/EventSystem.js";
|
12
12
|
import type { ICameraController } from "../engine/engine_types.js";
|
13
|
-
import { setCameraController } from "../engine/engine_camera.js";
|
13
|
+
import { setCameraController, useForAutoFit } from "../engine/engine_camera.js";
|
14
14
|
import { SyncedTransform } from "./SyncedTransform.js";
|
15
15
|
import { tryGetUIComponent } from "./ui/Utils.js";
|
16
16
|
import { GroundProjectedSkybox } from "three/examples/jsm/objects/GroundProjectedSkybox.js";
|
17
|
+
import { Mathf } from "../engine/engine_math.js";
|
17
18
|
|
18
19
|
const freeCam = getParam("freecam");
|
19
20
|
const debugCameraFit = getParam("debugcamerafit");
|
@@ -22,6 +23,21 @@
|
|
22
23
|
const disabledKeys = { LEFT: "", UP: "", RIGHT: "", BOTTOM: "" };
|
23
24
|
let defaultKeys: any = undefined;
|
24
25
|
|
26
|
+
export enum OrbitControlsEventsType {
|
27
|
+
/** Invoked with a CameraTargetReachedEvent */
|
28
|
+
CameraTargetReached = "target-reached",
|
29
|
+
}
|
30
|
+
export class CameraTargetReachedEvent extends CustomEvent<{ controls: OrbitControls, type: "camera" | "lookat" }> {
|
31
|
+
constructor(ctrls: OrbitControls, type: "camera" | "lookat") {
|
32
|
+
super(OrbitControlsEventsType.CameraTargetReached, {
|
33
|
+
detail: {
|
34
|
+
controls: ctrls,
|
35
|
+
type: type,
|
36
|
+
}
|
37
|
+
});
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
25
41
|
export class OrbitControls extends Behaviour implements ICameraController {
|
26
42
|
|
27
43
|
get isCameraController(): boolean {
|
@@ -86,20 +102,35 @@
|
|
86
102
|
|
87
103
|
debugLog: boolean = false;
|
88
104
|
|
89
|
-
/**
|
90
|
-
|
105
|
+
/**
|
106
|
+
* @deprecated use `targetLerpDuration` instead
|
107
|
+
* ~~The speed at which the camera target and the camera will be lerping to their destinations (if set via script or user input)~~
|
108
|
+
* */
|
109
|
+
get targetLerpSpeed() { return 5 }
|
110
|
+
set targetLerpSpeed(v) { this.targetLerpDuration = 1 / v; }
|
91
111
|
|
112
|
+
/** The duration in seconds it takes for the camera look ad and position lerp to reach their destination (when set via `setCameraTargetPosition` and `setLookTargetPosition`) */
|
113
|
+
@serializable()
|
114
|
+
targetLerpDuration = 1;
|
115
|
+
|
92
116
|
/** When enabled OrbitControls will automatically raycast find a look at target in start */
|
93
117
|
autoTarget: boolean = true;
|
94
118
|
|
95
|
-
private _lookTargetPosition!: Vector3;
|
96
119
|
private _controls: ThreeOrbitControls | null = null;
|
97
120
|
private _cameraObject: Object3D | null = null;
|
98
121
|
|
99
|
-
private
|
100
|
-
private
|
101
|
-
private
|
122
|
+
private _lookTargetLerpActive: boolean = false;
|
123
|
+
private _lookTargetStartPosition: Vector3 = new Vector3();
|
124
|
+
private _lookTargetEndPosition: Vector3 = new Vector3();
|
125
|
+
private _lookTargetLerp01: number = 0;
|
126
|
+
private _lookTargetLerpDuration: number = 0;
|
102
127
|
|
128
|
+
private _cameraLerpActive: boolean = false;
|
129
|
+
private _cameraStartPosition: Vector3 = new Vector3();
|
130
|
+
private _cameraEndPosition: Vector3 = new Vector3();
|
131
|
+
private _cameraLerp01: number = 0;
|
132
|
+
private _cameraLerpDuration: number = 0;
|
133
|
+
|
103
134
|
private _inputs: number = 0;
|
104
135
|
private _enableTime: number = 0; // use to disable double click when double clicking on UI
|
105
136
|
private _startedListeningToKeyEvents: boolean = false;
|
@@ -114,7 +145,6 @@
|
|
114
145
|
|
115
146
|
awake(): void {
|
116
147
|
this._didStart = false;
|
117
|
-
this._lookTargetPosition = new Vector3();
|
118
148
|
this._startedListeningToKeyEvents = false;
|
119
149
|
}
|
120
150
|
|
@@ -123,22 +153,22 @@
|
|
123
153
|
if (this.autoTarget) {
|
124
154
|
if (this._controls) {
|
125
155
|
const camGo = GameObject.getComponent(this.gameObject, Camera);
|
126
|
-
if (camGo && !this.
|
156
|
+
if (camGo && !this.setLookTargetFromConstraint()) {
|
127
157
|
if (this.debugLog)
|
128
158
|
console.log("NO TARGET");
|
129
159
|
const worldPosition = getWorldPosition(camGo.cam);
|
130
160
|
const distanceToCenter = worldPosition.length();
|
131
161
|
const forward = new Vector3(0, 0, -distanceToCenter).applyMatrix4(camGo.cam.matrixWorld);
|
132
|
-
this.
|
162
|
+
this.setLookTargetPosition(forward, true);
|
133
163
|
}
|
134
|
-
if (this.autoTarget && !this.
|
164
|
+
if (this.autoTarget && !this.setLookTargetFromConstraint()) {
|
135
165
|
const opts = new RaycastOptions();
|
136
166
|
// center of the screen:
|
137
167
|
opts.screenPoint = new Vector2(0, 0);
|
138
168
|
opts.lineThreshold = 0.1;
|
139
169
|
const hits = this.context.physics.raycast(opts);
|
140
170
|
if (hits.length > 0) {
|
141
|
-
this.
|
171
|
+
this.setLookTargetPosition(hits[0].point, true);
|
142
172
|
}
|
143
173
|
if (debugCameraFit)
|
144
174
|
console.log("OrbitControls hits", ...hits);
|
@@ -267,8 +297,8 @@
|
|
267
297
|
if (this.enableRotate) {
|
268
298
|
this.autoRotate = false;
|
269
299
|
}
|
270
|
-
this.
|
271
|
-
this.
|
300
|
+
this._cameraLerpActive = false;
|
301
|
+
this._lookTargetLerpActive = false;
|
272
302
|
}
|
273
303
|
this._inputs = 0;
|
274
304
|
|
@@ -278,35 +308,32 @@
|
|
278
308
|
this.setTargetFromRaycast();
|
279
309
|
}
|
280
310
|
|
281
|
-
if (this.
|
282
|
-
const step = this.context.time.deltaTime * this.targetLerpSpeed;
|
311
|
+
if (this._lookTargetLerpActive || this._cameraLerpActive) {
|
283
312
|
|
284
|
-
// confusing naming ahead:
|
285
|
-
// _targetObject: the target where the camera moves to
|
286
|
-
// targetPosition: the target where the look target moves to
|
287
|
-
|
288
313
|
// lerp the camera
|
289
|
-
if (this.
|
290
|
-
|
291
|
-
if (this.
|
292
|
-
|
293
|
-
|
314
|
+
if (this._cameraLerpActive && this._cameraObject) {
|
315
|
+
this._cameraLerp01 += this.context.time.deltaTime / this._cameraLerpDuration;
|
316
|
+
if (this._cameraLerp01 >= 1) {
|
317
|
+
this._cameraObject.position.copy(this._cameraEndPosition);
|
318
|
+
this._cameraLerpActive = false;
|
319
|
+
this.dispatchEvent(new CameraTargetReachedEvent(this, "camera"));
|
294
320
|
}
|
295
321
|
else {
|
296
|
-
|
322
|
+
const t = Mathf.easeInOutCubic(this._cameraLerp01);
|
323
|
+
this._cameraObject.position.lerpVectors(this._cameraStartPosition, this._cameraEndPosition, t);
|
297
324
|
}
|
298
|
-
const minDist = this.autoRotate ? .02 : .001;
|
299
|
-
const dist = this._cameraObject.position.distanceTo(this._cameraTargetPosition);
|
300
|
-
if (dist < minDist) {
|
301
|
-
this._lerpCameraToTarget = false;
|
302
|
-
}
|
303
325
|
}
|
304
326
|
|
305
327
|
// lerp the look target
|
306
|
-
if (this.
|
307
|
-
this.
|
308
|
-
if (this.
|
309
|
-
this.
|
328
|
+
if (this._lookTargetLerpActive) {
|
329
|
+
this._lookTargetLerp01 += this.context.time.deltaTime / this._lookTargetLerpDuration;
|
330
|
+
if (this._lookTargetLerp01 >= 1) {
|
331
|
+
this._controls.target.copy(this._lookTargetEndPosition);
|
332
|
+
this._lookTargetLerpActive = false;
|
333
|
+
this.dispatchEvent(new CameraTargetReachedEvent(this, "lookat"));
|
334
|
+
} else {
|
335
|
+
const t = Mathf.easeInOutCubic(this._lookTargetLerp01);
|
336
|
+
this._controls.target.lerpVectors(this._lookTargetStartPosition, this._lookTargetEndPosition, t);
|
310
337
|
}
|
311
338
|
}
|
312
339
|
}
|
@@ -324,76 +351,134 @@
|
|
324
351
|
this._controls.dampingFactor = this.dampingFactor;
|
325
352
|
this._controls.enablePan = this.enablePan;
|
326
353
|
this._controls.enableRotate = this.enableRotate;
|
354
|
+
|
327
355
|
if (typeof smoothcam === "number" || smoothcam === true) {
|
328
356
|
this._controls.enableDamping = true;
|
329
357
|
const factor = typeof smoothcam === "number" ? smoothcam : .99;
|
330
358
|
this._controls.dampingFactor = Math.max(.001, 1 - Math.min(1, factor));
|
331
359
|
}
|
360
|
+
|
361
|
+
if (!this.allowInterrupt) {
|
362
|
+
if (this._lookTargetLerpActive) {
|
363
|
+
this._controls.enablePan = false;
|
364
|
+
}
|
365
|
+
if (this._cameraLerpActive) {
|
366
|
+
this._controls.enableRotate = false;
|
367
|
+
this._controls.autoRotate = false;
|
368
|
+
}
|
369
|
+
if (this._lookTargetLerpActive || this._cameraLerpActive) {
|
370
|
+
this._controls.enableZoom = false;
|
371
|
+
}
|
372
|
+
}
|
332
373
|
//@ts-ignore
|
333
374
|
// this._controls.zoomToCursor = this.zoomToCursor;
|
334
375
|
if (!this.context.isInXR) {
|
335
|
-
if (!freeCam && this.lookAtConstraint?.locked) this.
|
376
|
+
if (!freeCam && this.lookAtConstraint?.locked) this.setLookTargetFromConstraint(0, this.lookAtConstraint01);
|
336
377
|
this._controls.update();
|
337
378
|
}
|
338
379
|
}
|
339
380
|
|
340
381
|
}
|
341
382
|
|
342
|
-
|
343
|
-
|
344
|
-
|
383
|
+
|
384
|
+
/**
|
385
|
+
* Sets camera target position and look direction. Does perform a raycast in the forward direction of the passed in object to find an orbit point
|
386
|
+
*/
|
387
|
+
public setCameraAndLookTarget(target: Object3D) {
|
388
|
+
if (!target || !(target instanceof Object3D)) return;
|
389
|
+
const worldPosition = getWorldPosition(target);
|
390
|
+
const forward = getWorldDirection(target);
|
391
|
+
this.setTargetFromRaycast(new Ray(worldPosition, forward));
|
392
|
+
this.setCameraTargetPosition(worldPosition);
|
345
393
|
}
|
394
|
+
|
346
395
|
/** Moves the camera to position smoothly.
|
347
396
|
* @param position The position in local space of the controllerObject to move the camera to. If null the camera will stop lerping to the target.
|
348
397
|
*/
|
349
|
-
public setCameraTargetPosition(position?: Vector3 | null,
|
350
|
-
if (!position)
|
398
|
+
public setCameraTargetPosition(position?: Object3D | Vector3 | null, immediateOrDuration: boolean | number = false) {
|
399
|
+
if (!position) return;
|
400
|
+
if (position instanceof Object3D) {
|
401
|
+
position = getWorldPosition(position) as Vector3;
|
402
|
+
}
|
403
|
+
if (!this._cameraEndPosition) this._cameraEndPosition = new Vector3();
|
404
|
+
this._cameraEndPosition.copy(position);
|
405
|
+
if (immediateOrDuration === true) {
|
406
|
+
this._cameraLerpActive = false;
|
407
|
+
this.controllerObject?.position.copy(this._cameraEndPosition);
|
408
|
+
}
|
409
|
+
else if (this._cameraObject) {
|
410
|
+
this._cameraLerpActive = true;
|
411
|
+
this._cameraLerp01 = 0;
|
412
|
+
this._cameraStartPosition.copy(this._cameraObject?.position);
|
413
|
+
if (typeof immediateOrDuration === "number") {
|
414
|
+
this._cameraLerpDuration = immediateOrDuration;
|
415
|
+
}
|
416
|
+
else this._cameraLerpDuration = this.targetLerpDuration;
|
417
|
+
}
|
418
|
+
}
|
419
|
+
/** True while the camera position is being lerped */
|
420
|
+
get cameraLerpActive() { return this._cameraLerpActive; }
|
421
|
+
/** Call to stop camera position lerping */
|
422
|
+
public stopCameraLerp() {
|
423
|
+
this._cameraLerpActive = false;
|
424
|
+
}
|
425
|
+
|
426
|
+
/** Moves the camera look-at target to a position smoothly. */
|
427
|
+
public setLookTargetPosition(position: Object3D | Vector3 | null = null, immediateOrDuration: boolean = false) {
|
428
|
+
if (!this._controls) return;
|
429
|
+
if (!position) return
|
430
|
+
if (position instanceof Object3D) {
|
431
|
+
position = getWorldPosition(position) as Vector3;
|
432
|
+
}
|
433
|
+
this._lookTargetEndPosition.copy(position);
|
434
|
+
|
435
|
+
if (immediateOrDuration === true) {
|
436
|
+
this._controls.target.copy(this._lookTargetEndPosition);
|
437
|
+
}
|
351
438
|
else {
|
352
|
-
this.
|
353
|
-
this.
|
354
|
-
|
355
|
-
|
439
|
+
this._lookTargetLerpActive = true;
|
440
|
+
this._lookTargetLerp01 = 0;
|
441
|
+
this._lookTargetStartPosition.copy(this._controls.target);
|
442
|
+
if (typeof immediateOrDuration === "number") {
|
443
|
+
this._lookTargetLerpDuration = immediateOrDuration;
|
356
444
|
}
|
445
|
+
else this._lookTargetLerpDuration = this.targetLerpDuration;
|
357
446
|
}
|
358
447
|
}
|
448
|
+
/** True while the camera look target is being lerped */
|
449
|
+
get lookTargetLerpActive() { return this._lookTargetLerpActive; }
|
450
|
+
/** Call to stop camera look target lerping */
|
451
|
+
public stopLookTargetLerp() {
|
452
|
+
this._lookTargetLerpActive = false;
|
453
|
+
}
|
359
454
|
|
360
455
|
/** Sets the look at target from an assigned lookAtConstraint source by index */
|
361
|
-
|
456
|
+
private setLookTargetFromConstraint(index: number = 0, t: number = 1): boolean {
|
362
457
|
if (!this._controls) return false;
|
363
458
|
const sources = this.lookAtConstraint?.sources;
|
364
459
|
if (sources && sources.length > 0) {
|
365
460
|
const target = sources[index];
|
366
461
|
if (target) {
|
367
|
-
target.getWorldPosition(this.
|
368
|
-
this.
|
462
|
+
target.getWorldPosition(this._lookTargetEndPosition);
|
463
|
+
this.lerpLookTarget(this._lookTargetEndPosition, t);
|
369
464
|
return true;
|
370
465
|
}
|
371
466
|
}
|
372
467
|
return false;
|
373
468
|
}
|
374
469
|
|
375
|
-
/**
|
376
|
-
public
|
377
|
-
if (!this._controls) return;
|
378
|
-
if (position !== null) this._lookTargetPosition.copy(position);
|
379
|
-
if (immediate)
|
380
|
-
this._controls.target.copy(this._lookTargetPosition);
|
381
|
-
else this._lerpToTargetPosition = true;
|
382
|
-
}
|
470
|
+
/** @deprecated use `controls.target.lerp(position, delta)` */
|
471
|
+
public lerpTarget(position: Vector3, delta: number) { return this.lerpLookTarget(position, delta); }
|
383
472
|
|
384
|
-
|
473
|
+
private lerpLookTarget(position: Vector3, delta: number) {
|
385
474
|
if (!this._controls) return;
|
386
|
-
this._controls.target.
|
475
|
+
if (delta >= 1) this._controls.target.copy(position);
|
476
|
+
else this._controls.target.lerp(position, delta);
|
387
477
|
}
|
388
478
|
|
389
|
-
|
390
|
-
if (!this._controls) return -1;
|
391
|
-
return this._controls.target.distanceTo(position);
|
392
|
-
}
|
393
|
-
|
394
|
-
private setTargetFromRaycast() {
|
479
|
+
private setTargetFromRaycast(ray?: Ray) {
|
395
480
|
if (!this.controls) return;
|
396
|
-
const rc = this.context.physics.raycast();
|
481
|
+
const rc = ray ? this.context.physics.raycastFromRay(ray) : this.context.physics.raycast();
|
397
482
|
for (const hit of rc) {
|
398
483
|
if (hit.distance > 0 && GameObject.isActiveInHierarchy(hit.object)) {
|
399
484
|
|
@@ -404,19 +489,14 @@
|
|
404
489
|
break;
|
405
490
|
}
|
406
491
|
}
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
// }
|
411
|
-
// console.log("Set target", this.targetPosition, hit.object.name, hit.object);
|
412
|
-
this._lookTargetPosition.copy(hit.point);
|
413
|
-
this._lerpToTargetPosition = true;
|
414
|
-
this._cameraTargetPosition = null;
|
492
|
+
|
493
|
+
this.setLookTargetPosition(hit.point);
|
494
|
+
|
415
495
|
if (this.context.mainCamera) {
|
416
|
-
this._lerpCameraToTarget = true;
|
417
496
|
const pos = getWorldPosition(this.context.mainCamera);
|
418
|
-
|
419
|
-
this._cameraObject?.parent?.worldToLocal(
|
497
|
+
const cameraTarget = pos.clone().sub(this.controls.target).add(this._lookTargetEndPosition);
|
498
|
+
this._cameraObject?.parent?.worldToLocal(cameraTarget);
|
499
|
+
this.setCameraTargetPosition(cameraTarget);
|
420
500
|
}
|
421
501
|
break;
|
422
502
|
}
|
@@ -449,6 +529,7 @@
|
|
449
529
|
let allowExpanding = true;
|
450
530
|
// we dont want to check invisible objects
|
451
531
|
if (!obj.visible) return;
|
532
|
+
if (useForAutoFit(obj) === false) return;
|
452
533
|
// ignore Box3Helpers
|
453
534
|
if (obj instanceof Box3Helper) allowExpanding = false;
|
454
535
|
if (obj instanceof GridHelper) allowExpanding = false;
|
@@ -516,7 +597,7 @@
|
|
516
597
|
controls.maxDistance = distance * 10;
|
517
598
|
controls.minDistance = distance * 0.01;
|
518
599
|
|
519
|
-
this.
|
600
|
+
this.setLookTargetPosition(center, immediate);
|
520
601
|
this.autoTarget = false;
|
521
602
|
|
522
603
|
// TODO: this doesnt take the Camera component nearClipPlane into account
|
@@ -528,7 +609,7 @@
|
|
528
609
|
|
529
610
|
if (camera.parent) {
|
530
611
|
const cameraLocalPosition = camera.parent!.worldToLocal(controls.target.clone().sub(direction));
|
531
|
-
this.
|
612
|
+
this.setCameraTargetPosition(cameraLocalPosition, immediate);
|
532
613
|
}
|
533
614
|
else console.error(`Can not fit camera ${camera.name} because it has no parent`)
|
534
615
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { Input, NEPointerEvent } from "../../engine/engine_input.js";
|
2
|
+
import { Object3D } from "three";
|
2
3
|
|
3
4
|
export interface IInputEventArgs {
|
4
5
|
get used(): boolean;
|
@@ -83,3 +84,20 @@
|
|
83
84
|
|
84
85
|
export interface IPointerEventHandler extends IPointerDownHandler,
|
85
86
|
IPointerUpHandler, IPointerEnterHandler, IPointerMoveHandler, IPointerExitHandler, IPointerClickHandler { }
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
/**
|
91
|
+
* @internal tests if the object has any PointerEventComponent used by the EventSystem
|
92
|
+
* This is used to skip raycasting on objects that have no components that use pointer events
|
93
|
+
*/
|
94
|
+
// export function hasPointerEventComponent(obj: Object3D) {
|
95
|
+
// const res = GameObject.foreachComponent(obj, comp => {
|
96
|
+
// const handler = comp as IPointerEventHandler;
|
97
|
+
// if (handler.onPointerDown || handler.onPointerUp || handler.onPointerEnter || handler.onPointerExit || handler.onPointerClick)
|
98
|
+
// return true;
|
99
|
+
// return undefined;
|
100
|
+
// });
|
101
|
+
// if (res === true) return true;
|
102
|
+
// return false;
|
103
|
+
// }
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { RaycastOptions } from "../../engine/engine_physics.js";
|
2
2
|
import { Behaviour, Component } from "../Component.js";
|
3
3
|
import { EventSystem } from "./EventSystem.js";
|
4
|
+
import { SkinnedMesh } from "three";
|
4
5
|
|
5
6
|
|
6
7
|
export class Raycaster extends Behaviour {
|
@@ -26,6 +27,8 @@
|
|
26
27
|
private targets: THREE.Object3D[] | null = null;
|
27
28
|
private raycastHits: THREE.Intersection[] = [];
|
28
29
|
|
30
|
+
ignoreSkinnedMeshes = false;
|
31
|
+
|
29
32
|
start(): void {
|
30
33
|
this.targets = [this.gameObject];
|
31
34
|
}
|
@@ -35,6 +38,12 @@
|
|
35
38
|
opts ??= new RaycastOptions();
|
36
39
|
opts.targets = this.targets;
|
37
40
|
opts.results = this.raycastHits;
|
41
|
+
opts.testObject = obj => {
|
42
|
+
if (this.ignoreSkinnedMeshes && obj instanceof SkinnedMesh) {
|
43
|
+
return "continue in children";
|
44
|
+
}
|
45
|
+
return true;
|
46
|
+
};
|
38
47
|
const hits = this.context.physics.raycast(opts);
|
39
48
|
// console.log(this.context.alias, hits);
|
40
49
|
return hits;
|
@@ -45,6 +54,11 @@
|
|
45
54
|
// eventCamera: Camera | null = null;
|
46
55
|
// ignoreReversedGraphics: boolean = false;
|
47
56
|
// rootRaycaster: GraphicRaycaster | null = null;
|
57
|
+
|
58
|
+
constructor() {
|
59
|
+
super();
|
60
|
+
this.ignoreSkinnedMeshes = true;
|
61
|
+
}
|
48
62
|
}
|
49
63
|
|
50
64
|
|
@@ -40,6 +40,7 @@
|
|
40
40
|
import { Button } from "../../engine-components/ui/Button.js";
|
41
41
|
import { CallInfo } from "../../engine-components/EventList.js";
|
42
42
|
import { Camera } from "../../engine-components/Camera.js";
|
43
|
+
import { CameraTargetReachedEvent } from "../../engine-components/OrbitControls.js";
|
43
44
|
import { Canvas } from "../../engine-components/ui/Canvas.js";
|
44
45
|
import { CanvasGroup } from "../../engine-components/ui/CanvasGroup.js";
|
45
46
|
import { CapsuleCollider } from "../../engine-components/Collider.js";
|
@@ -259,6 +260,7 @@
|
|
259
260
|
TypeStore.add("Button", Button);
|
260
261
|
TypeStore.add("CallInfo", CallInfo);
|
261
262
|
TypeStore.add("Camera", Camera);
|
263
|
+
TypeStore.add("CameraTargetReachedEvent", CameraTargetReachedEvent);
|
262
264
|
TypeStore.add("Canvas", Canvas);
|
263
265
|
TypeStore.add("CanvasGroup", CanvasGroup);
|
264
266
|
TypeStore.add("CapsuleCollider", CapsuleCollider);
|
@@ -114,6 +114,8 @@
|
|
114
114
|
hit?.createAnchor?.call(hit, pose.transform)?.then(anchor => {
|
115
115
|
if (this.context.isInAR)
|
116
116
|
this._anchor = anchor;
|
117
|
+
}).catch(ex => {
|
118
|
+
console.warn("Failed to create anchor", ex);
|
117
119
|
});
|
118
120
|
}
|
119
121
|
|
@@ -188,6 +188,9 @@
|
|
188
188
|
public get IsInVR() { return this._isInVR; }
|
189
189
|
public get IsInAR() { return this._isInAR; }
|
190
190
|
|
191
|
+
/** When enabled */
|
192
|
+
allowARPlacementReticle: boolean = true;
|
193
|
+
|
191
194
|
private rig!: Object3D;
|
192
195
|
private isInit: boolean = false;
|
193
196
|
|
@@ -229,10 +232,6 @@
|
|
229
232
|
|
230
233
|
this.context.renderer.xr.enabled = true;
|
231
234
|
|
232
|
-
// general WebXR support?
|
233
|
-
const browserSupportsXR = WebXR.XRSupported;
|
234
|
-
|
235
|
-
|
236
235
|
// TODO: move the whole buttons positioning out of here and make it configureable from css
|
237
236
|
// better set proper classes so user code can react to it instead
|
238
237
|
// of this hardcoded stuff
|
@@ -740,7 +739,7 @@
|
|
740
739
|
}
|
741
740
|
|
742
741
|
if (this.reticle) {
|
743
|
-
this.reticle.visible = this.reticleActive;
|
742
|
+
this.reticle.visible = this.reticleActive && this._webxr.allowARPlacementReticle;
|
744
743
|
if (this.reticleActive) {
|
745
744
|
if (pose) {
|
746
745
|
const matrix = pose.transform.matrix;
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import { Object3D, Quaternion, Vector3 } from "three";
|
5
5
|
import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
|
6
6
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
7
|
-
import { showBalloonWarning } from "../../engine/debug/index.js";
|
7
|
+
import { showBalloonMessage, showBalloonWarning } from "../../engine/debug/index.js";
|
8
8
|
|
9
9
|
import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.js";
|
10
10
|
import { USDZExporterContext, USDWriter, imageToCanvas } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
@@ -143,11 +143,15 @@
|
|
143
143
|
private readonly trackedImageIndexMap: Map<number, WebXRImageTrackingModel> = new Map();
|
144
144
|
|
145
145
|
private static _imageElements: Map<string, ImageBitmap | null> = new Map();
|
146
|
+
private webxr: WebXR | null = null;
|
146
147
|
|
147
148
|
awake(): void {
|
148
|
-
if(debug) console.log(this)
|
149
|
+
if (debug) console.log(this)
|
149
150
|
if (!this.trackedImages) return;
|
150
151
|
for (const trackedImage of this.trackedImages) {
|
152
|
+
if (trackedImage.object?.asset) {
|
153
|
+
trackedImage.object.asset.visible = false;
|
154
|
+
}
|
151
155
|
if (trackedImage.image) {
|
152
156
|
if (WebXRImageTracking._imageElements.has(trackedImage.image)) {
|
153
157
|
}
|
@@ -181,10 +185,9 @@
|
|
181
185
|
}
|
182
186
|
}
|
183
187
|
|
184
|
-
private xr: WebXR | null = null;
|
185
188
|
|
186
189
|
onEnable(): void {
|
187
|
-
this.
|
190
|
+
this.webxr = GameObject.findObjectOfType(WebXR);
|
188
191
|
WebXR.addEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
|
189
192
|
WebXR.addEventListener(WebXREvent.XRStarted, this.onXRStarted);
|
190
193
|
WebXR.addEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
|
@@ -221,17 +224,71 @@
|
|
221
224
|
}
|
222
225
|
}
|
223
226
|
|
224
|
-
private
|
227
|
+
private onXRStarted = (_: any) => {
|
228
|
+
// clear out all frame counters for tracking
|
229
|
+
for (const trackedData of this.imageToObjectMap.values()) {
|
230
|
+
trackedData.frames = 0;
|
231
|
+
}
|
232
|
+
};
|
225
233
|
|
226
|
-
private
|
227
|
-
|
234
|
+
private readonly imageToObjectMap = new Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number }>();
|
235
|
+
private readonly currentImages: WebXRTrackedImage[] = [];
|
228
236
|
|
237
|
+
|
238
|
+
private onXRUpdate = (evt): void => {
|
239
|
+
this.currentImages.length = 0;
|
240
|
+
|
241
|
+
const frame = evt.frame;
|
242
|
+
if (!frame) return;
|
243
|
+
|
244
|
+
if (frame.session && !("getImageTrackingResults" in frame)) {
|
245
|
+
const warning = "Image tracking is currently not supported on this device. On Chrome for Android, you can enable the <a href=\"chrome://flags/#webxr-incubations\">chrome://flags/#webxr-incubations</a> flag.";
|
246
|
+
console.log(warning);
|
247
|
+
showBalloonWarning(warning);
|
248
|
+
return;
|
249
|
+
}
|
250
|
+
|
251
|
+
if (frame.session && typeof frame.getImageTrackingResults === "function") {
|
252
|
+
const results = frame.getImageTrackingResults();
|
253
|
+
if (results.length > 0) {
|
254
|
+
const space = this.context.renderer.xr.getReferenceSpace();
|
255
|
+
if (space) {
|
256
|
+
for (const result of results) {
|
257
|
+
const state = result.trackingState;
|
258
|
+
const imageIndex = result.index;
|
259
|
+
const trackedImage = this.trackedImageIndexMap.get(imageIndex);
|
260
|
+
if (trackedImage) {
|
261
|
+
const pose = frame.getPose(result.imageSpace, space);
|
262
|
+
const imageData = new WebXRTrackedImage(this, trackedImage, result.image, result.measuredSize, state, pose);
|
263
|
+
this.currentImages.push(imageData);
|
264
|
+
}
|
265
|
+
else {
|
266
|
+
if (debug) {
|
267
|
+
console.warn("No tracked image for index", imageIndex);
|
268
|
+
}
|
269
|
+
}
|
270
|
+
}
|
271
|
+
if (this.currentImages.length > 0) {
|
272
|
+
try {
|
273
|
+
this.dispatchEvent(new CustomEvent("image-tracking", { detail: this.currentImages }));
|
274
|
+
if (this.webxr && this.webxr.allowARPlacementReticle) {
|
275
|
+
this.webxr.allowARPlacementReticle = false;
|
276
|
+
}
|
277
|
+
}
|
278
|
+
catch (e) {
|
279
|
+
console.error(e);
|
280
|
+
}
|
281
|
+
}
|
282
|
+
}
|
283
|
+
}
|
284
|
+
}
|
285
|
+
|
229
286
|
// disable any objects that are no longer tracked
|
230
287
|
for (const [model, object] of this.imageToObjectMap) {
|
231
288
|
if (!object.object || !model) continue;
|
232
289
|
let found = false;
|
233
|
-
for (const trackedImage of
|
234
|
-
if (trackedImage.model === model) {
|
290
|
+
for (const trackedImage of this.currentImages) {
|
291
|
+
if (trackedImage.state === "tracked" && trackedImage.model === model) {
|
235
292
|
found = true;
|
236
293
|
break;
|
237
294
|
}
|
@@ -240,7 +297,12 @@
|
|
240
297
|
GameObject.setActive(object.object, false);
|
241
298
|
}
|
242
299
|
}
|
300
|
+
}
|
243
301
|
|
302
|
+
|
303
|
+
private onImageTrackingUpdate = (event: any) => {
|
304
|
+
const images = event.detail as WebXRTrackedImage[];
|
305
|
+
|
244
306
|
for (const image of images) {
|
245
307
|
const model = image.model;
|
246
308
|
// don't do anything if we don't have an object to track - can be handled externally through events
|
@@ -252,15 +314,16 @@
|
|
252
314
|
this.imageToObjectMap.set(model, trackedData);
|
253
315
|
|
254
316
|
model.object.loadAssetAsync().then((asset: GameObject | null) => {
|
255
|
-
if (model.createObjectInstance)
|
317
|
+
if (model.createObjectInstance) {
|
256
318
|
asset = GameObject.instantiate(asset);
|
319
|
+
}
|
257
320
|
|
258
321
|
if (asset) {
|
259
322
|
trackedData!.object = asset;
|
260
323
|
|
261
324
|
// make sure to parent to the WebXR.rig
|
262
|
-
if (this.
|
263
|
-
this.
|
325
|
+
if (this.webxr) {
|
326
|
+
this.webxr.Rig.add(asset);
|
264
327
|
}
|
265
328
|
|
266
329
|
image.applyToObject(asset);
|
@@ -279,8 +342,8 @@
|
|
279
342
|
|
280
343
|
if (!trackedData.object) continue;
|
281
344
|
|
282
|
-
if (this.
|
283
|
-
this.
|
345
|
+
if (this.webxr) {
|
346
|
+
this.webxr.Rig.add(trackedData.object);
|
284
347
|
}
|
285
348
|
|
286
349
|
image.applyToObject(trackedData.object);
|
@@ -289,56 +352,4 @@
|
|
289
352
|
}
|
290
353
|
}
|
291
354
|
}
|
292
|
-
|
293
|
-
private onXRStarted = (_: any) => {
|
294
|
-
// clear out all frame counters for tracking
|
295
|
-
for (const trackedData of this.imageToObjectMap.values()) {
|
296
|
-
trackedData.frames = 0;
|
297
|
-
}
|
298
|
-
};
|
299
|
-
|
300
|
-
private onXRUpdate = (evt): void => {
|
301
|
-
const frame = evt.frame;
|
302
|
-
if (!frame) return;
|
303
|
-
|
304
|
-
if (frame.session && !("getImageTrackingResults" in frame)) {
|
305
|
-
showBalloonWarning("Image tracking is currently not supported on this device. On Chrome for Android, you can enable the <a href=\"chrome://flags/#webxr-incubations\">chrome://flags/#webxr-incubations</a> flag.");
|
306
|
-
return;
|
307
|
-
}
|
308
|
-
|
309
|
-
//@ts-ignore
|
310
|
-
if (frame.session && typeof frame.getImageTrackingResults === "function") {
|
311
|
-
//@ts-ignore
|
312
|
-
const results = frame.getImageTrackingResults();
|
313
|
-
if (results.length) {
|
314
|
-
const space = this.context.renderer.xr.getReferenceSpace();
|
315
|
-
if (space) {
|
316
|
-
const images: WebXRTrackedImage[] = [];
|
317
|
-
for (const result of results) {
|
318
|
-
const imageIndex = result.index;
|
319
|
-
const trackedImage = this.trackedImageIndexMap.get(imageIndex);
|
320
|
-
if (trackedImage) {
|
321
|
-
const pose = frame.getPose(result.imageSpace, space);
|
322
|
-
const state = result.trackingState;
|
323
|
-
const imageData = new WebXRTrackedImage(this, trackedImage, result.image, result.measuredSize, state, pose);
|
324
|
-
images.push(imageData);
|
325
|
-
}
|
326
|
-
else {
|
327
|
-
if (debug) {
|
328
|
-
console.warn("No tracked image for index", imageIndex);
|
329
|
-
}
|
330
|
-
}
|
331
|
-
}
|
332
|
-
if (images.length > 0) {
|
333
|
-
try {
|
334
|
-
this.dispatchEvent(new CustomEvent("image-tracking", { detail: images }));
|
335
|
-
}
|
336
|
-
catch (e) {
|
337
|
-
console.error(e);
|
338
|
-
}
|
339
|
-
}
|
340
|
-
}
|
341
|
-
}
|
342
|
-
}
|
343
|
-
}
|
344
355
|
}
|