File without changes
|
@@ -68,6 +68,7 @@
|
|
68
68
|
return "assets";
|
69
69
|
}
|
70
70
|
|
71
|
+
/** @returns the fullpath of the build */
|
71
72
|
export function getOutputDirectory() {
|
72
73
|
const projectConfig = tryLoadProjectConfig();
|
73
74
|
return process.cwd() + "/" + (projectConfig?.buildDirectory || "dist");
|
@@ -36,7 +36,10 @@
|
|
36
36
|
const needleConfig = tryLoadProjectConfig();
|
37
37
|
if (needleConfig) {
|
38
38
|
assetsDirName = needleConfig.assetsDirectory;
|
39
|
-
while(assetsDirName.startsWith('/')) assetsDirName = assetsDirName.substring(1);
|
39
|
+
while (assetsDirName.startsWith('/')) assetsDirName = assetsDirName.substring(1);
|
40
|
+
|
41
|
+
if (needleConfig.buildDirectory)
|
42
|
+
outdirName = needleConfig.buildDirectory;
|
40
43
|
}
|
41
44
|
|
42
45
|
if (copyIncludesFromEngine !== false) {
|
@@ -26,7 +26,7 @@
|
|
26
26
|
// console.log("Update vite defines -------------------------------------------");
|
27
27
|
if (!viteConfig.define) viteConfig.define = {};
|
28
28
|
const version = tryGetNeedleEngineVersion();
|
29
|
-
console.log("Needle Engine Version:"
|
29
|
+
console.log("Needle Engine Version: " + version, needleEngineConfig?.generator ?? "(unknown generator)");
|
30
30
|
if (version)
|
31
31
|
viteConfig.define.NEEDLE_ENGINE_VERSION = "\"" + version + "\"";
|
32
32
|
if (needleEngineConfig)
|
@@ -42,6 +42,9 @@
|
|
42
42
|
if (viteConfig.define.NEEDLE_USE_RAPIER === undefined) {
|
43
43
|
viteConfig.define.NEEDLE_USE_RAPIER = useRapier;
|
44
44
|
}
|
45
|
+
|
46
|
+
// this gives a timestamp containing the timezone
|
47
|
+
viteConfig.define.NEEDLE_PROJECT_BUILD_TIME = "\"" + new Date().toString() + "\"";
|
45
48
|
}
|
46
49
|
}
|
47
50
|
}
|
@@ -39,11 +39,14 @@
|
|
39
39
|
});
|
40
40
|
}
|
41
41
|
|
42
|
-
function triggerReloadOnClients() {
|
43
|
-
log(
|
44
|
-
|
45
|
-
|
46
|
-
|
42
|
+
async function triggerReloadOnClients() {
|
43
|
+
log(`Triggering reload on ${currentClients.size} clients...`)
|
44
|
+
for (const client of currentClients) {
|
45
|
+
client.send(JSON.stringify({ type: "full-reload" }));
|
46
|
+
}
|
47
|
+
return new Promise((resolve) => {
|
48
|
+
setTimeout(resolve, 100);
|
49
|
+
});
|
47
50
|
}
|
48
51
|
|
49
52
|
|
@@ -81,9 +84,6 @@
|
|
81
84
|
modified = true;
|
82
85
|
}
|
83
86
|
if (modified || requireInstall) {
|
84
|
-
if (modified) {
|
85
|
-
log("package.json has changed. Require install?", requireInstall)
|
86
|
-
}
|
87
87
|
|
88
88
|
let requireReload = false;
|
89
89
|
if (!requireInstall) {
|
@@ -95,7 +95,7 @@
|
|
95
95
|
if (newPackageJson.dependencies) {
|
96
96
|
for (const key in newPackageJson.dependencies) {
|
97
97
|
if (packageJson.dependencies[key] !== newPackageJson.dependencies[key] && newPackageJson.dependencies[key] !== undefined) {
|
98
|
-
log("
|
98
|
+
log("Detected new dependency: " + key)
|
99
99
|
requireReload = true;
|
100
100
|
requireInstall = true;
|
101
101
|
}
|
@@ -104,13 +104,16 @@
|
|
104
104
|
if (packageJson.devDependencies) {
|
105
105
|
for (const key in packageJson.devDependencies) {
|
106
106
|
if (packageJson.devDependencies[key] !== newPackageJson.devDependencies[key] && newPackageJson.devDependencies[key] !== undefined) {
|
107
|
-
log("
|
107
|
+
log("Detected new devDependency: " + key)
|
108
108
|
requireReload = true;
|
109
109
|
requireInstall = true;
|
110
110
|
}
|
111
111
|
}
|
112
112
|
}
|
113
113
|
|
114
|
+
if (modified) {
|
115
|
+
log("package.json has changed. Require install: " + (requireInstall ? "yes" : "no"))
|
116
|
+
}
|
114
117
|
|
115
118
|
packageJsonSize = packageJsonStat.size;
|
116
119
|
lastEditTime = packageJsonStat.mtime;
|
@@ -119,7 +122,7 @@
|
|
119
122
|
restart(server, projectDir, cachePath);
|
120
123
|
}
|
121
124
|
}
|
122
|
-
},
|
125
|
+
}, 2000);
|
123
126
|
}
|
124
127
|
|
125
128
|
function testIfInstallIsRequired(projectDir, packageJson) {
|
@@ -142,7 +145,7 @@
|
|
142
145
|
}
|
143
146
|
}
|
144
147
|
}
|
145
|
-
log("Dependency not installed"
|
148
|
+
log("Dependency not installed: " + key)
|
146
149
|
return true;
|
147
150
|
}
|
148
151
|
else {
|
@@ -161,9 +164,11 @@
|
|
161
164
|
const isRange = expectedVersion.includes("^") || expectedVersion.includes(">") || expectedVersion.includes("<");
|
162
165
|
if (!isRange) {
|
163
166
|
const packageJsonPath = path.join(depPath, "package.json");
|
167
|
+
/** @type {String} */
|
164
168
|
const installedVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
|
165
|
-
|
166
|
-
|
169
|
+
// fix check for cases where the version contains a alias e.g. npm:[email protected]
|
170
|
+
if (expectedVersion.trimEnd().endsWith(installedVersion.trim()) == false) {
|
171
|
+
log(`Dependency ${key} is installed but version is not the right one. Expected \"${expectedVersion}/" but got \"${installedVersion}\"`)
|
167
172
|
return true;
|
168
173
|
}
|
169
174
|
}
|
@@ -194,13 +199,13 @@
|
|
194
199
|
}
|
195
200
|
|
196
201
|
if (id !== restartId) return;
|
197
|
-
if (Date.now() - lastRestartTime <
|
202
|
+
if (Date.now() - lastRestartTime < 3000) return;
|
198
203
|
log("Restarting server...")
|
199
204
|
lastRestartTime = Date.now();
|
200
205
|
requireInstall = false;
|
201
206
|
if (existsSync(cachePath))
|
202
207
|
rmSync(cachePath, { recursive: true, force: true });
|
203
|
-
triggerReloadOnClients();
|
208
|
+
await triggerReloadOnClients();
|
204
209
|
|
205
210
|
// touch vite config to trigger reload
|
206
211
|
// const viteConfigPath = path.join(projectDir, "vite.config.js");
|
@@ -212,8 +217,9 @@
|
|
212
217
|
// }
|
213
218
|
|
214
219
|
// check if server is running
|
215
|
-
if (server.httpServer.listening)
|
220
|
+
if (server.httpServer.listening) {
|
216
221
|
server.restart();
|
222
|
+
}
|
217
223
|
isRunningRestart = false;
|
218
224
|
console.log("-----------------------------------------------")
|
219
225
|
}
|
@@ -42,6 +42,7 @@
|
|
42
42
|
|
43
43
|
import { vite_4_4_hack } from "./vite-4.4-hack.js";
|
44
44
|
import { needleImportsLogger } from "./imports-logger.js";
|
45
|
+
import { needleBuildInfo } from "./buildinfo.js";
|
45
46
|
|
46
47
|
|
47
48
|
export * from "./gzip.js";
|
@@ -57,6 +58,8 @@
|
|
57
58
|
*/
|
58
59
|
export const needlePlugins = async (command, config, userSettings) => {
|
59
60
|
|
61
|
+
if(!config) config = {}
|
62
|
+
|
60
63
|
// ensure we have user settings initialized with defaults
|
61
64
|
userSettings = { ...defaultUserSettings, ...userSettings }
|
62
65
|
const array = [
|
@@ -67,6 +70,7 @@
|
|
67
70
|
needlePoster(command, config, userSettings),
|
68
71
|
needleReload(command, config, userSettings),
|
69
72
|
needleBuild(command, config, userSettings),
|
73
|
+
needleBuildInfo(command, config, userSettings),
|
70
74
|
needleCopyFiles(command, config, userSettings),
|
71
75
|
needleTransformCodegen(command, config, userSettings),
|
72
76
|
needleDrop(command, config, userSettings),
|
@@ -109,6 +109,8 @@
|
|
109
109
|
}
|
110
110
|
else console.log("WARN: could not find needle engine package.json")
|
111
111
|
|
112
|
+
tags.push({ tag: 'meta', attrs: { name: 'needle:buildtime', content: new Date().toISOString() } });
|
113
|
+
|
112
114
|
return { html, tags }
|
113
115
|
},
|
114
116
|
}
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import {
|
1
|
+
import { BufferGeometry,Material, Matrix4, Object3D } from "three";
|
2
|
+
|
3
|
+
import { USDDocument,USDObject } from "../../ThreeUSDZExporter.js";
|
2
4
|
import { ActionBuilder, ActionModel } from "./BehavioursBuilder.js";
|
3
|
-
import { USDObject, USDDocument } from "../../ThreeUSDZExporter.js";
|
4
5
|
|
5
6
|
export abstract class DocumentAction {
|
6
7
|
|
@@ -1,7 +1,8 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
|
-
import * as utils from "./../engine/engine_three_utils.js";
|
3
1
|
import { Vector3 } from "three";
|
2
|
+
|
4
3
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
|
+
import * as utils from "./../engine/engine_three_utils.js";
|
5
|
+
import { Behaviour, GameObject } from "./Component.js";
|
5
6
|
|
6
7
|
export class AlignmentConstraint extends Behaviour {
|
7
8
|
|
@@ -1,10 +1,11 @@
|
|
1
|
-
import { Behaviour } from "./Component.js";
|
2
1
|
import { AnimationAction, AnimationClip, AnimationMixer, LoopOnce, LoopRepeat } from "three";
|
3
|
-
|
2
|
+
|
3
|
+
import { Mathf } from "../engine/engine_math.js";
|
4
4
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
-
import { Mathf } from "../engine/engine_math.js";
|
6
5
|
import type { Vec2 } from "../engine/engine_types.js";
|
7
6
|
import { getParam } from "../engine/engine_utils.js";
|
7
|
+
import { MixerEvent } from "./Animator.js";
|
8
|
+
import { Behaviour } from "./Component.js";
|
8
9
|
|
9
10
|
const debug = getParam("debuganimation");
|
10
11
|
|
@@ -1,9 +1,10 @@
|
|
1
|
+
import { AnimationClip, Bone,Interpolant, KeyframeTrack, Matrix4, Object3D, PropertyBinding, Quaternion, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { getParam } from "../../../../engine/engine_utils.js";
|
4
|
+
import { Animator } from "../../../Animator.js";
|
1
5
|
import { GameObject } from "../../../Component.js";
|
2
|
-
import { getParam } from "../../../../engine/engine_utils.js";
|
3
|
-
import { USDObject, buildMatrix, findStructuralNodesInBoneHierarchy, usdNumberFormatting as fn, getPathToSkeleton } from "../ThreeUSDZExporter.js";
|
4
6
|
import type { IUSDExporterExtension } from "../Extension.js";
|
5
|
-
import {
|
6
|
-
import { Animator } from "../../../Animator.js";
|
7
|
+
import { buildMatrix, findStructuralNodesInBoneHierarchy, getPathToSkeleton,usdNumberFormatting as fn, USDObject } from "../ThreeUSDZExporter.js";
|
7
8
|
|
8
9
|
const debug = getParam("debugusdzanimation");
|
9
10
|
const debugSerialization = getParam("debugusdzanimationserialization");
|
@@ -23,6 +23,22 @@
|
|
23
23
|
@serializable(Keyframe)
|
24
24
|
keys!: Array<Keyframe>;
|
25
25
|
|
26
|
+
clone() {
|
27
|
+
const curve = new AnimationCurve();
|
28
|
+
curve.keys = this.keys?.map(k => {
|
29
|
+
const key = new Keyframe();
|
30
|
+
key.time = k.time;
|
31
|
+
key.value = k.value;
|
32
|
+
key.inTangent = k.inTangent;
|
33
|
+
key.inWeight = k.inWeight;
|
34
|
+
key.outTangent = k.outTangent;
|
35
|
+
key.outWeight = k.outWeight;
|
36
|
+
key.weightedMode = k.weightedMode;
|
37
|
+
return key;
|
38
|
+
}) || [];
|
39
|
+
return curve;
|
40
|
+
}
|
41
|
+
|
26
42
|
get duration(): number {
|
27
43
|
if (!this.keys || this.keys.length == 0) return 0;
|
28
44
|
return this.keys[this.keys.length - 1].time;
|
@@ -38,9 +54,9 @@
|
|
38
54
|
for (let i = 0; i < this.keys.length; i++) {
|
39
55
|
const kf = this.keys[i];
|
40
56
|
if (kf.time <= time) {
|
41
|
-
const hasNextKeyframe = i+1 < this.keys.length;
|
57
|
+
const hasNextKeyframe = i + 1 < this.keys.length;
|
42
58
|
if (hasNextKeyframe) {
|
43
|
-
const nextKf = this.keys[i+1];
|
59
|
+
const nextKf = this.keys[i + 1];
|
44
60
|
// if the next
|
45
61
|
if (nextKf.time < time) continue;
|
46
62
|
// tangents are set to Infinity if interpolation is set to constant - in that case we should always return the floored value
|
@@ -1,11 +1,12 @@
|
|
1
|
+
import { Object3D } from "three";
|
1
2
|
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
|
+
|
4
|
+
import { addNewComponent } from "../engine/engine_components.js";
|
2
5
|
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
3
|
-
import {
|
6
|
+
import { Animation } from "./Animation.js";
|
4
7
|
import { Animator } from "./Animator.js";
|
5
|
-
import { Animation } from "./Animation.js";
|
6
8
|
import { GameObject } from "./Component.js";
|
7
9
|
import { PlayableDirector } from "./timeline/PlayableDirector.js";
|
8
|
-
import { Object3D } from "three";
|
9
10
|
|
10
11
|
|
11
12
|
const $objectAnimationKey = Symbol("objectIsAnimatedData");
|
@@ -1,9 +1,10 @@
|
|
1
|
+
import { AnimationClip,Object3D } from "three";
|
2
|
+
|
3
|
+
import { getParam } from "../../../../engine/engine_utils.js";
|
4
|
+
import { Animation } from "../../../Animation.js";
|
1
5
|
import { Animator } from "../../../Animator.js";
|
2
|
-
import {
|
3
|
-
import { Object3D, AnimationClip } from "three";
|
6
|
+
import { Behaviour, GameObject } from "../../../Component.js";
|
4
7
|
import { AnimationExtension } from "../extensions/Animation.js";
|
5
|
-
import { Behaviour, GameObject } from "../../../Component.js";
|
6
|
-
import { getParam } from "../../../../engine/engine_utils.js";
|
7
8
|
import { PlayAnimationOnClick } from "../extensions/behavior/BehaviourComponents.js";
|
8
9
|
|
9
10
|
const debug = getParam("debugusdz");
|
@@ -1,11 +1,12 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import type { AnimationAction, AnimationActionLoopStyles, AnimationMixer } from "three";
|
2
|
+
|
3
|
+
import { Mathf } from "../engine/engine_math.js";
|
4
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
3
5
|
import { getParam } from "../engine/engine_utils.js";
|
4
6
|
import type { AnimatorControllerModel } from "../engine/extensions/NEEDLE_animator_controller_model.js";
|
7
|
+
import { getObjectAnimated } from "./AnimationUtils.js";
|
5
8
|
import { AnimatorController } from "./AnimatorController.js";
|
6
|
-
import {
|
7
|
-
import { Mathf } from "../engine/engine_math.js";
|
8
|
-
import { getObjectAnimated } from "./AnimationUtils.js";
|
9
|
+
import { Behaviour } from "./Component.js";
|
9
10
|
|
10
11
|
const debug = getParam("debuganimator");
|
11
12
|
|
@@ -1,15 +1,16 @@
|
|
1
|
-
import { Animator } from "./Animator.js";
|
2
|
-
import type { AnimatorControllerModel, Condition, State, Transition } from "../engine/extensions/NEEDLE_animator_controller_model.js";
|
3
|
-
import { AnimatorConditionMode, AnimatorControllerParameterType, AnimatorStateInfo, createMotion, StateMachineBehaviour } from "../engine/extensions/NEEDLE_animator_controller_model.js";
|
4
1
|
import { AnimationAction, AnimationClip, AnimationMixer, AxesHelper, Euler, KeyframeTrack, LoopOnce, Object3D, Quaternion, Vector3 } from "three";
|
5
|
-
|
2
|
+
|
3
|
+
import { isDevEnvironment } from "../engine/debug/index.js";
|
4
|
+
import { Mathf } from "../engine/engine_math.js";
|
5
|
+
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
6
|
+
import { assign,SerializationContext, TypeSerializer } from "../engine/engine_serialization_core.js";
|
6
7
|
import { Context } from "../engine/engine_setup.js";
|
8
|
+
import { isAnimationAction } from "../engine/engine_three_utils.js";
|
7
9
|
import { TypeStore } from "../engine/engine_typestore.js";
|
8
|
-
import {
|
9
|
-
import {
|
10
|
-
import {
|
11
|
-
import {
|
12
|
-
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
10
|
+
import { deepClone, getParam } from "../engine/engine_utils.js";
|
11
|
+
import type { AnimatorControllerModel, Condition, State, Transition } from "../engine/extensions/NEEDLE_animator_controller_model.js";
|
12
|
+
import { AnimatorConditionMode, AnimatorControllerParameterType, AnimatorStateInfo, createMotion, StateMachineBehaviour } from "../engine/extensions/NEEDLE_animator_controller_model.js";
|
13
|
+
import { Animator } from "./Animator.js";
|
13
14
|
|
14
15
|
const debug = getParam("debuganimatorcontroller");
|
15
16
|
const debugRootMotion = getParam("debugrootmotion");
|
@@ -212,6 +213,7 @@
|
|
212
213
|
console.warn("AnimatorController has not been resolved, can not create model from string", this.model);
|
213
214
|
return null;
|
214
215
|
}
|
216
|
+
if (debug) console.warn("AnimatorController clone()", this.model);
|
215
217
|
// clone runtime controller but dont clone clip or action
|
216
218
|
const clonedModel = deepClone(this.model, (_owner, _key, _value) => {
|
217
219
|
if (_value === null || _value === undefined) return true;
|
@@ -224,6 +226,8 @@
|
|
224
226
|
}
|
225
227
|
// dont clone AnimationClip
|
226
228
|
if (_value["tracks"] !== undefined) return false;
|
229
|
+
// when assigned __concreteInstance during serialization
|
230
|
+
if (_value instanceof AnimatorController) return false;
|
227
231
|
return true;
|
228
232
|
}) as AnimatorControllerModel;
|
229
233
|
console.assert(clonedModel !== this.model);
|
@@ -581,7 +585,7 @@
|
|
581
585
|
}
|
582
586
|
|
583
587
|
private createActions(_animator: Animator) {
|
584
|
-
|
588
|
+
if (debug) console.log("AnimatorController createActions", this.model);
|
585
589
|
for (const layer of this.model.layers) {
|
586
590
|
const sm = layer.stateMachine;
|
587
591
|
for (let index = 0; index < sm.states.length; index++) {
|
@@ -608,8 +612,13 @@
|
|
608
612
|
if (this.animator && state.motion.clips) {
|
609
613
|
// TODO: we have to compare by name because on instantiate we clone objects but not the node object
|
610
614
|
const mapping = state.motion.clips?.find(e => e.node.name === this.animator?.gameObject?.name);
|
611
|
-
|
612
|
-
|
615
|
+
if (!mapping) {
|
616
|
+
if (debug || isDevEnvironment()) {
|
617
|
+
console.warn("Could not find clip for animator \"" + this.animator?.gameObject?.name + "\"", state.motion.clips.map(c => c.node.name));
|
618
|
+
}
|
619
|
+
}
|
620
|
+
else
|
621
|
+
state.motion.clip = mapping.clip;
|
613
622
|
}
|
614
623
|
|
615
624
|
// ensure we have a clip to blend to
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { EdgeDetectionMode, SMAAEffect, SMAAPreset } from "postprocessing";
|
2
|
+
|
2
3
|
import { serializable } from "../../../engine/engine_serialization.js";
|
3
4
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
4
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,22 +1,20 @@
|
|
1
|
+
export * from "./codegen/components.js";
|
1
2
|
export { Behaviour, Component, GameObject } from "./Component.js"
|
2
|
-
export * from "./codegen/components.js";
|
3
3
|
|
4
4
|
// We dont want to export everything in the extensions
|
5
|
+
export { ClearFlags } from "./Camera.js"
|
6
|
+
export * from "./export/index.js"
|
7
|
+
export * from "./js-extensions/Object3D.js";
|
5
8
|
export * from "./js-extensions/RGBAColor.js";
|
6
|
-
export * from "./js-extensions/Object3D.js";
|
7
|
-
export * from "./XRFlag.js"
|
8
|
-
|
9
|
-
export * from "./export/index.js"
|
10
9
|
export * from "./postprocessing/index.js"
|
10
|
+
export { type ISceneEventListener } from "./SceneSwitcher.js";
|
11
11
|
export * from "./timeline/index.js"
|
12
12
|
export * from "./ui/index.js"
|
13
13
|
export * from "./webxr/index.js"
|
14
|
+
export * from "./webxr/XRFlag.js"
|
14
15
|
|
15
|
-
export { ClearFlags } from "./Camera.js"
|
16
|
-
export { type ISceneEventListener } from "./SceneSwitcher.js";
|
17
|
-
|
18
16
|
import "./CameraUtils.js"
|
19
17
|
import "./AnimationUtils.js"
|
20
18
|
|
21
19
|
export { ParticleSystemBaseBehaviour, type QParticle, type QParticleBehaviour } from "./ParticleSystem.js"
|
22
|
-
|
20
|
+
export { ParticleSystemShapeType } from "./ParticleSystemModules.js"
|
@@ -1,42 +1,42 @@
|
|
1
1
|
|
2
|
-
export * from "./
|
2
|
+
export * from "./debug/index.js";
|
3
3
|
export * from "./engine_addressables.js";
|
4
4
|
export * from "./engine_application.js";
|
5
5
|
export * from "./engine_assetdatabase.js";
|
6
|
-
export * from "./engine_create_objects.js";
|
7
|
-
export * from "./engine_components_internal.js";
|
8
6
|
export * from "./engine_components.js";
|
9
7
|
export * from "./engine_components_internal.js";
|
8
|
+
export * from "./engine_components_internal.js";
|
9
|
+
export * from "./engine_constants.js";
|
10
|
+
export * from "./engine_context.js";
|
10
11
|
export * from "./engine_context_registry.js";
|
11
|
-
export * from "./engine_context.js";
|
12
12
|
export * from "./engine_coroutine.js"
|
13
|
-
export * from "./
|
14
|
-
export * from "./debug/index.js";
|
13
|
+
export * from "./engine_create_objects.js";
|
15
14
|
export * from "./engine_element.js";
|
15
|
+
export * from "./engine_element_attributes.js";
|
16
16
|
export * from "./engine_element_loading.js";
|
17
|
-
export * from "./
|
17
|
+
export * from "./engine_gameobject.js";
|
18
18
|
export { Gizmos } from "./engine_gizmos.js"
|
19
19
|
export * from "./engine_gltf.js";
|
20
20
|
export * from "./engine_hot_reload.js";
|
21
|
-
export * from "./
|
21
|
+
export * from "./engine_input.js";
|
22
|
+
export { InstancingUtil } from "./engine_instancing.js";
|
23
|
+
export { hasIndieLicense,hasProLicense } from "./engine_license.js";
|
24
|
+
export * from "./engine_lifecycle_api.js";
|
25
|
+
export * from "./engine_math.js";
|
22
26
|
export * from "./engine_networking.js";
|
23
|
-
export * from "./engine_networking_types.js";
|
24
27
|
export { syncField } from "./engine_networking_auto.js";
|
25
28
|
export * from "./engine_networking_files.js";
|
26
29
|
export * from "./engine_networking_instantiate.js";
|
30
|
+
export * from "./engine_networking_peer.js";
|
27
31
|
export * from "./engine_networking_streams.js";
|
32
|
+
export * from "./engine_networking_types.js";
|
28
33
|
export * from "./engine_networking_utils.js";
|
29
|
-
export * from "./engine_networking_peer.js";
|
30
34
|
export * from "./engine_patcher.js";
|
31
|
-
export * from "./engine_playerview.js";
|
32
35
|
export * from "./engine_physics.js";
|
33
36
|
export * from "./engine_physics.types.js";
|
34
37
|
export * from "./engine_physics_rapier.js";
|
38
|
+
export * from "./engine_playerview.js";
|
35
39
|
export * from "./engine_scenelighting.js";
|
36
|
-
export * from "./engine_input.js";
|
37
|
-
export * from "./engine_lifecycle_api.js";
|
38
|
-
export * from "./engine_math.js";
|
39
|
-
export * from "./js-extensions/index.js";
|
40
40
|
export * from "./engine_scenetools.js";
|
41
41
|
export * from "./engine_serialization.js";
|
42
42
|
export { type ISerializable } from "./engine_serialization_core.js";
|
@@ -44,12 +44,11 @@
|
|
44
44
|
export * from "./engine_three_utils.js";
|
45
45
|
export * from "./engine_time.js";
|
46
46
|
export * from "./engine_types.js";
|
47
|
+
export { registerType,TypeStore } from "./engine_typestore.js";
|
48
|
+
export { prefix,validate } from "./engine_util_decorator.js";
|
49
|
+
export * from "./engine_utils.js";
|
47
50
|
export * from "./engine_utils_screenshot.js";
|
48
51
|
export * from "./engine_web_api.js";
|
49
|
-
export * from "./
|
50
|
-
|
51
|
-
export
|
52
|
-
|
53
|
-
export { InstancingUtil } from "./engine_instancing.js";
|
54
|
-
export { validate, prefix } from "./engine_util_decorator.js";
|
55
|
-
export { hasProLicense, hasIndieLicense } from "./engine_license.js";
|
52
|
+
export * from "./engine_xr.js";
|
53
|
+
export * from "./extensions/index.js";
|
54
|
+
export * from "./js-extensions/index.js";
|
@@ -1,8 +1,9 @@
|
|
1
|
+
import { Object3D } from "three";
|
2
|
+
|
3
|
+
import { AudioSource } from "../../../../AudioSource.js";
|
1
4
|
import { GameObject } from "../../../../Component.js";
|
2
5
|
import type { IUSDExporterExtension } from "../../Extension.js";
|
3
6
|
import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
4
|
-
import { Object3D } from "three";
|
5
|
-
import { AudioSource } from "../../../../AudioSource.js";
|
6
7
|
|
7
8
|
export class AudioExtension implements IUSDExporterExtension {
|
8
9
|
|
@@ -1,7 +1,8 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
1
|
import { AudioListener as ThreeAudioListener } from "three";
|
2
|
+
|
3
3
|
import { AudioSource } from "./AudioSource.js";
|
4
4
|
import { Camera } from "./Camera.js";
|
5
|
+
import { Behaviour, GameObject } from "./Component.js";
|
5
6
|
|
6
7
|
|
7
8
|
export class AudioListener extends Behaviour {
|
@@ -1,11 +1,14 @@
|
|
1
|
-
import {
|
1
|
+
import { Audio, AudioContext, AudioLoader, PositionalAudio, Vector3 } from "three";
|
2
2
|
import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudioHelper.js';
|
3
|
+
|
4
|
+
import { isDevEnvironment } from "../engine/debug/index.js";
|
5
|
+
import { ApplicationEvents } from "../engine/engine_application.js";
|
6
|
+
import { Mathf } from "../engine/engine_math.js";
|
7
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
|
+
import { getTempVector } from "../engine/engine_three_utils.js";
|
9
|
+
import * as utils from "../engine/engine_utils.js";
|
3
10
|
import { AudioListener } from "./AudioListener.js";
|
4
|
-
import
|
5
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
6
|
-
import { ApplicationEvents } from "../engine/engine_application.js";
|
7
|
-
import { Audio, AudioContext, AudioLoader, PositionalAudio } from "three";
|
8
|
-
import { isDevEnvironment } from "../engine/debug/index.js";
|
11
|
+
import { Behaviour, GameObject } from "./Component.js";
|
9
12
|
|
10
13
|
|
11
14
|
const debug = utils.getParam("debugaudio");
|
@@ -65,6 +68,9 @@
|
|
65
68
|
playOnAwake: boolean = false;
|
66
69
|
|
67
70
|
@serializable()
|
71
|
+
preload: boolean = false;
|
72
|
+
|
73
|
+
@serializable()
|
68
74
|
get loop(): boolean {
|
69
75
|
if (this.sound) this._loop = this.sound.getLoop();
|
70
76
|
return this._loop;
|
@@ -140,23 +146,69 @@
|
|
140
146
|
if (!listener && this.context.mainCamera) listener = GameObject.addNewComponent(this.context.mainCamera, AudioListener);
|
141
147
|
if (listener?.listener) {
|
142
148
|
this.sound = new PositionalAudio(listener.listener);
|
143
|
-
this.gameObject
|
149
|
+
this.gameObject?.add(this.sound);
|
150
|
+
|
151
|
+
// this._listener = listener;
|
152
|
+
// this._originalSoundMatrixWorldFunction = this.sound.updateMatrixWorld;
|
153
|
+
// this.sound.updateMatrixWorld = this._onSoundMatrixWorld;
|
144
154
|
}
|
145
155
|
else if (debug) console.warn("No audio listener found in scene - can not play audio");
|
146
156
|
}
|
147
157
|
return this.sound;
|
148
158
|
}
|
149
159
|
|
160
|
+
// This is a hacky workaround to get the PositionalAudio behave like a 2D audio source
|
161
|
+
// private _listener: AudioListener | null = null;
|
162
|
+
// private _originalSoundMatrixWorldFunction: Function | null = null;
|
163
|
+
// private _onSoundMatrixWorld = (force: boolean) => {
|
164
|
+
// if (this._spatialBlend > .05) {
|
165
|
+
// if (this._originalSoundMatrixWorldFunction) {
|
166
|
+
// this._originalSoundMatrixWorldFunction.call(this.sound, force);
|
167
|
+
// }
|
168
|
+
// }
|
169
|
+
// else {
|
170
|
+
// // we use another object's matrix world function (but bound to the positional audio)
|
171
|
+
// // this is just a little trick to prevent calling the PositionalAudio's updateMatrixWorld function
|
172
|
+
// this.gameObject.updateMatrixWorld?.call(this.sound, force);
|
173
|
+
// if (this.sound && this._listener) {
|
174
|
+
// this.sound.gain.connect(this._listener.listener.getInput());
|
175
|
+
// // const pos = getTempVector().setFromMatrixPosition(this._listener.gameObject.matrixWorld);
|
176
|
+
// // const ctx = this.sound.context;
|
177
|
+
// // const delay = this._listener.listener.timeDelta;
|
178
|
+
// // const time = ctx.currentTime ;
|
179
|
+
// // this.sound.panner.positionX.setValueAtTime(pos.x, time);
|
180
|
+
// // this.sound.panner.positionY.setValueAtTime(pos.y, time);
|
181
|
+
// // this.sound.panner.positionZ.setValueAtTime(pos.z, time);
|
182
|
+
// // this.sound.panner.orientationX.setValueAtTime(0, time);
|
183
|
+
// // this.sound.panner.orientationY.setValueAtTime(0, time);
|
184
|
+
// // this.sound.panner.orientationZ.setValueAtTime(-1, time);
|
185
|
+
// }
|
186
|
+
// }
|
187
|
+
// }
|
188
|
+
|
150
189
|
public get ShouldPlay(): boolean { return this.shouldPlay; }
|
151
190
|
|
191
|
+
/** Get the audio context from the Sound */
|
192
|
+
public get audioContext() {
|
193
|
+
return this.sound?.context;
|
194
|
+
}
|
152
195
|
|
153
196
|
awake() {
|
154
|
-
if(debug) console.log(this);
|
197
|
+
if (debug) console.log(this);
|
155
198
|
this.audioLoader = new AudioLoader();
|
156
199
|
if (this.playOnAwake) this.shouldPlay = true;
|
200
|
+
|
201
|
+
if (this.preload) {
|
202
|
+
if (typeof this.clip === "string") {
|
203
|
+
this.audioLoader.load(this.clip, this.createAudio, () => { }, console.error);
|
204
|
+
}
|
205
|
+
}
|
157
206
|
}
|
158
207
|
|
159
208
|
onEnable(): void {
|
209
|
+
if (this.sound)
|
210
|
+
this.gameObject.add(this.sound);
|
211
|
+
|
160
212
|
if (!AudioSource.userInteractionRegistered) {
|
161
213
|
AudioSource.registerWaitForAllowAudio(() => {
|
162
214
|
if (this.enabled && !this.destroyed && this.shouldPlay)
|
@@ -202,50 +254,56 @@
|
|
202
254
|
this.sound?.setVolume(this.volume);
|
203
255
|
}
|
204
256
|
|
205
|
-
private lerp = (x, y, a) => x * (1 - a) + y * a;
|
206
|
-
|
207
257
|
private createAudio = (buffer?: AudioBuffer) => {
|
208
|
-
if (debug) console.log("
|
209
|
-
AudioSource.registerWaitForAllowAudio(() => {
|
210
|
-
if (debug)
|
211
|
-
console.log("finished loading", buffer);
|
258
|
+
if (debug) console.log("AudioBuffer finished loading", buffer);
|
212
259
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
if (sound.isPlaying)
|
219
|
-
sound.stop();
|
260
|
+
const sound = this.Sound;
|
261
|
+
if (!sound) {
|
262
|
+
if (debug) console.warn("Failed getting sound?", this.name);
|
263
|
+
return;
|
264
|
+
}
|
220
265
|
|
221
|
-
|
222
|
-
|
223
|
-
sound.loop = this._loop;
|
224
|
-
if (this.context.application.muted) sound.setVolume(0);
|
225
|
-
else sound.setVolume(this.volume);
|
226
|
-
sound.autoplay = this.shouldPlay;
|
227
|
-
// sound.setDistanceModel('linear');
|
228
|
-
// sound.setRolloffFactor(1);
|
229
|
-
this.applySpatialDistanceSettings();
|
230
|
-
// sound.setDirectionalCone(180, 360, 0.1);
|
231
|
-
if (sound.isPlaying)
|
232
|
-
sound.stop();
|
266
|
+
if (sound.isPlaying)
|
267
|
+
sound.stop();
|
233
268
|
|
234
|
-
|
269
|
+
if (buffer) sound.setBuffer(buffer);
|
270
|
+
sound.loop = this._loop;
|
271
|
+
if (this.context.application.muted) sound.setVolume(0);
|
272
|
+
else sound.setVolume(this.volume);
|
273
|
+
sound.autoplay = this.shouldPlay && AudioSource.userInteractionRegistered;
|
235
274
|
|
236
|
-
|
237
|
-
|
238
|
-
|
275
|
+
this.applySpatialDistanceSettings();
|
276
|
+
|
277
|
+
if (sound.isPlaying)
|
278
|
+
sound.stop();
|
279
|
+
|
280
|
+
// const src = sound.context.createBufferSource();
|
281
|
+
// src.buffer = sound.buffer;
|
282
|
+
// src.connect(sound.panner);
|
283
|
+
// src.start(this.audioContext?.currentTime);
|
284
|
+
// const gain = sound.context.createGain();
|
285
|
+
// gain.gain.value = 1 - this.spatialBlend;
|
286
|
+
// src.connect(gain);
|
287
|
+
|
288
|
+
// make sure we only play the sound if the user has interacted with the page
|
289
|
+
AudioSource.registerWaitForAllowAudio(this.__onAllowAudioCallback);
|
239
290
|
}
|
291
|
+
private __onAllowAudioCallback = () => {
|
292
|
+
if (this.shouldPlay)
|
293
|
+
this.play();
|
294
|
+
}
|
240
295
|
|
241
296
|
private applySpatialDistanceSettings() {
|
242
297
|
const sound = this.sound;
|
243
298
|
if (!sound) return;
|
244
299
|
this._needUpdateSpatialDistanceSettings = false;
|
245
|
-
const dist =
|
300
|
+
const dist = Mathf.lerp(10 * this._maxDistance / Math.max(0.0001, this.spatialBlend), this._minDistance, this.spatialBlend);
|
246
301
|
if (debug) console.log(this.name, this._minDistance, this._maxDistance, this.spatialBlend, "Ref distance=" + dist);
|
247
302
|
sound.setRefDistance(dist);
|
248
303
|
sound.setMaxDistance(Math.max(0.01, this._maxDistance));
|
304
|
+
// sound.setRolloffFactor(this.spatialBlend);
|
305
|
+
// sound.panner.positionZ.automationRate
|
306
|
+
|
249
307
|
// https://developer.mozilla.org/en-US/docs/Web/API/PannerNode/distanceModel
|
250
308
|
switch (this.rollOffMode) {
|
251
309
|
case AudioRolloffMode.Logarithmic:
|
@@ -269,7 +327,7 @@
|
|
269
327
|
}
|
270
328
|
}
|
271
329
|
|
272
|
-
private onNewClip(clip?: string | MediaStream) {
|
330
|
+
private async onNewClip(clip?: string | MediaStream) {
|
273
331
|
if (clip) this.clip = clip;
|
274
332
|
if (typeof clip === "string") {
|
275
333
|
if (debug)
|
@@ -285,7 +343,10 @@
|
|
285
343
|
this._lastClipStartedLoading = clip;
|
286
344
|
if (debug)
|
287
345
|
console.log("load audio", clip);
|
288
|
-
this.audioLoader.
|
346
|
+
const buffer = await this.audioLoader.loadAsync(clip).catch(console.error);
|
347
|
+
this._lastClipStartedLoading = null;
|
348
|
+
if (buffer)
|
349
|
+
this.createAudio(buffer);
|
289
350
|
}
|
290
351
|
else console.warn("Unsupported audio clip type", clip)
|
291
352
|
}
|
@@ -328,6 +389,7 @@
|
|
328
389
|
if (this.sound && !this.sound.isPlaying) {
|
329
390
|
const muted = this.context.application.muted;
|
330
391
|
if (muted) this.sound.setVolume(0);
|
392
|
+
this.gameObject?.add(this.sound);
|
331
393
|
|
332
394
|
if (this.clip instanceof MediaStream) {
|
333
395
|
|
@@ -411,7 +473,7 @@
|
|
411
473
|
this._hasEnded = true;
|
412
474
|
if (debug)
|
413
475
|
console.log("Audio clip ended", this.clip);
|
414
|
-
this.
|
476
|
+
this.dispatchEvent(new CustomEvent("ended", { detail: this }));
|
415
477
|
}
|
416
478
|
|
417
479
|
// this.gameObject.position.x = Math.sin(time.time) * 2;
|
@@ -1,11 +1,12 @@
|
|
1
1
|
import * as THREE from "three";
|
2
|
+
|
3
|
+
import { OwnershipModel } from "../../engine/engine_networking.js";
|
4
|
+
import type { IModel } from "../../engine/engine_networking_types.js";
|
5
|
+
import { Context } from "../../engine/engine_setup.js";
|
6
|
+
import * as utils from "../../engine/engine_three_utils.js";
|
2
7
|
import { TypeStore } from "../../engine/engine_typestore.js";
|
3
8
|
import { Behaviour, GameObject } from "../Component.js";
|
4
9
|
import { AvatarMarker } from "../webxr/WebXRAvatar.js";
|
5
|
-
import * as utils from "../../engine/engine_three_utils.js";
|
6
|
-
import { OwnershipModel } from "../../engine/engine_networking.js";
|
7
|
-
import { Context } from "../../engine/engine_setup.js";
|
8
|
-
import type { IModel } from "../../engine/engine_networking_types.js";
|
9
10
|
|
10
11
|
export class Avatar_POI {
|
11
12
|
|
@@ -1,9 +1,10 @@
|
|
1
|
+
import { Object3D } from "three";
|
2
|
+
|
3
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
4
|
+
import * as utils from "../../engine/engine_utils.js";
|
1
5
|
import { Behaviour, GameObject } from "../Component.js";
|
2
6
|
import { Voip } from "../Voip.js";
|
3
7
|
import { AvatarMarker } from "../webxr/WebXRAvatar.js";
|
4
|
-
import * as utils from "../../engine/engine_utils.js";
|
5
|
-
import { Object3D } from "three";
|
6
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
7
8
|
|
8
9
|
const debug = utils.getParam("debugmouth");
|
9
10
|
|
@@ -1,7 +1,8 @@
|
|
1
1
|
import { Object3D } from "three";
|
2
|
+
|
3
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
2
4
|
import { Behaviour, GameObject } from "../Component.js";
|
3
|
-
import { XRFlag, XRState } from "../XRFlag.js";
|
4
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
5
|
+
import { XRFlag, XRState } from "../webxr/XRFlag.js";
|
5
6
|
|
6
7
|
|
7
8
|
export class AvatarBlink_Simple extends Behaviour {
|
@@ -1,10 +1,11 @@
|
|
1
|
-
import { Behaviour, GameObject } from "../Component.js";
|
2
|
-
import * as utils from "../../engine/engine_three_utils.js"
|
3
1
|
import * as THREE from "three";
|
4
|
-
import { Avatar_Brain_LookAt } from "./Avatar_Brain_LookAt.js";
|
5
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
6
2
|
import { Object3D } from "three";
|
7
3
|
|
4
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
5
|
+
import * as utils from "../../engine/engine_three_utils.js"
|
6
|
+
import { Behaviour, GameObject } from "../Component.js";
|
7
|
+
import { Avatar_Brain_LookAt } from "./Avatar_Brain_LookAt.js";
|
8
|
+
|
8
9
|
export class AvatarEyeLook_Rotation extends Behaviour {
|
9
10
|
|
10
11
|
@serializable(Object3D)
|
@@ -1,12 +1,13 @@
|
|
1
|
+
import { Box3, Object3D, Vector3 } from "three";
|
1
2
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
|
-
|
3
|
+
|
4
|
+
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
5
|
+
import { getLoader } from "../engine/engine_gltf.js";
|
3
6
|
import * as loaders from "../engine/engine_loaders.js"
|
4
7
|
import { Context } from "../engine/engine_setup.js";
|
8
|
+
import * as utils from "../engine/engine_utils.js"
|
9
|
+
import { download_file } from "../engine/engine_web_api.js";
|
5
10
|
import { GameObject } from "./Component.js";
|
6
|
-
import { download_file } from "../engine/engine_web_api.js";
|
7
|
-
import { getLoader } from "../engine/engine_gltf.js";
|
8
|
-
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
9
|
-
import { Box3, Object3D, Vector3 } from "three";
|
10
11
|
|
11
12
|
const debug = utils.getParam("debugavatar");
|
12
13
|
|
@@ -1,7 +1,8 @@
|
|
1
|
-
import {
|
1
|
+
import { AxesHelper as _AxesHelper } from "three";
|
2
|
+
|
2
3
|
import * as params from "../engine/engine_default_parameters.js";
|
3
4
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
|
-
import {
|
5
|
+
import { Behaviour } from "./Component.js";
|
5
6
|
|
6
7
|
export class AxesHelper extends Behaviour {
|
7
8
|
@serializable()
|
@@ -1,11 +1,12 @@
|
|
1
1
|
// import { Canvas } from './Canvas.js';
|
2
|
+
import { AxesHelper, Object3D } from 'three';
|
2
3
|
import * as ThreeMeshUI from 'three-mesh-ui';
|
4
|
+
|
5
|
+
import { showGizmos } from '../../engine/engine_default_parameters.js';
|
6
|
+
import { getParam } from '../../engine/engine_utils.js';
|
3
7
|
import { Behaviour, GameObject } from "../Component.js";
|
4
8
|
import { EventSystem } from "./EventSystem.js";
|
5
|
-
import { showGizmos } from '../../engine/engine_default_parameters.js';
|
6
|
-
import { AxesHelper, Object3D } from 'three';
|
7
9
|
import type { ICanvas } from './Interfaces.js';
|
8
|
-
import { getParam } from '../../engine/engine_utils.js';
|
9
10
|
export const includesDir = "./include";
|
10
11
|
|
11
12
|
const debug = getParam("debugshadowcomponents");
|
@@ -24,22 +25,38 @@
|
|
24
25
|
|
25
26
|
export const $shadowDomOwner = Symbol("shadowDomOwner");
|
26
27
|
|
28
|
+
/** Derive from this class if you want to implement your own UI components
|
29
|
+
* It provides utility methods and simplifies managing the underlying three-mesh-ui hierarchy
|
30
|
+
*/
|
27
31
|
export class BaseUIComponent extends Behaviour {
|
28
32
|
|
33
|
+
/** Is this object on the root of the UI hierarchy ? */
|
29
34
|
isRoot() { return this.Root?.gameObject === this.gameObject; }
|
30
35
|
|
36
|
+
/** Access the parent canvas component */
|
31
37
|
get canvas() {
|
32
38
|
const cv = this.Root as any as ICanvas;
|
33
39
|
if (cv?.isCanvas) return cv;
|
34
40
|
return null;
|
35
41
|
}
|
42
|
+
/** @deprecated use `canvas` */
|
43
|
+
protected get Canvas() {
|
44
|
+
return this.canvas;
|
45
|
+
}
|
36
46
|
|
47
|
+
/** Mark the UI dirty which will trigger an THREE-Mesh-UI update */
|
37
48
|
markDirty() {
|
38
49
|
EventSystem.markUIDirty(this.context);
|
39
50
|
}
|
40
51
|
|
41
|
-
|
52
|
+
/** the underlying three-mesh-ui */
|
53
|
+
get shadowComponent() { return this._shadowComponent }
|
54
|
+
private set shadowComponent(val: Object3D | null) {
|
55
|
+
this._shadowComponent = val;
|
56
|
+
}
|
42
57
|
|
58
|
+
private _shadowComponent: Object3D | null = null;
|
59
|
+
|
43
60
|
private _controlsChildLayout = true;
|
44
61
|
get controlsChildLayout(): boolean { return this._controlsChildLayout; }
|
45
62
|
set controlsChildLayout(val: boolean) {
|
@@ -58,11 +75,6 @@
|
|
58
75
|
return this._root;
|
59
76
|
}
|
60
77
|
|
61
|
-
// TODO: rename to canvas
|
62
|
-
protected get Canvas() {
|
63
|
-
return this.canvas;
|
64
|
-
}
|
65
|
-
|
66
78
|
// private _intermediate?: Object3D;
|
67
79
|
protected _parentComponent?: BaseUIComponent | null = undefined;
|
68
80
|
|
@@ -77,7 +89,10 @@
|
|
77
89
|
super.onEnable();
|
78
90
|
}
|
79
91
|
|
80
|
-
|
92
|
+
/** Add a three-mesh-ui object to the UI hierarchy
|
93
|
+
* @param container the three-mesh-ui object to add
|
94
|
+
* @param parent the parent component to add the object to
|
95
|
+
*/
|
81
96
|
protected addShadowComponent(container: any, parent?: BaseUIComponent) {
|
82
97
|
|
83
98
|
this.removeShadowComponent();
|
@@ -134,21 +149,7 @@
|
|
134
149
|
if(debug) console.log(this.shadowComponent)
|
135
150
|
}
|
136
151
|
|
137
|
-
|
138
|
-
set(_state: object) {
|
139
|
-
// if (!this.shadowComponent) return;
|
140
|
-
// this.traverseOwnedShadowComponents(this.shadowComponent, this, o => {
|
141
|
-
// for (const ch of o.children) {
|
142
|
-
// console.log(this, ch);
|
143
|
-
// if (ch.isUI && typeof ch.set === "function") {
|
144
|
-
// // ch.set(state);
|
145
|
-
// // ch.update(true, true, true);
|
146
|
-
// }
|
147
|
-
// }
|
148
|
-
// })
|
149
|
-
}
|
150
|
-
|
151
|
-
protected setShadowComponentOwner(current: Object3D | null | undefined) {
|
152
|
+
protected setShadowComponentOwner(current: ThreeMeshUI.MeshUIBaseElement | Object3D | null | undefined) {
|
152
153
|
if (!current) return;
|
153
154
|
// TODO: only traverse our own hierarchy, we can stop if we find another owner
|
154
155
|
if (current[$shadowDomOwner] === undefined || current[$shadowDomOwner] === this) {
|
@@ -171,6 +172,7 @@
|
|
171
172
|
}
|
172
173
|
}
|
173
174
|
|
175
|
+
/** Remove the underlying UI object from the hierarchy */
|
174
176
|
protected removeShadowComponent() {
|
175
177
|
if (this.shadowComponent) {
|
176
178
|
this.shadowComponent.removeFromParent();
|
@@ -1,7 +1,8 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
|
-
import * as utils from "./../engine/engine_three_utils.js";
|
3
1
|
import { Vector3 } from "three";
|
4
2
|
|
3
|
+
import * as utils from "./../engine/engine_three_utils.js";
|
4
|
+
import { Behaviour, GameObject } from "./Component.js";
|
5
|
+
|
5
6
|
export class BasicIKConstraint extends Behaviour {
|
6
7
|
|
7
8
|
private from!: GameObject;
|
@@ -1,8 +1,8 @@
|
|
1
|
+
import { getParam } from "../../../../../engine/engine_utils.js";
|
1
2
|
import { GameObject } from "../../../../Component.js";
|
2
3
|
import type { IUSDExporterExtension } from "../../Extension.js";
|
3
4
|
import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
4
5
|
import { BehaviorModel } from "./BehavioursBuilder.js";
|
5
|
-
import { getParam } from "../../../../../engine/engine_utils.js";
|
6
6
|
|
7
7
|
const debug = getParam("debugusdz");
|
8
8
|
|
@@ -1,21 +1,20 @@
|
|
1
|
+
import { Group,Material, Mesh, Object3D, Quaternion, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { isDevEnvironment, showBalloonWarning } from "../../../../../engine/debug/index.js";
|
4
|
+
import { serializable } from "../../../../../engine/engine_serialization_decorator.js";
|
5
|
+
import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../../../../../engine/engine_three_utils.js";
|
6
|
+
import type { State } from "../../../../../engine/extensions/NEEDLE_animator_controller_model.js";
|
7
|
+
import { NEEDLE_progressive } from "../../../../../engine/extensions/NEEDLE_progressive.js";
|
8
|
+
import { Animator } from "../../../../Animator.js";
|
9
|
+
import { AudioSource } from "../../../../AudioSource.js";
|
1
10
|
import { Behaviour, GameObject } from "../../../../Component.js";
|
2
|
-
import { Animator } from "../../../../Animator.js";
|
3
11
|
import { Renderer } from "../../../../Renderer.js";
|
4
|
-
import { serializable } from "../../../../../engine/engine_serialization_decorator.js";
|
5
12
|
import type { IPointerClickHandler, PointerEventData } from "../../../../ui/PointerEvents.js";
|
13
|
+
import { ObjectRaycaster,Raycaster } from "../../../../ui/Raycaster.js";
|
14
|
+
import { USDDocument, USDObject, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
6
15
|
import { AnimationExtension, RegisteredAnimationInfo, type UsdzAnimation } from "../Animation.js";
|
7
|
-
import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../../../../../engine/engine_three_utils.js";
|
8
|
-
|
9
|
-
import { Object3D, Material, Vector3, Quaternion, Mesh, Group } from "three";
|
10
|
-
import { USDDocument, USDObject, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
11
|
-
|
12
16
|
import type { BehaviorExtension, UsdzBehaviour } from "./Behaviour.js";
|
13
|
-
import { ActionBuilder, ActionModel, AuralMode, BehaviorModel, type IBehaviorElement, MotionType, PlayAction, Space, TriggerBuilder
|
14
|
-
import { AudioSource } from "../../../../AudioSource.js";
|
15
|
-
import { NEEDLE_progressive } from "../../../../../engine/extensions/NEEDLE_progressive.js";
|
16
|
-
import { isDevEnvironment, showBalloonWarning } from "../../../../../engine/debug/index.js";
|
17
|
-
import { Raycaster, ObjectRaycaster } from "../../../../ui/Raycaster.js";
|
18
|
-
import type { State } from "../../../../../engine/extensions/NEEDLE_animator_controller_model.js";
|
17
|
+
import { ActionBuilder, ActionModel, AuralMode, BehaviorModel, GroupActionModel, type IBehaviorElement, MotionType, MultiplePerformOperation,PlayAction, Space, TriggerBuilder } from "./BehavioursBuilder.js";
|
19
18
|
|
20
19
|
function ensureRaycaster(obj: GameObject) {
|
21
20
|
if (!obj) return;
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import { Object3D } from "three";
|
2
|
-
import { USDDocument, USDObject, USDWriter, makeNameSafeForUSD } from "../../ThreeUSDZExporter.js";
|
3
2
|
|
3
|
+
import { getParam } from "../../../../../engine/engine_utils.js";
|
4
|
+
import { makeNameSafeForUSD,USDDocument, USDObject, USDWriter } from "../../ThreeUSDZExporter.js";
|
4
5
|
import { BehaviorExtension } from "./Behaviour.js";
|
5
|
-
import { getParam } from "../../../../../engine/engine_utils.js";
|
6
6
|
|
7
7
|
const debug = getParam("debugusdz");
|
8
8
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { BlendFunction, BloomEffect, SelectiveBloomEffect } from "postprocessing";
|
2
|
+
|
2
3
|
import { serializable } from "../../../engine/engine_serialization.js";
|
3
4
|
import { PostProcessingEffect } from "../PostProcessingEffect.js";
|
4
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { Box3, Color, type ColorRepresentation, LineSegments, Object3D, Vector3 } from "three";
|
2
|
+
|
3
3
|
import { CreateWireCube, Gizmos } from "../engine/engine_gizmos.js";
|
4
4
|
import { getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js";
|
5
|
-
import {
|
5
|
+
import { getParam } from "../engine/engine_utils.js";
|
6
|
+
import { Behaviour } from "./Component.js";
|
6
7
|
|
7
8
|
const gizmos = getParam("gizmos");
|
8
9
|
const debug = getParam("debugboxhelper");
|
@@ -1,15 +1,15 @@
|
|
1
|
+
import { showBalloonMessage } from "../../engine/debug/index.js";
|
2
|
+
import { Gizmos } from "../../engine/engine_gizmos.js";
|
3
|
+
import { PointerType } from "../../engine/engine_input.js";
|
4
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
5
|
+
import { getParam } from "../../engine/engine_utils.js";
|
6
|
+
import { Animator } from "../Animator.js";
|
1
7
|
import { Behaviour, GameObject } from "../Component.js";
|
2
8
|
import { EventList } from "../EventList.js";
|
9
|
+
import { RGBAColor } from "../js-extensions/RGBAColor.js";
|
10
|
+
import { Image } from "./Image.js";
|
3
11
|
import type { IPointerClickHandler, IPointerEnterHandler, IPointerEventHandler, IPointerExitHandler, PointerEventData } from "./PointerEvents.js";
|
4
|
-
import { Image } from "./Image.js";
|
5
|
-
import { RGBAColor } from "../js-extensions/RGBAColor.js";
|
6
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
7
|
-
import { Animator } from "../Animator.js";
|
8
|
-
import { getParam } from "../../engine/engine_utils.js";
|
9
|
-
import { showBalloonMessage } from "../../engine/debug/index.js";
|
10
12
|
import { GraphicRaycaster, ObjectRaycaster, Raycaster } from "./Raycaster.js";
|
11
|
-
import { PointerType } from "../../engine/engine_input.js";
|
12
|
-
import { Gizmos } from "../../engine/engine_gizmos.js";
|
13
13
|
|
14
14
|
const debug = getParam("debugbutton");
|
15
15
|
|
@@ -65,12 +65,12 @@
|
|
65
65
|
@serializable(EventList)
|
66
66
|
onClick?: EventList;
|
67
67
|
|
68
|
-
private _isHovered:
|
68
|
+
private _isHovered: number = 0;
|
69
69
|
|
70
70
|
onPointerEnter(_) {
|
71
|
+
this._isHovered += 1;
|
71
72
|
if (debug)
|
72
|
-
console.
|
73
|
-
this._isHovered = true;
|
73
|
+
console.warn("Button Enter", this._isHovered, this.animationTriggers?.highlightedTrigger, this.animator);
|
74
74
|
if (!this.interactable) return;
|
75
75
|
if (this.transition == Transition.Animation && this.animationTriggers && this.animator) {
|
76
76
|
this.animator.setTrigger(this.animationTriggers.highlightedTrigger);
|
@@ -82,10 +82,12 @@
|
|
82
82
|
}
|
83
83
|
|
84
84
|
onPointerExit() {
|
85
|
+
this._isHovered -= 1;
|
85
86
|
if (debug)
|
86
|
-
console.log("Button Exit", this.animationTriggers?.highlightedTrigger, this.animator);
|
87
|
-
this._isHovered = false;
|
87
|
+
console.log("Button Exit", this._isHovered, this.animationTriggers?.highlightedTrigger, this.animator);
|
88
88
|
if (!this.interactable) return;
|
89
|
+
if (this._isHovered > 0) return;
|
90
|
+
this._isHovered = 0;
|
89
91
|
if (this.transition == Transition.Animation && this.animationTriggers && this.animator) {
|
90
92
|
this.animator.setTrigger(this.animationTriggers.normalTrigger);
|
91
93
|
}
|
@@ -120,10 +122,10 @@
|
|
120
122
|
}
|
121
123
|
|
122
124
|
onPointerClick(args: PointerEventData) {
|
123
|
-
if (!this.interactable
|
125
|
+
if (!this.interactable) return;
|
124
126
|
|
127
|
+
if (args.button !== 0 && args.event.pointerType === PointerType.Mouse) return;
|
125
128
|
// Button clicks should only run with left mouse button while using mouse
|
126
|
-
if(args.pointerId !== 0 && this.context.input.getIsMouse(args.pointerId)) return;
|
127
129
|
if (debug) {
|
128
130
|
console.warn("Button Click", this.onClick);
|
129
131
|
showBalloonMessage("CLICKED button " + this.name + " at " + this.context.time.frameCount);
|
@@ -1,17 +1,17 @@
|
|
1
|
+
import { EquirectangularReflectionMapping, OrthographicCamera, PerspectiveCamera, Ray, SRGBColorSpace, Vector3 } from "three";
|
2
|
+
import { Texture } from "three";
|
3
|
+
|
4
|
+
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
|
5
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
6
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
7
|
+
import { Context } from "../engine/engine_setup.js";
|
8
|
+
import { RenderTexture } from "../engine/engine_texture.js";
|
9
|
+
import { getWorldPosition } from "../engine/engine_three_utils.js";
|
10
|
+
import type { ICamera } from "../engine/engine_types.js"
|
11
|
+
import { getParam } from "../engine/engine_utils.js";
|
1
12
|
import { Behaviour, GameObject } from "./Component.js";
|
2
|
-
import { getParam } from "../engine/engine_utils.js";
|
3
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
13
|
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
5
|
-
import { Context, XRSessionMode } from "../engine/engine_setup.js";
|
6
|
-
import type { ICamera } from "../engine/engine_types.js"
|
7
|
-
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
|
8
|
-
import { getWorldPosition } from "../engine/engine_three_utils.js";
|
9
|
-
import { Gizmos } from "../engine/engine_gizmos.js";
|
10
|
-
|
11
|
-
import { EquirectangularReflectionMapping, OrthographicCamera, PerspectiveCamera, Ray, SRGBColorSpace, Vector3 } from "three";
|
12
14
|
import { OrbitControls } from "./OrbitControls.js";
|
13
|
-
import { RenderTexture } from "../engine/engine_texture.js";
|
14
|
-
import { Texture } from "three";
|
15
15
|
|
16
16
|
export enum ClearFlags {
|
17
17
|
Skybox = 1,
|
@@ -350,7 +350,6 @@
|
|
350
350
|
if (this._backgroundBlurriness !== undefined)
|
351
351
|
this.context.scene.backgroundBlurriness = this._backgroundBlurriness;
|
352
352
|
if (this._backgroundIntensity !== undefined)
|
353
|
-
//@ts-ignore
|
354
353
|
this.context.scene.backgroundIntensity = this._backgroundIntensity;
|
355
354
|
|
356
355
|
break;
|
@@ -392,7 +391,7 @@
|
|
392
391
|
if (debug)
|
393
392
|
showBalloonMessage("Environment blend mode: " + environmentBlendMode + " on " + navigator.userAgent);
|
394
393
|
let transparent = environmentBlendMode === 'additive' || environmentBlendMode === 'alpha-blend';
|
395
|
-
if (context.
|
394
|
+
if (context.isInAR) {
|
396
395
|
if (environmentBlendMode === "opaque") {
|
397
396
|
// workaround for Quest 2 returning opaque when it should be alpha-blend
|
398
397
|
// check user agent if this is the Quest browser and return true if so
|
@@ -1,14 +1,15 @@
|
|
1
|
-
import {
|
1
|
+
import { Object3D } from "three";
|
2
|
+
|
3
|
+
import { getCameraController } from "../engine/engine_camera.js";
|
2
4
|
import { addNewComponent, getOrAddComponent } from "../engine/engine_components.js";
|
3
|
-
import {
|
4
|
-
import type { ICamera, IContext } from "../engine/engine_types.js";
|
5
|
-
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
5
|
+
import { Context } from "../engine/engine_context.js";
|
6
6
|
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
7
|
-
import { getCameraController } from "../engine/engine_camera.js";
|
8
|
-
import { Camera, ClearFlags } from "./Camera.js";
|
9
7
|
import { NeedleEngineHTMLElement } from "../engine/engine_element.js";
|
8
|
+
import type { ICamera, IContext } from "../engine/engine_types.js";
|
10
9
|
import { getParam } from "../engine/engine_utils.js";
|
11
|
-
import {
|
10
|
+
import { Camera, ClearFlags } from "./Camera.js";
|
11
|
+
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
12
|
+
import { OrbitControls } from "./OrbitControls.js";
|
12
13
|
|
13
14
|
const debug = getParam("debugmissingcamera");
|
14
15
|
|
@@ -1,17 +1,19 @@
|
|
1
|
-
import {
|
1
|
+
import { Matrix4, Object3D } from "three";
|
2
|
+
import * as ThreeMeshUI from 'three-mesh-ui'
|
3
|
+
|
4
|
+
import { Mathf } from "../../engine/engine_math.js";
|
2
5
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
3
6
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
7
|
+
import { delayForFrames, getParam } from "../../engine/engine_utils.js";
|
8
|
+
import { NeedleXREventArgs } from "../../engine/xr/index.js";
|
9
|
+
import { Camera } from "../Camera.js";
|
10
|
+
import { GameObject } from "../Component.js";
|
4
11
|
import { BaseUIComponent, UIRootComponent } from "./BaseUIComponent.js";
|
5
|
-
import {
|
6
|
-
import { Matrix4, Object3D } from "three";
|
7
|
-
import { RectTransform } from "./RectTransform.js";
|
12
|
+
import { EventSystem } from "./EventSystem.js";
|
8
13
|
import type { ICanvas, ICanvasEventReceiver, ILayoutGroup, IRectTransform } from "./Interfaces.js";
|
9
|
-
import { Camera } from "../Camera.js";
|
10
|
-
import { EventSystem } from "./EventSystem.js";
|
11
|
-
import * as ThreeMeshUI from 'three-mesh-ui'
|
12
|
-
import { getParam } from "../../engine/engine_utils.js";
|
13
14
|
import { LayoutGroup } from "./Layout.js";
|
14
|
-
import {
|
15
|
+
import { RectTransform } from "./RectTransform.js";
|
16
|
+
import { updateRenderSettings as updateRenderSettingsRecursive } from "./Utils.js";
|
15
17
|
|
16
18
|
export enum RenderMode {
|
17
19
|
ScreenSpaceOverlay = 0,
|
@@ -200,19 +202,37 @@
|
|
200
202
|
}
|
201
203
|
}
|
202
204
|
|
205
|
+
async onEnterXR(args: NeedleXREventArgs) {
|
206
|
+
// workaround for https://linear.app/needle/issue/NE-4114
|
207
|
+
if (this.screenspace) {
|
208
|
+
if (args.xr.isVR || args.xr.isPassThrough) {
|
209
|
+
this.gameObject.visible = false;
|
210
|
+
}
|
211
|
+
}
|
212
|
+
else {
|
213
|
+
this.gameObject.visible = false;
|
214
|
+
await delayForFrames(1).then(()=>{
|
215
|
+
this.gameObject.visible = true;
|
216
|
+
});
|
217
|
+
}
|
218
|
+
}
|
219
|
+
onLeaveXR(args: NeedleXREventArgs): void {
|
220
|
+
if (this.screenspace) {
|
221
|
+
if (args.xr.isVR || args.xr.isPassThrough) {
|
222
|
+
this.gameObject.visible = true;
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
|
203
227
|
onBeforeRenderRoutine = () => {
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
this.
|
209
|
-
this.shadowComponent?.updateWorldMatrix(true, true);
|
210
|
-
this.invokeBeforeRenderEvents();
|
211
|
-
EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context, true);
|
228
|
+
this.previousParent = this.gameObject.parent;
|
229
|
+
if ((this.context.xr?.isVR || this.context.xr?.isPassThrough) && this.screenspace) {
|
230
|
+
// see https://linear.app/needle/issue/NE-4114
|
231
|
+
this.gameObject.visible = false;
|
232
|
+
this.gameObject.removeFromParent();
|
212
233
|
return;
|
213
234
|
}
|
214
235
|
|
215
|
-
this.previousParent = this.gameObject.parent;
|
216
236
|
// console.log(this.previousParent?.name + "/" + this.gameObject.name);
|
217
237
|
|
218
238
|
if (this.renderOnTop || this.screenspace) {
|
@@ -231,7 +251,12 @@
|
|
231
251
|
}
|
232
252
|
|
233
253
|
onAfterRenderRoutine = () => {
|
234
|
-
if(this.context.
|
254
|
+
if ((this.context.xr?.isVR || this.context.xr?.isPassThrough) && this.screenspace) {
|
255
|
+
this.previousParent?.add(this.gameObject);
|
256
|
+
// this is currently causing an error during XR (https://linear.app/needle/issue/NE-4114)
|
257
|
+
// this.gameObject.visible = true;
|
258
|
+
return;
|
259
|
+
}
|
235
260
|
if ((this.screenspace || this.renderOnTop) && this.previousParent && this.context.mainCamera) {
|
236
261
|
if (this.screenspace) {
|
237
262
|
const camObj = this.context.mainCamera;
|
@@ -276,7 +301,7 @@
|
|
276
301
|
for (const ch of this._rectTransforms) {
|
277
302
|
if (matrixWorldChanged) ch.markDirty();
|
278
303
|
let layout = this._layoutGroups.get(ch.gameObject);
|
279
|
-
if(ch.isDirty && !layout){
|
304
|
+
if (ch.isDirty && !layout) {
|
280
305
|
layout = ch.gameObject.getComponentInParent(LayoutGroup) as LayoutGroup;
|
281
306
|
}
|
282
307
|
if (ch.isDirty || layout?.isDirty) {
|
@@ -1,9 +1,9 @@
|
|
1
|
-
import {
|
1
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
2
2
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
3
3
|
import { Behaviour, GameObject } from "../Component.js";
|
4
|
+
import { BaseUIComponent } from "./BaseUIComponent.js";
|
5
|
+
import { Graphic } from "./Graphic.js";
|
4
6
|
import { type ICanvasGroup, type IHasAlphaFactor } from "./Interfaces.js";
|
5
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
6
|
-
import { BaseUIComponent } from "./BaseUIComponent.js";
|
7
7
|
|
8
8
|
|
9
9
|
export class CanvasGroup extends Behaviour implements ICanvasGroup {
|
@@ -1,14 +1,15 @@
|
|
1
1
|
import { Quaternion, Ray, Vector2, Vector3 } from "three";
|
2
|
+
|
2
3
|
import { Mathf } from "../engine/engine_math.js";
|
4
|
+
import { RaycastOptions } from "../engine/engine_physics.js";
|
3
5
|
import { serializable } from "../engine/engine_serialization.js";
|
6
|
+
import { getWorldPosition } from "../engine/engine_three_utils.js";
|
4
7
|
import { Collision } from "../engine/engine_types.js";
|
8
|
+
import { getParam } from "../engine/engine_utils.js";
|
9
|
+
import { Animator } from "./Animator.js"
|
5
10
|
import { CapsuleCollider } from "./Collider.js";
|
6
11
|
import { Behaviour, GameObject } from "./Component.js";
|
7
12
|
import { Rigidbody } from "./RigidBody.js";
|
8
|
-
import { Animator } from "./Animator.js"
|
9
|
-
import { RaycastOptions } from "../engine/engine_physics.js";
|
10
|
-
import { getWorldPosition } from "../engine/engine_three_utils.js";
|
11
|
-
import { getParam } from "../engine/engine_utils.js";
|
12
13
|
|
13
14
|
const debug = getParam("debugcharactercontroller");
|
14
15
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { ChromaticAberrationEffect } from "postprocessing";
|
2
2
|
import { Vector2 } from "three";
|
3
|
+
|
3
4
|
import { serializable } from "../../../engine/engine_serialization.js";
|
4
5
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
5
6
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,13 +1,14 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { Group, Mesh, Vector3 } from "three"
|
2
|
+
|
3
|
+
import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
|
3
4
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
|
-
import {
|
5
|
+
import { getWorldScale } from "../engine/engine_three_utils.js";
|
5
6
|
// import { IColliderProvider, registerColliderProvider } from "../engine/engine_physics.js";
|
6
7
|
import type { IBoxCollider, ICollider, ISphereCollider } from "../engine/engine_types.js";
|
7
|
-
import { getWorldScale } from "../engine/engine_three_utils.js";
|
8
|
-
import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
|
9
8
|
import { validate } from "../engine/engine_util_decorator.js";
|
10
9
|
import { unwatchWrite, watchWrite } from "../engine/engine_utils.js";
|
10
|
+
import { Behaviour } from "./Component.js";
|
11
|
+
import { Rigidbody } from "./RigidBody.js";
|
11
12
|
|
12
13
|
|
13
14
|
export class Collider extends Behaviour implements ICollider {
|
@@ -105,8 +106,14 @@
|
|
105
106
|
onEnable() {
|
106
107
|
super.onEnable();
|
107
108
|
this.context.physics.engine?.addBoxCollider(this, this.size);
|
109
|
+
watchWrite(this.gameObject.scale, this.updateProperties);
|
108
110
|
}
|
109
111
|
|
112
|
+
onDisable(): void {
|
113
|
+
super.onDisable();
|
114
|
+
unwatchWrite(this.gameObject.scale, this.updateProperties);
|
115
|
+
}
|
116
|
+
|
110
117
|
onValidate(): void {
|
111
118
|
this.updateProperties();
|
112
119
|
}
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import { BrightnessContrastEffect, HueSaturationEffect } from "postprocessing";
|
2
|
+
import { LinearToneMapping, NoToneMapping } from "three";
|
3
|
+
|
2
4
|
import { serializable } from "../../../engine/engine_serialization.js";
|
3
5
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
4
6
|
import { VolumeParameter } from "../VolumeParameter.js";
|
5
7
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
6
|
-
import { LinearToneMapping, NoToneMapping } from "three";
|
7
8
|
|
8
9
|
|
9
10
|
export class ColorAdjustments extends PostProcessingEffect {
|
@@ -1,15 +1,17 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { isDevEnvironment } from "../engine/debug/index.js";
|
4
|
+
import { addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../engine/engine_components.js";
|
3
5
|
import { activeInHierarchyFieldName } from "../engine/engine_constants.js";
|
4
|
-
import {
|
6
|
+
import { destroy, findByGuid, foreachComponent, HideFlags, IInstantiateOptions, instantiate, isActiveInHierarchy, isActiveSelf, isDestroyed, isUsingInstancing, markAsInstancedRendered, setActive } from "../engine/engine_gameobject.js";
|
5
7
|
import * as main from "../engine/engine_mainloop_utils.js";
|
6
8
|
import { syncDestroy, syncInstantiate } from "../engine/engine_networking_instantiate.js";
|
7
|
-
import
|
8
|
-
import
|
9
|
-
import {
|
9
|
+
import { Context, FrameEvent } from "../engine/engine_setup.js";
|
10
|
+
import * as threeutils from "../engine/engine_three_utils.js";
|
11
|
+
import type { Collision, Constructor, ConstructorConcrete, GuidsMap, ICollider, IComponent, IGameObject, SourceIdentifier } from "../engine/engine_types.js";
|
12
|
+
import { INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs } from "../engine/engine_xr.js";
|
13
|
+
import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
10
14
|
|
11
|
-
import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
|
12
|
-
import { showBalloonWarning, isDevEnvironment } from "../engine/debug/index.js";
|
13
15
|
|
14
16
|
// export interface ISerializationCallbackReceiver {
|
15
17
|
// onBeforeSerialize?(): object | void;
|
@@ -81,8 +83,8 @@
|
|
81
83
|
* @param instance object to instantiate
|
82
84
|
* @param opts options for the instantiation (e.g. with what parent, position, etc.)
|
83
85
|
*/
|
84
|
-
public static instantiate(instance: GameObject | Object3D
|
85
|
-
return instantiate(instance, opts) as GameObject
|
86
|
+
public static instantiate(instance: GameObject | Object3D, opts: IInstantiateOptions | null = null): GameObject {
|
87
|
+
return instantiate(instance, opts) as GameObject;
|
86
88
|
}
|
87
89
|
|
88
90
|
/** Destroys a object on all connected clients (if you are in a networked session)
|
@@ -123,7 +125,7 @@
|
|
123
125
|
main.addScriptToArrays(comp, context!);
|
124
126
|
if (comp.__internalDidAwakeAndStart) return;
|
125
127
|
if (context!.new_script_start.includes(comp) === false) {
|
126
|
-
context!.new_script_start.push(comp as
|
128
|
+
context!.new_script_start.push(comp as Component);
|
127
129
|
}
|
128
130
|
}, true);
|
129
131
|
}
|
@@ -254,7 +256,7 @@
|
|
254
256
|
return getComponentsInParent(go, typeName, arr);
|
255
257
|
}
|
256
258
|
|
257
|
-
public static getAllComponents(go: IGameObject | Object3D):
|
259
|
+
public static getAllComponents(go: IGameObject | Object3D): Component[] {
|
258
260
|
const componentsList = go.userData?.components;
|
259
261
|
const newList = [...componentsList];
|
260
262
|
return newList;
|
@@ -294,7 +296,7 @@
|
|
294
296
|
abstract set worldQuaternion(val: Quaternion);
|
295
297
|
abstract get worldQuaternion(): Quaternion;
|
296
298
|
abstract set worldRotation(val: Vector3);
|
297
|
-
abstract get worldRotation(): Vector3;
|
299
|
+
abstract get worldRotation(): Vector3;
|
298
300
|
abstract set worldScale(val: Vector3);
|
299
301
|
abstract get worldScale(): Vector3;
|
300
302
|
|
@@ -305,17 +307,28 @@
|
|
305
307
|
|
306
308
|
|
307
309
|
|
308
|
-
|
310
|
+
/** Needle Engine component base class. Derive from this component to implement your own using the provided lifecycle methods. Components can be added to threejs objects using `GameObject.addComponent`.
|
311
|
+
*
|
312
|
+
* The most common lifecycle methods are `awake`, `start`, `onEanble`, `onDisable` `update` and `onDestroy`.
|
313
|
+
* XR specific callbacks include `onEnterXR`, `onLeaveXR`, `onUpdateXR`, `onControllerAdded` and `onControllerRemoved`.
|
314
|
+
* To receive pointer events implement `onPointerDown`, `onPointerUp`, `onPointerEnter`, `onPointerExit` and `onPointerMove`.
|
315
|
+
*/
|
316
|
+
export abstract class Component implements IComponent, EventTarget,
|
317
|
+
Partial<INeedleXRSessionEventReceiver>,
|
318
|
+
Partial<IPointerEventHandler>
|
319
|
+
{
|
309
320
|
|
310
321
|
get isComponent(): boolean { return true; }
|
311
322
|
|
312
323
|
private __context: Context | undefined;
|
324
|
+
/** Use the context to get access to many Needle Engine features and use physics, timing, access the camera or scene */
|
313
325
|
get context(): Context {
|
314
326
|
return this.__context ?? Context.Current;
|
315
327
|
}
|
316
328
|
set context(context: Context) {
|
317
329
|
this.__context = context;
|
318
330
|
}
|
331
|
+
/** shorthand for `this.context.scene` */
|
319
332
|
get scene(): Scene { return this.context.scene; }
|
320
333
|
|
321
334
|
get layer(): number {
|
@@ -355,7 +368,7 @@
|
|
355
368
|
return this.gameObject?.userData.hideFlags;
|
356
369
|
}
|
357
370
|
|
358
|
-
|
371
|
+
/** @returns true if the object is enabled and active in the hierarchy */
|
359
372
|
get activeAndEnabled(): boolean {
|
360
373
|
if (this.destroyed) return false;
|
361
374
|
if (this.__isEnabled === false) return false;
|
@@ -385,19 +398,27 @@
|
|
385
398
|
this.gameObject[activeInHierarchyFieldName] = val;
|
386
399
|
}
|
387
400
|
|
401
|
+
/** the object this component is attached to. Note that this is a threejs Object3D with some additional features */
|
388
402
|
gameObject!: GameObject;
|
403
|
+
/** the unique identifier for this component */
|
389
404
|
guid: string = "invalid";
|
405
|
+
/** holds the source identifier this object was created with/from (e.g. if it was part of a glTF file the sourceId holds the url to the glTF) */
|
390
406
|
sourceId?: SourceIdentifier;
|
391
407
|
// transform: Object3D = nullObject;
|
392
408
|
|
393
409
|
/** called on a component with a map of old to new guids (e.g. when instantiate generated new guids and e.g. timeline track bindings needs to remape them) */
|
394
410
|
resolveGuids?(guidsMap: GuidsMap): void;
|
395
411
|
|
396
|
-
/** called once when the component becomes active for the first time
|
412
|
+
/** called once when the component becomes active for the first time (once per component)
|
413
|
+
* This is the first callback to be called */
|
397
414
|
awake() { }
|
398
|
-
/** called every time when the component gets enabled (this is invoked after awake and before start)
|
415
|
+
/** called every time when the component gets enabled (this is invoked after awake and before start)
|
416
|
+
* or when it becomes active in the hierarchy (e.g. if a parent object or this.gameObject gets set to visible)
|
417
|
+
*/
|
399
418
|
onEnable() { }
|
419
|
+
/** called every time the component gets disabled or if a parent object (or this.gameObject) gets set to invisible */
|
400
420
|
onDisable() { }
|
421
|
+
/** Called when the component gets destroyed */
|
401
422
|
onDestroy() {
|
402
423
|
this.__destroyed = true;
|
403
424
|
}
|
@@ -409,11 +430,17 @@
|
|
409
430
|
/** Called for all scripts when the context gets paused or unpaused */
|
410
431
|
onPausedChanged?(isPaused: boolean, wasPaused: boolean): void;
|
411
432
|
|
433
|
+
/** called at the beginning of a frame (once per component) */
|
412
434
|
start?(): void;
|
435
|
+
/** first callback in a frame (called every frame when implemented) */
|
413
436
|
earlyUpdate?(): void;
|
437
|
+
/** regular callback in a frame (called every frame when implemented) */
|
414
438
|
update?(): void;
|
439
|
+
/** late callback in a frame (called every frame when implemented) */
|
415
440
|
lateUpdate?(): void;
|
441
|
+
/** called before the scene gets rendered in the main update loop */
|
416
442
|
onBeforeRender?(frame: XRFrame | null): void;
|
443
|
+
/** called after the scene was rendered */
|
417
444
|
onAfterRender?(): void;
|
418
445
|
|
419
446
|
onCollisionEnter?(col: Collision);
|
@@ -424,18 +451,79 @@
|
|
424
451
|
onTriggerStay?(col: ICollider);
|
425
452
|
onTriggerExit?(col: ICollider);
|
426
453
|
|
454
|
+
|
455
|
+
/** Optional callback, you can implement this to only get callbacks for VR or AR sessions if necessary.
|
456
|
+
* @returns true if the mode is supported (if false the mode is not supported by this component and it will not receive XR callbacks for this mode)
|
457
|
+
*/
|
458
|
+
supportsXR?(mode: XRSessionMode): boolean;
|
459
|
+
/** Called before the XR session is requested. Use this callback if you want to modify the session init features */
|
460
|
+
onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void;
|
461
|
+
/** Callback when this component joins a xr session (or becomes active in a running XR session) */
|
462
|
+
onEnterXR?(args: NeedleXREventArgs): void;
|
463
|
+
/** Callback when a xr session updates (while it is still active in XR session) */
|
464
|
+
onUpdateXR?(args: NeedleXREventArgs): void;
|
465
|
+
/** Callback when this component exists a xr session (or when it becomes inactive in a running XR session) */
|
466
|
+
onLeaveXR?(args: NeedleXREventArgs): void;
|
467
|
+
/** Callback when a controller is connected/added while in a XR session
|
468
|
+
* OR when the component joins a running XR session that has already connected controllers
|
469
|
+
* OR when the component becomes active during a running XR session that has already connected controllers */
|
470
|
+
onXRControllerAdded?(args: NeedleXRControllerEventArgs): void;
|
471
|
+
/** callback when a controller is removed while in a XR session
|
472
|
+
* OR when the component becomes inactive during a running XR session
|
473
|
+
*/
|
474
|
+
onXRControllerRemoved?(args: NeedleXRControllerEventArgs): void;
|
475
|
+
|
476
|
+
|
477
|
+
/* IPointerEventReceiver */
|
478
|
+
/* @inheritdoc */
|
479
|
+
onPointerEnter?(args: PointerEventData);
|
480
|
+
onPointerMove?(args: PointerEventData);
|
481
|
+
onPointerExit?(args: PointerEventData);
|
482
|
+
onPointerDown?(args: PointerEventData);
|
483
|
+
onPointerUp?(args: PointerEventData);
|
484
|
+
onPointerClick?(args: PointerEventData);
|
485
|
+
|
486
|
+
|
487
|
+
/** starts a coroutine (javascript generator function)
|
488
|
+
* `yield` will wait for the next frame:
|
489
|
+
* - Use `yield WaitForSeconds(1)` to wait for 1 second.
|
490
|
+
* - Use `yield WaitForFrames(10)` to wait for 10 frames.
|
491
|
+
* - Use `yield new Promise(...)` to wait for a promise to resolve.
|
492
|
+
* @param routine generator function to start
|
493
|
+
* @param evt event to register the coroutine for (default: FrameEvent.Update). Note that all coroutine FrameEvent callbacks are invoked after the matching regular component callbacks. For example `FrameEvent.Update` will be called after regular component `update()` methods)
|
494
|
+
* @returns the generator function (use it to stop the coroutine with `stopCoroutine`)
|
495
|
+
* @example
|
496
|
+
* ```ts
|
497
|
+
* onEnable() { this.startCoroutine(this.myCoroutine()); }
|
498
|
+
* private *myCoroutine() {
|
499
|
+
* while(this.activeAndEnabled) {
|
500
|
+
* console.log("Hello World", this.context.time.frame);
|
501
|
+
* // wait for 5 frames
|
502
|
+
* for(let i = 0; i < 5; i++) yield;
|
503
|
+
* }
|
504
|
+
* }
|
505
|
+
* ```
|
506
|
+
*/
|
427
507
|
startCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): Generator {
|
428
508
|
return this.context.registerCoroutineUpdate(this, routine, evt);
|
429
509
|
}
|
430
|
-
|
510
|
+
/**
|
511
|
+
* Stop a coroutine that was previously started with `startCoroutine`
|
512
|
+
* @param routine the routine to be stopped
|
513
|
+
* @param evt the frame event to unregister the routine from (default: FrameEvent.Update)
|
514
|
+
*/
|
431
515
|
stopCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): void {
|
432
516
|
this.context.unregisterCoroutineUpdate(routine, evt);
|
433
517
|
}
|
434
518
|
|
519
|
+
/** @returns true if this component was destroyed (`this.destroy()`) or the whole object this component was part of */
|
435
520
|
public get destroyed(): boolean {
|
436
521
|
return this.__destroyed;
|
437
522
|
}
|
438
523
|
|
524
|
+
/**
|
525
|
+
* Destroys this component (and removes it from the object)
|
526
|
+
*/
|
439
527
|
public destroy() {
|
440
528
|
if (this.__destroyed) return;
|
441
529
|
this.__internalDestroy();
|
@@ -464,7 +552,11 @@
|
|
464
552
|
|
465
553
|
/** @internal */
|
466
554
|
constructor() {
|
467
|
-
this.
|
555
|
+
this.__didAwake = false;
|
556
|
+
this.__didStart = false;
|
557
|
+
this.__didEnable = false;
|
558
|
+
this.__isEnabled = undefined;
|
559
|
+
this.__destroyed = false;
|
468
560
|
}
|
469
561
|
|
470
562
|
|
@@ -666,5 +758,6 @@
|
|
666
758
|
}
|
667
759
|
}
|
668
760
|
|
669
|
-
export
|
670
|
-
|
761
|
+
// For legacy reasons we need to export this as well
|
762
|
+
// (and we don't use extend to inherit the component docs)
|
763
|
+
export { Component as Behaviour };
|
@@ -1,3 +1,4 @@
|
|
1
|
+
/* eslint-disable */
|
1
2
|
// Export types
|
2
3
|
export class __Ignore {}
|
3
4
|
export { ActionBuilder } from "../export/usdz/extensions/behavior/BehavioursBuilder.js";
|
@@ -11,11 +12,11 @@
|
|
11
12
|
export { Animator } from "../Animator.js";
|
12
13
|
export { AnimatorController } from "../AnimatorController.js";
|
13
14
|
export { Antialiasing } from "../postprocessing/Effects/Antialiasing.js";
|
14
|
-
export { AttachedObject } from "../webxr/WebXRController.js";
|
15
15
|
export { AudioExtension } from "../export/usdz/extensions/behavior/AudioExtension.js";
|
16
16
|
export { AudioListener } from "../AudioListener.js";
|
17
17
|
export { AudioSource } from "../AudioSource.js";
|
18
18
|
export { AudioTrackHandler } from "../timeline/TimelineTracks.js";
|
19
|
+
export { Avatar } from "../webxr/Avatar.js";
|
19
20
|
export { Avatar_Brain_LookAt } from "../avatar/Avatar_Brain_LookAt.js";
|
20
21
|
export { Avatar_MouthShapes } from "../avatar/Avatar_MouthShapes.js";
|
21
22
|
export { Avatar_MustacheShake } from "../avatar/Avatar_MustacheShake.js";
|
@@ -30,7 +31,6 @@
|
|
30
31
|
export { BasicIKConstraint } from "../BasicIKConstraint.js";
|
31
32
|
export { BehaviorExtension } from "../export/usdz/extensions/behavior/Behaviour.js";
|
32
33
|
export { BehaviorModel } from "../export/usdz/extensions/behavior/BehavioursBuilder.js";
|
33
|
-
export { Behaviour } from "../Component.js";
|
34
34
|
export { Bloom } from "../postprocessing/Effects/Bloom.js";
|
35
35
|
export { BoxCollider } from "../Collider.js";
|
36
36
|
export { BoxGizmo } from "../Gizmos.js";
|
@@ -51,7 +51,6 @@
|
|
51
51
|
export { ColorAdjustments } from "../postprocessing/Effects/ColorAdjustments.js";
|
52
52
|
export { ColorBySpeedModule } from "../ParticleSystemModules.js";
|
53
53
|
export { ColorOverLifetimeModule } from "../ParticleSystemModules.js";
|
54
|
-
export { Component } from "../Component.js";
|
55
54
|
export { ContactShadows } from "../ContactShadows.js";
|
56
55
|
export { ControlTrackHandler } from "../timeline/TimelineTracks.js";
|
57
56
|
export { CustomBranding } from "../export/usdz/USDZExporter.js";
|
@@ -88,7 +87,6 @@
|
|
88
87
|
export { Image } from "../ui/Image.js";
|
89
88
|
export { InheritVelocityModule } from "../ParticleSystemModules.js";
|
90
89
|
export { InputField } from "../ui/InputField.js";
|
91
|
-
export { Interactable } from "../Interactable.js";
|
92
90
|
export { Light } from "../Light.js";
|
93
91
|
export { LimitVelocityOverLifetimeModule } from "../ParticleSystemModules.js";
|
94
92
|
export { LODGroup } from "../LODGroup.js";
|
@@ -102,6 +100,7 @@
|
|
102
100
|
export { MeshRenderer } from "../Renderer.js";
|
103
101
|
export { MinMaxCurve } from "../ParticleSystemModules.js";
|
104
102
|
export { MinMaxGradient } from "../ParticleSystemModules.js";
|
103
|
+
export { NeedleWebXRHtmlElement } from "../webxr/WebXRButtons.js";
|
105
104
|
export { NestedGltf } from "../NestedGltf.js";
|
106
105
|
export { Networking } from "../Networking.js";
|
107
106
|
export { NoiseModule } from "../ParticleSystemModules.js";
|
@@ -125,7 +124,6 @@
|
|
125
124
|
export { PreliminaryAction } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
|
126
125
|
export { PreliminaryTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
|
127
126
|
export { RawImage } from "../ui/Image.js";
|
128
|
-
export { Raycaster } from "../ui/Raycaster.js";
|
129
127
|
export { Rect } from "../ui/RectTransform.js";
|
130
128
|
export { RectTransform } from "../ui/RectTransform.js";
|
131
129
|
export { ReflectionProbe } from "../ReflectionProbe.js";
|
@@ -153,6 +151,7 @@
|
|
153
151
|
export { SizeOverLifetimeModule } from "../ParticleSystemModules.js";
|
154
152
|
export { SkinnedMeshRenderer } from "../Renderer.js";
|
155
153
|
export { SmoothFollow } from "../SmoothFollow.js";
|
154
|
+
export { SpatialGrabRaycaster } from "../ui/Raycaster.js";
|
156
155
|
export { SpatialHtml } from "../ui/SpatialHtml.js";
|
157
156
|
export { SpatialTrigger } from "../SpatialTrigger.js";
|
158
157
|
export { SpatialTriggerReceiver } from "../SpatialTrigger.js";
|
@@ -167,7 +166,7 @@
|
|
167
166
|
export { SyncedRoom } from "../SyncedRoom.js";
|
168
167
|
export { SyncedTransform } from "../SyncedTransform.js";
|
169
168
|
export { TapGestureTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
|
170
|
-
export { TeleportTarget } from "../webxr/
|
169
|
+
export { TeleportTarget } from "../webxr/TeleportTarget.js";
|
171
170
|
export { TestRunner } from "../TestRunner.js";
|
172
171
|
export { TestSimulateUserData } from "../TestRunner.js";
|
173
172
|
export { Text } from "../ui/Text.js";
|
@@ -197,20 +196,16 @@
|
|
197
196
|
export { Volume } from "../postprocessing/Volume.js";
|
198
197
|
export { VolumeParameter } from "../postprocessing/VolumeParameter.js";
|
199
198
|
export { VolumeProfile } from "../postprocessing/VolumeProfile.js";
|
200
|
-
export { VRUserState } from "../webxr/WebXRSync.js";
|
201
|
-
export { WebAR } from "../webxr/WebXR.js";
|
202
199
|
export { WebARCameraBackground } from "../webxr/WebARCameraBackground.js";
|
203
200
|
export { WebARSessionRoot } from "../webxr/WebARSessionRoot.js";
|
204
201
|
export { WebXR } from "../webxr/WebXR.js";
|
205
|
-
export { WebXRAvatar } from "../webxr/WebXRAvatar.js";
|
206
|
-
export { WebXRController } from "../webxr/WebXRController.js";
|
207
202
|
export { WebXRImageTracking } from "../webxr/WebXRImageTracking.js";
|
208
203
|
export { WebXRImageTrackingModel } from "../webxr/WebXRImageTracking.js";
|
209
204
|
export { WebXRPlaneTracking } from "../webxr/WebXRPlaneTracking.js";
|
210
|
-
export { WebXRSync } from "../webxr/WebXRSync.js";
|
211
205
|
export { WebXRTrackedImage } from "../webxr/WebXRImageTracking.js";
|
212
|
-
export {
|
213
|
-
export {
|
214
|
-
export {
|
206
|
+
export { XRControllerFollow } from "../webxr/controllers/XRControllerFollow.js";
|
207
|
+
export { XRControllerModel } from "../webxr/controllers/XRControllerModel.js";
|
208
|
+
export { XRControllerMovement } from "../webxr/controllers/XRControllerMovement.js";
|
209
|
+
export { XRFlag } from "../webxr/XRFlag.js";
|
215
210
|
export { XRRig } from "../webxr/WebXRRig.js";
|
216
|
-
export { XRState } from "../XRFlag.js";
|
211
|
+
export { XRState } from "../webxr/XRFlag.js";
|
@@ -1,11 +1,11 @@
|
|
1
|
-
import { Behaviour } from "./Component.js";
|
2
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
3
|
-
|
4
1
|
import { CustomBlending, DoubleSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, OrthographicCamera, PlaneGeometry, ShaderMaterial, WebGLRenderTarget } from "three";
|
5
2
|
import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js';
|
6
3
|
import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js';
|
4
|
+
|
5
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
7
6
|
import { getParam } from "../engine/engine_utils.js"
|
8
7
|
import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
|
8
|
+
import { Behaviour } from "./Component.js";
|
9
9
|
|
10
10
|
const debug = getParam("debugcontactshadows");
|
11
11
|
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import { getErrorCount } from "./debug_overlay.js";
|
2
|
-
import { getParam, isMobileDevice } from "../engine_utils.js";
|
3
1
|
import { isLocalNetwork } from "../engine_networking_utils.js";
|
2
|
+
import { getParam, isMobileDevice, isQuest } from "../engine_utils.js";
|
3
|
+
import { isDevEnvironment } from "./debug.js";
|
4
|
+
import { getErrorCount, makeErrorsVisibleForDevelopment } from "./debug_overlay.js";
|
4
5
|
|
5
6
|
let consoleInstance: any = null;
|
6
7
|
let consoleHtmlElement: HTMLElement | null = null;
|
@@ -22,8 +23,11 @@
|
|
22
23
|
currentUrl.searchParams.set("console", "1");
|
23
24
|
console.log("🌵 Tip: You can add the \"?console\" query parameter to the url to show the debug console (on mobile it will automatically open in the bottom right corner when your get errors during development)", "\nOpen this page console: " + currentUrl.toString());
|
24
25
|
}
|
25
|
-
const isMobile = isMobileDevice();
|
26
|
+
const isMobile = isMobileDevice() || (isQuest() && isDevEnvironment());
|
26
27
|
if (isMobile) {
|
28
|
+
// we need to invoke this here - otherwise we will miss errors that happen after the console is loaded
|
29
|
+
// and calling the method from the root needle-engine.ts import is evaluated later (if we import the method from the toplevel file and then invoke it)
|
30
|
+
makeErrorsVisibleForDevelopment();
|
27
31
|
beginWatchingLogs();
|
28
32
|
createConsole(true);
|
29
33
|
if (isMobile) {
|
@@ -191,7 +195,7 @@
|
|
191
195
|
}
|
192
196
|
`;
|
193
197
|
consoleHtmlElement?.prepend(styles);
|
194
|
-
if (startHidden === true)
|
198
|
+
if (startHidden === true && getErrorCount() <= 0)
|
195
199
|
hideDebugConsole();
|
196
200
|
console.log("🌵 Debug console has loaded");
|
197
201
|
}
|
@@ -1,6 +1,6 @@
|
|
1
|
+
import { ContextRegistry } from "../engine_context_registry.js";
|
2
|
+
import { isLocalNetwork } from "../engine_networking_utils.js";
|
1
3
|
import { getParam } from "../engine_utils.js";
|
2
|
-
import { isLocalNetwork } from "../engine_networking_utils.js";
|
3
|
-
import { ContextRegistry } from "../engine_context_registry.js";
|
4
4
|
|
5
5
|
const debug = getParam("debugdebug");
|
6
6
|
let hide = false;
|
@@ -15,7 +15,7 @@
|
|
15
15
|
}
|
16
16
|
|
17
17
|
export function getErrorCount() {
|
18
|
-
return
|
18
|
+
return _errorCount;
|
19
19
|
}
|
20
20
|
|
21
21
|
const originalConsoleError = console.error;
|
@@ -37,9 +37,10 @@
|
|
37
37
|
if (hide) return;
|
38
38
|
const isLocal = isLocalNetwork();
|
39
39
|
if (debug) console.log("Is this a local network?", isLocal);
|
40
|
-
if (isLocal)
|
40
|
+
if (isLocal)
|
41
|
+
{
|
41
42
|
if (debug)
|
42
|
-
console.
|
43
|
+
console.warn("Patch console", window.location.hostname);
|
43
44
|
console.error = patchedConsoleError;
|
44
45
|
window.addEventListener("error", (event) => {
|
45
46
|
if (hide) return;
|
@@ -66,10 +67,10 @@
|
|
66
67
|
}
|
67
68
|
|
68
69
|
|
69
|
-
let
|
70
|
+
let _errorCount = 0;
|
70
71
|
|
71
72
|
function onReceivedError() {
|
72
|
-
|
73
|
+
_errorCount += 1;
|
73
74
|
}
|
74
75
|
|
75
76
|
function onParseError(args: Array<any>) {
|
@@ -1,10 +1,13 @@
|
|
1
|
+
import { isLocalNetwork } from "../engine_networking_utils.js";
|
2
|
+
import { getParam } from "../engine_utils.js";
|
3
|
+
import { showDebugConsole } from "./debug_console.js";
|
1
4
|
import { addLog, LogType, setAllowOverlayMessages } from "./debug_overlay.js";
|
2
|
-
import { showDebugConsole } from "./debug_console.js";
|
3
|
-
import { isLocalNetwork } from "../engine_networking_utils.js";
|
4
5
|
|
5
6
|
export { showDebugConsole }
|
6
7
|
export { LogType, setAllowOverlayMessages };
|
7
8
|
|
9
|
+
const noDevLogs = getParam("nodevlogs");
|
10
|
+
|
8
11
|
/** Displays a debug message on screen for a certain amount of time */
|
9
12
|
export function showBalloonMessage(text: string, logType: LogType = LogType.Log): void {
|
10
13
|
addLog(logType, text);
|
@@ -22,6 +25,7 @@
|
|
22
25
|
|
23
26
|
/** True when the application runs on a local url */
|
24
27
|
export function isDevEnvironment(): boolean {
|
28
|
+
if (noDevLogs) return false;
|
25
29
|
if (_manuallySetDevEnvironment !== undefined) return _manuallySetDevEnvironment;
|
26
30
|
return isLocalNetwork();
|
27
31
|
}
|
@@ -1,5 +1,6 @@
|
|
1
1
|
|
2
2
|
import * as THREE from "three";
|
3
|
+
|
3
4
|
import { syncDestroy } from "../engine/engine_networking_instantiate.js";
|
4
5
|
import { getParam } from "../engine/engine_utils.js";
|
5
6
|
import { BoxHelperComponent } from "./BoxHelperComponent.js";
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { DepthOfFieldEffect } from "postprocessing";
|
2
|
+
|
3
|
+
import { Mathf } from "../../../engine/engine_math.js";
|
2
4
|
import { serializable } from "../../../engine/engine_serialization.js";
|
3
|
-
import { Mathf } from "../../../engine/engine_math.js";
|
4
5
|
import { getParam, isMobileDevice } from "../../../engine/engine_utils.js";
|
5
6
|
import { PostProcessingEffect } from "../PostProcessingEffect.js";
|
6
7
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
2
3
|
import { isMobileDevice } from "../engine/engine_utils.js";
|
3
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
4
|
import { Behaviour, GameObject } from "./Component.js";
|
5
5
|
|
6
6
|
|
@@ -1,104 +1,126 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import
|
1
|
+
import { AxesHelper, Box3, BufferGeometry, Camera, Color, Event, Line, LineBasicMaterial, Matrix3, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, PlaneHelper, Quaternion, Ray, Raycaster, SphereGeometry, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
4
|
+
import { InstancingUtil } from "../engine/engine_instancing.js";
|
5
|
+
import { Mathf } from "../engine/engine_math.js";
|
6
|
+
import { RaycastOptions } from "../engine/engine_physics.js";
|
7
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
8
|
import { Context } from "../engine/engine_setup.js";
|
5
|
-
import {
|
6
|
-
import {
|
7
|
-
import {
|
9
|
+
import { getWorldPosition, setWorldPosition } from "../engine/engine_three_utils.js";
|
10
|
+
import { IGameObject } from "../engine/engine_types.js";
|
11
|
+
import { getParam } from "../engine/engine_utils.js";
|
12
|
+
import { NeedleXRSession } from "../engine/engine_xr.js";
|
8
13
|
import { Avatar_POI } from "./avatar/Avatar_Brain_LookAt.js";
|
9
|
-
import {
|
10
|
-
import {
|
11
|
-
import type { KeyCode } from "../engine/engine_input.js";
|
12
|
-
import { nameofFactory } from "../engine/engine_utils.js";
|
13
|
-
import { InstancingUtil } from "../engine/engine_instancing.js";
|
14
|
+
import { Behaviour, GameObject } from "./Component.js";
|
15
|
+
import { UsageMarker } from "./Interactable.js";
|
14
16
|
import { OrbitControls } from "./OrbitControls.js";
|
15
|
-
import {
|
17
|
+
import { Rigidbody } from "./RigidBody.js";
|
18
|
+
import { SyncedTransform } from "./SyncedTransform.js";
|
19
|
+
import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
16
20
|
import { ObjectRaycaster } from "./ui/Raycaster.js";
|
17
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
18
21
|
|
19
|
-
const debug =
|
22
|
+
const debug = getParam("debugdrag");
|
20
23
|
|
21
|
-
export enum
|
22
|
-
|
23
|
-
|
24
|
+
export enum DragMode {
|
25
|
+
/** Object stays at the same horizontal plane as it started. Commonly used for objects on the floor */
|
26
|
+
XZPlane = 0,
|
27
|
+
/** Object is dragged as if it was attached to the pointer. In 2D, that means it's dragged along the camera screen plane. In XR, it's dragged by the controller/hand. */
|
28
|
+
Attached = 1,
|
29
|
+
/** Object is dragged along the initial raycast hit normal. */
|
30
|
+
HitNormal = 2,
|
31
|
+
/** Combination of XZ and Screen based on the viewing angle. Low angles result in Screen dragging and higher angles in XZ dragging. */
|
32
|
+
DynamicViewAngle = 3,
|
33
|
+
/** The drag plane is adjusted dynamically while dragging. */
|
34
|
+
SnapToSurfaces = 4,
|
24
35
|
}
|
25
36
|
|
26
|
-
|
27
|
-
selected: Object3D;
|
28
|
-
attached: Object3D | GameObject | null;
|
29
|
-
}
|
37
|
+
export class DragControls extends Behaviour implements IPointerEventHandler {
|
30
38
|
|
39
|
+
// dragPlane (floor, object, view)
|
40
|
+
// snap to surface (snap orientation?)
|
41
|
+
// two-handed drag (scale, rotate, move)
|
42
|
+
// keep upright (no tilt)
|
31
43
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
}
|
44
|
+
/** How and where the object is dragged along. */
|
45
|
+
@serializable()
|
46
|
+
public dragMode: DragMode = DragMode.DynamicViewAngle;
|
36
47
|
|
37
|
-
|
48
|
+
/** Snap dragged objects to a XYZ grid – 0 means: no snapping. */
|
49
|
+
@serializable()
|
50
|
+
public snapGridResolution: number = 0.0;
|
51
|
+
|
52
|
+
/** Keep the original rotation of the dragged object. */
|
53
|
+
@serializable()
|
54
|
+
public keepRotation: boolean = true;
|
55
|
+
|
56
|
+
/** How and where the object is dragged along while dragging in XR. */
|
57
|
+
@serializable()
|
58
|
+
public xrDragMode: DragMode = DragMode.Attached;
|
38
59
|
|
39
|
-
|
40
|
-
|
60
|
+
/** Keep the original rotation of the dragged object while dragging in XR. */
|
61
|
+
@serializable()
|
62
|
+
public xrKeepRotation: boolean = false;
|
41
63
|
|
42
|
-
/**
|
64
|
+
/** Accelerate dragging objects closer / further away when in XR */
|
43
65
|
@serializable()
|
44
|
-
public
|
66
|
+
public xrDistanceDragFactor: number = 1;
|
45
67
|
|
46
|
-
/** When enabled
|
68
|
+
/** When enabled, draws a line from the dragged object downwards to the next raycast hit. */
|
47
69
|
@serializable()
|
48
|
-
public
|
70
|
+
public showGizmo: boolean = false;
|
49
71
|
|
50
|
-
|
51
|
-
//
|
52
|
-
// public targets: Object3D[] | null = null;
|
72
|
+
// future:
|
73
|
+
// constraints?
|
53
74
|
|
54
|
-
|
75
|
+
public static get HasAnySelected(): boolean { return this._active > 0; }
|
76
|
+
private static _active: number = 0;
|
77
|
+
|
78
|
+
/** The object to be dragged – we pass this to handlers when they are created */
|
79
|
+
private targetObject: GameObject | null = null;
|
55
80
|
private orbit: OrbitControls | null = null;
|
81
|
+
private _dragHelper: LegacyDragVisualsHelper | null = null;
|
82
|
+
private static lastHovered: Object3D;
|
83
|
+
private _draggingRigidbodies: Rigidbody[] = [];
|
84
|
+
private _potentialDragStartEvt: PointerEventData | null = null;
|
85
|
+
private _dragHandlers: Map<Object3D, IDragHandler> = new Map();
|
86
|
+
private _totalMovement: Vector3 = new Vector3();
|
87
|
+
/** A marker is attached to components that are currently interacted with, to e.g. prevent them from being deleted. */
|
88
|
+
private _marker: UsageMarker | null = null;
|
89
|
+
private _isDragging: boolean = false;
|
90
|
+
private _didDrag: boolean = false;
|
56
91
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
super();
|
63
|
-
this.selectStartEventListener = [];
|
64
|
-
this.selectEndEventListener = [];
|
65
|
-
this._dragDelta = new Vector2();
|
92
|
+
setTargetObject(obj: Object3D | null) {
|
93
|
+
this.targetObject = obj as GameObject;
|
94
|
+
for (const handler of this._dragHandlers.values()) {
|
95
|
+
handler.setTargetObject(obj);
|
96
|
+
}
|
66
97
|
}
|
67
98
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
}
|
99
|
+
awake() {
|
100
|
+
// initialize all data that may be cloned incorrectly otherwise
|
101
|
+
this._potentialDragStartEvt = null;
|
102
|
+
this._dragHandlers = new Map();
|
103
|
+
this._totalMovement = new Vector3();
|
104
|
+
this._marker = null;
|
105
|
+
this._isDragging = false;
|
106
|
+
this._didDrag = false;
|
107
|
+
this._dragHelper = null;
|
108
|
+
this._draggingRigidbodies = [];
|
79
109
|
}
|
80
110
|
|
81
|
-
|
82
|
-
|
83
111
|
start() {
|
84
112
|
this.orbit = GameObject.findObjectOfType(OrbitControls, this.context);
|
85
|
-
if (!this.gameObject.getComponentInParent(ObjectRaycaster))
|
113
|
+
if (!this.gameObject.getComponentInParent(ObjectRaycaster))
|
86
114
|
this.gameObject.addNewComponent(ObjectRaycaster);
|
87
|
-
}
|
88
115
|
}
|
89
116
|
|
90
|
-
private static lastHovered: Object3D;
|
91
|
-
private _draggingRigidbodies: Rigidbody[] = [];
|
92
|
-
|
93
117
|
private allowEdit(_obj: Object3D | null = null) {
|
94
118
|
return this.context.connection.allowEditing;
|
95
119
|
}
|
96
120
|
|
97
121
|
onPointerEnter(evt: PointerEventData) {
|
98
122
|
if (!this.allowEdit(this.gameObject)) return;
|
99
|
-
if (
|
100
|
-
// const interactable = GameObject.getComponentInParent(evt.object, Interactable);
|
101
|
-
// if (!interactable) return;
|
123
|
+
if (evt.mode !== "screen") return;
|
102
124
|
const dc = GameObject.getComponentInParent(evt.object, DragControls);
|
103
125
|
if (!dc || dc !== this) return;
|
104
126
|
DragControls.lastHovered = evt.object;
|
@@ -107,83 +129,121 @@
|
|
107
129
|
|
108
130
|
onPointerExit(evt: PointerEventData) {
|
109
131
|
if (!this.allowEdit(this.gameObject)) return;
|
110
|
-
if (
|
132
|
+
if (evt.mode !== "screen") return;
|
111
133
|
if (DragControls.lastHovered !== evt.object) return;
|
112
|
-
// const interactable = GameObject.getComponentInParent(evt.object, Interactable);
|
113
|
-
// if (!interactable) return;
|
114
134
|
this.context.domElement.style.cursor = 'auto';
|
115
135
|
}
|
116
136
|
|
117
|
-
private _waitingForDragStart: PointerEventData | null = null;
|
118
|
-
|
119
137
|
onPointerDown(args: PointerEventData) {
|
120
138
|
if (!this.allowEdit(this.gameObject)) return;
|
121
|
-
if (
|
122
|
-
DragControls.
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
139
|
+
if (args.used) return;
|
140
|
+
DragControls.lastHovered = args.object;
|
141
|
+
|
142
|
+
if (args.button === 0) {
|
143
|
+
if (this._dragHandlers.size === 0) {
|
144
|
+
this._didDrag = false;
|
145
|
+
this._totalMovement.set(0, 0, 0);
|
146
|
+
this._potentialDragStartEvt = args;
|
147
|
+
}
|
148
|
+
|
149
|
+
DragControls._active += 1;
|
150
|
+
|
151
|
+
const newDragHandler = new DragPointerHandler(this, this.targetObject || this.gameObject);
|
152
|
+
this._dragHandlers.set(args.event.space, newDragHandler);
|
153
|
+
|
154
|
+
// We need to turn off OrbitControls immediately, otherwise they still get data for a short moment
|
155
|
+
// and they don't properly handle being disabled while already processing data (smoothing happens when enabling again)
|
156
|
+
if (this.orbit) this.orbit.enabled = false;
|
157
|
+
|
158
|
+
newDragHandler.onDragStart(args);
|
159
|
+
|
160
|
+
if (this._dragHandlers.size === 2) {
|
161
|
+
const iterator = this._dragHandlers.values();
|
162
|
+
const a = iterator.next().value;
|
163
|
+
const b = iterator.next().value;
|
164
|
+
const mtHandler = new MultiTouchDragHandler(this, this.targetObject || this.gameObject, a, b);
|
165
|
+
this._dragHandlers.set(this.gameObject, mtHandler);
|
166
|
+
|
167
|
+
mtHandler.onDragStart(args);
|
168
|
+
}
|
169
|
+
|
170
|
+
args.use();
|
171
|
+
}
|
130
172
|
}
|
131
173
|
|
132
174
|
onPointerMove(args: PointerEventData) {
|
133
|
-
if(this._isDragging || this.
|
175
|
+
if (this._isDragging || this._potentialDragStartEvt !== null) args.use();
|
134
176
|
}
|
135
177
|
|
136
178
|
onPointerUp(args: PointerEventData) {
|
137
|
-
|
179
|
+
|
180
|
+
if(debug) Gizmos.DrawLabel(args.point ?? this.gameObject.worldPosition, "POINTERUP:" + args.pointerId + ", " + args.button, .03, 3);
|
181
|
+
|
138
182
|
if (!this.allowEdit(this.gameObject)) return;
|
139
|
-
if (
|
140
|
-
|
141
|
-
|
142
|
-
this.
|
143
|
-
|
144
|
-
if (
|
183
|
+
if (args.button !== 0) return;
|
184
|
+
this._potentialDragStartEvt = null;
|
185
|
+
|
186
|
+
const handler = this._dragHandlers.get(args.event.space);
|
187
|
+
const mtHandler = this._dragHandlers.get(this.gameObject) as MultiTouchDragHandler;
|
188
|
+
if (mtHandler && (mtHandler.handlerA === handler || mtHandler.handlerB === handler)) {
|
189
|
+
// any of the two handlers has been released, so we can remove the multi-touch handler
|
190
|
+
this._dragHandlers.delete(this.gameObject);
|
191
|
+
mtHandler.onDragEnd(args);
|
192
|
+
}
|
193
|
+
|
194
|
+
if (handler) {
|
195
|
+
if (DragControls._active > 0)
|
196
|
+
DragControls._active -= 1;
|
197
|
+
|
198
|
+
if (handler.onDragEnd) handler.onDragEnd(args);
|
199
|
+
this._dragHandlers.delete(args.event.space);
|
200
|
+
|
201
|
+
if (this._dragHandlers.size === 0) {
|
202
|
+
this.onLastDragEnd(args);
|
203
|
+
}
|
204
|
+
args.use();
|
205
|
+
}
|
206
|
+
|
207
|
+
if (DragControls._active === 0) {
|
208
|
+
if (this.orbit) this.orbit.enabled = true;
|
209
|
+
}
|
145
210
|
}
|
146
211
|
|
147
|
-
|
148
212
|
update(): void {
|
149
|
-
if (WebXR.IsInWebXR) return;
|
150
213
|
|
214
|
+
for (const handler of this._dragHandlers.values()) {
|
215
|
+
if (handler.collectMovementInfo) handler.collectMovementInfo();
|
216
|
+
// TODO this doesn't make sense, we should instead just use the max here
|
217
|
+
// or even better, each handler can decide on their own how to handle this
|
218
|
+
if (handler.getTotalMovement) this._totalMovement.add(handler.getTotalMovement());
|
219
|
+
}
|
220
|
+
|
151
221
|
// drag start only after having dragged for some pixels
|
152
|
-
if (this.
|
222
|
+
if (this._potentialDragStartEvt) {
|
153
223
|
if (!this._didDrag) {
|
154
|
-
// this is so we can e.g. process clicks without having a drag change the position
|
155
|
-
//
|
156
|
-
|
157
|
-
if (delta)
|
158
|
-
this._dragDelta.add(delta);
|
159
|
-
if (this._dragDelta.length() > 2)
|
224
|
+
// this is so we can e.g. process clicks without having a drag change the position, e.g. a click to call a method.
|
225
|
+
// TODO probably needs to be treated differently for spatial (3D motion) and screen (2D pixel motion) drags
|
226
|
+
if (this._totalMovement.length() > 0.0003)
|
160
227
|
this._didDrag = true;
|
161
228
|
else return;
|
162
229
|
}
|
163
|
-
const args = this.
|
164
|
-
this.
|
165
|
-
this.
|
230
|
+
const args = this._potentialDragStartEvt;
|
231
|
+
this._potentialDragStartEvt = null;
|
232
|
+
this.onFirstDragStart(args);
|
166
233
|
}
|
167
234
|
|
168
|
-
|
169
|
-
this.
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
this.onDragEnd(null);
|
174
|
-
}
|
235
|
+
for (const handler of this._dragHandlers.values())
|
236
|
+
if (handler.onDragUpdate) handler.onDragUpdate(this._dragHandlers.size);
|
237
|
+
|
238
|
+
if (this._dragHelper && this._dragHelper.hasSelected)
|
239
|
+
this.onAnyDragUpdate();
|
175
240
|
}
|
176
241
|
|
177
|
-
|
178
|
-
private
|
179
|
-
private _dragDelta!: Vector2;
|
180
|
-
private _didDrag: boolean = false;
|
181
|
-
private _activePointerId?: number;
|
182
|
-
|
183
|
-
private onDragStart(evt: PointerEventData) {
|
242
|
+
/** Called when the first pointer starts dragging on this object. Not called for subsequent pointers on the same object. */
|
243
|
+
private onFirstDragStart(evt: PointerEventData) {
|
184
244
|
if (!this._dragHelper) {
|
185
245
|
if (this.context.mainCamera)
|
186
|
-
this._dragHelper = new
|
246
|
+
this._dragHelper = new LegacyDragVisualsHelper(this.context.mainCamera);
|
187
247
|
else
|
188
248
|
return;
|
189
249
|
}
|
@@ -192,46 +252,17 @@
|
|
192
252
|
const dc = GameObject.getComponentInParent(evt.object, DragControls);
|
193
253
|
if (!dc || dc !== this) return;
|
194
254
|
|
255
|
+
const object = this.targetObject || this.gameObject;
|
195
256
|
|
196
|
-
|
257
|
+
if (!object) return;
|
197
258
|
|
198
|
-
if (this.transformSelf) {
|
199
|
-
object = this.gameObject;
|
200
|
-
}
|
201
|
-
|
202
|
-
// raise event
|
203
|
-
const args: { selected: Object3D, attached: Object3D | null } = { selected: object, attached: object };
|
204
|
-
for (const listener of this.selectStartEventListener) {
|
205
|
-
listener(this, args);
|
206
|
-
}
|
207
|
-
|
208
|
-
this._activePointerId = evt.pointerId;
|
209
|
-
|
210
|
-
if (!args.attached) return;
|
211
|
-
if (args.attached !== object) {
|
212
|
-
// // if duplicatable changes the object being dragged
|
213
|
-
// // should it also change the active drag controls (e.g. if it has a own one)
|
214
|
-
// const drag = GameObject.getComponentInParent(args.attached, DragControls);
|
215
|
-
// if (drag && drag !== this) {
|
216
|
-
// // incredibly ugly code to pass the drag controls event to another drag controls instance
|
217
|
-
// // This is necessary since we dont call the onPointerUp events anymore for all objects
|
218
|
-
// // that have previously received the onPointerDown event.
|
219
|
-
// // NOTE: added the EventSystem.raisedPointerDownEvents array again because of this uglyness here. The code was originally removed in 757fc5e5bafd02aa13d6cd35dd5e8729c841465a and now we're adding it in 8ce886d8344d1abd5ebb89ae3e1fb8d6d47293da
|
220
|
-
// this.onDragEnd(null);
|
221
|
-
// drag.onPointerDown(evt);
|
222
|
-
// evt.object = args.attached;
|
223
|
-
// drag.onDragStart(evt);
|
224
|
-
// return;
|
225
|
-
// }
|
226
|
-
}
|
227
|
-
object = args.attached;
|
228
259
|
this._isDragging = true;
|
229
260
|
this._dragHelper.setSelected(object, this.context);
|
230
261
|
if (this.orbit) this.orbit.enabled = false;
|
231
262
|
|
232
263
|
const sync = GameObject.getComponentInChildren(object, SyncedTransform);
|
233
|
-
if (debug)
|
234
|
-
|
264
|
+
if (debug) console.log("DRAG START", sync, object);
|
265
|
+
|
235
266
|
if (sync) {
|
236
267
|
sync.fastMode = true;
|
237
268
|
sync?.requestOwnership();
|
@@ -239,30 +270,31 @@
|
|
239
270
|
|
240
271
|
this._marker = GameObject.addNewComponent(object, UsageMarker);
|
241
272
|
|
242
|
-
// console.log(object, this._marker);
|
243
|
-
|
244
273
|
this._draggingRigidbodies.length = 0;
|
245
274
|
const rbs = GameObject.getComponentsInChildren(object, Rigidbody);
|
246
275
|
if (rbs)
|
247
276
|
this._draggingRigidbodies.push(...rbs);
|
248
|
-
|
249
|
-
const l = nameofFactory<IDragEventListener>();
|
250
|
-
GameObject.invokeOnChildren(this._dragHelper.selected, l("onDragStart"));
|
251
277
|
}
|
252
278
|
|
253
|
-
|
279
|
+
/** Called each frame as long as any pointer is dragging this object. */
|
280
|
+
private onAnyDragUpdate() {
|
254
281
|
if (!this._dragHelper) return;
|
255
282
|
this._dragHelper.showGizmo = this.showGizmo;
|
256
|
-
this._dragHelper.useViewAngle = this.useViewAngle;
|
257
283
|
|
258
284
|
this._dragHelper.onUpdate(this.context);
|
259
285
|
for (const rb of this._draggingRigidbodies) {
|
260
286
|
rb.wakeUp();
|
261
287
|
rb.resetVelocities();
|
288
|
+
rb.resetForcesAndTorques();
|
262
289
|
}
|
290
|
+
|
291
|
+
const object = this.targetObject || this.gameObject;
|
292
|
+
|
293
|
+
InstancingUtil.markDirty(object);
|
263
294
|
}
|
264
295
|
|
265
|
-
|
296
|
+
/** Called when the last pointer has been removed from this object. */
|
297
|
+
private onLastDragEnd(evt: PointerEventData | null) {
|
266
298
|
if (!this || !this._isDragging) return;
|
267
299
|
this._isDragging = false;
|
268
300
|
if (!this._dragHelper) return;
|
@@ -271,8 +303,7 @@
|
|
271
303
|
}
|
272
304
|
this._draggingRigidbodies.length = 0;
|
273
305
|
const selected = this._dragHelper.selected;
|
274
|
-
if (debug)
|
275
|
-
console.log("DRAG END", selected, selected?.visible)
|
306
|
+
if (debug) console.log("DRAG END", selected, selected?.visible)
|
276
307
|
this._dragHelper.setSelected(null, this.context);
|
277
308
|
if (this.orbit) this.orbit.enabled = true;
|
278
309
|
if (evt?.object) {
|
@@ -282,23 +313,761 @@
|
|
282
313
|
// sync?.requestOwnership();
|
283
314
|
}
|
284
315
|
}
|
285
|
-
if (this._marker)
|
316
|
+
if (this._marker)
|
286
317
|
this._marker.destroy();
|
318
|
+
}
|
319
|
+
}
|
320
|
+
|
321
|
+
/** Handles two touch points affecting one object. Allows movement, scale and rotation of objects. */
|
322
|
+
class MultiTouchDragHandler implements IDragHandler {
|
323
|
+
|
324
|
+
handlerA: DragPointerHandler;
|
325
|
+
handlerB: DragPointerHandler;
|
326
|
+
|
327
|
+
private context: Context;
|
328
|
+
private settings: DragControls;
|
329
|
+
private gameObject: GameObject;
|
330
|
+
private _handlerAAttachmentPoint: Vector3 = new Vector3();
|
331
|
+
private _handlerBAttachmentPoint: Vector3 = new Vector3();
|
332
|
+
|
333
|
+
private _followObject: GameObject;
|
334
|
+
private _manipulatorObject: GameObject;
|
335
|
+
private _deviceMode!: XRTargetRayMode;
|
336
|
+
private _followObjectStartWorldQuaternion: Quaternion = new Quaternion();
|
337
|
+
|
338
|
+
constructor(dragControls: DragControls, gameObject: GameObject, pointerA: DragPointerHandler, pointerB: DragPointerHandler) {
|
339
|
+
this.context = dragControls.context;
|
340
|
+
this.settings = dragControls;
|
341
|
+
this.gameObject = gameObject;
|
342
|
+
this.handlerA = pointerA;
|
343
|
+
this.handlerB = pointerB;
|
344
|
+
|
345
|
+
this._followObject = new Object3D() as GameObject;
|
346
|
+
this._manipulatorObject = new Object3D() as GameObject;
|
347
|
+
|
348
|
+
this.context.scene.add(this._manipulatorObject);
|
349
|
+
|
350
|
+
const rig = NeedleXRSession.active?.rig?.gameObject;
|
351
|
+
|
352
|
+
if (!this.handlerA || !this.handlerB || !this.handlerA.hitPointInLocalSpace || !this.handlerB.hitPointInLocalSpace) {
|
353
|
+
console.error("Invalid: MultiTouchDragHandler needs two valid DragPointerHandlers with hitPointInLocalSpace set.");
|
354
|
+
return;
|
287
355
|
}
|
288
|
-
|
289
|
-
|
290
|
-
|
356
|
+
|
357
|
+
this._tempVec1.copy(this.handlerA.hitPointInLocalSpace);
|
358
|
+
this._tempVec2.copy(this.handlerB.hitPointInLocalSpace);
|
359
|
+
this.gameObject.localToWorld(this._tempVec1);
|
360
|
+
this.gameObject.localToWorld(this._tempVec2);
|
361
|
+
if (rig) {
|
362
|
+
rig.worldToLocal(this._tempVec1);
|
363
|
+
rig.worldToLocal(this._tempVec2);
|
291
364
|
}
|
365
|
+
this._initialDistance = this._tempVec1.distanceTo(this._tempVec2);
|
366
|
+
|
367
|
+
if (this._initialDistance < 0.02) {
|
368
|
+
if (debug) {
|
369
|
+
console.log("Finding alternative drag attachment points since initial distance is too low: " + this._initialDistance.toFixed(2));
|
370
|
+
}
|
371
|
+
// We want two reasonable pointer attachment points here.
|
372
|
+
// But if the hitPointInLocalSpace are very close to each other, we instead fall back to controller positions.
|
373
|
+
this.handlerA.followObject.parent!.getWorldPosition(this._tempVec1);
|
374
|
+
this.handlerB.followObject.parent!.getWorldPosition(this._tempVec2);
|
375
|
+
this._handlerAAttachmentPoint.copy(this._tempVec1);
|
376
|
+
this._handlerBAttachmentPoint.copy(this._tempVec2);
|
377
|
+
this.gameObject.worldToLocal(this._handlerAAttachmentPoint);
|
378
|
+
this.gameObject.worldToLocal(this._handlerBAttachmentPoint);
|
379
|
+
this._initialDistance = this._tempVec1.distanceTo(this._tempVec2);
|
292
380
|
|
293
|
-
|
294
|
-
|
381
|
+
if (this._initialDistance < 0.001) {
|
382
|
+
console.warn("Not supported right now – controller drag points for multitouch are too close!");
|
383
|
+
this._initialDistance = 1;
|
384
|
+
}
|
385
|
+
}
|
386
|
+
else {
|
387
|
+
this._handlerAAttachmentPoint.copy(this.handlerA.hitPointInLocalSpace);
|
388
|
+
this._handlerBAttachmentPoint.copy(this.handlerB.hitPointInLocalSpace);
|
389
|
+
}
|
390
|
+
|
391
|
+
this._tempVec3.lerpVectors(this._tempVec1, this._tempVec2, 0.5);
|
392
|
+
this._initialScale.copy(gameObject.scale);
|
393
|
+
|
394
|
+
if (debug) {
|
395
|
+
this._followObject.add(new AxesHelper(2));
|
396
|
+
this._manipulatorObject.add(new AxesHelper(5));
|
397
|
+
|
398
|
+
const formatVec = (v: Vector3) => `${v.x.toFixed(2)}, ${v.y.toFixed(2)}, ${v.z.toFixed(2)}`;
|
399
|
+
Gizmos.DrawLine(this._tempVec1, this._tempVec2, 0x00ffff, 0, false);
|
400
|
+
Gizmos.DrawLabel(this._tempVec3, "A:B " + this._initialDistance.toFixed(2) + "\n" + formatVec(this._tempVec1) + "\n" + formatVec(this._tempVec2), 0.03, 5);
|
401
|
+
}
|
295
402
|
}
|
403
|
+
|
404
|
+
onDragStart(_args: PointerEventData): void {
|
405
|
+
// align _followObject with the object we want to drag
|
406
|
+
this.gameObject.add(this._followObject);
|
407
|
+
this._followObject.matrixAutoUpdate = false;
|
408
|
+
this._followObject.matrix.identity();
|
409
|
+
this._deviceMode = _args.mode;
|
410
|
+
this._followObjectStartWorldQuaternion.copy(this._followObject.worldQuaternion);
|
411
|
+
|
412
|
+
// align _manipulatorObject in the same way it would if this was a drag update
|
413
|
+
this.alignManipulator();
|
414
|
+
|
415
|
+
// and then parent it to the space object so it follows along.
|
416
|
+
this._manipulatorObject.attach(this._followObject);
|
417
|
+
|
418
|
+
// store offsets in local space
|
419
|
+
this._manipulatorPosOffset.copy(this._followObject.position);
|
420
|
+
this._manipulatorRotOffset.copy(this._followObject.quaternion);
|
421
|
+
this._manipulatorScaleOffset.copy(this._followObject.scale);
|
422
|
+
}
|
423
|
+
|
424
|
+
onDragEnd(_args: PointerEventData): void {
|
425
|
+
if (!this.handlerA || !this.handlerB) {
|
426
|
+
console.error("onDragEnd called on MultiTouchDragHandler without valid handlers. This is likely a bug.");
|
427
|
+
return;
|
428
|
+
}
|
429
|
+
|
430
|
+
// we want to initialize the drag points for these handlers again.
|
431
|
+
// one of them will be removed, but we don't know here which one
|
432
|
+
this.handlerA.recenter();
|
433
|
+
this.handlerB.recenter();
|
434
|
+
|
435
|
+
// destroy helper objects
|
436
|
+
this._manipulatorObject.removeFromParent();
|
437
|
+
this._followObject.removeFromParent();
|
438
|
+
this._manipulatorObject.destroy();
|
439
|
+
this._followObject.destroy();
|
440
|
+
}
|
441
|
+
|
442
|
+
private _manipulatorPosOffset: Vector3 = new Vector3();
|
443
|
+
private _manipulatorRotOffset: Quaternion = new Quaternion();
|
444
|
+
private _manipulatorScaleOffset: Vector3 = new Vector3();
|
445
|
+
|
446
|
+
private _tempVec1: Vector3 = new Vector3();
|
447
|
+
private _tempVec2: Vector3 = new Vector3();
|
448
|
+
private _tempVec3: Vector3 = new Vector3();
|
449
|
+
private tempLookMatrix: Matrix4 = new Matrix4();
|
450
|
+
private _initialScale: Vector3 = new Vector3();
|
451
|
+
private _initialDistance: number = 0;
|
452
|
+
|
453
|
+
private alignManipulator() {
|
454
|
+
if (!this.handlerA || !this.handlerB) {
|
455
|
+
console.error("alignManipulator called on MultiTouchDragHandler without valid handlers. This is likely a bug.", this);
|
456
|
+
return;
|
457
|
+
}
|
458
|
+
|
459
|
+
if (!this.handlerA.followObject || !this.handlerB.followObject) {
|
460
|
+
console.error("alignManipulator called on MultiTouchDragHandler without valid follow objects. This is likely a bug.", this.handlerA, this.handlerB);
|
461
|
+
return;
|
462
|
+
}
|
463
|
+
|
464
|
+
this._tempVec1.copy(this._handlerAAttachmentPoint);
|
465
|
+
this._tempVec2.copy(this._handlerBAttachmentPoint);
|
466
|
+
this.handlerA.followObject.localToWorld(this._tempVec1);
|
467
|
+
this.handlerB.followObject.localToWorld(this._tempVec2);
|
468
|
+
this._tempVec3.lerpVectors(this._tempVec1, this._tempVec2, 0.5);
|
469
|
+
|
470
|
+
this._manipulatorObject.position.copy(this._tempVec3);
|
471
|
+
|
472
|
+
// - lookAt the second point on handlerB
|
473
|
+
const camera = this.context.mainCamera;
|
474
|
+
this.tempLookMatrix.lookAt(this._tempVec3, this._tempVec2, (camera as any as IGameObject).worldUp);
|
475
|
+
this._manipulatorObject.quaternion.setFromRotationMatrix(this.tempLookMatrix);
|
476
|
+
|
477
|
+
// - scale based on the distance between the two points
|
478
|
+
const dist = this._tempVec1.distanceTo(this._tempVec2);
|
479
|
+
this._manipulatorObject.scale.copy(this._initialScale).multiplyScalar(dist / this._initialDistance);
|
480
|
+
|
481
|
+
this._manipulatorObject.updateMatrix();
|
482
|
+
this._manipulatorObject.updateMatrixWorld(true);
|
483
|
+
|
484
|
+
if (debug) {
|
485
|
+
Gizmos.DrawLabel(this._tempVec3.clone().add(new Vector3(0,0.2,0)), "A:B " + dist.toFixed(2), 0.03);
|
486
|
+
Gizmos.DrawLine(this._tempVec1, this._tempVec2, 0x00ff00, 0, false);
|
487
|
+
|
488
|
+
// const wp = this._manipulatorObject.worldPosition;
|
489
|
+
// Gizmos.DrawWireSphere(wp, this._initialScale.length() * dist / this._initialDistance, 0x00ff00, 0, false);
|
490
|
+
}
|
491
|
+
}
|
492
|
+
|
493
|
+
onDragUpdate() {
|
494
|
+
// At this point we've run both the other handlers, but their effects have been suppressed because they can't handle
|
495
|
+
// two events at the same time. They're basically providing us with two Object3D's and we can combine these here
|
496
|
+
// into a reasonable two-handed translation/rotation/scale.
|
497
|
+
// One approach:
|
498
|
+
// - position our control object on the center between the two pointer control objects
|
499
|
+
|
500
|
+
// TODO close grab needs to be handled differently because there we don't have a hit point -
|
501
|
+
// Hit point is just the center of the object
|
502
|
+
// So probably we should fix that close grab has a better hit point approximation (point on bounds?)
|
503
|
+
|
504
|
+
this.alignManipulator();
|
505
|
+
|
506
|
+
// apply (smoothed) to the gameObject
|
507
|
+
const lerpStrength = 30;
|
508
|
+
const lerpFactor = 1.0;
|
509
|
+
|
510
|
+
this._followObject.position.copy(this._manipulatorPosOffset);
|
511
|
+
this._followObject.quaternion.copy(this._manipulatorRotOffset);
|
512
|
+
this._followObject.scale.copy(this._manipulatorScaleOffset);
|
513
|
+
|
514
|
+
const draggedObject = this.gameObject;
|
515
|
+
const targetObject = this._followObject;
|
516
|
+
|
517
|
+
targetObject.updateMatrix();
|
518
|
+
targetObject.updateMatrixWorld(true);
|
519
|
+
|
520
|
+
const isSpatialInput = this._deviceMode === "tracked-pointer";
|
521
|
+
const keepRotation = isSpatialInput ? this.settings.xrKeepRotation : this.settings.keepRotation;
|
522
|
+
|
523
|
+
// TODO refactor to a common place
|
524
|
+
// apply constraints (position grid snap, rotation, ...)
|
525
|
+
if (this.settings.snapGridResolution > 0) {
|
526
|
+
const wp = this._followObject.worldPosition;
|
527
|
+
const snap = this.settings.snapGridResolution;
|
528
|
+
wp.x = Math.round(wp.x / snap) * snap;
|
529
|
+
wp.y = Math.round(wp.y / snap) * snap;
|
530
|
+
wp.z = Math.round(wp.z / snap) * snap;
|
531
|
+
this._followObject.worldPosition = wp;
|
532
|
+
this._followObject.updateMatrix();
|
533
|
+
}
|
534
|
+
if (keepRotation) {
|
535
|
+
this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
|
536
|
+
this._followObject.updateMatrix();
|
537
|
+
}
|
538
|
+
|
539
|
+
// TODO refactor to a common place
|
540
|
+
// TODO should use unscaled time here // some test for lerp speed depending on distance
|
541
|
+
const t = Mathf.clamp01(this.context.time.deltaTime * lerpStrength * lerpFactor);// / (currentDist - 1 + 0.01));
|
542
|
+
|
543
|
+
const wp = draggedObject.worldPosition;
|
544
|
+
wp.lerp(targetObject.worldPosition, t);
|
545
|
+
draggedObject.worldPosition = wp;
|
546
|
+
|
547
|
+
const rot = draggedObject.worldQuaternion;
|
548
|
+
rot.slerp(targetObject.worldQuaternion, t);
|
549
|
+
draggedObject.worldQuaternion = rot;
|
550
|
+
|
551
|
+
const scl = draggedObject.worldScale;
|
552
|
+
scl.lerp(targetObject.worldScale, t);
|
553
|
+
draggedObject.worldScale = scl;
|
554
|
+
}
|
555
|
+
|
556
|
+
setTargetObject(obj: Object3D | null): void {
|
557
|
+
this.gameObject = obj as GameObject;
|
558
|
+
}
|
296
559
|
}
|
297
560
|
|
561
|
+
/** Common interface for pointer handlers (single touch and multi touch) */
|
562
|
+
interface IDragHandler {
|
563
|
+
/** Used to determine if a drag has happened for this handler */
|
564
|
+
getTotalMovement?(): Vector3;
|
565
|
+
/** Target object can change mid-flight (e.g. in Duplicatable), handlers should react properly to that */
|
566
|
+
setTargetObject(obj: Object3D | null): void;
|
567
|
+
|
568
|
+
/** Prewarms the drag – can already move internal points around here but should not move the object itself */
|
569
|
+
collectMovementInfo?(): void;
|
570
|
+
onDragStart?(args: PointerEventData): void;
|
571
|
+
onDragEnd?(args: PointerEventData): void;
|
572
|
+
/** The target object is moved around */
|
573
|
+
onDragUpdate?(numberOfPointers: number): void;
|
574
|
+
}
|
298
575
|
|
576
|
+
/** Handles a single pointer on an object. DragPointerHandlers are created on pointer enter,
|
577
|
+
* help with determining if there's actually a drag, and then perform operations based on spatial pointer data.
|
578
|
+
*/
|
579
|
+
class DragPointerHandler implements IDragHandler {
|
299
580
|
|
300
|
-
|
581
|
+
/** Absolute movement of the pointer. Used for determining if a motion/drag is happening.
|
582
|
+
* This is in world units, so very small for screens (near-plane space change) */
|
583
|
+
getTotalMovement(): Vector3 { return this._totalMovement; }
|
584
|
+
get followObject(): GameObject { return this._followObject; }
|
585
|
+
get hitPointInLocalSpace(): Vector3 { return this._hitPointInLocalSpace; }
|
301
586
|
|
587
|
+
private context: Context;
|
588
|
+
private gameObject: GameObject;
|
589
|
+
private settings: DragControls;
|
590
|
+
private _lastRig: IGameObject | undefined = undefined;
|
591
|
+
|
592
|
+
/** This object is placed at the pivot of the dragged object, and parented to the control space. */
|
593
|
+
private _followObject: GameObject;
|
594
|
+
private _totalMovement: Vector3 = new Vector3();
|
595
|
+
/** Motion along the pointer ray. On screens this doesn't change. In XR it can be used to determine how much
|
596
|
+
* effort someone is putting into moving an object closer or further away. */
|
597
|
+
private _totalMovementAlongRayDirection: number = 0;
|
598
|
+
/** Distance between _followObject and its parent at grab start, in local space */
|
599
|
+
private _grabStartDistance: number = 0;
|
600
|
+
private _deviceMode!: XRTargetRayMode;
|
601
|
+
private _followObjectStartPosition: Vector3 = new Vector3();
|
602
|
+
private _followObjectStartQuaternion: Quaternion = new Quaternion();
|
603
|
+
private _followObjectStartWorldQuaternion: Quaternion = new Quaternion();
|
604
|
+
private _lastDragPosRigSpace: Vector3 | undefined;
|
605
|
+
private _tempVec: Vector3 = new Vector3();
|
606
|
+
private _tempMat: Matrix4 = new Matrix4();
|
607
|
+
|
608
|
+
private _hitPointInLocalSpace: Vector3 = new Vector3();
|
609
|
+
private _hitNormalInLocalSpace: Vector3 = new Vector3();
|
610
|
+
private _bottomCenter = new Vector3();
|
611
|
+
private _backCenter = new Vector3();
|
612
|
+
private _backBottomCenter = new Vector3();
|
613
|
+
private _bounds = new Box3();
|
614
|
+
private _dragPlane = new Plane(new Vector3(0, 1, 0));
|
615
|
+
private _draggedOverObject: Object3D | null = null;
|
616
|
+
private _draggedOverObjectLastSetUp: Object3D | null = null;
|
617
|
+
private _draggedOverObjectLastNormal: Vector3 = new Vector3();
|
618
|
+
private _draggedOverObjectDuration: number = 0;
|
619
|
+
|
620
|
+
/** Allows overriding which object is dragged while a drag is already ongoing. Used for example by Duplicatable */
|
621
|
+
setTargetObject(obj: Object3D | null) {
|
622
|
+
this.gameObject = obj as GameObject;
|
623
|
+
}
|
624
|
+
|
625
|
+
constructor(dragControls: DragControls, gameObject: GameObject) {
|
626
|
+
this.settings = dragControls;
|
627
|
+
this.context = dragControls.context;
|
628
|
+
this.gameObject = gameObject;
|
629
|
+
this._followObject = new Object3D() as GameObject;
|
630
|
+
}
|
631
|
+
|
632
|
+
recenter() {
|
633
|
+
if (!this._followObject.parent) {
|
634
|
+
console.warn("Error: space follow object doesn't have parent but recenter() is called. This is likely a bug");
|
635
|
+
return;
|
636
|
+
}
|
637
|
+
|
638
|
+
const p = this._followObject.parent as GameObject;
|
639
|
+
|
640
|
+
this.gameObject.add(this._followObject);
|
641
|
+
this._followObject.matrixAutoUpdate = false;
|
642
|
+
|
643
|
+
this._followObject.position.set(0, 0, 0);
|
644
|
+
this._followObject.quaternion.set(0, 0, 0, 1);
|
645
|
+
this._followObject.scale.set(1, 1, 1);
|
646
|
+
|
647
|
+
this._followObject.updateMatrix();
|
648
|
+
this._followObject.updateMatrixWorld(true);
|
649
|
+
|
650
|
+
p.attach(this._followObject);
|
651
|
+
|
652
|
+
this._followObjectStartPosition.copy(this._followObject.position);
|
653
|
+
this._followObjectStartQuaternion.copy(this._followObject.quaternion);
|
654
|
+
this._followObjectStartWorldQuaternion.copy(this._followObject.worldQuaternion);
|
655
|
+
|
656
|
+
this._followObject.updateMatrix();
|
657
|
+
this._followObject.updateMatrixWorld(true);
|
658
|
+
|
659
|
+
const hitPointWP = this._hitPointInLocalSpace.clone();
|
660
|
+
this.gameObject.localToWorld(hitPointWP);
|
661
|
+
this._grabStartDistance = hitPointWP.distanceTo(p.worldPosition);
|
662
|
+
const rig = NeedleXRSession.active?.rig?.gameObject;
|
663
|
+
const rigScale = rig?.worldScale.x || 1;
|
664
|
+
this._grabStartDistance /= rigScale;
|
665
|
+
|
666
|
+
this._totalMovementAlongRayDirection = 0;
|
667
|
+
this._lastDragPosRigSpace = undefined;
|
668
|
+
|
669
|
+
if (debug)
|
670
|
+
{
|
671
|
+
Gizmos.DrawLine(hitPointWP, p.worldPosition, 0x00ff00, 0.5, false);
|
672
|
+
Gizmos.DrawLabel(p.worldPosition.add(new Vector3(0,0.1,0)), this._grabStartDistance.toFixed(2), 0.03, 0.5);
|
673
|
+
}
|
674
|
+
}
|
675
|
+
|
676
|
+
onDragStart(args: PointerEventData) {
|
677
|
+
|
678
|
+
args.event.space.add(this._followObject);
|
679
|
+
|
680
|
+
// prepare for drag, we will start dragging after an object has been dragged for a few centimeters
|
681
|
+
this._lastDragPosRigSpace = undefined;
|
682
|
+
|
683
|
+
if (args.point && args.normal) {
|
684
|
+
this._hitPointInLocalSpace.copy(args.point);
|
685
|
+
this.gameObject.worldToLocal(this._hitPointInLocalSpace);
|
686
|
+
this._hitNormalInLocalSpace.copy(args.normal);
|
687
|
+
}
|
688
|
+
else if (args) {
|
689
|
+
// can happen for e.g. close grabs; we can assume/guess a good hit point and normal based on the object's bounds or so
|
690
|
+
// convert controller world position to local space instead and use that as hit point
|
691
|
+
const controller = args.event.space as GameObject;
|
692
|
+
const controllerWp = controller.worldPosition;
|
693
|
+
this.gameObject.worldToLocal(controllerWp);
|
694
|
+
this._hitPointInLocalSpace.copy(controllerWp);
|
695
|
+
|
696
|
+
const controllerUp = controller.worldUp;
|
697
|
+
this._tempMat.copy(this.gameObject.matrixWorld).invert();
|
698
|
+
controllerUp.transformDirection(this._tempMat);
|
699
|
+
this._hitNormalInLocalSpace.copy(controllerUp);
|
700
|
+
}
|
701
|
+
|
702
|
+
this.recenter();
|
703
|
+
|
704
|
+
this._totalMovement.set(0, 0, 0);
|
705
|
+
this._deviceMode = args.mode;
|
706
|
+
|
707
|
+
|
708
|
+
const dragSource = this._followObject.parent as IGameObject;
|
709
|
+
const rayDirection = dragSource.worldForward;
|
710
|
+
|
711
|
+
const isSpatialInput = this._deviceMode === "tracked-pointer";
|
712
|
+
const dragMode = isSpatialInput ? this.settings.xrDragMode : this.settings.dragMode;
|
713
|
+
|
714
|
+
// set up drag plane; we don't really know the normal yet but we can already set the point
|
715
|
+
const hitWP = this._hitPointInLocalSpace.clone();
|
716
|
+
this.gameObject.localToWorld(hitWP);
|
717
|
+
|
718
|
+
switch (dragMode) {
|
719
|
+
case DragMode.XZPlane:
|
720
|
+
const up = new Vector3(0,1,0);
|
721
|
+
if (this.gameObject.parent) {
|
722
|
+
// TODO in this case _dragPlane should be in parent space, not world space,
|
723
|
+
// otherwise dragging the parent and this object at the same time doesn't keep the plane constrained
|
724
|
+
up.transformDirection(this.gameObject.parent.matrixWorld.clone().invert());
|
725
|
+
}
|
726
|
+
this._dragPlane.setFromNormalAndCoplanarPoint(up, hitWP);
|
727
|
+
break;
|
728
|
+
case DragMode.HitNormal:
|
729
|
+
const hitNormal = this._hitNormalInLocalSpace.clone();
|
730
|
+
hitNormal.transformDirection(this.gameObject.matrixWorld);
|
731
|
+
this._dragPlane.setFromNormalAndCoplanarPoint(hitNormal, hitWP);
|
732
|
+
break;
|
733
|
+
case DragMode.Attached:
|
734
|
+
this._dragPlane.setFromNormalAndCoplanarPoint(rayDirection, hitWP);
|
735
|
+
break;
|
736
|
+
case DragMode.DynamicViewAngle:
|
737
|
+
const v0 = new Vector3(0, 1, 0);
|
738
|
+
const v1 = rayDirection;
|
739
|
+
const angle = v0.angleTo(v1);
|
740
|
+
const angleThreshold = 0.5;
|
741
|
+
if (angle > Math.PI / 2 + angleThreshold || angle < Math.PI / 2 - angleThreshold)
|
742
|
+
this._dragPlane.setFromNormalAndCoplanarPoint(new Vector3(0, 1, 0), hitWP);
|
743
|
+
else
|
744
|
+
this._dragPlane.setFromNormalAndCoplanarPoint(rayDirection, hitWP);
|
745
|
+
break;
|
746
|
+
}
|
747
|
+
|
748
|
+
// calculate bounding box and snapping points. We want to either snap the "back" point or the "bottom" point.
|
749
|
+
const bbox = new Box3();
|
750
|
+
const p = this.gameObject.parent;
|
751
|
+
const localP = this.gameObject.position.clone();
|
752
|
+
const localQ = this.gameObject.quaternion.clone();
|
753
|
+
const localS = this.gameObject.scale.clone();
|
754
|
+
if (p) p.remove(this.gameObject);
|
755
|
+
this.gameObject.position.set(0, 0, 0);
|
756
|
+
this.gameObject.quaternion.set(0, 0, 0, 1);
|
757
|
+
this.gameObject.scale.set(1, 1, 1);
|
758
|
+
bbox.setFromObject(this.gameObject);
|
759
|
+
|
760
|
+
// get front center point of the bbox. basically (0, 0, 1) in local space
|
761
|
+
const bboxCenter = new Vector3();
|
762
|
+
bbox.getCenter(bboxCenter);
|
763
|
+
const bboxSize = new Vector3();
|
764
|
+
bbox.getSize(bboxSize);
|
765
|
+
|
766
|
+
// attachment points for dragging
|
767
|
+
this._bottomCenter.copy(bboxCenter.clone().add(new Vector3(0, -bboxSize.y / 2, 0)));
|
768
|
+
this._backCenter.copy(bboxCenter.clone().add(new Vector3(0, 0, bboxSize.z / 2)));
|
769
|
+
this._backBottomCenter.copy(bboxCenter.clone().add(new Vector3(0, -bboxSize.y / 2, bboxSize.z / 2)));
|
770
|
+
|
771
|
+
this._bounds.copy(bbox);
|
772
|
+
|
773
|
+
// restore original transform
|
774
|
+
if (p) p.add(this.gameObject);
|
775
|
+
this.gameObject.position.copy(localP);
|
776
|
+
this.gameObject.quaternion.copy(localQ);
|
777
|
+
this.gameObject.scale.copy(localS);
|
778
|
+
|
779
|
+
// surface snapping
|
780
|
+
this._draggedOverObject = null;
|
781
|
+
this._draggedOverObjectLastSetUp = null;
|
782
|
+
this._draggedOverObjectLastNormal.set(0, 1, 0);
|
783
|
+
this._draggedOverObjectDuration = 0;
|
784
|
+
}
|
785
|
+
|
786
|
+
collectMovementInfo() {
|
787
|
+
// we're dragging - there is a controlling object
|
788
|
+
if (!this._followObject.parent) return;
|
789
|
+
|
790
|
+
// TODO This should all be handled properly per-pointer
|
791
|
+
// and we want to have a chance to react to multiple pointers being on the same object.
|
792
|
+
// some common stuff (calculating of movement offsets, etc) could be done by default
|
793
|
+
// and then the main thing to override is the actual movement of the object based on N _followObjects
|
794
|
+
|
795
|
+
const dragSource = this._followObject.parent as IGameObject;
|
796
|
+
|
797
|
+
// modify _followObject with constraints, e.g.
|
798
|
+
// - dragging on a plane, e.g. the floor (keeping the distance to the floor plane constant)
|
799
|
+
/* TODO fix jump on drag start
|
800
|
+
const p0 = this._followObject.parent as GameObject;
|
801
|
+
const ray = new Ray(p0.worldPosition, p0.worldForward.multiplyScalar(-1));
|
802
|
+
const p = new Vector3();
|
803
|
+
const t0 = ray.intersectPlane(new Plane(new Vector3(0, 1, 0)), p);
|
804
|
+
if (t0 !== null)
|
805
|
+
this._followObject.worldPosition = t0;
|
806
|
+
*/
|
807
|
+
|
808
|
+
this._followObject.updateMatrix();
|
809
|
+
const dragPosRigSpace = dragSource.worldPosition;
|
810
|
+
const rig = NeedleXRSession.active?.rig?.gameObject;
|
811
|
+
if (rig)
|
812
|
+
rig.worldToLocal(dragPosRigSpace);
|
813
|
+
|
814
|
+
// sum up delta
|
815
|
+
// TODO We need to do all/most of these calculations in Rig Space instead of world space
|
816
|
+
// moving the rig while holding an object should not affect _rayDelta / _dragDelta
|
817
|
+
if (this._lastDragPosRigSpace === undefined || rig != this._lastRig) {
|
818
|
+
this._lastDragPosRigSpace = dragPosRigSpace.clone();
|
819
|
+
this._lastRig = rig;
|
820
|
+
}
|
821
|
+
this._tempVec.copy(dragPosRigSpace).sub(this._lastDragPosRigSpace);
|
822
|
+
|
823
|
+
const rayDirectionRigSpace = dragSource.worldForward;
|
824
|
+
if (rig) {
|
825
|
+
this._tempMat.copy(rig.matrixWorld).invert();
|
826
|
+
rayDirectionRigSpace.transformDirection(this._tempMat);
|
827
|
+
}
|
828
|
+
// sum up delta movement along ray
|
829
|
+
this._totalMovementAlongRayDirection += rayDirectionRigSpace.dot(this._tempVec);
|
830
|
+
this._tempVec.x = Math.abs(this._tempVec.x);
|
831
|
+
this._tempVec.y = Math.abs(this._tempVec.y);
|
832
|
+
this._tempVec.z = Math.abs(this._tempVec.z);
|
833
|
+
|
834
|
+
// sum up absolute total movement
|
835
|
+
this._totalMovement.add(this._tempVec);
|
836
|
+
this._lastDragPosRigSpace.copy(dragPosRigSpace);
|
837
|
+
|
838
|
+
if (debug) {
|
839
|
+
let wp = dragPosRigSpace;
|
840
|
+
// ray direction of the input source object
|
841
|
+
if (rig) {
|
842
|
+
wp = wp.clone();
|
843
|
+
wp.transformDirection(rig.matrixWorld);
|
844
|
+
}
|
845
|
+
Gizmos.DrawRay(wp, rayDirectionRigSpace, 0x0000ff);
|
846
|
+
}
|
847
|
+
}
|
848
|
+
|
849
|
+
onDragUpdate(numberOfPointers: number) {
|
850
|
+
|
851
|
+
// can only handle a single pointer
|
852
|
+
// if there's more, we defer to multi-touch drag handlers
|
853
|
+
if (numberOfPointers > 1) return;
|
854
|
+
|
855
|
+
const draggedObject = this.gameObject as IGameObject;
|
856
|
+
const dragSource = this._followObject.parent as IGameObject;
|
857
|
+
this._followObject.updateMatrix();
|
858
|
+
const dragSourceWP = dragSource.worldPosition;
|
859
|
+
const rayDirection = dragSource.worldForward;
|
860
|
+
|
861
|
+
|
862
|
+
// Actually move and rotate draggedObject
|
863
|
+
const isSpatialInput = this._deviceMode === "tracked-pointer";
|
864
|
+
const keepRotation = isSpatialInput ? this.settings.xrKeepRotation : this.settings.keepRotation;
|
865
|
+
const dragMode = isSpatialInput ? this.settings.xrDragMode : this.settings.dragMode;
|
866
|
+
|
867
|
+
const lerpStrength = 10;
|
868
|
+
// - keeping rotation constant during dragging
|
869
|
+
if (keepRotation) this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
|
870
|
+
this._followObject.updateMatrix();
|
871
|
+
this._followObject.updateMatrixWorld(true);
|
872
|
+
|
873
|
+
// Acceleration for moving the object - move followObject along the ray distance by _totalMovementAlongRayDirection
|
874
|
+
let currentDist = 1.0;
|
875
|
+
let lerpFactor = 1.0;
|
876
|
+
if (this._deviceMode === "tracked-pointer" && this._grabStartDistance > 0.5) // hands and controllers, but not touches
|
877
|
+
{
|
878
|
+
const factor = 1 + this._totalMovementAlongRayDirection * (2 * this.settings.xrDistanceDragFactor);
|
879
|
+
currentDist = Math.max(0.0, factor);
|
880
|
+
currentDist = currentDist * currentDist * currentDist;
|
881
|
+
}
|
882
|
+
else if (this._grabStartDistance <= 0.5)
|
883
|
+
{
|
884
|
+
// TODO there's still a frame delay between dragged objects and the hand models
|
885
|
+
lerpFactor = 3.0;
|
886
|
+
}
|
887
|
+
|
888
|
+
// reset _followObject to its original position and rotation
|
889
|
+
this._followObject.position.copy(this._followObjectStartPosition);
|
890
|
+
if (!keepRotation)
|
891
|
+
this._followObject.quaternion.copy(this._followObjectStartQuaternion);
|
892
|
+
|
893
|
+
// TODO restore previous functionality:
|
894
|
+
// When distance dragging, the HIT POINT should move along the ray until it reaches the controller;
|
895
|
+
// NOT the pivot point of the dragged object. E.g. grabbing a large cube and pulling towards you should at most
|
896
|
+
// move the grabbed point to your head and not slap the cube in your head.
|
897
|
+
this._followObject.position.multiplyScalar(currentDist);
|
898
|
+
this._followObject.updateMatrix();
|
899
|
+
|
900
|
+
const ray = new Ray(dragSourceWP, rayDirection);
|
901
|
+
|
902
|
+
// Surface snapping.
|
903
|
+
// Feels quite weird in VR right now!
|
904
|
+
if (dragMode == DragMode.SnapToSurfaces) {
|
905
|
+
// Idea: Do a sphere cast if we're still in the proximity of the current draggedObject.
|
906
|
+
// This would allow dragging slightly out of the object's bounds and still continue snapping to it.
|
907
|
+
// Do a regular raycast (without the dragged object) to determine if we should change what is dragged onto.
|
908
|
+
const opts = new RaycastOptions();
|
909
|
+
opts.ignore = [draggedObject];
|
910
|
+
const hits = this.context.physics.raycastFromRay(ray, opts);
|
911
|
+
|
912
|
+
if (hits.length > 0) {
|
913
|
+
const hit = hits[0];
|
914
|
+
// if we're above the same surface for a specified time, adjust drag options:
|
915
|
+
// - set that surface as the drag "plane". We will follow that object's surface instead now (raycast onto only that)
|
916
|
+
// - if the drag plane is an object, we also want to
|
917
|
+
// - calculate an initial rotation offset matching what surface/face the user originally started the drag on
|
918
|
+
// - rotate the dragged object to match the surface normal
|
919
|
+
if (this._draggedOverObject === hit.object)
|
920
|
+
this._draggedOverObjectDuration += this.context.time.deltaTime;
|
921
|
+
else {
|
922
|
+
this._draggedOverObject = hit.object;
|
923
|
+
this._draggedOverObjectDuration = 0;
|
924
|
+
}
|
925
|
+
|
926
|
+
if (hit.face) {
|
927
|
+
// Adjust drag plane if we're dragging over a different object (for a certain amount of time)
|
928
|
+
// or if the surface normal changed
|
929
|
+
if (this._draggedOverObjectDuration > 0.15 &&
|
930
|
+
(this._draggedOverObjectLastSetUp !== this._draggedOverObject ||
|
931
|
+
this._draggedOverObjectLastNormal.dot(hit.face.normal) < 0.999999)
|
932
|
+
) {
|
933
|
+
this._draggedOverObjectLastSetUp = this._draggedOverObject;
|
934
|
+
this._draggedOverObjectLastNormal.copy(hit.face.normal);
|
935
|
+
|
936
|
+
const center = new Vector3();
|
937
|
+
const size = new Vector3();
|
938
|
+
|
939
|
+
this._bounds.getCenter(center);
|
940
|
+
this._bounds.getSize(size);
|
941
|
+
center.sub(size.multiplyScalar(0.5).multiply(hit.face.normal));
|
942
|
+
this._hitPointInLocalSpace.copy(center);
|
943
|
+
this._hitNormalInLocalSpace.copy(hit.face.normal);
|
944
|
+
|
945
|
+
// ensure plane is far enough up that we don't drag into the surface
|
946
|
+
// Which offset we use here depends on the face normal direction we hit
|
947
|
+
// If we hit the bottom, we want to use the top, and vice versa
|
948
|
+
// To do this dynamically, we can find the intersection between our local bounds and the hit face normal (which is already in local space)
|
949
|
+
this._bounds.getCenter(center);
|
950
|
+
this._bounds.getSize(size);
|
951
|
+
center.add(size.multiplyScalar(0.5).multiply(hit.face.normal));
|
952
|
+
|
953
|
+
const offset = this._hitPointInLocalSpace.clone().add(center);
|
954
|
+
this._followObject.localToWorld(offset);
|
955
|
+
const offsetWP = this._followObject.worldPosition.sub(offset);
|
956
|
+
|
957
|
+
this._dragPlane.setFromNormalAndCoplanarPoint(hit.face.normal, hit.point.sub(offsetWP));
|
958
|
+
}
|
959
|
+
}
|
960
|
+
}
|
961
|
+
}
|
962
|
+
|
963
|
+
// Objects could also serve as "slots" for dragging other objects into. In that case, we don't want to snap to the surface,
|
964
|
+
// we want to snap to the pivot of that object. These dragged-over objects could also need to be invisible (a "slot")
|
965
|
+
// Raycast along the ray to the drag plane and move _followObject so that the grabbed point stays at the hit point
|
966
|
+
if (dragMode !== DragMode.Attached && ray.intersectPlane(this._dragPlane, this._tempVec)) {
|
967
|
+
|
968
|
+
this._followObject.worldPosition = this._tempVec;
|
969
|
+
this._followObject.updateMatrix();
|
970
|
+
this._followObject.updateMatrixWorld(true);
|
971
|
+
|
972
|
+
const newWP = this._hitPointInLocalSpace.clone();
|
973
|
+
this._followObject.localToWorld(newWP);
|
974
|
+
|
975
|
+
if (debug) {
|
976
|
+
Gizmos.DrawLine(newWP, this._tempVec, 0x00ffff, 0, false);
|
977
|
+
}
|
978
|
+
|
979
|
+
this._followObject.worldPosition = this._tempVec.multiplyScalar(2).sub(newWP);
|
980
|
+
this._followObject.updateMatrix();
|
981
|
+
|
982
|
+
/*
|
983
|
+
// TODO figure out nicer look rotation here
|
984
|
+
const normal = this._dragPlane.normal;
|
985
|
+
const lookPoint = normal.clone().multiplyScalar(1000).add(this._tempVec);
|
986
|
+
if (lookPoint) {
|
987
|
+
this._followObject.lookAt(lookPoint);
|
988
|
+
this._followObject.rotateX(Math.PI / 2);
|
989
|
+
}
|
990
|
+
*/
|
991
|
+
this._followObject.updateMatrix();
|
992
|
+
}
|
993
|
+
|
994
|
+
// TODO refactor to a common place
|
995
|
+
// apply constraints (position grid snap, rotation, ...)
|
996
|
+
if (this.settings.snapGridResolution > 0) {
|
997
|
+
const wp = this._followObject.worldPosition;
|
998
|
+
const snap = this.settings.snapGridResolution;
|
999
|
+
wp.x = Math.round(wp.x / snap) * snap;
|
1000
|
+
wp.y = Math.round(wp.y / snap) * snap;
|
1001
|
+
wp.z = Math.round(wp.z / snap) * snap;
|
1002
|
+
this._followObject.worldPosition = wp;
|
1003
|
+
this._followObject.updateMatrix();
|
1004
|
+
}
|
1005
|
+
if (keepRotation) {
|
1006
|
+
this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
|
1007
|
+
this._followObject.updateMatrix();
|
1008
|
+
}
|
1009
|
+
|
1010
|
+
// TODO refactor to a common place
|
1011
|
+
// TODO should use unscaled time here // some test for lerp speed depending on distance
|
1012
|
+
const t = Mathf.clamp01(this.context.time.deltaTime * lerpStrength * lerpFactor);// / (currentDist - 1 + 0.01));
|
1013
|
+
|
1014
|
+
const wp = draggedObject.worldPosition;
|
1015
|
+
wp.lerp(this._followObject.worldPosition, t);
|
1016
|
+
draggedObject.worldPosition = wp;
|
1017
|
+
|
1018
|
+
const rot = draggedObject.worldQuaternion;
|
1019
|
+
rot.slerp(this._followObject.worldQuaternion, t);
|
1020
|
+
draggedObject.worldQuaternion = rot;
|
1021
|
+
|
1022
|
+
|
1023
|
+
if (debug)
|
1024
|
+
{
|
1025
|
+
const hitPointWP = this._hitPointInLocalSpace.clone();
|
1026
|
+
draggedObject.localToWorld(hitPointWP);
|
1027
|
+
// draw grab attachment point and normal. They are in grabbed object space
|
1028
|
+
Gizmos.DrawSphere(hitPointWP, 0.02, 0xff0000);
|
1029
|
+
const hitNormalWP = this._hitNormalInLocalSpace.clone();
|
1030
|
+
hitNormalWP.applyQuaternion(rot);
|
1031
|
+
Gizmos.DrawRay(hitPointWP, hitNormalWP, 0xff0000);
|
1032
|
+
|
1033
|
+
// debug info
|
1034
|
+
Gizmos.DrawLabel(wp.add(new Vector3(0, 0.25, 0)),
|
1035
|
+
`Distance: ${this._totalMovement.length().toFixed(2)}\n
|
1036
|
+
Along Ray: ${this._totalMovementAlongRayDirection.toFixed(2)}\n
|
1037
|
+
Session: ${!!NeedleXRSession.active}\n
|
1038
|
+
Device: ${this._deviceMode}\n
|
1039
|
+
`,
|
1040
|
+
0.03
|
1041
|
+
);
|
1042
|
+
|
1043
|
+
// draw bottom/back snap points
|
1044
|
+
const bottomCenter = this._bottomCenter.clone();
|
1045
|
+
const backCenter = this._backCenter.clone();
|
1046
|
+
const backBottomCenter = this._backBottomCenter.clone();
|
1047
|
+
draggedObject.localToWorld(bottomCenter);
|
1048
|
+
draggedObject.localToWorld(backCenter);
|
1049
|
+
draggedObject.localToWorld(backBottomCenter);
|
1050
|
+
Gizmos.DrawSphere(bottomCenter, 0.01, 0x00ff00, 0, false);
|
1051
|
+
Gizmos.DrawSphere(backCenter, 0.01, 0x0000ff, 0, false);
|
1052
|
+
Gizmos.DrawSphere(backBottomCenter, 0.01, 0xff00ff, 0, false);
|
1053
|
+
Gizmos.DrawLine(bottomCenter, backBottomCenter, 0x00ffff, 0, false);
|
1054
|
+
Gizmos.DrawLine(backBottomCenter, backCenter, 0x00ffff, 0, false);
|
1055
|
+
}
|
1056
|
+
}
|
1057
|
+
|
1058
|
+
onDragEnd(args: PointerEventData) {
|
1059
|
+
console.assert(this._followObject.parent === args.event.space, "Drag end: _followObject is not parented to the space object");
|
1060
|
+
this._followObject.removeFromParent();
|
1061
|
+
this._followObject.destroy();
|
1062
|
+
this._lastDragPosRigSpace = undefined;
|
1063
|
+
}
|
1064
|
+
}
|
1065
|
+
|
1066
|
+
/** Currently does _only_ provide visuals support for DragControls operations.
|
1067
|
+
* Previously it also provided the actual drag functionality, but that has been moved to DragControls for now.
|
1068
|
+
*/
|
1069
|
+
class LegacyDragVisualsHelper {
|
1070
|
+
|
302
1071
|
showGizmo: boolean = true;
|
303
1072
|
useViewAngle: boolean = true;
|
304
1073
|
|
@@ -336,13 +1105,12 @@
|
|
336
1105
|
constructor(camera: Camera) {
|
337
1106
|
this._camera = camera;
|
338
1107
|
|
339
|
-
const line = new Line(
|
1108
|
+
const line = new Line(LegacyDragVisualsHelper.geometry);
|
340
1109
|
const mat = line.material as LineBasicMaterial;
|
341
1110
|
mat.color = new Color(.4, .4, .4);
|
342
1111
|
line.layers.set(2);
|
343
1112
|
line.name = 'line';
|
344
1113
|
line.scale.y = 1;
|
345
|
-
// line.matrixAutoUpdate = false;
|
346
1114
|
this._groundLine = line;
|
347
1115
|
|
348
1116
|
const geometry = new SphereGeometry(.5, 22, 22);
|
@@ -357,13 +1125,12 @@
|
|
357
1125
|
if (this._selected && context) {
|
358
1126
|
for (const rb of this._rbs) {
|
359
1127
|
rb.wakeUp();
|
360
|
-
// if (!rb.smoothedVelocity) continue;
|
361
1128
|
rb.setVelocity(0, 0, 0);
|
362
1129
|
}
|
363
1130
|
}
|
364
1131
|
|
365
1132
|
if (this._selected) {
|
366
|
-
|
1133
|
+
// TODO move somewhere else
|
367
1134
|
Avatar_POI.Remove(context, this._selected);
|
368
1135
|
}
|
369
1136
|
|
@@ -385,6 +1152,8 @@
|
|
385
1152
|
console.error("DragHelper: no context");
|
386
1153
|
return;
|
387
1154
|
}
|
1155
|
+
|
1156
|
+
// TODO move somewhere else
|
388
1157
|
Avatar_POI.Add(context, this._selected, null);
|
389
1158
|
|
390
1159
|
this._groundOffsetFactor = 0;
|
@@ -392,7 +1161,6 @@
|
|
392
1161
|
this._groundOffset.set(0, 0, 0);
|
393
1162
|
this._requireUpdateGroundPlane = true;
|
394
1163
|
|
395
|
-
// this._rbs = GameObject.getComponentsInChildren(this._selected, Rigidbody);
|
396
1164
|
this.onUpdateScreenSpacePlane();
|
397
1165
|
}
|
398
1166
|
}
|
@@ -402,6 +1170,16 @@
|
|
402
1170
|
private _didDragOnGroundPlaneLastFrame: boolean = false;
|
403
1171
|
|
404
1172
|
onUpdate(_context: Context) {
|
1173
|
+
|
1174
|
+
if (!this._selected) return;
|
1175
|
+
|
1176
|
+
const wp = getWorldPosition(this._selected);
|
1177
|
+
this.onUpdateWorldPosition(wp, this._groundPlanePoint, false);
|
1178
|
+
this.onUpdateGroundPlane();
|
1179
|
+
this._didDragOnGroundPlaneLastFrame = true;
|
1180
|
+
this._hasGroundPlane = true;
|
1181
|
+
|
1182
|
+
/*
|
405
1183
|
if (!this._context) return;
|
406
1184
|
|
407
1185
|
const mainKey: KeyCode = "Space";
|
@@ -488,6 +1266,7 @@
|
|
488
1266
|
this.onDidUpdate();
|
489
1267
|
}
|
490
1268
|
}
|
1269
|
+
*/
|
491
1270
|
}
|
492
1271
|
|
493
1272
|
private onUpdateWorldPosition(wp: Vector3, pointOnPlane: Vector3 | null, heightOnly: boolean) {
|
@@ -549,18 +1328,6 @@
|
|
549
1328
|
this._groundOffset.copy(this._intersection).sub(wp);
|
550
1329
|
}
|
551
1330
|
|
552
|
-
private onDidUpdate() {
|
553
|
-
// todo: when using instancing we need to mark the matrix to update
|
554
|
-
InstancingUtil.markDirty(this._selected);
|
555
|
-
|
556
|
-
for (const rb of this._rbs) {
|
557
|
-
rb.wakeUp();
|
558
|
-
rb.resetForcesAndTorques();
|
559
|
-
// rb.setBodyFromGameObject({ x: 0, y: 0, z: 0 });
|
560
|
-
rb.setAngularVelocity(0, 0, 0);
|
561
|
-
}
|
562
|
-
}
|
563
|
-
|
564
1331
|
private contains(obj: Object3D, toSearch: Object3D): boolean {
|
565
1332
|
if (obj === toSearch) return true;
|
566
1333
|
if (obj.children) {
|
@@ -1,10 +1,11 @@
|
|
1
|
-
import {
|
1
|
+
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
|
+
|
3
|
+
import * as files from "../engine/engine_networking_files.js";
|
2
4
|
import { RaycastOptions } from "../engine/engine_physics.js";
|
3
|
-
import * as files from "../engine/engine_networking_files.js";
|
4
5
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
6
|
+
import { getParam } from "../engine/engine_utils.js";
|
5
7
|
import { Networking } from "../engine-components/Networking.js";
|
6
|
-
import
|
7
|
-
import { getParam } from "../engine/engine_utils.js";
|
8
|
+
import { Behaviour, GameObject } from "./Component.js";
|
8
9
|
|
9
10
|
const debug = getParam("debugdroplistener");
|
10
11
|
|
@@ -1,22 +1,27 @@
|
|
1
|
+
import { Object3D, Quaternion, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { isDevEnvironment } from "../engine/debug/index.js";
|
4
|
+
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
5
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
1
6
|
import { Behaviour, GameObject } from "./Component.js";
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
import {
|
5
|
-
import { Animation } from "./Animation.js";
|
6
|
-
import { Vector3, Quaternion, Object3D } from "three";
|
7
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
|
-
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
7
|
+
import { DragControls } from "./DragControls.js";
|
8
|
+
import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
9
|
+
import { ObjectRaycaster } from "./ui/Raycaster.js";
|
9
10
|
|
10
|
-
export class Duplicatable extends
|
11
|
+
export class Duplicatable extends Behaviour implements IPointerEventHandler {
|
11
12
|
|
13
|
+
/** Duplicates will be parented into the set object. If not defined, this GameObject will be used as parent. */
|
12
14
|
@serializable(Object3D)
|
13
15
|
parent: GameObject | null = null;
|
16
|
+
|
17
|
+
/** The object to be duplicated */
|
14
18
|
@serializable(Object3D)
|
15
19
|
object: GameObject | null = null;
|
16
20
|
|
17
21
|
// limit max object spawn count per interval
|
18
22
|
@serializable()
|
19
23
|
limitCount = 10;
|
24
|
+
|
20
25
|
@serializable()
|
21
26
|
limitInterval = 60;
|
22
27
|
|
@@ -24,17 +29,7 @@
|
|
24
29
|
private _startPosition: THREE.Vector3 | null = null;
|
25
30
|
private _startQuaternion: THREE.Quaternion | null = null;
|
26
31
|
|
27
|
-
|
28
|
-
// TODO: add support to not having to assign a object to clone
|
29
|
-
// if(!this.object){
|
30
|
-
// const opts = new InstantiateOptions();
|
31
|
-
// opts.parent = this.gameObject;
|
32
|
-
// opts.idProvider = InstantiateIdProvider.createFromString(this.guid);
|
33
|
-
// const clone = GameObject.instantiate(this.gameObject, opts);
|
34
|
-
// const duplicatable =
|
35
|
-
// this.object = clone;
|
36
|
-
// }
|
37
|
-
// console.log(this, this.object);
|
32
|
+
start(): void {
|
38
33
|
if (this.object) {
|
39
34
|
if (this.object as any === this.gameObject) {
|
40
35
|
console.error("Can not duplicate self");
|
@@ -48,34 +43,45 @@
|
|
48
43
|
this._startQuaternion = this.object.quaternion?.clone() ?? new Quaternion(0, 0, 0, 1);
|
49
44
|
}
|
50
45
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
return;
|
57
|
-
}
|
58
|
-
const res = this.handleDuplication(args.selected);
|
59
|
-
if (res) {
|
60
|
-
console.assert(res !== args.selected, "Duplicated object is original");
|
61
|
-
args.attached = res;
|
62
|
-
}
|
63
|
-
});
|
46
|
+
// legacy – DragControls was required for duplication and so often the component is still there; we work around that by disabling it here
|
47
|
+
const dragControls = this.gameObject.getComponent(DragControls);
|
48
|
+
if (dragControls) {
|
49
|
+
if (isDevEnvironment()) console.warn(`Please remove DragControls from \"${dragControls.name}\": it's not needed anymore when the object also has a Duplicatable component`);
|
50
|
+
dragControls.enabled = false;
|
64
51
|
}
|
65
|
-
else console.warn("Could no find drag controls in parent", this.name);
|
66
52
|
|
67
|
-
|
68
|
-
|
69
|
-
args.grab = null;
|
70
|
-
return;
|
71
|
-
}
|
72
|
-
const res = this.handleDuplication(args.selected);
|
73
|
-
if (res) args.grab = res;
|
74
|
-
});
|
53
|
+
if (!this.gameObject.getComponentInParent(ObjectRaycaster))
|
54
|
+
this.gameObject.addNewComponent(ObjectRaycaster);
|
75
55
|
|
76
56
|
this.cloneLimitIntervalFn();
|
77
57
|
}
|
78
58
|
|
59
|
+
private _forwardPointerEvents: Map<Object3D, DragControls> = new Map();
|
60
|
+
|
61
|
+
onPointerDown(args: PointerEventData) {
|
62
|
+
if (!this.object) return;
|
63
|
+
if (!this.context.connection.allowEditing) return;
|
64
|
+
if (args.button !== 0) return;
|
65
|
+
|
66
|
+
const res = this.handleDuplication();
|
67
|
+
if (res) {
|
68
|
+
const dragControls = GameObject.getComponent(res, DragControls);
|
69
|
+
if (!dragControls) console.warn("Duplicated object does not have DragControls");
|
70
|
+
else {
|
71
|
+
dragControls.onPointerDown(args);
|
72
|
+
this._forwardPointerEvents.set(args.event.space, dragControls);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
onPointerUp(args: PointerEventData) {
|
78
|
+
const dragControls = this._forwardPointerEvents.get(args.event.space);
|
79
|
+
if (dragControls) {
|
80
|
+
dragControls.onPointerUp(args);
|
81
|
+
this._forwardPointerEvents.delete(args.event.space);
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
79
85
|
private cloneLimitIntervalFn() {
|
80
86
|
if (this.destroyed) return;
|
81
87
|
if (this._currentCount > 0) {
|
@@ -86,62 +92,39 @@
|
|
86
92
|
}, (this.limitInterval / this.limitCount) * 1000);
|
87
93
|
}
|
88
94
|
|
89
|
-
private handleDuplication(
|
95
|
+
private handleDuplication(): THREE.Object3D | null {
|
96
|
+
if (!this.object) return null;
|
90
97
|
if (this._currentCount >= this.limitCount) return null;
|
91
|
-
if (
|
92
|
-
if (selected === this.gameObject || this.handleMultiObject(selected)) {
|
98
|
+
if (this.object as any === this.gameObject) return null;
|
93
99
|
|
94
|
-
|
95
|
-
this.object.visible = true;
|
100
|
+
this.object.visible = true;
|
96
101
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
102
|
+
if (this._startPosition)
|
103
|
+
this.object.position.copy(this._startPosition);
|
104
|
+
if (this._startQuaternion)
|
105
|
+
this.object.quaternion.copy(this._startQuaternion);
|
101
106
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
107
|
+
const opts = new InstantiateOptions();
|
108
|
+
if (!this.parent) this.parent = this.gameObject.parent as GameObject;
|
109
|
+
if (this.parent) {
|
110
|
+
opts.parent = this.parent.guid ?? this.parent.userData?.guid;
|
111
|
+
opts.keepWorldPosition = true;
|
112
|
+
}
|
113
|
+
opts.position = this.worldPosition;
|
114
|
+
opts.rotation = this.worldQuaternion;
|
115
|
+
opts.context = this.context;
|
116
|
+
this._currentCount += 1;
|
112
117
|
|
113
|
-
|
114
|
-
|
115
|
-
|
118
|
+
const newInstance = GameObject.instantiateSynced(this.object as GameObject, opts) as GameObject;
|
119
|
+
console.assert(newInstance !== this.object, "Duplicated object is original");
|
120
|
+
this.object.visible = false;
|
116
121
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
+
// see if this fixes object being offset when duplicated and dragged - it looks like three clone has shared position/quaternion objects?
|
123
|
+
if (this._startPosition)
|
124
|
+
this.object.position.clone().copy(this._startPosition);
|
125
|
+
if (this._startQuaternion)
|
126
|
+
this.object.quaternion.clone().copy(this._startQuaternion);
|
122
127
|
|
123
|
-
|
124
|
-
}
|
125
|
-
return null;
|
128
|
+
return newInstance;
|
126
129
|
}
|
127
|
-
|
128
|
-
private handleMultiObject(selected: THREE.Object3D): boolean {
|
129
|
-
const shouldSearchInChildren = this.gameObject.type === "Group" || this.gameObject.type === "Object3D";
|
130
|
-
if (!shouldSearchInChildren) return false;
|
131
|
-
return this.isInChildren(this.gameObject, selected);
|
132
|
-
}
|
133
|
-
|
134
|
-
private isInChildren(current: THREE.Object3D, search: THREE.Object3D): boolean {
|
135
|
-
if (!current) return false;
|
136
|
-
if (current === search) return true;
|
137
|
-
if (current.children) {
|
138
|
-
for (const child of current.children) {
|
139
|
-
if (this.isInChildren(child, search)) {
|
140
|
-
return true;
|
141
|
-
}
|
142
|
-
}
|
143
|
-
}
|
144
|
-
return false;
|
145
|
-
}
|
146
|
-
|
147
130
|
}
|
@@ -1,13 +1,14 @@
|
|
1
|
+
import { Group, Object3D, Texture, TextureLoader } from "three";
|
2
|
+
|
1
3
|
import { deepClone, getParam, resolveUrl } from "../engine/engine_utils.js";
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
import { Group, Object3D, Texture, TextureLoader } from "three";
|
4
|
+
import { destroy, IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
|
5
|
+
import { getLoader } from "./engine_gltf.js";
|
5
6
|
import { processNewScripts } from "./engine_mainloop_utils.js";
|
6
7
|
import { registerPrefabProvider, syncInstantiate } from "./engine_networking_instantiate.js";
|
8
|
+
import { assign,SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
|
9
|
+
import { Context } from "./engine_setup.js";
|
10
|
+
import type { IComponent, IGameObject, SourceIdentifier } from "./engine_types.js";
|
7
11
|
import { download } from "./engine_web_api.js";
|
8
|
-
import { getLoader } from "./engine_gltf.js";
|
9
|
-
import type { IComponent, IGameObject, SourceIdentifier } from "./engine_types.js";
|
10
|
-
import { destroy, IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
|
11
12
|
|
12
13
|
const debug = getParam("debugaddressables");
|
13
14
|
|
@@ -1,7 +1,8 @@
|
|
1
|
-
import { InternalUsageTrackerPlugin } from "./extensions/usage_tracker.js";
|
2
1
|
import { Bone, BufferAttribute, BufferGeometry, InterleavedBuffer, InterleavedBufferAttribute, Material, Mesh, NeverCompare, Object3D, Scene, Skeleton, SkinnedMesh, Source, Texture, Uniform, WebGLRenderer } from "three";
|
2
|
+
|
3
3
|
import { addPatch } from "./engine_patcher.js";
|
4
4
|
import { getParam } from "./engine_utils.js";
|
5
|
+
import { InternalUsageTrackerPlugin } from "./extensions/usage_tracker.js";
|
5
6
|
|
6
7
|
|
7
8
|
export class AssetDatabase {
|
@@ -1,7 +1,8 @@
|
|
1
|
-
import type { ICameraController } from "./engine_types.js";
|
2
1
|
import { Camera, Object3D } from "three";
|
3
2
|
|
3
|
+
import type { ICameraController } from "./engine_types.js";
|
4
4
|
|
5
|
+
|
5
6
|
const $cameraController = Symbol("cameraController");
|
6
7
|
|
7
8
|
export function getCameraController(cam: Camera): ICameraController | null {
|
@@ -1,12 +1,13 @@
|
|
1
1
|
import { Object3D, Scene } from "three";
|
2
|
+
|
3
|
+
import { apply } from "../engine-components/js-extensions/Object3D.js";
|
4
|
+
import { ComponentEvents, ComponentLifecycleEvents } from "./engine_components_internal.js";
|
5
|
+
import { activeInHierarchyFieldName } from "./engine_constants.js";
|
6
|
+
import { removeScriptFromContext, updateActiveInHierarchyWithoutEventCall } from "./engine_mainloop_utils.js";
|
7
|
+
import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
|
8
|
+
import { Context, registerComponent } from "./engine_setup.js";
|
2
9
|
import type { Constructor, ConstructorConcrete, IComponent, IGameObject } from "./engine_types.js";
|
3
|
-
import { Context, registerComponent } from "./engine_setup.js";
|
4
10
|
import { getParam } from "./engine_utils.js";
|
5
|
-
import { removeScriptFromContext, updateActiveInHierarchyWithoutEventCall } from "./engine_mainloop_utils.js";
|
6
|
-
import { activeInHierarchyFieldName } from "./engine_constants.js";
|
7
|
-
import { apply } from "../engine-components/js-extensions/Object3D.js";
|
8
|
-
import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
|
9
|
-
import { ComponentEvents, ComponentLifecycleEvents } from "./engine_components_internal.js";
|
10
11
|
|
11
12
|
const debug = getParam("debuggetcomponent");
|
12
13
|
|
@@ -8,19 +8,22 @@
|
|
8
8
|
|
9
9
|
tryEval(`if(!globalThis["NEEDLE_ENGINE_VERSION"]) globalThis["NEEDLE_ENGINE_VERSION"] = "0.0.0";`)
|
10
10
|
tryEval(`if(!globalThis["NEEDLE_ENGINE_GENERATOR"]) globalThis["NEEDLE_ENGINE_GENERATOR"] = "unknown";`)
|
11
|
+
tryEval(`if(!globalThis["NEEDLE_PROJECT_BUILD_TIME"]) globalThis["NEEDLE_PROJECT_BUILD_TIME"] = "unknown";`)
|
11
12
|
|
12
13
|
declare const NEEDLE_ENGINE_VERSION: string
|
13
14
|
declare const NEEDLE_ENGINE_GENERATOR: string;
|
15
|
+
declare const NEEDLE_PROJECT_BUILD_TIME: string;
|
14
16
|
|
15
17
|
// Make sure to wrap the new global this define in underscores to prevent the bundler from replacing it with the actual value
|
16
18
|
tryEval(`globalThis["__NEEDLE_ENGINE_VERSION__"] = "` + NEEDLE_ENGINE_VERSION + `";`)
|
17
19
|
tryEval(`globalThis["__NEEDLE_ENGINE_GENERATOR__"] = "` + NEEDLE_ENGINE_GENERATOR + `";`)
|
20
|
+
tryEval(`globalThis["__NEEDLE_PROJECT_BUILD_TIME__"] = "` + NEEDLE_PROJECT_BUILD_TIME + `";`)
|
18
21
|
|
19
22
|
|
20
|
-
|
21
23
|
export const VERSION = NEEDLE_ENGINE_VERSION;
|
22
24
|
export const GENERATOR = NEEDLE_ENGINE_GENERATOR;
|
23
|
-
|
25
|
+
const BUILD_TIME = NEEDLE_PROJECT_BUILD_TIME;
|
26
|
+
if (debug) console.log(`Engine version: ${VERSION} (generator: ${GENERATOR})\nProject built at ${BUILD_TIME}`);
|
24
27
|
|
25
28
|
export const activeInHierarchyFieldName = "needle_isActiveInHierarchy";
|
26
29
|
export const builtinComponentKeyName = "builtin_components";
|
@@ -1,41 +1,36 @@
|
|
1
|
+
import { EffectComposer, RenderPass } from "postprocessing";
|
1
2
|
import {
|
2
3
|
BufferGeometry, Cache, Camera, Clock, Color, DepthTexture, Group,
|
3
4
|
Material, NearestFilter, NoToneMapping, Object3D, PCFSoftShadowMap,
|
4
5
|
PerspectiveCamera, RGBAFormat, Scene, SRGBColorSpace,
|
5
6
|
Texture, WebGLRenderer, type WebGLRendererParameters, WebGLRenderTarget, type WebXRArrayCamera
|
6
7
|
} from 'three';
|
8
|
+
import * as Stats from 'three/examples/jsm/libs/stats.module.js';
|
7
9
|
|
10
|
+
import { isDevEnvironment, LogType, showBalloonError, showBalloonMessage, showBalloonWarning } from './debug/index.js';
|
11
|
+
import { Addressables } from './engine_addressables.js';
|
12
|
+
import { Application } from './engine_application.js';
|
13
|
+
import { AssetDatabase } from './engine_assetdatabase.js';
|
14
|
+
import { VERSION } from './engine_constants.js';
|
15
|
+
import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
|
16
|
+
import { WaitForPromise } from './engine_coroutine.js';
|
17
|
+
import { destroy, foreachComponent } from './engine_gameobject.js';
|
18
|
+
import { getLoader } from './engine_gltf.js';
|
8
19
|
import { Input } from './engine_input.js';
|
20
|
+
import { invokeLifecycleFunctions } from './engine_lifecycle_functions_internal.js';
|
21
|
+
import { type ILightDataRegistry, LightDataRegistry } from './engine_lightdata.js';
|
22
|
+
import * as looputils from './engine_mainloop_utils.js';
|
23
|
+
import { NetworkConnection } from './engine_networking.js';
|
24
|
+
import { isLocalNetwork } from './engine_networking_utils.js';
|
9
25
|
import { Physics } from './engine_physics.js';
|
26
|
+
import { PlayerViewManager } from './engine_playerview.js';
|
27
|
+
import { RendererData as SceneLighting } from './engine_scenelighting.js';
|
28
|
+
import { logHierarchy } from './engine_three_utils.js';
|
10
29
|
import { Time } from './engine_time.js';
|
11
|
-
import {
|
12
|
-
|
13
|
-
import * as looputils from './engine_mainloop_utils.js';
|
30
|
+
import { type CoroutineData, type GLTF, type ICamera, type IComponent, type IContext, type ILight, INeedleXRSession, type LoadedGLTF } from "./engine_types.js";
|
14
31
|
import * as utils from "./engine_utils.js";
|
15
|
-
|
16
|
-
import { EffectComposer, RenderPass } from "postprocessing";
|
17
|
-
|
18
|
-
import { AssetDatabase } from './engine_assetdatabase.js';
|
19
|
-
|
20
|
-
import { logHierarchy } from './engine_three_utils.js';
|
21
|
-
|
22
|
-
import * as Stats from 'three/examples/jsm/libs/stats.module.js';
|
23
|
-
import { RendererData as SceneLighting } from './engine_scenelighting.js';
|
24
|
-
import { Addressables } from './engine_addressables.js';
|
25
|
-
import { Application } from './engine_application.js';
|
26
|
-
import { LightDataRegistry, type ILightDataRegistry } from './engine_lightdata.js';
|
27
|
-
import { PlayerViewManager } from './engine_playerview.js';
|
28
|
-
|
29
|
-
import { type CoroutineData, type GLTF, type ICamera, type IComponent, type IContext, type ILight, type LoadedGLTF } from "./engine_types.js";
|
30
|
-
import { destroy, foreachComponent } from './engine_gameobject.js';
|
31
|
-
import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
|
32
32
|
import { delay, getParam } from './engine_utils.js';
|
33
|
-
import {
|
34
|
-
import { isDevEnvironment, LogType, showBalloonError, showBalloonMessage, showBalloonWarning } from './debug/index.js';
|
35
|
-
import { getLoader } from './engine_gltf.js';
|
36
|
-
import { isLocalNetwork } from './engine_networking_utils.js';
|
37
|
-
import { WaitForPromise } from './engine_coroutine.js';
|
38
|
-
import { invokeLifecycleFunctions } from './engine_lifecycle_functions_internal.js';
|
33
|
+
import type { INeedleXRSessionEventReceiver, NeedleXRSession } from './engine_xr.js';
|
39
34
|
|
40
35
|
|
41
36
|
const debug = utils.getParam("debugcontext");
|
@@ -101,11 +96,6 @@
|
|
101
96
|
Undefined = -1,
|
102
97
|
}
|
103
98
|
|
104
|
-
export enum XRSessionMode {
|
105
|
-
ImmersiveVR = "immersive-vr",
|
106
|
-
ImmersiveAR = "immersive-ar",
|
107
|
-
}
|
108
|
-
|
109
99
|
/** threejs callback event signature */
|
110
100
|
export declare type OnRenderCallback = (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void
|
111
101
|
|
@@ -213,6 +203,7 @@
|
|
213
203
|
private _boundingClientRectFrame: number = -1;
|
214
204
|
private _boundingClientRect: DOMRect | null = null;
|
215
205
|
private _domX; private _domY;
|
206
|
+
/** update bounding rects + domX, domY */
|
216
207
|
private calculateBoundingClientRect() {
|
217
208
|
// workaround for mozilla webXR viewer
|
218
209
|
if (this.isInAR) {
|
@@ -227,30 +218,46 @@
|
|
227
218
|
this._domY = this._boundingClientRect.y;
|
228
219
|
}
|
229
220
|
|
221
|
+
/** The width of the `<needle-engine>` element on the website */
|
230
222
|
get domWidth(): number {
|
231
223
|
// for mozilla XR
|
232
224
|
if (this.isInAR) return window.innerWidth;
|
233
225
|
return this.domElement.clientWidth;
|
234
226
|
}
|
227
|
+
/** The height of the `<needle-engine>` element on the website */
|
235
228
|
get domHeight(): number {
|
236
229
|
// for mozilla XR
|
237
230
|
if (this.isInAR) return window.innerHeight;
|
238
231
|
return this.domElement.clientHeight;
|
239
232
|
}
|
233
|
+
/** the X position of the Needle Engine element on the website */
|
240
234
|
get domX(): number {
|
241
235
|
this.calculateBoundingClientRect();
|
242
236
|
return this._domX;
|
243
237
|
}
|
238
|
+
/** the Y position of the Needlee Engine element on the website */
|
244
239
|
get domY(): number {
|
245
240
|
this.calculateBoundingClientRect();
|
246
241
|
return this._domY;
|
247
242
|
}
|
248
243
|
get isInXR() { return this.renderer?.xr?.isPresenting || false; }
|
249
|
-
|
250
|
-
|
251
|
-
|
244
|
+
/** shorthand for `NeedleXRSession.active`
|
245
|
+
* Automatically set by NeedleXRSession when a XR session is active
|
246
|
+
* @returns the active XR session or null if no session is active
|
247
|
+
* */
|
248
|
+
xr: NeedleXRSession | null = null;
|
249
|
+
get xrSessionMode() { return this.xr?.mode; }
|
250
|
+
get isInVR() { return this.xrSessionMode === "immersive-vr"; }
|
251
|
+
get isInAR() { return this.xrSessionMode === "immersive-ar"; }
|
252
|
+
/** If a XR session is active and in pass through mode (immersive-ar on e.g. Quest) */
|
253
|
+
get isInPassThrough() { return this.xr ? this.xr.isPassThrough : false; }
|
254
|
+
/** access the raw `XRSession` object (shorthand for `context.renderer.xr.getSession()`). For more control use `NeedleXRSession.active` */
|
252
255
|
get xrSession() { return this.renderer?.xr?.getSession(); }
|
256
|
+
/** @returns the latest XRFrame (if a XRSession is currently active)
|
257
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame
|
258
|
+
*/
|
253
259
|
get xrFrame() { return this._xrFrame }
|
260
|
+
/** @returns the current WebXR camera (shorthand for `context.renderer.xr.getCamera()`) */
|
254
261
|
get xrCamera(): WebXRArrayCamera | undefined { return this.renderer?.xr?.getCamera(); }
|
255
262
|
private _xrFrame: XRFrame | null = null;
|
256
263
|
get arOverlayElement(): HTMLElement {
|
@@ -270,17 +277,37 @@
|
|
270
277
|
composer: EffectComposer | null = null;
|
271
278
|
|
272
279
|
// all scripts
|
273
|
-
scripts: IComponent[] = [];
|
274
|
-
scripts_pausedChanged: IComponent[] = [];
|
280
|
+
readonly scripts: IComponent[] = [];
|
281
|
+
readonly scripts_pausedChanged: IComponent[] = [];
|
275
282
|
// scripts with update event
|
276
|
-
scripts_earlyUpdate: IComponent[] = [];
|
277
|
-
scripts_update: IComponent[] = [];
|
278
|
-
scripts_lateUpdate: IComponent[] = [];
|
279
|
-
scripts_onBeforeRender: IComponent[] = [];
|
280
|
-
scripts_onAfterRender: IComponent[] = [];
|
281
|
-
scripts_WithCorroutines: IComponent[] = [];
|
282
|
-
|
283
|
+
readonly scripts_earlyUpdate: IComponent[] = [];
|
284
|
+
readonly scripts_update: IComponent[] = [];
|
285
|
+
readonly scripts_lateUpdate: IComponent[] = [];
|
286
|
+
readonly scripts_onBeforeRender: IComponent[] = [];
|
287
|
+
readonly scripts_onAfterRender: IComponent[] = [];
|
288
|
+
readonly scripts_WithCorroutines: IComponent[] = [];
|
289
|
+
readonly scripts_immersive_vr: INeedleXRSessionEventReceiver[] = [];
|
290
|
+
readonly scripts_immersive_ar: INeedleXRSessionEventReceiver[] = [];
|
291
|
+
readonly coroutines: { [FrameEvent: number]: Array<CoroutineData> } = {}
|
283
292
|
|
293
|
+
/** callbacks called once after the context has been created */
|
294
|
+
readonly post_setup_callbacks: Function[] = [];
|
295
|
+
/** called every frame at the beginning of the frame (after component start events and before earlyUpdate) */
|
296
|
+
readonly pre_update_callbacks: Function[] = [];
|
297
|
+
/** called every frame before rendering (after all component events) */
|
298
|
+
readonly pre_render_callbacks: Array<(frame: XRFrame | null) => void> = [];
|
299
|
+
/** called every frame after rendering (after all component events) */
|
300
|
+
readonly post_render_callbacks: Function[] = [];
|
301
|
+
|
302
|
+
/** called every frame befroe update (this list is emptied every frame) */
|
303
|
+
readonly pre_update_oneshot_callbacks: Function[] = [];
|
304
|
+
|
305
|
+
readonly new_scripts: IComponent[] = [];
|
306
|
+
readonly new_script_start: IComponent[] = [];
|
307
|
+
readonly new_scripts_pre_setup_callbacks: Function[] = [];
|
308
|
+
readonly new_scripts_post_setup_callbacks: Function[] = [];
|
309
|
+
readonly new_scripts_xr: INeedleXRSessionEventReceiver[] = [];
|
310
|
+
|
284
311
|
mainCameraComponent: ICamera | undefined;
|
285
312
|
|
286
313
|
private _camera: Camera | null = null;
|
@@ -300,20 +327,13 @@
|
|
300
327
|
this._camera = cam;
|
301
328
|
}
|
302
329
|
|
303
|
-
post_setup_callbacks: Function[] = [];
|
304
|
-
pre_update_callbacks: Function[] = [];
|
305
|
-
pre_render_callbacks: Function[] = [];
|
306
|
-
post_render_callbacks: Function[] = [];
|
307
|
-
|
308
|
-
new_scripts: IComponent[] = [];
|
309
|
-
new_script_start: IComponent[] = [];
|
310
|
-
new_scripts_pre_setup_callbacks: Function[] = [];
|
311
|
-
new_scripts_post_setup_callbacks: Function[] = [];
|
312
|
-
|
313
330
|
application: Application;
|
331
|
+
/** access timings (current frame number, deltaTime, timeScale, ...) */
|
314
332
|
time: Time;
|
315
333
|
input: Input;
|
334
|
+
/** access physics related methods (e.g. raycasting). To access the phyiscs engine use `context.physics.engine` */
|
316
335
|
physics: Physics;
|
336
|
+
/** access networking methods (use it to send or listen to messages or join a networking backend) */
|
317
337
|
connection: NetworkConnection;
|
318
338
|
/**
|
319
339
|
* @deprecated AssetDataBase is deprecated
|
@@ -394,7 +414,7 @@
|
|
394
414
|
}
|
395
415
|
}
|
396
416
|
}
|
397
|
-
if(debug) console.log("Using Renderer Parameters:", params, this.domElement)
|
417
|
+
if (debug) console.log("Using Renderer Parameters:", params, this.domElement)
|
398
418
|
|
399
419
|
this.renderer = new WebGLRenderer(params);
|
400
420
|
|
@@ -413,6 +433,8 @@
|
|
413
433
|
this.renderer.outputColorSpace = SRGBColorSpace;
|
414
434
|
// https://github.com/mrdoob/three.js/pull/25556
|
415
435
|
this.renderer.useLegacyLights = false;
|
436
|
+
|
437
|
+
this.input.bindEvents();
|
416
438
|
}
|
417
439
|
|
418
440
|
|
@@ -424,10 +446,13 @@
|
|
424
446
|
|
425
447
|
private _disposeCallbacks: Function[] = [];
|
426
448
|
|
427
|
-
// private _requestSizeUpdate : boolean = false;
|
428
449
|
|
429
|
-
updateSize
|
430
|
-
|
450
|
+
/** will request a renderer size update the next render call (will call updateSize the next update) */
|
451
|
+
requestSizeUpdate() { this._sizeChanged = true; }
|
452
|
+
|
453
|
+
/** update the renderer and canvas size */
|
454
|
+
updateSize(force: boolean = false) {
|
455
|
+
if (force || (!this.isManagedExternally && this.renderer.xr?.isPresenting === false)) {
|
431
456
|
this._sizeChanged = false;
|
432
457
|
const scaleFactor = this.resolutionScaleFactor;
|
433
458
|
const width = this.domWidth * scaleFactor;
|
@@ -478,7 +503,7 @@
|
|
478
503
|
async create(opts?: ContextCreateArgs) {
|
479
504
|
try {
|
480
505
|
this._isCreating = true;
|
481
|
-
if(opts !== this._originalCreationArgs)
|
506
|
+
if (opts !== this._originalCreationArgs)
|
482
507
|
this._originalCreationArgs = utils.deepClone(opts);
|
483
508
|
window.addEventListener("unhandledrejection", this.onUnhandledRejection)
|
484
509
|
const res = await this.internalOnCreate(opts);
|
@@ -531,11 +556,11 @@
|
|
531
556
|
if (this.renderer) {
|
532
557
|
this.renderer.setClearAlpha(0);
|
533
558
|
this.renderer.clear();
|
559
|
+
if (!this.isManagedExternally) {
|
560
|
+
if (debug) console.log("Disposing renderer");
|
561
|
+
this.renderer.dispose();
|
562
|
+
}
|
534
563
|
}
|
535
|
-
if (!this.isManagedExternally) {
|
536
|
-
if(debug) console.log("Disposing renderer");
|
537
|
-
this.renderer.dispose();
|
538
|
-
}
|
539
564
|
this.scene = null!;
|
540
565
|
this.renderer = null!;
|
541
566
|
this.input.dispose();
|
@@ -553,6 +578,10 @@
|
|
553
578
|
this._isCreated = false;
|
554
579
|
ContextRegistry.dispatchCallback(ContextEvent.ContextDestroyed, this);
|
555
580
|
ContextRegistry.unregister(this);
|
581
|
+
if (Context.Current === this) {
|
582
|
+
//@ts-ignore
|
583
|
+
Context.Current = null;
|
584
|
+
}
|
556
585
|
}
|
557
586
|
|
558
587
|
registerCoroutineUpdate(script: IComponent, coroutine: Generator, evt: FrameEvent): Generator {
|
@@ -704,7 +733,7 @@
|
|
704
733
|
private async internalOnCreate(opts?: ContextCreateArgs) {
|
705
734
|
const createId = ++this._createId;
|
706
735
|
|
707
|
-
if(debug) console.log("Creating context", this.name, opts);
|
736
|
+
if (debug) console.log("Creating context", this.name, opts);
|
708
737
|
|
709
738
|
this.clear();
|
710
739
|
// stop the animation loop if its running during creation
|
@@ -811,6 +840,8 @@
|
|
811
840
|
}
|
812
841
|
}
|
813
842
|
|
843
|
+
this.input.bindEvents();
|
844
|
+
|
814
845
|
Context.Current = this;
|
815
846
|
looputils.processNewScripts(this);
|
816
847
|
|
@@ -853,7 +884,7 @@
|
|
853
884
|
this._dispatchReadyAfterFrame = true;
|
854
885
|
const res = ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this, { files: loadedFiles });
|
855
886
|
if (res) {
|
856
|
-
if("internalSetLoadingMessage" in this.domElement && typeof this.domElement.internalSetLoadingMessage === "function")
|
887
|
+
if ("internalSetLoadingMessage" in this.domElement && typeof this.domElement.internalSetLoadingMessage === "function")
|
857
888
|
this.domElement?.internalSetLoadingMessage("finish loading");
|
858
889
|
await res;
|
859
890
|
}
|
@@ -897,7 +928,7 @@
|
|
897
928
|
}
|
898
929
|
|
899
930
|
args?.onLoadingStart?.call(this, i, file);
|
900
|
-
if(debug) console.log("Context Load " + file);
|
931
|
+
if (debug) console.log("Context Load " + file);
|
901
932
|
const res = await loader.loadSync(this, file, file, loadingHash, prog => {
|
902
933
|
progressArg.name = file;
|
903
934
|
progressArg.progress = prog;
|
@@ -973,9 +1004,9 @@
|
|
973
1004
|
catch (err) {
|
974
1005
|
this._renderlooperrors += 1;
|
975
1006
|
if ((isDevEnvironment() || debug) && (err instanceof Error || err instanceof TypeError))
|
976
|
-
showBalloonMessage(
|
977
|
-
console.error(err);
|
978
|
-
if (this._renderlooperrors
|
1007
|
+
showBalloonMessage(`Caught unhandled exception during render-loop - see console for details.`, LogType.Error);
|
1008
|
+
console.error("Frame #" + this.time.frame + "\n", err);
|
1009
|
+
if (this._renderlooperrors >= 3) {
|
979
1010
|
console.warn("Stopping render loop due to error")
|
980
1011
|
this.renderer.setAnimationLoop(null);
|
981
1012
|
}
|
@@ -1008,7 +1039,11 @@
|
|
1008
1039
|
|
1009
1040
|
private internalOnBeforeRender(timestamp: DOMHighResTimeStamp, frame: XRFrame | null) {
|
1010
1041
|
|
1042
|
+
const sessionStarted = frame !== null && this._xrFrame === null;
|
1011
1043
|
this._xrFrame = frame;
|
1044
|
+
if (sessionStarted) {
|
1045
|
+
this.domElement.dispatchEvent(new CustomEvent("xr-session-started", { detail: { context: this, session: this.xrSession, frame: frame } }));
|
1046
|
+
}
|
1012
1047
|
|
1013
1048
|
this._currentFrameEvent = FrameEvent.Undefined;
|
1014
1049
|
|
@@ -1047,6 +1082,13 @@
|
|
1047
1082
|
this.setCurrentCamera(last);
|
1048
1083
|
}
|
1049
1084
|
|
1085
|
+
if (this.pre_update_oneshot_callbacks) {
|
1086
|
+
for (const i in this.pre_update_oneshot_callbacks) {
|
1087
|
+
this.pre_update_oneshot_callbacks[i]();
|
1088
|
+
}
|
1089
|
+
this.pre_update_oneshot_callbacks.length = 0;
|
1090
|
+
}
|
1091
|
+
|
1050
1092
|
if (this.pre_update_callbacks) {
|
1051
1093
|
for (const i in this.pre_update_callbacks) {
|
1052
1094
|
this.pre_update_callbacks[i]();
|
@@ -1129,7 +1171,7 @@
|
|
1129
1171
|
|
1130
1172
|
if (this.pre_render_callbacks) {
|
1131
1173
|
for (const i in this.pre_render_callbacks) {
|
1132
|
-
this.pre_render_callbacks[i]();
|
1174
|
+
this.pre_render_callbacks[i](frame);
|
1133
1175
|
}
|
1134
1176
|
}
|
1135
1177
|
|
@@ -1207,8 +1249,8 @@
|
|
1207
1249
|
}
|
1208
1250
|
this._isRendering = true;
|
1209
1251
|
this.renderRequiredTextures();
|
1210
|
-
|
1211
1252
|
|
1253
|
+
|
1212
1254
|
if (this.composer && !this.isInXR) {
|
1213
1255
|
this.composer.render(this.time.deltaTime);
|
1214
1256
|
}
|
@@ -1,5 +1,7 @@
|
|
1
|
-
import {
|
1
|
+
import { BoxGeometry, ColorRepresentation,DoubleSide, Material, Mesh, MeshBasicMaterial, MeshStandardMaterial, PlaneGeometry, SphereGeometry } from "three"
|
2
2
|
|
3
|
+
import { Vec3 } from "./engine_types.js";
|
4
|
+
|
3
5
|
export enum PrimitiveType {
|
4
6
|
Quad = 0,
|
5
7
|
Cube = 1,
|
@@ -9,6 +11,10 @@
|
|
9
11
|
export type ObjectOptions = {
|
10
12
|
name?: string,
|
11
13
|
material?: Material,
|
14
|
+
position?: Vec3,
|
15
|
+
/** euler */
|
16
|
+
rotation?: Vec3,
|
17
|
+
scale?: Vec3,
|
12
18
|
}
|
13
19
|
|
14
20
|
export class ObjectUtils {
|
@@ -35,6 +41,12 @@
|
|
35
41
|
}
|
36
42
|
if (opts?.name)
|
37
43
|
obj.name = opts.name;
|
44
|
+
if (opts?.position)
|
45
|
+
obj.position.set(opts.position.x, opts.position.y, opts.position.z);
|
46
|
+
if (opts?.rotation)
|
47
|
+
obj.rotation.set(opts.rotation.x, opts.rotation.y, opts.rotation.z);
|
48
|
+
if (opts?.scale)
|
49
|
+
obj.scale.set(opts.scale.x, opts.scale.y, opts.scale.z);
|
38
50
|
return obj;
|
39
51
|
}
|
40
52
|
}
|
@@ -1,9 +1,9 @@
|
|
1
|
+
import { logoSVG } from "./assets/index.js"
|
1
2
|
import { showBalloonWarning } from "./debug/index.js";
|
3
|
+
import { hasCommercialLicense, hasProLicense, runtimeLicenseCheckPromise } from "./engine_license.js";
|
2
4
|
import { Mathf } from "./engine_math.js";
|
3
5
|
import { LoadingProgressArgs } from "./engine_setup.js";
|
4
6
|
import { getParam } from "./engine_utils.js";
|
5
|
-
import { logoSVG } from "./assets/index.js"
|
6
|
-
import { hasCommercialLicense, hasProLicense, runtimeLicenseCheckPromise } from "./engine_license.js";
|
7
7
|
|
8
8
|
const debug = getParam("debugloading");
|
9
9
|
const debugRendering = getParam("debugloadingrendering");
|
@@ -228,12 +228,24 @@
|
|
228
228
|
}
|
229
229
|
}
|
230
230
|
|
231
|
+
const container = document.createElement("div");
|
232
|
+
container.style.cssText = `
|
233
|
+
display: flex;
|
234
|
+
flex-direction: column;
|
235
|
+
align-items: center;
|
236
|
+
justify-content: center;
|
237
|
+
width: 100%;
|
238
|
+
opacity: 0;
|
239
|
+
transition: opacity 1.2s ease-in-out .2s;
|
240
|
+
`;
|
241
|
+
setTimeout(() => { container.style.opacity = "1"; }, 1);
|
242
|
+
this._loadingElement.appendChild(container);
|
231
243
|
|
232
244
|
const loadingBarContainer = document.createElement("div");
|
233
245
|
const maxWidth = 30;
|
234
246
|
loadingBarContainer.style.display = "flex";
|
235
247
|
loadingBarContainer.style.width = maxWidth + "%";
|
236
|
-
loadingBarContainer.style.height = "
|
248
|
+
loadingBarContainer.style.height = "3px";
|
237
249
|
if (loadingStyle === "light")
|
238
250
|
loadingBarContainer.style.backgroundColor = "rgba(0,0,0,.2)"
|
239
251
|
else
|
@@ -247,6 +259,15 @@
|
|
247
259
|
logo.style.marginBottom = "20px";
|
248
260
|
logo.style.userSelect = "none";
|
249
261
|
logo.style.objectFit = "contain";
|
262
|
+
if (!hasCommercialLicense()) {
|
263
|
+
logo.style.transition = "transform 1s ease-in-out, opacity 1s ease-in-out";
|
264
|
+
logo.style.transform = "translateY(10px)";
|
265
|
+
logo.style.opacity = "1";
|
266
|
+
setTimeout(() => {
|
267
|
+
logo.style.transform = "translateY(0px)";
|
268
|
+
logo.style.opacity = "1";
|
269
|
+
}, 1);
|
270
|
+
}
|
250
271
|
logo.src = logoSVG;
|
251
272
|
let isUsingCustomLogo = false;
|
252
273
|
if (hasLicense && this._element) {
|
@@ -261,8 +282,8 @@
|
|
261
282
|
logo.style.pointerEvents = "all";
|
262
283
|
logo.addEventListener("click", () => window.open("https://needle.tools", "_blank"));
|
263
284
|
}
|
264
|
-
|
265
|
-
|
285
|
+
container.appendChild(logo);
|
286
|
+
container.appendChild(loadingBarContainer);
|
266
287
|
|
267
288
|
|
268
289
|
this._loadingBar = document.createElement("div");
|
@@ -293,7 +314,7 @@
|
|
293
314
|
this._loadingTextContainer.style.display = "flex";
|
294
315
|
this._loadingTextContainer.style.justifyContent = "center";
|
295
316
|
this._loadingTextContainer.style.marginTop = "1.2em";
|
296
|
-
|
317
|
+
container.appendChild(this._loadingTextContainer);
|
297
318
|
|
298
319
|
const messageContainer = document.createElement("div");
|
299
320
|
this._messageContainer = messageContainer;
|
@@ -303,7 +324,7 @@
|
|
303
324
|
messageContainer.style.fontWeight = "200";
|
304
325
|
// messageContainer.style.border = "1px solid rgba(255,255,255,.1)";
|
305
326
|
messageContainer.style.justifyContent = "center";
|
306
|
-
|
327
|
+
container.appendChild(messageContainer);
|
307
328
|
|
308
329
|
if (hasLicense && this._element) {
|
309
330
|
const loadingTextColor = this._element.getAttribute("loading-text-color");
|
@@ -312,7 +333,7 @@
|
|
312
333
|
}
|
313
334
|
}
|
314
335
|
|
315
|
-
this.handleRuntimeLicense(
|
336
|
+
this.handleRuntimeLicense(container);
|
316
337
|
|
317
338
|
return this._loadingElement;
|
318
339
|
}
|
@@ -323,6 +344,16 @@
|
|
323
344
|
// if it's the case then we don't need to perform a runtime check
|
324
345
|
if (commercialLicense) return;
|
325
346
|
|
347
|
+
// If we don't have a commercial license, then we need to display our message
|
348
|
+
if (debugLicense) console.log("Loading UI has commercial license?", commercialLicense);
|
349
|
+
const nonCommercialContainer = document.createElement("div");
|
350
|
+
nonCommercialContainer.style.paddingTop = ".6em";
|
351
|
+
nonCommercialContainer.style.fontSize = ".8em";
|
352
|
+
nonCommercialContainer.style.textTransform = "uppercase";
|
353
|
+
nonCommercialContainer.innerText = "non commercial";
|
354
|
+
nonCommercialContainer.style.opacity = "0";
|
355
|
+
loadingElement.appendChild(nonCommercialContainer);
|
356
|
+
|
326
357
|
// Use the runtime license check
|
327
358
|
if (runtimeLicenseCheckPromise) {
|
328
359
|
if (debugLicense) console.log("Waiting for runtime license check");
|
@@ -330,13 +361,7 @@
|
|
330
361
|
commercialLicense = hasCommercialLicense();
|
331
362
|
}
|
332
363
|
if (commercialLicense) return;
|
333
|
-
|
334
|
-
|
335
|
-
if (debugLicense) console.log("Loading UI has commercial license?", commercialLicense);
|
336
|
-
const nonCommercialContainer = document.createElement("div");
|
337
|
-
nonCommercialContainer.style.paddingTop = ".6em";
|
338
|
-
nonCommercialContainer.style.fontSize = ".8em";
|
339
|
-
nonCommercialContainer.innerText = "NON COMMERCIAL";
|
340
|
-
loadingElement.appendChild(nonCommercialContainer);
|
364
|
+
nonCommercialContainer.style.transition = "opacity .5s ease-in-out";
|
365
|
+
nonCommercialContainer.style.opacity = "1";
|
341
366
|
}
|
342
367
|
}
|
@@ -16,6 +16,7 @@
|
|
16
16
|
private _createdAROnlyElements: Array<any> = [];
|
17
17
|
private _reparentedObjects: Array<{ el: Element, previousParent: HTMLElement | null }> = [];
|
18
18
|
private contentElement: HTMLElement | null = null;
|
19
|
+
private originalDomOverlayParent: ParentNode | null = null;
|
19
20
|
|
20
21
|
requestEndAR = () => {
|
21
22
|
this.onRequestedEndAR();
|
@@ -34,6 +35,22 @@
|
|
34
35
|
this._reparentedObjects.push({ el: el, previousParent: el.parentElement });
|
35
36
|
this.arContainer?.appendChild(el);
|
36
37
|
}
|
38
|
+
|
39
|
+
if(overlayContainer) {
|
40
|
+
this.originalDomOverlayParent = overlayContainer.parentNode;
|
41
|
+
if (this.originalDomOverlayParent)
|
42
|
+
{
|
43
|
+
console.log("Reparent DOM Overlay to body", overlayContainer, overlayContainer.style.display);
|
44
|
+
// mozilla webxr does hide elements on session start
|
45
|
+
// this is only necessary if we generated the overlay element
|
46
|
+
overlayContainer.style.display = "";
|
47
|
+
overlayContainer.style.visibility = "";
|
48
|
+
document.body.appendChild(overlayContainer);
|
49
|
+
}
|
50
|
+
}
|
51
|
+
else {
|
52
|
+
console.warn("WebXRViewer: No DOM Overlay found");
|
53
|
+
}
|
37
54
|
}
|
38
55
|
this.ensureQuitARButton(this.arContainer);
|
39
56
|
}
|
@@ -1,15 +1,15 @@
|
|
1
|
-
import {
|
2
|
-
import { AROverlayHandler, arContainerClassName } from "./engine_element_overlay.js";
|
1
|
+
import { getLoader, registerLoader } from "../engine/engine_gltf.js";
|
3
2
|
import { GameObject } from "../engine-components/Component.js";
|
3
|
+
import { isDevEnvironment, showBalloonWarning } from "./debug/index.js";
|
4
|
+
import { VERSION } from "./engine_constants.js";
|
4
5
|
import { calculateProgress01, EngineLoadingView, type ILoadingViewHandler } from "./engine_element_loading.js";
|
5
|
-
import {
|
6
|
+
import { arContainerClassName,AROverlayHandler } from "./engine_element_overlay.js";
|
7
|
+
import { hasCommercialLicense } from "./engine_license.js";
|
6
8
|
import { setDracoDecoderPath, setDracoDecoderType, setKtx2TranscoderPath } from "./engine_loaders.js";
|
7
|
-
import { getLoader, registerLoader } from "../engine/engine_gltf.js";
|
8
9
|
import { NeedleGltfLoader } from "./engine_scenetools.js";
|
10
|
+
import { Context, ContextCreateArgs, LoadingProgressArgs } from "./engine_setup.js";
|
9
11
|
import { type INeedleEngineComponent, type LoadedGLTF } from "./engine_types.js";
|
10
|
-
import {
|
11
|
-
import { hasCommercialLicense } from "./engine_license.js";
|
12
|
-
import { VERSION } from "./engine_constants.js";
|
12
|
+
import { getParam } from "./engine_utils.js";
|
13
13
|
|
14
14
|
//
|
15
15
|
// registering loader here too to make sure it's imported when using engine via vanilla js
|
@@ -143,12 +143,15 @@
|
|
143
143
|
}
|
144
144
|
:host .quit-ar-button {
|
145
145
|
position: absolute;
|
146
|
-
top:
|
146
|
+
// top: env(titlebar-area-y); /** this doesnt work **/
|
147
|
+
top: 60px; /** camera access needs a bit more space **/
|
147
148
|
right: 20px;
|
148
149
|
z-index: 9999;
|
149
150
|
}
|
150
151
|
</style>
|
151
|
-
<
|
152
|
+
<div> <!-- this wrapper is necessary for WebXR https://github.com/meta-quest/immersive-web-emulator/issues/55 -->
|
153
|
+
<canvas></canvas>
|
154
|
+
</div>
|
152
155
|
<div class="content">
|
153
156
|
<slot class="overlay-content"></slot>
|
154
157
|
</div>
|
@@ -167,6 +170,7 @@
|
|
167
170
|
console.log("<needle-engine> connected");
|
168
171
|
}
|
169
172
|
|
173
|
+
this.addEventListener("xr-session-started", this.onXRSessionStarted);
|
170
174
|
this.onSetupDesktop();
|
171
175
|
|
172
176
|
if (!this.getAttribute("src")) {
|
@@ -196,6 +200,8 @@
|
|
196
200
|
}
|
197
201
|
|
198
202
|
disconnectedCallback() {
|
203
|
+
this.removeEventListener("xr-session-started", this.onXRSessionStarted);
|
204
|
+
|
199
205
|
this._didFullyLoad = false;
|
200
206
|
const keepAlive = this.getAttribute("keep-alive");
|
201
207
|
const dispose = keepAlive == undefined || (keepAlive?.length > 0 && keepAlive !== "true" && keepAlive !== "1");
|
@@ -340,10 +346,15 @@
|
|
340
346
|
totalProgress01: this._loadingProgress01
|
341
347
|
};
|
342
348
|
const progressEvent = new CustomEvent("progress", { detail: progressEventDetail });
|
349
|
+
const displayNames = new Array<string>();
|
343
350
|
const args: ContextCreateArgs = {
|
344
351
|
files: filesToLoad,
|
345
352
|
onLoadingProgress: evt => {
|
346
|
-
|
353
|
+
const index = evt.index;
|
354
|
+
if (!displayNames[index] && evt.name) {
|
355
|
+
displayNames[index] = getDisplayName(evt.name);
|
356
|
+
}
|
357
|
+
evt.name = displayNames[index];
|
347
358
|
if (useDefaultLoading) this._loadingView?.onLoadingUpdate(evt);
|
348
359
|
progressEventDetail.name = evt.name;
|
349
360
|
progressEventDetail.progress = evt.progress;
|
@@ -384,6 +395,23 @@
|
|
384
395
|
}));
|
385
396
|
}
|
386
397
|
|
398
|
+
private onXRSessionStarted = () => {
|
399
|
+
const xrSessionMode = this.context.xrSessionMode;
|
400
|
+
if (xrSessionMode === "immersive-ar")
|
401
|
+
this.onEnterAR(this.context.xrSession!);
|
402
|
+
else if (xrSessionMode === "immersive-vr")
|
403
|
+
this.onEnterVR(this.context.xrSession!);
|
404
|
+
|
405
|
+
// handle session end:
|
406
|
+
this.context.xrSession?.addEventListener("end", () => {
|
407
|
+
this.dispatchEvent(new CustomEvent("xr-session-ended", { detail: { session: this.context.xrSession, context: this._context, sessionMode: xrSessionMode } }));
|
408
|
+
if (xrSessionMode === "immersive-ar")
|
409
|
+
this.onExitAR(this.context.xrSession!);
|
410
|
+
else if (xrSessionMode === "immersive-vr")
|
411
|
+
this.onExitVR(this.context.xrSession!);
|
412
|
+
});
|
413
|
+
};
|
414
|
+
|
387
415
|
/** called by the context when the first frame has been rendered */
|
388
416
|
private onReady = () => this._loadingView?.onLoadingFinished();
|
389
417
|
private onError = () => this._loadingView?.setMessage("Loading failed!");
|
@@ -474,8 +502,9 @@
|
|
474
502
|
return null;
|
475
503
|
}
|
476
504
|
|
477
|
-
onEnterAR(session: XRSession
|
505
|
+
onEnterAR(session: XRSession) {
|
478
506
|
this.onSetupAR();
|
507
|
+
const overlayContainer = this.getAROverlayContainer();
|
479
508
|
this._overlay_ar.onBegin(this._context!, overlayContainer, session);
|
480
509
|
this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
|
481
510
|
}
|
@@ -590,12 +619,36 @@
|
|
590
619
|
return hash;
|
591
620
|
}
|
592
621
|
|
593
|
-
function
|
622
|
+
function getDisplayName(str: string) {
|
594
623
|
const parts = str.split("/");
|
595
624
|
let name = parts[parts.length - 1];
|
596
625
|
// Remove params
|
597
626
|
const index = name.indexOf("?")
|
598
627
|
if (index > 0)
|
599
628
|
name = name.substring(0, index);
|
600
|
-
|
629
|
+
const extension = name.split(".").pop();
|
630
|
+
if (extension === "glb" || extension === "gltf")
|
631
|
+
name = name.substring(0, name.length - 4);
|
632
|
+
name = decodeURIComponent(name);
|
633
|
+
if (name.length > 3) {
|
634
|
+
let displayName = "";
|
635
|
+
for (let i = 0; i < name.length; i++) {
|
636
|
+
let c = name[i];
|
637
|
+
if (c === ' ' && displayName.length <= 0) continue;
|
638
|
+
const isFirstCharacter = displayName.length === 0;
|
639
|
+
if (isFirstCharacter == false && c === c.toUpperCase()) {
|
640
|
+
displayName += " " + c;
|
641
|
+
}
|
642
|
+
else {
|
643
|
+
if (isFirstCharacter) {
|
644
|
+
c = c.toUpperCase();
|
645
|
+
}
|
646
|
+
displayName += c;
|
647
|
+
}
|
648
|
+
}
|
649
|
+
if (debug) console.log("displayName", name, displayName);
|
650
|
+
return displayName;
|
651
|
+
}
|
652
|
+
if (debug) console.log("displayName", name);
|
653
|
+
return name;
|
601
654
|
}
|
@@ -1,17 +1,18 @@
|
|
1
1
|
import { Bone, Object3D, Quaternion, SkinnedMesh, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { apply } from "../engine-components/js-extensions/Object3D.js";
|
4
|
+
import { __internalNotifyObjectDestroyed as __internalRemoveReferences,disposeObjectResources } from "./engine_assetdatabase.js";
|
5
|
+
import { ComponentEvents,ComponentLifecycleEvents } from "./engine_components_internal.js";
|
6
|
+
import { activeInHierarchyFieldName } from "./engine_constants.js";
|
7
|
+
import { editorGuidKeyName } from "./engine_constants.js";
|
8
|
+
import { $isUsingInstancing, InstancingUtil } from "./engine_instancing.js";
|
2
9
|
import { processNewScripts } from "./engine_mainloop_utils.js";
|
3
10
|
import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
|
11
|
+
import { assign } from "./engine_serialization_core.js";
|
4
12
|
import { Context, registerComponent } from "./engine_setup.js";
|
5
13
|
import { logHierarchy, setWorldPosition, setWorldQuaternion } from "./engine_three_utils.js";
|
6
|
-
import { type GuidsMap, type IComponent as Component, type IComponent, type IGameObject as GameObject, type UIDProvider
|
14
|
+
import { type Constructor,type GuidsMap, type IComponent as Component, type IComponent, type IGameObject as GameObject, type UIDProvider } from "./engine_types.js";
|
7
15
|
import { getParam, tryFindObject } from "./engine_utils.js";
|
8
|
-
import { apply } from "../engine-components/js-extensions/Object3D.js";
|
9
|
-
import { $isUsingInstancing, InstancingUtil } from "./engine_instancing.js";
|
10
|
-
import { activeInHierarchyFieldName } from "./engine_constants.js";
|
11
|
-
import { assign } from "./engine_serialization_core.js";
|
12
|
-
import { disposeObjectResources, __internalNotifyObjectDestroyed as __internalRemoveReferences } from "./engine_assetdatabase.js";
|
13
|
-
import { editorGuidKeyName } from "./engine_constants.js";
|
14
|
-
import { ComponentLifecycleEvents, ComponentEvents } from "./engine_components_internal.js";
|
15
16
|
|
16
17
|
const debug = getParam("debuggetcomponent");
|
17
18
|
const debugInstantiate = getParam("debuginstantiate");
|
@@ -32,9 +33,11 @@
|
|
32
33
|
idProvider?: UIDProvider;
|
33
34
|
//** parent guid or object */
|
34
35
|
parent?: string | Object3D;
|
36
|
+
/** position in local space. Set `keepWorldPosition` to true if this is world space */
|
35
37
|
position?: Vector3;
|
36
38
|
/** for duplicatable parenting */
|
37
39
|
keepWorldPosition?: boolean;
|
40
|
+
/** rotation in local space. Set `keepWorldPosition` to true if this is world space */
|
38
41
|
rotation?: Quaternion;
|
39
42
|
scale?: Vector3;
|
40
43
|
/** if the instantiated object should be visible */
|
@@ -137,23 +140,49 @@
|
|
137
140
|
go[$isDestroyed] = value;
|
138
141
|
}
|
139
142
|
|
143
|
+
const $isDontDestroy = Symbol("isDontDestroy");
|
144
|
+
|
145
|
+
/** Mark an Object3D or component as not destroyable
|
146
|
+
* @param instance the object to be marked as not destroyable
|
147
|
+
* @param value true if the object should not be destroyed in `destroy`
|
148
|
+
*/
|
149
|
+
export function setDontDestroy(instance: Object3D | Component, value: boolean = true) {
|
150
|
+
instance[$isDontDestroy] = value;
|
151
|
+
}
|
152
|
+
|
153
|
+
const destroyed_components: Array<IComponent> = [];
|
154
|
+
const destroyed_objects: Array<Object3D> = [];
|
140
155
|
export function destroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false) {
|
141
|
-
|
142
|
-
|
143
|
-
|
156
|
+
destroyed_components.length = 0;
|
157
|
+
destroyed_objects.length = 0;
|
158
|
+
internalDestroy(instance, recursive, dispose, true);
|
159
|
+
for (const comp of destroyed_components) {
|
144
160
|
comp.gameObject = null!;
|
145
161
|
//@ts-ignore
|
146
162
|
comp.context = null;
|
147
163
|
}
|
164
|
+
// dipose resources and remove references
|
165
|
+
for (const obj of destroyed_objects) {
|
166
|
+
setDestroyed(obj, true);
|
167
|
+
if (dispose) {
|
168
|
+
disposeObjectResources(obj);
|
169
|
+
}
|
170
|
+
// This needs to be called after disposing because it removes the references to resources
|
171
|
+
__internalRemoveReferences(obj);
|
172
|
+
}
|
173
|
+
destroyed_objects.length = 0;
|
174
|
+
destroyed_components.length = 0;
|
148
175
|
}
|
149
176
|
|
150
|
-
function internalDestroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false, isRoot: boolean = true
|
177
|
+
function internalDestroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false, isRoot: boolean = true) {
|
151
178
|
if (instance === null || instance === undefined)
|
152
179
|
return;
|
153
180
|
|
154
181
|
const comp = instance as Component;
|
155
182
|
if (comp.isComponent) {
|
156
|
-
|
183
|
+
// Handle Component
|
184
|
+
if (comp[$isDontDestroy]) return;
|
185
|
+
destroyed_components.push(comp);
|
157
186
|
const go = comp.gameObject;
|
158
187
|
comp.__internalDisable();
|
159
188
|
comp.__internalDestroy();
|
@@ -161,44 +190,34 @@
|
|
161
190
|
return;
|
162
191
|
}
|
163
192
|
|
193
|
+
// handle Object3D
|
194
|
+
if (instance[$isDontDestroy]) return;
|
164
195
|
|
165
196
|
const obj = instance as GameObject;
|
166
|
-
setDestroyed(obj, true);
|
167
|
-
if (dispose) {
|
168
|
-
disposeObjectResources(obj);
|
169
|
-
}
|
170
|
-
// This needs to be called after disposing because it removes the references to resources
|
171
|
-
__internalRemoveReferences(obj);
|
172
|
-
|
173
197
|
if (debug) console.log(obj);
|
198
|
+
destroyed_objects.push(obj);
|
174
199
|
|
175
|
-
|
176
|
-
for (const ch of obj.children) {
|
177
|
-
internalDestroy(ch, recursive, dispose, false, allComponents);
|
178
|
-
}
|
179
|
-
}
|
180
|
-
|
200
|
+
// first disable and call onDestroy on components
|
181
201
|
const components = obj.userData.components;
|
182
202
|
if (components) {
|
183
203
|
let lastLength = components.length;
|
184
204
|
for (let i = 0; i < components.length; i++) {
|
185
205
|
const comp: Component = components[i];
|
186
|
-
|
187
|
-
|
188
|
-
comp.__internalDisable();
|
189
|
-
comp.__internalDestroy();
|
190
|
-
comp.gameObject = go;
|
191
|
-
// if (comp.destroy) {
|
192
|
-
// if (debug) console.log("destroying", comp);
|
193
|
-
// comp.destroy();
|
194
|
-
// }
|
195
|
-
// components will be removed from componentlist in destroy
|
206
|
+
internalDestroy(comp, recursive, dispose, false);
|
207
|
+
// components will be removed from componentlist in destroy
|
196
208
|
if (components.length < lastLength) {
|
197
209
|
lastLength = components.length;
|
198
210
|
i--;
|
199
211
|
}
|
200
212
|
}
|
201
213
|
}
|
214
|
+
// then continue in children of the passed in object
|
215
|
+
if (recursive && obj.children) {
|
216
|
+
for (const ch of obj.children) {
|
217
|
+
internalDestroy(ch, recursive, dispose, false);
|
218
|
+
}
|
219
|
+
}
|
220
|
+
|
202
221
|
if (isRoot)
|
203
222
|
obj.removeFromParent();
|
204
223
|
}
|
@@ -266,9 +285,7 @@
|
|
266
285
|
clone: Object3D;
|
267
286
|
}
|
268
287
|
|
269
|
-
export function instantiate(instance: GameObject | Object3D
|
270
|
-
if (instance === null) return null;
|
271
|
-
|
288
|
+
export function instantiate(instance: GameObject | Object3D, opts: IInstantiateOptions | null = null): GameObject {
|
272
289
|
let options: InstantiateOptions | null = null;
|
273
290
|
if (opts !== null) {
|
274
291
|
// if x is defined assume this is a vec3 - this is just to not break everything at once and stay a little bit backwards compatible
|
@@ -279,13 +296,8 @@
|
|
279
296
|
else {
|
280
297
|
// if (opts instanceof InstantiateOptions)
|
281
298
|
options = opts as InstantiateOptions;
|
282
|
-
// else {
|
283
|
-
// options = new InstantiateOptions();
|
284
|
-
// Object.assign(options, opts);
|
285
|
-
// }
|
286
299
|
}
|
287
300
|
}
|
288
|
-
console.log(options?.position)
|
289
301
|
|
290
302
|
let context = Context.Current;
|
291
303
|
if (options?.context) context = options.context;
|
@@ -1,11 +1,13 @@
|
|
1
|
-
import {
|
1
|
+
import { AxesHelper,Box3, BoxGeometry, BufferAttribute, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Mesh, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
|
2
|
+
import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
|
3
|
+
import { type Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
|
4
|
+
|
5
|
+
import { isDestroyed } from './engine_gameobject.js';
|
2
6
|
import { Context } from './engine_setup.js';
|
3
7
|
import { getWorldPosition, lookAtObject, setWorldPositionXYZ } from './engine_three_utils.js';
|
4
8
|
import type { Vec3, Vec4 } from './engine_types.js';
|
5
|
-
import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
|
6
9
|
import { getParam } from './engine_utils.js';
|
7
|
-
import {
|
8
|
-
import { isDestroyed } from './engine_gameobject.js';
|
10
|
+
import { NeedleXRSession } from './engine_xr.js';
|
9
11
|
|
10
12
|
const _tmp = new Vector3();
|
11
13
|
const _tmp2 = new Vector3();
|
@@ -21,6 +23,15 @@
|
|
21
23
|
|
22
24
|
export class Gizmos {
|
23
25
|
|
26
|
+
/**
|
27
|
+
* Allow creating gizmos
|
28
|
+
* If disabled then no gizmos will be added to the scene anymore
|
29
|
+
*/
|
30
|
+
static enabled = true;
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Returns true if a given object is a gizmo
|
34
|
+
*/
|
24
35
|
static isGizmo(obj: Object3D) {
|
25
36
|
return obj[$cacheSymbol] !== undefined;
|
26
37
|
}
|
@@ -29,10 +40,12 @@
|
|
29
40
|
* Draw a label in the scene or attached to an object (if a parent is provided)
|
30
41
|
* @returns a handle to the label that can be used to change the text
|
31
42
|
*/
|
32
|
-
static DrawLabel(position: Vec3, text: string, size: number = .
|
43
|
+
static DrawLabel(position: Vec3, text: string, size: number = .05, duration: number = 0, color?: ColorRepresentation, backgroundColor?: ColorRepresentation | ColorWithAlpha, parent?: Object3D,) {
|
44
|
+
if (!Gizmos.enabled) return null;
|
33
45
|
if (!color) color = defaultColor;
|
34
|
-
const
|
35
|
-
|
46
|
+
const rigScale = NeedleXRSession.active?.rigScale ?? 1;
|
47
|
+
const element = Internal.getTextLabel(duration, text, size * rigScale, color, backgroundColor);
|
48
|
+
if (parent instanceof Object3D) parent.add(element as any);
|
36
49
|
element.position.x = position.x;
|
37
50
|
element.position.y = position.y;
|
38
51
|
element.position.z = position.z;
|
@@ -40,6 +53,7 @@
|
|
40
53
|
}
|
41
54
|
|
42
55
|
static DrawRay(origin: Vec3, dir: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
56
|
+
if (!Gizmos.enabled) return;
|
43
57
|
const obj = Internal.getLine(duration);
|
44
58
|
const positions = obj.geometry.getAttribute("position");
|
45
59
|
positions.setXYZ(0, origin.x, origin.y, origin.z);
|
@@ -52,6 +66,7 @@
|
|
52
66
|
}
|
53
67
|
|
54
68
|
static DrawDirection(pt: Vec3, direction: Vec3 | Vec4, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true, lengthFactor: number = 1) {
|
69
|
+
if (!Gizmos.enabled) return;
|
55
70
|
const obj = Internal.getLine(duration);
|
56
71
|
const positions = obj.geometry.getAttribute("position");
|
57
72
|
positions.setXYZ(0, pt.x, pt.y, pt.z);
|
@@ -73,8 +88,8 @@
|
|
73
88
|
}
|
74
89
|
|
75
90
|
static DrawLine(pt0: Vec3, pt1: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
91
|
+
if (!Gizmos.enabled) return;
|
76
92
|
const obj = Internal.getLine(duration);
|
77
|
-
|
78
93
|
const positions = obj.geometry.getAttribute("position");
|
79
94
|
positions.setXYZ(0, pt0.x, pt0.y, pt0.z);
|
80
95
|
positions.setXYZ(1, pt1.x, pt1.y, pt1.z);
|
@@ -85,6 +100,7 @@
|
|
85
100
|
}
|
86
101
|
|
87
102
|
static DrawWireSphere(center: Vec3, radius: number, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
103
|
+
if (!Gizmos.enabled) return;
|
88
104
|
const obj = Internal.getSphere(radius, duration, true);
|
89
105
|
setWorldPositionXYZ(obj, center.x, center.y, center.z);
|
90
106
|
obj.material["color"].set(color);
|
@@ -93,6 +109,7 @@
|
|
93
109
|
}
|
94
110
|
|
95
111
|
static DrawSphere(center: Vec3, radius: number, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
112
|
+
if (!Gizmos.enabled) return;
|
96
113
|
const obj = Internal.getSphere(radius, duration, false);
|
97
114
|
setWorldPositionXYZ(obj, center.x, center.y, center.z);
|
98
115
|
obj.material["color"].set(color);
|
@@ -101,6 +118,7 @@
|
|
101
118
|
}
|
102
119
|
|
103
120
|
static DrawWireBox(center: Vec3, size: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
121
|
+
if (!Gizmos.enabled) return;
|
104
122
|
const obj = Internal.getBox(duration);
|
105
123
|
obj.position.set(center.x, center.y, center.z);
|
106
124
|
obj.scale.set(size.x, size.y, size.z);
|
@@ -111,6 +129,7 @@
|
|
111
129
|
}
|
112
130
|
|
113
131
|
static DrawWireBox3(box: Box3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
132
|
+
if (!Gizmos.enabled) return;
|
114
133
|
const obj = Internal.getBox(duration);
|
115
134
|
obj.position.copy(box.getCenter(_tmp));
|
116
135
|
obj.scale.copy(box.getSize(_tmp));
|
@@ -122,6 +141,7 @@
|
|
122
141
|
|
123
142
|
private static _up = new Vector3(0, 1, 0);
|
124
143
|
static DrawArrow(pt0: Vec3, pt1: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true, wireframe: boolean = false) {
|
144
|
+
if (!Gizmos.enabled) return;
|
125
145
|
const obj = Internal.getArrowHead(duration);
|
126
146
|
obj.position.set(pt1.x, pt1.y, pt1.z);
|
127
147
|
obj.quaternion.setFromUnitVectors(this._up.set(0, 1, 0), _tmp.set(pt1.x, pt1.y, pt1.z).sub(_tmp2.set(pt0.x, pt0.y, pt0.z)).normalize());
|
@@ -188,12 +208,13 @@
|
|
188
208
|
width: "auto",
|
189
209
|
fontSize: size,
|
190
210
|
color: color,
|
191
|
-
lineHeight:
|
211
|
+
lineHeight: 1,
|
192
212
|
backgroundColor: backgroundColor ?? undefined,
|
193
213
|
backgroundOpacity: opacity,
|
194
214
|
textContent: text,
|
195
|
-
borderRadius:
|
196
|
-
padding:
|
215
|
+
borderRadius: .5 * size,
|
216
|
+
padding: .8 * size,
|
217
|
+
whiteSpace: 'pre',
|
197
218
|
};
|
198
219
|
|
199
220
|
if (!element) {
|
@@ -201,7 +222,7 @@
|
|
201
222
|
const global = this;
|
202
223
|
const labelHandle = element as LabelHandle & Text;
|
203
224
|
labelHandle.setText = function (str: string) {
|
204
|
-
this.set({ textContent: str
|
225
|
+
this.set({ textContent: str });
|
205
226
|
global.tmuiNeedsUpdate = true;
|
206
227
|
};
|
207
228
|
}
|
@@ -211,9 +232,7 @@
|
|
211
232
|
// handle.setText(text);
|
212
233
|
}
|
213
234
|
this.tmuiNeedsUpdate = true;
|
214
|
-
element.
|
215
|
-
element.layers.enable(2);
|
216
|
-
this.registerTimedObject(Context.Current, element, duration, this.textLabelCache);
|
235
|
+
this.registerTimedObject(Context.Current, element as any, duration, this.textLabelCache as any);
|
217
236
|
return element as Text & LabelHandle;
|
218
237
|
}
|
219
238
|
|
@@ -269,20 +288,43 @@
|
|
269
288
|
private static textLabelCache: Array<Text> = [];
|
270
289
|
|
271
290
|
private static registerTimedObject(context: Context, object: Object3D, duration: number, cache: Array<Object3D>) {
|
272
|
-
|
291
|
+
const beforeRender = this.contextBeforeRenderCallbacks.get(context);
|
292
|
+
const postRender = this.contextPostRenderCallbacks.get(context);
|
293
|
+
|
294
|
+
if (!beforeRender) {
|
295
|
+
const cb = () => { this.onBeforeRender(context, this.timedObjectsBuffer) };
|
296
|
+
this.contextBeforeRenderCallbacks.set(context, cb);
|
297
|
+
context.pre_render_callbacks.push(cb);
|
298
|
+
}
|
299
|
+
// make sure gizmo pre render is the last one being called
|
300
|
+
else if (context.pre_render_callbacks[context.pre_render_callbacks.length - 1] !== beforeRender) {
|
301
|
+
const index = context.pre_render_callbacks.indexOf(beforeRender);
|
302
|
+
if (index >= 0) {
|
303
|
+
context.pre_render_callbacks.splice(index, 1);
|
304
|
+
}
|
305
|
+
context.pre_render_callbacks.push(beforeRender);
|
306
|
+
}
|
307
|
+
|
308
|
+
if (!postRender) {
|
273
309
|
const cb = () => { this.onPostRender(context, this.timedObjectsBuffer, this.timesBuffer) };
|
274
310
|
this.contextPostRenderCallbacks.set(context, cb);
|
275
311
|
context.post_render_callbacks.push(cb);
|
276
312
|
}
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
313
|
+
// make sure gizmo post render is the last one being called
|
314
|
+
else if (context.post_render_callbacks[context.post_render_callbacks.length - 1] !== postRender) {
|
315
|
+
const index = context.post_render_callbacks.indexOf(postRender);
|
316
|
+
if (index >= 0) {
|
317
|
+
context.post_render_callbacks.splice(index, 1);
|
318
|
+
}
|
319
|
+
context.post_render_callbacks.push(postRender);
|
281
320
|
}
|
282
321
|
|
322
|
+
object.traverse(obj => {
|
323
|
+
obj.layers.disableAll();
|
324
|
+
obj.layers.enable(2);
|
325
|
+
});
|
326
|
+
|
283
327
|
object.renderOrder = 999999;
|
284
|
-
object.layers.disableAll();
|
285
|
-
object.layers.enable(2);
|
286
328
|
object[$cacheSymbol] = cache;
|
287
329
|
this.timedObjectsBuffer.push(object);
|
288
330
|
this.timesBuffer.push(Context.Current.time.realtimeSinceStartup + duration);
|
@@ -304,13 +346,13 @@
|
|
304
346
|
for (let i = 0; i < objects.length; i++) {
|
305
347
|
const obj = objects[i];
|
306
348
|
if (ctx.mainCamera && obj instanceof ThreeMeshUI.MeshUIBaseElement) {
|
307
|
-
if (isDestroyed(obj)) {
|
349
|
+
if (isDestroyed(obj as any)) {
|
308
350
|
continue;
|
309
351
|
}
|
310
352
|
const isInXR = ctx.isInVR;
|
311
|
-
const keepUp =
|
353
|
+
const keepUp = false;
|
312
354
|
const copyRotation = !isInXR;
|
313
|
-
lookAtObject(obj, ctx.mainCamera, keepUp, copyRotation);
|
355
|
+
lookAtObject(obj as any, ctx.mainCamera, keepUp, copyRotation);
|
314
356
|
}
|
315
357
|
}
|
316
358
|
}
|
@@ -323,7 +365,7 @@
|
|
323
365
|
objects.splice(i, 1);
|
324
366
|
times.splice(i, 1);
|
325
367
|
obj.removeFromParent();
|
326
|
-
if (isDestroyed(obj)
|
368
|
+
if (isDestroyed(obj) != true) {
|
327
369
|
const cache = obj[$cacheSymbol];
|
328
370
|
cache.push(obj);
|
329
371
|
}
|
@@ -1,18 +1,20 @@
|
|
1
1
|
import "./codegen/register_types.js";
|
2
|
-
|
2
|
+
|
3
|
+
import { Object3D } from "three";
|
4
|
+
|
5
|
+
import { LogType, showBalloonMessage } from "./debug/index.js";
|
6
|
+
import { addNewComponent } from "./engine_components.js";
|
7
|
+
import { builtinComponentKeyName,editorGuidKeyName } from "./engine_constants.js";
|
8
|
+
import { debugExtension } from "./engine_default_parameters.js";
|
3
9
|
import { InstantiateIdProvider } from "./engine_networking_instantiate.js"
|
4
|
-
import {
|
10
|
+
import { isLocalNetwork } from "./engine_networking_utils.js";
|
5
11
|
import { deserializeObject, serializeObject } from "./engine_serialization.js";
|
6
12
|
import { assign, ImplementationInformation, type ISerializable, SerializationContext } from "./engine_serialization_core.js";
|
7
|
-
import {
|
8
|
-
import { debugExtension } from "./engine_default_parameters.js";
|
9
|
-
import { editorGuidKeyName, builtinComponentKeyName } from "./engine_constants.js";
|
13
|
+
import { Context } from "./engine_setup.js";
|
10
14
|
import type { GuidsMap, ICamera, IComponent, IGameObject, SourceIdentifier, UIDProvider } from "./engine_types.js";
|
11
|
-
import {
|
15
|
+
import { TypeStore } from "./engine_typestore.js";
|
12
16
|
import { getParam } from "./engine_utils.js";
|
13
|
-
import {
|
14
|
-
import { isLocalNetwork } from "./engine_networking_utils.js";
|
15
|
-
import { Object3D } from "three";
|
17
|
+
import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
|
16
18
|
|
17
19
|
|
18
20
|
const debug = debugExtension;
|
@@ -1,8 +1,9 @@
|
|
1
|
+
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
2
|
+
|
3
|
+
import { SerializationContext } from "./engine_serialization_core.js";
|
4
|
+
import { Context } from "./engine_setup.js";
|
1
5
|
import type { ConstructorConcrete, SourceIdentifier, UIDProvider } from "./engine_types.js";
|
2
|
-
import { Context } from "./engine_setup.js";
|
3
6
|
import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
|
4
|
-
import { SerializationContext } from "./engine_serialization_core.js";
|
5
|
-
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
6
7
|
|
7
8
|
|
8
9
|
export interface INeedleGltfLoader {
|
@@ -1,8 +1,8 @@
|
|
1
|
+
import { addLog, LogType } from "./debug/debug_overlay.js";
|
2
|
+
import { addScriptToArrays, removeScriptFromContext } from "./engine_mainloop_utils.js"
|
1
3
|
import type { IComponent } from "./engine_types.js";
|
2
4
|
import { TypeStore } from "./engine_typestore.js";
|
3
|
-
import { addScriptToArrays, removeScriptFromContext } from "./engine_mainloop_utils.js"
|
4
5
|
import { getParam } from "./engine_utils.js";
|
5
|
-
import { addLog, LogType } from "./debug/debug_overlay.js";
|
6
6
|
|
7
7
|
const debug = getParam("debughotreload");
|
8
8
|
|
@@ -1,22 +1,137 @@
|
|
1
|
-
import { Vector2 } from 'three';
|
1
|
+
import { Matrix4, Object3D, Ray, Vector2, Vector3 } from 'three';
|
2
|
+
|
2
3
|
import { showBalloonMessage, showBalloonWarning } from './debug/debug.js';
|
3
4
|
import { Context } from './engine_setup.js';
|
4
|
-
import type { IInput, Vec2 } from './engine_types.js';
|
5
|
-
import { getParam } from './engine_utils.js';
|
5
|
+
import type { ButtonName, IGameObject, IInput, MouseButtonName, Vec2 } from './engine_types.js';
|
6
|
+
import { EnumToPrimitiveUnion, getParam, isMozillaXR } from './engine_utils.js';
|
6
7
|
|
7
8
|
const debug = getParam("debuginput");
|
8
9
|
|
10
|
+
|
11
|
+
export const enum PointerType {
|
12
|
+
Mouse = "mouse",
|
13
|
+
Touch = "touch",
|
14
|
+
Controller = "controller",
|
15
|
+
Hand = "hand"
|
16
|
+
}
|
17
|
+
export type PointerTypeNames = EnumToPrimitiveUnion<PointerType>;
|
18
|
+
|
19
|
+
const enum PointerEnumType {
|
20
|
+
PointerDown = "pointerdown",
|
21
|
+
PointerUp = "pointerup",
|
22
|
+
PointerMove = "pointermove",
|
23
|
+
}
|
24
|
+
const enum KeyboardEnumType {
|
25
|
+
KeyDown = "keydown",
|
26
|
+
KeyUp = "keyup",
|
27
|
+
KeyPressed = "keypress"
|
28
|
+
}
|
29
|
+
|
30
|
+
export const enum InputEvents {
|
31
|
+
PointerDown = "pointerdown",
|
32
|
+
PointerUp = "pointerup",
|
33
|
+
PointerMove = "pointermove",
|
34
|
+
KeyDown = "keydown",
|
35
|
+
KeyUp = "keyup",
|
36
|
+
KeyPressed = "keypress"
|
37
|
+
}
|
38
|
+
/** e.g. `pointerdown` */
|
39
|
+
export type InputEventNames = EnumToPrimitiveUnion<InputEvents>;
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
export declare type NEPointerEventInit = PointerEventInit &
|
44
|
+
{
|
45
|
+
origin: object;
|
46
|
+
pointerId: number;
|
47
|
+
/** the index of the device */
|
48
|
+
deviceIndex: number;
|
49
|
+
pointerType: PointerTypeNames;
|
50
|
+
mode: XRTargetRayMode,
|
51
|
+
ray?: Ray;
|
52
|
+
/** The control object for this input. In the case of spatial devices the controller,
|
53
|
+
* otherwise a generated object in screen space. The object may not be in the scene. */
|
54
|
+
device: IGameObject;
|
55
|
+
buttonName: ButtonName | "none";
|
56
|
+
}
|
57
|
+
|
58
|
+
|
9
59
|
export class NEPointerEvent extends PointerEvent {
|
60
|
+
|
61
|
+
/** the device index: mouse and touch are always 0, otherwise e.g. index of the connected Gamepad or XRController */
|
62
|
+
readonly deviceIndex: number;
|
63
|
+
|
64
|
+
/** The origin of the event contains a reference to the creator of this event.
|
65
|
+
* This can be the Needle Engine input system or e.g. a XR controller
|
66
|
+
*/
|
67
|
+
readonly origin: object;
|
68
|
+
|
69
|
+
/** the browser event that triggered this event (if any) */
|
10
70
|
readonly source: Event | null;
|
11
71
|
|
12
|
-
|
13
|
-
|
72
|
+
readonly mode: XRTargetRayMode;
|
73
|
+
/** A ray in worldspace for the event.
|
74
|
+
* If the ray is undefined you can also use `space.worldForward` and `space.worldPosition` */
|
75
|
+
readonly ray?: Ray;
|
76
|
+
/** The device space (this object is not necessarily rendered in the scene but you can access or copy the matrix)
|
77
|
+
* E.g. you can access the input world space source position with `space.worldPosition` or world direction with `space.worldForward`
|
78
|
+
*/
|
79
|
+
readonly space: IGameObject;
|
80
|
+
|
81
|
+
/** true if this event is a click */
|
82
|
+
isClick: boolean = false;
|
83
|
+
/** true if this event is a double click */
|
84
|
+
isDoubleClick: boolean = false;
|
85
|
+
|
86
|
+
|
87
|
+
/** Unique identifier for this input: a combination of the deviceIndex + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 101) */
|
88
|
+
override get pointerId(): number { return this._pointerid; }
|
89
|
+
private readonly _pointerid;
|
90
|
+
|
91
|
+
// this is set via the init arguments (we override it here for intellisense to show the string options)
|
92
|
+
override get pointerType(): PointerTypeNames { return this._pointerType; }
|
93
|
+
private readonly _pointerType: PointerTypeNames;
|
94
|
+
|
95
|
+
// this is set via the init arguments (we override it here for intellisense to show the string options)
|
96
|
+
/** The input that raised this event like `pointerdown` */
|
97
|
+
override get type(): InputEventNames { return this._type; }
|
98
|
+
private readonly _type: InputEventNames;
|
99
|
+
|
100
|
+
constructor(type: InputEvents | InputEventNames, source: Event | null, init: NEPointerEventInit) {
|
101
|
+
super(type, init);
|
102
|
+
// apply the init arguments. Otherwise the arguments will be undefined in the bundled / published version of needle engine
|
103
|
+
// so we have to be careful if we override properties - we then also need to set them in the constructor
|
104
|
+
this._pointerid = init.pointerId;
|
105
|
+
this._pointerType = init.pointerType;
|
106
|
+
this._type = type;
|
107
|
+
|
108
|
+
this.deviceIndex = init.deviceIndex;
|
109
|
+
this.origin = init.origin;
|
14
110
|
this.source = source;
|
111
|
+
this.mode = init.mode;
|
112
|
+
this.ray = init.ray;
|
113
|
+
this.space = init.device;
|
15
114
|
}
|
115
|
+
|
116
|
+
private _immediatePropagationStopped = false;
|
117
|
+
get immediatePropagationStopped() {
|
118
|
+
return this._immediatePropagationStopped;
|
119
|
+
}
|
120
|
+
private _propagationStopped = false;
|
121
|
+
get propagationStopped() {
|
122
|
+
return this._immediatePropagationStopped || this._propagationStopped;
|
123
|
+
}
|
124
|
+
|
16
125
|
stopImmediatePropagation(): void {
|
126
|
+
this._immediatePropagationStopped = true;
|
17
127
|
super.stopImmediatePropagation();
|
18
128
|
this.source?.stopImmediatePropagation();
|
19
129
|
}
|
130
|
+
stopPropagation(): void {
|
131
|
+
this._propagationStopped = true;
|
132
|
+
super.stopPropagation();
|
133
|
+
this.source?.stopPropagation();
|
134
|
+
}
|
20
135
|
}
|
21
136
|
export class NEKeyboardEvent extends KeyboardEvent {
|
22
137
|
source?: Event
|
@@ -41,22 +156,49 @@
|
|
41
156
|
}
|
42
157
|
}
|
43
158
|
|
44
|
-
export enum InputEvents {
|
45
|
-
PointerDown = "pointerdown",
|
46
|
-
PointerUp = "pointerup",
|
47
|
-
PointerMove = "pointermove",
|
48
|
-
KeyDown = "keydown",
|
49
|
-
KeyUp = "keyup",
|
50
|
-
KeyPressed = "keypress"
|
51
|
-
}
|
52
159
|
|
53
|
-
export enum PointerType {
|
54
|
-
Mouse = "mouse",
|
55
|
-
Touch = "touch",
|
56
|
-
}
|
57
160
|
|
58
|
-
|
161
|
+
declare type PointerEventListener = (evt: NEPointerEvent) => void;
|
162
|
+
declare type KeyboardEventListener = (evt: NEKeyboardEvent) => void;
|
163
|
+
declare type InputEventListener = PointerEventListener | KeyboardEventListener;
|
59
164
|
|
165
|
+
export class Input implements IInput {
|
166
|
+
|
167
|
+
private readonly _eventListeners: { [key: string]: InputEventListener[] } = {};
|
168
|
+
|
169
|
+
addEventListener(type: InputEvents | InputEventNames, callback: PointerEventListener): void {
|
170
|
+
if (!this._eventListeners[type]) this._eventListeners[type] = [];
|
171
|
+
this._eventListeners[type].push(callback);
|
172
|
+
}
|
173
|
+
removeEventListener(type: InputEvents | InputEventNames, callback: PointerEventListener): void {
|
174
|
+
if (!this._eventListeners[type]) return;
|
175
|
+
const index = this._eventListeners[type].indexOf(callback);
|
176
|
+
if (index >= 0) this._eventListeners[type].splice(index, 1);
|
177
|
+
}
|
178
|
+
private dispatchEvent(evt: NEPointerEvent | NEKeyboardEvent) {
|
179
|
+
if (evt instanceof NEKeyboardEvent) {
|
180
|
+
const listeners = this._eventListeners[evt.type];
|
181
|
+
if (listeners) {
|
182
|
+
for (const l of listeners) {
|
183
|
+
(l as KeyboardEventListener)(evt);
|
184
|
+
}
|
185
|
+
}
|
186
|
+
}
|
187
|
+
else if (evt instanceof NEPointerEvent) {
|
188
|
+
const listeners = this._eventListeners[evt.type];
|
189
|
+
if (listeners) {
|
190
|
+
for (const l of listeners) {
|
191
|
+
if (evt.immediatePropagationStopped) {
|
192
|
+
if (debug) console.log("immediatePropagationStopped", evt.type);
|
193
|
+
break;
|
194
|
+
}
|
195
|
+
(l as PointerEventListener)(evt);
|
196
|
+
}
|
197
|
+
}
|
198
|
+
}
|
199
|
+
}
|
200
|
+
|
201
|
+
|
60
202
|
_doubleClickTimeThreshold = .2;
|
61
203
|
_longPressTimeThreshold = 1;
|
62
204
|
|
@@ -243,7 +385,41 @@
|
|
243
385
|
private _mouseWheelDeltaY: number[] = [0];
|
244
386
|
private _pointerEvent: Event[] = [];
|
245
387
|
private _pointerUsed: boolean[] = [];
|
388
|
+
/** This is added/updated for pointers. screenspace pointers set this to the camera near plane */
|
389
|
+
private _pointerSpace: IGameObject[] = [];
|
246
390
|
|
391
|
+
|
392
|
+
|
393
|
+
private readonly _pressedStack = new Map<number, number[]>();
|
394
|
+
private onDownButton(pointerId: number, button: number) {
|
395
|
+
let stack = this._pressedStack.get(pointerId);
|
396
|
+
if (!stack) {
|
397
|
+
stack = [];
|
398
|
+
this._pressedStack.set(pointerId, stack);
|
399
|
+
}
|
400
|
+
stack.push(button);
|
401
|
+
}
|
402
|
+
private onReleaseButton(pointerId: number, button: number) {
|
403
|
+
const stack = this._pressedStack.get(pointerId);
|
404
|
+
if (!stack) return;
|
405
|
+
const index = stack.indexOf(button);
|
406
|
+
if (index >= 0) stack.splice(index, 1);
|
407
|
+
}
|
408
|
+
/** the first button that was down and is currently pressed */
|
409
|
+
getFirstPressedButtonForPointer(pointerId: number): number | undefined {
|
410
|
+
const stack = this._pressedStack.get(pointerId);
|
411
|
+
if (!stack) return undefined;
|
412
|
+
return stack[0];
|
413
|
+
}
|
414
|
+
/** the last (most recent) button that was down and is currently pressed */
|
415
|
+
getLatestPressedButtonForPointer(pointerId: number): number | undefined {
|
416
|
+
const stack = this._pressedStack.get(pointerId);
|
417
|
+
if (!stack) return undefined;
|
418
|
+
return stack[stack.length - 1];
|
419
|
+
}
|
420
|
+
|
421
|
+
|
422
|
+
|
247
423
|
getKeyDown(): string | null {
|
248
424
|
for (const key in this.keysPressed) {
|
249
425
|
const k = this.keysPressed[key];
|
@@ -313,39 +489,58 @@
|
|
313
489
|
return null;
|
314
490
|
}
|
315
491
|
|
316
|
-
|
317
|
-
|
318
|
-
|
492
|
+
createInputEvent(args: NEPointerEvent) {
|
493
|
+
// TODO: technically we would need to check for circular invocations here!
|
494
|
+
switch (args.type) {
|
495
|
+
case InputEvents.PointerDown:
|
496
|
+
if (debug) showBalloonMessage("Create Pointer down");
|
497
|
+
this.onDownButton(args.deviceIndex, args.button);
|
498
|
+
this.onDown(args);
|
499
|
+
break;
|
500
|
+
case InputEvents.PointerMove:
|
501
|
+
if (debug) showBalloonMessage("Create Pointer move");
|
502
|
+
this.onMove(args);
|
503
|
+
break;
|
504
|
+
case InputEvents.PointerUp:
|
505
|
+
if (debug) showBalloonMessage("Create Pointer up");
|
506
|
+
this.onUp(args);
|
507
|
+
this.onReleaseButton(args.deviceIndex, args.button);
|
508
|
+
break;
|
509
|
+
}
|
319
510
|
}
|
320
511
|
|
321
|
-
createPointerMove(args: NEPointerEvent) {
|
322
|
-
if (debug) showBalloonMessage("Create Pointer move");
|
323
|
-
this.onMove(args);
|
324
|
-
}
|
325
|
-
|
326
|
-
createPointerUp(args: NEPointerEvent) {
|
327
|
-
if (debug) showBalloonMessage("Create Pointer up");
|
328
|
-
this.onUp(args);
|
329
|
-
}
|
330
|
-
|
331
512
|
convertScreenspaceToRaycastSpace(vec2: Vec2) {
|
332
513
|
vec2.x = (vec2.x - this.context.domX) / this.context.domWidth * 2 - 1;
|
333
514
|
vec2.y = -((vec2.y - this.context.domY) / this.context.domHeight) * 2 + 1;
|
334
515
|
}
|
335
516
|
|
336
517
|
constructor(context: Context) {
|
337
|
-
super();
|
338
518
|
this.context = context;
|
339
519
|
this.context.post_render_callbacks.push(this.onEndOfFrame);
|
520
|
+
}
|
340
521
|
|
341
|
-
|
522
|
+
/** this is the html element we subscribed to for events */
|
523
|
+
private _htmlEventSource!: HTMLElement;
|
524
|
+
|
525
|
+
bindEvents() {
|
526
|
+
this.unbindEvents();
|
527
|
+
|
528
|
+
// we subscribe to the canvas element because we don't want to receive events when the user is interacting with the UI
|
529
|
+
// e.g. if we have slotted HTML elements in the needle engine DOM elements we don't want to receive input events for those
|
530
|
+
this._htmlEventSource = this.context.renderer.domElement;
|
531
|
+
|
532
|
+
window.addEventListener('contextmenu', this.onContextMenu);
|
533
|
+
|
534
|
+
this._htmlEventSource.addEventListener('touchstart', this.onTouchStart, false);
|
535
|
+
window.addEventListener('touchstart', this.onTouchStartWindow);
|
342
536
|
window.addEventListener('touchmove', this.onTouchMove, { passive: true });
|
343
537
|
window.addEventListener('touchend', this.onTouchUp, false);
|
538
|
+
window.addEventListener("touchcancel", this.onTouchCancel, false);
|
344
539
|
|
345
|
-
|
540
|
+
this._htmlEventSource.addEventListener('mousedown', this.onMouseDown);
|
346
541
|
window.addEventListener('mousemove', this.onMouseMove, false);
|
347
542
|
window.addEventListener('mouseup', this.onMouseUp, false);
|
348
|
-
|
543
|
+
this._htmlEventSource.addEventListener('wheel', this.onMouseWheel, { passive: true });
|
349
544
|
|
350
545
|
window.addEventListener("keydown", this.onKeyDown, false);
|
351
546
|
window.addEventListener("keypress", this.onKeyPressed, false);
|
@@ -355,18 +550,19 @@
|
|
355
550
|
window.addEventListener('blur', this.onLostFocus);
|
356
551
|
}
|
357
552
|
|
358
|
-
|
359
|
-
|
360
|
-
if (index >= 0) this.context.post_render_callbacks.splice(index, 1);
|
553
|
+
unbindEvents() {
|
554
|
+
window.removeEventListener('contextmenu', this.onContextMenu);
|
361
555
|
|
362
|
-
|
556
|
+
this._htmlEventSource?.removeEventListener('touchstart', this.onTouchStart, false);
|
557
|
+
window.removeEventListener('touchstart', this.onTouchStartWindow);
|
363
558
|
window.removeEventListener('touchmove', this.onTouchMove, false);
|
364
559
|
window.removeEventListener('touchend', this.onTouchUp, false);
|
560
|
+
window.removeEventListener("touchcancel", this.onTouchCancel, false);
|
365
561
|
|
366
|
-
|
562
|
+
this._htmlEventSource?.removeEventListener('mousedown', this.onMouseDown, false);
|
367
563
|
window.removeEventListener('mousemove', this.onMouseMove, false);
|
368
564
|
window.removeEventListener('mouseup', this.onMouseUp, false);
|
369
|
-
|
565
|
+
this._htmlEventSource?.removeEventListener('wheel', this.onMouseWheel, false);
|
370
566
|
|
371
567
|
window.removeEventListener("keydown", this.onKeyDown, false);
|
372
568
|
window.removeEventListener("keypress", this.onKeyPressed, false);
|
@@ -375,6 +571,12 @@
|
|
375
571
|
window.removeEventListener('blur', this.onLostFocus);
|
376
572
|
}
|
377
573
|
|
574
|
+
dispose() {
|
575
|
+
const index = this.context.post_render_callbacks.indexOf(this.onEndOfFrame);
|
576
|
+
if (index >= 0) this.context.post_render_callbacks.splice(index, 1);
|
577
|
+
this.unbindEvents();
|
578
|
+
}
|
579
|
+
|
378
580
|
private onLostFocus = () => {
|
379
581
|
for (const kp in this.keysPressed) {
|
380
582
|
this.keysPressed[kp].pressed = false;
|
@@ -403,17 +605,41 @@
|
|
403
605
|
// if(evt.target === this.context.renderer.domElement) return true;
|
404
606
|
// const css = window.getComputedStyle(evt.target as HTMLElement);
|
405
607
|
// if(css.pointerEvents === "all") return false;
|
406
|
-
|
407
608
|
// We only check the target elements here since the canvas may be overlapped by other elements
|
408
609
|
// in which case we do not want to use the input (e.g. if a HTML element is being triggered)
|
409
|
-
if(evt.target === this.context.renderer?.domElement) return true;
|
410
|
-
if(evt.target === this.context.domElement) return true;
|
610
|
+
if (evt.target === this.context.renderer?.domElement) return true;
|
611
|
+
if (evt.target === this.context.domElement) return true;
|
612
|
+
|
613
|
+
// looks like in Mozilla WebXR viewer the target element is the body
|
614
|
+
if (this.context.isInAR && evt.target === document.body && isMozillaXR()) return true;
|
615
|
+
|
411
616
|
return false;
|
412
617
|
}
|
413
618
|
|
619
|
+
private onContextMenu = (evt: Event) => {
|
620
|
+
if (this.canReceiveInput(evt) === false)
|
621
|
+
return;
|
622
|
+
if (evt instanceof PointerEvent) {
|
623
|
+
// for longpress on touch there might open a context menu
|
624
|
+
// in which case we set the pointer pressed back to false (resetting the pressed pointer)
|
625
|
+
// we need to emit a pointer up event here as well
|
626
|
+
if (evt.pointerType === "touch") {
|
627
|
+
// for (const index in this._pointerPressed) {
|
628
|
+
// if (this._pointerTypes[index] === PointerType.Touch) {
|
629
|
+
// // this._pointerPressed[index] = false;
|
630
|
+
// // this throws orbit controls?
|
631
|
+
// // const ne = this.createPointerEventFromTouch("pointerup", parseInt(index), this._pointerPositions[index].x, this._pointerPositions[index].y, 0, evt);
|
632
|
+
// // this.onUp(ne);
|
633
|
+
// }
|
634
|
+
// }
|
635
|
+
}
|
636
|
+
}
|
637
|
+
}
|
638
|
+
|
414
639
|
private keysPressed: { [key: KeyCode | string]: { pressed: boolean, frame: number, startFrame: number, key: string, code: KeyCode | string } } = {};
|
415
640
|
|
416
641
|
private onKeyDown = (evt: KeyboardEvent) => {
|
642
|
+
if (debug) console.log(`key down ${evt.code}, ${this.context.application.hasFocus}`, evt);
|
417
643
|
if (!this.context.application.hasFocus)
|
418
644
|
return;
|
419
645
|
const ex = this.keysPressed[evt.code];
|
@@ -453,6 +679,12 @@
|
|
453
679
|
this._mouseWheelDeltaY[0] = current + evt.deltaY;
|
454
680
|
}
|
455
681
|
|
682
|
+
private onTouchStartWindow = (evt: TouchEvent) => {
|
683
|
+
// onTouchStart registers on the renderer canvas so that we don't receive events when clicking on UI elements slotted in Needle Engine
|
684
|
+
// however in AR we need to handle these events on the window since they're not emitted otherwise (see NE-4098)
|
685
|
+
if (!this.context.isInAR) return;
|
686
|
+
this.onTouchStart(evt);
|
687
|
+
};
|
456
688
|
private onTouchStart = (evt: TouchEvent) => {
|
457
689
|
if (evt.changedTouches.length <= 0) return;
|
458
690
|
if (this.canReceiveInput(evt) === false) return;
|
@@ -460,7 +692,8 @@
|
|
460
692
|
const touch = evt.changedTouches[i];
|
461
693
|
const id = this.getPointerIndex(touch.identifier)
|
462
694
|
if (debug) showBalloonMessage(`touch start #${id}, identifier:${touch.identifier}`);
|
463
|
-
const
|
695
|
+
const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
|
696
|
+
const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space, pressure: touch.force });
|
464
697
|
this.onDown(ne);
|
465
698
|
}
|
466
699
|
}
|
@@ -469,8 +702,9 @@
|
|
469
702
|
if (evt.changedTouches.length <= 0) return;
|
470
703
|
for (let i = 0; i < evt.changedTouches.length; i++) {
|
471
704
|
const touch = evt.changedTouches[i];
|
472
|
-
const id = this.getPointerIndex(touch.identifier)
|
473
|
-
const
|
705
|
+
const id = this.getPointerIndex(touch.identifier);
|
706
|
+
const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
|
707
|
+
const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space, pressure: touch.force });
|
474
708
|
this.onMove(ne);
|
475
709
|
}
|
476
710
|
}
|
@@ -480,38 +714,96 @@
|
|
480
714
|
for (let i = 0; i < evt.changedTouches.length; i++) {
|
481
715
|
const touch = evt.changedTouches[i];
|
482
716
|
const id = this.getPointerIndex(touch.identifier);
|
483
|
-
|
484
717
|
if (!this.isNewEvent(evt.timeStamp, id, this._pointerUpTimestamp)) continue;
|
485
|
-
|
486
|
-
if (debug) showBalloonMessage(`touch up #${id}, identifier:${touch.identifier}`);
|
487
|
-
const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { button: id, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch });
|
718
|
+
const ne = this.createPointerEventFromTouch("pointerup", touch.identifier, touch.clientX, touch.clientY, touch.force, evt);
|
488
719
|
this.onUp(ne);
|
489
720
|
}
|
490
721
|
}
|
722
|
+
private createPointerEventFromTouch(type: InputEventNames, touchIdentifier: number, x: number, y: number, force: number, evt: Event): NEPointerEvent {
|
723
|
+
const id = this.getPointerIndex(touchIdentifier);
|
724
|
+
if (debug) showBalloonMessage(`touch up #${id}, identifier:${touchIdentifier}`);
|
725
|
+
const space = this.getAndUpdateSpatialObjectForScreenPosition(id, x, y);
|
726
|
+
const ne = new NEPointerEvent(type, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: x, clientY: y, pointerType: PointerType.Touch, buttonName: "unknown", device: space, pressure: force });
|
727
|
+
return ne;
|
728
|
+
}
|
491
729
|
|
730
|
+
private onTouchCancel = (_evt: Event) => {
|
731
|
+
};
|
732
|
+
|
492
733
|
private onMouseDown = (evt: MouseEvent) => {
|
734
|
+
this.onDownButton(0, evt.button);
|
735
|
+
if (this.context.isInVR) return;
|
493
736
|
if (evt.defaultPrevented) return;
|
494
737
|
if (this.canReceiveInput(evt) === false) return;
|
495
|
-
|
496
|
-
const
|
738
|
+
// TODO: if we have multiple mouse devices we need to get the deviceId
|
739
|
+
const button = evt.button;
|
740
|
+
let buttonName: MouseButtonName | "none" = "none";
|
741
|
+
switch (button) {
|
742
|
+
case 0: buttonName = "left"; break;
|
743
|
+
case 1: buttonName = "middle"; break;
|
744
|
+
case 2: buttonName = "right"; break;
|
745
|
+
}
|
746
|
+
const pointerId = 0 + button;
|
747
|
+
const space = this.getAndUpdateSpatialObjectForScreenPosition(pointerId, evt.clientX, evt.clientY);
|
748
|
+
const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space, pressure: 1 });
|
497
749
|
this.onDown(ne);
|
498
750
|
}
|
499
751
|
|
500
752
|
private onMouseMove = (evt: MouseEvent) => {
|
753
|
+
if (this.context.isInVR) return;
|
501
754
|
if (evt.defaultPrevented) return;
|
502
|
-
|
503
|
-
const
|
755
|
+
// take the last pressed button (or should the first pressed button have priority?)
|
756
|
+
const pressedButton = this.getFirstPressedButtonForPointer(0);
|
757
|
+
const button = pressedButton ?? 0;
|
758
|
+
const pointerId = 0 + button;
|
759
|
+
const space = this.getAndUpdateSpatialObjectForScreenPosition(pointerId, evt.clientX, evt.clientY);
|
760
|
+
const pressure = pressedButton !== undefined ? 1 : 0;
|
761
|
+
const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: "none", device: space, pressure });
|
504
762
|
this.onMove(ne);
|
505
763
|
}
|
506
764
|
|
507
765
|
private onMouseUp = (evt: MouseEvent) => {
|
766
|
+
this.onReleaseButton(0, evt.button);
|
767
|
+
if (this.context.isInVR) return;
|
768
|
+
const button = evt.button;
|
769
|
+
if (!this.isNewEvent(evt.timeStamp, button, this._pointerUpTimestamp)) return;
|
770
|
+
let buttonName: MouseButtonName | "none" = "none";
|
771
|
+
switch (button) {
|
772
|
+
case 0: buttonName = "left"; break;
|
773
|
+
case 1: buttonName = "middle"; break;
|
774
|
+
case 2: buttonName = "right"; break;
|
775
|
+
}
|
776
|
+
const pointerId = 0 + button;
|
508
777
|
if (evt.defaultPrevented) return;
|
509
|
-
const
|
510
|
-
|
511
|
-
const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse });
|
778
|
+
const space = this.getAndUpdateSpatialObjectForScreenPosition(pointerId, evt.clientX, evt.clientY);
|
779
|
+
const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space, pressure: 0 });
|
512
780
|
this.onUp(ne);
|
513
781
|
}
|
514
782
|
|
783
|
+
private readonly tempNearPlaneVector = new Vector3();
|
784
|
+
private readonly tempFarPlaneVector = new Vector3();
|
785
|
+
private readonly tempLookMatrix = new Matrix4();
|
786
|
+
private getAndUpdateSpatialObjectForScreenPosition(id: number, screenX: number, screenY: number): IGameObject {
|
787
|
+
let space = this._pointerSpace[id]
|
788
|
+
if (!space) {
|
789
|
+
space = new Object3D() as unknown as IGameObject;
|
790
|
+
this._pointerSpace[id] = space;
|
791
|
+
}
|
792
|
+
this._pointerSpace[id] = space;
|
793
|
+
const camera = this.context.mainCamera;
|
794
|
+
if (camera) {
|
795
|
+
const pointOnNearPlane = this.tempNearPlaneVector.set(screenX, screenY, -1);
|
796
|
+
this.convertScreenspaceToRaycastSpace(pointOnNearPlane);
|
797
|
+
const pointOnFarPlane = this.tempFarPlaneVector.set(pointOnNearPlane.x, pointOnNearPlane.y, 1);
|
798
|
+
pointOnNearPlane.unproject(camera);
|
799
|
+
pointOnFarPlane.unproject(camera);
|
800
|
+
this.tempLookMatrix.lookAt(pointOnFarPlane, pointOnNearPlane, (camera as any as IGameObject).worldUp);
|
801
|
+
space.position.set(pointOnNearPlane.x, pointOnNearPlane.y, pointOnNearPlane.z);
|
802
|
+
space.quaternion.setFromRotationMatrix(this.tempLookMatrix);
|
803
|
+
}
|
804
|
+
return space;
|
805
|
+
}
|
806
|
+
|
515
807
|
// Prevent the same event being handled twice (e.g. on touch we get a mouseUp and touchUp evt with the same timestamp)
|
516
808
|
private isNewEvent(timestamp: number, index: number, arr: number[]): boolean {
|
517
809
|
while (arr.length <= index) arr.push(-1);
|
@@ -532,12 +824,18 @@
|
|
532
824
|
}
|
533
825
|
|
534
826
|
private onDown(evt: NEPointerEvent) {
|
535
|
-
|
827
|
+
const index = evt.pointerId;
|
828
|
+
if (this.getPointerPressed(index)) {
|
829
|
+
console.warn(`pointerId is already pressed: ${index}`, debug ? evt : '');
|
830
|
+
}
|
831
|
+
if (debug) console.log(evt.pointerType, "DOWN", index);
|
536
832
|
if (!this.isInRect(evt)) return;
|
537
833
|
|
834
|
+
// TODO: this whole pointer handling doesnt consider that in VR we have multiple pointers. It's not enough to store the button as the pointer ID anymore
|
835
|
+
|
538
836
|
// check if we received an mouse UP event for a touch (for some reason we get a mouse.down for touch.up)
|
539
837
|
if (evt.pointerType === PointerType.Mouse) {
|
540
|
-
const upTime = this._pointerUpTimestamp[
|
838
|
+
const upTime = this._pointerUpTimestamp[index];
|
541
839
|
if (upTime > 0 && evt.source?.timeStamp !== undefined) {
|
542
840
|
const diff = (evt.source.timeStamp - upTime);
|
543
841
|
// on android touch up and mouse up have the exact same value
|
@@ -550,20 +848,20 @@
|
|
550
848
|
}
|
551
849
|
}
|
552
850
|
|
553
|
-
this.setPointerState(
|
554
|
-
this.setPointerState(
|
555
|
-
this.setPointerStateT(
|
851
|
+
this.setPointerState(index, this._pointerPressed, true);
|
852
|
+
this.setPointerState(index, this._pointerDown, true);
|
853
|
+
this.setPointerStateT(index, this._pointerEvent, evt.source);
|
556
854
|
|
557
|
-
while (
|
558
|
-
this._pointerTypes[
|
855
|
+
while (index >= this._pointerTypes.length) this._pointerTypes.push(evt.pointerType);
|
856
|
+
this._pointerTypes[index] = evt.pointerType;
|
559
857
|
|
560
|
-
while (
|
561
|
-
this._pointerPositionDown[
|
562
|
-
while (
|
563
|
-
this._pointerPositions[
|
858
|
+
while (index >= this._pointerPositionDown.length) this._pointerPositionDown.push(new Vector2());
|
859
|
+
this._pointerPositionDown[index].set(evt.clientX, evt.clientY);
|
860
|
+
while (index >= this._pointerPositions.length) this._pointerPositions.push(new Vector2());
|
861
|
+
this._pointerPositions[index].set(evt.clientX, evt.clientY);
|
564
862
|
|
565
|
-
if (
|
566
|
-
this._pointerDownTime[
|
863
|
+
if (index >= this._pointerDownTime.length) this._pointerDownTime.push(0);
|
864
|
+
this._pointerDownTime[index] = this.context.time.time;
|
567
865
|
|
568
866
|
this.updatePointerPosition(evt);
|
569
867
|
|
@@ -571,63 +869,60 @@
|
|
571
869
|
}
|
572
870
|
// moveEvent?: Event;
|
573
871
|
private onMove(evt: NEPointerEvent) {
|
574
|
-
const index = evt.
|
575
|
-
|
872
|
+
const index = evt.pointerId;
|
873
|
+
|
576
874
|
const isDown = this.getPointerPressed(index);
|
577
875
|
if (isDown === false && !this.isInRect(evt)) return;
|
578
876
|
if (evt.pointerType === PointerType.Touch && !isDown) return;
|
579
|
-
if (debug) console.log(evt.pointerType, "MOVE", index);
|
580
|
-
|
877
|
+
if (debug) console.log(evt.pointerType, "MOVE", index, "hasSpace=" + evt.space != null);
|
878
|
+
|
581
879
|
this.updatePointerPosition(evt);
|
582
880
|
this.setPointerStateT(index, this._pointerEvent, evt.source);
|
583
881
|
this.onDispatchEvent(evt);
|
584
882
|
}
|
585
883
|
private onUp(evt: NEPointerEvent) {
|
586
|
-
|
587
|
-
|
588
|
-
const wasDown = this._pointerPressed[evt.button];
|
884
|
+
const index = evt.pointerId;
|
885
|
+
const wasDown = this.getPointerPressed(index);
|
589
886
|
if (!wasDown) {
|
590
|
-
if (debug) console.log(evt.pointerType, "UP",
|
887
|
+
if (debug) console.log(evt.pointerType, "UP", index, "was not down");
|
591
888
|
return;
|
592
889
|
}
|
593
|
-
if (debug) console.log(evt.pointerType, "UP",
|
594
|
-
this.setPointerState(
|
595
|
-
this.setPointerStateT(
|
890
|
+
if (debug) console.log(evt.pointerType, "UP", index);
|
891
|
+
this.setPointerState(index, this._pointerPressed, false);
|
892
|
+
this.setPointerStateT(index, this._pointerEvent, evt.source);
|
893
|
+
this.setPointerState(index, this._pointerUp, true);
|
596
894
|
|
597
|
-
|
598
|
-
|
599
|
-
// return;
|
600
|
-
// }
|
601
|
-
this.setPointerState(evt.button, this._pointerUp, true);
|
895
|
+
while (index >= this._pointerUsed.length) this._pointerUsed.push(false);
|
896
|
+
this.setPointerState(index, this._pointerUsed, false);
|
602
897
|
|
603
|
-
while (evt.button >= this._pointerUsed.length) this._pointerUsed.push(false);
|
604
|
-
this.setPointerState(evt.button, this._pointerUsed, false);
|
605
|
-
|
606
898
|
this.updatePointerPosition(evt);
|
607
899
|
|
608
|
-
if (!this._pointerPositionDown[
|
609
|
-
if (debug) showBalloonWarning("Received pointer up event without matching down event for button: " +
|
610
|
-
console.warn("Received pointer up event without matching down event for button: " +
|
900
|
+
if (!this._pointerPositionDown[index]) {
|
901
|
+
if (debug) showBalloonWarning("Received pointer up event without matching down event for button: " + index);
|
902
|
+
console.warn("Received pointer up event without matching down event for button: " + index)
|
611
903
|
return;
|
612
904
|
}
|
613
|
-
const dx = evt.clientX - this._pointerPositionDown[
|
614
|
-
const dy = evt.clientY - this._pointerPositionDown[
|
905
|
+
const dx = evt.clientX - this._pointerPositionDown[index].x;
|
906
|
+
const dy = evt.clientY - this._pointerPositionDown[index].y;
|
615
907
|
|
616
|
-
if (
|
908
|
+
if (index >= this._pointerUpTime.length) this._pointerUpTime.push(-99);
|
617
909
|
|
618
|
-
|
910
|
+
|
619
911
|
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) {
|
620
|
-
|
912
|
+
if (debug) console.log("CLICK", index)
|
913
|
+
this.setPointerState(index, this._pointerClick, true);
|
914
|
+
evt.isClick = true;
|
621
915
|
|
622
916
|
// handle double click
|
623
|
-
const lastUp = this._pointerUpTime[
|
917
|
+
const lastUp = this._pointerUpTime[index];
|
624
918
|
const dt = this.context.time.time - lastUp;
|
625
919
|
// console.log(dt);
|
626
920
|
if (dt < this._doubleClickTimeThreshold && dt > 0) {
|
627
|
-
this.setPointerState(
|
921
|
+
this.setPointerState(index, this._pointerDoubleClick, true);
|
922
|
+
evt.isDoubleClick = true;
|
628
923
|
}
|
629
924
|
}
|
630
|
-
this._pointerUpTime[
|
925
|
+
this._pointerUpTime[index] = this.context.time.time;
|
631
926
|
|
632
927
|
this.onDispatchEvent(evt);
|
633
928
|
}
|
@@ -645,11 +940,11 @@
|
|
645
940
|
let dx = evt.clientX - lf.x;
|
646
941
|
let dy = evt.clientY - lf.y;
|
647
942
|
// if pointer is locked, clientX and Y are not changed, but Movement is.
|
648
|
-
if(evt.source instanceof MouseEvent || evt.source instanceof TouchEvent) {
|
943
|
+
if (evt.source instanceof MouseEvent || evt.source instanceof TouchEvent) {
|
649
944
|
const source = evt.source as PointerEvent;
|
650
|
-
if(dx === 0 && source.movementX !== 0)
|
945
|
+
if (dx === 0 && source.movementX !== 0)
|
651
946
|
dx = source.movementX || 0;
|
652
|
-
if(dy === 0 && source.movementY !== 0)
|
947
|
+
if (dy === 0 && source.movementY !== 0)
|
653
948
|
dy = source.movementY || 0;
|
654
949
|
}
|
655
950
|
delta.x += dx;
|
@@ -691,16 +986,16 @@
|
|
691
986
|
}
|
692
987
|
|
693
988
|
private setPointerState(index: number, arr: boolean[], value: boolean) {
|
694
|
-
while (arr.length <= index) arr.push(false);
|
695
989
|
arr[index] = value;
|
696
990
|
}
|
697
991
|
|
698
992
|
private setPointerStateT<T>(index: number, arr: T[], value: T) {
|
699
|
-
while (arr.length <= index) arr.push(null as any);
|
993
|
+
// while (arr.length <= index) arr.push(null as any);
|
700
994
|
arr[index] = value;
|
995
|
+
return value;
|
701
996
|
}
|
702
997
|
|
703
|
-
private onDispatchEvent(evt: NEPointerEvent |
|
998
|
+
private onDispatchEvent(evt: NEPointerEvent | NEKeyboardEvent) {
|
704
999
|
const prevContext = Context.Current;
|
705
1000
|
try {
|
706
1001
|
Context.Current = this.context;
|
@@ -800,81 +1095,81 @@
|
|
800
1095
|
| "F11"
|
801
1096
|
| "F12";
|
802
1097
|
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
1098
|
+
// KEY_1 = 49,
|
1099
|
+
// KEY_2 = 50,
|
1100
|
+
// KEY_3 = 51,
|
1101
|
+
// KEY_4 = 52,
|
1102
|
+
// KEY_5 = 53,
|
1103
|
+
// KEY_6 = 54,
|
1104
|
+
// KEY_7 = 55,
|
1105
|
+
// KEY_8 = 56,
|
1106
|
+
// KEY_9 = 57,
|
1107
|
+
// KEY_A = 65,
|
1108
|
+
// KEY_B = 66,
|
1109
|
+
// KEY_C = 67,
|
1110
|
+
// KEY_D = "d",
|
1111
|
+
// KEY_E = 69,
|
1112
|
+
// KEY_F = 70,
|
1113
|
+
// KEY_G = 71,
|
1114
|
+
// KEY_H = 72,
|
1115
|
+
// KEY_I = 73,
|
1116
|
+
// KEY_J = 74,
|
1117
|
+
// KEY_K = 75,
|
1118
|
+
// KEY_L = 76,
|
1119
|
+
// KEY_M = 77,
|
1120
|
+
// KEY_N = 78,
|
1121
|
+
// KEY_O = 79,
|
1122
|
+
// KEY_P = 80,
|
1123
|
+
// KEY_Q = 81,
|
1124
|
+
// KEY_R = 82,
|
1125
|
+
// KEY_S = 83,
|
1126
|
+
// KEY_T = 84,
|
1127
|
+
// KEY_U = 85,
|
1128
|
+
// KEY_V = 86,
|
1129
|
+
// KEY_W = 87,
|
1130
|
+
// KEY_X = 88,
|
1131
|
+
// KEY_Y = 89,
|
1132
|
+
// KEY_Z = 90,
|
1133
|
+
// LEFT_META = 91,
|
1134
|
+
// RIGHT_META = 92,
|
1135
|
+
// SELECT = 93,
|
1136
|
+
// NUMPAD_0 = 96,
|
1137
|
+
// NUMPAD_1 = 97,
|
1138
|
+
// NUMPAD_2 = 98,
|
1139
|
+
// NUMPAD_3 = 99,
|
1140
|
+
// NUMPAD_4 = 100,
|
1141
|
+
// NUMPAD_5 = 101,
|
1142
|
+
// NUMPAD_6 = 102,
|
1143
|
+
// NUMPAD_7 = 103,
|
1144
|
+
// NUMPAD_8 = 104,
|
1145
|
+
// NUMPAD_9 = 105,
|
1146
|
+
// MULTIPLY = 106,
|
1147
|
+
// ADD = 107,
|
1148
|
+
// SUBTRACT = 109,
|
1149
|
+
// DECIMAL = 110,
|
1150
|
+
// DIVIDE = 111,
|
1151
|
+
// F1 = 112,
|
1152
|
+
// F2 = 113,
|
1153
|
+
// F3 = 114,
|
1154
|
+
// F4 = 115,
|
1155
|
+
// F5 = 116,
|
1156
|
+
// F6 = 117,
|
1157
|
+
// F7 = 118,
|
1158
|
+
// F8 = 119,
|
1159
|
+
// F9 = 120,
|
1160
|
+
// F10 = 121,
|
1161
|
+
// F11 = 122,
|
1162
|
+
// F12 = 123,
|
1163
|
+
// NUM_LOCK = 144,
|
1164
|
+
// SCROLL_LOCK = 145,
|
1165
|
+
// SEMICOLON = 186,
|
1166
|
+
// EQUALS = 187,
|
1167
|
+
// COMMA = 188,
|
1168
|
+
// DASH = 189,
|
1169
|
+
// PERIOD = 190,
|
1170
|
+
// FORWARD_SLASH = 191,
|
1171
|
+
// GRAVE_ACCENT = 192,
|
1172
|
+
// OPEN_BRACKET = 219,
|
1173
|
+
// BACK_SLASH = 220,
|
1174
|
+
// CLOSE_BRACKET = 221,
|
1175
|
+
// SINGLE_QUOTE = 222
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import {
|
1
|
+
import { logoSVG } from "./assets/index.js";
|
2
|
+
import { GENERATOR, VERSION } from "./engine_constants.js";
|
2
3
|
import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
|
3
4
|
import type { IContext } from "./engine_types.js";
|
4
|
-
import {
|
5
|
-
import { GENERATOR, VERSION } from "./engine_constants.js";
|
5
|
+
import { getParam, isMobileDevice } from "./engine_utils.js";
|
6
6
|
|
7
7
|
const debug = getParam("debuglicense");
|
8
8
|
|
@@ -50,18 +50,21 @@
|
|
50
50
|
const licenseUrl = "https://engine.needle.tools/licensing/check?location=" + encodeURIComponent(window.location.href) + "&version=" + VERSION + "&generator=" + encodeURIComponent(GENERATOR);
|
51
51
|
const res = await fetch(licenseUrl, {
|
52
52
|
method: "GET",
|
53
|
-
}).catch(
|
53
|
+
}).catch(_err => {
|
54
|
+
if (debug) console.error("License check failed", _err);
|
55
|
+
return undefined;
|
56
|
+
});
|
54
57
|
if (res?.status === 200) {
|
55
58
|
applicationIsForbidden = false;
|
56
59
|
if (debug) console.log("License check succeeded");
|
57
60
|
NEEDLE_ENGINE_LICENSE_TYPE = "pro";
|
58
61
|
}
|
59
|
-
else if (res
|
62
|
+
else if (res?.status === 403) {
|
60
63
|
applicationIsForbidden = true;
|
61
64
|
applicationForbiddenText = await res.text();
|
62
65
|
}
|
63
66
|
else {
|
64
|
-
if (debug) console.log("License check failed with status " + res
|
67
|
+
if (debug) console.log("License check failed with status " + res?.status);
|
65
68
|
}
|
66
69
|
}
|
67
70
|
catch (err) {
|
@@ -136,23 +139,34 @@
|
|
136
139
|
const licenseDelay = 1200;
|
137
140
|
|
138
141
|
async function onNonCommercialVersionDetected(ctx: IContext) {
|
142
|
+
// if the engine loads faster than the license check, we need to capture the ready event here
|
143
|
+
let isReady = false;
|
144
|
+
ctx.domElement.addEventListener("ready", () => isReady = true);
|
145
|
+
|
139
146
|
await runtimeLicenseCheckPromise?.catch(() => { });
|
140
147
|
if (hasCommercialLicense()) return;
|
141
148
|
logNonCommercialUse();
|
142
|
-
|
149
|
+
|
150
|
+
// check if the engine is already ready (meaning has finished loading)
|
151
|
+
if (isReady) {
|
143
152
|
insertNonCommercialUseHint(ctx);
|
144
|
-
}
|
153
|
+
}
|
154
|
+
else {
|
155
|
+
ctx.domElement.addEventListener("ready", () => {
|
156
|
+
insertNonCommercialUseHint(ctx);
|
157
|
+
});
|
158
|
+
}
|
145
159
|
}
|
146
160
|
|
147
161
|
function insertNonCommercialUseHint(ctx: IContext) {
|
148
|
-
|
149
162
|
const licenseElement = createLicenseElement();
|
150
163
|
const style = createLicenseStyle();
|
151
164
|
|
152
165
|
const imgElement = document.createElement("img");
|
153
166
|
imgElement.src = logoSVG;
|
154
167
|
imgElement.classList.add("logo");
|
155
|
-
|
168
|
+
const imageElementCssText = `width: 55px; height: 55px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;`;
|
169
|
+
imgElement.style.cssText = imageElementCssText;
|
156
170
|
licenseElement.appendChild(imgElement);
|
157
171
|
|
158
172
|
const setAndUpdateStyle = () => {
|
@@ -165,9 +179,9 @@
|
|
165
179
|
if (imgElement.parentElement !== licenseElement) {
|
166
180
|
licenseElement.appendChild(imgElement);
|
167
181
|
}
|
168
|
-
if (imgElement.src !== logoSVG) {
|
182
|
+
if (imgElement.src !== logoSVG || imageElementCssText !== imgElement.style.cssText) {
|
169
183
|
imgElement.setAttribute("src", logoSVG);
|
170
|
-
imgElement.style.cssText =
|
184
|
+
imgElement.style.cssText = imageElementCssText
|
171
185
|
}
|
172
186
|
};
|
173
187
|
|
@@ -1,30 +1,54 @@
|
|
1
|
+
import { FrameEvent } from "./engine_context.js";
|
1
2
|
import { ContextEvent } from "./engine_context_registry.js";
|
2
|
-
import { FrameEvent } from "./engine_context.js";
|
3
3
|
import { LifecycleMethod, registerFrameEventCallback } from "./engine_lifecycle_functions_internal.js";
|
4
4
|
|
5
5
|
|
6
6
|
/**
|
7
7
|
* Register a callback in the engine context created event.
|
8
8
|
* This happens once per context (after the context has been created and the first content has been loaded)
|
9
|
-
|
9
|
+
* ```ts
|
10
|
+
* onInitialized((ctx : Context) => {
|
11
|
+
* // do something
|
12
|
+
* }
|
13
|
+
* ```
|
14
|
+
* */
|
10
15
|
export function onInitialized(cb: LifecycleMethod) {
|
11
16
|
registerFrameEventCallback(cb, ContextEvent.ContextCreated);
|
12
17
|
}
|
13
18
|
|
14
19
|
/** Register a callback in the engine start event.
|
15
|
-
* This happens at the beginning of each frame
|
20
|
+
* This happens at the beginning of each frame
|
21
|
+
* ```ts
|
22
|
+
* onStart((ctx : Context) => {
|
23
|
+
* // do something
|
24
|
+
* }
|
25
|
+
* ```
|
26
|
+
* */
|
16
27
|
export function onStart(cb: LifecycleMethod) {
|
17
28
|
registerFrameEventCallback(cb, FrameEvent.Start);
|
18
29
|
}
|
19
30
|
|
20
31
|
|
21
32
|
/** Register a callback in the engine update event
|
22
|
-
* This is called every frame
|
33
|
+
* This is called every frame
|
34
|
+
* ```ts
|
35
|
+
* onUpdate((ctx : Context) => {
|
36
|
+
* // do something
|
37
|
+
* }
|
38
|
+
* ```
|
23
39
|
* */
|
24
40
|
export function onUpdate(cb: LifecycleMethod) {
|
25
41
|
registerFrameEventCallback(cb, FrameEvent.Update);
|
26
42
|
}
|
27
43
|
|
44
|
+
/** Register a callback in the engine onBeforeRender event
|
45
|
+
* This is called every frame
|
46
|
+
* ```ts
|
47
|
+
* onBeforeRender((ctx : Context) => {
|
48
|
+
* // do something
|
49
|
+
* }
|
50
|
+
* ```
|
51
|
+
* */
|
28
52
|
export function onBeforeRender(cb: LifecycleMethod) {
|
29
53
|
registerFrameEventCallback(cb, FrameEvent.OnBeforeRender);
|
30
54
|
}
|
@@ -1,6 +1,6 @@
|
|
1
|
+
import { type Context,FrameEvent } from "./engine_context.js";
|
2
|
+
import type { ContextEvent } from "./engine_context_registry.js";
|
1
3
|
import { safeInvoke } from "./engine_generic_utils.js";
|
2
|
-
import { FrameEvent, type Context } from "./engine_context.js";
|
3
|
-
import type { ContextEvent } from "./engine_context_registry.js";
|
4
4
|
|
5
5
|
export declare type LifecycleMethod = (ctx: Context) => void;
|
6
6
|
export declare type Event = ContextEvent | FrameEvent;
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { ShaderChunk, Texture, UniformsLib, Vector4 } from "three";
|
2
|
+
|
3
3
|
import { Context } from "./engine_setup.js";
|
4
|
+
import type { SourceIdentifier } from "./engine_types.js";
|
4
5
|
import { getParam } from "./engine_utils.js";
|
5
|
-
import
|
6
|
+
import { LightmapType } from "./extensions/NEEDLE_lightmaps.js";
|
6
7
|
|
7
8
|
const debugLightmap = getParam("debuglightmaps") ? true : false;
|
8
9
|
|
@@ -1,10 +1,10 @@
|
|
1
1
|
|
2
|
-
import {
|
2
|
+
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
3
|
+
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
3
4
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
4
|
-
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
5
5
|
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
6
|
-
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
7
6
|
|
7
|
+
import { Context } from "./engine_setup.js"
|
8
8
|
import { getParam } from "./engine_utils.js";
|
9
9
|
|
10
10
|
const debug = getParam("debugdecoders");
|
@@ -1,11 +1,13 @@
|
|
1
|
+
import { CubeCamera, Object3D, Scene, WebGLCubeRenderTarget } from 'three';
|
2
|
+
|
3
|
+
import { isDevEnvironment } from "./debug/index.js";
|
4
|
+
import * as constants from "./engine_constants.js";
|
5
|
+
import { ContextRegistry } from "./engine_context_registry.js";
|
6
|
+
import { isActiveSelf } from './engine_gameobject.js';
|
1
7
|
import { safeInvoke } from "./engine_generic_utils.js";
|
2
|
-
import
|
8
|
+
import type { IComponent, IContext } from './engine_types.js';
|
3
9
|
import { getParam } from './engine_utils.js';
|
4
|
-
import {
|
5
|
-
import type { IComponent, IContext } from './engine_types.js';
|
6
|
-
import { isActiveSelf } from './engine_gameobject.js';
|
7
|
-
import { ContextRegistry } from "./engine_context_registry.js";
|
8
|
-
import { isDevEnvironment } from "./debug/index.js";
|
10
|
+
import type { INeedleXRSessionEventReceiver } from "./engine_xr.js";
|
9
11
|
|
10
12
|
const debug = getParam("debugnewscripts");
|
11
13
|
const debugHierarchy = getParam("debughierarchy");
|
@@ -208,9 +210,12 @@
|
|
208
210
|
if (script.onBeforeRender) context.scripts_onBeforeRender.push(script);
|
209
211
|
if (script.onAfterRender) context.scripts_onAfterRender.push(script);
|
210
212
|
if (script.onPausedChanged) context.scripts_pausedChanged.push(script);
|
213
|
+
if (isNeedleXRSessionEventReceiver(script, null)) context.new_scripts_xr.push(script);
|
214
|
+
// do we want to check if a XR session is active before adding scripts here?
|
215
|
+
if (isNeedleXRSessionEventReceiver(script, "immersive-vr")) context.scripts_immersive_vr.push(script);
|
216
|
+
if (isNeedleXRSessionEventReceiver(script, "immersive-ar")) context.scripts_immersive_ar.push(script);
|
211
217
|
}
|
212
218
|
|
213
|
-
|
214
219
|
export function removeScriptFromContext(script: any, context: IContext) {
|
215
220
|
removeFromArray(script, context.new_scripts);
|
216
221
|
removeFromArray(script, context.new_script_start);
|
@@ -221,6 +226,9 @@
|
|
221
226
|
removeFromArray(script, context.scripts_onBeforeRender);
|
222
227
|
removeFromArray(script, context.scripts_onAfterRender);
|
223
228
|
removeFromArray(script, context.scripts_pausedChanged);
|
229
|
+
removeFromArray(script, context.new_scripts_xr);
|
230
|
+
removeFromArray(script, context.scripts_immersive_vr);
|
231
|
+
removeFromArray(script, context.scripts_immersive_ar);
|
224
232
|
context.stopAllCoroutinesFrom(script);
|
225
233
|
}
|
226
234
|
|
@@ -229,7 +237,26 @@
|
|
229
237
|
if (index >= 0) array.splice(index, 1);
|
230
238
|
}
|
231
239
|
|
240
|
+
export function isNeedleXRSessionEventReceiver(script: any, mode: XRSessionMode | null): script is INeedleXRSessionEventReceiver {
|
241
|
+
if (script) {
|
242
|
+
const i = script as Partial<INeedleXRSessionEventReceiver>;
|
243
|
+
if (i.onBeforeXR ||
|
244
|
+
i.onEnterXR ||
|
245
|
+
i.onUpdateXR ||
|
246
|
+
i.onLeaveXR ||
|
247
|
+
i.onXRControllerAdded ||
|
248
|
+
i.onXRControllerRemoved
|
249
|
+
) {
|
250
|
+
if (mode != null) {
|
251
|
+
if (i.supportsXR?.(mode) === false) return false;
|
252
|
+
}
|
253
|
+
return true;
|
254
|
+
}
|
255
|
+
}
|
256
|
+
return false;
|
257
|
+
}
|
232
258
|
|
259
|
+
|
233
260
|
export function updateIsActive(obj?: Object3D) {
|
234
261
|
if (!obj) obj = ContextRegistry.Current.scene;
|
235
262
|
if (!obj) {
|
@@ -1,6 +1,6 @@
|
|
1
|
-
import { getParam } from "./engine_utils.js";
|
2
1
|
import { isDevEnvironment } from "./debug/index.js";
|
3
2
|
import type { IComponent } from "./engine_types.js";
|
3
|
+
import { getParam } from "./engine_utils.js";
|
4
4
|
|
5
5
|
const debug = getParam("debugautosync");
|
6
6
|
|
@@ -1,8 +1,9 @@
|
|
1
1
|
// import { SyncedTransform } from "../engine-components/SyncedTransform.js";
|
2
2
|
// import { DragControls } from "../engine-components/DragControls.js"
|
3
3
|
// import { ObjectRaycaster } from "../engine-components/ui/Raycaster.js";
|
4
|
+
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
5
|
+
|
4
6
|
import type { UIDProvider } from "./engine_types.js";
|
5
|
-
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
6
7
|
// import { Animation } from "../engine-components/Animation.js";
|
7
8
|
|
8
9
|
|
@@ -1,15 +1,16 @@
|
|
1
|
+
import { BoxGeometry, BoxHelper, Mesh, MeshBasicMaterial, Object3D, Vector3 } from "three";
|
2
|
+
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
3
|
+
|
4
|
+
import { getLoader } from "../engine/engine_gltf.js";
|
5
|
+
import { NetworkConnection } from "../engine/engine_networking.js";
|
6
|
+
import { generateSeed, InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
1
7
|
import { Context } from "../engine/engine_setup.js";
|
2
8
|
import * as web from "../engine/engine_web_api.js";
|
3
|
-
import {
|
4
|
-
import {
|
9
|
+
import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
|
10
|
+
import { findByGuid } from "./engine_gameobject.js";
|
5
11
|
import * as def from "./engine_networking_files_default_components.js"
|
6
|
-
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
7
|
-
import { getLoader } from "../engine/engine_gltf.js";
|
8
12
|
import type { IModel } from "./engine_networking_types.js";
|
9
13
|
import type { IGameObject } from "./engine_types.js";
|
10
|
-
import { findByGuid } from "./engine_gameobject.js";
|
11
|
-
import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
|
12
|
-
import { BoxGeometry, BoxHelper, Mesh, MeshBasicMaterial, Object3D, Vector3 } from "three";
|
13
14
|
|
14
15
|
export enum File_Event {
|
15
16
|
File_Spawned = "file-spawned",
|
@@ -1,20 +1,20 @@
|
|
1
1
|
// import { IModel, NetworkConnection } from "./engine_networking.js"
|
2
2
|
import * as THREE from "three";
|
3
|
-
import {
|
4
|
-
import * as utils from "./engine_utils.js"
|
5
|
-
import type { INetworkConnection } from "./engine_networking_types.js";
|
6
|
-
import type { IGameObject as GameObject, IComponent as Component } from "./engine_types.js"
|
7
|
-
|
3
|
+
import { Object3D } from "three";
|
8
4
|
// https://github.com/uuidjs/uuid
|
9
5
|
// v5 takes string and namespace
|
10
6
|
import { v5 } from 'uuid';
|
11
|
-
|
7
|
+
|
8
|
+
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
9
|
+
import { destroy, findByGuid, IInstantiateOptions, instantiate } from "./engine_gameobject.js";
|
10
|
+
import { InstantiateOptions } from "./engine_gameobject.js";
|
11
|
+
import type { INetworkConnection } from "./engine_networking_types.js";
|
12
12
|
import type { IModel } from "./engine_networking_types.js";
|
13
13
|
import { SendQueue } from "./engine_networking_types.js";
|
14
|
-
import {
|
15
|
-
import {
|
16
|
-
import {
|
17
|
-
import
|
14
|
+
import { Context } from "./engine_setup.js"
|
15
|
+
import type { IComponent as Component,IGameObject as GameObject } from "./engine_types.js"
|
16
|
+
import type { UIDProvider } from "./engine_types.js";
|
17
|
+
import * as utils from "./engine_utils.js"
|
18
18
|
|
19
19
|
|
20
20
|
|
@@ -163,7 +163,7 @@
|
|
163
163
|
}
|
164
164
|
}
|
165
165
|
|
166
|
-
class NewInstanceModel implements IModel {
|
166
|
+
export class NewInstanceModel implements IModel {
|
167
167
|
guid: string;
|
168
168
|
originalGuid: string;
|
169
169
|
seed: number | undefined;
|
@@ -176,6 +176,9 @@
|
|
176
176
|
rotation: { x: number, y: number, z: number, w: number } | undefined;
|
177
177
|
scale: { x: number, y: number, z: number } | undefined;
|
178
178
|
|
179
|
+
/** Set to true to prevent this model from being instantiated */
|
180
|
+
preventCreation?: boolean = undefined;
|
181
|
+
|
179
182
|
constructor(originalGuid: string, newGuid: string) {
|
180
183
|
this.originalGuid = originalGuid;
|
181
184
|
this.guid = newGuid;
|
@@ -249,11 +252,13 @@
|
|
249
252
|
export function beginListenInstantiate(context: Context) {
|
250
253
|
context.connection.beginListen(InstantiateEvent.NewInstanceCreated, async (model: NewInstanceModel) => {
|
251
254
|
const obj: GameObject | null = await tryResolvePrefab(model.originalGuid, context.scene) as GameObject;
|
255
|
+
if (model.preventCreation === true) {
|
256
|
+
return;
|
257
|
+
}
|
252
258
|
if (!obj) {
|
253
259
|
console.warn("could not find object that was instantiated: " + model.guid);
|
254
260
|
return;
|
255
261
|
}
|
256
|
-
// console.log(model);
|
257
262
|
const options = new InstantiateOptions();
|
258
263
|
if (model.position)
|
259
264
|
options.position = new THREE.Vector3(model.position.x, model.position.y, model.position.z);
|
@@ -1,5 +1,6 @@
|
|
1
|
+
import type { DataConnection, PeerJSOption } from "peerjs";
|
1
2
|
import Peer, { type PeerConnectOption } from "peerjs";
|
2
|
-
|
3
|
+
|
3
4
|
import { type ConstructorConcrete } from "./engine_types.js";
|
4
5
|
|
5
6
|
let peerOptions: PeerJSOption | undefined = undefined;
|
@@ -1,12 +1,13 @@
|
|
1
|
-
import { type Context } from "./engine_context.js";
|
2
1
|
import Peer, { MediaConnection } from "peerjs"
|
2
|
+
import { EventDispatcher } from "three";
|
3
|
+
|
3
4
|
import { RoomEvents } from "../engine/engine_networking.js";
|
4
5
|
import { UserJoinedOrLeftRoomModel } from "../engine/engine_networking.js";
|
6
|
+
import { getPeerjsInstance } from "../engine/engine_networking_peer.js";
|
7
|
+
import { type Context } from "./engine_context.js";
|
5
8
|
import type { IModel } from "./engine_networking_types.js";
|
6
|
-
import {
|
7
|
-
import { EventDispatcher } from "three";
|
9
|
+
import { type IComponent } from "./engine_types.js";
|
8
10
|
import { getParam } from "./engine_utils.js";
|
9
|
-
import { type IComponent } from "./engine_types.js";
|
10
11
|
|
11
12
|
|
12
13
|
|
@@ -56,7 +57,7 @@
|
|
56
57
|
Outgoing = "outgoing",
|
57
58
|
}
|
58
59
|
|
59
|
-
class CallHandle extends EventDispatcher {
|
60
|
+
class CallHandle extends EventDispatcher<any> {
|
60
61
|
readonly userId: string;
|
61
62
|
readonly direction: CallDirection;
|
62
63
|
readonly call: MediaConnection;
|
@@ -105,7 +106,7 @@
|
|
105
106
|
}
|
106
107
|
}
|
107
108
|
|
108
|
-
export class PeerHandle extends EventDispatcher {
|
109
|
+
export class PeerHandle extends EventDispatcher<any> {
|
109
110
|
|
110
111
|
private static readonly instances: Map<string, PeerHandle> = new Map();
|
111
112
|
|
@@ -305,7 +306,7 @@
|
|
305
306
|
// userId: string;
|
306
307
|
// }
|
307
308
|
|
308
|
-
export class NetworkedStreams extends EventDispatcher {
|
309
|
+
export class NetworkedStreams extends EventDispatcher<any> {
|
309
310
|
|
310
311
|
static create(comp: IComponent) {
|
311
312
|
const peer = PeerHandle.getOrCreate(comp.context, comp.context.connection.connectionId!);
|
@@ -1,19 +1,21 @@
|
|
1
1
|
const defaultNetworkingBackendUrlProvider = "https://urls.needle.tools/default-networking-backend/index";
|
2
2
|
let serverUrl: string | undefined = "wss://needle-tiny-starter.glitch.me/socket";
|
3
3
|
|
4
|
-
import { Websocket, type WebsocketBuilder } from 'websocket-ts';
|
5
|
-
// import { Networking } from '../engine-components/Networking.js';
|
6
|
-
import { Context } from './engine_setup.js';
|
7
|
-
import * as utils from "./engine_utils.js";
|
8
4
|
import * as flatbuffers from 'flatbuffers';
|
5
|
+
import { type Websocket } from 'websocket-ts';
|
6
|
+
|
9
7
|
import * as schemes from "../engine-schemes/schemes.js";
|
8
|
+
import { isDevEnvironment } from './debug/debug.js';
|
10
9
|
import { PeerNetworking } from './engine_networking_peer.js';
|
11
10
|
import { type IModel, type INetworkConnection, SendQueue } from './engine_networking_types.js';
|
12
11
|
import { isHostedOnGlitch } from './engine_networking_utils.js';
|
13
|
-
import {
|
12
|
+
// import { Networking } from '../engine-components/Networking.js';
|
13
|
+
import { Context } from './engine_setup.js';
|
14
|
+
import * as utils from "./engine_utils.js";
|
14
15
|
|
15
16
|
export const debugNet = utils.getParam("debugnet") ? true : false;
|
16
17
|
export const debugOwner = debugNet || utils.getParam("debugowner") ? true : false;
|
18
|
+
const debugnetBin = utils.getParam("debugnetbin");
|
17
19
|
|
18
20
|
export interface INetworkingWebsocketUrlProvider {
|
19
21
|
getWebsocketUrl(): string | null;
|
@@ -389,7 +391,7 @@
|
|
389
391
|
|
390
392
|
/** Send a binary message to the server (broadcasted to all connected users) */
|
391
393
|
public sendBinary(bin: Uint8Array) {
|
392
|
-
if (
|
394
|
+
if (debugnetBin) console.log("<< send binary", this.context.time.frame, (bin.length / 1024) + " KB");
|
393
395
|
this._ws?.send(bin);
|
394
396
|
}
|
395
397
|
|
@@ -547,10 +549,11 @@
|
|
547
549
|
console.error("⊠Websocket error", i, ev);
|
548
550
|
resolve(false);
|
549
551
|
})
|
550
|
-
.onMessage(this.onMessage.bind(this))
|
551
552
|
.onRetry(() => { console.log("Retry connecting to networking websocket") })
|
552
553
|
.build();
|
553
|
-
|
554
|
+
ws.addEventListener(pkg.WebsocketEvent.message, (socket, msg) => {
|
555
|
+
this.onMessage(socket, msg);
|
556
|
+
});
|
554
557
|
});
|
555
558
|
}
|
556
559
|
|
@@ -581,6 +584,7 @@
|
|
581
584
|
}
|
582
585
|
|
583
586
|
private async handleIncomingBinaryMessage(blob: Blob) {
|
587
|
+
if (debugnetBin) console.log("<< bin", this.context.time.frame);
|
584
588
|
const buf = await blob.arrayBuffer();
|
585
589
|
var data = new Uint8Array(buf);
|
586
590
|
const bb = new flatbuffers.ByteBuffer(data);
|
@@ -1,29 +1,29 @@
|
|
1
|
+
import { ActiveCollisionTypes, ActiveEvents, Ball, CoefficientCombineRule, Collider, ColliderDesc, Cuboid, EventQueue, JointData, QueryFilterFlags, Ray, RigidBody, RigidBodyType, ShapeColliderTOI, ShapeType, World } from '@dimforge/rapier3d-compat';
|
1
2
|
import { BufferAttribute, BufferGeometry, LineBasicMaterial, LineSegments, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from 'three'
|
2
3
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
3
|
-
|
4
|
+
|
5
|
+
import { CollisionDetectionMode, type PhysicsMaterial, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
|
6
|
+
import { isDevEnvironment } from './debug/debug.js';
|
7
|
+
import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
|
8
|
+
import { foreachComponent } from './engine_gameobject.js';
|
9
|
+
import { Gizmos } from './engine_gizmos.js';
|
10
|
+
import { Mathf } from './engine_math.js';
|
4
11
|
import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPositionXYZ, setWorldQuaternionXYZW } from "./engine_three_utils.js"
|
5
12
|
import type {
|
13
|
+
IBoxCollider,
|
14
|
+
ICollider,
|
15
|
+
IComponent,
|
16
|
+
IContext,
|
17
|
+
IGameObject,
|
6
18
|
IPhysicsEngine,
|
7
|
-
IComponent,
|
8
|
-
ICollider,
|
9
19
|
IRigidbody,
|
20
|
+
ISphereCollider,
|
21
|
+
Vec2,
|
10
22
|
Vec3,
|
11
|
-
IGameObject,
|
12
|
-
Vec2,
|
13
|
-
IContext,
|
14
|
-
ISphereCollider,
|
15
|
-
IBoxCollider,
|
16
23
|
} from './engine_types.js';
|
17
|
-
import { ContactPoint
|
18
|
-
import { foreachComponent } from './engine_gameobject.js';
|
19
|
-
|
20
|
-
import { ActiveCollisionTypes, ActiveEvents, CoefficientCombineRule, Ball, Collider, ColliderDesc, EventQueue, JointData, QueryFilterFlags, RigidBody, RigidBodyType, ShapeColliderTOI, World, Ray, ShapeType, Cuboid } from '@dimforge/rapier3d-compat';
|
21
|
-
import { CollisionDetectionMode, type PhysicsMaterial, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
|
22
|
-
import { Gizmos } from './engine_gizmos.js';
|
23
|
-
import { Mathf } from './engine_math.js';
|
24
|
+
import { Collision,ContactPoint } from './engine_types.js';
|
24
25
|
import { SphereOverlapResult } from './engine_types.js';
|
25
|
-
import {
|
26
|
-
import { isDevEnvironment } from './debug/debug.js';
|
26
|
+
import { CircularBuffer, getParam } from "./engine_utils.js"
|
27
27
|
|
28
28
|
const debugPhysics = getParam("debugphysics");
|
29
29
|
const debugColliderPlacement = getParam("debugcolliderplacement");
|
@@ -166,12 +166,14 @@
|
|
166
166
|
addForce(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
|
167
167
|
this.validate();
|
168
168
|
const body = this.internal_getRigidbody(rigidbody);
|
169
|
-
body
|
169
|
+
if(body) body.addForce(force, wakeup)
|
170
|
+
else console.warn("Rigidbody doesn't exist: can not apply force");
|
170
171
|
}
|
171
172
|
addImpulse(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
|
172
173
|
this.validate();
|
173
174
|
const body = this.internal_getRigidbody(rigidbody);
|
174
|
-
body
|
175
|
+
if (body) body.applyImpulse(force, wakeup);
|
176
|
+
else console.warn("Rigidbody doesn't exist: can not apply impulse");
|
175
177
|
}
|
176
178
|
getLinearVelocity(comp: IRigidbody | ICollider): Vec3 | null {
|
177
179
|
this.validate();
|
@@ -204,13 +206,15 @@
|
|
204
206
|
applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
|
205
207
|
this.validate();
|
206
208
|
const body = this.internal_getRigidbody(rb);
|
207
|
-
body
|
209
|
+
if(body) body.applyImpulse(vec, wakeup);
|
210
|
+
else console.warn("Rigidbody doesn't exist: can not apply impulse");
|
208
211
|
}
|
209
212
|
|
210
213
|
wakeup(rb: IRigidbody) {
|
211
214
|
this.validate();
|
212
215
|
const body = this.internal_getRigidbody(rb);
|
213
|
-
body
|
216
|
+
if(body) body.wakeUp();
|
217
|
+
else console.warn("Rigidbody doesn't exist: can not wake up");
|
214
218
|
}
|
215
219
|
isSleeping(rb: IRigidbody) {
|
216
220
|
this.validate();
|
@@ -220,12 +224,14 @@
|
|
220
224
|
setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
|
221
225
|
this.validate();
|
222
226
|
const body = this.internal_getRigidbody(rb);
|
223
|
-
body
|
227
|
+
if(body) body.setAngvel(vec, wakeup);
|
228
|
+
else console.warn("Rigidbody doesn't exist: can not set angular velocity");
|
224
229
|
}
|
225
230
|
setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
|
226
231
|
this.validate();
|
227
232
|
const body = this.internal_getRigidbody(rb);
|
228
|
-
body
|
233
|
+
if(body) body.setLinvel(vec, wakeup);
|
234
|
+
else console.warn("Rigidbody doesn't exist: can not set linear velocity");
|
229
235
|
}
|
230
236
|
|
231
237
|
private context?: IContext;
|
@@ -892,9 +898,11 @@
|
|
892
898
|
case ShapeType.Cuboid:
|
893
899
|
const cuboid = shape as Cuboid;
|
894
900
|
const sc = col as IBoxCollider;
|
895
|
-
const
|
896
|
-
const
|
897
|
-
const
|
901
|
+
const obj = col.gameObject;
|
902
|
+
const scale = getWorldScale(obj, this._tempPosition);
|
903
|
+
const newX = sc.size.x * 0.5 * scale.x;
|
904
|
+
const newY = sc.size.y * 0.5 * scale.y;
|
905
|
+
const newZ = sc.size.z * 0.5 * scale.z;
|
898
906
|
sizeHasChanged = cuboid.halfExtents.x !== newX || cuboid.halfExtents.y !== newY || cuboid.halfExtents.z !== newZ;
|
899
907
|
cuboid.halfExtents.x = newX;
|
900
908
|
cuboid.halfExtents.y = newY;
|
@@ -988,6 +996,22 @@
|
|
988
996
|
}
|
989
997
|
this.world.step(this.eventQueue);
|
990
998
|
this._isUpdatingPhysicsWorld = false;
|
999
|
+
}
|
1000
|
+
|
1001
|
+
public postStep() {
|
1002
|
+
if (!this.world) return;
|
1003
|
+
if (!this.enabled) return;
|
1004
|
+
this._isUpdatingPhysicsWorld = true;
|
1005
|
+
this.syncObjects();
|
1006
|
+
this._isUpdatingPhysicsWorld = false;
|
1007
|
+
|
1008
|
+
if (this.eventQueue && !this.collisionHandler) {
|
1009
|
+
this.collisionHandler = new PhysicsCollisionHandler(this.world, this.eventQueue);
|
1010
|
+
}
|
1011
|
+
if (this.collisionHandler) {
|
1012
|
+
this.collisionHandler.handleCollisionEvents();
|
1013
|
+
this.collisionHandler.update();
|
1014
|
+
}
|
991
1015
|
this.updateDebugRendering(this.world);
|
992
1016
|
}
|
993
1017
|
|
@@ -995,7 +1019,7 @@
|
|
995
1019
|
if (debugPhysics || debugColliderPlacement || showColliders || this.debugRenderColliders === true) {
|
996
1020
|
if (!this.lines) {
|
997
1021
|
const material = new LineBasicMaterial({
|
998
|
-
color:
|
1022
|
+
color: 0x77dd77,
|
999
1023
|
fog: false,
|
1000
1024
|
// vertexColors: THREE.VertexColors
|
1001
1025
|
});
|
@@ -1017,22 +1041,6 @@
|
|
1017
1041
|
}
|
1018
1042
|
}
|
1019
1043
|
|
1020
|
-
public postStep() {
|
1021
|
-
if (!this.world) return;
|
1022
|
-
if (!this.enabled) return;
|
1023
|
-
this._isUpdatingPhysicsWorld = true;
|
1024
|
-
this.syncObjects();
|
1025
|
-
this._isUpdatingPhysicsWorld = false;
|
1026
|
-
|
1027
|
-
if (this.eventQueue && !this.collisionHandler) {
|
1028
|
-
this.collisionHandler = new PhysicsCollisionHandler(this.world, this.eventQueue);
|
1029
|
-
}
|
1030
|
-
if (this.collisionHandler) {
|
1031
|
-
this.collisionHandler.handleCollisionEvents();
|
1032
|
-
this.collisionHandler.update();
|
1033
|
-
}
|
1034
|
-
}
|
1035
|
-
|
1036
1044
|
/** sync rendered objects with physics world (except for colliders without rigidbody) */
|
1037
1045
|
private syncObjects() {
|
1038
1046
|
if (debugColliderPlacement) return;
|
@@ -1069,8 +1077,8 @@
|
|
1069
1077
|
if (center && center.isVector3) {
|
1070
1078
|
this._tempQuaternion.set(rot.x, rot.y, rot.z, rot.w);
|
1071
1079
|
const offset = this._tempPosition.copy(center).applyQuaternion(this._tempQuaternion);
|
1072
|
-
|
1073
|
-
|
1080
|
+
const scale = getWorldScale(obj.gameObject);
|
1081
|
+
offset.multiply(scale);
|
1074
1082
|
pos.x -= offset.x;
|
1075
1083
|
pos.y -= offset.y;
|
1076
1084
|
pos.z -= offset.z;
|
@@ -1167,8 +1175,14 @@
|
|
1167
1175
|
this._tempCenterPos.z = center.z;
|
1168
1176
|
getWorldScale(collider.gameObject, this._tempCenterVec);
|
1169
1177
|
this._tempCenterPos.multiply(this._tempCenterVec);
|
1170
|
-
|
1171
|
-
|
1178
|
+
if (!collider.attachedRigidbody)
|
1179
|
+
{
|
1180
|
+
getWorldQuaternion(collider.gameObject, this._tempCenterQuaternion);
|
1181
|
+
this._tempCenterPos.applyQuaternion(this._tempCenterQuaternion);
|
1182
|
+
}
|
1183
|
+
else {
|
1184
|
+
this._tempCenterPos.applyQuaternion(collider.gameObject.quaternion);
|
1185
|
+
}
|
1172
1186
|
targetVector.x += this._tempCenterPos.x;
|
1173
1187
|
targetVector.y += this._tempCenterPos.y;
|
1174
1188
|
targetVector.z += this._tempCenterPos.z;
|
@@ -1282,6 +1296,7 @@
|
|
1282
1296
|
this.eventQueue.drainCollisionEvents((handle1, handle2, started) => {
|
1283
1297
|
const col1 = this.world!.getCollider(handle1);
|
1284
1298
|
const col2 = this.world!.getCollider(handle2);
|
1299
|
+
if (!col1 || !col2) return;
|
1285
1300
|
const colliderComponent1 = col1[$componentKey];
|
1286
1301
|
const colliderComponent2 = col2[$componentKey];
|
1287
1302
|
if (debugCollisions)
|
@@ -1,10 +1,11 @@
|
|
1
|
-
import { Box3, Camera, type Intersection, Layers, Mesh, Object3D, Ray, Raycaster, Sphere, Vector2, Vector3
|
1
|
+
import { AxesHelper, Box3, Camera, type Intersection, Layers, Line,Mesh, Object3D, Ray, Raycaster, Sphere, Vector2, Vector3 } from 'three'
|
2
|
+
|
3
|
+
import { Gizmos } from './engine_gizmos.js';
|
2
4
|
import { Context } from './engine_setup.js';
|
3
|
-
import { getParam } from "./engine_utils.js"
|
4
5
|
import { getWorldPosition } from "./engine_three_utils.js"
|
5
6
|
import type { Vec2, Vec3, } from './engine_types.js';
|
6
7
|
import type { IPhysicsEngine } from './engine_types.js';
|
7
|
-
import {
|
8
|
+
import { getParam } from "./engine_utils.js"
|
8
9
|
|
9
10
|
const debugPhysics = getParam("debugphysics");
|
10
11
|
const layerMaskHelper: Layers = new Layers();
|
@@ -12,7 +13,7 @@
|
|
12
13
|
export declare type RaycastTestObjectReturnType = void | boolean | "continue in children";
|
13
14
|
export declare type RaycastTestObjectCallback = (obj: Object3D) => RaycastTestObjectReturnType;
|
14
15
|
|
15
|
-
declare interface IRaycastOptions {
|
16
|
+
export declare interface IRaycastOptions {
|
16
17
|
/** Optionally a custom raycaster can be provided. Other properties will then be set on this raycaster */
|
17
18
|
raycaster?: Raycaster;
|
18
19
|
/** Optional ray that can be used for raycasting
|
@@ -165,17 +166,19 @@
|
|
165
166
|
if (obj.type === "Mesh" && obj.layers.test(mask) && !Gizmos.isGizmo(obj)) {
|
166
167
|
const mesh = obj as Mesh;
|
167
168
|
const geo = mesh.geometry;
|
168
|
-
if (
|
169
|
-
geo.
|
170
|
-
|
171
|
-
if (
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
169
|
+
if (geo) {
|
170
|
+
if (!geo.boundingBox)
|
171
|
+
geo.computeBoundingBox();
|
172
|
+
if (geo.boundingBox) {
|
173
|
+
if (mesh.matrixWorldNeedsUpdate) mesh.updateMatrixWorld();
|
174
|
+
const test = this.tempBoundingBox.copy(geo.boundingBox).applyMatrix4(mesh.matrixWorld);
|
175
|
+
if (sp.intersectsBox(test)) {
|
176
|
+
const wp = getWorldPosition(obj);
|
177
|
+
const dist = wp.distanceTo(sp.center);
|
178
|
+
const int = new SphereIntersection(obj, dist, wp);
|
179
|
+
results.push(int);
|
180
|
+
if (!traverseChildsAfterHit) return;
|
181
|
+
}
|
179
182
|
}
|
180
183
|
}
|
181
184
|
}
|
@@ -188,7 +191,7 @@
|
|
188
191
|
}
|
189
192
|
}
|
190
193
|
|
191
|
-
public raycastFromRay(ray: Ray, options: IRaycastOptions |
|
194
|
+
public raycastFromRay(ray: Ray, options: IRaycastOptions | null = null): Array<Intersection> {
|
192
195
|
const opts = options ?? this.defaultRaycastOptions;
|
193
196
|
opts.ray = ray;
|
194
197
|
const res = this.raycast(opts);
|
@@ -203,7 +206,7 @@
|
|
203
206
|
* Use raycastPhysics for raycasting against physic colliders only. Depending on your scenario this might be faster.
|
204
207
|
* @param options raycast options. If null, default options will be used.
|
205
208
|
*/
|
206
|
-
public raycast(options: IRaycastOptions |
|
209
|
+
public raycast(options: IRaycastOptions | null = null): Array<Intersection> {
|
207
210
|
if (!options) options = this.defaultRaycastOptions;
|
208
211
|
const mp = options.screenPoint ?? this.context.input.mousePositionRC;
|
209
212
|
const rc = options.raycaster ?? this.raycaster;
|
@@ -271,8 +274,10 @@
|
|
271
274
|
|
272
275
|
private intersect(raycaster: Raycaster, objects: Object3D[], results: Intersection[], options: IRaycastOptions) {
|
273
276
|
for (const obj of objects) {
|
277
|
+
// dont raycast invisible objects
|
278
|
+
if (obj.visible === false) continue;
|
279
|
+
|
274
280
|
if (Gizmos.isGizmo(obj)) continue;
|
275
|
-
|
276
281
|
// dont raycast object if it's a line and the line threshold is < 0
|
277
282
|
if (options.lineThreshold !== undefined && options.lineThreshold < 0) {
|
278
283
|
if (obj instanceof Line) {
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import { getParam } from "./engine_utils.js";
|
2
1
|
import { Object3D } from "three";
|
2
|
+
|
3
3
|
import { Context } from "./engine_setup.js";
|
4
|
+
import { getParam } from "./engine_utils.js";
|
4
5
|
|
5
6
|
const debug = getParam("debugplayerview");
|
6
7
|
|
@@ -1,10 +1,11 @@
|
|
1
|
-
import {
|
1
|
+
import { EquirectangularReflectionMapping, LightProbe, SphericalHarmonics3, SRGBColorSpace,Texture, Vector4, WebGLCubeRenderTarget } from "three";
|
2
|
+
|
3
|
+
import { AssetReference } from "./engine_addressables.js";
|
2
4
|
import { Context } from "./engine_setup.js";
|
3
|
-
import { SceneLightSettings } from "./extensions/NEEDLE_lighting_settings.js";
|
4
5
|
import { createFlatTexture, createTrilightTexture } from "./engine_shaders.js";
|
6
|
+
import { type SourceIdentifier } from "./engine_types.js";
|
5
7
|
import { getParam } from "./engine_utils.js";
|
6
|
-
import {
|
7
|
-
import { AssetReference } from "./engine_addressables.js";
|
8
|
+
import { SceneLightSettings } from "./extensions/NEEDLE_lighting_settings.js";
|
8
9
|
// import { LightProbeGenerator } from "three/examples/jsm/lights/LightProbeGenerator.js"
|
9
10
|
|
10
11
|
const debug = getParam("debugenvlight");
|
@@ -1,17 +1,18 @@
|
|
1
|
-
import {
|
1
|
+
import { Object3D } from "three";
|
2
2
|
import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
3
|
+
|
4
|
+
import { showBalloonMessage } from "./debug/index.js";
|
5
|
+
import { getLoader, type INeedleGltfLoader, registerLoader } from "./engine_gltf.js";
|
6
|
+
import { createBuiltinComponents, writeBuiltinComponentData } from "./engine_gltf_builtin_components.js";
|
3
7
|
// import * as object from "./engine_gltf_builtin_components.js";
|
4
8
|
import * as loaders from "./engine_loaders.js"
|
9
|
+
import { registerPrewarmObject } from "./engine_mainloop_utils.js";
|
10
|
+
import { SerializationContext } from "./engine_serialization_core.js";
|
11
|
+
import { Context } from "./engine_setup.js"
|
12
|
+
import { type SourceIdentifier, type UIDProvider } from "./engine_types.js";
|
5
13
|
import * as utils from "./engine_utils.js";
|
6
14
|
import { registerComponentExtension, registerExtensions } from "./extensions/extensions.js";
|
7
|
-
import { getLoader, type INeedleGltfLoader, registerLoader } from "./engine_gltf.js";
|
8
|
-
import { type SourceIdentifier, type UIDProvider } from "./engine_types.js";
|
9
|
-
import { createBuiltinComponents, writeBuiltinComponentData } from "./engine_gltf_builtin_components.js";
|
10
|
-
import { SerializationContext } from "./engine_serialization_core.js";
|
11
15
|
import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
|
12
|
-
import { registerPrewarmObject } from "./engine_mainloop_utils.js";
|
13
|
-
import { Object3D } from "three";
|
14
|
-
import { showBalloonMessage } from "./debug/index.js";
|
15
16
|
|
16
17
|
|
17
18
|
export class NeedleGltfLoader implements INeedleGltfLoader {
|
@@ -1,14 +1,15 @@
|
|
1
1
|
import * as THREE from "three";
|
2
|
+
import { Color, CompressedTexture, Object3D, Texture, WebGLRenderTarget } from "three";
|
3
|
+
|
4
|
+
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
|
5
|
+
import { Behaviour, Component, GameObject } from "../engine-components/Component.js";
|
6
|
+
import { CallInfo, EventList } from "../engine-components/EventList.js";
|
2
7
|
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
8
|
+
import { AssetReference } from "./engine_addressables.js";
|
9
|
+
import { debugExtension } from "./engine_default_parameters.js";
|
3
10
|
import { SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
|
4
|
-
import { Behaviour, Component, GameObject } from "../engine-components/Component.js";
|
5
|
-
import { debugExtension } from "./engine_default_parameters.js";
|
6
|
-
import { CallInfo, EventList } from "../engine-components/EventList.js";
|
7
|
-
import { Color, CompressedTexture, Object3D, Texture, WebGLRenderTarget } from "three";
|
8
11
|
import { RenderTexture } from "./engine_texture.js";
|
9
|
-
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
|
10
12
|
import { resolveUrl } from "./engine_utils.js";
|
11
|
-
import { AssetReference } from "./engine_addressables.js";
|
12
13
|
|
13
14
|
// export class SourcePath {
|
14
15
|
// src?:string
|
@@ -156,6 +157,14 @@
|
|
156
157
|
|
157
158
|
onDeserialize(data: any, context: SerializationContext) {
|
158
159
|
if (data?.guid) {
|
160
|
+
|
161
|
+
// it's a workaround for VolumeParameter having a guid as well. Generally we will probably never have to resolve a component in the scene when it's coming from the persistent asset extension (like in the case for postprocessing volume parameters)
|
162
|
+
if (data.___persistentAsset) {
|
163
|
+
if(debugExtension) console.log("Skipping component deserialization because it's a persistent asset", data);
|
164
|
+
return undefined;
|
165
|
+
}
|
166
|
+
|
167
|
+
const currentPath = context.path;
|
159
168
|
// TODO: need to serialize some identifier for referenced components as well, maybe just guid?
|
160
169
|
// because here the components are created but dont have their former guid assigned
|
161
170
|
// and will later in the stack just get a newly generated guid
|
@@ -173,8 +182,9 @@
|
|
173
182
|
res = this.findObjectForGuid(data.guid, context.context?.scene);
|
174
183
|
if (res) return res;
|
175
184
|
}
|
176
|
-
if (isDevEnvironment() || debugExtension)
|
177
|
-
console.warn("Could not resolve component reference"
|
185
|
+
if (isDevEnvironment() || debugExtension) {
|
186
|
+
console.warn("Could not resolve component reference: \"" + currentPath + "\" using guid " + data.guid, context.target);
|
187
|
+
}
|
178
188
|
data["could_not_resolve"] = true;
|
179
189
|
return undefined;
|
180
190
|
}
|
@@ -1,13 +1,14 @@
|
|
1
|
+
import { AnimationClip, Material, Mesh, Object3D, Texture } from "three";
|
1
2
|
import { type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
|
-
|
3
|
-
import { AnimationClip, Material, Mesh, Object3D, Texture } from "three";
|
4
|
-
import { Context } from "./engine_setup.js";
|
5
|
-
import { isPersistentAsset } from "./extensions/NEEDLE_persistent_assets.js";
|
6
|
-
import { type ConstructorConcrete, type SourceIdentifier } from "./engine_types.js";
|
3
|
+
|
7
4
|
import { debugExtension } from "../engine/engine_default_parameters.js";
|
8
|
-
import { LogType
|
5
|
+
import { addLog,LogType } from "./debug/debug_overlay.js";
|
9
6
|
import { isLocalNetwork } from "./engine_networking_utils.js";
|
7
|
+
import { Context } from "./engine_setup.js";
|
8
|
+
import { Constructor, type ConstructorConcrete, type SourceIdentifier } from "./engine_types.js";
|
10
9
|
import { $BuiltInTypeFlag } from "./engine_typestore.js";
|
10
|
+
import { getParam } from "./engine_utils.js";
|
11
|
+
import { isPersistentAsset } from "./extensions/NEEDLE_persistent_assets.js";
|
11
12
|
|
12
13
|
const debug = getParam("debugserializer");
|
13
14
|
|
@@ -124,7 +125,7 @@
|
|
124
125
|
// }
|
125
126
|
// }
|
126
127
|
|
127
|
-
constructor(type:
|
128
|
+
constructor(type: Constructor<any> | Constructor<any>[]) {
|
128
129
|
if (Array.isArray(type)) {
|
129
130
|
for (const key of type)
|
130
131
|
helper.register(key.name, this);
|
@@ -359,7 +360,6 @@
|
|
359
360
|
obj.onAfterDeserialize(serializedData, context);
|
360
361
|
}
|
361
362
|
|
362
|
-
context.path = undefined;
|
363
363
|
return true;
|
364
364
|
}
|
365
365
|
|
@@ -1,7 +1,6 @@
|
|
1
|
-
import { serializeObject
|
1
|
+
import { deserializeObject,serializeObject } from "./engine_serialization_core.js";
|
2
2
|
|
3
|
-
export { serializeObject
|
3
|
+
export { deserializeObject,serializeObject };
|
4
4
|
|
5
|
-
export
|
6
|
-
|
7
|
-
export * from "./engine_serialization_builtin_serializer.js";
|
5
|
+
export * from "./engine_serialization_builtin_serializer.js";
|
6
|
+
export { serializable, serializeable } from "./engine_serialization_decorator.js"
|
@@ -1,9 +1,10 @@
|
|
1
1
|
|
2
|
+
import { Color,DataTexture, FileLoader, RGBAFormat, Vector4 } from "three";
|
3
|
+
|
4
|
+
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
2
5
|
import * as loader from "./engine_fileloader.js"
|
6
|
+
import { Mathf } from "./engine_math.js";
|
3
7
|
import * as SHADERDATA from "./shaders/shaderData.js"
|
4
|
-
import { Vector4, FileLoader, DataTexture, RGBAFormat, Color } from "three";
|
5
|
-
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
6
|
-
import { Mathf } from "./engine_math.js";
|
7
8
|
|
8
9
|
|
9
10
|
const white = new Uint8Array(4);
|
@@ -1,5 +1,6 @@
|
|
1
|
+
import { EffectComposer } from "postprocessing";
|
1
2
|
import { Camera, Mesh, Object3D, Texture, WebGLRenderer, WebGLRenderTarget } from "three";
|
2
|
-
|
3
|
+
|
3
4
|
import { findResourceUsers } from "./engine_assetdatabase.js";
|
4
5
|
|
5
6
|
|
@@ -1,6 +1,7 @@
|
|
1
|
+
import { AnimationAction, Euler, Mesh,Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, Texture, Uniform, Vector3 } from "three";
|
2
|
+
import { ShaderMaterial,WebGLRenderer } from "three";
|
3
|
+
|
1
4
|
import { Mathf } from "./engine_math.js"
|
2
|
-
import { Vector3, Quaternion, Uniform, Texture, AnimationAction, PerspectiveCamera, Object3D, Euler, PlaneGeometry, Scene, Mesh } from "three";
|
3
|
-
import { WebGLRenderer, ShaderMaterial } from "three";
|
4
5
|
import { CircularBuffer } from "./engine_utils.js";
|
5
6
|
|
6
7
|
|
@@ -47,11 +48,24 @@
|
|
47
48
|
|
48
49
|
|
49
50
|
const _tempVecs = new CircularBuffer(() => new Vector3(), 100);
|
50
|
-
export function getTempVector(
|
51
|
+
export function getTempVector(vecOrX?: Vector3 | number | DOMPointReadOnly, y?: number, z?: number) {
|
51
52
|
const vec = _tempVecs.get();
|
52
|
-
if(
|
53
|
+
if (vecOrX instanceof Vector3) vec.copy(vecOrX);
|
54
|
+
else if (vecOrX instanceof DOMPointReadOnly) vec.set(vecOrX.x, vecOrX.y, vecOrX.z);
|
55
|
+
else {
|
56
|
+
if (typeof vecOrX === "number") vec.x = vecOrX;
|
57
|
+
if (typeof y === "number") vec.y = y;
|
58
|
+
if (typeof z === "number") vec.z = z;
|
59
|
+
}
|
53
60
|
return vec;
|
54
61
|
}
|
62
|
+
const _tempQuats = new CircularBuffer(() => new Quaternion(), 100);
|
63
|
+
export function getTempQuaternion(value?: Quaternion | DOMPointReadOnly) {
|
64
|
+
const val = _tempQuats.get();
|
65
|
+
if (value instanceof Quaternion) val.copy(value);
|
66
|
+
else if (value instanceof DOMPointReadOnly) val.set(value.x, value.y, value.z, value.w);
|
67
|
+
return val;
|
68
|
+
}
|
55
69
|
|
56
70
|
|
57
71
|
const _worldPositions = new CircularBuffer(() => new Vector3(), 100);
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { Clock } from 'three'
|
2
|
+
|
3
|
+
import { type ITime } from './engine_types.js';
|
2
4
|
import { getParam } from './engine_utils.js';
|
3
|
-
import { type ITime } from './engine_types.js';
|
4
5
|
|
5
6
|
const timescaleUrl = getParam("timescale");
|
6
7
|
let timeScale = 1;
|
@@ -45,8 +46,8 @@
|
|
45
46
|
this.frame += 1;
|
46
47
|
this.time += this.deltaTime;
|
47
48
|
|
48
|
-
if (this._fpsSamples.length <
|
49
|
-
else this._fpsSamples[(this._fpsSampleIndex++) %
|
49
|
+
if (this._fpsSamples.length < 60) this._fpsSamples.push(this.deltaTime);
|
50
|
+
else this._fpsSamples[(this._fpsSampleIndex++) % 60] = this.deltaTime;
|
50
51
|
let sum = 0;
|
51
52
|
for (let i = 0; i < this._fpsSamples.length; i++)
|
52
53
|
sum += this._fpsSamples[i];
|
@@ -1,10 +1,12 @@
|
|
1
|
-
import {
|
2
|
-
import type { Camera, Color, Material, Object3D, Quaternion, Ray, Scene, WebGLRenderer, Mesh } from "three";
|
1
|
+
import type { Camera, Color, Material, Mesh, Object3D, Quaternion, Ray, Scene, WebGLRenderer } from "three";
|
3
2
|
import { Vector3 } from "three";
|
3
|
+
import { type GLTF as GLTF3 } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
|
+
|
4
5
|
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
5
6
|
import { CollisionDetectionMode, type PhysicsMaterial, RigidbodyConstraints } from "./engine_physics.types.js";
|
7
|
+
import { RenderTexture } from "./engine_texture.js";
|
6
8
|
import { CircularBuffer } from "./engine_utils.js";
|
7
|
-
import
|
9
|
+
import type { INeedleXRSessionEventReceiver, NeedleXRSession } from "./engine_xr.js";
|
8
10
|
|
9
11
|
export type GLTF = GLTF3 & {
|
10
12
|
// asset: { generator: string, version: string }
|
@@ -72,13 +74,14 @@
|
|
72
74
|
|
73
75
|
scripts: IComponent[];
|
74
76
|
scripts_pausedChanged: IComponent[];
|
75
|
-
// scripts with update event
|
76
77
|
scripts_earlyUpdate: IComponent[];
|
77
78
|
scripts_update: IComponent[];
|
78
79
|
scripts_lateUpdate: IComponent[];
|
79
80
|
scripts_onBeforeRender: IComponent[];
|
80
81
|
scripts_onAfterRender: IComponent[];
|
81
82
|
scripts_WithCorroutines: IComponent[];
|
83
|
+
scripts_immersive_vr: INeedleXRSessionEventReceiver[];
|
84
|
+
scripts_immersive_ar: INeedleXRSessionEventReceiver[];
|
82
85
|
coroutines: { [FrameEvent: number]: Array<CoroutineData> };
|
83
86
|
|
84
87
|
post_setup_callbacks: Function[];
|
@@ -90,10 +93,13 @@
|
|
90
93
|
new_script_start: IComponent[];
|
91
94
|
new_scripts_pre_setup_callbacks: Function[];
|
92
95
|
new_scripts_post_setup_callbacks: Function[];
|
96
|
+
new_scripts_xr: INeedleXRSessionEventReceiver[];
|
93
97
|
|
94
98
|
stopAllCoroutinesFrom(script: IComponent);
|
95
99
|
}
|
96
100
|
|
101
|
+
export type INeedleXRSession = NeedleXRSession;
|
102
|
+
|
97
103
|
export declare interface INeedleEngineComponent extends HTMLElement {
|
98
104
|
getAROverlayContainer(): HTMLElement;
|
99
105
|
onEnterAR(session: XRSession, overlayContainer: HTMLElement);
|
@@ -507,3 +513,20 @@
|
|
507
513
|
/** Enable to visualize raycasts in the scene with gizmos */
|
508
514
|
debugRenderRaycasts: boolean;
|
509
515
|
}
|
516
|
+
|
517
|
+
|
518
|
+
/** Typical mouse button names for most devices */
|
519
|
+
export type MouseButtonName = "left" | "right" | "middle";
|
520
|
+
|
521
|
+
/** Button names on typical controllers (since there seems to be no agreed naming)
|
522
|
+
* https://w3c.github.io/gamepad/#remapping
|
523
|
+
*/
|
524
|
+
export type GamepadButtonName = "a-button" | "b-button" | "x-button" | "y-button";
|
525
|
+
/** Button names as used in the xr profile */
|
526
|
+
|
527
|
+
export type XRControllerButtonName = "thumbrest" | "xr-standard-trigger" | "xr-standard-squeeze" | "xr-standard-thumbstick" | "xr-standard-touchpad" | "menu" | GamepadButtonName;
|
528
|
+
|
529
|
+
export type XRGestureName = "pinch";
|
530
|
+
|
531
|
+
/** All known (types) button names for various devices and cases combined. You should use the device specific names if you e.g. know you only deal with a mouse use MouseButtonName */
|
532
|
+
export type ButtonName = "unknown" | MouseButtonName | GamepadButtonName | XRControllerButtonName | XRGestureName;
|
@@ -1,7 +1,8 @@
|
|
1
|
+
import { Quaternion, Vector2, Vector3, Vector4 } from "three";
|
2
|
+
|
3
|
+
import { isDevEnvironment, LogType, showBalloonMessage } from "./debug/index.js";
|
1
4
|
import { $isAssigningProperties } from "./engine_serialization_core.js";
|
2
|
-
import { LogType, isDevEnvironment, showBalloonMessage } from "./debug/index.js";
|
3
5
|
import { type Constructor, type IComponent } from "./engine_types.js";
|
4
|
-
import { Quaternion, Vector2, Vector3, Vector4 } from "three";
|
5
6
|
import { watchWrite } from "./engine_utils.js";
|
6
7
|
|
7
8
|
|
@@ -1,6 +1,7 @@
|
|
1
|
+
import { Camera,PerspectiveCamera } from "three";
|
2
|
+
|
1
3
|
import { ContextRegistry } from "./engine_context_registry.js";
|
2
4
|
import { Context } from "./engine_setup.js";
|
3
|
-
import { PerspectiveCamera, Camera } from "three";
|
4
5
|
|
5
6
|
declare type ImageMimeType = "image/webp" | "image/png";
|
6
7
|
|
@@ -1,5 +1,8 @@
|
|
1
1
|
// use for typesafe interface method calls
|
2
2
|
import { Quaternion, type Vector, Vector2, Vector3, Vector4 } from "three";
|
3
|
+
|
4
|
+
import { type Context } from "./engine_context.js";
|
5
|
+
import { ContextRegistry } from "./engine_context_registry.js";
|
3
6
|
import { type SourceIdentifier } from "./engine_types.js";
|
4
7
|
|
5
8
|
// https://schneidenbach.gitbooks.io/typescript-cookbook/content/nameof-operator.html
|
@@ -8,6 +11,8 @@
|
|
8
11
|
return nameofFactory<T>()(name);
|
9
12
|
}
|
10
13
|
|
14
|
+
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
|
15
|
+
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
|
11
16
|
|
12
17
|
export function isDebugMode(): boolean {
|
13
18
|
return getParam("debug") ? true : false;
|
@@ -207,12 +212,37 @@
|
|
207
212
|
return obj;
|
208
213
|
}
|
209
214
|
|
215
|
+
/** @returns a promise that resolves after a certain amount of milliseconds
|
216
|
+
* e.g. `await delay(1000)` will wait for 1 second
|
217
|
+
*/
|
210
218
|
export function delay(milliseconds: number): Promise<void> {
|
211
|
-
return new Promise((
|
212
|
-
setTimeout(
|
219
|
+
return new Promise((resolve, _reject) => {
|
220
|
+
setTimeout(resolve, milliseconds);
|
213
221
|
});
|
214
222
|
}
|
215
223
|
|
224
|
+
/** @returns a promise that resolves after a certain amount of frames
|
225
|
+
* e.g. `await delayForFrames(10)` will wait for 10 frames to pass
|
226
|
+
*/
|
227
|
+
export function delayForFrames(frameCount: number, context?: Context): Promise<void> {
|
228
|
+
|
229
|
+
if (frameCount <= 0) return Promise.resolve();
|
230
|
+
if (!context) context = ContextRegistry.Current as Context;
|
231
|
+
if (!context) return Promise.reject("No context");
|
232
|
+
|
233
|
+
const endFrame = context.time.frameCount + frameCount;
|
234
|
+
return new Promise((resolve, reject) => {
|
235
|
+
if (!context) return reject("No context");
|
236
|
+
const cb = () => {
|
237
|
+
if (context!.time.frameCount >= endFrame) {
|
238
|
+
context!.pre_update_callbacks.splice(context!.pre_update_callbacks.indexOf(cb), 1);
|
239
|
+
resolve();
|
240
|
+
}
|
241
|
+
}
|
242
|
+
context!.pre_update_callbacks.push(cb);
|
243
|
+
});
|
244
|
+
}
|
245
|
+
|
216
246
|
// 1) if a timeline is exported via menu item the audio clip path is relative to the glb (same folder)
|
217
247
|
// we need to detect that here and build the new audio source path relative to the new glb location
|
218
248
|
// the same is/might be true for any file that is/will be exported via menu item
|
@@ -516,10 +546,6 @@
|
|
516
546
|
return json;
|
517
547
|
}
|
518
548
|
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
549
|
declare type AttributeChangeCallback = (value: string | null) => void;
|
524
550
|
declare type HtmlElementExtra = {
|
525
551
|
observer: MutationObserver,
|
@@ -611,4 +637,42 @@
|
|
611
637
|
anyFailed: anyFailed,
|
612
638
|
results: res,
|
613
639
|
};
|
640
|
+
}
|
641
|
+
|
642
|
+
|
643
|
+
|
644
|
+
|
645
|
+
|
646
|
+
|
647
|
+
/** using https://github.com/davidshimjs/qrcodejs */
|
648
|
+
export async function generateQRCode(args: { domElement?: HTMLElement, text: string, width?: number, height?: number, colorDark?: string, colorLight?: string, correctLevel?: any }): Promise<HTMLElement> {
|
649
|
+
|
650
|
+
// ensure that the QRCode library is loaded
|
651
|
+
if (!globalThis["QRCode"]) {
|
652
|
+
const url = "https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js";
|
653
|
+
let script = document.head.querySelector(`script[src="${url}"]`) as HTMLScriptElement;
|
654
|
+
if (!script) {
|
655
|
+
script = document.createElement("script");
|
656
|
+
script.src = url;
|
657
|
+
document.head.appendChild(script);
|
658
|
+
}
|
659
|
+
|
660
|
+
await new Promise((resolve, _reject) => {
|
661
|
+
script.addEventListener("load", () => {
|
662
|
+
resolve(true);
|
663
|
+
});
|
664
|
+
});
|
665
|
+
}
|
666
|
+
|
667
|
+
const QRCODE = globalThis["QRCode"];
|
668
|
+
const target = args.domElement ?? document.createElement("div");
|
669
|
+
new QRCODE(target, {
|
670
|
+
width: args.width ?? 256,
|
671
|
+
height: args.height ?? 256,
|
672
|
+
colorDark: "#000000",
|
673
|
+
colorLight: "#ffffff",
|
674
|
+
correctLevel: QRCODE.CorrectLevel.M,
|
675
|
+
...args,
|
676
|
+
});
|
677
|
+
return target;
|
614
678
|
}
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import "./engine_hot_reload.js";
|
2
|
+
import "./tests/test_utils.js";
|
2
3
|
|
4
|
+
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
5
|
+
import * as engine_scenetools from "./engine_scenetools.js";
|
3
6
|
import * as engine_setup from "./engine_setup.js";
|
4
|
-
import * as engine_scenetools from "./engine_scenetools.js";
|
5
|
-
import "./tests/test_utils.js";
|
6
|
-
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
7
7
|
|
8
8
|
const engine : any = {
|
9
9
|
...engine_setup,
|
@@ -1,19 +1,18 @@
|
|
1
|
+
import { Intersection, Object3D } from "three";
|
2
|
+
|
3
|
+
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
4
|
+
import { Input, InputEventNames, InputEvents, NEPointerEvent, PointerType } from "../../engine/engine_input.js";
|
5
|
+
import { Mathf } from "../../engine/engine_math.js";
|
1
6
|
import { RaycastOptions, RaycastTestObjectReturnType } from "../../engine/engine_physics.js";
|
2
|
-
import { Behaviour, Component, GameObject } from "../Component.js";
|
3
|
-
import { WebXR } from "../webxr/WebXR.js";
|
4
|
-
import { ControllerEvents, WebXRController } from "../webxr/WebXRController.js";
|
5
|
-
import * as ThreeMeshUI from 'three-mesh-ui'
|
6
7
|
import { Context } from "../../engine/engine_setup.js";
|
7
|
-
import {
|
8
|
+
import { IComponent } from "../../engine/engine_types.js";
|
9
|
+
import { getParam } from "../../engine/engine_utils.js";
|
10
|
+
import { Behaviour, GameObject } from "../Component.js";
|
11
|
+
import { $shadowDomOwner } from "./BaseUIComponent.js";
|
12
|
+
import type { ICanvasGroup } from "./Interfaces.js";
|
13
|
+
import { hasPointerEventComponent, type IPointerEventHandler, IPointerUpHandler, PointerEventData } from "./PointerEvents.js";
|
8
14
|
import { ObjectRaycaster, Raycaster } from "./Raycaster.js";
|
9
|
-
import { InputEvents, NEPointerEvent, PointerType } from "../../engine/engine_input.js";
|
10
|
-
import { Mesh, Object3D } from "three";
|
11
|
-
import type { ICanvasGroup } from "./Interfaces.js";
|
12
|
-
import { getParam } from "../../engine/engine_utils.js";
|
13
15
|
import { UIRaycastUtils } from "./RaycastUtils.js";
|
14
|
-
import { $shadowDomOwner } from "./BaseUIComponent.js";
|
15
|
-
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
16
|
-
import { Mathf } from "../../engine/engine_math.js";
|
17
16
|
import { isUIObject } from "./Utils.js";
|
18
17
|
|
19
18
|
const debug = getParam("debugeventsystem");
|
@@ -29,6 +28,8 @@
|
|
29
28
|
hasActiveUI: boolean
|
30
29
|
}
|
31
30
|
|
31
|
+
declare type IComponentCanMaybeReceiveEvents = IPointerEventHandler & IComponent & { interactable?: boolean };
|
32
|
+
|
32
33
|
export class EventSystem extends Behaviour {
|
33
34
|
private static _eventSystemMap = new Map<Context, EventSystem[]>();
|
34
35
|
|
@@ -93,10 +94,9 @@
|
|
93
94
|
const res = GameObject.findObjectOfType(Raycaster, this.context);
|
94
95
|
if (!res) {
|
95
96
|
const rc = GameObject.addNewComponent(this.context.scene, ObjectRaycaster);
|
96
|
-
rc.ignoreSkinnedMeshes = true;
|
97
97
|
this.raycaster.push(rc);
|
98
98
|
if (isDevEnvironment() || debug)
|
99
|
-
console.warn("Added an ObjectRaycaster to the scene because no raycaster was found.
|
99
|
+
console.warn("Added an ObjectRaycaster to the scene because no raycaster was found.");
|
100
100
|
}
|
101
101
|
}
|
102
102
|
}
|
@@ -112,89 +112,16 @@
|
|
112
112
|
}
|
113
113
|
}
|
114
114
|
|
115
|
-
private _selectStartFn?: any;
|
116
|
-
private _selectEndFn?: any;
|
117
|
-
private _selectUpdateFn?: any;
|
118
|
-
private _handleEventCycleFn?: any;
|
119
115
|
private _handleInputFn?: any;
|
120
116
|
|
121
117
|
onEnable(): void {
|
122
|
-
|
123
|
-
this._selectStartFn ??= (ctrl, args: { grab: THREE.Object3D | null }) => {
|
124
|
-
if (!args.grab) return;
|
125
|
-
MeshUIHelper.resetLastSelected();
|
126
|
-
const opts = new PointerEventData(this.context.input);
|
127
|
-
opts.inputSource = ctrl;
|
128
|
-
opts.pointerId = 0;
|
129
|
-
opts.isDown = ctrl.selectionDown;
|
130
|
-
opts.isUp = ctrl.selectionUp;
|
131
|
-
opts.isPressed = ctrl.selectionPressed;
|
132
|
-
opts.isClicked = false;
|
133
|
-
grabbed.set(ctrl, args.grab);
|
134
|
-
if (args.grab && !this.handleEventOnObject(args.grab, opts)) {
|
135
|
-
args.grab = null;
|
136
|
-
};
|
137
|
-
}
|
138
|
-
this._selectEndFn ??= (ctrl: WebXRController, args: { grab: THREE.Object3D }) => {
|
139
|
-
if (!args.grab) return;
|
140
|
-
const opts = new PointerEventData(this.context.input);
|
141
|
-
opts.inputSource = ctrl;
|
142
|
-
opts.pointerId = 0;
|
143
|
-
opts.isDown = ctrl.selectionDown;
|
144
|
-
opts.isUp = ctrl.selectionUp;
|
145
|
-
opts.isPressed = ctrl.selectionPressed;
|
146
|
-
opts.isClicked = ctrl.selectionClick;
|
147
|
-
this.handleEventOnObject(args.grab, opts);
|
148
|
-
|
149
|
-
const prevGrabbed = grabbed.get(ctrl);
|
150
|
-
grabbed.set(ctrl, null);
|
151
|
-
if (prevGrabbed) {
|
152
|
-
|
153
|
-
for (const key of this.pressedByID.keys()) {
|
154
|
-
const e = this.pressedByID[key] as {
|
155
|
-
obj: Object3D<Event>;
|
156
|
-
data: PointerEventData;
|
157
|
-
handler: IPointerEventHandler;
|
158
|
-
};
|
159
|
-
|
160
|
-
if (e && e.obj === prevGrabbed && e.handler) {
|
161
|
-
e.handler.onPointerUp?.call(e.handler, opts);
|
162
|
-
this.pressedByID.delete(key);
|
163
|
-
}
|
164
|
-
}
|
165
|
-
}
|
166
|
-
};
|
167
|
-
|
168
|
-
const controllerRcOpts = new RaycastOptions();
|
169
|
-
this._selectUpdateFn ??= (_ctrl: WebXRController) => {
|
170
|
-
controllerRcOpts.ray = _ctrl.getRay();
|
171
|
-
const rc = this.performRaycast(controllerRcOpts) ?? [];
|
172
|
-
const opts = new PointerEventData(this.context.input);
|
173
|
-
opts.inputSource = _ctrl;
|
174
|
-
opts.pointerId = _ctrl.input?.handedness === "right" ? 0 : 1;
|
175
|
-
opts.isDown = _ctrl.selectionDown;
|
176
|
-
opts.isUp = _ctrl.selectionUp;
|
177
|
-
opts.isPressed = _ctrl.selectionPressed;
|
178
|
-
opts.isClicked = false;
|
179
|
-
this.handleIntersections(opts.pointerId, rc, opts);
|
180
|
-
};
|
181
|
-
|
182
|
-
WebXRController.addEventListener(ControllerEvents.SelectStart, this._selectStartFn);
|
183
|
-
WebXRController.addEventListener(ControllerEvents.SelectEnd, this._selectEndFn);
|
184
|
-
WebXRController.addEventListener(ControllerEvents.Update, this._selectUpdateFn);
|
185
|
-
|
186
|
-
this._handleInputFn = this.onPointerEvent.bind(this);
|
187
|
-
|
118
|
+
this._handleInputFn ??= this.onPointerEvent.bind(this);
|
188
119
|
this.context.input.addEventListener(InputEvents.PointerDown, this._handleInputFn);
|
189
120
|
this.context.input.addEventListener(InputEvents.PointerUp, this._handleInputFn);
|
190
121
|
this.context.input.addEventListener(InputEvents.PointerMove, this._handleInputFn);
|
191
122
|
}
|
192
123
|
|
193
124
|
onDisable(): void {
|
194
|
-
WebXRController.removeEventListener(ControllerEvents.SelectStart, this._selectStartFn);
|
195
|
-
WebXRController.removeEventListener(ControllerEvents.SelectEnd, this._selectEndFn);
|
196
|
-
WebXRController.removeEventListener(ControllerEvents.Update, this._selectUpdateFn);
|
197
|
-
|
198
125
|
this.context.input.removeEventListener(InputEvents.PointerDown, this._handleInputFn);
|
199
126
|
this.context.input.removeEventListener(InputEvents.PointerUp, this._handleInputFn);
|
200
127
|
this.context.input.removeEventListener(InputEvents.PointerMove, this._handleInputFn);
|
@@ -224,30 +151,39 @@
|
|
224
151
|
*/
|
225
152
|
private onPointerEvent(pointerEvent: NEPointerEvent) {
|
226
153
|
if (pointerEvent === undefined) return;
|
154
|
+
if (pointerEvent.propagationStopped) return;
|
227
155
|
|
228
|
-
//
|
229
|
-
// because otherwise it would be taken as 3 unique pointers and create OnEnter and OnExit events which is not expected
|
230
|
-
const id = pointerEvent.pointerType == PointerType.Touch ? pointerEvent.button : 0;
|
156
|
+
// Use the pointerID, this is the touch id or the mouse (always 0, could be index of the mouse DEVICE) or the index of the XRInputSource
|
231
157
|
const data = new PointerEventData(this.context.input, pointerEvent);
|
158
|
+
this._currentPointerEventName = pointerEvent.type;
|
232
159
|
|
233
160
|
data.inputSource = this.context.input;
|
234
|
-
data.
|
235
|
-
data.
|
161
|
+
data.isClick = pointerEvent.isClick;
|
162
|
+
data.isDoubleClick = pointerEvent.isDoubleClick;
|
236
163
|
// using the input type directly instead of input API -> otherwise onMove events can sometimes be getPointerUp() == true
|
237
164
|
data.isDown = pointerEvent.type == InputEvents.PointerDown;
|
238
165
|
data.isUp = pointerEvent.type == InputEvents.PointerUp;
|
239
|
-
data.isPressed = this.context.input.getPointerPressed(pointerEvent.
|
166
|
+
data.isPressed = this.context.input.getPointerPressed(pointerEvent.pointerId);
|
240
167
|
|
241
|
-
if (debug
|
168
|
+
if (debug) {
|
169
|
+
if (data.isDown) console.log("DOWN", data.pointerId);
|
170
|
+
else if (data.isUp) console.log("UP", data.pointerId);
|
171
|
+
if (data.isClick) console.log("CLICK", data.pointerId);
|
172
|
+
}
|
242
173
|
|
243
174
|
// raycast
|
244
175
|
const options = new RaycastOptions();
|
245
|
-
|
176
|
+
if (pointerEvent.ray) {
|
177
|
+
options.ray = pointerEvent.ray;
|
178
|
+
}
|
179
|
+
else {
|
180
|
+
options.screenPoint = this.context.input.getPointerPositionRC(pointerEvent.pointerId)!;
|
181
|
+
}
|
246
182
|
|
183
|
+
|
247
184
|
const hits = this.performRaycast(options);
|
248
|
-
if (!hits) return;
|
249
185
|
|
250
|
-
if (debug && data.
|
186
|
+
if (debug && data.isClick) {
|
251
187
|
showBalloonMessage("EventSystem: " + data.pointerId + " - " + this.context.time.frame + " - Up:" + data.isUp + ", Down:" + data.isDown)
|
252
188
|
}
|
253
189
|
|
@@ -257,12 +193,12 @@
|
|
257
193
|
hasActiveUI: this.currentActiveMeshUIComponents.length > 0,
|
258
194
|
}
|
259
195
|
|
260
|
-
this.dispatchEvent(new CustomEvent(EventSystemEvents.BeforeHandleInput, { detail: evt }))
|
196
|
+
this.dispatchEvent(new CustomEvent(EventSystemEvents.BeforeHandleInput, { detail: evt }));
|
261
197
|
|
262
|
-
// handle
|
263
|
-
this.handleIntersections(
|
198
|
+
// then handle the intersections and call the callbacks on the regular objects
|
199
|
+
this.handleIntersections(hits, data);
|
264
200
|
|
265
|
-
this.dispatchEvent(new CustomEvent<AfterHandleInputEvent>(EventSystemEvents.AfterHandleInput, { detail: evt }))
|
201
|
+
this.dispatchEvent(new CustomEvent<AfterHandleInputEvent>(EventSystemEvents.AfterHandleInput, { detail: evt }));
|
266
202
|
}
|
267
203
|
|
268
204
|
private readonly _sortedHits: THREE.Intersection[] = [];
|
@@ -271,6 +207,10 @@
|
|
271
207
|
* cache for objects that we want to raycast against. It's cleared before each call to performRaycast invoking raycasters
|
272
208
|
*/
|
273
209
|
private readonly _testObjectsCache = new Map<Object3D, boolean>();
|
210
|
+
/** that's the raycaster that is CURRENTLY being used for raycasting (the shouldRaycastObject method uses this) */
|
211
|
+
private _currentlyActiveRaycaster: Raycaster | null = null;
|
212
|
+
private _currentPointerEventName: InputEventNames | null = null;
|
213
|
+
|
274
214
|
/**
|
275
215
|
* Checks if an object that we encounter has an event component and if it does, we add it to our objects cache
|
276
216
|
* If it doesnt we tell our raycasting system to ignore it and continue in the child hierarchy
|
@@ -283,57 +223,72 @@
|
|
283
223
|
* */
|
284
224
|
private shouldRaycastObject = (obj: Object3D): RaycastTestObjectReturnType => {
|
285
225
|
// check if this object is actually a UI shadow hierarchy object
|
286
|
-
let
|
226
|
+
let uiOwner: Object3D | null = null;
|
287
227
|
const isUI = isUIObject(obj);
|
288
228
|
// if yes we want to grab the actual object that is the owner of the shadow dom
|
289
229
|
// and check that object for the event component
|
290
230
|
if (isUI) {
|
291
|
-
|
231
|
+
uiOwner = obj[$shadowDomOwner]?.gameObject;
|
292
232
|
}
|
293
233
|
|
294
234
|
// check if the object was seen previously
|
295
|
-
if (this._testObjectsCache.has(obj) || (
|
235
|
+
if (this._testObjectsCache.has(obj) || (uiOwner && this._testObjectsCache.has(uiOwner))) {
|
296
236
|
// if yes we check if it was previously stored as "YES WE NEED TO RAYCAST THIS"
|
297
237
|
const prev = this._testObjectsCache.get(obj)!;
|
298
238
|
if (prev === false) return "continue in children"
|
299
239
|
return true;
|
300
240
|
}
|
301
241
|
else {
|
242
|
+
|
243
|
+
// if the object has another raycaster component than the one that is currently raycasting, we ignore this here
|
244
|
+
// because then this other raycaster is responsible for raycasting this object
|
245
|
+
// const rc = GameObject.getComponent(obj, Raycaster);
|
246
|
+
// if (rc?.activeAndEnabled && rc !== this._currentlyActiveRaycaster) return false;
|
247
|
+
|
302
248
|
// the object was not yet seen so we test if it has an event component
|
303
|
-
let hasEventComponent = hasPointerEventComponent(obj);
|
304
|
-
if (!hasEventComponent &&
|
249
|
+
let hasEventComponent = hasPointerEventComponent(obj, this._currentPointerEventName);
|
250
|
+
if (!hasEventComponent && uiOwner) hasEventComponent = hasPointerEventComponent(uiOwner, this._currentPointerEventName);
|
305
251
|
|
306
252
|
if (hasEventComponent) {
|
307
253
|
// it has an event component: we add it and all its children to the cache
|
308
254
|
// we don't need to do the same for the shadow component hierarchy
|
309
255
|
// because the next object that will be detecting that the shadow owner was already seen
|
310
256
|
this._testObjectsCache.set(obj, true);
|
311
|
-
obj.
|
312
|
-
this._testObjectsCache.set(o, true);
|
313
|
-
})
|
257
|
+
for (const ch of obj.children) this.shouldRaycastObject_AddToYesCache(ch);
|
314
258
|
return true;
|
315
259
|
}
|
316
260
|
this._testObjectsCache.set(obj, false);
|
317
261
|
return "continue in children"
|
318
262
|
}
|
319
263
|
}
|
264
|
+
private shouldRaycastObject_AddToYesCache(obj: Object3D) {
|
265
|
+
// if the object has another raycaster component than the one that is currently raycasting, we ignore this here
|
266
|
+
// because then this other raycaster is responsible for raycasting this object
|
267
|
+
// const rc = GameObject.getComponent(obj, Raycaster);
|
268
|
+
// if (rc?.activeAndEnabled && rc !== this._currentlyActiveRaycaster) return;
|
320
269
|
|
270
|
+
this._testObjectsCache.set(obj, true);
|
271
|
+
for (const ch of obj.children) {
|
272
|
+
this.shouldRaycastObject_AddToYesCache(ch);
|
273
|
+
}
|
274
|
+
}
|
275
|
+
|
321
276
|
/** the raycast filter is always overriden */
|
322
277
|
private performRaycast(opts: RaycastOptions | null): THREE.Intersection[] | null {
|
323
278
|
if (!this.raycaster) return null;
|
324
|
-
|
279
|
+
// we clear the cache of previously seen objects
|
280
|
+
this._testObjectsCache.clear();
|
325
281
|
this._sortedHits.length = 0;
|
326
282
|
|
327
283
|
if (!opts) opts = new RaycastOptions();
|
328
|
-
|
329
|
-
// we clear the cache of previously seen objects
|
330
|
-
this._testObjectsCache.clear();
|
331
284
|
opts.testObject = this.shouldRaycastObject;
|
332
285
|
|
333
286
|
for (const rc of this.raycaster) {
|
334
287
|
if (!rc.activeAndEnabled) continue;
|
335
288
|
|
289
|
+
this._currentlyActiveRaycaster = rc;
|
336
290
|
const res = rc.performRaycast(opts);
|
291
|
+
this._currentlyActiveRaycaster = null;
|
337
292
|
|
338
293
|
if (res && res.length > 0) {
|
339
294
|
// console.log(res.length, res.map(r => r.object.name));
|
@@ -346,36 +301,55 @@
|
|
346
301
|
return this._sortedHits;
|
347
302
|
}
|
348
303
|
|
349
|
-
private
|
304
|
+
private assignHitInformation(args: PointerEventData, hit?: Intersection) {
|
305
|
+
if (!hit) {
|
306
|
+
args.point = undefined;
|
307
|
+
args.normal = undefined;
|
308
|
+
args.face = undefined;
|
309
|
+
args.distance = undefined;
|
310
|
+
args.instanceId = undefined;
|
311
|
+
}
|
312
|
+
else {
|
313
|
+
args.point = hit.point;
|
314
|
+
args.normal = hit.normal;
|
315
|
+
args.face = hit.face;
|
316
|
+
args.distance = hit.distance;
|
317
|
+
args.instanceId = hit.instanceId;
|
318
|
+
}
|
319
|
+
}
|
320
|
+
|
321
|
+
private handleIntersections(hits: Intersection[] | null | undefined, args: PointerEventData): boolean {
|
322
|
+
|
350
323
|
if (hits?.length) {
|
351
324
|
hits = this.sortCandidates(hits);
|
352
325
|
for (const hit of hits) {
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
args
|
357
|
-
|
358
|
-
args.instanceId = hit.instanceId;
|
359
|
-
if (this.handleEventOnObject(object, args)) {
|
326
|
+
if (args.event.immediatePropagationStopped) {
|
327
|
+
return false;
|
328
|
+
}
|
329
|
+
this.assignHitInformation(args, hit);
|
330
|
+
if (this.handleEventOnObject(hit.object, args)) {
|
360
331
|
return true;
|
361
332
|
}
|
362
333
|
}
|
363
334
|
}
|
364
335
|
|
336
|
+
// first invoke captured pointers
|
337
|
+
this.assignHitInformation(args, hits?.[0]);
|
338
|
+
this.invokePointerCapture(args);
|
339
|
+
|
365
340
|
// pointer has not hit any object to handle
|
366
341
|
|
367
342
|
// thus is not hovering over anything
|
368
|
-
const hoveredData = this.hoveredByID.get(
|
343
|
+
const hoveredData = this.hoveredByID.get(args.pointerId);
|
369
344
|
if (hoveredData) {
|
370
|
-
this.
|
345
|
+
this.propagatePointerExit(hoveredData.obj, hoveredData.data, null);
|
371
346
|
}
|
372
|
-
this.hoveredByID.delete(
|
347
|
+
this.hoveredByID.delete(args.pointerId);
|
373
348
|
|
374
349
|
// if it was up, it means it doesn't should notify things that it down on before
|
375
350
|
if (args.isUp) {
|
376
|
-
|
377
|
-
|
378
|
-
this.pressedByID.delete(id);
|
351
|
+
this.pressedByID.get(args.pointerId)?.handlers.forEach(h => this.invokeOnPointerUp(args, h));
|
352
|
+
this.pressedByID.delete(args.pointerId);
|
379
353
|
}
|
380
354
|
|
381
355
|
return false;
|
@@ -416,34 +390,29 @@
|
|
416
390
|
private handleEventOnObject(object: THREE.Object3D, args: PointerEventData): boolean {
|
417
391
|
// ensures that invisible objects are ignored
|
418
392
|
if (!this.testIsVisible(object)) {
|
419
|
-
if (args.
|
393
|
+
if (args.isClick && debug)
|
420
394
|
console.log("not allowed", object);
|
421
395
|
return false;
|
422
396
|
}
|
423
397
|
|
424
398
|
// Event without pointer can't be handled
|
425
399
|
if (args.pointerId === undefined) {
|
426
|
-
if(debug) console.
|
400
|
+
if (debug) console.error("Event without pointer can't be handled", args);
|
427
401
|
return false;
|
428
402
|
}
|
429
403
|
|
430
|
-
// We want to call all event methods even if the event was used
|
431
|
-
// Used event can't be handled
|
432
|
-
// if (args.used) return false;
|
433
|
-
|
434
404
|
// Correct the handled object to match the relevant object in shadow dom (?)
|
435
|
-
const originalObject = object;
|
436
405
|
args.object = object;
|
437
406
|
|
438
407
|
const parent = object.parent as any;
|
439
408
|
let isShadow = false;
|
440
|
-
const clicked = args.
|
409
|
+
const clicked = args.isClick ?? false;
|
441
410
|
|
442
411
|
let canvasGroup: ICanvasGroup | null = null;
|
443
412
|
|
444
413
|
// handle potential shadow dom built from three mesh ui
|
445
414
|
if (parent && parent.isUI) {
|
446
|
-
const pressedOrClicked = (args.isPressed || args.
|
415
|
+
const pressedOrClicked = (args.isPressed || args.isClick) ?? false;
|
447
416
|
if (parent[$shadowDomOwner]) {
|
448
417
|
const actualGo = parent[$shadowDomOwner].gameObject;
|
449
418
|
if (actualGo) {
|
@@ -472,11 +441,12 @@
|
|
472
441
|
// Handle OnPointerExit -> in case when we are about to hover something new
|
473
442
|
// TODO: we need to keep track of the components that already received a PointerEnterEvent -> we can have a hierarchy where the hovered object changes but the component is on the parent and should not receive a PointerExit event because it's still hovered (just another child object)
|
474
443
|
const hovering = this.hoveredByID.get(args.pointerId);
|
475
|
-
const
|
444
|
+
const prevHovering = hovering?.obj;
|
445
|
+
const isNewlyHovering = prevHovering !== object;
|
476
446
|
|
477
447
|
// trigger onPointerExit
|
478
|
-
if (isNewlyHovering &&
|
479
|
-
this.
|
448
|
+
if (isNewlyHovering && prevHovering) {
|
449
|
+
this.propagatePointerExit(prevHovering, hovering.data, object);
|
480
450
|
}
|
481
451
|
|
482
452
|
// save hovered object
|
@@ -499,7 +469,7 @@
|
|
499
469
|
}
|
500
470
|
}
|
501
471
|
if (canvasGroup === null || canvasGroup.interactable) {
|
502
|
-
this.handleMainInteraction(object, args,
|
472
|
+
this.handleMainInteraction(object, args, prevHovering ?? null);
|
503
473
|
}
|
504
474
|
|
505
475
|
return true;
|
@@ -508,22 +478,17 @@
|
|
508
478
|
/**
|
509
479
|
* Propagate up in hiearchy and call the callback for each component that is possibly a handler
|
510
480
|
*/
|
511
|
-
private propagate(object:
|
481
|
+
private propagate(object: Object3D | null, onComponent: (behaviour: Behaviour) => void) {
|
512
482
|
|
513
483
|
while (true) {
|
514
|
-
// Propagate up the hierarchy
|
515
484
|
|
516
|
-
if(
|
485
|
+
if (!object) break;
|
517
486
|
|
518
487
|
GameObject.foreachComponent(object, comp => {
|
519
488
|
// TODO: implement Stop Immediate Propagation
|
520
|
-
|
521
489
|
onComponent(comp);
|
522
|
-
// return undefined to continue iterating
|
523
|
-
return undefined;
|
524
490
|
}, false);
|
525
491
|
|
526
|
-
if (!object.parent) break;
|
527
492
|
// walk up
|
528
493
|
object = object.parent;
|
529
494
|
}
|
@@ -533,43 +498,59 @@
|
|
533
498
|
/**
|
534
499
|
* Propagate up in hiearchy and call handlers based on the pointer event data
|
535
500
|
*/
|
536
|
-
private handleMainInteraction(object:
|
537
|
-
if (args.pointerId === undefined) return;
|
501
|
+
private handleMainInteraction(object: Object3D, args: PointerEventData, prevHovering: Object3D | null) {
|
538
502
|
const pressedEvent = this.pressedByID.get(args.pointerId);
|
503
|
+
const hoveredObjectChanged = prevHovering !== object;
|
539
504
|
|
540
|
-
this
|
541
|
-
|
505
|
+
// TODO: should we not move this check up before we even raycast for "pointerMove" events? We dont need to do any processing if the pointer didnt move
|
506
|
+
let isMoving = true;
|
507
|
+
switch (args.event.pointerType) {
|
508
|
+
case "mouse":
|
509
|
+
case "touch":
|
510
|
+
const posLastFrame = this.context.input.getPointerPositionLastFrame(args.pointerId!)!;
|
511
|
+
const posThisFrame = this.context.input.getPointerPosition(args.pointerId!)!;
|
512
|
+
isMoving = posLastFrame && !Mathf.approximately(posLastFrame, posThisFrame);
|
513
|
+
break;
|
514
|
+
case "controller":
|
515
|
+
case "hand":
|
516
|
+
// for hands and controller we assume they are never totally still (except for simulated environments)
|
517
|
+
// we might want to add a threshold here (e.g. if a user holds their hand very still or controller)
|
518
|
+
// so maybe check the angle everxy frame?
|
519
|
+
break;
|
520
|
+
}
|
542
521
|
|
522
|
+
this.propagate(object, (behaviour) => {
|
523
|
+
const comp = behaviour as IComponentCanMaybeReceiveEvents;
|
524
|
+
|
543
525
|
if (comp.interactable === false) return;
|
526
|
+
if (!comp.activeAndEnabled || !comp.enabled) return;
|
544
527
|
|
545
528
|
if (comp.onPointerEnter) {
|
546
|
-
if (
|
547
|
-
|
529
|
+
if (hoveredObjectChanged) {
|
530
|
+
this.handlePointerEnter(comp, args);
|
548
531
|
}
|
549
532
|
}
|
550
533
|
|
551
534
|
if (args.isDown) {
|
552
535
|
if (comp.onPointerDown) {
|
553
536
|
comp.onPointerDown(args);
|
554
|
-
|
555
537
|
// Set the handler that we called the down event on
|
556
538
|
// So we can call the up event on the same handler
|
557
539
|
// In a scenario where we Down on one object and Up on another
|
558
540
|
pressedEvent?.handlers.add(comp);
|
541
|
+
this.handlePointerCapture(args, comp);
|
559
542
|
}
|
560
543
|
}
|
561
544
|
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
if (isMoving && comp.onPointerMove) {
|
567
|
-
comp.onPointerMove(args);
|
545
|
+
if (comp.onPointerMove) {
|
546
|
+
if (isMoving)
|
547
|
+
comp.onPointerMove(args);
|
548
|
+
this.handlePointerCapture(args, comp);
|
568
549
|
}
|
569
550
|
|
570
551
|
if (args.isUp) {
|
571
552
|
if (comp.onPointerUp) {
|
572
|
-
|
553
|
+
this.invokeOnPointerUp(args, comp);
|
573
554
|
|
574
555
|
// We don't want to call Up twice if we Down and Up on the same object
|
575
556
|
// But if we Down on one and Up on another we want to call Up on the first one as well
|
@@ -577,16 +558,9 @@
|
|
577
558
|
// The original component that received the down event SHOULD also receive the up event
|
578
559
|
pressedEvent?.handlers.delete(comp);
|
579
560
|
}
|
580
|
-
|
581
|
-
// handle onExit on touchUp
|
582
|
-
// onExit on mouse is handled when we hover over something else / on nothing
|
583
|
-
if (comp.onPointerExit && args.event?.pointerType === PointerType.Touch) {
|
584
|
-
comp.onPointerExit(args);
|
585
|
-
this.hoveredByID.delete(args.pointerId!);
|
586
|
-
}
|
587
561
|
}
|
588
562
|
|
589
|
-
if (args.
|
563
|
+
if (args.isClick) {
|
590
564
|
if (comp.onPointerClick) {
|
591
565
|
comp.onPointerClick(args);
|
592
566
|
}
|
@@ -597,31 +571,153 @@
|
|
597
571
|
// If user drags away from the object, then it doesn't get the UP event
|
598
572
|
if (args.isUp) {
|
599
573
|
pressedEvent?.handlers.forEach((handler) => {
|
600
|
-
|
601
|
-
handler.onPointerUp(args);
|
602
|
-
}
|
574
|
+
this.invokeOnPointerUp(args, handler);
|
603
575
|
});
|
604
576
|
|
605
577
|
this.pressedByID.delete(args.pointerId);
|
606
578
|
}
|
607
579
|
}
|
608
580
|
|
609
|
-
/**
|
610
|
-
|
611
|
-
|
612
|
-
private triggerOnExit(object: THREE.Object3D, args: PointerEventData) {
|
613
|
-
args.used = false;
|
614
|
-
|
615
|
-
this.propagate(object, args, (behaviour) => {
|
581
|
+
/** Propagate up in hierarchy and call onPointerExit */
|
582
|
+
private propagatePointerExit(object: Object3D, args: PointerEventData, newObject: Object3D | null) {
|
583
|
+
this.propagate(object, (behaviour) => {
|
616
584
|
if (!behaviour.gameObject || behaviour.destroyed) return;
|
617
585
|
|
618
586
|
const inst: any = behaviour;
|
619
587
|
if (inst.onPointerExit) {
|
620
|
-
|
588
|
+
// if the newly hovered object is a child of the current object, we don't want to call onPointerExit
|
589
|
+
if (newObject && this.isChild(newObject, behaviour.gameObject)) {
|
590
|
+
return;
|
591
|
+
}
|
592
|
+
this.handlePointerExit(inst, args);
|
621
593
|
}
|
622
594
|
});
|
623
595
|
}
|
624
596
|
|
597
|
+
/** handles onPointerUp - this will also release the pointerCapture */
|
598
|
+
private invokeOnPointerUp(evt: PointerEventData, handler: IPointerUpHandler) {
|
599
|
+
handler.onPointerUp?.call(handler, evt);
|
600
|
+
this.releasePointerCapture(evt, handler);
|
601
|
+
}
|
602
|
+
|
603
|
+
/** Responsible for invoking onPointerEnter (and updating onPointerExit). We invoke onPointerEnter once per active pointerId */
|
604
|
+
private handlePointerEnter(comp: IComponentCanMaybeReceiveEvents, args: PointerEventData) {
|
605
|
+
if (comp.onPointerEnter) {
|
606
|
+
if (this.updatePointerState(comp, args.pointerId, this.pointerEnterSymbol, true)) {
|
607
|
+
comp.onPointerEnter(args);
|
608
|
+
}
|
609
|
+
}
|
610
|
+
this.updatePointerState(comp, args.pointerId, this.pointerExitSymbol, false);
|
611
|
+
}
|
612
|
+
|
613
|
+
/** Responsible for invoking onPointerExit (and updating onPointerEnter). We invoke onPointerExit once per active pointerId */
|
614
|
+
private handlePointerExit(comp: IComponentCanMaybeReceiveEvents, evt: PointerEventData) {
|
615
|
+
if (comp.onPointerExit) {
|
616
|
+
if (this.updatePointerState(comp, evt.pointerId, this.pointerExitSymbol, true)) {
|
617
|
+
comp.onPointerExit(evt);
|
618
|
+
}
|
619
|
+
}
|
620
|
+
this.updatePointerState(comp, evt.pointerId, this.pointerEnterSymbol, false);
|
621
|
+
}
|
622
|
+
|
623
|
+
/** updates the pointer state list for a component
|
624
|
+
* @param comp the component to update
|
625
|
+
* @param pointerId the pointerId to update
|
626
|
+
* @param symbol the symbol to use for the state
|
627
|
+
* @param add if true, the pointerId is added to the state list, if false the pointerId will be removed
|
628
|
+
*/
|
629
|
+
private updatePointerState(comp: IComponentCanMaybeReceiveEvents, pointerId: number, symbol: symbol, add: boolean) {
|
630
|
+
let state = comp[symbol];
|
631
|
+
|
632
|
+
if (add) {
|
633
|
+
// the pointer is already in the state list
|
634
|
+
if (state && state.includes(pointerId)) return false;
|
635
|
+
state = state || [];
|
636
|
+
state.push(pointerId);
|
637
|
+
comp[symbol] = state;
|
638
|
+
return true;
|
639
|
+
}
|
640
|
+
else {
|
641
|
+
if (!state || !state.includes(pointerId)) return false;
|
642
|
+
const i = state.indexOf(pointerId);
|
643
|
+
if (i !== -1) {
|
644
|
+
state.splice(i, 1);
|
645
|
+
}
|
646
|
+
return true;
|
647
|
+
}
|
648
|
+
}
|
649
|
+
|
650
|
+
/** the list of component handlers that requested pointerCapture for a specific pointerId */
|
651
|
+
private readonly _capturedPointer: { [id: number]: IPointerEventHandler[] } = {};
|
652
|
+
|
653
|
+
/** check if the event was marked to be captured: if yes add the current component to the captured list */
|
654
|
+
private handlePointerCapture(evt: PointerEventData, comp: IPointerEventHandler) {
|
655
|
+
if (evt.z__pointer_ctured) {
|
656
|
+
evt.z__pointer_ctured = false;
|
657
|
+
const id = evt.pointerId;
|
658
|
+
// only the onPointerMove event is called with captured pointers so we don't need to add it to our list if it doesnt implement onPointerMove
|
659
|
+
if (comp.onPointerMove) {
|
660
|
+
const list = this._capturedPointer[id] || [];
|
661
|
+
list.push(comp);
|
662
|
+
this._capturedPointer[id] = list;
|
663
|
+
}
|
664
|
+
else {
|
665
|
+
if (isDevEnvironment() && !comp["z__warned_no_pointermove"]) {
|
666
|
+
comp["z__warned_no_pointermove"] = true;
|
667
|
+
console.warn("PointerCapture was requested but the component doesn't implement onPointerMove. It will not receive any pointer events");
|
668
|
+
}
|
669
|
+
}
|
670
|
+
}
|
671
|
+
else if (evt.z__pointer_cture_rleased) {
|
672
|
+
evt.z__pointer_cture_rleased = false;
|
673
|
+
this.releasePointerCapture(evt, comp);
|
674
|
+
}
|
675
|
+
}
|
676
|
+
|
677
|
+
/** removes the component from the pointer capture list */
|
678
|
+
releasePointerCapture(evt: PointerEventData, component: IPointerEventHandler) {
|
679
|
+
const id = evt.pointerId;
|
680
|
+
if (this._capturedPointer[id]) {
|
681
|
+
const i = this._capturedPointer[id].indexOf(component);
|
682
|
+
if (i !== -1) {
|
683
|
+
this._capturedPointer[id].splice(i, 1);
|
684
|
+
if (debug) console.log("released pointer capture", id, component, this._capturedPointer)
|
685
|
+
}
|
686
|
+
}
|
687
|
+
}
|
688
|
+
/** invoke the pointerMove event on all captured handlers */
|
689
|
+
private invokePointerCapture(evt: PointerEventData) {
|
690
|
+
if (evt.event.type === InputEvents.PointerMove) {
|
691
|
+
const id = evt.pointerId;
|
692
|
+
const captured = this._capturedPointer[id];
|
693
|
+
if (captured) {
|
694
|
+
if (debug) console.log("Captured", id, captured)
|
695
|
+
for (let i = 0; i < captured.length; i++) {
|
696
|
+
const handler = captured[i];
|
697
|
+
// check if it was destroyed
|
698
|
+
const comp = handler as IComponent;
|
699
|
+
if (comp.destroyed) {
|
700
|
+
captured.splice(i, 1);
|
701
|
+
i--;
|
702
|
+
continue;
|
703
|
+
}
|
704
|
+
// invoke pointer move
|
705
|
+
handler.onPointerMove?.call(handler, evt);
|
706
|
+
}
|
707
|
+
}
|
708
|
+
}
|
709
|
+
}
|
710
|
+
|
711
|
+
private readonly pointerEnterSymbol = Symbol("pointerEnter");
|
712
|
+
private readonly pointerExitSymbol = Symbol("pointerExit");
|
713
|
+
|
714
|
+
private isChild(obj: Object3D, possibleChild: Object3D): boolean {
|
715
|
+
if (!obj || !possibleChild) return false;
|
716
|
+
if (obj === possibleChild) return true;
|
717
|
+
if (!obj.parent) return false;
|
718
|
+
return this.isChild(obj.parent, possibleChild);
|
719
|
+
}
|
720
|
+
|
625
721
|
private handleMeshUiObjectWithoutShadowDom(obj: any, pressed: boolean) {
|
626
722
|
if (!obj || !obj.isUI) return true;
|
627
723
|
const hit = this.handleMeshUIIntersection(obj, pressed);
|
@@ -629,7 +725,7 @@
|
|
629
725
|
return hit;
|
630
726
|
}
|
631
727
|
|
632
|
-
private currentActiveMeshUIComponents:
|
728
|
+
private currentActiveMeshUIComponents: Object3D[] = [];
|
633
729
|
|
634
730
|
private handleMeshUIIntersection(meshUiObject: THREE.Object3D, pressed: boolean): boolean {
|
635
731
|
const res = MeshUIHelper.updateState(meshUiObject, pressed);
|
@@ -697,8 +793,8 @@
|
|
697
793
|
threeMeshUI.update();
|
698
794
|
}
|
699
795
|
|
700
|
-
static updateState(intersect: THREE.Object3D, _selectState: boolean):
|
701
|
-
let foundBlock:
|
796
|
+
static updateState(intersect: THREE.Object3D, _selectState: boolean): Object3D | null {
|
797
|
+
let foundBlock: Object3D | null = null;
|
702
798
|
|
703
799
|
if (intersect) {
|
704
800
|
foundBlock = this.findBlockInParent(intersect);
|
@@ -725,7 +821,7 @@
|
|
725
821
|
this.needsUpdate = true;
|
726
822
|
}
|
727
823
|
|
728
|
-
static findBlockInParent(elem: any):
|
824
|
+
static findBlockInParent(elem: any): Object3D | null {
|
729
825
|
if (!elem) return null;
|
730
826
|
if (elem.isBlock) {
|
731
827
|
// @TODO : Replace states managements
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import { serializable } from "../engine/engine_serialization.js";
|
2
|
+
import { Behaviour } from "./Component.js"
|
2
3
|
import { EventList } from "./EventList.js";
|
4
|
+
import { EventType } from "./EventType.js"
|
3
5
|
import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js"
|
4
|
-
import { Behaviour } from "./Component.js"
|
5
|
-
import { EventType } from "./EventType.js"
|
6
6
|
|
7
7
|
class TriggerEvent {
|
8
8
|
@serializable()
|
@@ -1,9 +1,10 @@
|
|
1
|
-
import {
|
1
|
+
import { Texture } from "three";
|
2
2
|
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
|
3
|
-
import { Texture } from "three";
|
4
3
|
import { type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
5
4
|
|
5
|
+
import { getParam } from "../engine_utils.js";
|
6
6
|
|
7
|
+
|
7
8
|
const debug = getParam("debugexr");
|
8
9
|
|
9
10
|
export class EXT_texture_exr implements GLTFLoaderPlugin {
|
@@ -1,7 +1,8 @@
|
|
1
|
-
import { type IExtensionReferenceResolver } from "./extension_resolver.js";
|
2
1
|
import { GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
|
+
|
3
3
|
import { debugExtension } from "../engine_default_parameters.js";
|
4
4
|
import { getParam } from "../engine_utils.js";
|
5
|
+
import { type IExtensionReferenceResolver } from "./extension_resolver.js";
|
5
6
|
|
6
7
|
const debug = getParam("debugresolvedependencies");
|
7
8
|
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import { USDObject, USDZExporterContext } from "./ThreeUSDZExporter.js";
|
2
1
|
import { Object3D } from "three";
|
3
2
|
|
3
|
+
import { USDObject, USDZExporterContext } from "./ThreeUSDZExporter.js";
|
4
|
+
|
4
5
|
export interface IUSDExporterExtension {
|
5
6
|
|
6
7
|
get extensionName(): string;
|
@@ -1,20 +1,21 @@
|
|
1
|
-
import {
|
1
|
+
import { GLTFExporter, GLTFExporterPlugin, GLTFWriter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
2
2
|
import { GLTFLoader, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
|
+
|
4
|
+
import { isDevEnvironment } from "../debug/index.js";
|
5
|
+
import { isResourceTrackingEnabled } from "../engine_assetdatabase.js";
|
6
|
+
import { Context } from "../engine_setup.js";
|
7
|
+
import { type ConstructorConcrete, type SourceIdentifier } from "../engine_types.js";
|
8
|
+
import { getParam } from "../engine_utils.js";
|
9
|
+
import { NEEDLE_lightmaps } from "../extensions/NEEDLE_lightmaps.js";
|
10
|
+
import { EXT_texture_exr } from "./EXT_texture_exr.js";
|
3
11
|
import { NEEDLE_components } from "./NEEDLE_components.js";
|
4
|
-
import { EXT_texture_exr } from "./EXT_texture_exr.js";
|
5
12
|
import { NEEDLE_gameobject_data } from "./NEEDLE_gameobject_data.js";
|
13
|
+
import { NEEDLE_lighting_settings } from "./NEEDLE_lighting_settings.js";
|
6
14
|
import { NEEDLE_persistent_assets } from "./NEEDLE_persistent_assets.js";
|
7
|
-
import {
|
8
|
-
import { type ConstructorConcrete, type SourceIdentifier } from "../engine_types.js";
|
9
|
-
import { Context } from "../engine_setup.js";
|
10
|
-
import { NEEDLE_lighting_settings } from "./NEEDLE_lighting_settings.js";
|
15
|
+
import { NEEDLE_progressive } from "./NEEDLE_progressive.js";
|
11
16
|
import { NEEDLE_render_objects } from "./NEEDLE_render_objects.js";
|
12
|
-
import {
|
17
|
+
import { NEEDLE_techniques_webgl } from "./NEEDLE_techniques_webgl.js";
|
13
18
|
import { InternalUsageTrackerPlugin } from "./usage_tracker.js";
|
14
|
-
import { isResourceTrackingEnabled } from "../engine_assetdatabase.js";
|
15
|
-
import { getParam } from "../engine_utils.js";
|
16
|
-
import { isDevEnvironment } from "../debug/index.js";
|
17
|
-
import { GLTFExporter, GLTFExporterPlugin, GLTFWriter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
18
19
|
|
19
20
|
const debug = getParam("debugextensions");
|
20
21
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { Object3D } from "three";
|
2
|
+
|
2
3
|
import type { Constructor } from "../../engine/engine_types.js";
|
3
4
|
|
4
5
|
const handlers: Map<any, ApplyPrototypeExtension> = new Map();
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
1
|
import { FlyControls as ThreeFlyControls } from "three/examples/jsm/controls/FlyControls.js";
|
2
|
+
|
3
3
|
import { Camera } from "./Camera.js";
|
4
|
+
import { Behaviour, GameObject } from "./Component.js";
|
4
5
|
|
5
6
|
export class FlyControls extends Behaviour {
|
6
7
|
private _controls: ThreeFlyControls | null = null;
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import { Behaviour } from "./Component.js";
|
2
1
|
import { Color, Fog as Fog3 } from "three";
|
2
|
+
|
3
3
|
import { serializable } from "../engine/engine_serialization.js";
|
4
|
+
import { Behaviour } from "./Component.js";
|
4
5
|
|
5
6
|
|
6
7
|
export enum FogMode {
|
@@ -1,10 +1,11 @@
|
|
1
|
-
import { Behaviour } from "./Component.js";
|
2
1
|
import * as THREE from "three";
|
2
|
+
import { BoxHelper, Color } from "three";
|
3
|
+
|
4
|
+
import * as params from "../engine/engine_default_parameters.js";
|
3
5
|
import * as Gizmos from "../engine/engine_gizmos.js";
|
4
|
-
import
|
6
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
7
|
import { FrameEvent } from "../engine/engine_setup.js";
|
6
|
-
import {
|
7
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
|
+
import { Behaviour } from "./Component.js";
|
8
9
|
|
9
10
|
|
10
11
|
export class BoxGizmo extends Behaviour {
|
@@ -1,17 +1,17 @@
|
|
1
1
|
import { Object3D, Vector3 } from "three";
|
2
|
+
import { AnimationClip } from "three";
|
2
3
|
import { GLTFExporter, type GLTFExporterOptions } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
3
4
|
|
4
|
-
import { Behaviour, GameObject } from "../../Component.js";
|
5
|
-
import GLTFMeshGPUInstancingExtension from '../../../include/three/EXT_mesh_gpu_instancing_exporter.js';
|
6
|
-
import { Renderer } from "../../Renderer.js";
|
7
5
|
import { SerializationContext } from "../../../engine/engine_serialization_core.js";
|
8
6
|
import { serializable } from "../../../engine/engine_serialization_decorator.js";
|
9
|
-
import { NEEDLE_components } from "../../../engine/extensions/NEEDLE_components.js";
|
10
7
|
import { getWorldPosition } from "../../../engine/engine_three_utils.js";
|
11
|
-
import { BoxHelperComponent } from "../../BoxHelperComponent.js";
|
12
|
-
import { AnimationClip } from "three";
|
13
8
|
import { getParam } from "../../../engine/engine_utils.js";
|
14
9
|
import { registerExportExtensions } from "../../../engine/extensions/index.js";
|
10
|
+
import { NEEDLE_components } from "../../../engine/extensions/NEEDLE_components.js";
|
11
|
+
import GLTFMeshGPUInstancingExtension from '../../../include/three/EXT_mesh_gpu_instancing_exporter.js';
|
12
|
+
import { BoxHelperComponent } from "../../BoxHelperComponent.js";
|
13
|
+
import { Behaviour, GameObject } from "../../Component.js";
|
14
|
+
import { Renderer } from "../../Renderer.js";
|
15
15
|
|
16
16
|
const debugExport = getParam("debuggltfexport");
|
17
17
|
|
@@ -1,14 +1,15 @@
|
|
1
|
-
import {
|
1
|
+
import { Color, LinearSRGBColorSpace, Object3D, SRGBColorSpace, Texture } from 'three';
|
2
2
|
import * as ThreeMeshUI from 'three-mesh-ui'
|
3
|
+
import SimpleStateBehavior from "three-mesh-ui/examples/behaviors/states/SimpleStateBehavior.js"
|
4
|
+
|
5
|
+
import { serializable } from '../../engine/engine_serialization_decorator.js';
|
6
|
+
import { GameObject } from '../Component.js';
|
3
7
|
import { RGBAColor } from "../js-extensions/RGBAColor.js"
|
4
8
|
import { BaseUIComponent } from "./BaseUIComponent.js";
|
5
|
-
import {
|
6
|
-
import {
|
9
|
+
import { type IGraphic, type IRectTransformChangedReceiver } from './Interfaces.js';
|
10
|
+
import { Outline } from './Outline.js';
|
7
11
|
import { RectTransform } from './RectTransform.js';
|
8
12
|
import { onChange, scheduleAction } from "./Utils.js"
|
9
|
-
import { GameObject } from '../Component.js';
|
10
|
-
import SimpleStateBehavior from "three-mesh-ui/examples/behaviors/states/SimpleStateBehavior.js"
|
11
|
-
import { Outline } from './Outline.js';
|
12
13
|
|
13
14
|
const _colorStateObject: { backgroundColor: Color, backgroundOpacity: number, borderColor: Color, borderOpacity: number } = {
|
14
15
|
backgroundColor: new Color(1, 1, 1),
|
@@ -137,7 +138,7 @@
|
|
137
138
|
onEnable(): void {
|
138
139
|
super.onEnable();
|
139
140
|
if (this.uiObject) {
|
140
|
-
this.rectTransform.shadowComponent?.add(this.uiObject);
|
141
|
+
this.rectTransform.shadowComponent?.add(this.uiObject as unknown as Object3D);
|
141
142
|
this.addShadowComponent(this.uiObject, this.rectTransform);
|
142
143
|
}
|
143
144
|
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import { Behaviour } from "./Component.js";
|
2
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
3
|
-
import * as params from "../engine/engine_default_parameters.js";
|
4
1
|
import { Color, GridHelper as _GridHelper } from "three";
|
5
2
|
|
3
|
+
import * as params from "../engine/engine_default_parameters.js";
|
4
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
+
import { Behaviour } from "./Component.js";
|
6
|
+
|
6
7
|
export class GridHelper extends Behaviour {
|
7
8
|
|
8
9
|
@serializable()
|
@@ -1,9 +1,10 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
|
-
import { GroundProjectedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundProjectedSkybox.js';
|
3
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
|
-
import { Watch as Watch, getParam } from "../engine/engine_utils.js";
|
5
1
|
import { Texture } from "three";
|
2
|
+
import { GroundedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundedSkybox.js';
|
6
3
|
|
4
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
+
import { getParam,Watch as Watch } from "../engine/engine_utils.js";
|
6
|
+
import { Behaviour, GameObject } from "./Component.js";
|
7
|
+
|
7
8
|
const debug = getParam("debuggroundprojection");
|
8
9
|
|
9
10
|
export class GroundProjectedEnv extends Behaviour {
|
@@ -81,14 +82,19 @@
|
|
81
82
|
if (!this.env || this.context.scene.environment !== this._lastEnvironment) {
|
82
83
|
if (debug)
|
83
84
|
console.log("Create/Update Ground Projection", this.context.scene.environment.name);
|
84
|
-
this.env = new GroundProjection(this.context.scene.environment);
|
85
|
+
this.env = new GroundProjection(this.context.scene.environment, this._height, this.radius, 32);
|
86
|
+
this.env.position.y = this._height;
|
85
87
|
}
|
86
88
|
this._lastEnvironment = this.context.scene.environment;
|
87
89
|
if (!this.env.parent)
|
88
90
|
this.gameObject.add(this.env);
|
91
|
+
|
92
|
+
/* TODO realtime adjustments aren't possible anymore with GroundedSkybox (mesh generation)
|
89
93
|
this.env.scale.setScalar(this._scale);
|
90
94
|
this.env.radius = this._radius;
|
91
95
|
this.env.height = this._height;
|
96
|
+
*/
|
97
|
+
|
92
98
|
// dont make the ground projection raycastable by default
|
93
99
|
if (this.env.isObject3D === true) {
|
94
100
|
this.env.layers.set(2);
|
@@ -1,5 +1,6 @@
|
|
1
|
+
import { Color, Texture } from 'three';
|
2
|
+
|
1
3
|
import { serializable } from '../../engine/engine_serialization_decorator.js';
|
2
|
-
import { Color, Texture } from 'three';
|
3
4
|
import { MaskableGraphic } from './Graphic.js';
|
4
5
|
|
5
6
|
|
@@ -1,3 +1,3 @@
|
|
1
|
-
export {
|
2
|
-
export { USDObject
|
3
|
-
export {
|
1
|
+
export { type UsdzBehaviour } from "./extensions/behavior/Behaviour.js";
|
2
|
+
export { imageToCanvas,USDObject } from "./ThreeUSDZExporter.js";
|
3
|
+
export { USDZExporter } from "./USDZExporter.js";
|
@@ -1,4 +1,4 @@
|
|
1
|
+
export * from "./PostProcessingEffect.js";
|
2
|
+
export * from "./PostProcessingHandler.js"
|
1
3
|
export * from "./VolumeParameter.js"
|
2
|
-
export * from "./PostProcessingHandler.js"
|
3
|
-
export * from "./PostProcessingEffect.js";
|
4
4
|
export * from "./VolumeProfile.js";
|
@@ -1,4 +1,4 @@
|
|
1
|
+
export { type ITimelineAnimationCallbacks as ITimelineAnimationOverride } from "./PlayableDirector.js"
|
1
2
|
export * from "./SignalAsset.js"
|
2
|
-
export * from "./TimelineTracks.js"
|
3
3
|
export * from "./TimelineModels.js"
|
4
|
-
export
|
4
|
+
export * from "./TimelineTracks.js"
|
@@ -1,4 +1,3 @@
|
|
1
|
-
export
|
2
|
-
export * from "./WebXRPlaneTracking.js";
|
1
|
+
export { WebXR as WebXR } from "./WebXR.js";
|
3
2
|
export * from "./WebXRImageTracking.js";
|
4
|
-
export * from "./
|
3
|
+
export * from "./WebXRPlaneTracking.js";
|
@@ -1,5 +1,5 @@
|
|
1
1
|
export * from "./extensions.js"
|
2
2
|
export * from "./NEEDLE_animator_controller_model.js"
|
3
|
+
export { SceneLightSettings } from "./NEEDLE_lighting_settings.js"
|
3
4
|
export * from "./NEEDLE_progressive.js"
|
4
|
-
export { CustomShader } from "./NEEDLE_techniques_webgl.js"
|
5
|
-
export { SceneLightSettings } from "./NEEDLE_lighting_settings.js"
|
5
|
+
export { CustomShader } from "./NEEDLE_techniques_webgl.js"
|
@@ -1,10 +1,10 @@
|
|
1
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
2
|
+
import { FrameEvent } from "../../engine/engine_setup.js";
|
3
|
+
import { getParam, isiOS } from "../../engine/engine_utils.js";
|
1
4
|
import { Behaviour, GameObject } from "../Component.js";
|
5
|
+
import { EventList } from "../EventList.js";
|
2
6
|
import { type IPointerEventHandler } from "./PointerEvents.js";
|
3
|
-
import { FrameEvent } from "../../engine/engine_setup.js";
|
4
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
5
7
|
import { Text } from "./Text.js";
|
6
|
-
import { getParam, isiOS } from "../../engine/engine_utils.js";
|
7
|
-
import { EventList } from "../EventList.js";
|
8
8
|
import { tryGetUIComponent } from "./Utils.js";
|
9
9
|
|
10
10
|
const debug = getParam("debuginputfield");
|
@@ -1,19 +1,11 @@
|
|
1
1
|
import { Behaviour } from "./Component.js";
|
2
|
-
import type { IPointerClickHandler, PointerEventData } from "./ui/PointerEvents.js";
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
onPointerClick(_args: PointerEventData) {
|
10
|
-
}
|
11
|
-
}
|
12
|
-
|
13
|
-
|
14
|
-
// TODO: how do we sync things like that...
|
3
|
+
/**
|
4
|
+
* Marks an object as currently being interacted with.
|
5
|
+
* For example, DragControls set this on the dragged object to prevent DeleteBox from deleting it.
|
6
|
+
*/
|
15
7
|
export class UsageMarker extends Behaviour
|
16
8
|
{
|
17
|
-
public isUsed
|
18
|
-
public usedBy
|
9
|
+
public isUsed: boolean = true;
|
10
|
+
public usedBy: any = null;
|
19
11
|
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { Vector3 } from "three";
|
2
|
+
|
2
3
|
import { serializable } from "../engine/engine_serialization.js";
|
3
4
|
import { Behaviour } from "./Component.js";
|
4
5
|
import { Rigidbody } from "./RigidBody.js";
|
@@ -1,9 +1,9 @@
|
|
1
|
-
import {
|
1
|
+
import { serializable } from "../../engine/engine_serialization.js";
|
2
|
+
import { getParam } from "../../engine/engine_utils.js";
|
2
3
|
import { Behaviour, GameObject } from "../Component.js";
|
3
|
-
import { serializable } from "../../engine/engine_serialization.js";
|
4
4
|
import { Canvas } from "./Canvas.js";
|
5
|
+
import { type ILayoutGroup, type IRectTransform } from "./Interfaces.js";
|
5
6
|
import { RectTransform } from "./RectTransform.js";
|
6
|
-
import { getParam } from "../../engine/engine_utils.js";
|
7
7
|
|
8
8
|
const debug = getParam("debuguilayout");
|
9
9
|
|
@@ -1,13 +1,14 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
1
|
import * as THREE from "three";
|
2
|
+
import { Color, DirectionalLight, OrthographicCamera } from "three";
|
3
|
+
|
4
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
+
import { FrameEvent } from "../engine/engine_setup.js";
|
6
|
+
import { setWorldPositionXYZ } from "../engine/engine_three_utils.js";
|
7
|
+
import type { ILight } from "../engine/engine_types.js";
|
3
8
|
import { getParam, isMobileDevice } from "../engine/engine_utils.js";
|
4
|
-
import {
|
5
|
-
import {
|
6
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
7
|
-
import { Color, DirectionalLight, OrthographicCamera } from "three";
|
8
|
-
import { WebXR, WebXREvent } from "./webxr/WebXR.js";
|
9
|
+
import { NeedleXREventArgs } from "../engine/xr/index.js";
|
10
|
+
import { Behaviour, GameObject } from "./Component.js";
|
9
11
|
import { WebARSessionRoot } from "./webxr/WebARSessionRoot.js";
|
10
|
-
import type { ILight } from "../engine/engine_types.js";
|
11
12
|
|
12
13
|
// https://threejs.org/examples/webgl_shadowmap_csm.html
|
13
14
|
|
@@ -270,8 +271,6 @@
|
|
270
271
|
}
|
271
272
|
if (this.type === LightType.Directional)
|
272
273
|
this.startCoroutine(this.updateMainLightRoutine(), FrameEvent.LateUpdate);
|
273
|
-
this._webXRStartedListener = WebXR.addEventListener(WebXREvent.XRStarted, this.onWebXRStarted.bind(this));
|
274
|
-
this._webXREndedListener = WebXR.addEventListener(WebXREvent.XRStopped, this.onWebXREnded.bind(this));
|
275
274
|
}
|
276
275
|
|
277
276
|
onDisable() {
|
@@ -282,15 +281,13 @@
|
|
282
281
|
else
|
283
282
|
this.light.visible = false;
|
284
283
|
}
|
285
|
-
WebXR.removeEventListener(WebXREvent.XRStarted, this._webXRStartedListener);
|
286
|
-
WebXR.removeEventListener(WebXREvent.XRStopped, this._webXREndedListener);
|
287
284
|
}
|
288
285
|
|
289
286
|
private _webXRStartedListener?: Function;
|
290
287
|
private _webXREndedListener?: Function;
|
291
288
|
private _webARRoot?: WebARSessionRoot;
|
292
289
|
|
293
|
-
|
290
|
+
onEnterXR(_args: NeedleXREventArgs): void {
|
294
291
|
this._webARRoot = GameObject.getComponentInParent(this.gameObject, WebARSessionRoot) ?? undefined;
|
295
292
|
// this.startCoroutine(this._updateLightIntensityInARRoutine());
|
296
293
|
}
|
@@ -303,7 +300,7 @@
|
|
303
300
|
// }
|
304
301
|
// }
|
305
302
|
|
306
|
-
|
303
|
+
onLeaveXR(_args: NeedleXREventArgs): void {
|
307
304
|
// this.updateIntensity();
|
308
305
|
}
|
309
306
|
|
@@ -1,10 +1,11 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
1
|
import * as THREE from "three";
|
3
|
-
import { Renderer } from "./Renderer.js";
|
4
|
-
import { getParam } from "../engine/engine_utils.js";
|
5
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
6
2
|
import { Vector3 } from "three";
|
7
3
|
|
4
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
+
import { getParam } from "../engine/engine_utils.js";
|
6
|
+
import { Behaviour, GameObject } from "./Component.js";
|
7
|
+
import { Renderer } from "./Renderer.js";
|
8
|
+
|
8
9
|
const debug = getParam("debuglods");
|
9
10
|
const noLods = getParam("nolods");
|
10
11
|
|
@@ -1,6 +1,6 @@
|
|
1
|
-
import { Behaviour } from "../../engine-components/Component.js";
|
2
1
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
3
2
|
import { getParam } from "../../engine/engine_utils.js";
|
3
|
+
import { Behaviour } from "../../engine-components/Component.js";
|
4
4
|
|
5
5
|
const debug = getParam("logstats");
|
6
6
|
|
@@ -1,11 +1,11 @@
|
|
1
|
+
import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
|
2
|
+
|
1
3
|
import { serializable } from "../../engine/engine_serialization.js";
|
2
|
-
import { Behaviour } from "../Component.js";
|
3
|
-
import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
|
4
4
|
import { getWorldPosition, getWorldQuaternion, lookAtObject, setWorldQuaternion } from "../../engine/engine_three_utils.js";
|
5
|
-
|
6
|
-
import { USDObject } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
7
5
|
import { type UsdzBehaviour } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
|
8
6
|
import { ActionBuilder, BehaviorModel, TriggerBuilder, USDVec3 } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
7
|
+
import { USDObject } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
8
|
+
import { Behaviour } from "../Component.js";
|
9
9
|
|
10
10
|
export class LookAt extends Behaviour implements UsdzBehaviour {
|
11
11
|
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
1
|
import * as THREE from "three";
|
3
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
2
|
import { Object3D } from "three";
|
5
3
|
|
4
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
+
import { Behaviour, GameObject } from "./Component.js";
|
6
|
+
|
6
7
|
export class LookAtConstraint extends Behaviour {
|
7
8
|
|
8
9
|
constraintActive: boolean = true;
|
@@ -1,7 +1,8 @@
|
|
1
|
+
import { AnimationAction, AnimationClip, MathUtils, Object3D } from "three"
|
2
|
+
|
3
|
+
import { InstantiateIdProvider } from "../../engine/engine_networking_instantiate.js";
|
1
4
|
import { Animator } from "../../engine-components/Animator.js";
|
2
|
-
import { AnimationAction, AnimationClip, MathUtils, Object3D } from "three"
|
3
5
|
import { Context } from "../engine_setup.js";
|
4
|
-
import { InstantiateIdProvider } from "../../engine/engine_networking_instantiate.js";
|
5
6
|
|
6
7
|
|
7
8
|
export declare type AnimatorControllerModel = {
|
@@ -1,12 +1,13 @@
|
|
1
|
+
import { Object3D } from "three";
|
2
|
+
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
1
3
|
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
|
+
|
5
|
+
import { apply } from "../../engine-components/js-extensions/Object3D.js";
|
6
|
+
import { builtinComponentKeyName } from "../engine_constants.js";
|
7
|
+
import { debugExtension } from "../engine_default_parameters.js";
|
8
|
+
import { getLoader } from "../engine_gltf.js";
|
2
9
|
import { type NodeToObjectMap, type ObjectToNodeMap, SerializationContext } from "../engine_serialization_core.js";
|
3
|
-
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
4
|
-
import { debugExtension } from "../engine_default_parameters.js";
|
5
|
-
import { builtinComponentKeyName } from "../engine_constants.js";
|
6
10
|
import { resolveReferences } from "./extension_utils.js";
|
7
|
-
import { apply } from "../../engine-components/js-extensions/Object3D.js";
|
8
|
-
import { getLoader } from "../engine_gltf.js";
|
9
|
-
import { Object3D } from "three";
|
10
11
|
|
11
12
|
export const debug = debugExtension
|
12
13
|
const componentsArrayExportKey = "$___Export_Components";
|
@@ -39,7 +39,7 @@
|
|
39
39
|
// }
|
40
40
|
|
41
41
|
// private lastIndex: number = -1;
|
42
|
-
afterRoot(_result: GLTF): Promise<
|
42
|
+
afterRoot(_result: GLTF): Promise<any> | null {
|
43
43
|
// console.log("AFTER ROOT", _result);
|
44
44
|
const promises: Promise<void>[] = [];
|
45
45
|
for (let index = 0; index < this.parser.json.nodes?.length; index++) {
|
@@ -52,7 +52,7 @@
|
|
52
52
|
}
|
53
53
|
}
|
54
54
|
}
|
55
|
-
return Promise.all(promises).then(() =>
|
55
|
+
return Promise.all(promises).then(() => null);
|
56
56
|
}
|
57
57
|
|
58
58
|
private async findAndApplyExtensionData(nodeId: number, ext: GameObjectData) {
|
@@ -76,7 +76,7 @@
|
|
76
76
|
node.userData.static = ext.static ?? false;
|
77
77
|
|
78
78
|
node.visible = ext.activeSelf ?? true;
|
79
|
-
|
79
|
+
|
80
80
|
node["guid"] = ext.guid;
|
81
81
|
// console.log(node.name, ext.activeSelf, node);
|
82
82
|
}
|
@@ -1,14 +1,15 @@
|
|
1
1
|
import { AmbientLight, Color, HemisphereLight, Object3D } from "three";
|
2
|
+
import { LightProbe } from "three";
|
2
3
|
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
|
-
|
4
|
+
|
4
5
|
import { Behaviour, GameObject } from "../../engine-components/Component.js";
|
6
|
+
import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
|
7
|
+
import { Mathf } from "../engine_math.js";
|
5
8
|
import { AmbientMode, DefaultReflectionMode } from "../engine_scenelighting.js";
|
9
|
+
import { Context } from "../engine_setup.js";
|
10
|
+
import { type SourceIdentifier } from "../engine_types.js";
|
11
|
+
import { getParam } from "../engine_utils.js";
|
6
12
|
import { LightmapType } from "./NEEDLE_lightmaps.js";
|
7
|
-
import { getParam } from "../engine_utils.js";
|
8
|
-
import { Context } from "../engine_setup.js";
|
9
|
-
import { LightProbe } from "three";
|
10
|
-
import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
|
11
|
-
import { Mathf } from "../engine_math.js";
|
12
13
|
|
13
14
|
export const EXTENSION_NAME = "NEEDLE_lighting_settings";
|
14
15
|
const debug = getParam("debugenvlight");
|
@@ -1,12 +1,13 @@
|
|
1
|
-
import { type ILightDataRegistry } from "../engine_lightdata.js";
|
2
1
|
import { LinearSRGBColorSpace, SRGBColorSpace, Texture, TextureLoader } from "three";
|
2
|
+
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
|
3
3
|
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
|
+
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
|
5
|
+
|
6
|
+
import { isDevEnvironment } from "../debug/debug.js";
|
7
|
+
import { type ILightDataRegistry } from "../engine_lightdata.js";
|
4
8
|
import { type SourceIdentifier } from "../engine_types.js";
|
9
|
+
import { getParam, PromiseAllWithErrors, resolveUrl } from "../engine_utils.js";
|
5
10
|
import { resolveReferences } from "./extension_utils.js";
|
6
|
-
import { getParam, PromiseAllWithErrors, resolveUrl } from "../engine_utils.js";
|
7
|
-
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
|
8
|
-
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
|
9
|
-
import { isDevEnvironment } from "../debug/debug.js";
|
10
11
|
|
11
12
|
// the lightmap extension is aimed to also export export skyboxes and custom reflection maps
|
12
13
|
// should we rename it?
|
@@ -60,7 +61,7 @@
|
|
60
61
|
if (debug)
|
61
62
|
console.log(ext);
|
62
63
|
|
63
|
-
return new Promise(async (
|
64
|
+
return new Promise(async (resolve, _reject) => {
|
64
65
|
|
65
66
|
const dependencies: Array<Promise<any>> = [];
|
66
67
|
for (const entry of arr) {
|
@@ -97,7 +98,7 @@
|
|
97
98
|
if (isDevEnvironment())
|
98
99
|
console.error("Failed to load lightmap extension", results);
|
99
100
|
}
|
100
|
-
|
101
|
+
resolve();
|
101
102
|
});
|
102
103
|
}
|
103
104
|
}
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import { resolveReferences } from "./extension_utils.js";
|
2
1
|
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
|
-
|
2
|
+
|
4
3
|
import { debugExtension } from "../engine_default_parameters.js";
|
5
4
|
import { TypeStore } from "../engine_typestore.js";
|
5
|
+
import { type IExtensionReferenceResolver } from "./extension_resolver.js";
|
6
|
+
import { resolveReferences } from "./extension_utils.js";
|
6
7
|
|
7
8
|
export const EXTENSION_NAME = "NEEDLE_persistent_assets";
|
8
9
|
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import { Material, RawShaderMaterial, Texture, TextureLoader } from "three";
|
2
2
|
import { type GLTF, GLTFLoader, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
|
+
|
4
|
+
import { addDracoAndKTX2Loaders } from "../engine_loaders.js";
|
5
|
+
import { Context } from "../engine_setup.js";
|
3
6
|
import { type SourceIdentifier } from "../engine_types.js";
|
4
|
-
import {
|
5
|
-
import { addDracoAndKTX2Loaders } from "../engine_loaders.js";
|
6
|
-
import { PromiseAllWithErrors, PromiseErrorResult, delay, getParam, resolveUrl } from "../engine_utils.js";
|
7
|
+
import { delay, getParam, PromiseAllWithErrors, PromiseErrorResult, resolveUrl } from "../engine_utils.js";
|
7
8
|
|
8
9
|
export const EXTENSION_NAME = "NEEDLE_progressive";
|
9
10
|
|
@@ -132,6 +133,7 @@
|
|
132
133
|
if (t.source)
|
133
134
|
t.source[$progressiveTextureExtension] = ext;
|
134
135
|
NEEDLE_progressive.cache.set(t.uuid, ext);
|
136
|
+
return t;
|
135
137
|
});
|
136
138
|
}
|
137
139
|
}
|
@@ -1,34 +1,34 @@
|
|
1
1
|
|
2
|
-
import { type SourceIdentifier } from "../engine_types.js";
|
3
|
-
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
|
-
import { type IComponent as Component, type IRenderer } from "../engine_types.js";
|
5
|
-
|
6
2
|
import {
|
7
|
-
|
8
|
-
|
9
|
-
|
3
|
+
AlwaysStencilFunc,
|
4
|
+
DecrementStencilOp,
|
5
|
+
DecrementWrapStencilOp,
|
10
6
|
EqualStencilFunc,
|
11
|
-
|
7
|
+
GreaterEqualStencilFunc,
|
12
8
|
GreaterStencilFunc,
|
13
|
-
NotEqualStencilFunc,
|
14
|
-
GreaterEqualStencilFunc,
|
15
|
-
AlwaysStencilFunc,
|
16
|
-
// stencil ops
|
17
|
-
ZeroStencilOp,
|
18
|
-
KeepStencilOp,
|
19
|
-
ReplaceStencilOp,
|
20
9
|
IncrementStencilOp,
|
21
|
-
DecrementStencilOp,
|
22
10
|
IncrementWrapStencilOp,
|
23
|
-
DecrementWrapStencilOp,
|
24
11
|
InvertStencilOp,
|
12
|
+
KeepStencilOp,
|
13
|
+
LessEqualStencilFunc,
|
14
|
+
LessStencilFunc,
|
15
|
+
// stencil funcs
|
16
|
+
NeverStencilFunc,
|
17
|
+
NotEqualStencilFunc,
|
18
|
+
ReplaceStencilOp,
|
25
19
|
type StencilFunc,
|
26
20
|
type StencilOp as ThreeStencilOp,
|
21
|
+
// stencil ops
|
22
|
+
ZeroStencilOp,
|
27
23
|
} from "three";
|
28
|
-
import {
|
24
|
+
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
25
|
+
|
29
26
|
import { showBalloonWarning } from "../debug/index.js";
|
30
27
|
import { isUsingInstancing } from "../engine_gameobject.js";
|
31
28
|
import { isLocalNetwork } from "../engine_networking_utils.js";
|
29
|
+
import { type SourceIdentifier } from "../engine_types.js";
|
30
|
+
import { type IComponent as Component, type IRenderer } from "../engine_types.js";
|
31
|
+
import { getParam } from "../engine_utils.js";
|
32
32
|
|
33
33
|
const debug = getParam("debugstencil");
|
34
34
|
|
@@ -1,12 +1,13 @@
|
|
1
|
+
import { AlwaysDepth, BackSide, Camera, DoubleSide, EqualDepth, FrontSide, GLSL3, GreaterDepth, GreaterEqualDepth, type IUniform, LessDepth, LessEqualDepth, LinearSRGBColorSpace, Material, Matrix4, NotEqualDepth, Object3D, RawShaderMaterial, Texture, Vector3, Vector4 } from 'three';
|
1
2
|
import { type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
|
-
|
3
|
-
import { AlwaysDepth, BackSide, Camera, DoubleSide, EqualDepth, FrontSide, GLSL3, GreaterDepth, GreaterEqualDepth, type IUniform, LessDepth, LessEqualDepth, LinearSRGBColorSpace, Material, Matrix4, NotEqualDepth, Object3D, RawShaderMaterial, Texture, Vector3, Vector4 } from 'three';
|
3
|
+
|
4
4
|
import { Context } from '../engine_setup.js';
|
5
|
+
import { FindShaderTechniques, SetUnitySphericalHarmonics,ToUnityMatrixArray, whiteDefaultTexture } from '../engine_shaders.js';
|
6
|
+
import { getWorldPosition } from "../engine_three_utils.js";
|
7
|
+
import { type SourceIdentifier } from "../engine_types.js";
|
8
|
+
import { type ILight } from "../engine_types.js";
|
5
9
|
import { getParam } from "../engine_utils.js";
|
6
10
|
import * as SHADERDATA from "../shaders/shaderData.js"
|
7
|
-
import { type SourceIdentifier } from "../engine_types.js";
|
8
|
-
import { type ILight } from "../engine_types.js";
|
9
|
-
import { getWorldPosition } from "../engine_three_utils.js";
|
10
11
|
|
11
12
|
const debug = getParam("debugcustomshader");
|
12
13
|
|
@@ -88,7 +89,9 @@
|
|
88
89
|
if (debug)
|
89
90
|
console.log(this);
|
90
91
|
|
92
|
+
//@ts-ignore - TODO: how to override and do we even need this?
|
91
93
|
this.type = "NEEDLE_CUSTOM_SHADER";
|
94
|
+
|
92
95
|
if (!this.uniforms[this._objToWorldName])
|
93
96
|
this.uniforms[this._objToWorldName] = { value: [] };
|
94
97
|
if (!this.uniforms[this._worldToObjectName])
|
@@ -1,6 +1,3 @@
|
|
1
|
-
import { makeErrorsVisibleForDevelopment } from "./engine/debug/debug_overlay.js";
|
2
|
-
makeErrorsVisibleForDevelopment();
|
3
|
-
|
4
1
|
import "./engine/engine_element.js";
|
5
2
|
import "./engine/engine_setup.js";
|
6
3
|
export * from "./engine/api.js";
|
@@ -1,9 +1,9 @@
|
|
1
|
+
import { AssetReference, type ProgressCallback } from "../engine/engine_addressables.js";
|
2
|
+
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
3
|
+
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
4
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
1
5
|
import { getParam } from "../engine/engine_utils.js";
|
2
6
|
import { Behaviour } from "../engine-components/Component.js";
|
3
|
-
import { AssetReference, type ProgressCallback } from "../engine/engine_addressables.js";
|
4
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
-
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
6
|
-
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
7
7
|
|
8
8
|
const debug = getParam("debugnestedgltf");
|
9
9
|
|
@@ -1,6 +1,6 @@
|
|
1
|
-
import { serializable } from "../engine/engine_serialization.js";
|
2
1
|
import type { INetworkingWebsocketUrlProvider } from "../engine/engine_networking.js";
|
3
2
|
import { isLocalNetwork } from "../engine/engine_networking_utils.js";
|
3
|
+
import { serializable } from "../engine/engine_serialization.js";
|
4
4
|
import { getParam } from "../engine/engine_utils.js";
|
5
5
|
import { Behaviour } from "./Component.js";
|
6
6
|
|
@@ -1,24 +1,23 @@
|
|
1
|
-
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
|
2
1
|
import { Object3D, Quaternion, Vector3 } from "three";
|
3
|
-
import
|
4
|
-
|
5
|
-
import {
|
2
|
+
import { TransformControlsGizmo } from "three/examples/jsm/controls/TransformControls.js";
|
3
|
+
|
4
|
+
import { addNewComponent, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../../engine/engine_components.js";
|
5
|
+
import { destroy,isActiveSelf, setActive } from "../../engine/engine_gameobject.js";
|
6
6
|
import {
|
7
|
-
|
7
|
+
getTempVector,
|
8
8
|
getWorldPosition,
|
9
|
-
setWorldQuaternion,
|
10
9
|
getWorldQuaternion,
|
10
|
+
getWorldRotation,
|
11
11
|
getWorldScale,
|
12
|
-
|
12
|
+
setWorldPosition,
|
13
|
+
setWorldQuaternion,
|
13
14
|
setWorldRotation,
|
14
|
-
|
15
|
-
getTempVector
|
16
|
-
}
|
15
|
+
setWorldScale}
|
17
16
|
from "../../engine/engine_three_utils.js";
|
17
|
+
import type { Constructor, ConstructorConcrete, IComponent as Component,IComponent } from "../../engine/engine_types.js";
|
18
|
+
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
|
18
19
|
|
19
|
-
import { TransformControlsGizmo } from "three/examples/jsm/controls/TransformControls.js";
|
20
20
|
|
21
|
-
|
22
21
|
// used to decorate cloned object3D objects with the same added components defined above
|
23
22
|
export function apply(object: Object3D) {
|
24
23
|
if (object && object.isObject3D === true) {
|
@@ -1,7 +1,8 @@
|
|
1
|
+
import { Euler, Plane,Quaternion, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
|
+
import * as utils from "./../engine/engine_three_utils.js";
|
1
5
|
import { Behaviour, GameObject } from "./Component.js";
|
2
|
-
import * as utils from "./../engine/engine_three_utils.js";
|
3
|
-
import { Quaternion, Euler, Vector3, Plane } from "three";
|
4
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
6
|
|
6
7
|
export class OffsetConstraint extends Behaviour {
|
7
8
|
|
@@ -1,9 +1,9 @@
|
|
1
1
|
|
2
|
+
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
3
|
+
import { serializable } from "../../engine/engine_serialization.js";
|
4
|
+
import { isiOS,isSafari } from "../../engine/engine_utils.js";
|
5
|
+
import { Behaviour } from "../Component.js";
|
2
6
|
import { type IPointerClickHandler, PointerEventData } from "../ui/index.js";
|
3
|
-
import { Behaviour } from "../Component.js";
|
4
|
-
import { serializable } from "../../engine/engine_serialization.js";
|
5
|
-
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
6
|
-
import { isSafari } from "../../engine/engine_utils.js";
|
7
7
|
import { ObjectRaycaster, Raycaster } from "../ui/Raycaster.js";
|
8
8
|
import { tryGetUIComponent } from "../ui/Utils.js";
|
9
9
|
|
@@ -34,7 +34,6 @@
|
|
34
34
|
|
35
35
|
if (isDevEnvironment()) showBalloonMessage("Open URL: " + this.url)
|
36
36
|
|
37
|
-
|
38
37
|
switch (this.mode) {
|
39
38
|
case OpenURLMode.NewTab:
|
40
39
|
if (isSafari()) {
|
@@ -44,10 +43,12 @@
|
|
44
43
|
globalThis.open(this.url, "_blank");
|
45
44
|
break;
|
46
45
|
case OpenURLMode.SameTab:
|
47
|
-
if
|
46
|
+
// TODO: test if "same tab" now also works on iOS
|
47
|
+
if (isSafari() && isiOS()) {
|
48
48
|
globalThis.open(this.url, "_top");
|
49
49
|
}
|
50
|
-
else
|
50
|
+
else
|
51
|
+
globalThis.open(this.url, "_self");
|
51
52
|
break;
|
52
53
|
case OpenURLMode.NewWindow:
|
53
54
|
if (isSafari()) {
|
@@ -58,19 +59,10 @@
|
|
58
59
|
|
59
60
|
}
|
60
61
|
}
|
61
|
-
|
62
62
|
start(): void {
|
63
63
|
const raycaster = this.gameObject.getComponentInParent(ObjectRaycaster);
|
64
64
|
if (!raycaster) this.gameObject.addNewComponent(ObjectRaycaster);
|
65
65
|
}
|
66
|
-
|
67
|
-
onEnable(): void {
|
68
|
-
if (isSafari()) window.addEventListener("touchend", this._safariNewTabWorkaround);
|
69
|
-
}
|
70
|
-
onDisable(): void {
|
71
|
-
if (isSafari()) window.removeEventListener("touchend", this._safariNewTabWorkaround);
|
72
|
-
}
|
73
|
-
|
74
66
|
onPointerEnter(args) {
|
75
67
|
if (!args.used && this.clickable)
|
76
68
|
this.context.input.setCursorPointer();
|
@@ -83,30 +75,6 @@
|
|
83
75
|
if (this.clickable && !args.used && this.url?.length)
|
84
76
|
this.open();
|
85
77
|
}
|
86
|
-
|
87
|
-
private _safariNewTabWorkaround = () => {
|
88
|
-
if (!this.clickable || !this.url?.length) return;
|
89
|
-
// we only need this workaround for opening a new tab
|
90
|
-
if (this.mode === OpenURLMode.SameTab) return;
|
91
|
-
// When we process the click directly in the browser event we can open a new tab
|
92
|
-
// by emitting a link attribute and calling onClick
|
93
|
-
const raycaster = this.gameObject.getComponentInParent(Raycaster);
|
94
|
-
if (raycaster) {
|
95
|
-
const hits = raycaster.performRaycast();
|
96
|
-
if (!hits) return;
|
97
|
-
for (const hit of hits) {
|
98
|
-
if (hit.object === this.gameObject || tryGetUIComponent(hit.object)?.gameObject === this.gameObject) {
|
99
|
-
this._validateUrl();
|
100
|
-
var a = document.createElement('a') as HTMLAnchorElement;
|
101
|
-
a.setAttribute("target", "_blank");
|
102
|
-
a.setAttribute("href", this.url);
|
103
|
-
a.click();
|
104
|
-
break;
|
105
|
-
}
|
106
|
-
}
|
107
|
-
}
|
108
|
-
}
|
109
|
-
|
110
78
|
private _validateUrl() {
|
111
79
|
if (!this.url) return;
|
112
80
|
if (this.url.startsWith("www.")) {
|
@@ -1,21 +1,21 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
|
1
|
+
import { Box3, Box3Helper, GridHelper, Mesh, Object3D, PerspectiveCamera, Ray,ShadowMaterial, Vector2, Vector3 } from "three";
|
2
|
+
import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
3
|
+
import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js";
|
4
|
+
|
5
|
+
import { setCameraController, useForAutoFit } from "../engine/engine_camera.js";
|
6
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
7
|
+
import { Mathf } from "../engine/engine_math.js";
|
5
8
|
import { RaycastOptions } from "../engine/engine_physics.js";
|
6
9
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
10
|
+
import { getWorldDirection, getWorldPosition, getWorldRotation, setWorldRotation } from "../engine/engine_three_utils.js";
|
11
|
+
import type { ICameraController } from "../engine/engine_types.js";
|
7
12
|
import { getParam, isMobileDevice } from "../engine/engine_utils.js";
|
8
|
-
|
9
|
-
import {
|
10
|
-
import {
|
13
|
+
import { Camera } from "./Camera.js";
|
14
|
+
import { Behaviour, GameObject } from "./Component.js";
|
15
|
+
import { LookAtConstraint } from "./LookAtConstraint.js";
|
16
|
+
import { SyncedTransform } from "./SyncedTransform.js";
|
11
17
|
import { type AfterHandleInputEvent, EventSystem, EventSystemEvents } from "./ui/EventSystem.js";
|
12
|
-
import type { ICameraController } from "../engine/engine_types.js";
|
13
|
-
import { setCameraController, useForAutoFit } from "../engine/engine_camera.js";
|
14
|
-
import { SyncedTransform } from "./SyncedTransform.js";
|
15
18
|
import { tryGetUIComponent } from "./ui/Utils.js";
|
16
|
-
import { GroundProjectedSkybox } from "three/examples/jsm/objects/GroundProjectedSkybox.js";
|
17
|
-
import { Mathf } from "../engine/engine_math.js";
|
18
|
-
import { Gizmos } from "../engine/engine_gizmos.js";
|
19
19
|
|
20
20
|
|
21
21
|
const debug = getParam("debugorbit");
|
@@ -373,7 +373,7 @@
|
|
373
373
|
this._controls.enableZoom = false;
|
374
374
|
}
|
375
375
|
}
|
376
|
-
|
376
|
+
|
377
377
|
// this._controls.zoomToCursor = this.zoomToCursor;
|
378
378
|
if (!this.context.isInXR) {
|
379
379
|
if (!freeCam && this.lookAtConstraint?.locked) this.setLookTargetFromConstraint(0, this.lookAtConstraint01);
|
@@ -542,7 +542,7 @@
|
|
542
542
|
if (obj instanceof Box3Helper) allowExpanding = false;
|
543
543
|
if (obj instanceof GridHelper) allowExpanding = false;
|
544
544
|
// ignore GroundProjectedEnv
|
545
|
-
if (obj instanceof
|
545
|
+
if (obj instanceof GroundedSkybox) allowExpanding = false;
|
546
546
|
// // Ignore shadow catcher geometry
|
547
547
|
if ((obj as Mesh).material instanceof ShadowMaterial) allowExpanding = false;
|
548
548
|
// ONLY fit meshes
|
@@ -1,7 +1,8 @@
|
|
1
|
-
import {
|
1
|
+
import { Color, Vector2 } from "three"
|
2
|
+
|
2
3
|
import { serializable } from "../../engine/engine_serialization.js";
|
3
4
|
import { Behaviour } from "../Component.js";
|
4
|
-
import {
|
5
|
+
import { RGBAColor } from "../js-extensions/index.js";
|
5
6
|
|
6
7
|
export class Outline extends Behaviour {
|
7
8
|
|
@@ -1,32 +1,31 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
1
|
import * as THREE from "three";
|
3
|
-
import {
|
4
|
-
import {
|
2
|
+
import { AxesHelper, BackSide, BufferGeometry, Color, FrontSide, Material, Matrix4, Mesh, MeshBasicMaterial, MeshStandardMaterial, Object3D, OneMinusDstAlphaFactor, PlaneGeometry, Quaternion, Sprite, SpriteMaterial, Vector3, Vector4 } from "three";
|
3
|
+
import type { BatchedRenderer, Behavior, BehaviorPlugin, BillBoardSettings, BurstParameters, ColorGenerator, EmissionState, EmitSubParticleSystem, EmitterShape, FunctionColorGenerator, FunctionJSON, FunctionValueGenerator, IntervalValue, MeshSettings, Particle, ParticleEmitter, ParticleSystemParameters, PointEmitter, RecordState, RotationGenerator, SizeOverLife, TrailSettings, ValueGenerator, VFXBatchSettings } from "three.quarks";
|
4
|
+
import { BatchedParticleRenderer, ConstantColor, ConstantValue, ParticleSystem as _ParticleSystem, RenderMode, TrailBatch, TrailParticle } from "three.quarks";
|
5
5
|
|
6
|
+
import { isDevEnvironment, showBalloonWarning } from "../engine/debug/index.js";
|
7
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
8
|
+
import { Mathf } from "../engine/engine_math.js";
|
6
9
|
// https://github.dev/creativelifeform/three-nebula
|
7
10
|
// import System, { Emitter, Position, Life, SpriteRenderer, Particle, Body, MeshRenderer, } from 'three-nebula.js';
|
8
|
-
|
9
11
|
import { serializable } from "../engine/engine_serialization.js";
|
10
|
-
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
11
|
-
import { AxesHelper, BufferGeometry, Color, Material, Matrix4, Mesh, MeshStandardMaterial, Object3D, OneMinusDstAlphaFactor, PlaneGeometry, Quaternion, Sprite, SpriteMaterial, Vector3, Vector4 } from "three";
|
12
|
-
import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldScale } from "../engine/engine_three_utils.js";
|
13
12
|
import { assign } from "../engine/engine_serialization_core.js";
|
14
|
-
import {
|
15
|
-
import type { BatchedRenderer, Behavior, BehaviorPlugin, BillBoardSettings, BurstParameters, ColorGenerator, EmissionState, EmitSubParticleSystem, EmitterShape, FunctionColorGenerator, FunctionJSON, FunctionValueGenerator, IntervalValue, MeshSettings, Particle, ParticleEmitter, ParticleSystemParameters, PointEmitter, RecordState, RotationGenerator, SizeOverLife, TrailSettings, VFXBatchSettings, ValueGenerator } from "three.quarks";
|
13
|
+
import { Context } from "../engine/engine_setup.js";
|
16
14
|
import { createFlatTexture } from "../engine/engine_shaders.js";
|
17
|
-
import {
|
18
|
-
import {
|
15
|
+
import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldScale } from "../engine/engine_three_utils.js";
|
16
|
+
import { getParam } from "../engine/engine_utils.js";
|
17
|
+
import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
|
18
|
+
import { Behaviour, GameObject } from "./Component.js";
|
19
|
+
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
20
|
+
import { ColorBySpeedModule, ColorOverLifetimeModule, EmissionModule, InheritVelocityModule, type IParticleSystem, LimitVelocityOverLifetimeModule, MainModule, MinMaxCurve, MinMaxGradient, NoiseModule, ParticleBurst, ParticleSystemRenderMode, ParticleSystemScalingMode, ParticleSystemShapeType, ParticleSystemSimulationSpace, RotationBySpeedModule, RotationOverLifetimeModule, ShapeModule, SizeBySpeedModule, SizeOverLifetimeModule, TextureSheetAnimationModule, TrailModule, VelocityOverLifetimeModule } from "./ParticleSystemModules.js"
|
19
21
|
import { ParticleSubEmitter } from "./ParticleSystemSubEmitter.js";
|
20
|
-
import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
|
21
|
-
import { Gizmos } from "../engine/engine_gizmos.js";
|
22
|
-
import { isDevEnvironment, showBalloonWarning } from "../engine/debug/index.js";
|
23
22
|
|
24
23
|
const debug = getParam("debugparticles");
|
25
24
|
const suppressProgressiveLoading = getParam("noprogressive");
|
26
25
|
const debugProgressiveLoading = getParam("debugprogressive");
|
27
26
|
|
28
27
|
|
29
|
-
export type {
|
28
|
+
export type { Particle as QParticle, Behavior as QParticleBehaviour } from "three.quarks"
|
30
29
|
|
31
30
|
|
32
31
|
|
@@ -81,23 +80,22 @@
|
|
81
80
|
return res;
|
82
81
|
}
|
83
82
|
|
84
|
-
private static _havePatchedQuarkShaders = false;
|
85
|
-
|
86
83
|
getMaterial(trailEnabled: boolean = false) {
|
84
|
+
let material = (trailEnabled === true && this.trailMaterial) ? this.trailMaterial : this.particleMaterial;
|
87
85
|
|
88
|
-
if (
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
86
|
+
if (material) {
|
87
|
+
if (trailEnabled) {
|
88
|
+
// the particle material for trails must be DoubleSide or BackSide (since otherwise the trail is invisible)
|
89
|
+
if (material.side === FrontSide) {
|
90
|
+
// don't modify the assigned material
|
91
|
+
material = material.clone();
|
92
|
+
material.side = BackSide;
|
93
|
+
if(trailEnabled) this.trailMaterial = material;
|
94
|
+
else this.particleMaterial = material;
|
95
|
+
}
|
96
96
|
}
|
97
97
|
}
|
98
98
|
|
99
|
-
const material = (trailEnabled === true && this.trailMaterial) ? this.trailMaterial : this.particleMaterial;
|
100
|
-
|
101
99
|
// progressive load on start
|
102
100
|
// TODO: figure out how to do this before particle system rendering so we only load textures for visible materials
|
103
101
|
if (material && !suppressProgressiveLoading && material["_didRequestTextureLOD"] === undefined) {
|
@@ -398,7 +396,7 @@
|
|
398
396
|
let size = particle.size;
|
399
397
|
if (size <= 0 && !this.system.trails.sizeAffectsWidth) {
|
400
398
|
// Not sure where we get to 100* from, tested in SOC trong com
|
401
|
-
size =
|
399
|
+
size = 20 * this.system.trails.widthOverTrail.evaluate(.5, trailParticle[$trailWidthRandom]);
|
402
400
|
}
|
403
401
|
state.size = this.system.trails.getWidth(size, age01, pos01, trailParticle[$trailWidthRandom]);
|
404
402
|
state.color.copy(particle.color);
|
@@ -430,9 +428,8 @@
|
|
430
428
|
initialize(particle: Particle): void {
|
431
429
|
const simulationSpeed = this.system.main.simulationSpeed;
|
432
430
|
|
433
|
-
|
434
|
-
particle.
|
435
|
-
particle.velocity.copy(this.system.shape.getDirection(particle.position)).multiplyScalar(particle.startSpeed);
|
431
|
+
particle.startSpeed = this.system.main.startSpeed.evaluate(Math.random(), Math.random());
|
432
|
+
particle.velocity.copy(this.system.shape.getDirection(particle, particle.position)).multiplyScalar(particle.startSpeed);
|
436
433
|
if (this.system.inheritVelocity?.enabled) {
|
437
434
|
this.system.inheritVelocity.applyInitial(particle.velocity);
|
438
435
|
}
|
@@ -616,8 +613,7 @@
|
|
616
613
|
if (mat && mat["map"]) {
|
617
614
|
const original = mat["map"]! as THREE.Texture;
|
618
615
|
// cache the last original one so we're not creating tons of clones
|
619
|
-
if (this.clonedTexture.original !== original || !this.clonedTexture.clone)
|
620
|
-
{
|
616
|
+
if (this.clonedTexture.original !== original || !this.clonedTexture.clone) {
|
621
617
|
const tex = original.clone();
|
622
618
|
tex.premultiplyAlpha = false;
|
623
619
|
tex.colorSpace = THREE.LinearSRGBColorSpace;
|
@@ -756,7 +752,7 @@
|
|
756
752
|
readonly limitVelocityOverLifetime!: LimitVelocityOverLifetimeModule;
|
757
753
|
|
758
754
|
@serializable(InheritVelocityModule)
|
759
|
-
|
755
|
+
inheritVelocity!: InheritVelocityModule;
|
760
756
|
|
761
757
|
@serializable(ColorBySpeedModule)
|
762
758
|
readonly colorBySpeed!: ColorBySpeedModule;
|
@@ -935,6 +931,8 @@
|
|
935
931
|
}
|
936
932
|
|
937
933
|
awake(): void {
|
934
|
+
this._worldPositionFrame = -1;
|
935
|
+
|
938
936
|
this._renderer = this.gameObject.getComponent(ParticleSystemRenderer) as ParticleSystemRenderer;
|
939
937
|
|
940
938
|
if (!this.main) {
|
@@ -968,6 +966,12 @@
|
|
968
966
|
const emitter = this._particleSystem.emitter;
|
969
967
|
this.context.scene.add(emitter);
|
970
968
|
|
969
|
+
if (this.inheritVelocity.system && this.inheritVelocity.system !== this) {
|
970
|
+
this.inheritVelocity = this.inheritVelocity.clone();
|
971
|
+
}
|
972
|
+
this.inheritVelocity.awake(this);
|
973
|
+
|
974
|
+
|
971
975
|
if (debug) {
|
972
976
|
console.log(this);
|
973
977
|
this.gameObject.add(new AxesHelper(1))
|
@@ -1110,6 +1114,7 @@
|
|
1110
1114
|
this._interface.update();
|
1111
1115
|
this.shape.update(this, this.context, this.main.simulationSpace, this.gameObject);
|
1112
1116
|
this.noise.update(this.context);
|
1117
|
+
|
1113
1118
|
this.inheritVelocity?.update(this.context);
|
1114
1119
|
this.velocityOverLifetime.update(this);
|
1115
1120
|
}
|
@@ -1,14 +1,18 @@
|
|
1
|
-
import {
|
1
|
+
import { createNoise4D, type NoiseFunction4D } from 'simplex-noise';
|
2
|
+
import { BufferGeometry, Euler, Matrix4, Mesh, Object3D, Quaternion, Triangle, Vector2, Vector3, Vector4 } from "three";
|
3
|
+
import type { EmitterShape, Particle, ShapeJSON } from "three.quarks";
|
4
|
+
|
5
|
+
import { isDevEnvironment } from '../engine/debug/index.js';
|
6
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
2
7
|
import { Mathf } from "../engine/engine_math.js";
|
3
8
|
import { serializable } from "../engine/engine_serialization.js";
|
4
|
-
import {
|
5
|
-
import {
|
9
|
+
import { Context } from "../engine/engine_setup.js";
|
10
|
+
import { getTempVector, getWorldQuaternion } from '../engine/engine_three_utils.js';
|
6
11
|
import type { Vec2, Vec3 } from "../engine/engine_types.js";
|
7
|
-
import { Context } from "../engine/engine_setup.js";
|
8
|
-
import type { EmitterShape, Particle, ShapeJSON } from "three.quarks";
|
9
|
-
import { createNoise4D, type NoiseFunction4D } from 'simplex-noise';
|
10
|
-
import { Gizmos } from "../engine/engine_gizmos.js";
|
11
12
|
import { getParam } from "../engine/engine_utils.js";
|
13
|
+
import { AnimationCurve } from "./AnimationCurve.js";
|
14
|
+
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
15
|
+
import { MeshRenderer } from './Renderer.js';
|
12
16
|
|
13
17
|
const debug = getParam("debugparticles");
|
14
18
|
|
@@ -179,6 +183,19 @@
|
|
179
183
|
@serializable()
|
180
184
|
curveMultiplier?: number;
|
181
185
|
|
186
|
+
clone() {
|
187
|
+
const clone = new MinMaxCurve();
|
188
|
+
clone.mode = this.mode;
|
189
|
+
clone.constant = this.constant;
|
190
|
+
clone.constantMin = this.constantMin;
|
191
|
+
clone.constantMax = this.constantMax;
|
192
|
+
clone.curve = this.curve?.clone();
|
193
|
+
clone.curveMin = this.curveMin?.clone();
|
194
|
+
clone.curveMax = this.curveMax?.clone();
|
195
|
+
clone.curveMultiplier = this.curveMultiplier;
|
196
|
+
return clone;
|
197
|
+
}
|
198
|
+
|
182
199
|
evaluate(t01: number, lerpFactor?: number): number {
|
183
200
|
const t = lerpFactor === undefined ? Math.random() : lerpFactor;
|
184
201
|
switch (this.mode) {
|
@@ -486,6 +503,13 @@
|
|
486
503
|
}
|
487
504
|
}
|
488
505
|
|
506
|
+
|
507
|
+
export enum ParticleSystemMeshShapeType {
|
508
|
+
Vertex = 0,
|
509
|
+
Edge = 1,
|
510
|
+
Triangle = 2,
|
511
|
+
}
|
512
|
+
|
489
513
|
export class ShapeModule implements EmitterShape {
|
490
514
|
|
491
515
|
// Emittershape start
|
@@ -493,7 +517,7 @@
|
|
493
517
|
return ParticleSystemShapeType[this.shapeType];
|
494
518
|
}
|
495
519
|
initialize(particle: Particle): void {
|
496
|
-
this.
|
520
|
+
this.onInitialize(particle);
|
497
521
|
particle.position.copy(this._vector);
|
498
522
|
}
|
499
523
|
toJSON(): ShapeJSON {
|
@@ -543,6 +567,30 @@
|
|
543
567
|
@serializable()
|
544
568
|
randomPositionAmount!: number;
|
545
569
|
|
570
|
+
/** Controls if particles should spawn off vertices, faces or edges. `shapeType` must be set to `MeshRenderer` */
|
571
|
+
@serializable()
|
572
|
+
meshShapeType?: ParticleSystemMeshShapeType;
|
573
|
+
/** When assigned and `shapeType` is set to `MeshRenderer` particles will spawn using a mesh in the scene.
|
574
|
+
* Use the `meshShapeType` to choose if particles should be spawned from vertices, faces or edges
|
575
|
+
* To re-assign use the `setMesh` function to cache the mesh and geometry
|
576
|
+
* */
|
577
|
+
@serializable(MeshRenderer)
|
578
|
+
meshRenderer?: MeshRenderer;
|
579
|
+
|
580
|
+
private _meshObj?: Mesh;
|
581
|
+
private _meshGeometry?: BufferGeometry;
|
582
|
+
setMesh(mesh: MeshRenderer) {
|
583
|
+
this.meshRenderer = mesh;
|
584
|
+
if (mesh) {
|
585
|
+
this._meshObj = mesh.sharedMeshes[Math.floor(Math.random() * mesh.sharedMeshes.length)];
|
586
|
+
this._meshGeometry = this._meshObj.geometry;
|
587
|
+
}
|
588
|
+
else {
|
589
|
+
this._meshObj = undefined;
|
590
|
+
this._meshGeometry = undefined;
|
591
|
+
}
|
592
|
+
}
|
593
|
+
|
546
594
|
private system!: IParticleSystem;
|
547
595
|
private _space?: ParticleSystemSimulationSpace;
|
548
596
|
private readonly _worldSpaceMatrix: Matrix4 = new Matrix4();
|
@@ -593,12 +641,14 @@
|
|
593
641
|
/** initializer implementation */
|
594
642
|
private _vector: Vector3 = new Vector3(0, 0, 0);
|
595
643
|
private _temp: Vector3 = new Vector3(0, 0, 0);
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
}
|
600
|
-
getPosition(): void {
|
644
|
+
private _triangle: Triangle = new Triangle();
|
645
|
+
|
646
|
+
onInitialize(particle: Particle): void {
|
601
647
|
this._vector.set(0, 0, 0);
|
648
|
+
// remove mesh from particle in case it got destroyed (we dont want to keep a reference to a destroyed mesh in the particle system)
|
649
|
+
particle["mesh"] = undefined;
|
650
|
+
particle["mesh_geometry"] = undefined;
|
651
|
+
|
602
652
|
const pos = this._temp.copy(this.position);
|
603
653
|
const isWorldSpace = this._space === ParticleSystemSimulationSpace.World;
|
604
654
|
if (isWorldSpace) {
|
@@ -624,8 +674,64 @@
|
|
624
674
|
case ParticleSystemShapeType.Circle:
|
625
675
|
this.randomCirclePoint(this.position, radius, this.radiusThickness, this.arc, this._vector);
|
626
676
|
break;
|
677
|
+
case ParticleSystemShapeType.MeshRenderer:
|
678
|
+
const renderer = this.meshRenderer;
|
679
|
+
if (renderer?.destroyed == false) this.setMesh(renderer);
|
680
|
+
const mesh = particle["mesh"] = this._meshObj;
|
681
|
+
const geometry = particle["mesh_geometry"] = this._meshGeometry;
|
682
|
+
if (mesh && geometry) {
|
683
|
+
switch (this.meshShapeType) {
|
684
|
+
case ParticleSystemMeshShapeType.Vertex:
|
685
|
+
{
|
686
|
+
const vertices = geometry.getAttribute("position");
|
687
|
+
const index = Math.floor(Math.random() * vertices.count);
|
688
|
+
this._vector.fromBufferAttribute(vertices, index);
|
689
|
+
this._vector.applyMatrix4(mesh.matrixWorld);
|
690
|
+
particle["mesh_normal"] = index;
|
691
|
+
}
|
692
|
+
break;
|
693
|
+
case ParticleSystemMeshShapeType.Edge:
|
694
|
+
break;
|
695
|
+
case ParticleSystemMeshShapeType.Triangle:
|
696
|
+
{
|
697
|
+
const faces = geometry.index;
|
698
|
+
if (faces) {
|
699
|
+
let u = Math.random();
|
700
|
+
let v = Math.random();
|
701
|
+
if (u + v > 1) {
|
702
|
+
u = 1 - u;
|
703
|
+
v = 1 - v;
|
704
|
+
}
|
705
|
+
const faceIndex = Math.floor(Math.random() * (faces.count / 3));
|
706
|
+
let i0 = faceIndex * 3;
|
707
|
+
let i1 = faceIndex * 3 + 1;
|
708
|
+
let i2 = faceIndex * 3 + 2;
|
709
|
+
i0 = faces.getX(i0);
|
710
|
+
i1 = faces.getX(i1);
|
711
|
+
i2 = faces.getX(i2);
|
712
|
+
const positionAttribute = geometry.getAttribute("position");
|
713
|
+
this._triangle.a.fromBufferAttribute(positionAttribute, i0);
|
714
|
+
this._triangle.b.fromBufferAttribute(positionAttribute, i1);
|
715
|
+
this._triangle.c.fromBufferAttribute(positionAttribute, i2);
|
716
|
+
this._vector
|
717
|
+
.set(0, 0, 0)
|
718
|
+
.addScaledVector(this._triangle.a, u)
|
719
|
+
.addScaledVector(this._triangle.b, v)
|
720
|
+
.addScaledVector(this._triangle.c, 1 - (u + v));
|
721
|
+
this._vector.applyMatrix4(mesh.matrixWorld);
|
722
|
+
particle["mesh_normal"] = faceIndex;
|
723
|
+
}
|
724
|
+
}
|
725
|
+
break;
|
726
|
+
}
|
727
|
+
}
|
728
|
+
break;
|
627
729
|
default:
|
628
730
|
this._vector.set(0, 0, 0);
|
731
|
+
if (isDevEnvironment() && !globalThis["__particlesystem_shapetype_unsupported"]) {
|
732
|
+
console.warn("ParticleSystem ShapeType is not supported:", ParticleSystemShapeType[this.shapeType]);
|
733
|
+
globalThis["__particlesystem_shapetype_unsupported"] = true;
|
734
|
+
}
|
629
735
|
break;
|
630
736
|
// case ParticleSystemShapeType.Hemisphere:
|
631
737
|
// randomSpherePoint(this.position.x, this.position.y, this.position.z, this.radius, this.radiusThickness, 180, this._vector);
|
@@ -651,7 +757,7 @@
|
|
651
757
|
|
652
758
|
private _dir: Vector3 = new Vector3();
|
653
759
|
|
654
|
-
getDirection(pos: Vec3): Vector3 {
|
760
|
+
getDirection(particle: Particle, pos: Vec3): Vector3 {
|
655
761
|
if (!this.enabled) {
|
656
762
|
this._dir.set(0, 0, 1);
|
657
763
|
return this._dir;
|
@@ -676,6 +782,47 @@
|
|
676
782
|
else
|
677
783
|
this._dir.sub(this.position)
|
678
784
|
break;
|
785
|
+
case ParticleSystemShapeType.MeshRenderer:
|
786
|
+
const mesh = particle["mesh"];
|
787
|
+
const geometry = particle["mesh_geometry"];
|
788
|
+
if (mesh && geometry) {
|
789
|
+
switch (this.meshShapeType) {
|
790
|
+
case ParticleSystemMeshShapeType.Vertex:
|
791
|
+
{
|
792
|
+
const normal = geometry.getAttribute("normal");
|
793
|
+
const index = particle["mesh_normal"];
|
794
|
+
this._dir.fromBufferAttribute(normal, index);
|
795
|
+
}
|
796
|
+
break;
|
797
|
+
case ParticleSystemMeshShapeType.Edge:
|
798
|
+
break;
|
799
|
+
case ParticleSystemMeshShapeType.Triangle:
|
800
|
+
{
|
801
|
+
const faces = geometry.index;
|
802
|
+
if (faces) {
|
803
|
+
const index = particle["mesh_normal"];
|
804
|
+
const i0 = faces.getX(index * 3);
|
805
|
+
const i1 = faces.getX(index * 3 + 1);
|
806
|
+
const i2 = faces.getX(index * 3 + 2);
|
807
|
+
const positionAttribute = geometry.getAttribute("position");
|
808
|
+
const a = getTempVector();
|
809
|
+
const b = getTempVector();
|
810
|
+
const c = getTempVector();
|
811
|
+
a.fromBufferAttribute(positionAttribute, i0);
|
812
|
+
b.fromBufferAttribute(positionAttribute, i1);
|
813
|
+
c.fromBufferAttribute(positionAttribute, i2);
|
814
|
+
a.sub(b);
|
815
|
+
c.sub(b);
|
816
|
+
a.cross(c);
|
817
|
+
this._dir.copy(a).multiplyScalar(-1);
|
818
|
+
const rot = getWorldQuaternion(mesh);
|
819
|
+
this._dir.applyQuaternion(rot)
|
820
|
+
}
|
821
|
+
}
|
822
|
+
break;
|
823
|
+
}
|
824
|
+
}
|
825
|
+
break;
|
679
826
|
default:
|
680
827
|
this._dir.set(0, 0, 1);
|
681
828
|
break;
|
@@ -741,7 +888,7 @@
|
|
741
888
|
vec.z = z;
|
742
889
|
}
|
743
890
|
|
744
|
-
private randomCirclePoint(pos:Vec3, radius:number, thickness:number, arg:number, vec:Vec3){
|
891
|
+
private randomCirclePoint(pos: Vec3, radius: number, thickness: number, arg: number, vec: Vec3) {
|
745
892
|
const u = Math.random();
|
746
893
|
const theta = 2 * Math.PI * u * (arg / 360);
|
747
894
|
const r = Mathf.lerp(1, 1 - (Math.pow(1 - Math.random(), Math.PI)), thickness) * (radius);
|
@@ -972,7 +1119,7 @@
|
|
972
1119
|
@serializable()
|
973
1120
|
worldSpace: boolean = false;
|
974
1121
|
|
975
|
-
getWidth(size: number, _life01: number, pos01: number, t
|
1122
|
+
getWidth(size: number, _life01: number, pos01: number, t: number) {
|
976
1123
|
const res = this.widthOverTrail.evaluate(pos01, t);
|
977
1124
|
size *= res;
|
978
1125
|
return size;
|
@@ -1384,22 +1531,54 @@
|
|
1384
1531
|
@serializable()
|
1385
1532
|
mode!: ParticleSystemInheritVelocityMode;
|
1386
1533
|
|
1534
|
+
clone() {
|
1535
|
+
const ni = new InheritVelocityModule();
|
1536
|
+
ni.enabled = this.enabled;
|
1537
|
+
ni.curve = this.curve?.clone();
|
1538
|
+
ni.curveMultiplier = this.curveMultiplier;
|
1539
|
+
ni.mode = this.mode;
|
1540
|
+
return ni;
|
1541
|
+
}
|
1542
|
+
|
1387
1543
|
system!: IParticleSystem;
|
1388
|
-
private _lastWorldPosition!: Vector3;
|
1389
|
-
private _velocity: Vector3 = new Vector3();
|
1390
|
-
private _temp: Vector3 = new Vector3();
|
1391
1544
|
|
1545
|
+
private get _lastWorldPosition() {
|
1546
|
+
if (!this.system['_iv_lastWorldPosition']) {
|
1547
|
+
this.system['_iv_lastWorldPosition'] = new Vector3();
|
1548
|
+
}
|
1549
|
+
return this.system['_iv_lastWorldPosition'];
|
1550
|
+
}
|
1551
|
+
private get _velocity() {
|
1552
|
+
if (!this.system['_iv_velocity']) {
|
1553
|
+
this.system['_iv_velocity'] = new Vector3();
|
1554
|
+
}
|
1555
|
+
return this.system['_iv_velocity'];
|
1556
|
+
}
|
1557
|
+
|
1558
|
+
private readonly _temp: Vector3 = new Vector3();
|
1559
|
+
private _firstUpdate: boolean = true;
|
1560
|
+
|
1561
|
+
awake(system: IParticleSystem) {
|
1562
|
+
this.system = system;
|
1563
|
+
this.reset();
|
1564
|
+
}
|
1565
|
+
|
1566
|
+
reset() {
|
1567
|
+
this._firstUpdate = true;
|
1568
|
+
}
|
1569
|
+
|
1392
1570
|
update(_context: Context) {
|
1393
1571
|
if (!this.enabled) return;
|
1394
1572
|
if (this.system.worldspace === false) return;
|
1395
|
-
if (this.
|
1573
|
+
if (this._firstUpdate) {
|
1574
|
+
this._firstUpdate = false;
|
1575
|
+
this._velocity.set(0, 0, 0);
|
1576
|
+
this._lastWorldPosition.copy(this.system.worldPos);
|
1577
|
+
}
|
1578
|
+
else if (this._lastWorldPosition) {
|
1396
1579
|
this._velocity.copy(this.system.worldPos).sub(this._lastWorldPosition).multiplyScalar(1 / this.system.deltaTime);
|
1397
1580
|
this._lastWorldPosition.copy(this.system.worldPos);
|
1398
1581
|
}
|
1399
|
-
else {
|
1400
|
-
this._velocity.set(0, 0, 0);
|
1401
|
-
this._lastWorldPosition = this.system.worldPos.clone();
|
1402
|
-
}
|
1403
1582
|
}
|
1404
1583
|
|
1405
1584
|
// TODO: make work for subsystems
|
@@ -1413,8 +1592,10 @@
|
|
1413
1592
|
}
|
1414
1593
|
}
|
1415
1594
|
|
1595
|
+
private _frames = 0;
|
1416
1596
|
applyCurrent(vel: Vector3, t01: number, lerpFactor: number) {
|
1417
1597
|
if (!this.enabled) return;
|
1598
|
+
if (!this.system) return;
|
1418
1599
|
if (this.system.worldspace === false) return;
|
1419
1600
|
if (this.mode === ParticleSystemInheritVelocityMode.Current) {
|
1420
1601
|
const factor = this.curve.evaluate(t01, lerpFactor);
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
3
|
-
|
1
|
+
import { Matrix4,Quaternion, Vector3 } from "three";
|
2
|
+
import { type Behavior, type EmissionState, type Particle, type ParticleSystem } from "three.quarks";
|
3
|
+
|
4
4
|
import { CircularBuffer } from "../engine/engine_utils.js";
|
5
5
|
import { $particleLife, SubEmitterType } from "./ParticleSystem.js";
|
6
|
+
import type { IParticleSystem } from "./ParticleSystemModules.js";
|
6
7
|
|
7
8
|
const VECTOR_ONE = new Vector3(1, 1, 1);
|
8
9
|
const VECTOR_Z = new Vector3(0, 0, 1);
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import {
|
1
|
+
import { PixelationEffect as PixelationEffectPP } from "postprocessing";
|
2
|
+
|
3
|
+
import { serializable } from "../../../engine/engine_serialization.js";
|
2
4
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
3
|
-
import { PixelationEffect as PixelationEffectPP } from "postprocessing";
|
4
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
5
|
-
import {
|
6
|
+
import { registerCustomEffectType } from "../VolumeProfile.js";
|
6
7
|
|
7
8
|
export class PixelationEffect extends PostProcessingEffect {
|
8
9
|
get typeName(): string {
|
@@ -1,16 +1,17 @@
|
|
1
|
+
import * as THREE from 'three';
|
2
|
+
import { Object3D, Quaternion, Vector3 } from 'three';
|
3
|
+
|
4
|
+
import { FrameEvent } from '../../engine/engine_context.js';
|
5
|
+
import { isLocalNetwork } from '../../engine/engine_networking_utils.js';
|
6
|
+
import type { GuidsMap } from '../../engine/engine_types.js';
|
7
|
+
import { deepClone, delay, getParam } from '../../engine/engine_utils.js';
|
1
8
|
import { Animator } from '../Animator.js';
|
2
|
-
import { Behaviour, GameObject } from '../Component.js';
|
3
|
-
import * as THREE from 'three';
|
4
9
|
import { AudioListener } from '../AudioListener.js';
|
5
10
|
import { AudioSource } from '../AudioSource.js';
|
11
|
+
import { Behaviour, GameObject } from '../Component.js';
|
6
12
|
import { SignalReceiver } from './SignalAsset.js';
|
7
13
|
import * as Models from "./TimelineModels.js";
|
8
14
|
import * as Tracks from "./TimelineTracks.js";
|
9
|
-
import { deepClone, delay, getParam } from '../../engine/engine_utils.js';
|
10
|
-
import type { GuidsMap } from '../../engine/engine_types.js';
|
11
|
-
import { Object3D, Quaternion, Vector3 } from 'three';
|
12
|
-
import { isLocalNetwork } from '../../engine/engine_networking_utils.js';
|
13
|
-
import { FrameEvent } from '../../engine/engine_context.js';
|
14
15
|
|
15
16
|
const debug = getParam("debugtimeline");
|
16
17
|
|
@@ -164,9 +165,9 @@
|
|
164
165
|
if (!this.isValid()) return;
|
165
166
|
const pauseChanged = this._isPaused == true;
|
166
167
|
this._isPaused = false;
|
167
|
-
if (pauseChanged) this.invokePauseChangedMethodsOnTracks();
|
168
168
|
if (this._isPlaying) return;
|
169
169
|
this._isPlaying = true;
|
170
|
+
if (pauseChanged) this.invokePauseChangedMethodsOnTracks();
|
170
171
|
if (this.waitForAudio) {
|
171
172
|
// Make sure audio tracks have loaded at the current time
|
172
173
|
const promises: Array<Promise<any>> = [];
|
@@ -518,7 +519,7 @@
|
|
518
519
|
const clipModel = track.clips[i];
|
519
520
|
const animModel = clipModel.asset as Models.AnimationClipModel;
|
520
521
|
if (!animModel) {
|
521
|
-
console.error(
|
522
|
+
console.error(`Timeline ${this.name}: clip #${i} on track \"${track.name}\" has no animation data`);
|
522
523
|
continue;
|
523
524
|
}
|
524
525
|
// console.log(clipModel, track);
|
@@ -1,40 +1,45 @@
|
|
1
|
+
import * as THREE from "three";
|
2
|
+
|
3
|
+
import { WaitForSeconds } from "../engine/engine_coroutine.js";
|
1
4
|
import { RoomEvents } from "../engine/engine_networking.js";
|
5
|
+
import { PlayerState } from "../engine-components-experimental/networking/PlayerSync.js";
|
2
6
|
import { Behaviour, GameObject } from "./Component.js";
|
3
|
-
import * as THREE from "three";
|
4
7
|
import { AvatarMarker } from "./webxr/WebXRAvatar.js";
|
5
|
-
import { WaitForSeconds } from "../engine/engine_coroutine.js";
|
6
8
|
|
7
9
|
|
8
10
|
export class PlayerColor extends Behaviour {
|
9
11
|
|
10
|
-
awake(): void {
|
11
|
-
// console.log("AWAKE", this.name);
|
12
|
-
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.tryAssignColor.bind(this));
|
13
|
-
}
|
14
|
-
|
15
12
|
private _didAssignPlayerColor: boolean = false;
|
16
13
|
|
17
14
|
onEnable(): void {
|
18
|
-
|
15
|
+
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.tryAssignColor);
|
19
16
|
if (!this._didAssignPlayerColor)
|
20
17
|
this.startCoroutine(this.waitForConnection());
|
21
18
|
}
|
19
|
+
onDisable(): void {
|
20
|
+
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.tryAssignColor);
|
21
|
+
}
|
22
22
|
|
23
23
|
private *waitForConnection() {
|
24
|
-
while (!this.destroyed && this.
|
24
|
+
while (!this.destroyed && this.activeAndEnabled) {
|
25
25
|
yield WaitForSeconds(.2);
|
26
26
|
if (this.tryAssignColor()) break;
|
27
27
|
}
|
28
|
-
// console.log("STOP WAITING", this.name, this.destroyed);
|
29
28
|
}
|
30
29
|
|
31
|
-
private tryAssignColor()
|
32
|
-
const marker = GameObject.getComponentInParent(this.gameObject,
|
33
|
-
if (marker && marker.
|
30
|
+
private tryAssignColor = () => {
|
31
|
+
const marker = GameObject.getComponentInParent(this.gameObject, PlayerState);
|
32
|
+
if (marker && marker.owner) {
|
34
33
|
this._didAssignPlayerColor = true;
|
35
|
-
this.assignUserColor(marker.
|
34
|
+
this.assignUserColor(marker.owner);
|
36
35
|
return true;
|
37
36
|
}
|
37
|
+
const avatar = GameObject.getComponentInParent(this.gameObject, AvatarMarker);
|
38
|
+
if (avatar?.connectionId) {
|
39
|
+
this._didAssignPlayerColor = true;
|
40
|
+
this.assignUserColor(avatar.connectionId);
|
41
|
+
return true;
|
42
|
+
}
|
38
43
|
return false;
|
39
44
|
}
|
40
45
|
|
@@ -1,39 +1,69 @@
|
|
1
|
-
import {
|
1
|
+
import { Object3D } from "three";
|
2
|
+
|
2
3
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
3
|
-
import {
|
4
|
+
import { RoomEvents } from "../../engine/engine_networking.js";
|
4
5
|
import { syncField } from "../../engine/engine_networking_auto.js"
|
5
|
-
import { RoomEvents } from "../../engine/engine_networking.js";
|
6
6
|
import { syncDestroy } from "../../engine/engine_networking_instantiate.js";
|
7
|
-
import {
|
8
|
-
|
9
|
-
import {
|
7
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
8
|
+
import { IGameObject } from "../../engine/engine_types.js";
|
9
|
+
import { delay, getParam } from "../../engine/engine_utils.js";
|
10
|
+
import { Behaviour, Component, GameObject } from "../../engine-components/Component.js";
|
10
11
|
import { EventList } from "../../engine-components/EventList.js";
|
11
12
|
|
12
13
|
|
13
14
|
const debug = getParam("debugplayersync");
|
14
15
|
|
15
16
|
export class PlayerSync extends Behaviour {
|
17
|
+
|
18
|
+
/** when enabled PlayerSync will automatically load and instantiate the assigned asset when joining a networked room */
|
19
|
+
@serializable()
|
20
|
+
autoSync: boolean = true;
|
21
|
+
|
22
|
+
/** This asset will be loaded and instantiated when PlayerSync becomes active and joins a networked room */
|
16
23
|
@serializable(AssetReference)
|
17
24
|
asset?: AssetReference;
|
18
25
|
|
26
|
+
/** Event called when */
|
19
27
|
@serializable(EventList)
|
20
28
|
onPlayerSpawned?: EventList;
|
21
29
|
|
30
|
+
|
31
|
+
private _localInstance?: Promise<IGameObject>;
|
32
|
+
|
22
33
|
awake(): void {
|
23
34
|
this.watchTabVisible();
|
35
|
+
if (!this.onPlayerSpawned) this.onPlayerSpawned = new EventList();
|
24
36
|
}
|
25
37
|
|
26
38
|
onEnable(): void {
|
27
39
|
this.context.connection.beginListen(RoomEvents.RoomStateSent, this.onJoinedRoom);
|
40
|
+
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
|
41
|
+
if (this.context.connection.isInRoom) {
|
42
|
+
this.onJoinedRoom();
|
43
|
+
}
|
28
44
|
}
|
29
45
|
onDisable(): void {
|
30
|
-
this.context.connection.stopListen(RoomEvents.RoomStateSent, this.onJoinedRoom)
|
46
|
+
this.context.connection.stopListen(RoomEvents.RoomStateSent, this.onJoinedRoom);
|
47
|
+
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
|
31
48
|
}
|
32
49
|
|
33
|
-
private onJoinedRoom =
|
34
|
-
if (debug) console.log("PlayerSync.
|
50
|
+
private onJoinedRoom = () => {
|
51
|
+
if (debug) console.log("PlayerSync.joinedRoom. autoSync is set to " + this.autoSync);
|
52
|
+
if (this.autoSync) this.getInstance();
|
53
|
+
}
|
35
54
|
|
36
|
-
|
55
|
+
async getInstance() {
|
56
|
+
if (this._localInstance) return this._localInstance;
|
57
|
+
|
58
|
+
if (debug) console.log("PlayerSync.createInstance", this.asset?.uri);
|
59
|
+
|
60
|
+
if (!this.asset?.asset && !this.asset?.uri) {
|
61
|
+
console.error("PlayerSync: can not create an instance because \"asset\" is not set!");
|
62
|
+
return null;
|
63
|
+
}
|
64
|
+
|
65
|
+
this._localInstance = this.asset?.instantiateSynced({ parent: this.gameObject }, true) as Promise<IGameObject>;
|
66
|
+
const instance = await this._localInstance;
|
37
67
|
if (instance) {
|
38
68
|
const pl = GameObject.getComponent(instance, PlayerState);
|
39
69
|
if (pl) {
|
@@ -41,15 +71,29 @@
|
|
41
71
|
this.onPlayerSpawned?.invoke(instance);
|
42
72
|
}
|
43
73
|
else {
|
74
|
+
this._localInstance = undefined;
|
44
75
|
console.error("<strong>Failed finding PlayerState on " + this.asset?.uri + "</strong>: please make sure the asset has a PlayerState component!");
|
45
76
|
GameObject.destroySynced(instance);
|
46
77
|
}
|
47
78
|
}
|
48
|
-
else{
|
79
|
+
else {
|
80
|
+
this._localInstance = undefined;
|
49
81
|
console.warn("PlayerSync: failed instantiating asset!")
|
50
82
|
}
|
83
|
+
|
84
|
+
return this._localInstance;
|
51
85
|
}
|
52
86
|
|
87
|
+
destroyInstance() {
|
88
|
+
this._localInstance?.then(go => {
|
89
|
+
if (debug) console.log("PlayerSync.destroyInstance", go);
|
90
|
+
return GameObject.destroySynced(go);
|
91
|
+
});
|
92
|
+
this._localInstance = undefined;
|
93
|
+
}
|
94
|
+
|
95
|
+
|
96
|
+
|
53
97
|
private watchTabVisible() {
|
54
98
|
window.addEventListener("visibilitychange", _ => {
|
55
99
|
if (document.visibilityState === "visible") {
|
@@ -90,19 +134,22 @@
|
|
90
134
|
return PlayerState._local;
|
91
135
|
}
|
92
136
|
|
93
|
-
|
94
|
-
static isLocalPlayer(obj: Object3D | Component): boolean {
|
137
|
+
static getFor(obj: Object3D | Component) {
|
95
138
|
if (obj instanceof Object3D) {
|
96
|
-
|
97
|
-
return state?.isLocalPlayer ?? false;
|
139
|
+
return GameObject.getComponentInParent(obj, PlayerState);
|
98
140
|
}
|
99
141
|
else if (obj instanceof Component) {
|
100
|
-
|
101
|
-
return state?.isLocalPlayer ?? false;
|
142
|
+
return GameObject.getComponentInParent(obj.gameObject, PlayerState);
|
102
143
|
}
|
103
|
-
return
|
144
|
+
return undefined;
|
104
145
|
}
|
105
146
|
|
147
|
+
//** use to check if a component or gameobject is part of a instance owned by the local player */
|
148
|
+
static isLocalPlayer(obj: Object3D | Component): boolean {
|
149
|
+
const state = PlayerState.getFor(obj);
|
150
|
+
return state?.isLocalPlayer ?? false;
|
151
|
+
}
|
152
|
+
|
106
153
|
// static Callback
|
107
154
|
private static _callbacks: { [key: string]: PlayerStateEventCallback[] } = {};
|
108
155
|
/**
|
@@ -133,6 +180,9 @@
|
|
133
180
|
@syncField(PlayerState.prototype.onOwnerChange)
|
134
181
|
owner?: string;
|
135
182
|
|
183
|
+
/** when enabled PlayerSync will not destroy itself when not connected anymore */
|
184
|
+
dontDestroy: boolean = false;
|
185
|
+
|
136
186
|
get isLocalPlayer(): boolean {
|
137
187
|
return this.owner === this.context.connection.connectionId;
|
138
188
|
}
|
@@ -152,13 +202,13 @@
|
|
152
202
|
}
|
153
203
|
|
154
204
|
// call local events
|
155
|
-
if(!this.hasOwner) {
|
205
|
+
if (!this.hasOwner) {
|
156
206
|
this.hasOwner = true;
|
157
207
|
this.onFirstOwnerChangeEvent?.invoke(detail);
|
158
208
|
}
|
159
209
|
|
160
210
|
this.onOwnerChangeEvent?.invoke(detail);
|
161
|
-
|
211
|
+
|
162
212
|
// call remote events
|
163
213
|
if (this.owner === this.context.connection.connectionId) {
|
164
214
|
PlayerState._local.push(this);
|
@@ -188,20 +238,63 @@
|
|
188
238
|
}
|
189
239
|
|
190
240
|
|
191
|
-
start() {
|
241
|
+
async start() {
|
242
|
+
if (debug) console.log("PLAYERSTATE.START, owner: " + this.owner, this.context.connection.usersInRoom([]))
|
243
|
+
|
244
|
+
// generate number from owner
|
245
|
+
// if (this.owner) {
|
246
|
+
// // string to number
|
247
|
+
// let num = 0;
|
248
|
+
// for (let i = 0; i < this.owner.length; i++) {
|
249
|
+
// num += this.owner.charCodeAt(i);
|
250
|
+
// }
|
251
|
+
// console.log(num)
|
252
|
+
// num = num / 1000
|
253
|
+
// this.gameObject.position.y = num;
|
254
|
+
// }
|
255
|
+
|
192
256
|
// If a player is spawned but not in the room anymore we want to destroy it
|
193
257
|
// this might happen in a case where all users get disconnected at once and the server
|
194
258
|
// still has the syncInstantiate messages that are sent to all clients
|
195
|
-
if (this.owner
|
196
|
-
|
197
|
-
this.
|
198
|
-
|
259
|
+
if (this.owner) {
|
260
|
+
// a slight delay is necessary right now because the syncInstantiate call might has created this object already with the owner assigned but the user has not yet joined the room
|
261
|
+
if (!this.context.connection.isInRoom) await delay(300);
|
262
|
+
if (this.context.connection.userIsInRoom(this.owner) == false) {
|
263
|
+
if (debug) console.log(`PlayerSync.start → doDestroy \"${this.name}\" because user \"${this.owner}\" is not in room anymore...`, "Currently in room:", ...this.context.connection.usersInRoom())
|
264
|
+
this.doDestroy();
|
265
|
+
}
|
199
266
|
}
|
267
|
+
else if (!this.owner) {
|
268
|
+
if (debug) console.warn("PlayerState.start → owner is undefined!", this.name);
|
269
|
+
// we can delete it here immediately because it is not synced anymore or the owner has left the room
|
270
|
+
// we could also do this in a timeout and check if the owner is still not assigned after a second (but that would be a hack)
|
271
|
+
setTimeout(() => {
|
272
|
+
if (!this.destroyed && !this.owner) {
|
273
|
+
if (!this.dontDestroy) {
|
274
|
+
if (debug) console.warn(`PlayerState.start → owner is still undefined: destroying \"${this.name}\" instance now`);
|
275
|
+
this.doDestroy();
|
276
|
+
}
|
277
|
+
else if (debug) console.warn("PlayerState.start → owner is still undefined but dontDestroy is set to true", this.name);
|
278
|
+
}
|
279
|
+
else console.log("PlayerState.start → owner is assigned", this.owner);
|
280
|
+
}, 2000);
|
281
|
+
}
|
200
282
|
}
|
201
283
|
|
284
|
+
// onEnable() {
|
285
|
+
// if (debug) this.startCoroutine(this.debugRoutine());
|
286
|
+
// }
|
287
|
+
|
288
|
+
// *debugRoutine() {
|
289
|
+
// while (!this.destroyed && this.activeAndEnabled) {
|
290
|
+
// Gizmos.DrawLabel(this.gameObject.worldPosition, this.owner ?? "no owner");
|
291
|
+
// yield;
|
292
|
+
// }
|
293
|
+
// }
|
294
|
+
|
202
295
|
/** this tells the server that this client has been destroyed and the networking message for the instantiate will be removed */
|
203
296
|
doDestroy() {
|
204
|
-
if (debug) console.log("PlayerSync.doDestroy → syncDestroy", this);
|
297
|
+
if (debug) console.log("PlayerSync.doDestroy → syncDestroy", this.name);
|
205
298
|
syncDestroy(this.gameObject, this.context.connection);
|
206
299
|
}
|
207
300
|
|
@@ -1,102 +1,171 @@
|
|
1
|
-
import { GameObject } from "../Component.js";
|
2
|
-
import { Input, NEPointerEvent } from "../../engine/engine_input.js";
|
3
1
|
import { Face, Object3D, Vector3 } from "three";
|
4
2
|
|
3
|
+
import { Input, InputEventNames, NEPointerEvent } from "../../engine/engine_input.js";
|
4
|
+
import { GamepadButtonName, MouseButtonName } from "../../engine/engine_types.js";
|
5
|
+
import { GameObject } from "../Component.js";
|
6
|
+
|
5
7
|
export interface IInputEventArgs {
|
6
8
|
get used(): boolean;
|
7
|
-
|
8
|
-
|
9
|
+
use(): void;
|
10
|
+
stopImmediatePropagation?(): void;
|
9
11
|
}
|
10
12
|
|
13
|
+
/** This pointer event data object is passed to all event receivers that are currently active
|
14
|
+
* It contains hit information if an object was hovered or clicked
|
15
|
+
* If the event is received in onPointerDown or onPointerMove, you can call `setPointerCapture` to receive onPointerMove events even when the pointer has left the object until you call `releasePointerCapture` or when the pointerUp event happens
|
16
|
+
* You can get additional information about the event or event source via the `event` property (of type `NEPointerEvent`)
|
17
|
+
*/
|
11
18
|
export class PointerEventData implements IInputEventArgs {
|
12
19
|
|
13
|
-
|
14
|
-
|
20
|
+
/** the original event */
|
21
|
+
readonly event: NEPointerEvent;
|
15
22
|
|
23
|
+
/** the index of the used device
|
24
|
+
* mouse and touch are always 0, controller is the gamepad index or XRController index
|
25
|
+
*/
|
26
|
+
get deviceIndex() { return this.event.deviceIndex; }
|
27
|
+
|
28
|
+
/** a combination of the pointerId + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 101) */
|
29
|
+
get pointerId() { return this.event.pointerId; }
|
30
|
+
|
31
|
+
/**
|
32
|
+
* mouse button 0 === LEFT, 1 === MIDDLE, 2 === RIGHT
|
33
|
+
* */
|
34
|
+
readonly button: number;
|
35
|
+
readonly buttonName: MouseButtonName | GamepadButtonName | undefined;
|
36
|
+
get pressure(): number { return this.event.pressure; }
|
37
|
+
|
38
|
+
private _used: boolean = false;
|
39
|
+
/** true when `use()` has been called */
|
40
|
+
get used(): boolean {
|
41
|
+
return this._used;
|
42
|
+
}
|
43
|
+
|
44
|
+
/** mark this event to be used */
|
16
45
|
use() {
|
17
|
-
this.
|
46
|
+
if (this._used) return;
|
47
|
+
this._used = true;
|
18
48
|
if (this.pointerId !== undefined)
|
19
49
|
this.input.setPointerUsed(this.pointerId);
|
20
50
|
}
|
21
51
|
|
22
|
-
|
23
|
-
|
52
|
+
private _propagationStopped: boolean = false;
|
53
|
+
get propagationStopped() {
|
54
|
+
return this._propagationStopped;
|
24
55
|
}
|
25
56
|
|
26
|
-
|
27
|
-
|
28
|
-
|
57
|
+
/** Call this method to stop immediate propagation on the `event` object.
|
58
|
+
* WARNING: this is currently equivalent to stopImmediatePropagation
|
59
|
+
*/
|
60
|
+
stopPropagation() {
|
61
|
+
// we currently don't have a distinction between stopPropagation and stopImmediatePropagation
|
62
|
+
this._propagationStopped = true;
|
63
|
+
this.event.stopImmediatePropagation();
|
29
64
|
}
|
65
|
+
/** Call this method to stop immediate propagation on the `event` object.
|
66
|
+
*/
|
67
|
+
stopImmediatePropagation() {
|
68
|
+
this._propagationStopped = true;
|
69
|
+
this.event.stopImmediatePropagation();
|
70
|
+
}
|
30
71
|
|
31
|
-
/**@
|
32
|
-
|
33
|
-
|
72
|
+
/**@ignore internal flag, pointer captured (we dont want to see it in intellisense) */
|
73
|
+
z__pointer_ctured: boolean = false;
|
74
|
+
/** Call this method in `onPointerDown` or `onPointerMove` to receive onPointerMove events for this pointerId even when the pointer has left the object until you call `releasePointerCapture` or when the pointerUp event happens
|
75
|
+
*/
|
76
|
+
setPointerCapture() {
|
77
|
+
this.z__pointer_ctured = true;
|
34
78
|
}
|
79
|
+
/**@ignore internal flag, pointer capture released */
|
80
|
+
z__pointer_cture_rleased: boolean = false;
|
81
|
+
/** call this method in `onPointerDown` or `onPointerMove` to stop receiving onPointerMove events */
|
82
|
+
releasePointerCapture() {
|
83
|
+
this.z__pointer_cture_rleased = true;
|
84
|
+
}
|
35
85
|
|
86
|
+
|
36
87
|
/** Who initiated this event */
|
37
88
|
inputSource: Input | any;
|
38
89
|
|
90
|
+
/** Returns the input target ray mode e.g. "screen" for 2D mouse and touch events */
|
91
|
+
get mode(): XRTargetRayMode { return this.event.mode; }
|
92
|
+
|
39
93
|
/** The object this event hit or interacted with */
|
40
94
|
object!: THREE.Object3D;
|
41
95
|
/** The world position of this event */
|
42
96
|
point?: Vector3;
|
43
|
-
/** The
|
97
|
+
/** The object-space normal of this event */
|
44
98
|
normal?: Vector3;
|
99
|
+
/** */
|
45
100
|
face?: Face | null;
|
101
|
+
/** The distance of the hit point from the origin */
|
46
102
|
distance?: number;
|
103
|
+
/** The instance ID of an object hit by a raycast (if a instanced object was hit) */
|
47
104
|
instanceId?: number;
|
48
105
|
|
49
|
-
pointerId: number | undefined;
|
50
106
|
isDown: boolean | undefined;
|
51
107
|
isUp: boolean | undefined;
|
52
108
|
isPressed: boolean | undefined;
|
53
|
-
|
109
|
+
isClick: boolean | undefined;
|
110
|
+
isDoubleClick: boolean | undefined;
|
54
111
|
|
55
|
-
/** mouse button 0 === LEFT, 1 === MIDDLE, 2 === RIGHT */
|
56
|
-
readonly button: number | string;
|
57
112
|
|
58
113
|
private input: Input;
|
59
114
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
constructor(input: Input, event?: NEPointerEvent) {
|
64
|
-
this._event = event;
|
115
|
+
constructor(input: Input, event: NEPointerEvent) {
|
116
|
+
this.event = event;
|
65
117
|
this.input = input;
|
66
|
-
this.button = event
|
118
|
+
this.button = event.button;
|
67
119
|
}
|
68
120
|
|
69
121
|
clone() {
|
70
|
-
const clone = new PointerEventData(this.input, this.
|
122
|
+
const clone = new PointerEventData(this.input, this.event);
|
71
123
|
Object.assign(clone, this);
|
72
124
|
return clone;
|
73
125
|
}
|
126
|
+
|
127
|
+
/**@deprecated use use() */
|
128
|
+
Use() {
|
129
|
+
this.use();
|
130
|
+
}
|
131
|
+
|
132
|
+
/**@deprecated use stopPropagation() */
|
133
|
+
StopPropagation() {
|
134
|
+
this.event.stopImmediatePropagation();
|
135
|
+
}
|
74
136
|
}
|
75
137
|
|
76
138
|
export interface IPointerDownHandler {
|
139
|
+
/** Called when a button is started to being pressed on an object (or a child object) */
|
77
140
|
onPointerDown?(args: PointerEventData);
|
78
141
|
}
|
79
142
|
|
80
143
|
export interface IPointerUpHandler {
|
144
|
+
/** Called when a button is released (which was previously pressed in `onPointerDown`) */
|
81
145
|
onPointerUp?(args: PointerEventData);
|
82
146
|
}
|
83
147
|
|
84
148
|
export interface IPointerEnterHandler {
|
149
|
+
/** Called when a pointer (mouse, touch, xr controller) starts pointing on/hovering an object (or a child object) */
|
85
150
|
onPointerEnter?(args: PointerEventData);
|
86
151
|
}
|
87
152
|
|
88
153
|
export interface IPointerMoveHandler {
|
154
|
+
/** Called when a pointer (mouse, touch, xr controller) is moving over an object (or a child object) */
|
89
155
|
onPointerMove?(args: PointerEventData);
|
90
156
|
}
|
91
157
|
|
92
158
|
export interface IPointerExitHandler {
|
159
|
+
/** Called when a pointer (mouse, touch, xr controller) exists an object (it was hovering the object before but now it's not anymore) */
|
93
160
|
onPointerExit?(args: PointerEventData);
|
94
161
|
}
|
95
162
|
|
96
163
|
export interface IPointerClickHandler {
|
164
|
+
/** Called when an object (or any child object) is clicked (needs a EventSystem in the scene) */
|
97
165
|
onPointerClick?(args: PointerEventData);
|
98
166
|
}
|
99
167
|
|
168
|
+
/** Implement on your component to receive input events via the `EventSystem` component */
|
100
169
|
export interface IPointerEventHandler extends IPointerDownHandler,
|
101
170
|
IPointerUpHandler, IPointerEnterHandler, IPointerMoveHandler, IPointerExitHandler, IPointerClickHandler { }
|
102
171
|
|
@@ -106,11 +175,30 @@
|
|
106
175
|
* @internal tests if the object has any PointerEventComponent used by the EventSystem
|
107
176
|
* This is used to skip raycasting on objects that have no components that use pointer events
|
108
177
|
*/
|
109
|
-
export function hasPointerEventComponent(obj: Object3D) {
|
178
|
+
export function hasPointerEventComponent(obj: Object3D, event?: InputEventNames | null) {
|
110
179
|
const res = GameObject.foreachComponent(obj, comp => {
|
180
|
+
// ignore disabled components
|
181
|
+
if (!comp.enabled) return undefined;
|
182
|
+
|
111
183
|
const handler = comp as IPointerEventHandler;
|
112
|
-
if
|
113
|
-
|
184
|
+
// if a specific event is passed in, we only check for that event
|
185
|
+
if (event) {
|
186
|
+
switch (event) {
|
187
|
+
case "pointerdown":
|
188
|
+
if (handler.onPointerDown) return true;
|
189
|
+
break;
|
190
|
+
case "pointerup":
|
191
|
+
if (handler.onPointerUp || handler.onPointerClick) return true;
|
192
|
+
break;
|
193
|
+
case "pointermove":
|
194
|
+
if (handler.onPointerEnter || handler.onPointerExit || handler.onPointerMove) return true;
|
195
|
+
break;
|
196
|
+
}
|
197
|
+
}
|
198
|
+
else {
|
199
|
+
if (handler.onPointerDown || handler.onPointerUp || handler.onPointerEnter || handler.onPointerExit || handler.onPointerClick)
|
200
|
+
return true;
|
201
|
+
}
|
114
202
|
// undefined means continue
|
115
203
|
return undefined;
|
116
204
|
}, false);
|
@@ -1,10 +1,11 @@
|
|
1
|
+
import { Effect, Pass } from "postprocessing";
|
2
|
+
|
3
|
+
import type { EditorModification, IEditorModification } from "../../engine/engine_editor-sync.js";
|
1
4
|
import { serializable } from "../../engine/engine_serialization.js";
|
2
|
-
import { Effect, Pass } from "postprocessing";
|
3
|
-
import { VolumeParameter } from "./VolumeParameter.js";
|
4
|
-
import { Component } from "../Component.js";
|
5
5
|
import type { ISerializable, SerializationContext } from "../../engine/engine_serialization_core.js";
|
6
|
-
import type { EditorModification, IEditorModification } from "../../engine/engine_editor-sync.js";
|
7
6
|
import { getParam } from "../../engine/engine_utils.js";
|
7
|
+
import { Component } from "../Component.js";
|
8
|
+
import { VolumeParameter } from "./VolumeParameter.js";
|
8
9
|
|
9
10
|
const debug = getParam("debugpost");
|
10
11
|
|
@@ -1,12 +1,13 @@
|
|
1
|
+
import { N8AOPostPass } from "n8ao";
|
2
|
+
import { BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, DepthDownsamplingPass, DepthOfFieldEffect, Effect, EffectComposer, EffectPass, HueSaturationEffect, NormalPass, Pass, PixelationEffect, RenderPass, SelectiveBloomEffect, SSAOEffect, VignetteEffect } from "postprocessing";
|
1
3
|
import { HalfFloatType } from "three";
|
4
|
+
|
5
|
+
import { showBalloonWarning } from "../../engine/debug/index.js";
|
2
6
|
import { Context } from "../../engine/engine_setup.js";
|
7
|
+
import type { Constructor } from "../../engine/engine_types.js";
|
3
8
|
import { getParam, isMobileDevice } from "../../engine/engine_utils.js";
|
4
|
-
import { BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, DepthDownsamplingPass, DepthOfFieldEffect, Effect, EffectComposer, EffectPass, HueSaturationEffect, NormalPass, Pass, PixelationEffect, RenderPass, SelectiveBloomEffect, SSAOEffect, VignetteEffect } from "postprocessing";
|
5
|
-
import { showBalloonWarning } from "../../engine/debug/index.js";
|
6
9
|
import { Camera } from "../Camera.js";
|
7
10
|
import { PostProcessingEffect } from "./PostProcessingEffect.js";
|
8
|
-
import type { Constructor } from "../../engine/engine_types.js";
|
9
|
-
import { N8AOPostPass } from "n8ao";
|
10
11
|
|
11
12
|
const debug = getParam("debugpost");
|
12
13
|
|
@@ -1,5 +1,5 @@
|
|
1
|
+
import type { KeyCode } from "../engine/engine_input.js";
|
1
2
|
import { Behaviour } from "../engine-components/Component.js";
|
2
|
-
import type { KeyCode } from "../engine/engine_input.js";
|
3
3
|
|
4
4
|
export class PresentationMode extends Behaviour {
|
5
5
|
|
@@ -1,11 +1,17 @@
|
|
1
|
+
import { SkinnedMesh } from "three";
|
2
|
+
|
3
|
+
import { IRaycastOptions, RaycastOptions } from "../../engine/engine_physics.js";
|
1
4
|
import { serializable } from "../../engine/engine_serialization.js";
|
2
|
-
import {
|
3
|
-
import { Behaviour
|
5
|
+
import { NeedleXRSession } from "../../engine/engine_xr.js";
|
6
|
+
import { Behaviour } from "../Component.js";
|
4
7
|
import { EventSystem } from "./EventSystem.js";
|
5
|
-
import { SkinnedMesh } from "three";
|
6
8
|
|
7
9
|
|
8
|
-
|
10
|
+
/** Derive from this class to create your own custom Raycaster
|
11
|
+
* If you override awake, onEnable or onDisable, be sure to call the base class methods
|
12
|
+
* Implement `performRaycast` to perform your custom raycasting logic
|
13
|
+
*/
|
14
|
+
export abstract class Raycaster extends Behaviour {
|
9
15
|
awake(): void {
|
10
16
|
EventSystem.createIfNoneExists(this.context);
|
11
17
|
}
|
@@ -18,9 +24,7 @@
|
|
18
24
|
EventSystem.get(this.context)?.unregister(this);
|
19
25
|
}
|
20
26
|
|
21
|
-
performRaycast(_opts
|
22
|
-
return null;
|
23
|
-
}
|
27
|
+
abstract performRaycast(_opts?: IRaycastOptions | RaycastOptions | null): THREE.Intersection[] | null;
|
24
28
|
}
|
25
29
|
|
26
30
|
|
@@ -35,7 +39,7 @@
|
|
35
39
|
this.targets = [this.gameObject];
|
36
40
|
}
|
37
41
|
|
38
|
-
performRaycast(opts: RaycastOptions | null = null): THREE.Intersection[] | null {
|
42
|
+
performRaycast(opts: IRaycastOptions | RaycastOptions | null = null): THREE.Intersection[] | null {
|
39
43
|
if (!this.targets) return null;
|
40
44
|
opts ??= new RaycastOptions();
|
41
45
|
opts.targets = this.targets;
|
@@ -70,4 +74,19 @@
|
|
70
74
|
}
|
71
75
|
}
|
72
76
|
|
77
|
+
export class SpatialGrabRaycaster extends Raycaster {
|
78
|
+
performRaycast(_opts?: IRaycastOptions | RaycastOptions | null): THREE.Intersection[] | null {
|
79
|
+
// ensure we're in XR, otherwise return
|
80
|
+
if (!NeedleXRSession.active) return null;
|
81
|
+
if (!_opts?.ray) return null;
|
73
82
|
|
83
|
+
const rayOrigin = _opts.ray.origin;
|
84
|
+
const radius = 0.01;
|
85
|
+
|
86
|
+
// TODO if needed, check if the input source is a XR controller or hand
|
87
|
+
// draw gizmo around ray origin
|
88
|
+
// Gizmos.DrawSphere(rayOrigin, radius, 0x00ff0022);
|
89
|
+
|
90
|
+
return this.context.physics.sphereOverlap(rayOrigin, radius);
|
91
|
+
}
|
92
|
+
}
|
@@ -1,8 +1,9 @@
|
|
1
|
+
import { Object3D } from "three";
|
2
|
+
|
1
3
|
import { foreachComponent } from "../../engine/engine_gameobject.js";
|
2
4
|
import { type IComponent } from "../../engine/engine_types.js";
|
3
5
|
import { $shadowDomOwner } from "./BaseUIComponent.js";
|
4
6
|
import { type ICanvasGroup, type IGraphic } from "./Interfaces.js";
|
5
|
-
import { Object3D } from "three";
|
6
7
|
|
7
8
|
|
8
9
|
export class UIRaycastUtils {
|
@@ -1,13 +1,14 @@
|
|
1
|
+
import { Matrix4, Object3D, Quaternion, Vector2, Vector3 } from "three";
|
1
2
|
import * as ThreeMeshUI from 'three-mesh-ui'
|
2
|
-
import { BaseUIComponent } from "./BaseUIComponent.js";
|
3
3
|
import { type DocumentedOptions as ThreeMeshUIEveryOptions } from "three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js";
|
4
|
+
|
5
|
+
import { foreachComponentEnumerator } from "../../engine/engine_gameobject.js";
|
4
6
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
5
|
-
import { Matrix4, Object3D, Quaternion, Vector2, Vector3 } from "three";
|
6
7
|
import { getParam } from "../../engine/engine_utils.js";
|
8
|
+
import { GameObject } from '../Component.js';
|
9
|
+
import { BaseUIComponent } from "./BaseUIComponent.js";
|
10
|
+
import { type ICanvas, type IRectTransform, type IRectTransformChangedReceiver } from "./Interfaces.js";
|
7
11
|
import { onChange } from "./Utils.js";
|
8
|
-
import { foreachComponentEnumerator } from "../../engine/engine_gameobject.js";
|
9
|
-
import { type ICanvas, type IRectTransform, type IRectTransformChangedReceiver } from "./Interfaces.js";
|
10
|
-
import { GameObject } from '../Component.js';
|
11
12
|
|
12
13
|
const debug = getParam("debugui");
|
13
14
|
const debugLayout = getParam("debuguilayout");
|
@@ -1,10 +1,11 @@
|
|
1
|
-
import { Behaviour } from "./Component.js";
|
2
1
|
import { EquirectangularReflectionMapping, Material, Object3D, SRGBColorSpace, Texture, Vector3 } from "three";
|
2
|
+
|
3
3
|
import { serializable } from "../engine/engine_serialization.js";
|
4
4
|
import { Context } from "../engine/engine_setup.js";
|
5
5
|
import type { IRenderer } from "../engine/engine_types.js";
|
6
|
+
import { getParam } from "../engine/engine_utils.js";
|
6
7
|
import { BoxHelperComponent } from "./BoxHelperComponent.js";
|
7
|
-
import {
|
8
|
+
import { Behaviour } from "./Component.js";
|
8
9
|
|
9
10
|
export const debug = getParam("debugreflectionprobe");
|
10
11
|
const disable = getParam("noreflectionprobe");
|
@@ -1,4 +1,5 @@
|
|
1
|
-

|
1
|
+
/* eslint-disable */
|
2
|
+
import { TypeStore } from "./../engine_typestore.js"
|
2
3
|
|
3
4
|
// Import types
|
4
5
|
import { __Ignore } from "../../engine-components/codegen/components.js";
|
@@ -13,11 +14,11 @@
|
|
13
14
|
import { Animator } from "../../engine-components/Animator.js";
|
14
15
|
import { AnimatorController } from "../../engine-components/AnimatorController.js";
|
15
16
|
import { Antialiasing } from "../../engine-components/postprocessing/Effects/Antialiasing.js";
|
16
|
-
import { AttachedObject } from "../../engine-components/webxr/WebXRController.js";
|
17
17
|
import { AudioExtension } from "../../engine-components/export/usdz/extensions/behavior/AudioExtension.js";
|
18
18
|
import { AudioListener } from "../../engine-components/AudioListener.js";
|
19
19
|
import { AudioSource } from "../../engine-components/AudioSource.js";
|
20
20
|
import { AudioTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
|
21
|
+
import { Avatar } from "../../engine-components/webxr/Avatar.js";
|
21
22
|
import { Avatar_Brain_LookAt } from "../../engine-components/avatar/Avatar_Brain_LookAt.js";
|
22
23
|
import { Avatar_MouthShapes } from "../../engine-components/avatar/Avatar_MouthShapes.js";
|
23
24
|
import { Avatar_MustacheShake } from "../../engine-components/avatar/Avatar_MustacheShake.js";
|
@@ -32,7 +33,6 @@
|
|
32
33
|
import { BasicIKConstraint } from "../../engine-components/BasicIKConstraint.js";
|
33
34
|
import { BehaviorExtension } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
|
34
35
|
import { BehaviorModel } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
35
|
-
import { Behaviour } from "../../engine-components/Component.js";
|
36
36
|
import { Bloom } from "../../engine-components/postprocessing/Effects/Bloom.js";
|
37
37
|
import { BoxCollider } from "../../engine-components/Collider.js";
|
38
38
|
import { BoxGizmo } from "../../engine-components/Gizmos.js";
|
@@ -53,7 +53,6 @@
|
|
53
53
|
import { ColorAdjustments } from "../../engine-components/postprocessing/Effects/ColorAdjustments.js";
|
54
54
|
import { ColorBySpeedModule } from "../../engine-components/ParticleSystemModules.js";
|
55
55
|
import { ColorOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
|
56
|
-
import { Component } from "../../engine-components/Component.js";
|
57
56
|
import { ContactShadows } from "../../engine-components/ContactShadows.js";
|
58
57
|
import { ControlTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
|
59
58
|
import { CustomBranding } from "../../engine-components/export/usdz/USDZExporter.js";
|
@@ -90,7 +89,6 @@
|
|
90
89
|
import { Image } from "../../engine-components/ui/Image.js";
|
91
90
|
import { InheritVelocityModule } from "../../engine-components/ParticleSystemModules.js";
|
92
91
|
import { InputField } from "../../engine-components/ui/InputField.js";
|
93
|
-
import { Interactable } from "../../engine-components/Interactable.js";
|
94
92
|
import { Light } from "../../engine-components/Light.js";
|
95
93
|
import { LimitVelocityOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
|
96
94
|
import { LODGroup } from "../../engine-components/LODGroup.js";
|
@@ -104,6 +102,7 @@
|
|
104
102
|
import { MeshRenderer } from "../../engine-components/Renderer.js";
|
105
103
|
import { MinMaxCurve } from "../../engine-components/ParticleSystemModules.js";
|
106
104
|
import { MinMaxGradient } from "../../engine-components/ParticleSystemModules.js";
|
105
|
+
import { NeedleWebXRHtmlElement } from "../../engine-components/webxr/WebXRButtons.js";
|
107
106
|
import { NestedGltf } from "../../engine-components/NestedGltf.js";
|
108
107
|
import { Networking } from "../../engine-components/Networking.js";
|
109
108
|
import { NoiseModule } from "../../engine-components/ParticleSystemModules.js";
|
@@ -130,7 +129,6 @@
|
|
130
129
|
import { PreliminaryTrigger } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents.js";
|
131
130
|
import { PresentationMode } from "../../engine-components-experimental/Presentation.js";
|
132
131
|
import { RawImage } from "../../engine-components/ui/Image.js";
|
133
|
-
import { Raycaster } from "../../engine-components/ui/Raycaster.js";
|
134
132
|
import { Rect } from "../../engine-components/ui/RectTransform.js";
|
135
133
|
import { RectTransform } from "../../engine-components/ui/RectTransform.js";
|
136
134
|
import { ReflectionProbe } from "../../engine-components/ReflectionProbe.js";
|
@@ -158,6 +156,7 @@
|
|
158
156
|
import { SizeOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
|
159
157
|
import { SkinnedMeshRenderer } from "../../engine-components/Renderer.js";
|
160
158
|
import { SmoothFollow } from "../../engine-components/SmoothFollow.js";
|
159
|
+
import { SpatialGrabRaycaster } from "../../engine-components/ui/Raycaster.js";
|
161
160
|
import { SpatialHtml } from "../../engine-components/ui/SpatialHtml.js";
|
162
161
|
import { SpatialTrigger } from "../../engine-components/SpatialTrigger.js";
|
163
162
|
import { SpatialTriggerReceiver } from "../../engine-components/SpatialTrigger.js";
|
@@ -172,7 +171,7 @@
|
|
172
171
|
import { SyncedRoom } from "../../engine-components/SyncedRoom.js";
|
173
172
|
import { SyncedTransform } from "../../engine-components/SyncedTransform.js";
|
174
173
|
import { TapGestureTrigger } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents.js";
|
175
|
-
import { TeleportTarget } from "../../engine-components/webxr/
|
174
|
+
import { TeleportTarget } from "../../engine-components/webxr/TeleportTarget.js";
|
176
175
|
import { TestRunner } from "../../engine-components/TestRunner.js";
|
177
176
|
import { TestSimulateUserData } from "../../engine-components/TestRunner.js";
|
178
177
|
import { Text } from "../../engine-components/ui/Text.js";
|
@@ -202,23 +201,19 @@
|
|
202
201
|
import { Volume } from "../../engine-components/postprocessing/Volume.js";
|
203
202
|
import { VolumeParameter } from "../../engine-components/postprocessing/VolumeParameter.js";
|
204
203
|
import { VolumeProfile } from "../../engine-components/postprocessing/VolumeProfile.js";
|
205
|
-
import { VRUserState } from "../../engine-components/webxr/WebXRSync.js";
|
206
|
-
import { WebAR } from "../../engine-components/webxr/WebXR.js";
|
207
204
|
import { WebARCameraBackground } from "../../engine-components/webxr/WebARCameraBackground.js";
|
208
205
|
import { WebARSessionRoot } from "../../engine-components/webxr/WebARSessionRoot.js";
|
209
206
|
import { WebXR } from "../../engine-components/webxr/WebXR.js";
|
210
|
-
import { WebXRAvatar } from "../../engine-components/webxr/WebXRAvatar.js";
|
211
|
-
import { WebXRController } from "../../engine-components/webxr/WebXRController.js";
|
212
207
|
import { WebXRImageTracking } from "../../engine-components/webxr/WebXRImageTracking.js";
|
213
208
|
import { WebXRImageTrackingModel } from "../../engine-components/webxr/WebXRImageTracking.js";
|
214
209
|
import { WebXRPlaneTracking } from "../../engine-components/webxr/WebXRPlaneTracking.js";
|
215
|
-
import { WebXRSync } from "../../engine-components/webxr/WebXRSync.js";
|
216
210
|
import { WebXRTrackedImage } from "../../engine-components/webxr/WebXRImageTracking.js";
|
217
|
-
import {
|
218
|
-
import {
|
219
|
-
import {
|
211
|
+
import { XRControllerFollow } from "../../engine-components/webxr/controllers/XRControllerFollow.js";
|
212
|
+
import { XRControllerModel } from "../../engine-components/webxr/controllers/XRControllerModel.js";
|
213
|
+
import { XRControllerMovement } from "../../engine-components/webxr/controllers/XRControllerMovement.js";
|
214
|
+
import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
|
220
215
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
221
|
-
import { XRState } from "../../engine-components/XRFlag.js";
|
216
|
+
import { XRState } from "../../engine-components/webxr/XRFlag.js";
|
222
217
|
|
223
218
|
// Register types
|
224
219
|
TypeStore.add("__Ignore", __Ignore);
|
@@ -233,11 +228,11 @@
|
|
233
228
|
TypeStore.add("Animator", Animator);
|
234
229
|
TypeStore.add("AnimatorController", AnimatorController);
|
235
230
|
TypeStore.add("Antialiasing", Antialiasing);
|
236
|
-
TypeStore.add("AttachedObject", AttachedObject);
|
237
231
|
TypeStore.add("AudioExtension", AudioExtension);
|
238
232
|
TypeStore.add("AudioListener", AudioListener);
|
239
233
|
TypeStore.add("AudioSource", AudioSource);
|
240
234
|
TypeStore.add("AudioTrackHandler", AudioTrackHandler);
|
235
|
+
TypeStore.add("Avatar", Avatar);
|
241
236
|
TypeStore.add("Avatar_Brain_LookAt", Avatar_Brain_LookAt);
|
242
237
|
TypeStore.add("Avatar_MouthShapes", Avatar_MouthShapes);
|
243
238
|
TypeStore.add("Avatar_MustacheShake", Avatar_MustacheShake);
|
@@ -252,7 +247,6 @@
|
|
252
247
|
TypeStore.add("BasicIKConstraint", BasicIKConstraint);
|
253
248
|
TypeStore.add("BehaviorExtension", BehaviorExtension);
|
254
249
|
TypeStore.add("BehaviorModel", BehaviorModel);
|
255
|
-
TypeStore.add("Behaviour", Behaviour);
|
256
250
|
TypeStore.add("Bloom", Bloom);
|
257
251
|
TypeStore.add("BoxCollider", BoxCollider);
|
258
252
|
TypeStore.add("BoxGizmo", BoxGizmo);
|
@@ -273,7 +267,6 @@
|
|
273
267
|
TypeStore.add("ColorAdjustments", ColorAdjustments);
|
274
268
|
TypeStore.add("ColorBySpeedModule", ColorBySpeedModule);
|
275
269
|
TypeStore.add("ColorOverLifetimeModule", ColorOverLifetimeModule);
|
276
|
-
TypeStore.add("Component", Component);
|
277
270
|
TypeStore.add("ContactShadows", ContactShadows);
|
278
271
|
TypeStore.add("ControlTrackHandler", ControlTrackHandler);
|
279
272
|
TypeStore.add("CustomBranding", CustomBranding);
|
@@ -310,7 +303,6 @@
|
|
310
303
|
TypeStore.add("Image", Image);
|
311
304
|
TypeStore.add("InheritVelocityModule", InheritVelocityModule);
|
312
305
|
TypeStore.add("InputField", InputField);
|
313
|
-
TypeStore.add("Interactable", Interactable);
|
314
306
|
TypeStore.add("Light", Light);
|
315
307
|
TypeStore.add("LimitVelocityOverLifetimeModule", LimitVelocityOverLifetimeModule);
|
316
308
|
TypeStore.add("LODGroup", LODGroup);
|
@@ -324,6 +316,7 @@
|
|
324
316
|
TypeStore.add("MeshRenderer", MeshRenderer);
|
325
317
|
TypeStore.add("MinMaxCurve", MinMaxCurve);
|
326
318
|
TypeStore.add("MinMaxGradient", MinMaxGradient);
|
319
|
+
TypeStore.add("NeedleWebXRHtmlElement", NeedleWebXRHtmlElement);
|
327
320
|
TypeStore.add("NestedGltf", NestedGltf);
|
328
321
|
TypeStore.add("Networking", Networking);
|
329
322
|
TypeStore.add("NoiseModule", NoiseModule);
|
@@ -350,7 +343,6 @@
|
|
350
343
|
TypeStore.add("PreliminaryTrigger", PreliminaryTrigger);
|
351
344
|
TypeStore.add("PresentationMode", PresentationMode);
|
352
345
|
TypeStore.add("RawImage", RawImage);
|
353
|
-
TypeStore.add("Raycaster", Raycaster);
|
354
346
|
TypeStore.add("Rect", Rect);
|
355
347
|
TypeStore.add("RectTransform", RectTransform);
|
356
348
|
TypeStore.add("ReflectionProbe", ReflectionProbe);
|
@@ -378,6 +370,7 @@
|
|
378
370
|
TypeStore.add("SizeOverLifetimeModule", SizeOverLifetimeModule);
|
379
371
|
TypeStore.add("SkinnedMeshRenderer", SkinnedMeshRenderer);
|
380
372
|
TypeStore.add("SmoothFollow", SmoothFollow);
|
373
|
+
TypeStore.add("SpatialGrabRaycaster", SpatialGrabRaycaster);
|
381
374
|
TypeStore.add("SpatialHtml", SpatialHtml);
|
382
375
|
TypeStore.add("SpatialTrigger", SpatialTrigger);
|
383
376
|
TypeStore.add("SpatialTriggerReceiver", SpatialTriggerReceiver);
|
@@ -422,20 +415,16 @@
|
|
422
415
|
TypeStore.add("Volume", Volume);
|
423
416
|
TypeStore.add("VolumeParameter", VolumeParameter);
|
424
417
|
TypeStore.add("VolumeProfile", VolumeProfile);
|
425
|
-
TypeStore.add("VRUserState", VRUserState);
|
426
|
-
TypeStore.add("WebAR", WebAR);
|
427
418
|
TypeStore.add("WebARCameraBackground", WebARCameraBackground);
|
428
419
|
TypeStore.add("WebARSessionRoot", WebARSessionRoot);
|
429
420
|
TypeStore.add("WebXR", WebXR);
|
430
|
-
TypeStore.add("WebXRAvatar", WebXRAvatar);
|
431
|
-
TypeStore.add("WebXRController", WebXRController);
|
432
421
|
TypeStore.add("WebXRImageTracking", WebXRImageTracking);
|
433
422
|
TypeStore.add("WebXRImageTrackingModel", WebXRImageTrackingModel);
|
434
423
|
TypeStore.add("WebXRPlaneTracking", WebXRPlaneTracking);
|
435
|
-
TypeStore.add("WebXRSync", WebXRSync);
|
436
424
|
TypeStore.add("WebXRTrackedImage", WebXRTrackedImage);
|
425
|
+
TypeStore.add("XRControllerFollow", XRControllerFollow);
|
426
|
+
TypeStore.add("XRControllerModel", XRControllerModel);
|
427
|
+
TypeStore.add("XRControllerMovement", XRControllerMovement);
|
437
428
|
TypeStore.add("XRFlag", XRFlag);
|
438
|
-
TypeStore.add("XRGrabModel", XRGrabModel);
|
439
|
-
TypeStore.add("XRGrabRendering", XRGrabRendering);
|
440
429
|
TypeStore.add("XRRig", XRRig);
|
441
430
|
TypeStore.add("XRState", XRState);
|
@@ -1,21 +1,22 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
1
|
import * as THREE from "three";
|
3
|
-
|
4
|
-
|
2
|
+
import { AxesHelper, Material, Matrix4, Mesh, Object3D, SkinnedMesh, Texture, Vector4 } from "three";
|
3
|
+
|
4
|
+
import { showBalloonWarning } from "../engine/debug/index.js";
|
5
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
6
|
+
import { $instancingAutoUpdateBounds, $instancingRenderer, InstancingUtil, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
|
7
|
+
import { isLocalNetwork } from "../engine/engine_networking_utils.js";
|
8
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
9
|
import { Context, FrameEvent } from "../engine/engine_setup.js";
|
10
|
+
import { getTempVector } from "../engine/engine_three_utils.js";
|
11
|
+
import type { IRenderer, ISharedMaterials } from "../engine/engine_types.js";
|
6
12
|
import { getParam } from "../engine/engine_utils.js";
|
7
|
-
import {
|
8
|
-
import { AxesHelper, Material, Matrix4, Mesh, Object3D, SkinnedMesh, Texture, Vector4 } from "three";
|
13
|
+
import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
|
9
14
|
import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects.js";
|
10
|
-
import {
|
11
|
-
import {
|
12
|
-
import type { IRenderer, ISharedMaterials } from "../engine/engine_types.js";
|
15
|
+
import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
|
16
|
+
import { Behaviour, GameObject } from "./Component.js";
|
13
17
|
import { ReflectionProbe } from "./ReflectionProbe.js";
|
14
|
-
import {
|
15
|
-
import {
|
16
|
-
import { showBalloonWarning } from "../engine/debug/index.js";
|
17
|
-
import { Gizmos } from "../engine/engine_gizmos.js";
|
18
|
-
import { getTempVector } from "../engine/engine_three_utils.js";
|
18
|
+
// import { RendererCustomShader } from "./RendererCustomShader.js";
|
19
|
+
import { RendererLightmap } from "./RendererLightmap.js";
|
19
20
|
|
20
21
|
// for staying compatible with old code
|
21
22
|
export { InstancingUtil } from "../engine/engine_instancing.js";
|
@@ -240,7 +241,11 @@
|
|
240
241
|
// private _materialProperties: Array<MaterialProperties> | undefined = undefined;
|
241
242
|
private _lightmaps?: RendererLightmap[];
|
242
243
|
|
243
|
-
|
244
|
+
/** Get the mesh Object3D for this renderer
|
245
|
+
* Warn: if this is a multimaterial object it will return the first mesh only
|
246
|
+
* @returns the mesh object3D.
|
247
|
+
* */
|
248
|
+
get sharedMesh(): Mesh | SkinnedMesh | undefined {
|
244
249
|
if (this.gameObject.type === "Mesh") {
|
245
250
|
return this.gameObject as unknown as Mesh
|
246
251
|
}
|
@@ -253,11 +258,31 @@
|
|
253
258
|
return undefined;
|
254
259
|
}
|
255
260
|
|
256
|
-
|
261
|
+
private readonly _sharedMeshes: Mesh[] = [];
|
262
|
+
/** Get all the mesh Object3D for this renderer
|
263
|
+
* @returns an array of mesh object3D.
|
264
|
+
*/
|
265
|
+
get sharedMeshes(): Mesh[] {
|
266
|
+
if (this.destroyed || !this.gameObject) return this._sharedMeshes;
|
267
|
+
this._sharedMeshes.length = 0;
|
268
|
+
if (this.gameObject.type === "Group") {
|
269
|
+
for (const ch of this.gameObject.children) {
|
270
|
+
if (ch.type === "Mesh" || ch.type === "SkinnedMesh") {
|
271
|
+
this._sharedMeshes.push(ch as Mesh);
|
272
|
+
}
|
273
|
+
}
|
274
|
+
}
|
275
|
+
else if (this.gameObject.type === "Mesh" || this.gameObject.type === "SkinnedMesh") {
|
276
|
+
this._sharedMeshes.push(this.gameObject as unknown as Mesh);
|
277
|
+
}
|
278
|
+
return this._sharedMeshes;
|
279
|
+
}
|
280
|
+
|
281
|
+
get sharedMaterial(): Material {
|
257
282
|
return this.sharedMaterials[0];
|
258
283
|
}
|
259
284
|
|
260
|
-
set sharedMaterial(mat:
|
285
|
+
set sharedMaterial(mat: Material) {
|
261
286
|
const cur = this.sharedMaterials[0];
|
262
287
|
if (cur === mat) return;
|
263
288
|
this.sharedMaterials[0] = mat;
|
@@ -265,12 +290,12 @@
|
|
265
290
|
}
|
266
291
|
|
267
292
|
/**@deprecated please use sharedMaterial */
|
268
|
-
get material():
|
293
|
+
get material(): Material {
|
269
294
|
return this.sharedMaterials[0];
|
270
295
|
}
|
271
296
|
|
272
297
|
/**@deprecated please use sharedMaterial */
|
273
|
-
set material(mat:
|
298
|
+
set material(mat: Material) {
|
274
299
|
this.sharedMaterial = mat;
|
275
300
|
}
|
276
301
|
|
@@ -455,12 +480,10 @@
|
|
455
480
|
|
456
481
|
private _isInstancingEnabled: boolean = false;
|
457
482
|
private handles: InstanceHandle[] | null | undefined = undefined;
|
458
|
-
private prevLayers: number[] | null | undefined = undefined;
|
459
483
|
|
460
484
|
private clearInstancingState() {
|
461
485
|
this._isInstancingEnabled = false;
|
462
486
|
this.handles = undefined;
|
463
|
-
this.prevLayers = undefined;
|
464
487
|
}
|
465
488
|
|
466
489
|
setInstancingEnabled(enabled: boolean): boolean {
|
@@ -519,6 +542,9 @@
|
|
519
542
|
}
|
520
543
|
|
521
544
|
onEnable() {
|
545
|
+
// ensure shared meshes are initialized
|
546
|
+
const _ = this.sharedMeshes;
|
547
|
+
|
522
548
|
this.setVisibility(true);
|
523
549
|
|
524
550
|
if (this._isInstancingEnabled) {
|
@@ -606,11 +632,7 @@
|
|
606
632
|
if (this._isInstancingEnabled && this.handles) {
|
607
633
|
for (let i = 0; i < this.handles.length; i++) {
|
608
634
|
const handle = this.handles[i];
|
609
|
-
|
610
|
-
const layer = handle.object.layers.mask;
|
611
|
-
if (i >= this.prevLayers.length) this.prevLayers.push(layer);
|
612
|
-
else this.prevLayers[i] = layer;
|
613
|
-
handle.object.layers.disableAll();
|
635
|
+
setCustomVisibility(handle.object, false);
|
614
636
|
}
|
615
637
|
}
|
616
638
|
|
@@ -677,10 +699,10 @@
|
|
677
699
|
}
|
678
700
|
|
679
701
|
onAfterRender() {
|
680
|
-
if (this._isInstancingEnabled && this.handles
|
702
|
+
if (this._isInstancingEnabled && this.handles) {
|
681
703
|
for (let i = 0; i < this.handles.length; i++) {
|
682
704
|
const handle = this.handles[i];
|
683
|
-
handle.object
|
705
|
+
setCustomVisibility(handle.object, true);
|
684
706
|
}
|
685
707
|
}
|
686
708
|
|
@@ -999,8 +1021,8 @@
|
|
999
1021
|
this.inst = new THREE.InstancedMesh(geo, material, count);
|
1000
1022
|
this.inst[$instancingAutoUpdateBounds] = true;
|
1001
1023
|
this.inst.count = 0;
|
1002
|
-
this.inst.layers.set(2);
|
1003
1024
|
this.inst.visible = true;
|
1025
|
+
this.context.scene.add(this.inst);
|
1004
1026
|
|
1005
1027
|
// Not handled by RawShaderMaterial, so we need to set the define explicitly.
|
1006
1028
|
// Edge case: theoretically some users of the material could use it in an
|
@@ -1015,25 +1037,24 @@
|
|
1015
1037
|
material.needsUpdate = true;
|
1016
1038
|
}
|
1017
1039
|
|
1018
|
-
// this.inst.castShadow = true;
|
1019
|
-
// this.inst.receiveShadow = true;
|
1020
|
-
this.context.scene.add(this.inst);
|
1021
1040
|
context.pre_render_callbacks.push(this.onBeforeRender);
|
1022
|
-
|
1023
|
-
// this.context.pre_render_callbacks.push(this.onPreRender.bind(this));
|
1024
|
-
|
1025
|
-
// setInterval(() => {
|
1026
|
-
// this.inst.visible = !this.inst.visible;
|
1027
|
-
// }, 500);
|
1041
|
+
context.post_render_callbacks.push(this.onAfterRender);
|
1028
1042
|
}
|
1029
1043
|
|
1030
1044
|
private onBeforeRender = () => {
|
1045
|
+
// ensure the instanced mesh is rendered / has correct layers
|
1046
|
+
this.inst.layers.enableAll();
|
1047
|
+
|
1031
1048
|
if (this._needUpdateBounds && this.inst[$instancingAutoUpdateBounds] === true) {
|
1032
1049
|
if (debugInstancing)
|
1033
1050
|
console.log("Update instancing bounds", this.name, this.inst.matrixWorldNeedsUpdate);
|
1034
1051
|
this.updateBounds();
|
1035
1052
|
}
|
1036
1053
|
}
|
1054
|
+
private onAfterRender = () => {
|
1055
|
+
// hide the instanced mesh again when its not being rendered (for raycasting we still use the original object)
|
1056
|
+
this.inst.layers.disableAll();
|
1057
|
+
}
|
1037
1058
|
|
1038
1059
|
private randomColor() {
|
1039
1060
|
return new THREE.Color(Math.random(), Math.random(), Math.random());
|
@@ -1076,7 +1097,7 @@
|
|
1076
1097
|
if (this.inst.count > 0)
|
1077
1098
|
this.inst.visible = true;
|
1078
1099
|
|
1079
|
-
|
1100
|
+
if (debugInstancing) console.log("Added", this.name, this.inst.count);
|
1080
1101
|
}
|
1081
1102
|
|
1082
1103
|
remove(handle: InstanceHandle) {
|
@@ -1116,6 +1137,7 @@
|
|
1116
1137
|
this.inst.visible = false;
|
1117
1138
|
|
1118
1139
|
this.inst.instanceMatrix.needsUpdate = true;
|
1140
|
+
if (debugInstancing) console.log("Removed", this.name, this.inst.count);
|
1119
1141
|
}
|
1120
1142
|
|
1121
1143
|
updateInstance(mat: THREE.Matrix4, index: number) {
|
@@ -1,4 +1,5 @@
|
|
1
|
-
import { Material, Mesh,
|
1
|
+
import { Material, Mesh, ShaderMaterial, Texture, Vector4,type WebGLProgramParametersWithUniforms } from "three";
|
2
|
+
|
2
3
|
import type { Context, OnRenderCallback } from "../engine/engine_setup.js";
|
3
4
|
import { getParam } from "../engine/engine_utils.js";
|
4
5
|
|
@@ -99,7 +100,7 @@
|
|
99
100
|
}
|
100
101
|
}
|
101
102
|
|
102
|
-
private onBeforeCompile = (shader:
|
103
|
+
private onBeforeCompile = (shader: WebGLProgramParametersWithUniforms, _) => {
|
103
104
|
if (debug) console.log("Lightmaps, before compile", shader)
|
104
105
|
//@ts-ignore
|
105
106
|
shader.lightMapUv = "uv1";
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import { Mathf } from "../../engine/engine_math.js";
|
2
1
|
import { Color } from "three";
|
3
2
|
|
3
|
+
import { Mathf } from "../../engine/engine_math.js";
|
4
|
+
|
4
5
|
export class RGBAColor extends Color {
|
5
6
|
alpha: number = 1;
|
6
7
|
|
@@ -1,13 +1,14 @@
|
|
1
|
-
import { Behaviour } from "./Component.js";
|
2
1
|
import * as THREE from 'three'
|
2
|
+
import { Matrix4, Object3D, Vector3 } from "three";
|
3
|
+
|
4
|
+
import { CollisionDetectionMode, RigidbodyConstraints } from "../engine/engine_physics.types.js";
|
5
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
6
|
+
import { Context, FrameEvent } from "../engine/engine_setup.js";
|
3
7
|
import { getWorldPosition } from "../engine/engine_three_utils.js";
|
4
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
-
import { Watch } from "../engine/engine_utils.js";
|
6
|
-
import { Matrix4, Object3D, Vector3 } from "three";
|
7
8
|
import type { IRigidbody, Vec3 } from "../engine/engine_types.js";
|
8
|
-
import { CollisionDetectionMode, RigidbodyConstraints } from "../engine/engine_physics.types.js";
|
9
9
|
import { validate } from "../engine/engine_util_decorator.js";
|
10
|
-
import {
|
10
|
+
import { Watch } from "../engine/engine_utils.js";
|
11
|
+
import { Behaviour } from "./Component.js";
|
11
12
|
|
12
13
|
class TransformWatch {
|
13
14
|
|
@@ -361,10 +362,17 @@
|
|
361
362
|
return this.context.physics.engine?.isSleeping(this);
|
362
363
|
}
|
363
364
|
|
365
|
+
/** Call to force an update of the rigidbody properties in the physics engine */
|
366
|
+
public updateProperties() {
|
367
|
+
this._propertiesChanged = false;
|
368
|
+
return this.context.physics.engine?.updateProperties(this);
|
369
|
+
}
|
370
|
+
|
364
371
|
/** Forces affect the rigid-body's acceleration whereas impulses affect the rigid-body's velocity
|
365
372
|
* the acceleration change is equal to the force divided by the mass:
|
366
373
|
* @link see https://rapier.rs/docs/user_guides/javascript/rigid_bodies#forces-and-impulses */
|
367
374
|
public applyForce(vec: Vector3 | Vec3, _rel?: THREE.Vector3, wakeup: boolean = true) {
|
375
|
+
if (this._propertiesChanged) this.updateProperties();
|
368
376
|
this.context.physics.engine?.addForce(this, vec, wakeup);
|
369
377
|
}
|
370
378
|
|
@@ -372,6 +380,7 @@
|
|
372
380
|
* the velocity change is equal to the impulse divided by the mass
|
373
381
|
* @link see https://rapier.rs/docs/user_guides/javascript/rigid_bodies#forces-and-impulses */
|
374
382
|
public applyImpulse(vec: Vector3 | Vec3, wakeup: boolean = true) {
|
383
|
+
if (this._propertiesChanged) this.updateProperties();
|
375
384
|
this.context.physics.engine?.applyImpulse(this, vec, wakeup);
|
376
385
|
}
|
377
386
|
|
@@ -1,11 +1,12 @@
|
|
1
|
+
import { Object3D } from "three";
|
2
|
+
|
1
3
|
import { AssetReference } from "../engine/engine_addressables.js";
|
4
|
+
import { registerObservableAttribute } from "../engine/engine_element_extras.js";
|
2
5
|
import { InputEvents } from "../engine/engine_input.js";
|
3
6
|
import { isLocalNetwork } from "../engine/engine_networking_utils.js";
|
7
|
+
import { serializable } from "../engine/engine_serialization.js";
|
4
8
|
import { delay, getParam, setParamWithoutReload } from "../engine/engine_utils.js";
|
5
|
-
import { serializable } from "../engine/engine_serialization.js";
|
6
9
|
import { Behaviour, GameObject } from "./Component.js";
|
7
|
-
import { registerObservableAttribute } from "../engine/engine_element_extras.js";
|
8
|
-
import { Object3D } from "three";
|
9
10
|
|
10
11
|
const debug = getParam("debugsceneswitcher");
|
11
12
|
|
@@ -125,9 +126,9 @@
|
|
125
126
|
|
126
127
|
async onEnable() {
|
127
128
|
globalThis.addEventListener("popstate", this.onPopState);
|
128
|
-
this.context.input.addEventListener(InputEvents.KeyDown, this.
|
129
|
-
this.context.input.addEventListener(InputEvents.PointerMove, this.
|
130
|
-
this.context.input.addEventListener(InputEvents.PointerUp, this.
|
129
|
+
this.context.input.addEventListener(InputEvents.KeyDown, this.onInputKeyDown);
|
130
|
+
this.context.input.addEventListener(InputEvents.PointerMove, this.onInputPointerMove);
|
131
|
+
this.context.input.addEventListener(InputEvents.PointerUp, this.onInputPointerUp);
|
131
132
|
|
132
133
|
if (!this._engineElementOverserver) {
|
133
134
|
this._engineElementOverserver = new MutationObserver((mutations) => {
|
@@ -172,9 +173,9 @@
|
|
172
173
|
|
173
174
|
onDisable(): void {
|
174
175
|
globalThis.removeEventListener("popstate", this.onPopState);
|
175
|
-
this.context.input.removeEventListener(InputEvents.KeyDown, this.
|
176
|
-
this.context.input.removeEventListener(InputEvents.PointerMove, this.
|
177
|
-
this.context.input.removeEventListener(InputEvents.PointerUp, this.
|
176
|
+
this.context.input.removeEventListener(InputEvents.KeyDown, this.onInputKeyDown);
|
177
|
+
this.context.input.removeEventListener(InputEvents.PointerMove, this.onInputPointerMove);
|
178
|
+
this.context.input.removeEventListener(InputEvents.PointerUp, this.onInputPointerUp);
|
178
179
|
this._preloadScheduler?.stop();
|
179
180
|
}
|
180
181
|
|
@@ -202,7 +203,7 @@
|
|
202
203
|
|
203
204
|
private normalizedSwipeThresholdX = 0.1;
|
204
205
|
private _didSwipe: boolean = false;
|
205
|
-
private
|
206
|
+
private onInputPointerMove = (e: any) => {
|
206
207
|
if (!this.useSwipe) return;
|
207
208
|
if (!this._didSwipe && e.button === 0 && e.pointerType === "touch" && this.context.input.getPointerPressedCount() === 1) {
|
208
209
|
const delta = this.context.input.getPointerPositionDelta(e.button);
|
@@ -220,13 +221,13 @@
|
|
220
221
|
}
|
221
222
|
}
|
222
223
|
|
223
|
-
private
|
224
|
+
private onInputPointerUp = (e: any) => {
|
224
225
|
if (e.button === 0) {
|
225
226
|
this._didSwipe = false;
|
226
227
|
}
|
227
228
|
};
|
228
229
|
|
229
|
-
private
|
230
|
+
private onInputKeyDown = (e: any) => {
|
230
231
|
if (!this.useKeyboard) return;
|
231
232
|
if (!this.scenes) return;
|
232
233
|
const key = e.key.toLowerCase();
|
@@ -1,7 +1,8 @@
|
|
1
1
|
|
2
2
|
import * as flatbuffers from "flatbuffers"
|
3
|
+
|
4
|
+
import { SyncedTransformModel } from "./synced-transform-model.js";
|
3
5
|
import { Transform } from "./transform.js";
|
4
|
-
import { SyncedTransformModel } from "./synced-transform-model.js";
|
5
6
|
|
6
7
|
// registry
|
7
8
|
export const binaryIdentifierCasts : {[key:string] : (bin:flatbuffers.ByteBuffer) => object} = {};
|
@@ -1,12 +1,12 @@
|
|
1
|
+
import { showBalloonWarning } from "../engine/debug/index.js";
|
2
|
+
import { RoomEvents } from "../engine/engine_networking.js";
|
3
|
+
import { disposeStream, NetworkedStreamEvents,NetworkedStreams, PeerHandle, StreamEndedEvent, StreamReceivedEvent } from "../engine/engine_networking_streams.js";
|
4
|
+
import { serializable } from "../engine/engine_serialization.js";
|
5
|
+
import { delay, getParam } from "../engine/engine_utils.js";
|
6
|
+
import { AudioSource } from "./AudioSource.js";
|
1
7
|
import { Behaviour, GameObject } from "./Component.js";
|
8
|
+
import type { IPointerClickHandler, PointerEventData } from "./ui/PointerEvents.js";
|
2
9
|
import { AspectMode, VideoPlayer } from "./VideoPlayer.js";
|
3
|
-
import { serializable } from "../engine/engine_serialization.js";
|
4
|
-
import type { IPointerClickHandler, PointerEventData } from "./ui/PointerEvents.js";
|
5
|
-
import { AudioSource } from "./AudioSource.js";
|
6
|
-
import { delay, getParam } from "../engine/engine_utils.js";
|
7
|
-
import { showBalloonWarning } from "../engine/debug/index.js";
|
8
|
-
import { NetworkedStreams, disposeStream, StreamReceivedEvent, StreamEndedEvent, PeerHandle, NetworkedStreamEvents } from "../engine/engine_networking_streams.js";
|
9
|
-
import { RoomEvents } from "../engine/engine_networking.js";
|
10
10
|
|
11
11
|
const debug = getParam("debugscreensharing");
|
12
12
|
|
@@ -146,6 +146,7 @@
|
|
146
146
|
delay(1000).then(() => {
|
147
147
|
if (this.enabled && this.autoConnect && !this.isReceiving && !this.isSending && this.context.connection.isInRoom)
|
148
148
|
this.share()
|
149
|
+
return 0;
|
149
150
|
});
|
150
151
|
}
|
151
152
|
}
|
@@ -181,7 +182,7 @@
|
|
181
182
|
if (this._activeShareRequest) return this._activeShareRequest;
|
182
183
|
this._activeShareRequest = this.internalShare(opts);
|
183
184
|
return this._activeShareRequest.then(() =>{
|
184
|
-
this._activeShareRequest = null;
|
185
|
+
return this._activeShareRequest = null;
|
185
186
|
})
|
186
187
|
}
|
187
188
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { BlendFunction, DepthDownsamplingPass, NormalPass, SSAOEffect } from "postprocessing";
|
2
2
|
import { Color, PerspectiveCamera } from "three";
|
3
|
+
|
3
4
|
import { serializable } from "../../../engine/engine_serialization.js";
|
4
5
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
5
6
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,10 +1,11 @@
|
|
1
|
+
import { N8AOPostPass } from "n8ao";
|
1
2
|
import { Color, NeverDepth, PerspectiveCamera } from "three";
|
3
|
+
|
2
4
|
import { serializable } from "../../../engine/engine_serialization.js";
|
5
|
+
import { validate } from "../../../engine/engine_util_decorator.js";
|
3
6
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
4
7
|
import { VolumeParameter } from "../VolumeParameter.js";
|
5
8
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
6
|
-
import { N8AOPostPass } from "n8ao";
|
7
|
-
import { validate } from "../../../engine/engine_util_decorator.js";
|
8
9
|
|
9
10
|
// https://github.com/N8python/n8ao
|
10
11
|
|
@@ -1,8 +1,9 @@
|
|
1
|
+
import { AdditiveBlending, Material, Mesh, MeshBasicMaterial, MeshStandardMaterial,ShadowMaterial } from "three";
|
2
|
+
|
3
|
+
import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
|
4
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
1
5
|
import { Behaviour, GameObject } from "./Component.js";
|
2
6
|
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
3
|
-
import { ShadowMaterial, AdditiveBlending, Material, MeshBasicMaterial, Mesh, MeshStandardMaterial } from "three";
|
4
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
-
import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
|
6
7
|
import { Renderer } from "./Renderer.js";
|
7
8
|
|
8
9
|
enum ShadowMode {
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import { EventList } from "../EventList.js";
|
2
|
-
import { Behaviour } from "../Component.js";
|
3
1
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
4
2
|
import { getParam } from "../../engine/engine_utils.js";
|
3
|
+
import { Behaviour } from "../Component.js";
|
4
|
+
import { EventList } from "../EventList.js";
|
5
5
|
|
6
6
|
const debug = getParam("debugsignals")
|
7
7
|
|
@@ -1,15 +1,16 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
1
|
+
import { EquirectangularRefractionMapping, NeverDepth, SRGBColorSpace, sRGBEncoding, Texture, TextureLoader } from "three"
|
2
|
+
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
|
3
3
|
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
|
4
|
-
|
5
|
-
import {
|
6
|
-
import { syncField } from "../engine/engine_networking_auto.js";
|
7
|
-
import { Camera, ClearFlags } from "./Camera.js";
|
8
|
-
import { PromiseAllWithErrors, addAttributeChangeCallback, getParam, removeAttributeChangeCallback } from "../engine/engine_utils.js";
|
4
|
+
|
5
|
+
import { disposeObjectResources, setDisposable } from "../engine/engine_assetdatabase.js";
|
9
6
|
import { ContextRegistry } from "../engine/engine_context_registry.js";
|
10
7
|
import { registerObservableAttribute } from "../engine/engine_element_extras.js";
|
8
|
+
import { syncField } from "../engine/engine_networking_auto.js";
|
9
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
11
10
|
import { type IContext } from "../engine/engine_types.js";
|
12
|
-
import {
|
11
|
+
import { addAttributeChangeCallback, getParam, PromiseAllWithErrors, removeAttributeChangeCallback } from "../engine/engine_utils.js";
|
12
|
+
import { Camera, ClearFlags } from "./Camera.js";
|
13
|
+
import { Behaviour, GameObject } from "./Component.js";
|
13
14
|
|
14
15
|
const debug = getParam("debugskybox");
|
15
16
|
|
@@ -92,7 +93,7 @@
|
|
92
93
|
const entry = cache.shift();
|
93
94
|
if (entry) { disposeCachedTexture(entry.texture); }
|
94
95
|
}
|
95
|
-
texture.then(t => setDisposable(t, false));
|
96
|
+
texture.then(t => { return setDisposable(t, false) });
|
96
97
|
cache.push({ src, texture });
|
97
98
|
}
|
98
99
|
|
@@ -1,11 +1,12 @@
|
|
1
|
-
import { Camera } from "./Camera.js";
|
2
|
-
import { Behaviour, GameObject } from "./Component.js";
|
3
1
|
import * as THREE from "three";
|
2
|
+
import { Object3D } from "three";
|
3
|
+
|
4
4
|
import { Mathf } from "../engine/engine_math.js";
|
5
|
+
import { Axes } from "../engine/engine_physics.types.js";
|
5
6
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
6
|
-
import { Object3D } from "three";
|
7
7
|
import { getWorldPosition, getWorldQuaternion } from "../engine/engine_three_utils.js";
|
8
|
-
import {
|
8
|
+
import { Camera } from "./Camera.js";
|
9
|
+
import { Behaviour, GameObject } from "./Component.js";
|
9
10
|
|
10
11
|
export class SmoothFollow extends Behaviour {
|
11
12
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import * as THREE from 'three'
|
2
2
|
import { HTMLMesh } from 'three/examples/jsm/interactive/HTMLMesh.js';
|
3
3
|
import { InteractiveGroup } from 'three/examples/jsm/interactive/InteractiveGroup.js';
|
4
|
+
|
4
5
|
import { getWorldEuler, getWorldRotation, setWorldRotationXYZ } from '../../engine/engine_three_utils.js';
|
5
6
|
import { Behaviour } from '../Component.js';
|
6
7
|
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import { BoxHelper, Layers } from "three";
|
2
|
+
|
3
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
|
+
import { getParam } from "../engine/engine_utils.js";
|
5
|
+
import { BoxHelperComponent } from "./BoxHelperComponent.js"
|
2
6
|
import { Behaviour, GameObject } from "./Component.js";
|
3
|
-
import { BoxHelperComponent } from "./BoxHelperComponent.js"
|
4
7
|
import { EventList } from "./EventList.js";
|
5
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
6
|
-
import { getParam } from "../engine/engine_utils.js";
|
7
8
|
|
8
9
|
const debug = getParam("debugspatialtrigger");
|
9
10
|
|
@@ -1,21 +1,21 @@
|
|
1
|
-
import { Behaviour, Component, GameObject } from "./Component.js";
|
2
|
-
import { Camera } from "./Camera.js";
|
3
1
|
import * as THREE from "three";
|
4
|
-
import { OrbitControls } from "./OrbitControls.js";
|
5
|
-
import { WebXR, WebXREvent } from "./webxr/WebXR.js";
|
6
|
-
import { AvatarMarker } from "./webxr/WebXRAvatar.js";
|
7
|
-
import { XRStateFlag } from "./XRFlag.js";
|
8
|
-
import { SmoothFollow } from "./SmoothFollow.js";
|
9
2
|
import { Object3D } from "three";
|
3
|
+
|
10
4
|
import { InputEvents } from "../engine/engine_input.js";
|
11
|
-
import { Context } from "../engine/engine_setup.js";
|
12
|
-
import { getParam } from "../engine/engine_utils.js";
|
13
|
-
import { PlayerView, ViewDevice } from "../engine/engine_playerview.js";
|
14
|
-
import { RaycastOptions } from "../engine/engine_physics.js";
|
15
5
|
import { RoomEvents } from "../engine/engine_networking.js";
|
16
|
-
import type { ICamera } from "../engine/engine_types.js";
|
17
6
|
import type { IModel } from "../engine/engine_networking_types.js";
|
7
|
+
import { RaycastOptions } from "../engine/engine_physics.js";
|
8
|
+
import { PlayerView, ViewDevice } from "../engine/engine_playerview.js";
|
18
9
|
import { serializable } from "../engine/engine_serialization.js";
|
10
|
+
import { Context } from "../engine/engine_setup.js";
|
11
|
+
import type { ICamera } from "../engine/engine_types.js";
|
12
|
+
import { getParam } from "../engine/engine_utils.js";
|
13
|
+
import { Camera } from "./Camera.js";
|
14
|
+
import { Behaviour, Component, GameObject } from "./Component.js";
|
15
|
+
import { OrbitControls } from "./OrbitControls.js";
|
16
|
+
import { SmoothFollow } from "./SmoothFollow.js";
|
17
|
+
import { AvatarMarker } from "./webxr/WebXRAvatar.js";
|
18
|
+
import { XRStateFlag } from "./webxr/XRFlag.js";
|
19
19
|
|
20
20
|
|
21
21
|
export enum SpectatorMode {
|
@@ -145,23 +145,11 @@
|
|
145
145
|
if (!this._handler && this.cam)
|
146
146
|
this._handler = new SpectatorHandler(this.context, this.cam, this);
|
147
147
|
|
148
|
-
|
149
|
-
this.eventSub_WebXRRequestStartEvent = this.onXRSessionRequestStart.bind(this);
|
150
|
-
this.eventSub_WebXRStartEvent = this.onXRSessionStart.bind(this);
|
151
|
-
this.eventSub_WebXREndEvent = this.onXRSessionEnded.bind(this);
|
152
|
-
|
153
|
-
WebXR.addEventListener(WebXREvent.RequestVRSession, this.eventSub_WebXRRequestStartEvent);
|
154
|
-
WebXR.addEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
|
155
|
-
WebXR.addEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
|
156
|
-
|
157
148
|
this.orbit = GameObject.getComponent(this.context.mainCamera, OrbitControls);
|
158
149
|
}
|
159
150
|
|
160
151
|
onDestroy(): void {
|
161
152
|
this.stopSpectating();
|
162
|
-
WebXR.removeEventListener(WebXREvent.RequestVRSession, this.eventSub_WebXRStartEvent);
|
163
|
-
WebXR.removeEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
|
164
|
-
WebXR.removeEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
|
165
153
|
this._handler?.destroy();
|
166
154
|
this._networking?.destroy();
|
167
155
|
}
|
@@ -173,13 +161,13 @@
|
|
173
161
|
return standalone && !isHololens;
|
174
162
|
}
|
175
163
|
|
176
|
-
|
164
|
+
onBeforeXR(_evt) {
|
177
165
|
if (!this.isSupportedPlatform()) return;
|
178
166
|
GameObject.setActive(this.gameObject, true);
|
179
167
|
}
|
180
168
|
|
181
169
|
|
182
|
-
|
170
|
+
onEnterXR(_evt) {
|
183
171
|
if (!this.isSupportedPlatform()) return;
|
184
172
|
if (debug) console.log(this.context.mainCamera);
|
185
173
|
if (this.context.mainCamera) {
|
@@ -187,7 +175,7 @@
|
|
187
175
|
}
|
188
176
|
}
|
189
177
|
|
190
|
-
|
178
|
+
onLeaveXR(_evt) {
|
191
179
|
this.context.removeCamera(this.cam as ICamera);
|
192
180
|
GameObject.setActive(this.gameObject, false);
|
193
181
|
if (this.orbit) this.orbit.enabled = true;
|
@@ -224,14 +212,16 @@
|
|
224
212
|
const previousRenderTarget = renderer.getRenderTarget();
|
225
213
|
let oldFramebuffer: WebGLFramebuffer | null = null;
|
226
214
|
|
215
|
+
const webglState = renderer.state as THREE.WebGLState & { bindXRFramebuffer?: Function };
|
216
|
+
|
227
217
|
// seems that in some cases, renderer.getRenderTarget returns null
|
228
218
|
// even when we're rendering to a headset.
|
229
219
|
if (!previousRenderTarget) {
|
230
|
-
if (!renderer.state.bindFramebuffer || !
|
220
|
+
if (!renderer.state.bindFramebuffer || !webglState.bindXRFramebuffer)
|
231
221
|
return;
|
232
222
|
|
233
223
|
oldFramebuffer = renderer["_framebuffer"];
|
234
|
-
|
224
|
+
webglState.bindXRFramebuffer(null);
|
235
225
|
}
|
236
226
|
|
237
227
|
this.setAvatarFlagsBeforeRender();
|
@@ -279,8 +269,8 @@
|
|
279
269
|
|
280
270
|
if (previousRenderTarget)
|
281
271
|
renderer.setRenderTarget(previousRenderTarget);
|
282
|
-
else
|
283
|
-
|
272
|
+
else if (webglState.bindXRFramebuffer)
|
273
|
+
webglState.bindXRFramebuffer(oldFramebuffer);
|
284
274
|
|
285
275
|
this.resetAvatarFlags();
|
286
276
|
}
|
@@ -289,7 +279,7 @@
|
|
289
279
|
const isFirstPersonMode = this._mode === SpectatorMode.FirstPerson;
|
290
280
|
|
291
281
|
for (const av of AvatarMarker.instances) {
|
292
|
-
if (av.avatar && "isLocalAvatar" in av.avatar) {
|
282
|
+
if (av.avatar && "isLocalAvatar" in av.avatar && "flags" in av.avatar) {
|
293
283
|
let mask = XRStateFlag.All;
|
294
284
|
if (this.isSpectatingSelf)
|
295
285
|
mask = isFirstPersonMode && av.avatar.isLocalAvatar ? XRStateFlag.FirstPerson : XRStateFlag.ThirdPerson;
|
@@ -308,7 +298,7 @@
|
|
308
298
|
const flags = av.avatar.flags;
|
309
299
|
if (!flags) continue;
|
310
300
|
for (const flag of flags) {
|
311
|
-
if (av.avatar?.isLocalAvatar) {
|
301
|
+
if ("isLocalAvatar" in av.avatar && av.avatar?.isLocalAvatar) {
|
312
302
|
flag.UpdateVisible(XRStateFlag.FirstPerson);
|
313
303
|
}
|
314
304
|
else {
|
@@ -1,9 +1,10 @@
|
|
1
|
-
import { Behaviour } from "./Component.js";
|
2
1
|
import * as THREE from "three";
|
2
|
+
import { Material, NearestFilter, Texture } from "three";
|
3
|
+
|
3
4
|
import { serializable, serializeable } from "../engine/engine_serialization_decorator.js";
|
4
|
-
import {
|
5
|
+
import { getParam } from "../engine/engine_utils.js";
|
6
|
+
import { Behaviour } from "./Component.js";
|
5
7
|
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
6
|
-
import { getParam } from "../engine/engine_utils.js";
|
7
8
|
|
8
9
|
const debug = getParam("debugspriterenderer");
|
9
10
|
const showWireframe = getParam("wireframe");
|
@@ -1,20 +1,20 @@
|
|
1
|
+
import { Builder } from "flatbuffers";
|
2
|
+
import { Object3D } from "three";
|
3
|
+
|
4
|
+
import { isDevEnvironment } from "../engine/debug/index.js";
|
5
|
+
import { AssetReference } from "../engine/engine_addressables.js";
|
6
|
+
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
7
|
+
import { InstancingUtil } from "../engine/engine_instancing.js";
|
1
8
|
import { NetworkConnection } from "../engine/engine_networking.js";
|
2
|
-
import {
|
3
|
-
import {
|
9
|
+
import { ViewDevice } from "../engine/engine_playerview.js";
|
10
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
11
|
import * as utils from "../engine/engine_three_utils.js"
|
5
|
-
import {
|
6
|
-
import { Builder } from "flatbuffers";
|
12
|
+
import { registerBinaryType } from "../engine-schemes/schemes.js";
|
7
13
|
import { SyncedCameraModel } from "../engine-schemes/synced-camera-model.js";
|
8
14
|
import { Vec3 } from "../engine-schemes/vec3.js";
|
9
|
-
import {
|
10
|
-
import {
|
11
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
12
|
-
import { Object3D } from "three";
|
15
|
+
import { Camera } from "./Camera.js";
|
16
|
+
import { Behaviour, GameObject } from "./Component.js";
|
13
17
|
import { AvatarMarker } from "./webxr/WebXRAvatar.js";
|
14
|
-
import { AssetReference } from "../engine/engine_addressables.js";
|
15
|
-
import { ViewDevice } from "../engine/engine_playerview.js";
|
16
|
-
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
17
|
-
import { isDevEnvironment } from "../engine/debug/index.js";
|
18
18
|
|
19
19
|
const SyncedCameraModelIdentifier = "SCAM";
|
20
20
|
registerBinaryType(SyncedCameraModelIdentifier, SyncedCameraModel.getRootAsSyncedCameraModel);
|
@@ -130,7 +130,7 @@
|
|
130
130
|
}
|
131
131
|
}
|
132
132
|
|
133
|
-
if (
|
133
|
+
if (this.context.isInXR) return;
|
134
134
|
|
135
135
|
const cam = this.context.mainCamera
|
136
136
|
if (cam === null) {
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import {
|
1
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
2
2
|
import * as utils from "../engine/engine_utils.js"
|
3
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
3
|
import { getParam } from "../engine/engine_utils.js";
|
4
|
+
import { Behaviour } from "./Component.js";
|
5
5
|
|
6
6
|
const viewParamName = "view";
|
7
7
|
const debug = utils.getParam("debugsyncedroom");
|
@@ -1,15 +1,17 @@
|
|
1
|
+
import * as flatbuffers from "flatbuffers";
|
1
2
|
import * as THREE from 'three'
|
3
|
+
|
4
|
+
import { InstancingUtil } from "../engine/engine_instancing.js";
|
5
|
+
import { onUpdate } from '../engine/engine_lifecycle_api.js';
|
2
6
|
import { OwnershipModel, RoomEvents } from "../engine/engine_networking.js"
|
3
|
-
import {
|
4
|
-
import {
|
7
|
+
import { sendDestroyed } from '../engine/engine_networking_instantiate.js';
|
8
|
+
import { setWorldEuler } from '../engine/engine_three_utils.js';
|
5
9
|
import * as utils from "../engine/engine_utils.js"
|
6
|
-
import {
|
7
|
-
import { InstancingUtil } from "../engine/engine_instancing.js";
|
10
|
+
import { registerBinaryType } from '../engine-schemes/schemes.js';
|
8
11
|
import { SyncedTransformModel } from '../engine-schemes/synced-transform-model.js';
|
9
|
-
import * as flatbuffers from "flatbuffers";
|
10
12
|
import { Transform } from '../engine-schemes/transform.js';
|
11
|
-
import {
|
12
|
-
import {
|
13
|
+
import { Behaviour, GameObject } from "./Component.js";
|
14
|
+
import { Rigidbody } from "./RigidBody.js";
|
13
15
|
|
14
16
|
const debug = utils.getParam("debugsync");
|
15
17
|
export const SyncedTransformIdentifier = "STRS";
|
@@ -35,8 +37,19 @@
|
|
35
37
|
}
|
36
38
|
|
37
39
|
|
40
|
+
let FAST_ACTIVE_SYNCTRANSFORMS = 0;
|
41
|
+
let FAST_INTERVAL = 0;
|
42
|
+
onUpdate((ctx) => {
|
43
|
+
const isRunningOnGlitch = ctx.connection.currentServerUrl?.includes("glitch");
|
44
|
+
const threshold = isRunningOnGlitch ? 10 : 40;
|
45
|
+
FAST_INTERVAL = Math.floor(FAST_ACTIVE_SYNCTRANSFORMS / threshold);
|
46
|
+
FAST_ACTIVE_SYNCTRANSFORMS = 0;
|
47
|
+
if(debug && FAST_INTERVAL > 0) console.log("Sync Transform Fast Interval", FAST_INTERVAL);
|
48
|
+
})
|
49
|
+
|
38
50
|
export class SyncedTransform extends Behaviour {
|
39
51
|
|
52
|
+
|
40
53
|
// public autoOwnership: boolean = true;
|
41
54
|
public overridePhysics: boolean = true
|
42
55
|
public interpolatePosition: boolean = true;
|
@@ -57,6 +70,7 @@
|
|
57
70
|
private _receivedFastUpdate: boolean = false;
|
58
71
|
private _shouldRequestOwnership: boolean = false;
|
59
72
|
|
73
|
+
/** Request ownership of an object - you need to be connected to a room */
|
60
74
|
public requestOwnership() {
|
61
75
|
if (debug)
|
62
76
|
console.log("Request ownership");
|
@@ -292,8 +306,12 @@
|
|
292
306
|
|
293
307
|
const updateInterval = 10;
|
294
308
|
const fastUpdate = this.rb || this.fastMode;
|
309
|
+
|
295
310
|
if (this._needsUpdate && (updateInterval <= 0 || updateInterval > 0 && this.context.time.frameCount % updateInterval === 0 || fastUpdate)) {
|
296
311
|
|
312
|
+
FAST_ACTIVE_SYNCTRANSFORMS++;
|
313
|
+
if (fastUpdate && FAST_INTERVAL > 0 && this.context.time.frameCount % FAST_INTERVAL !== 0) return;
|
314
|
+
|
297
315
|
if (debug)
|
298
316
|
console.log("send update", this.context.connection.connectionId, this.guid, this.gameObject.name, this.gameObject.guid);
|
299
317
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
|
+
import { noVoip } from "../../engine-components/Voip.js";
|
2
3
|
import * as utils from "../engine_utils.js";
|
3
|
-
import { noVoip } from "../../engine-components/Voip.js";
|
4
4
|
|
5
5
|
|
6
6
|
export function detect_run_tests(){
|
@@ -1,11 +1,12 @@
|
|
1
|
-
import
|
1
|
+
import * as flatbuffers from 'flatbuffers';
|
2
|
+
import { Vector3 } from "three";
|
3
|
+
|
4
|
+
import type { IModel } from "../engine/engine_networking_types.js";
|
2
5
|
import * as tests from "../engine/tests/test_utils.js";
|
3
|
-
import { createTransformModel, SyncedTransform, SyncedTransformIdentifier } from "./SyncedTransform.js";
|
4
|
-
import * as flatbuffers from 'flatbuffers';
|
5
6
|
import { SyncedTransformModel } from "../engine-schemes/synced-transform-model.js";
|
7
|
+
import { Behaviour } from "./Component.js";
|
6
8
|
import { Rigidbody } from "./RigidBody.js";
|
7
|
-
import {
|
8
|
-
import type { IModel } from "../engine/engine_networking_types.js";
|
9
|
+
import { createTransformModel, SyncedTransform, SyncedTransformIdentifier } from "./SyncedTransform.js";
|
9
10
|
|
10
11
|
export class TestRunner extends Behaviour {
|
11
12
|
awake(): void {
|
@@ -1,12 +1,13 @@
|
|
1
|
-
import {
|
1
|
+
import { Color } from 'three';
|
2
2
|
import * as ThreeMeshUI from 'three-mesh-ui'
|
3
3
|
import type { DocumentedOptions as ThreeMeshUIEveryOptions } from "three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js";
|
4
|
-
|
5
|
-
import { updateRenderSettings } from './Utils.js';
|
6
|
-
import { Canvas } from './Canvas.js';
|
4
|
+
|
7
5
|
import { serializable } from '../../engine/engine_serialization_decorator.js';
|
8
6
|
import { getParam } from '../../engine/engine_utils.js';
|
7
|
+
import { Canvas } from './Canvas.js';
|
8
|
+
import { Graphic } from './Graphic.js';
|
9
9
|
import { type ICanvas, type ICanvasEventReceiver, type IHasAlphaFactor } from './Interfaces.js';
|
10
|
+
import { updateRenderSettings } from './Utils.js';
|
10
11
|
|
11
12
|
const debug = getParam("debugtext");
|
12
13
|
|
@@ -313,12 +314,12 @@
|
|
313
314
|
const child = this.uiObject.children[i];
|
314
315
|
// @ts-ignore
|
315
316
|
if (child.isUI) {
|
316
|
-
this.uiObject.remove(child);
|
317
|
+
this.uiObject.remove(child as any);
|
317
318
|
child.clear();
|
318
319
|
}
|
319
320
|
}
|
320
321
|
const el = new ThreeMeshUI.Inline({ textContent: text.substring(0, currentTag.startIndex), color: 'inherit' });
|
321
|
-
this.uiObject.add(el);
|
322
|
+
this.uiObject.add(el as any);
|
322
323
|
}
|
323
324
|
|
324
325
|
const stackArray: Array<TagStackEntry> = [];
|
@@ -335,13 +336,13 @@
|
|
335
336
|
opts.textContent = this.getText(text, currentTag, next);
|
336
337
|
this.handleTag(currentTag, opts, stackArray);
|
337
338
|
const el = new ThreeMeshUI.Inline(opts);
|
338
|
-
this.uiObject?.add(el)
|
339
|
+
this.uiObject?.add(el as any)
|
339
340
|
|
340
341
|
} else {
|
341
342
|
opts.textContent = text.substring(currentTag.endIndex);
|
342
343
|
this.handleTag(currentTag, opts, stackArray);
|
343
344
|
const el = new ThreeMeshUI.Inline(opts);
|
344
|
-
this.uiObject?.add(el);
|
345
|
+
this.uiObject?.add(el as any);
|
345
346
|
}
|
346
347
|
currentTag = next;
|
347
348
|
}
|
@@ -1,36 +1,37 @@
|
|
1
|
-
import { Renderer } from '../../Renderer.js';
|
2
|
-
import { GameObject } from '../../Component.js';
|
3
|
-
import type { OffscreenCanvasExt } from '../../../engine/engine_shims.js';
|
4
1
|
import '../../../engine/engine_shims.js';
|
2
|
+
|
5
3
|
import {
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
WebGLRenderer,
|
4
|
+
AnimationClip,
|
5
|
+
Bone,
|
6
|
+
BufferAttribute,
|
7
|
+
BufferGeometry,
|
8
|
+
Color,
|
9
|
+
DoubleSide,
|
10
|
+
Material,
|
14
11
|
MathUtils,
|
15
12
|
Matrix4,
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
Color,
|
13
|
+
Mesh,
|
14
|
+
MeshBasicMaterial,
|
15
|
+
MeshPhysicalMaterial,
|
20
16
|
MeshStandardMaterial,
|
21
|
-
MeshPhysicalMaterial,
|
22
17
|
Object3D,
|
23
|
-
|
24
|
-
|
18
|
+
OrthographicCamera,
|
19
|
+
PerspectiveCamera,
|
20
|
+
PlaneGeometry,
|
21
|
+
Scene,
|
22
|
+
ShaderMaterial,
|
25
23
|
SkinnedMesh,
|
26
24
|
SRGBColorSpace,
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
} from 'three';
|
25
|
+
Texture,
|
26
|
+
Uniform,
|
27
|
+
Vector4,
|
28
|
+
WebGLRenderer} from 'three';
|
32
29
|
import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
|
33
30
|
|
31
|
+
import type { OffscreenCanvasExt } from '../../../engine/engine_shims.js';
|
32
|
+
import { GameObject } from '../../Component.js';
|
33
|
+
import { Renderer } from '../../Renderer.js';
|
34
|
+
|
34
35
|
function makeNameSafe( str ) {
|
35
36
|
str = str.replace( /[^a-zA-Z0-9_]/g, '' );
|
36
37
|
|
@@ -1080,7 +1081,9 @@
|
|
1080
1081
|
|
1081
1082
|
if ( geometry ) {
|
1082
1083
|
writer.beginBlock( `def ${objType} "${name}"`, "(", false );
|
1083
|
-
|
1084
|
+
// NE-4084: To use the doubleSided workaround with skeletal meshes we'd have to
|
1085
|
+
// also emit extra data for jointIndices etc., so we're skipping skinned meshes here.
|
1086
|
+
if (context.quickLookCompatible && material && material.side === DoubleSide && !isSkinnedMesh)
|
1084
1087
|
writer.appendLine(`prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry_doubleSided>`);
|
1085
1088
|
else
|
1086
1089
|
writer.appendLine(`prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>`);
|
@@ -1758,17 +1761,17 @@
|
|
1758
1761
|
];
|
1759
1762
|
|
1760
1763
|
export {
|
1761
|
-
USDZExporter,
|
1762
|
-
USDZExporterContext,
|
1763
|
-
USDWriter,
|
1764
|
-
USDObject,
|
1765
1764
|
buildMatrix,
|
1765
|
+
decompressGpuTexture,
|
1766
|
+
findStructuralNodesInBoneHierarchy,
|
1766
1767
|
getBoneName,
|
1767
1768
|
getPathToSkeleton,
|
1769
|
+
imageToCanvas,
|
1770
|
+
makeNameSafe as makeNameSafeForUSD,
|
1771
|
+
USDDocument,
|
1768
1772
|
fn as usdNumberFormatting,
|
1769
|
-
|
1770
|
-
|
1771
|
-
|
1772
|
-
|
1773
|
-
findStructuralNodesInBoneHierarchy,
|
1773
|
+
USDObject,
|
1774
|
+
USDWriter,
|
1775
|
+
USDZExporter,
|
1776
|
+
USDZExporterContext,
|
1774
1777
|
};
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import {
|
1
|
+
import { KernelSize, TiltShiftEffect as TiltShift } from "postprocessing";
|
2
|
+
|
3
|
+
import { serializable } from "../../../engine/engine_serialization.js";
|
2
4
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
3
|
-
import { KernelSize, TiltShiftEffect as TiltShift } from "postprocessing";
|
4
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
5
|
-
import {
|
6
|
+
import { registerCustomEffectType } from "../VolumeProfile.js";
|
6
7
|
|
7
8
|
|
8
9
|
export class TiltShiftEffect extends PostProcessingEffect {
|
@@ -1,14 +1,15 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import {
|
1
|
+
import { AnimationAction, AnimationClip, AnimationMixer, Audio, AudioListener, AudioLoader, Euler, Object3D, Quaternion, QuaternionKeyframeTrack, Vector3, VectorKeyframeTrack } from "three";
|
2
|
+
|
3
|
+
import { isDevEnvironment } from "../../engine/debug/index.js";
|
4
4
|
import { Context } from "../../engine/engine_setup.js";
|
5
|
-
import { SignalReceiver } from "./SignalAsset.js";
|
6
|
-
import { Audio, AudioListener, AnimationAction, AnimationClip, AnimationMixer, AudioLoader, Euler, Object3D, Quaternion, QuaternionKeyframeTrack, Vector3, VectorKeyframeTrack } from "three";
|
7
5
|
import { getParam, resolveUrl } from "../../engine/engine_utils.js";
|
6
|
+
import { setObjectAnimated } from "../AnimationUtils.js";
|
7
|
+
import { Animator } from "../Animator.js"
|
8
8
|
import { AudioSource } from "../AudioSource.js";
|
9
|
-
import {
|
10
|
-
import {
|
11
|
-
import {
|
9
|
+
import { GameObject } from "../Component.js";
|
10
|
+
import { PlayableDirector } from "./PlayableDirector.js";
|
11
|
+
import { SignalReceiver } from "./SignalAsset.js";
|
12
|
+
import * as Models from "./TimelineModels.js";
|
12
13
|
|
13
14
|
const debug = getParam("debugtimeline");
|
14
15
|
|
@@ -563,14 +564,16 @@
|
|
563
564
|
|
564
565
|
const muteAudioTracks = getParam("mutetimeline");
|
565
566
|
|
567
|
+
declare type AudioClipModel = Models.ClipModel & { _didTriggerPlay: boolean };
|
568
|
+
|
566
569
|
export class AudioTrackHandler extends TrackHandler {
|
567
570
|
|
568
|
-
models: Array<
|
571
|
+
models: Array<AudioClipModel> = [];
|
569
572
|
listener!: AudioListener;
|
570
573
|
audio: Array<Audio> = [];
|
571
574
|
audioContextTimeOffset: Array<number> = [];
|
572
575
|
lastTime: number = 0;
|
573
|
-
audioSource?:AudioSource;
|
576
|
+
audioSource?: AudioSource;
|
574
577
|
|
575
578
|
private _audioLoader: AudioLoader | null = null;
|
576
579
|
|
@@ -591,7 +594,9 @@
|
|
591
594
|
addModel(model: Models.ClipModel) {
|
592
595
|
const audio = new Audio(this.listener as any);
|
593
596
|
this.audio.push(audio);
|
594
|
-
|
597
|
+
const audioClipModel = model as AudioClipModel;
|
598
|
+
audioClipModel._didTriggerPlay = false;
|
599
|
+
this.models.push(audioClipModel);
|
595
600
|
}
|
596
601
|
|
597
602
|
onDisable() {
|
@@ -599,6 +604,9 @@
|
|
599
604
|
if (audio.isPlaying)
|
600
605
|
audio.stop();
|
601
606
|
}
|
607
|
+
for (const model of this.models) {
|
608
|
+
model._didTriggerPlay = false;
|
609
|
+
}
|
602
610
|
}
|
603
611
|
|
604
612
|
onDestroy() {
|
@@ -626,8 +634,23 @@
|
|
626
634
|
if (audio?.isPlaying)
|
627
635
|
audio.stop();
|
628
636
|
}
|
637
|
+
for (const model of this.models) {
|
638
|
+
model._didTriggerPlay = false;
|
639
|
+
}
|
629
640
|
}
|
630
641
|
|
642
|
+
private _playableDirectorResumed = false;
|
643
|
+
onPauseChanged() {
|
644
|
+
// if the timeline gets paused we stop all audio clips
|
645
|
+
// we dont reset the triggerPlay here (this will automatically reset when the timeline start evaluating again)
|
646
|
+
for (let i = 0; i < this.audio.length; i++) {
|
647
|
+
const audio = this.audio[i];
|
648
|
+
if (audio?.isPlaying)
|
649
|
+
audio.stop();
|
650
|
+
}
|
651
|
+
this._playableDirectorResumed = this.director.isPlaying;
|
652
|
+
}
|
653
|
+
|
631
654
|
evaluate(time: number) {
|
632
655
|
if (muteAudioTracks) return;
|
633
656
|
if (this.track.muted) return;
|
@@ -636,6 +659,8 @@
|
|
636
659
|
return;
|
637
660
|
}
|
638
661
|
const isMuted = this.director.context.application.muted;
|
662
|
+
const resumePlay = this._playableDirectorResumed;
|
663
|
+
this._playableDirectorResumed = false;
|
639
664
|
// this is just so that we dont hear the very first beat when the audio starts but is muted
|
640
665
|
// if we dont add a delay we hear a little bit of the audio before it shuts down
|
641
666
|
// MAYBE instead of doing it like this we should connect a custom audio node (or disconnect the output node?)
|
@@ -653,15 +678,24 @@
|
|
653
678
|
audio.playbackRate = this.director.context.time.timeScale * this.director.speed;
|
654
679
|
audio.loop = asset.loop;
|
655
680
|
if (time >= model.start && time <= model.end && time < this.director.duration) {
|
656
|
-
if (this.director.isPlaying
|
657
|
-
if
|
658
|
-
|
659
|
-
|
681
|
+
if (!audio.isPlaying || !this.director.isPlaying) {
|
682
|
+
// if the timeline is paused we trigger the audio clip once when the model is entered
|
683
|
+
// we dont playback the audio clip if we scroll back in time
|
684
|
+
// this is to support audioclip playback when using timeline with manual scrolling (scrollytelling)
|
685
|
+
if (resumePlay || (!model._didTriggerPlay && this.lastTime < time)) {
|
686
|
+
// we don't want to clip in the audio if it's a very short clip
|
687
|
+
const clipDuration = model.duration * model.timeScale;
|
688
|
+
if (clipDuration > .3)
|
689
|
+
audio.offset = model.clipIn + (time - model.start) * model.timeScale;
|
690
|
+
else audio.offset = 0;
|
691
|
+
if (debug) console.log("Timeline Audio (" + this.track.name + ") play with offset " + audio.offset + " - " + model.asset.clip);
|
692
|
+
audio.play(playTimeOffset);
|
693
|
+
model._didTriggerPlay = true;
|
694
|
+
}
|
695
|
+
else {
|
696
|
+
// do nothing...
|
697
|
+
}
|
660
698
|
}
|
661
|
-
else if (!audio.isPlaying) {
|
662
|
-
audio.offset = model.clipIn + (time - model.start) * model.timeScale;
|
663
|
-
audio.play(playTimeOffset);
|
664
|
-
}
|
665
699
|
else {
|
666
700
|
const targetOffset = model.clipIn + (time - model.start) * model.timeScale;
|
667
701
|
// seems it's non-trivial to get the right time from audio sources;
|
@@ -677,7 +711,7 @@
|
|
677
711
|
}
|
678
712
|
let vol = asset.volume as number;
|
679
713
|
|
680
|
-
if(this.track.volume !== undefined)
|
714
|
+
if (this.track.volume !== undefined)
|
681
715
|
vol *= this.track.volume;
|
682
716
|
|
683
717
|
if (isMuted) vol = 0;
|
@@ -692,8 +726,12 @@
|
|
692
726
|
audio.setVolume(vol * this.director.weight);
|
693
727
|
}
|
694
728
|
else {
|
695
|
-
|
696
|
-
|
729
|
+
model._didTriggerPlay = false;
|
730
|
+
if (this.director.isPlaying) {
|
731
|
+
if (audio.isPlaying) {
|
732
|
+
audio.stop();
|
733
|
+
}
|
734
|
+
}
|
697
735
|
}
|
698
736
|
}
|
699
737
|
this.lastTime = time;
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { ACESFilmicToneMapping, LinearToneMapping, NoToneMapping, ReinhardToneMapping } from "three";
|
2
|
+
|
2
3
|
import { serializable } from "../../../engine/engine_serialization.js";
|
3
4
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
4
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,10 +1,11 @@
|
|
1
|
+
import { MathUtils,Mesh } from "three";
|
2
|
+
import { TransformControls } from "three/examples/jsm/controls/TransformControls.js";
|
3
|
+
|
4
|
+
import * as params from "../engine/engine_default_parameters.js";
|
5
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
1
6
|
import { Behaviour, GameObject } from "./Component.js";
|
7
|
+
import { OrbitControls } from "./OrbitControls.js";
|
2
8
|
import { SyncedTransform } from "./SyncedTransform.js";
|
3
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
|
-
import * as params from "../engine/engine_default_parameters.js";
|
5
|
-
import { Mesh, MathUtils } from "three";
|
6
|
-
import { TransformControls } from "three/examples/jsm/controls/TransformControls.js";
|
7
|
-
import { OrbitControls } from "./OrbitControls.js";
|
8
9
|
|
9
10
|
export class TransformGizmo extends Behaviour {
|
10
11
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
|
2
|
+
import { Mesh, Object3D } from "three";
|
2
3
|
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
|
-
|
4
|
+
|
4
5
|
import { getParam } from "../engine_utils.js";
|
5
6
|
|
6
7
|
|
@@ -1,24 +1,25 @@
|
|
1
|
+
import { Matrix4,Mesh, Object3D } from "three";
|
2
|
+
|
3
|
+
import { showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
|
4
|
+
import { hasProLicense } from "../../../engine/engine_license.js";
|
5
|
+
import { serializable } from "../../../engine/engine_serialization.js";
|
6
|
+
import { Context } from "../../../engine/engine_setup.js";
|
1
7
|
import { delay, getParam, isiOS, isMobileDevice, isSafari } from "../../../engine/engine_utils.js";
|
2
|
-
import { Object3D, Mesh, Matrix4 } from "three";
|
3
|
-
import { USDZExporter as ThreeUSDZExporter } from "./ThreeUSDZExporter.js";
|
4
|
-
import { AnimationExtension } from "./extensions/Animation.js"
|
5
|
-
import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js";
|
6
|
-
import { getFormattedDate } from "./utils/timeutils.js";
|
7
|
-
import { registerAnimatorsImplictly } from "./utils/animationutils.js";
|
8
|
-
import type { IUSDExporterExtension } from "./Extension.js";
|
9
8
|
import { Behaviour, GameObject } from "../../Component.js";
|
10
|
-
import {
|
11
|
-
import { serializable } from "../../../engine/engine_serialization.js";
|
12
|
-
import { showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
|
13
|
-
import { Context } from "../../../engine/engine_setup.js";
|
9
|
+
import { Renderer } from "../../Renderer.js"
|
14
10
|
import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
|
15
|
-
import {
|
11
|
+
import { NeedleWebXRHtmlElement } from "../../webxr/WebXRButtons.js";
|
12
|
+
import { XRFlag, XRState, XRStateFlag } from "../../webxr/XRFlag.js";
|
13
|
+
import type { IUSDExporterExtension } from "./Extension.js";
|
14
|
+
import { AnimationExtension } from "./extensions/Animation.js"
|
15
|
+
import { AudioExtension } from "./extensions/behavior/AudioExtension.js";
|
16
16
|
import { BehaviorExtension } from "./extensions/behavior/Behaviour.js";
|
17
|
-
import { AudioExtension } from "./extensions/behavior/AudioExtension.js";
|
18
17
|
import { TextExtension } from "./extensions/USDZText.js";
|
19
18
|
import { USDZUIExtension } from "./extensions/USDZUI.js";
|
20
|
-
import {
|
21
|
-
import {
|
19
|
+
import { USDZExporter as ThreeUSDZExporter } from "./ThreeUSDZExporter.js";
|
20
|
+
import { registerAnimatorsImplictly } from "./utils/animationutils.js";
|
21
|
+
import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js";
|
22
|
+
import { getFormattedDate } from "./utils/timeutils.js";
|
22
23
|
|
23
24
|
const debug = getParam("debugusdz");
|
24
25
|
|
@@ -76,7 +77,7 @@
|
|
76
77
|
extensions: IUSDExporterExtension[] = [];
|
77
78
|
|
78
79
|
private link!: HTMLAnchorElement;
|
79
|
-
private
|
80
|
+
private button?: HTMLButtonElement;
|
80
81
|
|
81
82
|
start() {
|
82
83
|
if (debug) {
|
@@ -114,8 +115,9 @@
|
|
114
115
|
const ios = isiOS()
|
115
116
|
const safari = isSafari();
|
116
117
|
if (debug || (ios && safari)) {
|
117
|
-
if (
|
118
|
-
this.
|
118
|
+
if (this.allowCreateQuicklookButton)
|
119
|
+
this.button = this.createQuicklookButton();
|
120
|
+
|
119
121
|
this.lastCallback = this.quicklookCallback.bind(this);
|
120
122
|
this.link = ensureQuicklookLinkIsCreated(this.context);
|
121
123
|
this.link.addEventListener('message', this.lastCallback);
|
@@ -127,12 +129,13 @@
|
|
127
129
|
}
|
128
130
|
|
129
131
|
onDisable() {
|
132
|
+
this.button?.remove();
|
130
133
|
this.link?.removeEventListener('message', this.lastCallback);
|
131
|
-
const ios = isiOS()
|
132
|
-
const safari = isSafari();
|
133
|
-
if (debug || (ios && safari)) {
|
134
|
-
|
135
|
-
}
|
134
|
+
// const ios = isiOS()
|
135
|
+
// const safari = isSafari();
|
136
|
+
// if (debug || (ios && safari)) {
|
137
|
+
// this.removeQuicklookButton();
|
138
|
+
// }
|
136
139
|
if (debug)
|
137
140
|
showBalloonMessage("USDZ Exporter disabled: " + this.name);
|
138
141
|
|
@@ -381,76 +384,7 @@
|
|
381
384
|
return obj;
|
382
385
|
}
|
383
386
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
private _quicklookButton?: HTMLElement;
|
388
|
-
|
389
|
-
private async createQuicklookButton() {
|
390
|
-
if (!this.webxr) {
|
391
|
-
await delay(1);
|
392
|
-
this.webxr = GameObject.findObjectOfType(WebXR) ?? undefined;
|
393
|
-
if (this.webxr) {
|
394
|
-
if (this.webxr.VRButton) this.webxr.VRButton.parentElement?.removeChild(this.webxr.VRButton);
|
395
|
-
// check if we have an AR button already and re-use that
|
396
|
-
if (this.webxr.ARButton && this._quicklookButton !== this.webxr.ARButton) {
|
397
|
-
this._quicklookButton = this.webxr.ARButton;
|
398
|
-
// Hack to remove the immersiveweb link
|
399
|
-
const linkInButton = this.webxr.ARButton.parentElement?.querySelector("a");
|
400
|
-
if (linkInButton) {
|
401
|
-
linkInButton.href = "";
|
402
|
-
}
|
403
|
-
this.webxr.ARButton.innerText = "Open in Quicklook";
|
404
|
-
this.webxr.ARButton.disabled = false;
|
405
|
-
this.webxr.ARButton.addEventListener("click", evt => {
|
406
|
-
evt.preventDefault();
|
407
|
-
this.exportAsync();
|
408
|
-
});
|
409
|
-
this.webxr.ARButton.classList.add("quicklook-ar-button");
|
410
|
-
this._quicklookButtonContainer = this.webxr.ARButton.parentElement;
|
411
|
-
this.dispatchEvent(new CustomEvent("created-button", { detail: this.webxr.ARButton }))
|
412
|
-
}
|
413
|
-
// create a button if WebXR didnt create one yet
|
414
|
-
else {
|
415
|
-
this.webxr.createARButton = false;
|
416
|
-
this.webxr.createVRButton = false;
|
417
|
-
let container = this.context.domElement.shadowRoot!.querySelector(".webxr-buttons");
|
418
|
-
if (!container) {
|
419
|
-
container = document.createElement("div");
|
420
|
-
container.classList.add("webxr-buttons");
|
421
|
-
}
|
422
|
-
const button = document.createElement("button");
|
423
|
-
button.innerText = "Open in Quicklook";
|
424
|
-
button.addEventListener("click", () => {
|
425
|
-
this.exportAsync();
|
426
|
-
});
|
427
|
-
button.classList.add('webxr-ar-button');
|
428
|
-
button.classList.add('webxr-button');
|
429
|
-
button.classList.add("quicklook-ar-button");
|
430
|
-
this._quicklookButton = button;
|
431
|
-
container.appendChild(button);
|
432
|
-
this._quicklookButtonContainer = container;
|
433
|
-
this.dispatchEvent(new CustomEvent("created-button", { detail: button }))
|
434
|
-
}
|
435
|
-
}
|
436
|
-
else {
|
437
|
-
console.warn("Could not find WebXR component: will not create Quicklook button", Context.Current);
|
438
|
-
}
|
439
|
-
}
|
440
|
-
}
|
441
|
-
|
442
|
-
|
443
|
-
private _quicklookButtonContainer: Element | null = null;
|
444
|
-
private async addQuicklookButton() {
|
445
|
-
await this.createQuicklookButton();
|
446
|
-
if (this._quicklookButton && this._quicklookButtonContainer) {
|
447
|
-
this._quicklookButtonContainer.appendChild(this._quicklookButton);
|
448
|
-
}
|
449
|
-
}
|
450
|
-
private removeQuicklookButton() {
|
451
|
-
this._quicklookButton?.remove();
|
452
|
-
}
|
453
|
-
|
387
|
+
private static invertForwardMatrix = new Matrix4().makeRotationY(Math.PI);
|
454
388
|
private applyWebARSessionRoot() {
|
455
389
|
if (!this.objectToExport) return;
|
456
390
|
|
@@ -475,7 +409,20 @@
|
|
475
409
|
if (debug) console.log("AR Session Root scale", scale, target);
|
476
410
|
target.matrix.makeScale(scale, scale, scale);
|
477
411
|
if (sessionRoot.invertForward) {
|
478
|
-
target.matrix.multiply(
|
412
|
+
target.matrix.multiply(USDZExporter.invertForwardMatrix);
|
479
413
|
}
|
414
|
+
|
415
|
+
// TODO we should refactor this and use one common method in WebARSessionRoot to place an object –
|
416
|
+
// basically the inverted effect of WebARSessionRoot.onApplyPose()
|
417
|
+
|
418
|
+
// TODO why are we not reverting this transformation after the export?
|
480
419
|
}
|
420
|
+
|
421
|
+
|
422
|
+
private createQuicklookButton() {
|
423
|
+
const buttoncontainer = NeedleWebXRHtmlElement.getOrCreate(this.context);
|
424
|
+
const button = buttoncontainer.createQuicklookButton();
|
425
|
+
if(!button.parentNode) buttoncontainer.appendChild(button);
|
426
|
+
return button;
|
427
|
+
}
|
481
428
|
}
|
@@ -1,11 +1,12 @@
|
|
1
|
+
import { Color, Material, Matrix4, MeshStandardMaterial, Object3D, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { GameObject } from "../../../Component.js";
|
4
|
+
import { RectTransform } from "../../../ui/RectTransform.js";
|
5
|
+
import { Text } from "../../../ui/Text.js"
|
6
|
+
import { TextAnchor } from "../../../ui/Text.js";
|
1
7
|
import type { IUSDExporterExtension } from "../Extension.js";
|
2
8
|
import type { IBehaviorElement } from "../extensions/behavior/BehavioursBuilder.js";
|
3
9
|
import { USDDocument, USDObject, USDWriter, USDZExporterContext } from "../ThreeUSDZExporter.js";
|
4
|
-
import { GameObject } from "../../../Component.js";
|
5
|
-
import { Text } from "../../../ui/Text.js"
|
6
|
-
import { RectTransform } from "../../../ui/RectTransform.js";
|
7
|
-
import { Color, Material, Matrix4, MeshStandardMaterial, Object3D, Vector3 } from "three";
|
8
|
-
import { TextAnchor } from "../../../ui/Text.js";
|
9
10
|
|
10
11
|
|
11
12
|
export enum TextWrapMode {
|
@@ -1,13 +1,14 @@
|
|
1
|
-
import
|
2
|
-
|
1
|
+
import { Color, Mesh, MeshBasicMaterial, Object3D } from "three";
|
2
|
+
|
3
3
|
import { GameObject } from "../../../Component.js";
|
4
|
+
import { $shadowDomOwner } from "../../../ui/BaseUIComponent.js";
|
4
5
|
import { Canvas } from "../../../ui/Canvas.js";
|
6
|
+
import { RenderMode } from "../../../ui/Canvas.js";
|
5
7
|
import { CanvasGroup } from "../../../ui/CanvasGroup.js";
|
6
|
-
import { $shadowDomOwner } from "../../../ui/BaseUIComponent.js";
|
7
8
|
import { RectTransform } from "../../../ui/RectTransform.js";
|
8
|
-
import {
|
9
|
+
import type { IUSDExporterExtension } from "../Extension.js";
|
10
|
+
import { USDObject, USDZExporterContext } from "../ThreeUSDZExporter.js";
|
9
11
|
import { TextExtension } from "./USDZText.js";
|
10
|
-
import { RenderMode } from "../../../ui/Canvas.js";
|
11
12
|
|
12
13
|
export class USDZUIExtension implements IUSDExporterExtension {
|
13
14
|
get extensionName(): string {
|
@@ -31,7 +32,7 @@
|
|
31
32
|
height = rt.height;
|
32
33
|
|
33
34
|
const shadowRootModel = USDObject.createEmpty();
|
34
|
-
const shadowComponent = rt.shadowComponent;
|
35
|
+
const shadowComponent = rt.shadowComponent as unknown as Object3D;
|
35
36
|
model.add(shadowRootModel);
|
36
37
|
|
37
38
|
if (shadowComponent) {
|
@@ -52,7 +53,7 @@
|
|
52
53
|
childModel.matrix.copy(child.matrix);
|
53
54
|
|
54
55
|
const childParent = child.parent;
|
55
|
-
const isText = childParent && typeof childParent["textContent"] === "string" && childParent["textContent"].length;
|
56
|
+
const isText = !!childParent && typeof childParent["textContent"] === "string" && childParent["textContent"].length > 0;
|
56
57
|
let hierarchyOpacity = opacityMap.get(childParent!) || 1;
|
57
58
|
|
58
59
|
// TODO CanvasGroup doesn't render something but modifies opacity
|
@@ -64,8 +65,11 @@
|
|
64
65
|
|
65
66
|
if (child instanceof Mesh && isText) {
|
66
67
|
// get shadoDomOwner so we can export Text from the text extension directly
|
67
|
-
const shadowDomOwner = child[$shadowDomOwner]
|
68
|
-
|
68
|
+
const shadowDomOwner = child[$shadowDomOwner];
|
69
|
+
if (!shadowDomOwner)
|
70
|
+
console.error("Error when exporting UI: shadow component owner not found. This is likely a bug.", child);
|
71
|
+
else
|
72
|
+
textExt.exportText(shadowDomOwner.gameObject, childModel, _context);
|
69
73
|
}
|
70
74
|
|
71
75
|
if (child instanceof Mesh && !isText)
|
@@ -25,6 +25,9 @@
|
|
25
25
|
/** Set to true to create an imports.log file that shows all module imports. The file is generated when stopping the server. */
|
26
26
|
logModuleImportChains: boolean;
|
27
27
|
|
28
|
+
/** Set to true to disable generating the buildinfo.json file in your output directory */
|
29
|
+
noBuildInfo: boolean;
|
30
|
+
|
28
31
|
/** required for @serializable https://github.com/vitejs/vite/issues/13736 */
|
29
32
|
vite44Hack: boolean;
|
30
33
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
|
2
|
-
import {
|
2
|
+
import { DoubleSide, FrontSide, Object3D } from "three"
|
3
|
+
import ThreeMeshUI from "three-mesh-ui";
|
4
|
+
|
3
5
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
4
6
|
import { Behaviour } from "../Component.js";
|
5
7
|
import { $shadowDomOwner, BaseUIComponent } from "./BaseUIComponent.js";
|
@@ -27,7 +29,7 @@
|
|
27
29
|
receiveShadows?: boolean;
|
28
30
|
}
|
29
31
|
|
30
|
-
export function updateRenderSettings(shadowComponent: Object3D, settings: RenderSettings) {
|
32
|
+
export function updateRenderSettings(shadowComponent: Object3D | ThreeMeshUI.MeshUIBaseElement, settings: RenderSettings) {
|
31
33
|
if (!shadowComponent) return;
|
32
34
|
// const owner = shadowComponent[$shadowDomOwner];
|
33
35
|
// if (!owner)
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
|
2
1
|
import { Vector3 } from "three";
|
2
|
+
|
3
3
|
import { slerp } from "../../engine/engine_three_utils.js";
|
4
|
+
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
|
4
5
|
|
5
6
|
export function apply(object: Vector3) {
|
6
7
|
if (object && object.isVector3 === true) {
|
@@ -1,13 +1,14 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
3
1
|
import { Material, Mesh, Object3D, ShaderMaterial, SRGBColorSpace, sRGBEncoding, Texture, Vector2, Vector4, VideoTexture } from "three";
|
2
|
+
|
3
|
+
import { isDevEnvironment } from "../engine/debug/index.js";
|
4
|
+
import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
|
4
5
|
import { awaitInput } from "../engine/engine_input_utils.js";
|
6
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
7
|
+
import { Context } from "../engine/engine_setup.js";
|
8
|
+
import { getWorldScale } from "../engine/engine_three_utils.js";
|
5
9
|
import { getParam } from "../engine/engine_utils.js";
|
10
|
+
import { Behaviour, GameObject } from "./Component.js";
|
6
11
|
import { Renderer } from "./Renderer.js";
|
7
|
-
import { getWorldScale } from "../engine/engine_three_utils.js";
|
8
|
-
import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
|
9
|
-
import { Context } from "../engine/engine_setup.js";
|
10
|
-
import { isDevEnvironment } from "../engine/debug/index.js";
|
11
12
|
|
12
13
|
const debug = getParam("debugvideo");
|
13
14
|
|
@@ -184,11 +185,13 @@
|
|
184
185
|
private _isPlaying: boolean = false;
|
185
186
|
private wasPlaying: boolean = false;
|
186
187
|
|
187
|
-
/** ensure's the video
|
188
|
-
|
188
|
+
/** ensure's the video element has been created and will start loading the clip */
|
189
|
+
preloadVideo() {
|
189
190
|
if (debug) console.log("Video Preload: " + this.name, this.clip);
|
190
191
|
this.create(false);
|
191
192
|
}
|
193
|
+
/** @deprecated use `preloadVideo()` */
|
194
|
+
preload() { this.preloadVideo(); }
|
192
195
|
|
193
196
|
/** Set a new video stream
|
194
197
|
* starts to play automatically if the videoplayer hasnt been active before and playOnAwake is true */
|
@@ -234,7 +237,7 @@
|
|
234
237
|
this.create(true);
|
235
238
|
}
|
236
239
|
else {
|
237
|
-
this.
|
240
|
+
this.preloadVideo();
|
238
241
|
}
|
239
242
|
|
240
243
|
if (this.screenspace) {
|
@@ -1,8 +1,9 @@
|
|
1
|
+
import { VignetteEffect } from "postprocessing";
|
2
|
+
|
1
3
|
import { serializable } from "../../../engine/engine_serialization.js";
|
4
|
+
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
2
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
3
|
-
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
4
6
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
5
|
-
import { VignetteEffect } from "postprocessing";
|
6
7
|
|
7
8
|
|
8
9
|
export class Vignette extends PostProcessingEffect {
|
@@ -1,11 +1,12 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { AudioAnalyser } from "three";
|
2
|
+
|
3
|
+
import { isDevEnvironment, showBalloonError, showBalloonWarning } from "../engine/debug/index.js";
|
4
|
+
import { RoomEvents } from "../engine/engine_networking.js";
|
5
|
+
import { disposeStream,NetworkedStreamEvents, NetworkedStreams, StreamEndedEvent, StreamReceivedEvent } from "../engine/engine_networking_streams.js"
|
3
6
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
7
|
import { getParam, microphonePermissionsGranted } from "../engine/engine_utils.js";
|
5
|
-
import { RoomEvents } from "../engine/engine_networking.js";
|
6
8
|
import { delay } from "../engine/engine_utils.js";
|
7
|
-
import {
|
8
|
-
import { AudioAnalyser } from "three";
|
9
|
+
import { Behaviour } from "./Component.js";
|
9
10
|
|
10
11
|
export const noVoip = "noVoip";
|
11
12
|
const debugParam = getParam("debugvoip");
|
@@ -1,13 +1,14 @@
|
|
1
|
-
import {
|
1
|
+
import { EffectComposer } from "postprocessing";
|
2
|
+
|
3
|
+
import { isDevEnvironment } from "../../engine/debug/index.js";
|
4
|
+
import type { EditorModification, IEditorModification as IEditorModificationReceiver } from "../../engine/engine_editor-sync.js";
|
2
5
|
import { serializeable } from "../../engine/engine_serialization_decorator.js";
|
3
6
|
import { getParam } from "../../engine/engine_utils.js";
|
4
|
-
import {
|
5
|
-
import
|
7
|
+
import { Behaviour } from "../Component.js";
|
8
|
+
import { PostProcessingEffect } from "./PostProcessingEffect.js";
|
6
9
|
import { PostProcessingHandler } from "./PostProcessingHandler.js";
|
7
|
-
import { PostProcessingEffect } from "./PostProcessingEffect.js";
|
8
10
|
import { VolumeParameter } from "./VolumeParameter.js";
|
9
|
-
import {
|
10
|
-
import { EffectComposer } from "postprocessing";
|
11
|
+
import { VolumeProfile } from "./VolumeProfile.js";
|
11
12
|
|
12
13
|
const debug = getParam("debugpost");
|
13
14
|
|
@@ -24,102 +24,109 @@
|
|
24
24
|
return (obj || new VrUserStateBuffer()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
25
25
|
}
|
26
26
|
|
27
|
-
|
28
|
-
guid(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
29
|
-
guid(optionalEncoding?:any):string|Uint8Array|null {
|
27
|
+
time():flatbuffers.Long {
|
30
28
|
const offset = this.bb!.__offset(this.bb_pos, 4);
|
31
|
-
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
32
|
-
}
|
33
|
-
|
34
|
-
time():flatbuffers.Long {
|
35
|
-
const offset = this.bb!.__offset(this.bb_pos, 6);
|
36
29
|
return offset ? this.bb!.readInt64(this.bb_pos + offset) : this.bb!.createLong(0, 0);
|
37
30
|
}
|
38
31
|
|
39
32
|
avatarId():string|null
|
40
33
|
avatarId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
41
34
|
avatarId(optionalEncoding?:any):string|Uint8Array|null {
|
42
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
35
|
+
const offset = this.bb!.__offset(this.bb_pos, 6);
|
43
36
|
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
44
37
|
}
|
45
38
|
|
46
39
|
position(obj?:Vec3):Vec3|null {
|
47
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
40
|
+
const offset = this.bb!.__offset(this.bb_pos, 8);
|
48
41
|
return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
|
49
42
|
}
|
50
43
|
|
51
44
|
rotation(obj?:Vec4):Vec4|null {
|
52
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
45
|
+
const offset = this.bb!.__offset(this.bb_pos, 10);
|
53
46
|
return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
|
54
47
|
}
|
55
48
|
|
56
49
|
scale():number {
|
57
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
50
|
+
const offset = this.bb!.__offset(this.bb_pos, 12);
|
58
51
|
return offset ? this.bb!.readFloat32(this.bb_pos + offset) : 0.0;
|
59
52
|
}
|
60
53
|
|
54
|
+
headPosition(obj?:Vec3):Vec3|null {
|
55
|
+
const offset = this.bb!.__offset(this.bb_pos, 14);
|
56
|
+
return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
|
57
|
+
}
|
58
|
+
|
59
|
+
headRotation(obj?:Vec4):Vec4|null {
|
60
|
+
const offset = this.bb!.__offset(this.bb_pos, 16);
|
61
|
+
return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
|
62
|
+
}
|
63
|
+
|
61
64
|
posLeftHand(obj?:Vec3):Vec3|null {
|
62
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
65
|
+
const offset = this.bb!.__offset(this.bb_pos, 18);
|
63
66
|
return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
|
64
67
|
}
|
65
68
|
|
66
69
|
posRightHand(obj?:Vec3):Vec3|null {
|
67
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
70
|
+
const offset = this.bb!.__offset(this.bb_pos, 20);
|
68
71
|
return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
|
69
72
|
}
|
70
73
|
|
71
74
|
rotLeftHand(obj?:Vec4):Vec4|null {
|
72
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
75
|
+
const offset = this.bb!.__offset(this.bb_pos, 22);
|
73
76
|
return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
|
74
77
|
}
|
75
78
|
|
76
79
|
rotRightHand(obj?:Vec4):Vec4|null {
|
77
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
80
|
+
const offset = this.bb!.__offset(this.bb_pos, 24);
|
78
81
|
return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
|
79
82
|
}
|
80
83
|
|
81
84
|
static startVrUserStateBuffer(builder:flatbuffers.Builder) {
|
82
|
-
builder.startObject(
|
85
|
+
builder.startObject(11);
|
83
86
|
}
|
84
87
|
|
85
|
-
static addGuid(builder:flatbuffers.Builder, guidOffset:flatbuffers.Offset) {
|
86
|
-
builder.addFieldOffset(0, guidOffset, 0);
|
87
|
-
}
|
88
|
-
|
89
88
|
static addTime(builder:flatbuffers.Builder, time:flatbuffers.Long) {
|
90
|
-
builder.addFieldInt64(
|
89
|
+
builder.addFieldInt64(0, time, builder.createLong(0, 0));
|
91
90
|
}
|
92
91
|
|
93
92
|
static addAvatarId(builder:flatbuffers.Builder, avatarIdOffset:flatbuffers.Offset) {
|
94
|
-
builder.addFieldOffset(
|
93
|
+
builder.addFieldOffset(1, avatarIdOffset, 0);
|
95
94
|
}
|
96
95
|
|
97
96
|
static addPosition(builder:flatbuffers.Builder, positionOffset:flatbuffers.Offset) {
|
98
|
-
builder.addFieldStruct(
|
97
|
+
builder.addFieldStruct(2, positionOffset, 0);
|
99
98
|
}
|
100
99
|
|
101
100
|
static addRotation(builder:flatbuffers.Builder, rotationOffset:flatbuffers.Offset) {
|
102
|
-
builder.addFieldStruct(
|
101
|
+
builder.addFieldStruct(3, rotationOffset, 0);
|
103
102
|
}
|
104
103
|
|
105
104
|
static addScale(builder:flatbuffers.Builder, scale:number) {
|
106
|
-
builder.addFieldFloat32(
|
105
|
+
builder.addFieldFloat32(4, scale, 0.0);
|
107
106
|
}
|
108
107
|
|
108
|
+
static addHeadPosition(builder:flatbuffers.Builder, headPositionOffset:flatbuffers.Offset) {
|
109
|
+
builder.addFieldStruct(5, headPositionOffset, 0);
|
110
|
+
}
|
111
|
+
|
112
|
+
static addHeadRotation(builder:flatbuffers.Builder, headRotationOffset:flatbuffers.Offset) {
|
113
|
+
builder.addFieldStruct(6, headRotationOffset, 0);
|
114
|
+
}
|
115
|
+
|
109
116
|
static addPosLeftHand(builder:flatbuffers.Builder, posLeftHandOffset:flatbuffers.Offset) {
|
110
|
-
builder.addFieldStruct(
|
117
|
+
builder.addFieldStruct(7, posLeftHandOffset, 0);
|
111
118
|
}
|
112
119
|
|
113
120
|
static addPosRightHand(builder:flatbuffers.Builder, posRightHandOffset:flatbuffers.Offset) {
|
114
|
-
builder.addFieldStruct(
|
121
|
+
builder.addFieldStruct(8, posRightHandOffset, 0);
|
115
122
|
}
|
116
123
|
|
117
124
|
static addRotLeftHand(builder:flatbuffers.Builder, rotLeftHandOffset:flatbuffers.Offset) {
|
118
|
-
builder.addFieldStruct(
|
125
|
+
builder.addFieldStruct(9, rotLeftHandOffset, 0);
|
119
126
|
}
|
120
127
|
|
121
128
|
static addRotRightHand(builder:flatbuffers.Builder, rotRightHandOffset:flatbuffers.Offset) {
|
122
|
-
builder.addFieldStruct(
|
129
|
+
builder.addFieldStruct(10, rotRightHandOffset, 0);
|
123
130
|
}
|
124
131
|
|
125
132
|
static endVrUserStateBuffer(builder:flatbuffers.Builder):flatbuffers.Offset {
|
@@ -1,49 +1,54 @@
|
|
1
|
-
import { Behaviour } from "../Component.js";
|
2
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
3
|
-
import { RGBAColor } from "../js-extensions/RGBAColor.js"
|
4
|
-
import { WebXR } from "./WebXR.js";
|
5
1
|
import {
|
6
|
-
|
7
|
-
Texture,
|
2
|
+
DoubleSide,
|
8
3
|
Mesh, MeshBasicMaterial,
|
9
|
-
|
4
|
+
PerspectiveCamera,
|
10
5
|
PlaneGeometry,
|
6
|
+
Scene,
|
11
7
|
ShaderLib,
|
12
8
|
ShaderMaterial,
|
13
|
-
|
14
|
-
|
9
|
+
Texture,
|
10
|
+
UniformsUtils,
|
15
11
|
} from "three";
|
16
12
|
|
13
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
14
|
+
import { getParam } from "../../engine/engine_utils.js";
|
15
|
+
import { NeedleXREventArgs } from "../../engine/engine_xr.js";
|
16
|
+
import { Behaviour } from "../Component.js";
|
17
|
+
import { RGBAColor } from "../js-extensions/RGBAColor.js"
|
18
|
+
|
19
|
+
const debug = getParam("debugarcamera");
|
20
|
+
|
17
21
|
export class WebARCameraBackground extends Behaviour {
|
18
22
|
|
19
|
-
|
20
|
-
|
21
|
-
|
23
|
+
onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
|
24
|
+
args.optionalFeatures = args.optionalFeatures || [];
|
25
|
+
args.optionalFeatures.push('camera-access');
|
22
26
|
|
23
|
-
|
24
|
-
public backgroundTint: RGBAColor = new RGBAColor(1,1,1,1);
|
25
|
-
|
26
|
-
public get background() {
|
27
|
-
return this.backgroundPlane;
|
27
|
+
if (debug) console.warn("Requesting camera-access");
|
28
28
|
}
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
onEnable(): void {
|
33
|
-
this._preRender = this.preRender.bind(this);
|
34
|
-
this.context.pre_render_callbacks.push(this._preRender);
|
35
|
-
|
30
|
+
onEnterXR(_args: NeedleXREventArgs): void {
|
36
31
|
if (this.backgroundPlane) {
|
37
|
-
this.
|
32
|
+
this.context.scene.add(this.backgroundPlane);
|
38
33
|
this.backgroundPlane.visible = false;
|
39
34
|
}
|
35
|
+
|
36
|
+
if (this.backgroundPlane) this.context.scene.add(this.backgroundPlane);
|
37
|
+
this.context.pre_render_callbacks.push(this.preRender);
|
40
38
|
}
|
41
39
|
|
42
|
-
|
43
|
-
this.
|
40
|
+
onLeaveXR(_args: NeedleXREventArgs): void {
|
41
|
+
if (this.backgroundPlane) this.backgroundPlane.removeFromParent();
|
42
|
+
const i = this.context.pre_render_callbacks.indexOf(this.preRender);
|
43
|
+
if (i >= 0)
|
44
|
+
this.context.pre_render_callbacks.splice(i, 1);
|
45
|
+
}
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
+
@serializable()
|
48
|
+
public backgroundTint: RGBAColor = new RGBAColor(1,1,1,1);
|
49
|
+
|
50
|
+
public get background() {
|
51
|
+
return this.backgroundPlane;
|
47
52
|
}
|
48
53
|
|
49
54
|
private backgroundPlane?: Mesh;
|
@@ -58,11 +63,13 @@
|
|
58
63
|
return function forceTextureInitialization(renderer, texture) {
|
59
64
|
material.map = texture;
|
60
65
|
renderer.render(scene, camera);
|
66
|
+
if (debug) console.warn("Force texture initialization");
|
61
67
|
};
|
62
68
|
}();
|
63
69
|
|
64
|
-
|
65
|
-
|
70
|
+
|
71
|
+
|
72
|
+
private preRender = () => {
|
66
73
|
if (!this || !this.gameObject) return;
|
67
74
|
|
68
75
|
const xr = this.context.renderer.xr;
|
@@ -81,19 +88,14 @@
|
|
81
88
|
// from three: WebGLBackground
|
82
89
|
if (this.backgroundPlane === undefined) {
|
83
90
|
this.backgroundPlane = makeFullscreenPlane(this.backgroundTint);
|
84
|
-
this.gameObject.add(this.backgroundPlane);
|
85
91
|
}
|
92
|
+
if(this.backgroundPlane.parent !== this.scene)
|
93
|
+
this.scene.add(this.backgroundPlane);
|
86
94
|
|
87
95
|
// WebXR Raw Camera Access -
|
88
96
|
// we composite the camera texture into the scene background by rendering it first.
|
89
97
|
this.updateFromFrame(frame);
|
90
98
|
}
|
91
|
-
|
92
|
-
/*
|
93
|
-
if (this.planeMesh) {
|
94
|
-
this.planeMesh.visible = frame != null;
|
95
|
-
}
|
96
|
-
*/
|
97
99
|
}
|
98
100
|
|
99
101
|
onBeforeRender(frame: XRFrame | null) {
|
@@ -131,17 +133,9 @@
|
|
131
133
|
this.backgroundPlane.setTexture(this.threeTexture);
|
132
134
|
this.backgroundPlane.visible = true;
|
133
135
|
}
|
134
|
-
|
135
|
-
|
136
|
-
// setting color space doesn't work.
|
137
|
-
// Plus we need to understand how we can supply a custom shader in
|
138
|
-
// this case.
|
139
|
-
/*
|
140
|
-
if (this.threeTexture) {
|
141
|
-
this.context.scene.background = this.threeTexture;
|
142
|
-
this.threeTexture.colorSpace = NoColorSpace;
|
136
|
+
else {
|
137
|
+
if (debug) console.warn("No background plane to set texture on");
|
143
138
|
}
|
144
|
-
*/
|
145
139
|
}
|
146
140
|
}
|
147
141
|
else {
|
@@ -175,15 +169,14 @@
|
|
175
169
|
gl_FragColor = texColor * <backgroundTint>;
|
176
170
|
|
177
171
|
#include <tonemapping_fragment>
|
178
|
-
#include <
|
179
|
-
|
172
|
+
#include <colorspace_fragment>
|
180
173
|
}
|
181
174
|
`;
|
182
175
|
|
183
176
|
// not sure where we want to move this and in which form is best (extends Object3D?)
|
184
177
|
export function makeFullscreenPlane(tint: RGBAColor ) {
|
185
178
|
const replacementTint = "vec4(" + tint.r.toFixed(3) + "," + tint.g.toFixed(3) + "," + tint.b.toFixed(3) + "," + tint.a.toFixed(3) + ")";
|
186
|
-
console.log(replacementTint);
|
179
|
+
if (debug) console.log(replacementTint);
|
187
180
|
const planeMesh = new Mesh(
|
188
181
|
new PlaneGeometry(2, 2),
|
189
182
|
// @ts-ignore
|
@@ -191,7 +184,7 @@
|
|
191
184
|
name: 'BackgroundMaterial',
|
192
185
|
uniforms: UniformsUtils.clone( ShaderLib.background.uniforms ),
|
193
186
|
vertexShader: ShaderLib.background.vertexShader,
|
194
|
-
fragmentShader: backgroundFragment.
|
187
|
+
fragmentShader: backgroundFragment.replaceAll("<backgroundTint>", replacementTint), // ShaderLib.background.fragmentShader,
|
195
188
|
side: DoubleSide,
|
196
189
|
depthTest: false,
|
197
190
|
depthWrite: false,
|
@@ -211,8 +204,8 @@
|
|
211
204
|
// Option 1: add the planeMesh to our scene for rendering.
|
212
205
|
// This is useful for applying custom shader effects on the background (instead of using the system composite)
|
213
206
|
planeMesh.renderOrder = -10000; // render first
|
214
|
-
planeMesh.layers.disableAll();
|
215
|
-
planeMesh.layers.
|
207
|
+
// planeMesh.layers.disableAll();
|
208
|
+
planeMesh.layers.set(2); // ignore raycasts
|
216
209
|
planeMesh.frustumCulled = false;
|
217
210
|
|
218
211
|
// should be a class, for now lets just define a method for the weird way the texture needs to be set
|
@@ -1,224 +1,424 @@
|
|
1
|
+
import { AxesHelper, DoubleSide, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Quaternion, Raycaster, RingGeometry, Scene, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
|
4
|
+
import { AssetReference } from "../../engine/engine_addressables.js";
|
5
|
+
import { Context } from "../../engine/engine_context.js";
|
6
|
+
import { ObjectUtils, PrimitiveType } from "../../engine/engine_create_objects.js";
|
7
|
+
import { destroy, instantiate } from "../../engine/engine_gameobject.js";
|
8
|
+
import { NEPointerEvent } from "../../engine/engine_input.js";
|
9
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
10
|
+
import { IComponent, IGameObject } from "../../engine/engine_types.js";
|
11
|
+
import { getParam } from "../../engine/engine_utils.js";
|
12
|
+
import { NeedleXRController, NeedleXREventArgs, NeedleXRHitTestResult, NeedleXRSession } from "../../engine/engine_xr.js";
|
1
13
|
import { Behaviour, GameObject } from "../Component.js";
|
2
|
-
import { Matrix4, Object3D, Plane, Quaternion, Ray, Raycaster, Vector2, Vector3 } from "three";
|
3
|
-
import { WebAR, WebXR } from "./WebXR.js";
|
4
|
-
import { InstancingUtil } from "../../engine/engine_instancing.js";
|
5
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
6
|
-
import { Context } from "../../engine/engine_context.js";
|
7
|
-
import { isQuest } from "../../engine/engine_utils.js";
|
8
14
|
|
9
15
|
// https://github.com/takahirox/takahirox.github.io/blob/master/js.mmdeditor/examples/js/controls/DeviceOrientationControls.js
|
10
16
|
|
11
|
-
const
|
17
|
+
const debug = getParam("debugwebxr");
|
12
18
|
|
13
|
-
|
19
|
+
const invertForwardMatrix = new Matrix4().makeRotationY(Math.PI);
|
14
20
|
|
15
|
-
|
21
|
+
// TODO: webarsessionroot needs to place the rig (and not itself)
|
16
22
|
|
17
|
-
|
18
|
-
return this.webAR?.webxr.Rig;
|
19
|
-
}
|
23
|
+
export class WebARSessionRoot extends Behaviour {
|
20
24
|
|
25
|
+
/** The scale of a user in AR:
|
26
|
+
* a large value makes the scene appear smaller
|
27
|
+
* default is 1
|
28
|
+
*/
|
21
29
|
@serializable()
|
22
|
-
invertForward: boolean = false;
|
23
|
-
|
24
|
-
/** Preview feature: enable touch transform */
|
25
|
-
@serializable()
|
26
|
-
arTouchTransform: boolean = false;
|
27
|
-
|
28
|
-
@serializable()
|
29
30
|
get arScale(): number {
|
30
31
|
return this._arScale;
|
31
32
|
}
|
32
33
|
set arScale(val: number) {
|
33
34
|
if (val === this._arScale) return;
|
34
35
|
this._arScale = val;
|
35
|
-
this.
|
36
|
+
this.onScaleChanged();
|
36
37
|
}
|
38
|
+
private _arScale: number = 1;
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
|
40
|
+
/** When enabled the placed scene forward direction will towards the XRRig */
|
41
|
+
@serializable()
|
42
|
+
invertForward: boolean = false;
|
43
|
+
|
44
|
+
/** When assigned this asset will be loaded and visualize the placement while in AR */
|
45
|
+
@serializable(AssetReference)
|
46
|
+
customReticle?: AssetReference;
|
47
|
+
|
48
|
+
/** When enabled we will create a XR anchor for the scene placement
|
49
|
+
* and make sure the scene is at that anchored point during a XR session */
|
50
|
+
@serializable()
|
51
|
+
useXRAnchor: boolean = false;
|
52
|
+
|
53
|
+
/** Preview feature: enable touch transform */
|
54
|
+
@serializable()
|
55
|
+
arTouchTransform: boolean = false;
|
56
|
+
|
57
|
+
/** true if we're currently placing the scene */
|
58
|
+
private _isPlacing = true;
|
59
|
+
|
60
|
+
/** This is the world matrix of the ar session root when entering webxr
|
61
|
+
* it is applied when the scene has been placed (e.g. if the session root is x:10, z:10 we want this position to be the center of the scene)
|
62
|
+
*/
|
63
|
+
private readonly _startOffset: Matrix4 = new Matrix4();
|
64
|
+
|
65
|
+
private _createdPlacementObject: Object3D | null = null;
|
66
|
+
private readonly _reparentedComponents: Array<{ comp: IComponent, originalObject: IGameObject }> = [];
|
67
|
+
|
68
|
+
// move objects into a temporary scene while placing (which is not rendered) so that the components won't be disabled during this process
|
69
|
+
// e.g. we want the avatar to still be updated while placing
|
70
|
+
// another possibly solution would be to ensure from this component that the Rig is *also* not disabled while placing
|
71
|
+
private readonly _placementScene: Scene = new Scene();
|
72
|
+
|
73
|
+
/** the reticles used for placement */
|
74
|
+
private readonly _reticle: IGameObject[] = [];
|
75
|
+
/** needs to be in sync with the reticles */
|
76
|
+
private readonly _hits: XRHitTestResult[] = [];
|
77
|
+
|
78
|
+
private _placementStartTime: number = -1;
|
79
|
+
private _rigPlacementMatrix?: Matrix4;
|
80
|
+
/** if useAnchor is enabled this is the anchor we have created on placing the scene using the placement hit */
|
81
|
+
private _anchor: XRAnchor | null = null;
|
82
|
+
/** user input is used for ar touch transform */
|
41
83
|
private userInput?: WebXRSessionRootUserInput;
|
42
84
|
|
43
|
-
|
44
|
-
|
45
|
-
if (xr) {
|
46
|
-
xr.Rig.updateMatrix();
|
47
|
-
this._initalMatrix.copy(xr.Rig.matrix);
|
48
|
-
}
|
85
|
+
onEnable(): void {
|
86
|
+
this.customReticle?.preload();
|
49
87
|
}
|
50
88
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
private _placementPose: Matrix4 | null = null;
|
55
|
-
private _isTouching: boolean = false;
|
56
|
-
private _rigStartPose: Matrix4 | undefined | null = null;
|
57
|
-
private _gotFirstHitTestResult: boolean = false;
|
58
|
-
private _anchor: XRAnchor | null = null;
|
89
|
+
supportsXR(mode: XRSessionMode): boolean {
|
90
|
+
return mode === "immersive-ar";
|
91
|
+
}
|
59
92
|
|
60
|
-
|
93
|
+
onEnterXR(_args: NeedleXREventArgs): void {
|
94
|
+
if (debug) console.log("ENTER WEBXR: SessionRoot start...");
|
61
95
|
|
62
|
-
this._placementPose = null;
|
63
|
-
this.gameObject.visible = false;
|
64
|
-
this.gameObject.matrixAutoUpdate = false;
|
65
|
-
this._startPose = this.gameObject.matrix.clone();
|
66
|
-
this._rigStartPose = this.rig?.matrix.clone();
|
67
|
-
this._gotFirstHitTestResult = false;
|
68
96
|
this._anchor = null;
|
69
|
-
session.addEventListener('selectstart', this._selectStartFn);
|
70
|
-
session.addEventListener('selectend', this._selectEndFn);
|
71
|
-
// setTimeout(() => this.gameObject.visible = false, 1000); // TODO test on phone AR and Hololens if this was still needed
|
72
97
|
|
73
|
-
//
|
74
|
-
|
98
|
+
// if (_args.xr.session.enabledFeatures?.includes("image-tracking")) {
|
99
|
+
// console.warn("Image tracking is enabled - will not place scene");
|
100
|
+
// return;
|
101
|
+
// }
|
75
102
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
103
|
+
// save the transform of the session root in the scene to apply it when placing the scene
|
104
|
+
this.gameObject.updateMatrixWorld();
|
105
|
+
this._startOffset.copy(this.gameObject.matrixWorld);
|
106
|
+
|
107
|
+
// create a new root object for the session placement scripts
|
108
|
+
// and move all the children in the scene in a temporary scene that is not rendered
|
109
|
+
const rootObject = new Object3D();
|
110
|
+
this._createdPlacementObject = rootObject;
|
111
|
+
rootObject.name = "AR Session Root";
|
112
|
+
this._placementScene.name = "AR Placement Scene";
|
113
|
+
this._placementScene.children.length = 0;
|
114
|
+
for (let i = this.context.scene.children.length - 1; i >= 0; i--) {
|
115
|
+
const ch = this.context.scene.children[i];
|
116
|
+
this._placementScene.add(ch);
|
82
117
|
}
|
118
|
+
this.context.scene.add(rootObject);
|
83
119
|
|
84
|
-
//
|
85
|
-
|
120
|
+
// reparent components
|
121
|
+
// save which gameobject the sessionroot component was previously attached to
|
122
|
+
this._reparentedComponents.length = 0;
|
123
|
+
this._reparentedComponents.push({ comp: this, originalObject: this.gameObject });
|
124
|
+
GameObject.addComponent(rootObject, this);
|
125
|
+
// const webXR = GameObject.findObjectOfType(WebXR2);
|
126
|
+
// if (webXR) {
|
127
|
+
// this._reparentedComponents.push({ comp: webXR, originalObject: webXR.gameObject });
|
128
|
+
// GameObject.addComponent(rootObject, webXR);
|
129
|
+
// const playerSync = GameObject.findObjectOfType(XRFlag);
|
130
|
+
// }
|
131
|
+
|
132
|
+
// recreate the reticle every time we enter AR
|
133
|
+
for (const ret of this._reticle) {
|
134
|
+
destroy(ret);
|
135
|
+
}
|
136
|
+
this._reticle.length = 0;
|
137
|
+
this._isPlacing = true;
|
138
|
+
this.context.input.addEventListener("pointerup", this.onPlaceScene);
|
86
139
|
}
|
140
|
+
onLeaveXR() {
|
141
|
+
// TODO: WebARSessionRoot doesnt work when we enter passthrough and leave XR without having placed the session!!!
|
142
|
+
this.context.input.removeEventListener("pointerup", this.onPlaceScene)
|
143
|
+
this.onRevertSceneChanges();
|
144
|
+
// this._anchor?.delete();
|
145
|
+
this._anchor = null;
|
146
|
+
this._rigPlacementMatrix = undefined;
|
147
|
+
}
|
148
|
+
onUpdateXR(args: NeedleXREventArgs): void {
|
87
149
|
|
88
|
-
|
150
|
+
// disable session placement while images are being tracked
|
151
|
+
if (args.xr.isTrackingImages) {
|
152
|
+
for (const ret of this._reticle)
|
153
|
+
ret.visible = false;
|
154
|
+
return;
|
155
|
+
}
|
89
156
|
|
90
|
-
if (
|
91
|
-
|
92
|
-
|
93
|
-
|
157
|
+
if (this._isPlacing) {
|
158
|
+
const rigObject = args.xr.rig?.gameObject;
|
159
|
+
// the rig should be parented to the scene while placing
|
160
|
+
// since the camera is always parented to the rig this ensures that the camera is always rendering
|
161
|
+
if (rigObject && rigObject.parent !== this.context.scene) {
|
162
|
+
this.context.scene.add(rigObject);
|
94
163
|
}
|
95
164
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
165
|
+
// in pass through mode we want to place the scene using an XR controller
|
166
|
+
let controllersDidHit = false;
|
167
|
+
if (args.xr.isPassThrough && args.xr.controllers.length > 0) {
|
168
|
+
for (const ctrl of args.xr.controllers) {
|
169
|
+
// with this we can only place with the left / first controller right now
|
170
|
+
// we also only have one reticle... this should probably be refactored a bit so we can have multiple reticles
|
171
|
+
// and then place at the reticle for which the user clicked the place button
|
172
|
+
const hit = ctrl.getHitTest();
|
173
|
+
if (hit) {
|
174
|
+
controllersDidHit = true;
|
175
|
+
this.updateReticleAndHits(args.xr, ctrl.index, hit, args.xr.rigScale);
|
176
|
+
}
|
177
|
+
}
|
178
|
+
}
|
179
|
+
// in screen AR mode we use "camera" hit testing (or when using the simulator where controller hit testing is not supported)
|
180
|
+
if (!controllersDidHit) {
|
181
|
+
const hit = args.xr.getHitTest();
|
182
|
+
if (hit) {
|
183
|
+
this.updateReticleAndHits(args.xr, 0, hit, args.xr.rigScale);
|
184
|
+
}
|
185
|
+
}
|
100
186
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
187
|
+
}
|
188
|
+
else {
|
189
|
+
if (this._anchor && args.xr.referenceSpace) {
|
190
|
+
const pose = args.xr.frame.getPose(this._anchor.anchorSpace, args.xr.referenceSpace);
|
191
|
+
if (pose && this.context.time.frame % 20 === 0) {
|
192
|
+
// apply the anchor pose to one of the reticles
|
193
|
+
const converted = args.xr.convertSpace(pose.transform);
|
194
|
+
const reticle = this._reticle[0];
|
195
|
+
if (reticle) {
|
196
|
+
reticle.position.copy(converted.position);
|
197
|
+
reticle.quaternion.copy(converted.quaternion);
|
198
|
+
this.onApplyPose(reticle);
|
199
|
+
}
|
200
|
+
}
|
201
|
+
}
|
105
202
|
|
106
|
-
|
203
|
+
// scene has been placed
|
204
|
+
if (this.arTouchTransform) {
|
205
|
+
if (!this.userInput) this.userInput = new WebXRSessionRootUserInput(this.context);
|
206
|
+
this.userInput?.enable();
|
107
207
|
}
|
208
|
+
else this.userInput?.disable();
|
209
|
+
if (this.arTouchTransform && this.userInput?.hasChanged) {
|
210
|
+
if (args.xr.rig) {
|
211
|
+
const rig = args.xr.rig.gameObject;
|
212
|
+
this.userInput.applyMatrixTo(rig.matrix, true);
|
213
|
+
rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
|
214
|
+
// if the rig is scaled large we want the drag touch to be faster
|
215
|
+
this.userInput.factor = rig.scale.x;
|
216
|
+
}
|
217
|
+
this.userInput.reset();
|
218
|
+
}
|
108
219
|
}
|
109
|
-
return false;
|
110
220
|
}
|
111
221
|
|
112
|
-
private
|
113
|
-
|
114
|
-
|
115
|
-
if (this.context.isInAR)
|
116
|
-
this._anchor = anchor;
|
117
|
-
}).catch(ex => {
|
118
|
-
console.warn("Failed to create anchor", ex);
|
119
|
-
});
|
120
|
-
}
|
222
|
+
private updateReticleAndHits(_xr: NeedleXRSession, i: number, hit: NeedleXRHitTestResult, scale: number) {
|
223
|
+
// save the hit test
|
224
|
+
this._hits[i] = hit.hit;
|
121
225
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
const referenceSpace = this.context.renderer.xr.getReferenceSpace();
|
128
|
-
if (referenceSpace) {
|
129
|
-
const pose = frame.getPose(this._anchor.anchorSpace, referenceSpace);
|
130
|
-
if (pose) {
|
131
|
-
const poseMatrix = this._anchorMatrix.fromArray(pose.transform.matrix).invert();
|
132
|
-
this.placeAt(this._rig, poseMatrix);
|
133
|
-
return;
|
134
|
-
}
|
226
|
+
let reticle = this._reticle[i];
|
227
|
+
if (!reticle) {
|
228
|
+
if (this.customReticle) {
|
229
|
+
if (this.customReticle.asset) {
|
230
|
+
reticle = instantiate(this.customReticle.asset);
|
135
231
|
}
|
232
|
+
else {
|
233
|
+
this.customReticle.loadAssetAsync();
|
234
|
+
return;
|
235
|
+
}
|
136
236
|
}
|
137
|
-
else
|
138
|
-
|
237
|
+
else {
|
238
|
+
reticle = new Mesh(
|
239
|
+
new RingGeometry(0.07, 0.09, 32).rotateX(- Math.PI / 2),
|
240
|
+
new MeshBasicMaterial({ side: DoubleSide })
|
241
|
+
) as any as IGameObject;
|
242
|
+
reticle.name = "AR Placement Reticle";
|
139
243
|
}
|
244
|
+
if (debug) {
|
245
|
+
const axes = new AxesHelper(1);
|
246
|
+
axes.position.y += .01;
|
247
|
+
reticle.add(axes);
|
248
|
+
}
|
249
|
+
this._reticle[i] = reticle;
|
250
|
+
reticle.matrixAutoUpdate = false;
|
251
|
+
reticle.visible = false;
|
140
252
|
}
|
253
|
+
|
254
|
+
reticle.position.lerp(hit.position, this.context.time.deltaTime / .1);
|
255
|
+
reticle.quaternion.slerp(hit.quaternion, this.context.time.deltaTime / .05);
|
256
|
+
reticle.scale.set(scale, scale, scale);
|
257
|
+
// if (this.invertForward) {
|
258
|
+
// reticle.rotateY(Math.PI);
|
259
|
+
// }
|
260
|
+
|
261
|
+
// Workaround: For a custom reticle we apply the view based transform during placement preview
|
262
|
+
// See NE-4161 for context
|
263
|
+
if (this.customReticle)
|
264
|
+
this.applyViewBasedTransform(reticle);
|
265
|
+
|
266
|
+
reticle.updateMatrix();
|
267
|
+
reticle.visible = true;
|
268
|
+
if (reticle.parent !== this.context.scene)
|
269
|
+
this.context.scene.add(reticle);
|
270
|
+
|
271
|
+
if (this._placementStartTime < 0) {
|
272
|
+
this._placementStartTime = this.context.time.realtimeSinceStartup;
|
273
|
+
}
|
141
274
|
}
|
142
275
|
|
143
|
-
private
|
144
|
-
|
276
|
+
private onPlaceScene = (evt: NEPointerEvent) => {
|
277
|
+
if (this._isPlacing == false) return;
|
145
278
|
|
146
|
-
|
147
|
-
|
148
|
-
this._placementPose.copy(mat);
|
279
|
+
let reticle: IGameObject | undefined = this._reticle[0];
|
280
|
+
let hit = this._hits[0];
|
149
281
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
this.
|
282
|
+
if (evt.origin instanceof NeedleXRController) {
|
283
|
+
// until we can use hit testing for both controllers and have multple reticles we only allow placement with the first controller
|
284
|
+
const controllerReticle = this._reticle[evt.origin.index];
|
285
|
+
if (controllerReticle) {
|
286
|
+
reticle = controllerReticle;
|
287
|
+
hit = this._hits[evt.origin.index];
|
156
288
|
}
|
289
|
+
}
|
157
290
|
|
158
|
-
|
159
|
-
|
291
|
+
if (!reticle) {
|
292
|
+
console.warn("No reticle to place...");
|
293
|
+
return;
|
294
|
+
}
|
160
295
|
|
161
|
-
|
162
|
-
|
296
|
+
if (!reticle.visible) {
|
297
|
+
console.warn("Reticle is not visible (can not place)");
|
298
|
+
return;
|
163
299
|
}
|
164
|
-
else this._rig = null;
|
165
|
-
this.gameObject.visible = true;
|
166
|
-
}
|
167
300
|
|
168
|
-
|
169
|
-
|
170
|
-
|
301
|
+
if (NeedleXRSession.active?.isTrackingImages) {
|
302
|
+
console.warn("Scene Placement is disabled while images are being tracked");
|
303
|
+
return;
|
304
|
+
}
|
171
305
|
|
172
|
-
this.
|
173
|
-
|
174
|
-
|
175
|
-
this.
|
176
|
-
|
177
|
-
|
306
|
+
// if we place the scene we don't want this event to be propagated to any sub-objects (via the EventSystem) anymore and trigger e.g. a click on objects for the "place tap" event
|
307
|
+
evt.stopImmediatePropagation();
|
308
|
+
|
309
|
+
this._isPlacing = false;
|
310
|
+
this.context.input.removeEventListener("pointerup", this.onPlaceScene);
|
311
|
+
|
312
|
+
this.onRevertSceneChanges();
|
313
|
+
|
314
|
+
this.onApplyPose(reticle);
|
315
|
+
|
316
|
+
if (this.useXRAnchor) {
|
317
|
+
this.onCreateAnchor(NeedleXRSession.active!, hit);
|
178
318
|
}
|
179
|
-
if (rig) {
|
180
|
-
rig.matrixAutoUpdate = true;
|
181
|
-
if (this._rigStartPose) {
|
182
|
-
this._rigStartPose.decompose(rig.position, rig.quaternion, rig.scale);
|
183
|
-
// console.log(rig.position, rig.quaternion, rig.scale);
|
184
|
-
}
|
185
|
-
}
|
186
|
-
InstancingUtil.markDirty(this.gameObject, true);
|
187
|
-
// HACK to fix physics being not in correct place after exiting AR
|
188
|
-
setTimeout(() => {
|
189
|
-
if (!this.gameObject) return;
|
190
|
-
this.gameObject.matrixAutoUpdate = true;
|
191
|
-
this.gameObject.visible = true;
|
192
|
-
}, 100);
|
193
319
|
}
|
194
320
|
|
195
|
-
|
196
|
-
|
197
|
-
this._isTouching = true;
|
321
|
+
private onScaleChanged() {
|
322
|
+
// TODO: implement
|
198
323
|
}
|
199
324
|
|
200
|
-
private
|
201
|
-
this.
|
325
|
+
private onRevertSceneChanges() {
|
326
|
+
for (const ret of this._reticle) {
|
327
|
+
if (!ret) continue;
|
328
|
+
ret.visible = false;
|
329
|
+
ret?.removeFromParent();
|
330
|
+
}
|
331
|
+
this._reticle.length = 0;
|
332
|
+
|
333
|
+
for (let i = this._placementScene.children.length - 1; i >= 0; i--) {
|
334
|
+
const ch = this._placementScene.children[i];
|
335
|
+
this.context.scene.add(ch);
|
336
|
+
}
|
337
|
+
this._createdPlacementObject?.removeFromParent();
|
338
|
+
|
339
|
+
for (const reparented of this._reparentedComponents) {
|
340
|
+
GameObject.addComponent(reparented.originalObject, reparented.comp);
|
341
|
+
}
|
202
342
|
}
|
203
343
|
|
204
|
-
private
|
205
|
-
|
206
|
-
|
344
|
+
private async onCreateAnchor(session: NeedleXRSession, hit: XRHitTestResult) {
|
345
|
+
if (hit.createAnchor === undefined) {
|
346
|
+
console.warn("Hit does not support creating an anchor", hit);
|
347
|
+
if (isDevEnvironment()) showBalloonWarning("Hit does not support creating an anchor");
|
207
348
|
return;
|
208
349
|
}
|
209
|
-
|
210
|
-
|
211
|
-
|
350
|
+
else {
|
351
|
+
const anchor = await hit.createAnchor(session.viewerPose!.transform);
|
352
|
+
// make sure the session is still active
|
353
|
+
if (session.running && anchor) {
|
354
|
+
this._anchor = anchor;
|
355
|
+
}
|
212
356
|
}
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
357
|
+
}
|
358
|
+
|
359
|
+
private applyViewBasedTransform(reticle: Object3D) {
|
360
|
+
// Make reticle face the user to unify the placement experience across devices.
|
361
|
+
// The pose that we're receiving from the hit test varies between devices:
|
362
|
+
// - Quest: currently aligned to the mesh that was hit (depends on room setup), has changed a couple times
|
363
|
+
// - Android WebXR: looking at the camera, but pretty random when on a wall
|
364
|
+
// - Mozilla WebXR Viewer: aligned to the start of the session
|
365
|
+
const camGo = this.context.mainCamera as Object3D as GameObject;
|
366
|
+
const reticleGo = reticle as GameObject;
|
367
|
+
const camWP = camGo.worldPosition;
|
368
|
+
const reticleWp = reticleGo.worldPosition;
|
369
|
+
// const distance = camWP.distanceTo(reticleWp);
|
370
|
+
camWP.y = reticleWp.y;
|
371
|
+
reticle.lookAt(camWP);
|
372
|
+
|
373
|
+
// TODO: ability to scale the reticle so that we can fit the scene depending on the view angle or distance to the reticle.
|
374
|
+
// Currently, doing this leads to wrong placement of the scene.
|
375
|
+
/*
|
376
|
+
const rigScale = NeedleXRSession.active?.rigScale || 1;
|
377
|
+
const scale = distance * rigScale;
|
378
|
+
reticle.scale.set(scale, scale, scale);
|
379
|
+
*/
|
380
|
+
}
|
381
|
+
|
382
|
+
private onApplyPose(reticle: Object3D) {
|
383
|
+
const rigObject = NeedleXRSession.active?.rig?.gameObject;
|
384
|
+
const rigScale = NeedleXRSession.active?.rigScale || 1;
|
385
|
+
if (rigObject) {
|
386
|
+
// save the previous rig parent
|
387
|
+
const previousParent = rigObject.parent || this.context.scene;
|
388
|
+
|
389
|
+
// if we have placed this rig before and this is just "replacing" with the anchor
|
390
|
+
// we need to make sure the XRRig attached to the reticle is at the same position as last time
|
391
|
+
// since in the following code we move it inside the reticle (relative to the reticle)
|
392
|
+
if (this._rigPlacementMatrix) {
|
393
|
+
this._rigPlacementMatrix?.decompose(rigObject.position, rigObject.quaternion, rigObject.scale);
|
394
|
+
}
|
395
|
+
else {
|
396
|
+
this._rigPlacementMatrix = rigObject.matrix.clone();
|
397
|
+
}
|
398
|
+
|
399
|
+
this.applyViewBasedTransform(reticle);
|
400
|
+
reticle.updateMatrix();
|
401
|
+
// attach rig to reticle (since the reticle is in rig space it's a easy way to place the rig where we want it relative to the reticle)
|
402
|
+
this.context.scene.add(reticle);
|
403
|
+
reticle.attach(rigObject);
|
404
|
+
reticle.removeFromParent();
|
405
|
+
|
406
|
+
// move rig now relative to the reticle
|
407
|
+
// TODO support scaled reticle
|
408
|
+
rigObject.scale.set(this.arScale, this.arScale, this.arScale);
|
409
|
+
rigObject.position.multiplyScalar(this.arScale);
|
410
|
+
|
411
|
+
rigObject.updateMatrix();
|
412
|
+
// if invert forward is disabled we need to invert the forward rotation
|
413
|
+
// we want to look into positive Z direction (if invertForward is enabled we look into negative Z direction)
|
414
|
+
if (this.invertForward)
|
415
|
+
rigObject.matrix.premultiply(invertForwardMatrix);
|
416
|
+
rigObject.matrix.premultiply(this._startOffset);
|
417
|
+
|
418
|
+
// apply the rig modifications and add it back to the previous parent
|
419
|
+
rigObject.matrix.decompose(rigObject.position, rigObject.quaternion, rigObject.scale);
|
420
|
+
previousParent.add(rigObject);
|
218
421
|
}
|
219
|
-
rig.matrix.multiplyMatrices(tempMatrix.makeScale(scale, scale, scale), this._placementPose);
|
220
|
-
rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
|
221
|
-
rig.updateMatrixWorld();
|
222
422
|
}
|
223
423
|
}
|
224
424
|
|
@@ -234,11 +434,14 @@
|
|
234
434
|
twoFingerRotate: boolean = true;
|
235
435
|
twoFingerScale: boolean = true;
|
236
436
|
|
437
|
+
factor: number = 1;
|
438
|
+
|
237
439
|
readonly context: Context;
|
238
440
|
readonly offset: Matrix4;
|
239
441
|
readonly plane: Plane;
|
240
442
|
|
241
443
|
private _scale: number = 1;
|
444
|
+
private _hasChanged: boolean = false;
|
242
445
|
|
243
446
|
// readonly translate: Vector3 = new Vector3();
|
244
447
|
// readonly rotation: Quaternion = new Quaternion();
|
@@ -270,8 +473,21 @@
|
|
270
473
|
this._scale = 1;
|
271
474
|
this.offset.identity();
|
272
475
|
}
|
273
|
-
|
274
|
-
|
476
|
+
get hasChanged() { return this._hasChanged; }
|
477
|
+
|
478
|
+
/**
|
479
|
+
* Applies the matrix to the offset matrix
|
480
|
+
* @param matrix the matrix to apply the drag offset to
|
481
|
+
* @param invert if true the offset matrix will be inverted before applying it to the matrix and premultiplied
|
482
|
+
*/
|
483
|
+
applyMatrixTo(matrix: Matrix4, invert: boolean) {
|
484
|
+
this._hasChanged = false;
|
485
|
+
if (invert) {
|
486
|
+
this.offset.invert();
|
487
|
+
matrix.premultiply(this.offset);
|
488
|
+
}
|
489
|
+
else
|
490
|
+
matrix.multiply(this.offset);
|
275
491
|
// if (this._needsUpdate)
|
276
492
|
// this.updateMatrix();
|
277
493
|
// matrix.premultiply(this._rotationMatrix);
|
@@ -324,7 +540,7 @@
|
|
324
540
|
}
|
325
541
|
private touchMove = (evt: TouchEvent) => {
|
326
542
|
if (evt.defaultPrevented) return;
|
327
|
-
|
543
|
+
|
328
544
|
if (evt.touches.length === 1) {
|
329
545
|
// if we had multiple touches before due to e.g. pinching / rotating
|
330
546
|
// and stopping one of the touches, we don't want to move the scene suddenly
|
@@ -405,21 +621,26 @@
|
|
405
621
|
// this.translate.z -= dz;
|
406
622
|
// this._needsUpdate = true;
|
407
623
|
// return
|
408
|
-
|
409
|
-
dx *= .75;
|
410
|
-
dz *= .75;
|
624
|
+
|
411
625
|
// increase diff if the scene is scaled small
|
412
626
|
dx /= this._scale;
|
413
627
|
dz /= this._scale;
|
628
|
+
|
629
|
+
dx *= this.factor;
|
630
|
+
dz *= this.factor;
|
631
|
+
|
414
632
|
// apply it
|
415
|
-
this.offset.elements[12]
|
416
|
-
this.offset.elements[14]
|
633
|
+
this.offset.elements[12] += dx;
|
634
|
+
this.offset.elements[14] += dz;
|
635
|
+
if (dx !== 0 || dz !== 0)
|
636
|
+
this._hasChanged = true;
|
417
637
|
};
|
418
638
|
|
419
639
|
private readonly _tempMatrix: Matrix4 = new Matrix4();
|
420
640
|
|
421
641
|
private addScale(diff: number) {
|
422
642
|
diff /= window.innerWidth
|
643
|
+
diff *= -1;
|
423
644
|
|
424
645
|
// this.scale.x *= 1 + diff;
|
425
646
|
// this.scale.y *= 1 + diff;
|
@@ -433,14 +654,19 @@
|
|
433
654
|
// apply the scale
|
434
655
|
this._tempMatrix.makeScale(1 - diff, 1 - diff, 1 - diff);
|
435
656
|
this.offset.premultiply(this._tempMatrix);
|
657
|
+
if (diff !== 0)
|
658
|
+
this._hasChanged = true;
|
436
659
|
}
|
437
660
|
|
438
661
|
|
439
662
|
private addRotation(rot: number) {
|
663
|
+
rot *= -1;
|
440
664
|
// this.rotation.multiply(new Quaternion().setFromAxisAngle(WebXRSessionRootUserInput.up, rot));
|
441
665
|
// this._needsUpdate = true;
|
442
666
|
// return;
|
443
667
|
this._tempMatrix.makeRotationY(rot);
|
444
668
|
this.offset.premultiply(this._tempMatrix);
|
669
|
+
if (rot !== 0)
|
670
|
+
this._hasChanged = true;
|
445
671
|
}
|
446
672
|
}
|
@@ -1,762 +1,301 @@
|
|
1
|
-
import {
|
2
|
-
import { ARButton } from '../../include/three/ARButton.js';
|
3
|
-
import { VRButton } from '../../include/three/VRButton.js';
|
1
|
+
import { Object3D } from "three";
|
4
2
|
|
3
|
+
import { showBalloonWarning } from "../../engine/debug/index.js";
|
5
4
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
6
|
-
import { serializable } from "../../engine/
|
7
|
-
import {
|
8
|
-
import {
|
9
|
-
import
|
10
|
-
import { getParam, isMozillaXR, isQuest, setOrAddParamsToUrl } from "../../engine/engine_utils.js";
|
11
|
-
|
5
|
+
import { serializable } from "../../engine/engine_serialization.js";
|
6
|
+
import { getParam, isDesktop, isiOS, isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
|
7
|
+
import { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
8
|
+
import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync.js";
|
12
9
|
import { Behaviour, GameObject } from "../Component.js";
|
13
|
-
import {
|
10
|
+
import { USDZExporter } from "../export/usdz/USDZExporter.js";
|
11
|
+
import { SpatialGrabRaycaster } from "../ui/Raycaster.js";
|
12
|
+
import { Avatar } from "./Avatar.js";
|
13
|
+
import { XRControllerModel } from "./controllers/XRControllerModel.js";
|
14
|
+
import { XRControllerMovement } from "./controllers/XRControllerMovement.js";
|
14
15
|
import { WebARSessionRoot } from "./WebARSessionRoot.js";
|
15
|
-
import {
|
16
|
-
import {
|
17
|
-
import { WebXRSync } from "./WebXRSync.js";
|
18
|
-
import { XRState, XRStateFlag } from "../XRFlag.js";
|
19
|
-
import { showBalloonWarning } from '../../engine/debug/index.js';
|
20
|
-
import { isDestroyed } from '../../engine/engine_gameobject.js';
|
16
|
+
import { NeedleWebXRHtmlElement } from "./WebXRButtons.js";
|
17
|
+
import { XRState, XRStateFlag } from "./XRFlag.js";
|
21
18
|
|
22
|
-
const
|
19
|
+
const debug = getParam("debugwebxr");
|
20
|
+
const debugQuicklook = getParam("debugusdz");
|
23
21
|
|
24
|
-
export
|
25
|
-
if (isMozillaXR()) return true;
|
26
|
-
if ("xr" in navigator) {
|
27
|
-
//@ts-ignore
|
28
|
-
return (await navigator["xr"].isSessionSupported('immersive-ar')) === true;
|
29
|
-
}
|
30
|
-
return false;
|
31
|
-
}
|
32
|
-
export async function detectVRSupport() {
|
33
|
-
if ("xr" in navigator) {
|
34
|
-
//@ts-ignore
|
35
|
-
return (await navigator["xr"].isSessionSupported('immersive-vr')) === true;
|
36
|
-
}
|
37
|
-
return false;
|
38
|
-
}
|
22
|
+
export class WebXR extends Behaviour {
|
39
23
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
24
|
+
// UI
|
25
|
+
/** When enabled a button will be added to the UI to enter VR */
|
26
|
+
createVRButton: boolean = true;
|
27
|
+
/** When enabled a button will be added to the UI to enter AR */
|
28
|
+
createARButton: boolean = true;
|
29
|
+
/** When enabled a send to quest button will be shown if the device does not support VR */
|
30
|
+
createSendToQuestButton: boolean = true;
|
31
|
+
/** When enabled a QRCode will be created to open the website on a mobile device */
|
32
|
+
createQRCode: boolean = true;
|
44
33
|
|
45
|
-
//
|
34
|
+
// VR Settings
|
35
|
+
/** When enabled default movement behaviour will be added */
|
36
|
+
useDefaultControls: boolean = true;
|
37
|
+
/** When enabled controller models will automatically be created and updated when you are using controllers in WebXR */
|
38
|
+
showControllerModels: boolean = true;
|
39
|
+
/** When enabled hand models will automatically be created and updated when you are using hands in WebXR */
|
40
|
+
showHandModels: boolean = true;
|
46
41
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
42
|
+
// AR Settings
|
43
|
+
/** When enabled the scene must be placed in AR */
|
44
|
+
usePlacementReticle: boolean = true;
|
45
|
+
/** When enabled you can position, rotate or scale your AR scene with one or two fingers */
|
46
|
+
usePlacementAdjustment: boolean = true;
|
47
|
+
/** Used when `usePlacementReticle` is enabled */
|
48
|
+
arSceneScale: number = 1;
|
49
|
+
/** Experimental: When enabled an XRAnchor will be created for the AR scene and the position will be updated to the anchor position every few frames */
|
50
|
+
useXRAnchor: boolean = false;
|
54
51
|
|
55
|
-
|
56
|
-
|
57
|
-
};
|
52
|
+
/** When enabled a USDZExporter component will be added to the scene (if none is found) */
|
53
|
+
useQuicklookExport: boolean = false;
|
58
54
|
|
59
|
-
export class WebXR extends Behaviour {
|
60
55
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
56
|
+
/** Preview feature enabling occlusion (when available: https://github.com/cabanier/three.js/commit/b6ee92bcd8f20718c186120b7f19a3b68a1d4e47)
|
57
|
+
* Enables the 'depth-sensing' WebXR feature to provide realtime depth occlusion. Only supported on Oculus Quest right now.
|
58
|
+
*/
|
59
|
+
useDepthSensing: boolean = false;
|
65
60
|
|
61
|
+
|
62
|
+
/** This avatar representation will be spawned when you enter a webxr session */
|
66
63
|
@serializable(AssetReference)
|
67
64
|
defaultAvatar?: AssetReference;
|
68
|
-
@serializable()
|
69
|
-
handModelPath: string = "";
|
70
65
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
createARButton: boolean = true;
|
66
|
+
private _playerSync?: PlayerSync;
|
67
|
+
/** these components were created by the WebXR component on session start and will be cleaned up again in session end */
|
68
|
+
private readonly _createdComponentsInSession: Behaviour[] = [];
|
75
69
|
|
76
|
-
private
|
77
|
-
private static events: EventDispatcher = new EventDispatcher();
|
70
|
+
private _usdzExporter?: USDZExporter;
|
78
71
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
public static get IsVRSupported(): boolean { return vrSupported; }
|
83
|
-
|
84
|
-
private static _optionalFeatures_VR: string[] = ['local-floor', 'bounded-floor', 'hand-tracking', 'high-fixed-foveation-level', 'layers'];
|
85
|
-
private static _optionalFeatures_AR: string[] = ['anchors', 'local-floor', 'hand-tracking', 'layers'];
|
86
|
-
public static get OptionalFeatures_VR(): string[] { return this._optionalFeatures_VR; }
|
87
|
-
public static get OptionalFeatures_AR(): string[] { return this._optionalFeatures_AR; }
|
88
|
-
|
89
|
-
public static addEventListener(type: string, listener: any): any {
|
90
|
-
this.events.addEventListener(type, listener);
|
91
|
-
return listener;
|
72
|
+
awake() {
|
73
|
+
NeedleXRSession.getXRSync(this.context);
|
74
|
+
if (debug) window.addEventListener("keydown", evt => { if (evt.key === "x") this.exitXR(); });
|
92
75
|
}
|
93
|
-
public static removeEventListener(type: string, listener: any): any {
|
94
|
-
this.events.removeEventListener(type, listener);
|
95
|
-
return listener;
|
96
|
-
}
|
97
|
-
private static dispatchEvent(type: string, event: any): void {
|
98
|
-
this.events.dispatchEvent({ type, detail: event });
|
99
|
-
}
|
100
76
|
|
101
|
-
|
102
|
-
if
|
103
|
-
|
77
|
+
onEnable(): void {
|
78
|
+
// check if we're on a secure connection:
|
79
|
+
if (window.location.protocol !== "https:") {
|
80
|
+
showBalloonWarning("<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API\" target=\"_blank\">WebXR</a> only works on secure connections (https).");
|
104
81
|
}
|
105
|
-
else
|
106
|
-
webXR.__internalAwake();
|
107
|
-
const options = { optionalFeatures: WebXR.OptionalFeatures_VR };
|
108
|
-
const vrButton = VRButton.createButton(webXR.context.renderer, options);
|
109
|
-
vrButton.classList.add('webxr-ar-button');
|
110
|
-
vrButton.classList.add('webxr-button');
|
111
|
-
this.resetButtonStyles(vrButton);
|
112
|
-
// if (this.enableAR) vrButton.style.marginLeft = "60px";
|
113
|
-
if (opts?.registerClick ?? true)
|
114
|
-
vrButton.addEventListener('click', webXR.onClickedVRButton.bind(webXR));
|
115
|
-
return vrButton;
|
116
|
-
}
|
117
82
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
options.optionalFeatures.push('anchors');
|
83
|
+
if (this.useQuicklookExport) {
|
84
|
+
const existingUSDZExporter = GameObject.findObjectOfType(USDZExporter);
|
85
|
+
if (!existingUSDZExporter) {
|
86
|
+
// if no USDZ Exporter is found we add one and assign the scene to be exported
|
87
|
+
if (debug) console.log("WebXR: Adding USDZExporter");
|
88
|
+
this._usdzExporter = GameObject.addNewComponent(this.gameObject, USDZExporter);
|
89
|
+
this._usdzExporter.objectToExport = this.context.scene;
|
90
|
+
}
|
127
91
|
}
|
128
|
-
else {
|
129
|
-
console.warn("No dom overlay root found, HTML overlays on top of screen-based AR will not work.");
|
130
|
-
}
|
131
92
|
|
132
|
-
|
133
|
-
|
134
|
-
arButton.classList.add('webxr-button');
|
135
|
-
WebXR.resetButtonStyles(arButton);
|
136
|
-
if (opts?.registerClick ?? true)
|
137
|
-
arButton.addEventListener('click', webXR.onClickedARButton.bind(webXR));
|
138
|
-
return arButton;
|
139
|
-
}
|
93
|
+
this.handleCreatingHTML();
|
94
|
+
this.handleOfferSession();
|
140
95
|
|
141
|
-
private static onModifyAROptions(options) {
|
142
|
-
WebXR.dispatchEvent(WebXREvent.ModifyAROptions, options);
|
143
|
-
}
|
144
|
-
|
145
|
-
public static resetButtonStyles(button) {
|
146
|
-
if (!button) return;
|
147
|
-
button.style.position = "";
|
148
|
-
button.style.bottom = "";
|
149
|
-
button.style.left = "";
|
150
|
-
}
|
151
|
-
|
152
|
-
public endSession() {
|
153
|
-
const session = this.context.renderer.xr.getSession();
|
154
|
-
if (session) session.end();
|
155
|
-
}
|
156
|
-
|
157
|
-
public get Rig(): Object3D {
|
158
|
-
this.ensureRig();
|
159
|
-
return this.rig;
|
160
|
-
}
|
161
|
-
|
162
|
-
|
163
|
-
private controllers: WebXRController[] = [];
|
164
|
-
public get Controllers(): WebXRController[] {
|
165
|
-
return this.controllers;
|
166
|
-
}
|
167
|
-
|
168
|
-
public get LeftController(): WebXRController | null {
|
169
|
-
if (this.controllers.length > 0 && this.controllers[0].input?.handedness === "left") return this.controllers[0];
|
170
|
-
if (this.controllers.length > 1 && this.controllers[1].input?.handedness === "left") return this.controllers[1];
|
171
|
-
return null;
|
172
|
-
}
|
173
|
-
|
174
|
-
public get RightController(): WebXRController | null {
|
175
|
-
if (this.controllers.length > 0 && this.controllers[0].input?.handedness === "right") return this.controllers[0];
|
176
|
-
if (this.controllers.length > 1 && this.controllers[1].input?.handedness === "right") return this.controllers[1];
|
177
|
-
return null;
|
178
|
-
}
|
179
|
-
|
180
|
-
public get ARButton(): HTMLButtonElement | undefined {
|
181
|
-
return this._arButton;
|
182
|
-
}
|
183
|
-
|
184
|
-
public get VRButton(): HTMLButtonElement | undefined {
|
185
|
-
return this._vrButton;
|
186
|
-
}
|
187
|
-
|
188
|
-
public get IsInVR() { return this._isInVR; }
|
189
|
-
public get IsInAR() { return this._isInAR; }
|
190
|
-
|
191
|
-
/** When enabled */
|
192
|
-
allowARPlacementReticle: boolean = true;
|
193
|
-
|
194
|
-
private rig!: Object3D;
|
195
|
-
private isInit: boolean = false;
|
196
|
-
|
197
|
-
private _requestedAR: boolean = false;
|
198
|
-
private _requestedVR: boolean = false;
|
199
|
-
private _isInAR: boolean = false;
|
200
|
-
private _isInVR: boolean = false;
|
201
|
-
|
202
|
-
private _arButton?: HTMLButtonElement;
|
203
|
-
private _vrButton?: HTMLButtonElement;
|
204
|
-
|
205
|
-
private webAR: WebAR | null = null;
|
206
|
-
|
207
|
-
awake(): void {
|
208
|
-
// as the webxr component is most of the times currently loaded as part of the scene
|
209
|
-
// and not part of the glTF directly and thus does not go through the whole serialization process currently
|
210
|
-
// we need to to manuall make sure it is of the correct type here
|
211
96
|
if (this.defaultAvatar) {
|
212
|
-
|
213
|
-
|
214
|
-
}
|
97
|
+
this._playerSync = this.gameObject.getOrAddComponent(PlayerSync);
|
98
|
+
this._playerSync.autoSync = false;
|
215
99
|
}
|
216
|
-
if (
|
217
|
-
|
218
|
-
|
100
|
+
if (this._playerSync) {
|
101
|
+
this._playerSync.asset = this.defaultAvatar;
|
102
|
+
this._playerSync.onPlayerSpawned?.removeEventListener(this.onAvatarSpawned);
|
103
|
+
this._playerSync.onPlayerSpawned?.addEventListener(this.onAvatarSpawned);
|
219
104
|
}
|
220
|
-
this.webAR = new WebAR(this);
|
221
105
|
|
222
|
-
if
|
223
|
-
|
224
|
-
|
106
|
+
// if a button container exists and is not attached to any parent element we attach it to the shadow root (assuming it was removed during onDisable)
|
107
|
+
if (this._container && !this._container.parentNode) {
|
108
|
+
this.context.domElement.shadowRoot?.appendChild(this._container);
|
225
109
|
}
|
226
110
|
}
|
227
111
|
|
228
|
-
|
229
|
-
if
|
230
|
-
|
231
|
-
this.
|
232
|
-
|
233
|
-
this.context.renderer.xr.enabled = true;
|
234
|
-
|
235
|
-
// TODO: move the whole buttons positioning out of here and make it configureable from css
|
236
|
-
// better set proper classes so user code can react to it instead
|
237
|
-
// of this hardcoded stuff
|
238
|
-
let arButton, vrButton;
|
239
|
-
const buttonsContainer = document.createElement('div');
|
240
|
-
buttonsContainer.classList.add("webxr-buttons");
|
241
|
-
buttonsContainer.style.cssText = `
|
242
|
-
position: absolute;
|
243
|
-
bottom: 21px;
|
244
|
-
left: 50%;
|
245
|
-
transform: translate(-50%, 0%);
|
246
|
-
z-index: 1000;
|
247
|
-
|
248
|
-
display: flex;
|
249
|
-
flex-direction: row;
|
250
|
-
justify-content: center;
|
251
|
-
align-items: flex-start;
|
252
|
-
gap: 10px;
|
253
|
-
`;
|
254
|
-
this.context.appendHTMLElement(buttonsContainer);
|
255
|
-
|
256
|
-
const forceButtons = debugWebXR;
|
257
|
-
if (debugWebXR) console.log("ARSupported?", arSupported, "VRSupported?", vrSupported);
|
258
|
-
|
259
|
-
// AR support
|
260
|
-
if (forceButtons || (this.createARButton && this.enableAR && arSupported)) {
|
261
|
-
arButton = WebXR.createARButton(this);
|
262
|
-
this._arButton = arButton;
|
263
|
-
buttonsContainer.appendChild(arButton);
|
264
|
-
}
|
265
|
-
|
266
|
-
// VR support
|
267
|
-
if (forceButtons || (this.createVRButton && this.enableVR && vrSupported)) {
|
268
|
-
vrButton = WebXR.createVRButton(this);
|
269
|
-
this._vrButton = vrButton;
|
270
|
-
buttonsContainer.appendChild(vrButton);
|
271
|
-
}
|
272
|
-
|
273
|
-
setTimeout(() => {
|
274
|
-
WebXR.resetButtonStyles(vrButton);
|
275
|
-
WebXR.resetButtonStyles(arButton);
|
276
|
-
}, 1000);
|
112
|
+
onDisable(): void {
|
113
|
+
// remove the container automatically if it was added to the shadow root
|
114
|
+
this._container?.remove();
|
115
|
+
this._usdzExporter?.destroy();
|
277
116
|
}
|
278
117
|
|
279
|
-
private
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
onBeforeRender(frame:XRFrame | null | undefined) {
|
286
|
-
if (!frame) return;
|
287
|
-
// TODO: figure out why screen is black if we enable the code written here
|
288
|
-
// const referenceSpace = renderer.xr.getReferenceSpace();
|
289
|
-
const session = this.context.renderer.xr.getSession();
|
290
|
-
|
291
|
-
|
292
|
-
if (session) {
|
293
|
-
const referenceSpace = this.context.renderer.xr.getReferenceSpace();
|
294
|
-
if(!referenceSpace) return;
|
295
|
-
const pose = frame.getViewerPose(referenceSpace);
|
296
|
-
if (!pose) return;
|
297
|
-
this._currentHeadPose = pose;
|
298
|
-
const transform: XRRigidTransform = pose?.transform;
|
299
|
-
if (transform) {
|
300
|
-
this._transformOrientation.set(transform.orientation.x, transform.orientation.y, transform.orientation.z, transform.orientation.w);
|
118
|
+
private async handleOfferSession() {
|
119
|
+
if (this.createVRButton) {
|
120
|
+
const hasVRSupport = await NeedleXRSession.isVRSupported();
|
121
|
+
if (hasVRSupport && this.createVRButton) {
|
122
|
+
return NeedleXRSession.offerSession("immersive-vr", "default", this.context);
|
301
123
|
}
|
302
|
-
|
303
|
-
|
304
|
-
|
124
|
+
}
|
125
|
+
if (this.createARButton) {
|
126
|
+
const hasARSupport = await NeedleXRSession.isARSupported();
|
127
|
+
if (hasARSupport && this.createARButton) {
|
128
|
+
return NeedleXRSession.offerSession("immersive-ar", "default", this.context);
|
305
129
|
}
|
306
|
-
else if (this.IsInVR) {
|
307
|
-
if (this.context.mainCamera) {
|
308
|
-
this.ensureRig();
|
309
|
-
}
|
310
|
-
}
|
311
|
-
|
312
|
-
for (const ctrl of this.controllers) {
|
313
|
-
ctrl.onUpdate(session);
|
314
|
-
}
|
315
|
-
|
316
|
-
if (this._isInAR) {
|
317
|
-
this.webAR?.onUpdate(session, frame);
|
318
|
-
}
|
319
130
|
}
|
320
|
-
|
321
|
-
WebXR.events.dispatchEvent({ type: WebXREvent.XRUpdate, frame: frame, xr: this.context.renderer.xr, rig: this.rig });
|
131
|
+
return false;
|
322
132
|
}
|
323
133
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
this._requestedVR = false;
|
328
|
-
|
329
|
-
// if we do this on enter xr the state has already been changed in AR mode
|
330
|
-
// so we need to to this before session has started
|
331
|
-
this.captureStateBeforeXR();
|
332
|
-
}
|
134
|
+
/** the currently active webxr input session */
|
135
|
+
get session(): NeedleXRSession | null {
|
136
|
+
return NeedleXRSession.active ?? null;
|
333
137
|
}
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
// happens e.g. when headset is off and xr session never actually started
|
339
|
-
if (this._requestedVR) {
|
340
|
-
this.onExitXR(null);
|
341
|
-
return;
|
342
|
-
}
|
343
|
-
|
344
|
-
this._requestedAR = false;
|
345
|
-
this._requestedVR = true;
|
346
|
-
this.captureStateBeforeXR();
|
347
|
-
|
348
|
-
// build controllers before session begins - this seems to fix issue with controller models not appearing/not getting connection event
|
349
|
-
this.ensureRig();
|
350
|
-
for (let i = 0; i < 2; i++) {
|
351
|
-
WebXRController.Create(this, i, this.gameObject as GameObject, ControllerType.PhysicalDevice);
|
352
|
-
}
|
353
|
-
|
354
|
-
WebXR.events.dispatchEvent({ type: WebXREvent.RequestVRSession });
|
355
|
-
}
|
138
|
+
/** immersive-vr or immersive-ar */
|
139
|
+
get sessionMode(): XRSessionMode | null {
|
140
|
+
return NeedleXRSession.activeMode ?? null;;
|
356
141
|
}
|
357
142
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
this._originalCameraRotation.copy(getWorldQuaternion(this.context.mainCamera));
|
362
|
-
this._originalCameraParent = this.context.mainCamera.parent;
|
363
|
-
}
|
364
|
-
if (this.Rig) {
|
365
|
-
this._originalXRRigParent = this.Rig.parent;
|
366
|
-
this._originalXRRigPosition.copy(this.Rig.position);
|
367
|
-
this._originalXRRigRotation.copy(this.Rig.quaternion);
|
368
|
-
}
|
143
|
+
/** Call to start an WebVR session */
|
144
|
+
async enterVR(init?: XRSessionInit): Promise<NeedleXRSession | null> {
|
145
|
+
return NeedleXRSession.start("immersive-vr", init, this.context);
|
369
146
|
}
|
147
|
+
/** Call to start an WebAR session */
|
148
|
+
async enterAR(init?: XRSessionInit): Promise<NeedleXRSession | null> {
|
149
|
+
return NeedleXRSession.start("immersive-ar", init, this.context);
|
150
|
+
}
|
151
|
+
/** Call to end a WebXR (AR or VR) session */
|
152
|
+
exitXR() {
|
153
|
+
NeedleXRSession.stop();
|
154
|
+
}
|
370
155
|
|
371
|
-
private
|
372
|
-
if (!this.rig || isDestroyed(this.rig)) {
|
373
|
-
// currently just used for pose
|
374
|
-
const xrRig = GameObject.findObjectOfType(XRRig, this.context);
|
375
|
-
if (xrRig) {
|
376
|
-
// make it match unity forward
|
377
|
-
this.rig = xrRig.gameObject;
|
378
|
-
this.rig.rotateY(Math.PI);
|
379
|
-
// this.rig.position.copy(existing.worldPosition);
|
380
|
-
// this.rig.quaternion.premultiply(existing.worldQuaternion);
|
381
|
-
}
|
382
|
-
else {
|
383
|
-
this.rig = new Group();
|
384
|
-
this.rig.rotateY(Math.PI);
|
385
|
-
this.rig.name = "XRRig";
|
386
|
-
this.context.scene.add(this.rig);
|
387
|
-
}
|
388
|
-
}
|
156
|
+
private _previousXRState: number = 0;
|
389
157
|
|
390
|
-
|
391
|
-
if (
|
392
|
-
|
393
|
-
|
394
|
-
// Hack: make sure we have the correct position and rotation (e.g. where we are dealing with an implicitly created rig)
|
395
|
-
// This handles the case where we switch between multiple scenes
|
396
|
-
if (this.IsInVR) {
|
397
|
-
const other = GameObject.findObjectOfType(XRRig);
|
398
|
-
if (other && other?.gameObject !== this.rig) {
|
399
|
-
this.rig.position.copy(other.gameObject.position);
|
400
|
-
this.rig.quaternion.copy(other.gameObject.quaternion);
|
401
|
-
this.rig.rotateY(Math.PI);
|
402
|
-
this.rig.scale.copy(other.gameObject.scale);
|
403
|
-
}
|
404
|
-
}
|
158
|
+
onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
|
159
|
+
if (_mode == "immersive-ar" && this.useDepthSensing) {
|
160
|
+
args.optionalFeatures = args.optionalFeatures || [];
|
161
|
+
args.optionalFeatures.push("depth-sensing");
|
405
162
|
}
|
406
163
|
}
|
407
164
|
|
165
|
+
async onEnterXR(args: NeedleXREventArgs) {
|
166
|
+
if (debug) console.log("WebXR onEnterXR")
|
167
|
+
// set XR flags
|
168
|
+
this._previousXRState = XRState.Global.Mask;
|
169
|
+
const isVR = args.xr.isVR;
|
170
|
+
XRState.Global.Set(isVR ? XRStateFlag.VR : XRStateFlag.AR);
|
408
171
|
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
const space = this.context.renderer.xr.getReferenceSpace();
|
424
|
-
if (space && this.rig) {
|
425
|
-
const pose = frame.getViewerPose(space);
|
426
|
-
const rot = pose?.transform.orientation;
|
427
|
-
if (rot) {
|
428
|
-
const quat = new Quaternion(rot.x, rot.y, rot.z, rot.w);
|
429
|
-
const eu = new Euler().setFromQuaternion(quat);
|
430
|
-
this.rig.rotateY(eu.y);
|
431
|
-
// this.rig.quaternion.multiply(quat);
|
172
|
+
// Handle AR session root
|
173
|
+
if (this.usePlacementReticle && args.xr.isAR) {
|
174
|
+
let sessionroot = GameObject.findObjectOfType(WebARSessionRoot);
|
175
|
+
if (!sessionroot) {
|
176
|
+
const implicitSessionRoot = new Object3D();
|
177
|
+
for (const ch of this.context.scene.children)
|
178
|
+
implicitSessionRoot.add(ch);
|
179
|
+
this.context.scene.add(implicitSessionRoot);
|
180
|
+
sessionroot = GameObject.addNewComponent(implicitSessionRoot, WebARSessionRoot)!;
|
181
|
+
this._createdComponentsInSession.push(sessionroot);
|
182
|
+
sessionroot.arScale = this.arSceneScale;
|
183
|
+
sessionroot.arTouchTransform = this.usePlacementAdjustment;
|
184
|
+
sessionroot.useXRAnchor = this.useXRAnchor;
|
432
185
|
}
|
186
|
+
else if(debug) console.log("WebXR: WebARSessionRoot already exists, not creating a new one")
|
433
187
|
}
|
434
188
|
|
435
|
-
//
|
436
|
-
|
437
|
-
|
438
|
-
if (this.context.mainCamera) {
|
439
|
-
const cam = xr.getCamera() as WebXRArrayCamera;
|
440
|
-
if (debugWebXR) console.log("WebXRCamera", cam);
|
441
|
-
const cull = this.context.mainCameraComponent?.cullingMask;
|
442
|
-
if (cam && cull !== undefined) {
|
443
|
-
for (const c of cam.cameras) {
|
444
|
-
c.layers.mask = cull;
|
445
|
-
}
|
446
|
-
cam.layers.mask = cull;
|
447
|
-
}
|
448
|
-
else if (cam) {
|
449
|
-
for (const c of cam.cameras) {
|
450
|
-
c.layers.enableAll();
|
451
|
-
}
|
452
|
-
cam.layers.enableAll();
|
453
|
-
}
|
454
|
-
if (this._requestedAR) {
|
455
|
-
this.context.scene.add(this.rig);
|
456
|
-
}
|
189
|
+
// handle VR controls
|
190
|
+
if (this.useDefaultControls) {
|
191
|
+
this.setDefaultMovementEnabled(true);
|
457
192
|
}
|
193
|
+
if (this.showControllerModels || this.showHandModels) {
|
194
|
+
this.setDefaultControllerRenderingEnabled(true);
|
195
|
+
}
|
458
196
|
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
switch (flag) {
|
464
|
-
case XRStateFlag.AR:
|
465
|
-
this.context.xrSessionMode = XRSessionMode.ImmersiveAR;
|
466
|
-
this._isInAR = true;
|
467
|
-
this.webAR?.onBegin(session);
|
468
|
-
break;
|
469
|
-
case XRStateFlag.VR:
|
470
|
-
this.context.xrSessionMode = XRSessionMode.ImmersiveVR;
|
471
|
-
this._isInVR = true;
|
472
|
-
this.onEnterVR(session);
|
473
|
-
break;
|
197
|
+
// ensure we have a spatial grab raycaster for close grabs
|
198
|
+
let raycaster = GameObject.findObjectOfType(SpatialGrabRaycaster);
|
199
|
+
if (!raycaster) {
|
200
|
+
raycaster = this.gameObject.addNewComponent(SpatialGrabRaycaster);
|
474
201
|
}
|
475
202
|
|
476
|
-
|
477
|
-
console.log("[XR] session end");
|
478
|
-
WebXR._isInXr = false;
|
479
|
-
this.onExitXR(session);
|
480
|
-
});
|
481
|
-
|
482
|
-
this.onEnterXR_HandleMirrorWindow(session);
|
483
|
-
|
484
|
-
WebXR.events.dispatchEvent({ type: WebXREvent.XRStarted, session: session });
|
203
|
+
this.createLocalAvatar(args.xr);
|
485
204
|
}
|
486
205
|
|
487
|
-
|
206
|
+
onLeaveXR(_: NeedleXREventArgs): void {
|
207
|
+
// revert XR flags
|
208
|
+
XRState.Global.Set(this._previousXRState);
|
488
209
|
|
489
|
-
|
210
|
+
this._playerSync?.destroyInstance();
|
490
211
|
|
491
|
-
|
492
|
-
|
493
|
-
this.webAR?.onEnd(session);
|
494
|
-
}
|
495
|
-
else {
|
496
|
-
// if in VR we want to restore the FOV
|
497
|
-
this.context.mainCameraComponent?.applyClearFlagsIfIsActiveCamera();
|
498
|
-
}
|
212
|
+
for (const comp of this._createdComponentsInSession) {
|
213
|
+
comp.destroy();
|
499
214
|
}
|
215
|
+
this._createdComponentsInSession.length = 0;
|
500
216
|
|
501
|
-
this.
|
502
|
-
|
503
|
-
this._requestedAR = false;
|
504
|
-
this._requestedVR = false;
|
505
|
-
this.context.xrSessionMode = undefined;
|
217
|
+
this.handleOfferSession();
|
218
|
+
}
|
506
219
|
|
507
|
-
if (this.xrMirrorWindow) {
|
508
|
-
this.xrMirrorWindow.close();
|
509
|
-
this.xrMirrorWindow = null;
|
510
|
-
}
|
511
220
|
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
this.context.mainCamera.scale.set(1, 1, 1);
|
221
|
+
/** Call to enable or disable default controller behaviour */
|
222
|
+
setDefaultMovementEnabled(enabled: boolean): XRControllerMovement | null {
|
223
|
+
let movement = this.gameObject.getComponent(XRControllerMovement)
|
224
|
+
if (!movement && enabled) {
|
225
|
+
movement = this.gameObject.addNewComponent(XRControllerMovement)!;
|
226
|
+
this._createdComponentsInSession.push(movement);
|
519
227
|
}
|
520
|
-
|
521
|
-
|
522
|
-
this._originalXRRigParent?.add(this.rig);
|
523
|
-
this.rig.position.copy(this._originalXRRigPosition);
|
524
|
-
this.rig.quaternion.copy(this._originalXRRigRotation);
|
525
|
-
}
|
526
|
-
|
527
|
-
XRState.Global.Set(XRStateFlag.Browser | XRStateFlag.ThirdPerson);
|
528
|
-
WebXR.events.dispatchEvent({ type: WebXREvent.XRStopped, session: session });
|
228
|
+
if (movement) movement.enabled = enabled;
|
229
|
+
return movement;
|
529
230
|
}
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
this.
|
231
|
+
/** Call to enable or disable default controller rendering */
|
232
|
+
setDefaultControllerRenderingEnabled(enabled: boolean): XRControllerModel | null {
|
233
|
+
let models = this.gameObject.getComponent(XRControllerModel);
|
234
|
+
if (!models && enabled) {
|
235
|
+
models = this.gameObject.addNewComponent(XRControllerModel)!;
|
236
|
+
this._createdComponentsInSession.push(models);
|
237
|
+
models.createControllerModel = this.showControllerModels;
|
238
|
+
models.createHandModel == this.showHandModels;
|
537
239
|
}
|
538
|
-
|
240
|
+
if (models) models.enabled = enabled;
|
241
|
+
return models;
|
539
242
|
}
|
540
243
|
|
541
|
-
private xrMirrorWindow: Window | null = null;
|
542
244
|
|
543
|
-
private onEnterXR_HandleMirrorWindow(session: XRSession) {
|
544
|
-
if (!getParam("mirror")) return;
|
545
|
-
setTimeout(() => {
|
546
|
-
if (!WebXR.IsInWebXR) return;
|
547
|
-
const url = new URL(window.location.href);
|
548
|
-
setOrAddParamsToUrl(url.searchParams, noVoip, 1);
|
549
|
-
setOrAddParamsToUrl(url.searchParams, "isMirror", 1);
|
550
|
-
const str = url.toString();
|
551
|
-
this.xrMirrorWindow = window.open(str, "webxr sync", "popup=yes");
|
552
|
-
if (this.xrMirrorWindow) {
|
553
|
-
this.xrMirrorWindow.onload = () => {
|
554
|
-
if (this.xrMirrorWindow)
|
555
|
-
this.xrMirrorWindow.onbeforeunload = () => {
|
556
|
-
if (WebXR.IsInWebXR)
|
557
|
-
session.end();
|
558
|
-
};
|
559
|
-
}
|
560
|
-
}
|
561
|
-
}, 1000);
|
562
|
-
}
|
563
|
-
}
|
564
245
|
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
get webxr(): WebXR { return this._webxr; }
|
571
|
-
|
572
|
-
private _webxr: WebXR;
|
573
|
-
|
574
|
-
private reticle: Object3D | null = null;
|
575
|
-
private reticleParent: Object3D | null = null;
|
576
|
-
private hitTestSource: XRHitTestSource | null = null;
|
577
|
-
private reticleActive: boolean = true;
|
578
|
-
|
579
|
-
// scene.background before entering AR
|
580
|
-
private previousBackground: Color | null | Texture = null;
|
581
|
-
private previousEnvironment: Texture | null = null;
|
582
|
-
|
583
|
-
private sessionRoot: WebARSessionRoot | null = null;
|
584
|
-
private _previousParent: Object3D | null = null;
|
585
|
-
// we need this in case the session root is on the same object as the webxr component
|
586
|
-
// so if we disable the session root we attach the webxr component to this temporary object
|
587
|
-
// to still receive updates
|
588
|
-
private static tempWebXRObject: Object3D;
|
589
|
-
|
590
|
-
private get context() { return this.webxr.context; }
|
591
|
-
|
592
|
-
constructor(webxr: WebXR) {
|
593
|
-
this._webxr = webxr;
|
246
|
+
protected async createLocalAvatar(xr: NeedleXRSession) {
|
247
|
+
if (this._playerSync && xr.running) {
|
248
|
+
this._playerSync.asset = this.defaultAvatar;
|
249
|
+
await this._playerSync.getInstance();
|
250
|
+
}
|
594
251
|
}
|
595
252
|
|
596
|
-
private
|
597
|
-
|
598
|
-
|
599
|
-
|
253
|
+
private onAvatarSpawned = (instance: GameObject) => {
|
254
|
+
// spawned webxr avatars must have a avatar component
|
255
|
+
if (debug) console.log("WebXR.onAvatarSpawned", instance);
|
256
|
+
GameObject.getOrAddComponent(instance, Avatar);
|
257
|
+
};
|
600
258
|
|
601
|
-
getAROverlayContainer(): HTMLElement | null {
|
602
|
-
this.arDomOverlay = this.webxr.context.domElement as HTMLElement;
|
603
|
-
// for react cases we dont have an Engine Element
|
604
|
-
const element: any = this.arDomOverlay;
|
605
|
-
if (element.getAROverlayContainer)
|
606
|
-
this.arOverlayElement = element.getAROverlayContainer();
|
607
|
-
else this.arOverlayElement = this.arDomOverlay;
|
608
|
-
return this.arOverlayElement;
|
609
|
-
}
|
610
259
|
|
611
|
-
setReticleActive(active: boolean) {
|
612
|
-
this.reticleActive = active;
|
613
|
-
}
|
614
260
|
|
615
|
-
async onBegin(session: XRSession) {
|
616
|
-
const context = this.webxr.context;
|
617
|
-
this.reticleActive = true;
|
618
|
-
this.didPlaceARSessionRoot = false;
|
619
|
-
this.getAROverlayContainer();
|
620
261
|
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
262
|
+
// HTML UI
|
263
|
+
/** Calling this function will get the Needle WebXR button container (it will be created if it doesnt exist yet)
|
264
|
+
* @returns the Needle WebXR button container */
|
265
|
+
getButtonsContainer(): NeedleWebXRHtmlElement {
|
266
|
+
if (!this._container) {
|
267
|
+
this._container = NeedleWebXRHtmlElement.getOrCreate(this.context);
|
625
268
|
}
|
269
|
+
return this._container;
|
270
|
+
}
|
626
271
|
|
627
|
-
|
628
|
-
|
629
|
-
if (!this.sessionRoot) {
|
630
|
-
// TODO: adding it on the scene directly doesnt work (probably because then everything in the scene is disabled including this component). See code a bit furhter below where we add this component to a temporary object inside the scene
|
631
|
-
const obj = this.webxr.gameObject;
|
632
|
-
this.sessionRoot = GameObject.addNewComponent(obj, WebARSessionRoot);
|
633
|
-
console.warn("WebAR: No ARSessionRoot found, creating one automatically on the WebXR object");
|
634
|
-
}
|
272
|
+
private _container?: NeedleWebXRHtmlElement;
|
273
|
+
private handleCreatingHTML() {
|
635
274
|
|
636
|
-
this.
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
this.hitTestSource = source;
|
643
|
-
}).catch((err) => {
|
644
|
-
this.noHitTestAvailable = true;
|
645
|
-
console.warn("WebXR: Hit test not supported", err);
|
646
|
-
});
|
647
|
-
});
|
648
|
-
|
649
|
-
if (!this.reticle && this.sessionRoot) {
|
650
|
-
this.reticle = new Mesh(
|
651
|
-
new RingGeometry(0.07, 0.09, 32).rotateX(- Math.PI / 2),
|
652
|
-
new MeshBasicMaterial()
|
653
|
-
);
|
654
|
-
this.reticle.name = "AR Placement reticle";
|
655
|
-
this.reticle.matrixAutoUpdate = false;
|
656
|
-
this.reticle.visible = false;
|
657
|
-
|
658
|
-
// create AR reticle parent to allow WebXRSessionRoot to be translated, rotated or scaled
|
659
|
-
this.reticleParent = new Object3D();
|
660
|
-
this.reticleParent.name = "AR Reticle Parent";
|
661
|
-
this.reticleParent.matrixAutoUpdate = false;
|
662
|
-
this.reticleParent.add(this.reticle);
|
663
|
-
// this.reticleParent.matrix.copy(this.sessionRoot.gameObject.matrixWorld);
|
664
|
-
|
665
|
-
if (this.webxr.scene) {
|
666
|
-
this.context.scene.add(this.reticleParent);
|
667
|
-
// this.context.scene.add(this.reticle);
|
668
|
-
this.context.scene.visible = true;
|
275
|
+
if (this.createARButton || this.createVRButton || this.useQuicklookExport) {
|
276
|
+
// Quicklook / iOS
|
277
|
+
if ((isiOS() && isSafari()) || debugQuicklook) {
|
278
|
+
if (this.useQuicklookExport) {
|
279
|
+
this.getButtonsContainer().createQuicklookButton();
|
280
|
+
}
|
669
281
|
}
|
670
|
-
|
282
|
+
// WebXR
|
283
|
+
if (this.createARButton) this.getButtonsContainer().createARButton();
|
284
|
+
if (this.createVRButton) this.getButtonsContainer().createVRButton();
|
671
285
|
}
|
672
286
|
|
673
|
-
this.
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
if (this.sessionRoot) {
|
679
|
-
this.sessionRoot.webAR = this;
|
680
|
-
this.sessionRoot?.onBegin(session);
|
287
|
+
if (this.createSendToQuestButton && !isQuest()) {
|
288
|
+
NeedleXRSession.isVRSupported().then(supported => {
|
289
|
+
if (!supported) this.getButtonsContainer().createSendToQuestButton();
|
290
|
+
});
|
681
291
|
}
|
682
|
-
else console.warn("No WebARSessionRoot found in scene")
|
683
292
|
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
}
|
689
|
-
|
690
|
-
onEnd(session: XRSession) {
|
691
|
-
if (this._previousParent) {
|
692
|
-
GameObject.addComponent(this._previousParent as GameObject, this.webxr);
|
693
|
-
this._previousParent = null;
|
293
|
+
if (this.createQRCode && !isMobileDevice()) {
|
294
|
+
NeedleXRSession.isXRSupported().then(supported => {
|
295
|
+
if (isDesktop() || !supported) this.getButtonsContainer().createQRCode();
|
296
|
+
});
|
694
297
|
}
|
695
|
-
this.hitTestSource = null;
|
696
|
-
const context = this.webxr.context;
|
697
|
-
context.scene.background = this.previousBackground;
|
698
|
-
context.scene.environment = this.previousEnvironment;
|
699
|
-
if (this.sessionRoot) {
|
700
|
-
this.sessionRoot.onEnd(this.webxr.Rig, session);
|
701
|
-
}
|
702
|
-
|
703
|
-
const el = this.context.domElement as INeedleEngineComponent;
|
704
|
-
el.onExitAR?.call(el, session);
|
705
|
-
|
706
|
-
this.context.mainCameraComponent?.applyClearFlagsIfIsActiveCamera();
|
707
298
|
}
|
708
299
|
|
709
|
-
onUpdate(session: XRSession, frame: XRFrame) {
|
710
300
|
|
711
|
-
if (this.noHitTestAvailable === true) {
|
712
|
-
if (this.reticle)
|
713
|
-
this.reticle.visible = false;
|
714
|
-
if (!this.didPlaceARSessionRoot) {
|
715
|
-
this.didPlaceARSessionRoot = true;
|
716
|
-
const rig = this.webxr.Rig;
|
717
|
-
const placementMatrix = arPlacementWithoutHitTestMatrix.clone();
|
718
|
-
// if (rig) {
|
719
|
-
// const positionFromRig = new Vector3(0, 0, 0).add(rig.position).divideScalar(this.sessionRoot?.arScale ?? 1);
|
720
|
-
// placementMatrix.multiply(new Matrix4().makeTranslation(positionFromRig.x, positionFromRig.y, positionFromRig.z));
|
721
|
-
// // placementMatrix.setPosition(positionFromRig);
|
722
|
-
// }
|
723
|
-
this.sessionRoot?.placeAt(rig, placementMatrix);
|
724
|
-
}
|
725
|
-
return;
|
726
|
-
}
|
727
|
-
|
728
|
-
if (!this.hitTestSource) return;
|
729
|
-
const hitTestResults = frame.getHitTestResults(this.hitTestSource);
|
730
|
-
if (hitTestResults.length) {
|
731
|
-
const hit = hitTestResults[0];
|
732
|
-
const referenceSpace = this.webxr.context.renderer.xr.getReferenceSpace();
|
733
|
-
if (referenceSpace) {
|
734
|
-
const pose = hit.getPose(referenceSpace);
|
735
|
-
|
736
|
-
if (this.sessionRoot) {
|
737
|
-
const didPlace = this.sessionRoot.onUpdate(this.webxr.Rig, session, hit, pose);
|
738
|
-
this.didPlaceARSessionRoot = didPlace;
|
739
|
-
}
|
740
|
-
|
741
|
-
if (this.reticle) {
|
742
|
-
this.reticle.visible = this.reticleActive && this._webxr.allowARPlacementReticle;
|
743
|
-
if (this.reticleActive) {
|
744
|
-
if (pose) {
|
745
|
-
const matrix = pose.transform.matrix;
|
746
|
-
this.reticle.matrix.fromArray(matrix);
|
747
|
-
if (this.webxr.Rig)
|
748
|
-
this.reticle.matrix.premultiply(this.webxr.Rig.matrix);
|
749
|
-
}
|
750
|
-
}
|
751
|
-
}
|
752
|
-
}
|
753
|
-
|
754
|
-
} else {
|
755
|
-
this.sessionRoot?.onUpdate(this.webxr.Rig, session, null, null);
|
756
|
-
if (this.reticle)
|
757
|
-
this.reticle.visible = false;
|
758
|
-
}
|
759
|
-
}
|
760
301
|
}
|
761
|
-
|
762
|
-
const arPlacementWithoutHitTestMatrix = new Matrix4().identity().makeTranslation(0, 0, 0);
|
@@ -1,16 +1,8 @@
|
|
1
|
-
import { Behaviour, GameObject } from "../Component.js";
|
2
|
-
import { WebXR } from "./WebXR.js";
|
3
|
-
import { Quaternion, Vector3 } from "three";
|
4
|
-
import { AvatarLoader } from "../AvatarLoader.js";
|
5
|
-
import { XRFlag, XRStateFlag } from "../XRFlag.js";
|
6
|
-
import { Avatar_POI } from "../avatar/Avatar_Brain_LookAt.js";
|
7
|
-
import { Context } from "../../engine/engine_setup.js";
|
8
|
-
import { AssetReference } from "../../engine/engine_addressables.js";
|
9
1
|
import { Object3D } from "three";
|
10
|
-
|
2
|
+
|
11
3
|
import { getParam } from "../../engine/engine_utils.js";
|
12
|
-
import {
|
13
|
-
import {
|
4
|
+
import { Behaviour, GameObject } from "../Component.js";
|
5
|
+
import { XRFlag } from "./XRFlag.js";
|
14
6
|
|
15
7
|
export const debug = getParam("debugavatar");
|
16
8
|
|
@@ -19,6 +11,12 @@
|
|
19
11
|
gameObject: Object3D;
|
20
12
|
}
|
21
13
|
|
14
|
+
/**
|
15
|
+
* This is used to mark an object being controlled / owned by a player
|
16
|
+
* This system might be refactored and moved to a more centralized place in a future version
|
17
|
+
*/
|
18
|
+
// We might be updating this system in the future to a centralized API (PlayerView)
|
19
|
+
// but since currently quite a few core components rely on it, we're keeping it for now
|
22
20
|
export class AvatarMarker extends Behaviour {
|
23
21
|
|
24
22
|
public static getAvatar(index: number): AvatarMarker | null {
|
@@ -44,7 +42,7 @@
|
|
44
42
|
|
45
43
|
|
46
44
|
public connectionId!: string;
|
47
|
-
public avatar?:
|
45
|
+
public avatar?: Object3D & { flags?: XRFlag[] }
|
48
46
|
|
49
47
|
awake() {
|
50
48
|
AvatarMarker.instances.push(this);
|
@@ -65,292 +63,4 @@
|
|
65
63
|
isLocalAvatar() {
|
66
64
|
return this.connectionId === this.context.connection.connectionId;
|
67
65
|
}
|
68
|
-
|
69
|
-
setVisible(visible: boolean) {
|
70
|
-
if (this.avatar) {
|
71
|
-
if ("setVisible" in this.avatar)
|
72
|
-
this.avatar.setVisible(visible);
|
73
|
-
else {
|
74
|
-
GameObject.setActive(this.avatar, visible);
|
75
|
-
}
|
76
|
-
}
|
77
|
-
}
|
78
66
|
}
|
79
|
-
|
80
|
-
|
81
|
-
export class WebXRAvatar {
|
82
|
-
private static loader: AvatarLoader = new AvatarLoader();
|
83
|
-
|
84
|
-
private _isVisible: boolean = true;
|
85
|
-
setVisible(visible: boolean) {
|
86
|
-
this._isVisible = visible;
|
87
|
-
this.updateVisibility();
|
88
|
-
}
|
89
|
-
|
90
|
-
get isWebXRAvatar() { return true; }
|
91
|
-
|
92
|
-
// TODO: set layers on all avatars
|
93
|
-
/** the user id */
|
94
|
-
public guid: string;
|
95
|
-
|
96
|
-
private root: Object3D | null = null;
|
97
|
-
public head: Object3D | null = null;
|
98
|
-
public handLeft: Object3D | null = null;
|
99
|
-
public handRight: Object3D | null = null;
|
100
|
-
public lastUpdate: number = -1;
|
101
|
-
public isLocalAvatar: boolean = false;
|
102
|
-
public flags: XRFlag[] | null = null;
|
103
|
-
private headScale: Vector3 = new Vector3(1, 1, 1);
|
104
|
-
private handLeftScale: Vector3 = new Vector3(1, 1, 1);
|
105
|
-
private handRightScale: Vector3 = new Vector3(1, 1, 1);
|
106
|
-
|
107
|
-
private readonly webxr: WebXR;
|
108
|
-
|
109
|
-
private lastAvatarId: string | null = null;
|
110
|
-
private hasAvatarOverride: boolean = false;
|
111
|
-
|
112
|
-
|
113
|
-
private context: Context;
|
114
|
-
private avatarMarker: AvatarMarker | null = null;
|
115
|
-
|
116
|
-
constructor(context: Context, guid: string, webXR: WebXR) {
|
117
|
-
this.context = context;
|
118
|
-
this.guid = guid;
|
119
|
-
this.webxr = webXR;
|
120
|
-
this.setupCustomAvatar(this.webxr.defaultAvatar);
|
121
|
-
}
|
122
|
-
|
123
|
-
public updateFlags() {
|
124
|
-
if (!this.flags)
|
125
|
-
return;
|
126
|
-
let mask = this.isLocalAvatar ? XRStateFlag.FirstPerson : XRStateFlag.ThirdPerson;
|
127
|
-
if (this.context.isInVR)
|
128
|
-
mask |= XRStateFlag.VR;
|
129
|
-
else if (this.context.isInAR)
|
130
|
-
mask |= XRStateFlag.AR;
|
131
|
-
else
|
132
|
-
mask |= XRStateFlag.Browser;
|
133
|
-
for (const f of this.flags) {
|
134
|
-
f.gameObject.visible = true;
|
135
|
-
f.UpdateVisible(mask);
|
136
|
-
}
|
137
|
-
}
|
138
|
-
|
139
|
-
public async setAvatarOverride(avatarId: string | null): Promise<boolean | null> {
|
140
|
-
this.hasAvatarOverride = avatarId !== null;
|
141
|
-
if (this.hasAvatarOverride && this.lastAvatarId !== avatarId) {
|
142
|
-
this.lastAvatarId = avatarId;
|
143
|
-
if (avatarId != null && avatarId.length > 0)
|
144
|
-
return await this.setupCustomAvatar(avatarId);
|
145
|
-
}
|
146
|
-
return null;
|
147
|
-
}
|
148
|
-
|
149
|
-
private _headTarget: Object3D = new Object3D();
|
150
|
-
private _handLeftTarget: Object3D = new Object3D();
|
151
|
-
private _handRightTarget: Object3D = new Object3D();
|
152
|
-
private _canInterpolate: boolean = false;
|
153
|
-
|
154
|
-
private static invertRotation: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
155
|
-
|
156
|
-
public tryUpdate(state: VRUserState, _timeDiff: number) {
|
157
|
-
if (state.guid === this.guid) {
|
158
|
-
|
159
|
-
if (this.lastAvatarId !== state.avatarId && state.avatarId && state.avatarId.length > 0) {
|
160
|
-
this.lastAvatarId = state.avatarId;
|
161
|
-
this.setupCustomAvatar(state.avatarId);
|
162
|
-
}
|
163
|
-
|
164
|
-
this.lastUpdate = state.time;
|
165
|
-
if (this.head) {
|
166
|
-
|
167
|
-
const device = this.webxr.IsInAR ? ViewDevice.Handheld : ViewDevice.Headset;
|
168
|
-
const viewObj = this.head;
|
169
|
-
// if (this.isLocalAvatar) {
|
170
|
-
// if (this.context.mainCamera && this.context.isInXR) {
|
171
|
-
// viewObj = this.context.renderer.xr.getCamera(this.context.mainCamera);
|
172
|
-
// }
|
173
|
-
// }
|
174
|
-
this.context.players.setPlayerView(state.guid, viewObj, device);
|
175
|
-
|
176
|
-
InstancingUtil.markDirty(this.head);
|
177
|
-
|
178
|
-
this._canInterpolate = true;
|
179
|
-
const ht = this.isLocalAvatar ? this.head : this._headTarget;
|
180
|
-
ht.position.set(state.position.x, state.position.y, state.position.z);
|
181
|
-
// not sure how position in local space can be correct but rotation is wrong / offset when parent rotates
|
182
|
-
ht.quaternion.set(state.rotation.x, state.rotation.y, state.rotation.z, state.rotation.w);
|
183
|
-
ht.scale.set(state.scale, state.scale, state.scale);
|
184
|
-
ht.scale.multiply(this.headScale);
|
185
|
-
|
186
|
-
if (this.handLeft) {
|
187
|
-
const ht = this.isLocalAvatar ? this.handLeft : this._handLeftTarget;
|
188
|
-
ht.position.set(state.posLeftHand.x, state.posLeftHand.y, state.posLeftHand.z);
|
189
|
-
ht.quaternion.set(state.rotLeftHand["_x"], state.rotLeftHand["_y"], state.rotLeftHand["_z"], state.rotLeftHand["_w"]);
|
190
|
-
ht.quaternion.multiply(WebXRAvatar.invertRotation);
|
191
|
-
ht.scale.set(state.scale, state.scale, state.scale);
|
192
|
-
ht.scale.multiply(this.handLeftScale);
|
193
|
-
InstancingUtil.markDirty(this.handLeft);
|
194
|
-
}
|
195
|
-
|
196
|
-
if (this.handRight) {
|
197
|
-
const ht = this.isLocalAvatar ? this.handRight : this._handRightTarget;
|
198
|
-
ht.position.set(state.posRightHand.x, state.posRightHand.y, state.posRightHand.z);
|
199
|
-
ht.quaternion.set(state.rotRightHand["_x"], state.rotRightHand["_y"], state.rotRightHand["_z"], state.rotRightHand["_w"]);
|
200
|
-
ht.quaternion.multiply(WebXRAvatar.invertRotation);
|
201
|
-
ht.scale.set(state.scale, state.scale, state.scale);
|
202
|
-
ht.scale.multiply(this.handRightScale);
|
203
|
-
InstancingUtil.markDirty(this.handRight);
|
204
|
-
}
|
205
|
-
}
|
206
|
-
}
|
207
|
-
}
|
208
|
-
|
209
|
-
public update() {
|
210
|
-
if (this.isLocalAvatar)
|
211
|
-
return;
|
212
|
-
if (!this._canInterpolate)
|
213
|
-
return;
|
214
|
-
const t = this.context.time.deltaTime / .1;
|
215
|
-
if (this.head) {
|
216
|
-
this.head.position.lerp(this._headTarget.position, t);
|
217
|
-
this.head.quaternion.slerp(this._headTarget.quaternion, t);
|
218
|
-
this.head.scale.lerp(this._headTarget.scale, t);
|
219
|
-
}
|
220
|
-
if (this.handLeft && this._handLeftTarget) {
|
221
|
-
this.handLeft.position.lerp(this._handLeftTarget.position, t);
|
222
|
-
this.handLeft.quaternion.slerp(this._handLeftTarget.quaternion, t);
|
223
|
-
this.handLeft.scale.lerp(this._handLeftTarget.scale, t);
|
224
|
-
}
|
225
|
-
if (this.handRight && this._handRightTarget) {
|
226
|
-
this.handRight.position.lerp(this._handRightTarget.position, t);
|
227
|
-
this.handRight.quaternion.slerp(this._handRightTarget.quaternion, t);
|
228
|
-
this.handRight.scale.lerp(this._handRightTarget.scale, t);
|
229
|
-
}
|
230
|
-
}
|
231
|
-
|
232
|
-
public destroy() {
|
233
|
-
if (debug)
|
234
|
-
console.log("Destroy avatar", this.guid);
|
235
|
-
this.root?.removeFromParent();
|
236
|
-
this.avatarMarker?.destroy();
|
237
|
-
this.lastAvatarId = null;
|
238
|
-
|
239
|
-
if (this.head) {
|
240
|
-
Avatar_POI.Remove(this.context, this.head);
|
241
|
-
}
|
242
|
-
// this.head?.removeFromParent();
|
243
|
-
// this.handLeft?.removeFromParent();
|
244
|
-
// this.handRight?.removeFromParent();
|
245
|
-
}
|
246
|
-
|
247
|
-
private updateVisibility() {
|
248
|
-
const root = this.root;
|
249
|
-
if (root) {
|
250
|
-
GameObject.setActive(root, this._isVisible);
|
251
|
-
}
|
252
|
-
}
|
253
|
-
|
254
|
-
private async setupCustomAvatar(avatarId: string | Object3D | AssetReference | undefined): Promise<boolean> {
|
255
|
-
if (debug)
|
256
|
-
console.log("LOAD", avatarId, this);
|
257
|
-
|
258
|
-
if (!avatarId || (typeof avatarId === "string" && avatarId.length <= 0))
|
259
|
-
return false;
|
260
|
-
|
261
|
-
if (this.head) {
|
262
|
-
Avatar_POI.Remove(this.context, this.head);
|
263
|
-
}
|
264
|
-
|
265
|
-
const reference = avatarId as AssetReference;
|
266
|
-
if (reference?.loadAssetAsync !== undefined) {
|
267
|
-
await reference.loadAssetAsync();
|
268
|
-
const prefab = reference.asset as Object3D;
|
269
|
-
GameObject.setActive(prefab, false);
|
270
|
-
avatarId = GameObject.instantiate(prefab as Object3D) as Object3D;
|
271
|
-
GameObject.setActive(avatarId, true);
|
272
|
-
// console.log("Avatar", avatarId);
|
273
|
-
}
|
274
|
-
if (debug)
|
275
|
-
console.log(avatarId);
|
276
|
-
|
277
|
-
const model = await WebXRAvatar.loader.getOrCreateNewAvatarInstance(this.context, avatarId as (Object3D | string));
|
278
|
-
if (debug)
|
279
|
-
console.log(model, model?.isValid, this.lastAvatarId, avatarId);
|
280
|
-
// if (this.lastAvatarId !== avatarId) {
|
281
|
-
// // avatar id changed in the meantime
|
282
|
-
// return true;
|
283
|
-
// }
|
284
|
-
if (model?.isValid) {
|
285
|
-
this.root = model.root;
|
286
|
-
|
287
|
-
this.root.position.set(0, 0, 0);
|
288
|
-
this.root.quaternion.set(0, 0, 0, 1);
|
289
|
-
this.root.scale.set(1, 1, 1); // should we allow a scaled avatar root?!
|
290
|
-
|
291
|
-
this.avatarMarker = GameObject.addNewComponent(this.root as GameObject, AvatarMarker) as AvatarMarker;
|
292
|
-
this.avatarMarker.connectionId = this.guid;
|
293
|
-
this.avatarMarker.avatar = this;
|
294
|
-
|
295
|
-
if (this.head && this.head !== model.head)
|
296
|
-
this.head?.removeFromParent();
|
297
|
-
this.head = model.head;
|
298
|
-
this.headScale.copy(this.head.scale);
|
299
|
-
|
300
|
-
if (this.head && !this.isLocalAvatar) {
|
301
|
-
Avatar_POI.Add(this.context, this.head, this.avatarMarker);
|
302
|
-
}
|
303
|
-
|
304
|
-
if (model.leftHand)
|
305
|
-
this.handLeft?.removeFromParent();
|
306
|
-
this.handLeft = model.leftHand ?? this.handLeft;
|
307
|
-
if (this.handLeft)
|
308
|
-
this.handLeftScale.copy(this.handLeft.scale);
|
309
|
-
else
|
310
|
-
this.handLeftScale.set(1, 1, 1);
|
311
|
-
|
312
|
-
if (model.rigthHand)
|
313
|
-
this.handRight?.removeFromParent();
|
314
|
-
this.handRight = model.rigthHand ?? this.handRight;
|
315
|
-
if (this.handRight)
|
316
|
-
this.handRightScale.copy(this.handRight.scale);
|
317
|
-
else
|
318
|
-
this.handRightScale.set(1, 1, 1);
|
319
|
-
|
320
|
-
|
321
|
-
this.context.scene.add(this.root);
|
322
|
-
// scene.add(this.handLeft);
|
323
|
-
// scene.add(this.handRight);
|
324
|
-
// this.mouthShapes = null;
|
325
|
-
// this.needSearchEyes = true;
|
326
|
-
if (this.flags == null)
|
327
|
-
this.flags = [];
|
328
|
-
this.flags.length = 0;
|
329
|
-
this.flags.push(...GameObject.getComponentsInChildren(this.root as GameObject, XRFlag));
|
330
|
-
// if no flags are found add at least a head flag to hide head in first person VR
|
331
|
-
if (this.flags.length <= 0) {
|
332
|
-
if (this.head) {
|
333
|
-
const flag = GameObject.addNewComponent(this.head, XRFlag) as XRFlag;
|
334
|
-
// TODO: the defaults are wrong? should be Desktop | ThirdPerson ?
|
335
|
-
flag.visibleIn = XRStateFlag.ThirdPerson | XRStateFlag.VR;
|
336
|
-
this.flags.push(flag);
|
337
|
-
if (debug)
|
338
|
-
console.log("Added flag to head: " + flag.visibleIn, this.head.name);
|
339
|
-
}
|
340
|
-
}
|
341
|
-
|
342
|
-
if (debug)
|
343
|
-
console.log("[Avatar], is Local? ", this.isLocalAvatar, this.root);
|
344
|
-
this.updateFlags();
|
345
|
-
|
346
|
-
this.updateVisibility();
|
347
|
-
|
348
|
-
return true;
|
349
|
-
}
|
350
|
-
else {
|
351
|
-
if (debug)
|
352
|
-
console.warn("build avatar failed");
|
353
|
-
return false;
|
354
|
-
}
|
355
|
-
}
|
356
|
-
}
|
@@ -1,1168 +0,0 @@
|
|
1
|
-
import { BoxHelper, BufferGeometry, Color, Euler, Group, type Intersection, Layers, Line, LineBasicMaterial, Material, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, Quaternion, Ray, SphereGeometry, Vector2, Vector3 } from "three";
|
2
|
-
import { OculusHandModel } from 'three/examples/jsm/webxr/OculusHandModel.js';
|
3
|
-
import { OculusHandPointerModel } from 'three/examples/jsm/webxr/OculusHandPointerModel.js';
|
4
|
-
import { XRControllerModel, XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
|
5
|
-
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
6
|
-
|
7
|
-
import { InstancingUtil } from "../../engine/engine_instancing.js";
|
8
|
-
import { Mathf } from "../../engine/engine_math.js";
|
9
|
-
import { RaycastOptions } from "../../engine/engine_physics.js";
|
10
|
-
import { getWorldPosition, getWorldQuaternion, setWorldPosition, setWorldQuaternion } from "../../engine/engine_three_utils.js";
|
11
|
-
import { getParam, resolveUrl } from "../../engine/engine_utils.js";
|
12
|
-
import { addDracoAndKTX2Loaders } from "../../engine/engine_loaders.js";
|
13
|
-
|
14
|
-
import { Avatar_POI } from "../avatar/Avatar_Brain_LookAt.js";
|
15
|
-
import { Behaviour, GameObject } from "../Component.js";
|
16
|
-
import { Interactable, UsageMarker } from "../Interactable.js";
|
17
|
-
import { Rigidbody } from "../RigidBody.js";
|
18
|
-
import { SyncedTransform } from "../SyncedTransform.js";
|
19
|
-
import { UIRaycastUtils } from "../ui/RaycastUtils.js";
|
20
|
-
import { WebXR } from "./WebXR.js";
|
21
|
-
import { XRRig } from "./WebXRRig.js";
|
22
|
-
import { InputEvents, NEPointerEvent, PointerType } from "../../engine/engine_input.js";
|
23
|
-
|
24
|
-
const debug = getParam("debugwebxrcontroller");
|
25
|
-
|
26
|
-
export enum ControllerType {
|
27
|
-
PhysicalDevice = 0,
|
28
|
-
Touch = 1,
|
29
|
-
}
|
30
|
-
|
31
|
-
export enum ControllerEvents {
|
32
|
-
SelectStart = "select-start",
|
33
|
-
SelectEnd = "select-end",
|
34
|
-
Update = "update",
|
35
|
-
}
|
36
|
-
|
37
|
-
export class TeleportTarget extends Behaviour {
|
38
|
-
|
39
|
-
}
|
40
|
-
|
41
|
-
export class WebXRController extends Behaviour {
|
42
|
-
|
43
|
-
public static Factory: XRControllerModelFactory = new XRControllerModelFactory();
|
44
|
-
|
45
|
-
private static raycastColor: Color = new Color(.9, .3, .3);
|
46
|
-
private static raycastNoHitColor: Color = new Color(.6, .6, .6);
|
47
|
-
private static geometry = new BufferGeometry().setFromPoints([new Vector3(0, 0, 0), new Vector3(0, 0, -1)]);
|
48
|
-
private static handModels: { [index: number]: OculusHandPointerModel } = {};
|
49
|
-
|
50
|
-
private static CreateRaycastLine(): Line {
|
51
|
-
const line = new Line(this.geometry);
|
52
|
-
const mat = line.material as LineBasicMaterial;
|
53
|
-
mat.color = this.raycastColor;
|
54
|
-
// mat.linewidth = 10;
|
55
|
-
line.layers.set(2);
|
56
|
-
line.name = 'line';
|
57
|
-
line.scale.z = 1;
|
58
|
-
return line;
|
59
|
-
}
|
60
|
-
|
61
|
-
private static CreateRaycastHitPoint(): Mesh {
|
62
|
-
const geometry = new SphereGeometry(.5, 22, 22);
|
63
|
-
const material = new MeshBasicMaterial({ color: this.raycastColor });
|
64
|
-
const sphere = new Mesh(geometry, material);
|
65
|
-
sphere.visible = false;
|
66
|
-
sphere.layers.set(2);
|
67
|
-
return sphere;
|
68
|
-
}
|
69
|
-
|
70
|
-
public static Create(owner: WebXR, index: number, addTo: GameObject, type: ControllerType): WebXRController {
|
71
|
-
const ctrl = addTo ? GameObject.addNewComponent(addTo, WebXRController, false) : new WebXRController();
|
72
|
-
|
73
|
-
ctrl.webXR = owner;
|
74
|
-
ctrl.index = index;
|
75
|
-
ctrl.type = type;
|
76
|
-
|
77
|
-
const context = owner.context;
|
78
|
-
// from https://github.com/mrdoob/js/blob/master/examples/webxr_vr_dragging.html
|
79
|
-
// controllers
|
80
|
-
ctrl.controller = context.renderer.xr.getController(index);
|
81
|
-
ctrl.controllerGrip = context.renderer.xr.getControllerGrip(index);
|
82
|
-
ctrl.controllerModel = this.Factory.createControllerModel(ctrl.controller);
|
83
|
-
ctrl.controllerGrip.add(ctrl.controllerModel);
|
84
|
-
|
85
|
-
ctrl.hand = context.renderer.xr.getHand(index);
|
86
|
-
|
87
|
-
const loader = new GLTFLoader();
|
88
|
-
addDracoAndKTX2Loaders(loader, context);
|
89
|
-
if (ctrl.webXR.handModelPath && ctrl.webXR.handModelPath !== "")
|
90
|
-
loader.setPath(resolveUrl(owner.sourceId, ctrl.webXR.handModelPath));
|
91
|
-
else
|
92
|
-
// from XRHandMeshModel.js
|
93
|
-
loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/generic-hand/');
|
94
|
-
//@ts-ignore
|
95
|
-
const hand = new OculusHandModel(ctrl.hand, loader);
|
96
|
-
|
97
|
-
ctrl.hand.add(hand);
|
98
|
-
ctrl.hand.traverse(x => x.layers.set(2));
|
99
|
-
|
100
|
-
ctrl.handPointerModel = new OculusHandPointerModel(ctrl.hand, ctrl.controller);
|
101
|
-
|
102
|
-
|
103
|
-
// TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
|
104
|
-
ctrl.controller.addEventListener('connected', (_) => {
|
105
|
-
ctrl.setControllerLayers(ctrl.controllerModel, 2);
|
106
|
-
ctrl.setControllerLayers(ctrl.controllerGrip, 2);
|
107
|
-
ctrl.setControllerLayers(ctrl.hand, 2);
|
108
|
-
setTimeout(() => {
|
109
|
-
ctrl.setControllerLayers(ctrl.controllerModel, 2);
|
110
|
-
ctrl.setControllerLayers(ctrl.controllerGrip, 2);
|
111
|
-
ctrl.setControllerLayers(ctrl.hand, 2);
|
112
|
-
}, 1000);
|
113
|
-
});
|
114
|
-
|
115
|
-
// TODO: unsubscribe! this should be moved into onenable and ondisable!
|
116
|
-
// TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
|
117
|
-
ctrl.hand.addEventListener('connected', (event) => {
|
118
|
-
const xrInputSource = event.data;
|
119
|
-
if (xrInputSource.hand) {
|
120
|
-
if (owner.Rig) owner.Rig.add(ctrl.hand);
|
121
|
-
ctrl.type = ControllerType.PhysicalDevice;
|
122
|
-
ctrl.handPointerModel.traverse(x => x.layers.set(2)); // ignore raycast
|
123
|
-
ctrl.handPointerModel.pointerObject?.traverse(x => x.layers.set(2));
|
124
|
-
|
125
|
-
// when exiting and re-entering xr the joints are not parented to the hand anymore
|
126
|
-
// this is a workaround to fix that temporarely
|
127
|
-
// see https://github.com/needle-tools/needle-tiny-playground/issues/123
|
128
|
-
const jnts = ctrl.hand["joints"];
|
129
|
-
if (jnts) {
|
130
|
-
for (const key of Object.keys(jnts)) {
|
131
|
-
const joint = jnts[key];
|
132
|
-
if (joint.parent) continue;
|
133
|
-
ctrl.hand.add(joint);
|
134
|
-
}
|
135
|
-
}
|
136
|
-
}
|
137
|
-
});
|
138
|
-
|
139
|
-
return ctrl;
|
140
|
-
}
|
141
|
-
|
142
|
-
// TODO: replace with component events
|
143
|
-
public static addEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
|
144
|
-
const list = this.eventSubs[evt] ?? [];
|
145
|
-
list.push(callback);
|
146
|
-
this.eventSubs[evt] = list;
|
147
|
-
}
|
148
|
-
|
149
|
-
// TODO: replace with component events
|
150
|
-
public static removeEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
|
151
|
-
if (!callback) return;
|
152
|
-
const list = this.eventSubs[evt] ?? [];
|
153
|
-
const idx = list.indexOf(callback);
|
154
|
-
if (idx >= 0) list.splice(idx, 1);
|
155
|
-
this.eventSubs[evt] = list;
|
156
|
-
}
|
157
|
-
|
158
|
-
private static eventSubs: { [key: string]: Function[] } = {};
|
159
|
-
|
160
|
-
public webXR?: WebXR;
|
161
|
-
public index: number = -1;
|
162
|
-
public controllerModel!: XRControllerModel;
|
163
|
-
public controller!: Group;
|
164
|
-
public controllerGrip!: Group;
|
165
|
-
public hand!: Group;
|
166
|
-
public handPointerModel!: OculusHandPointerModel;
|
167
|
-
public grabbed: AttachedObject | null = null;
|
168
|
-
public input: XRInputSource | null = null;
|
169
|
-
public type: ControllerType = ControllerType.PhysicalDevice;
|
170
|
-
public showRaycastLine: boolean = true;
|
171
|
-
public enableRaycasts: boolean = true;
|
172
|
-
public enableDefaultControls: boolean = true;
|
173
|
-
|
174
|
-
get isUsingHands(): boolean {
|
175
|
-
const r = this.input?.hand;
|
176
|
-
return r !== null && r !== undefined;
|
177
|
-
}
|
178
|
-
|
179
|
-
get wrist(): Object3D | null {
|
180
|
-
if (!this.hand) return null;
|
181
|
-
const jnts = this.hand["joints"];
|
182
|
-
if (!jnts) return null;
|
183
|
-
return jnts["wrist"];
|
184
|
-
}
|
185
|
-
|
186
|
-
private _wristQuaternion: Quaternion | null = null;
|
187
|
-
getWristQuaternion(): Quaternion | null {
|
188
|
-
const wrist = this.wrist;
|
189
|
-
if (!wrist) return null;
|
190
|
-
if (!this._wristQuaternion) this._wristQuaternion = new Quaternion();
|
191
|
-
const wr = getWorldQuaternion(wrist).multiply(this._wristQuaternion.setFromEuler(new Euler(-Math.PI / 4, 0, 0)));
|
192
|
-
return wr;
|
193
|
-
}
|
194
|
-
|
195
|
-
private movementVector: Vector3 = new Vector3();
|
196
|
-
private worldRot: Quaternion = new Quaternion();
|
197
|
-
private joystick: Vector2 = new Vector2();
|
198
|
-
private didRotate: boolean = false;
|
199
|
-
private didTeleport: boolean = false;
|
200
|
-
private didChangeScale: boolean = false;
|
201
|
-
private static PreviousCameraFarDistance: number | undefined = undefined;
|
202
|
-
private static MovementSpeedFactor: number = 1;
|
203
|
-
|
204
|
-
private lastHit: Intersection | null = null;
|
205
|
-
|
206
|
-
private raycastLine: Line | null = null;
|
207
|
-
private _raycastHitPoint: Object3D | null = null;
|
208
|
-
private _connnectedCallback: any | null = null;
|
209
|
-
private _disconnectedCallback: any | null = null;
|
210
|
-
private _selectStartEvt: any | null = null;
|
211
|
-
private _selectEndEvt: any | null = null;
|
212
|
-
|
213
|
-
public get selectionDown(): boolean { return this._selectionPressed && !this._selectionPressedLastFrame; }
|
214
|
-
public get selectionUp(): boolean { return !this._selectionPressed && this._selectionPressedLastFrame; }
|
215
|
-
public get selectionPressed(): boolean { return this._selectionPressed; }
|
216
|
-
public get selectionClick(): boolean { return this._selectionEndTime - this._selectionStartTime < 0.3; }
|
217
|
-
public get raycastHitPoint(): Object3D | null { return this._raycastHitPoint; }
|
218
|
-
|
219
|
-
private _selectionPressed: boolean = false;
|
220
|
-
private _selectionPressedLastFrame: boolean = false;
|
221
|
-
private _selectionStartTime: number = 0;
|
222
|
-
private _selectionEndTime: number = 0;
|
223
|
-
|
224
|
-
public get useSmoothing(): boolean { return this._useSmoothing };
|
225
|
-
private _useSmoothing: boolean = true;
|
226
|
-
|
227
|
-
awake(): void {
|
228
|
-
if (!this.controller) {
|
229
|
-
console.warn("WebXRController: Missing controller object.", this);
|
230
|
-
return;
|
231
|
-
}
|
232
|
-
this._connnectedCallback = this.onSourceConnected.bind(this);
|
233
|
-
this._disconnectedCallback = this.onSourceDisconnected.bind(this);
|
234
|
-
this._selectStartEvt = this.onSelectStart.bind(this);
|
235
|
-
this._selectEndEvt = this.onSelectEnd.bind(this);
|
236
|
-
if (this.type === ControllerType.Touch) {
|
237
|
-
this.controllerGrip.addEventListener("connected", this._connnectedCallback);
|
238
|
-
this.controllerGrip.addEventListener("disconnected", this._disconnectedCallback);
|
239
|
-
this.controller.addEventListener('selectstart', this._selectStartEvt);
|
240
|
-
this.controller.addEventListener('selectend', this._selectEndEvt);
|
241
|
-
}
|
242
|
-
if (this.type === ControllerType.PhysicalDevice) {
|
243
|
-
this.controller.addEventListener('selectstart', this._selectStartEvt);
|
244
|
-
this.controller.addEventListener('selectend', this._selectEndEvt);
|
245
|
-
}
|
246
|
-
}
|
247
|
-
|
248
|
-
onDestroy(): void {
|
249
|
-
if (this.type === ControllerType.Touch) {
|
250
|
-
this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
|
251
|
-
this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
|
252
|
-
this.controller.removeEventListener('selectstart', this._selectStartEvt);
|
253
|
-
this.controller.removeEventListener('selectend', this._selectEndEvt);
|
254
|
-
}
|
255
|
-
if (this.type === ControllerType.PhysicalDevice) {
|
256
|
-
this.controller.removeEventListener('selectstart', this._selectStartEvt);
|
257
|
-
this.controller.removeEventListener('selectend', this._selectEndEvt);
|
258
|
-
}
|
259
|
-
|
260
|
-
this.hand?.clear();
|
261
|
-
this.controllerGrip?.clear();
|
262
|
-
this.controller?.clear();
|
263
|
-
}
|
264
|
-
|
265
|
-
public onEnable(): void {
|
266
|
-
if (!this.webXR) {
|
267
|
-
console.warn("No WebXR component assigned to WebXRController.");
|
268
|
-
return;
|
269
|
-
}
|
270
|
-
|
271
|
-
if (this.hand)
|
272
|
-
this.hand.name = "Hand";
|
273
|
-
if (this.controllerGrip)
|
274
|
-
this.controllerGrip.name = "ControllerGrip";
|
275
|
-
if (this.controller)
|
276
|
-
this.controller.name = "Controller";
|
277
|
-
if (this.raycastLine)
|
278
|
-
this.raycastLine.name = "RaycastLine;"
|
279
|
-
|
280
|
-
if (this.webXR.Controllers.indexOf(this) < 0)
|
281
|
-
this.webXR.Controllers.push(this);
|
282
|
-
|
283
|
-
if (!this.raycastLine)
|
284
|
-
this.raycastLine = WebXRController.CreateRaycastLine();
|
285
|
-
if (!this._raycastHitPoint)
|
286
|
-
this._raycastHitPoint = WebXRController.CreateRaycastHitPoint();
|
287
|
-
|
288
|
-
this.webXR.Rig?.add(this.hand);
|
289
|
-
this.webXR.Rig?.add(this.controllerGrip);
|
290
|
-
this.webXR.Rig?.add(this.controller);
|
291
|
-
this.webXR.Rig?.add(this.raycastLine);
|
292
|
-
this.raycastLine?.add(this._raycastHitPoint);
|
293
|
-
this._raycastHitPoint.visible = false;
|
294
|
-
this.hand.add(this.handPointerModel);
|
295
|
-
if (debug)
|
296
|
-
console.log("ADDED TO RIG", this.webXR.Rig);
|
297
|
-
|
298
|
-
// // console.log("enable", this.index, this.controllerGrip.uuid)
|
299
|
-
}
|
300
|
-
|
301
|
-
onDisable(): void {
|
302
|
-
// console.log("XR controller disabled", this);
|
303
|
-
this.hand?.removeFromParent();
|
304
|
-
this.controllerGrip?.removeFromParent();
|
305
|
-
this.controller?.removeFromParent();
|
306
|
-
this.raycastLine?.removeFromParent();
|
307
|
-
this._raycastHitPoint?.removeFromParent();
|
308
|
-
// console.log("Disable", this._connnectedCallback, this._disconnectedCallback);
|
309
|
-
// this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
|
310
|
-
// this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
|
311
|
-
|
312
|
-
if (this.webXR) {
|
313
|
-
const i = this.webXR.Controllers.indexOf(this);
|
314
|
-
if (i >= 0)
|
315
|
-
this.webXR.Controllers.splice(i, 1);
|
316
|
-
}
|
317
|
-
}
|
318
|
-
|
319
|
-
// onDestroy(): void {
|
320
|
-
// console.log("destroyed", this.index);
|
321
|
-
// }
|
322
|
-
|
323
|
-
private _isConnected: boolean = false;
|
324
|
-
|
325
|
-
private onSourceConnected(e: { data: XRInputSource, target: any }) {
|
326
|
-
if (this._isConnected) {
|
327
|
-
console.warn("Received connected event for controller that is already connected", this.index, e);
|
328
|
-
return;
|
329
|
-
}
|
330
|
-
this._isConnected = true;
|
331
|
-
this.input = e.data;
|
332
|
-
|
333
|
-
if (this.type === ControllerType.Touch) {
|
334
|
-
this.onSelectStart();
|
335
|
-
}
|
336
|
-
}
|
337
|
-
|
338
|
-
private onSourceDisconnected(_e: any) {
|
339
|
-
if (!this._isConnected) {
|
340
|
-
console.warn("Received discnnected event for controller that is not connected", _e);
|
341
|
-
return;
|
342
|
-
}
|
343
|
-
this._isConnected = false;
|
344
|
-
if (this.type === ControllerType.Touch) {
|
345
|
-
this.onSelectEnd();
|
346
|
-
}
|
347
|
-
this.input = null;
|
348
|
-
}
|
349
|
-
|
350
|
-
private createPointerEvent(type: string) {
|
351
|
-
switch (type) {
|
352
|
-
case "down":
|
353
|
-
this.context.input.createPointerDown(new NEPointerEvent(InputEvents.PointerDown, null, { clientX: 0, clientY: 0, button: this.index, pointerType: PointerType.Touch }));
|
354
|
-
break;
|
355
|
-
case "move":
|
356
|
-
break;
|
357
|
-
case "up":
|
358
|
-
this.context.input.createPointerUp(new NEPointerEvent(InputEvents.PointerUp, null, { clientX: 0, clientY: 0, button: this.index, pointerType: PointerType.Touch }));
|
359
|
-
break;
|
360
|
-
}
|
361
|
-
}
|
362
|
-
|
363
|
-
rayRotation: Quaternion = new Quaternion();
|
364
|
-
|
365
|
-
private raycastUpdate(raycastLine: Line, wp: Vector3) {
|
366
|
-
const allowRaycastLineVisible = this.showRaycastLine && this.type !== ControllerType.Touch;
|
367
|
-
if (this.type === ControllerType.Touch) {
|
368
|
-
raycastLine.visible = false;
|
369
|
-
}
|
370
|
-
else if (this.isUsingHands) {
|
371
|
-
raycastLine.visible = !this.grabbed && allowRaycastLineVisible;
|
372
|
-
setWorldPosition(raycastLine, wp);
|
373
|
-
const jnts = this.hand!['joints'];
|
374
|
-
if (jnts) {
|
375
|
-
const wrist = jnts['wrist'];
|
376
|
-
if (wrist && this.grabbed && this.grabbed.isCloseGrab) {
|
377
|
-
const wr = this.getWristQuaternion();
|
378
|
-
if (wr)
|
379
|
-
this.rayRotation.copy(wr);
|
380
|
-
// this.rayRotation.slerp(wr, this.useSmoothing ? t * 2 : 1);
|
381
|
-
}
|
382
|
-
}
|
383
|
-
setWorldQuaternion(raycastLine, this.rayRotation);
|
384
|
-
}
|
385
|
-
else {
|
386
|
-
raycastLine.visible = allowRaycastLineVisible;
|
387
|
-
setWorldQuaternion(raycastLine, this.rayRotation);
|
388
|
-
setWorldPosition(raycastLine, wp);
|
389
|
-
}
|
390
|
-
}
|
391
|
-
|
392
|
-
update(): void {
|
393
|
-
if (!this.webXR) return;
|
394
|
-
|
395
|
-
// TODO: we should wait until we actually have models, this is just a workaround
|
396
|
-
if (this.context.time.frameCount % 60 === 0) {
|
397
|
-
this.setControllerLayers(this.controller, 2);
|
398
|
-
this.setControllerLayers(this.controllerGrip, 2);
|
399
|
-
this.setControllerLayers(this.hand, 2);
|
400
|
-
}
|
401
|
-
|
402
|
-
const subs = WebXRController.eventSubs[ControllerEvents.Update];
|
403
|
-
if (subs && subs.length > 0) {
|
404
|
-
for (const sub of subs) {
|
405
|
-
sub(this);
|
406
|
-
}
|
407
|
-
}
|
408
|
-
|
409
|
-
let t = 1;
|
410
|
-
if (this.type === ControllerType.PhysicalDevice) t = this.context.time.deltaTime / .1;
|
411
|
-
else if (this.isUsingHands && this.handPointerModel.pinched) t = this.context.time.deltaTime / .3;
|
412
|
-
this.rayRotation.slerp(getWorldQuaternion(this.controller), this.useSmoothing ? t : 1.0);
|
413
|
-
const wp = getWorldPosition(this.controller);
|
414
|
-
|
415
|
-
// hide hand pointer model, it's giant and doesn't really help
|
416
|
-
if (this.isUsingHands && this.handPointerModel.cursorObject) {
|
417
|
-
this.handPointerModel.cursorObject.visible = false;
|
418
|
-
}
|
419
|
-
|
420
|
-
// perform raycasts
|
421
|
-
if(this.enableRaycasts)
|
422
|
-
{
|
423
|
-
if (this.raycastLine) {
|
424
|
-
this.raycastUpdate(this.raycastLine, wp);
|
425
|
-
}
|
426
|
-
|
427
|
-
this.lastHit = this.updateLastHit();
|
428
|
-
|
429
|
-
if (this.grabbed) {
|
430
|
-
this.grabbed.update();
|
431
|
-
}
|
432
|
-
}
|
433
|
-
else { // hide line when raycasting is disabled
|
434
|
-
if (this.raycastLine) {
|
435
|
-
this.raycastLine.visible = false;
|
436
|
-
}
|
437
|
-
}
|
438
|
-
|
439
|
-
this._selectionPressedLastFrame = this._selectionPressed;
|
440
|
-
|
441
|
-
if (this.selectStartCallback) {
|
442
|
-
this.selectStartCallback();
|
443
|
-
}
|
444
|
-
}
|
445
|
-
|
446
|
-
onUpdate(session: XRSession) {
|
447
|
-
this.lastHit = null;
|
448
|
-
|
449
|
-
if (!session || session.inputSources.length <= this.index) {
|
450
|
-
this.input = null;
|
451
|
-
return;
|
452
|
-
}
|
453
|
-
if (this.type === ControllerType.PhysicalDevice)
|
454
|
-
this.input = session.inputSources[this.index];
|
455
|
-
if (!this.input) return;
|
456
|
-
const rig = this.webXR!.Rig;
|
457
|
-
if (!rig) return;
|
458
|
-
|
459
|
-
if (this._didNotEndSelection && !this.handPointerModel.pinched) {
|
460
|
-
this._didNotEndSelection = false;
|
461
|
-
this.onSelectEnd();
|
462
|
-
}
|
463
|
-
|
464
|
-
this.updateStick(this.input);
|
465
|
-
|
466
|
-
const buttons = this.input?.gamepad?.buttons;
|
467
|
-
|
468
|
-
if(this.enableDefaultControls) {
|
469
|
-
switch (this.input.handedness) {
|
470
|
-
case "left":
|
471
|
-
this.movementUpdate(rig, buttons);
|
472
|
-
break;
|
473
|
-
|
474
|
-
case "right":
|
475
|
-
this.rotationUpdate(rig, buttons);
|
476
|
-
break;
|
477
|
-
}
|
478
|
-
}
|
479
|
-
}
|
480
|
-
|
481
|
-
|
482
|
-
private movementUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
|
483
|
-
const speedFactor = 3 * WebXRController.MovementSpeedFactor;
|
484
|
-
const powFactor = 2;
|
485
|
-
const speed = Mathf.clamp01(this.joystick.length() * 2);
|
486
|
-
|
487
|
-
const sideDir = this.joystick.x > 0 ? 1 : -1;
|
488
|
-
let side = Math.pow(this.joystick.x, powFactor);
|
489
|
-
side *= sideDir;
|
490
|
-
side *= speed;
|
491
|
-
|
492
|
-
|
493
|
-
const forwardDir = this.joystick.y > 0 ? 1 : -1;
|
494
|
-
let forward = Math.pow(this.joystick.y, powFactor);
|
495
|
-
forward *= forwardDir;
|
496
|
-
side *= speed;
|
497
|
-
|
498
|
-
rig.getWorldQuaternion(this.worldRot);
|
499
|
-
this.movementVector.set(side, 0, forward);
|
500
|
-
this.movementVector.applyQuaternion(this.webXR!.TransformOrientation);
|
501
|
-
this.movementVector.y = 0;
|
502
|
-
this.movementVector.applyQuaternion(this.worldRot);
|
503
|
-
this.movementVector.multiplyScalar(speedFactor * this.context.time.deltaTime);
|
504
|
-
rig.position.add(this.movementVector);
|
505
|
-
|
506
|
-
if (this.isUsingHands)
|
507
|
-
this.runTeleport(rig, buttons);
|
508
|
-
}
|
509
|
-
|
510
|
-
private rotationUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
|
511
|
-
const rotate = this.joystick.x;
|
512
|
-
const rotAbs = Math.abs(rotate);
|
513
|
-
if (rotAbs < 0.4) {
|
514
|
-
this.didRotate = false;
|
515
|
-
}
|
516
|
-
else if (rotAbs > .5 && !this.didRotate) {
|
517
|
-
const dir = rotate > 0 ? -1 : 1;
|
518
|
-
rig.rotateY(Mathf.toRadians(30 * dir));
|
519
|
-
this.didRotate = true;
|
520
|
-
}
|
521
|
-
|
522
|
-
this.runTeleport(rig, buttons);
|
523
|
-
}
|
524
|
-
private _pinchStartTime: number | undefined = undefined;
|
525
|
-
|
526
|
-
private runTeleport(rig: Object3D, buttons?: readonly GamepadButton[]) {
|
527
|
-
let teleport = -this.joystick.y;
|
528
|
-
if (this.hand?.visible && !this.grabbed) {
|
529
|
-
const pinched = this.handPointerModel.isPinched();
|
530
|
-
if (pinched && this._pinchStartTime === undefined) {
|
531
|
-
this._pinchStartTime = this.context.time.time;
|
532
|
-
}
|
533
|
-
if (pinched && this._pinchStartTime && this.context.time.time - this._pinchStartTime > .8) {
|
534
|
-
// hacky approach for basic hand teleportation -
|
535
|
-
// we teleport if we pinch and the back of the hand points down (open hand gesture)
|
536
|
-
// const v1 = new Vector3();
|
537
|
-
// const worldQuaternion = new Quaternion();
|
538
|
-
// this.controller.getWorldQuaternion(worldQuaternion);
|
539
|
-
// v1.copy(this.controller.up).applyQuaternion(worldQuaternion);
|
540
|
-
// const dotPr = -v1.dot(this.controller.up);
|
541
|
-
teleport = this.handPointerModel.isPinched() ? 1 : 0;
|
542
|
-
}
|
543
|
-
if (!pinched) this._pinchStartTime = undefined;
|
544
|
-
}
|
545
|
-
else this._pinchStartTime = undefined;
|
546
|
-
|
547
|
-
const inVR = this.webXR!.IsInVR;
|
548
|
-
const xrRig = this.webXR!.Rig;
|
549
|
-
let doTeleport = teleport > .5 && inVR;
|
550
|
-
let isInMiniatureMode = xrRig ? xrRig?.scale?.x < .999 : false;
|
551
|
-
let newRigScale: number | null = null;
|
552
|
-
|
553
|
-
if (buttons && this.input && !this.input.hand) {
|
554
|
-
for (let i = 0; i < buttons.length; i++) {
|
555
|
-
const btn = buttons[i];
|
556
|
-
// button[4] seems to be the A button if it exists. On hololens it's randomly pressed though for hands
|
557
|
-
// see https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping
|
558
|
-
if (i === 4) {
|
559
|
-
if (btn.pressed && !this.didChangeScale && inVR) {
|
560
|
-
this.didChangeScale = true;
|
561
|
-
const rig = xrRig;
|
562
|
-
if (rig) {
|
563
|
-
const args = this.switchScale(rig, doTeleport, isInMiniatureMode, newRigScale);
|
564
|
-
doTeleport = args.doTeleport;
|
565
|
-
isInMiniatureMode = args.isInMiniatureMode;
|
566
|
-
newRigScale = args.newRigScale;
|
567
|
-
}
|
568
|
-
}
|
569
|
-
else if (!btn.pressed)
|
570
|
-
this.didChangeScale = false;
|
571
|
-
}
|
572
|
-
}
|
573
|
-
}
|
574
|
-
|
575
|
-
if (doTeleport) {
|
576
|
-
if (!this.didTeleport) {
|
577
|
-
const rc = this.raycast();
|
578
|
-
this.didTeleport = true;
|
579
|
-
if (rc && rc.length > 0) {
|
580
|
-
const hit = rc[0];
|
581
|
-
if (isInMiniatureMode || this.isValidTeleportTarget(hit.object)) {
|
582
|
-
const point = hit.point;
|
583
|
-
setWorldPosition(rig, point);
|
584
|
-
}
|
585
|
-
}
|
586
|
-
}
|
587
|
-
}
|
588
|
-
else if (teleport < .1) {
|
589
|
-
this.didTeleport = false;
|
590
|
-
}
|
591
|
-
|
592
|
-
if (newRigScale !== null) {
|
593
|
-
rig.scale.set(newRigScale, newRigScale, newRigScale);
|
594
|
-
rig.updateMatrixWorld();
|
595
|
-
}
|
596
|
-
}
|
597
|
-
|
598
|
-
|
599
|
-
private isValidTeleportTarget(obj: Object3D): boolean {
|
600
|
-
return GameObject.getComponentInParent(obj, TeleportTarget) != null;
|
601
|
-
}
|
602
|
-
|
603
|
-
private switchScale(rig: Object3D, doTeleport: boolean, isInMiniatureMode: boolean, newRigScale: number | null) {
|
604
|
-
if (!isInMiniatureMode) {
|
605
|
-
isInMiniatureMode = true;
|
606
|
-
doTeleport = true;
|
607
|
-
newRigScale = .1;
|
608
|
-
WebXRController.MovementSpeedFactor = newRigScale * 2;
|
609
|
-
const cam = this.context.mainCamera as PerspectiveCamera;
|
610
|
-
WebXRController.PreviousCameraFarDistance = cam.far;
|
611
|
-
cam.far /= newRigScale;
|
612
|
-
}
|
613
|
-
else {
|
614
|
-
isInMiniatureMode = false;
|
615
|
-
rig.scale.set(1, 1, 1);
|
616
|
-
newRigScale = 1;
|
617
|
-
WebXRController.MovementSpeedFactor = 1;
|
618
|
-
const cam = this.context.mainCamera as PerspectiveCamera;
|
619
|
-
if (WebXRController.PreviousCameraFarDistance)
|
620
|
-
cam.far = WebXRController.PreviousCameraFarDistance;
|
621
|
-
}
|
622
|
-
return { doTeleport, isInMiniatureMode, newRigScale }
|
623
|
-
}
|
624
|
-
|
625
|
-
private updateStick(inputSource: XRInputSource) {
|
626
|
-
if (!inputSource || !inputSource.gamepad || inputSource.gamepad.axes?.length < 4) return;
|
627
|
-
this.joystick.x = inputSource.gamepad.axes[2];
|
628
|
-
this.joystick.y = inputSource.gamepad.axes[3];
|
629
|
-
}
|
630
|
-
|
631
|
-
private updateLastHit(): Intersection | null {
|
632
|
-
const rc = this.raycast();
|
633
|
-
const hit = rc ? rc[0] : null;
|
634
|
-
this.lastHit = hit;
|
635
|
-
let factor = 1;
|
636
|
-
if (this.webXR!.Rig) {
|
637
|
-
factor /= this.webXR!.Rig.scale.x;
|
638
|
-
}
|
639
|
-
// if (!hit) factor = 0;
|
640
|
-
|
641
|
-
if (this.raycastLine) {
|
642
|
-
this.raycastLine.scale.z = factor * (this.lastHit?.distance ?? 9999);
|
643
|
-
const mat = this.raycastLine.material as LineBasicMaterial;
|
644
|
-
if (hit != null) mat.color = WebXRController.raycastColor;
|
645
|
-
else mat.color = WebXRController.raycastNoHitColor;
|
646
|
-
}
|
647
|
-
if (this._raycastHitPoint) {
|
648
|
-
if (this.lastHit != null) {
|
649
|
-
this._raycastHitPoint.position.z = -1;// -this.lastHit.distance;
|
650
|
-
const scale = Mathf.clamp(this.lastHit.distance * .01 * factor, .015, .1);
|
651
|
-
this._raycastHitPoint.scale.set(scale, scale, scale);
|
652
|
-
}
|
653
|
-
this._raycastHitPoint.visible = this.lastHit !== null && this.lastHit !== undefined;
|
654
|
-
}
|
655
|
-
return hit;
|
656
|
-
}
|
657
|
-
|
658
|
-
private onSelectStart() {
|
659
|
-
if (!this.context.connection.allowEditing) return;
|
660
|
-
// console.log("SELECT START", _event);
|
661
|
-
// if we process the event immediately the controller
|
662
|
-
// world positions are not yet correctly updated and we have info from the last frame
|
663
|
-
// so we delay the event processing one frame
|
664
|
-
// only necessary for AR - ideally we can get it to work right here
|
665
|
-
// but should be fine as a workaround for now
|
666
|
-
this.selectStartCallback = () => this.onHandleSelectStart();
|
667
|
-
}
|
668
|
-
|
669
|
-
private selectStartCallback: Function | null = null;
|
670
|
-
private lastSelectStartObject: Object3D | null = null;;
|
671
|
-
|
672
|
-
private onHandleSelectStart() {
|
673
|
-
this.selectStartCallback = null;
|
674
|
-
this._selectionPressed = true;
|
675
|
-
this._selectionStartTime = this.context.time.time;
|
676
|
-
this._selectionEndTime = 1000;
|
677
|
-
// console.log("DOWN", this.index, WebXRController.eventSubs);
|
678
|
-
|
679
|
-
// let maxDistance = this.isUsingHands ? .1 : undefined;
|
680
|
-
let intersections: Intersection[] | null = null;
|
681
|
-
let closeGrab: boolean = false;
|
682
|
-
if (this.isUsingHands) {
|
683
|
-
intersections = this.overlap();
|
684
|
-
if (intersections.length <= 0) {
|
685
|
-
intersections = this.raycast();
|
686
|
-
closeGrab = false;
|
687
|
-
}
|
688
|
-
else {
|
689
|
-
closeGrab = true;
|
690
|
-
}
|
691
|
-
}
|
692
|
-
else intersections = this.raycast();
|
693
|
-
|
694
|
-
if (debug)
|
695
|
-
console.log("onHandleSelectStart", "close grab? " + closeGrab, "intersections", intersections);
|
696
|
-
|
697
|
-
if (intersections && intersections.length > 0) {
|
698
|
-
for (const intersection of intersections) {
|
699
|
-
const object = intersection.object;
|
700
|
-
this.lastSelectStartObject = object;
|
701
|
-
const args = { selected: object, grab: object };
|
702
|
-
const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
|
703
|
-
if (subs && subs.length > 0) {
|
704
|
-
for (const sub of subs) {
|
705
|
-
sub(this, args);
|
706
|
-
}
|
707
|
-
}
|
708
|
-
if (args.grab !== object && debug)
|
709
|
-
console.log("Grabbed object changed", "original", object, "new", args.grab);
|
710
|
-
if (args.grab) {
|
711
|
-
this.grabbed = AttachedObject.TryTake(this, args.grab, intersection, closeGrab);
|
712
|
-
}
|
713
|
-
break;
|
714
|
-
}
|
715
|
-
}
|
716
|
-
else {
|
717
|
-
const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
|
718
|
-
const args = { selected: null, grab: null };
|
719
|
-
if (subs && subs.length > 0) {
|
720
|
-
for (const sub of subs) {
|
721
|
-
sub(this, args);
|
722
|
-
}
|
723
|
-
}
|
724
|
-
}
|
725
|
-
}
|
726
|
-
|
727
|
-
private _didNotEndSelection: boolean = false;
|
728
|
-
|
729
|
-
private onSelectEnd() {
|
730
|
-
if (this.isUsingHands) {
|
731
|
-
if (this.handPointerModel.pinched) {
|
732
|
-
this._didNotEndSelection = true;
|
733
|
-
return;
|
734
|
-
}
|
735
|
-
}
|
736
|
-
|
737
|
-
if (!this._selectionPressed) return;
|
738
|
-
this.selectStartCallback = null;
|
739
|
-
this._selectionPressed = false;
|
740
|
-
this._selectionEndTime = this.context.time.time;
|
741
|
-
|
742
|
-
const args = { grab: this.grabbed?.selected ?? this.lastSelectStartObject };
|
743
|
-
const subs = WebXRController.eventSubs[ControllerEvents.SelectEnd];
|
744
|
-
if (subs && subs.length > 0) {
|
745
|
-
for (const sub of subs) {
|
746
|
-
sub(this, args);
|
747
|
-
}
|
748
|
-
}
|
749
|
-
|
750
|
-
if (this.grabbed) {
|
751
|
-
this.grabbed.free();
|
752
|
-
this.grabbed = null;
|
753
|
-
}
|
754
|
-
}
|
755
|
-
|
756
|
-
private testIsVisible(obj: Object3D | null): boolean {
|
757
|
-
if (!obj) return false;
|
758
|
-
if (GameObject.isActiveInHierarchy(obj) === false) return false;
|
759
|
-
if (UIRaycastUtils.isInteractable(obj) === false) {
|
760
|
-
return false;
|
761
|
-
}
|
762
|
-
return true;
|
763
|
-
// if (!obj.visible) return false;
|
764
|
-
// return this.testIsVisible(obj.parent);
|
765
|
-
}
|
766
|
-
|
767
|
-
private setControllerLayers(obj: Object3D, layer: number) {
|
768
|
-
if (!obj) return;
|
769
|
-
obj.layers.set(layer);
|
770
|
-
if (obj.children) {
|
771
|
-
for (const ch of obj.children) {
|
772
|
-
if (this.grabbed?.selected === ch || this.grabbed?.selectedMesh === ch) {
|
773
|
-
continue;
|
774
|
-
}
|
775
|
-
this.setControllerLayers(ch, layer);
|
776
|
-
}
|
777
|
-
}
|
778
|
-
}
|
779
|
-
|
780
|
-
public getRay(): Ray {
|
781
|
-
const ray = new Ray();
|
782
|
-
// this.tempMatrix.identity().extractRotation(this.controller.matrixWorld);
|
783
|
-
// ray.origin.setFromMatrixPosition(this.controller.matrixWorld);
|
784
|
-
ray.origin.copy(getWorldPosition(this.controller));
|
785
|
-
ray.direction.set(0, 0, -1).applyQuaternion(this.rayRotation);
|
786
|
-
return ray;
|
787
|
-
}
|
788
|
-
|
789
|
-
private closeGrabBoundingBoxHelper?: BoxHelper;
|
790
|
-
|
791
|
-
public overlap(): Intersection[] {
|
792
|
-
const overlapCenter = (this.isUsingHands && this.handPointerModel) ? this.handPointerModel.pointerObject : this.controllerGrip;
|
793
|
-
|
794
|
-
if (debug) {
|
795
|
-
if (!this.closeGrabBoundingBoxHelper && overlapCenter) {
|
796
|
-
this.closeGrabBoundingBoxHelper = new BoxHelper(overlapCenter, 0xffff00);
|
797
|
-
this.scene.add(this.closeGrabBoundingBoxHelper);
|
798
|
-
}
|
799
|
-
|
800
|
-
if (this.closeGrabBoundingBoxHelper && overlapCenter) {
|
801
|
-
this.closeGrabBoundingBoxHelper.setFromObject(overlapCenter);
|
802
|
-
}
|
803
|
-
}
|
804
|
-
|
805
|
-
if (!overlapCenter)
|
806
|
-
return new Array<Intersection>();
|
807
|
-
|
808
|
-
const wp = getWorldPosition(overlapCenter).clone();
|
809
|
-
return this.context.physics.sphereOverlap(wp, .02);
|
810
|
-
}
|
811
|
-
|
812
|
-
public raycast(): Intersection[] {
|
813
|
-
const opts = new RaycastOptions();
|
814
|
-
opts.layerMask = new Layers();
|
815
|
-
opts.layerMask.enableAll();
|
816
|
-
opts.layerMask.disable(2);
|
817
|
-
opts.ray = this.getRay();
|
818
|
-
const hits = this.context.physics.raycast(opts);
|
819
|
-
for (let i = 0; i < hits.length; i++) {
|
820
|
-
const hit = hits[i];
|
821
|
-
const obj = hit.object;
|
822
|
-
if (!this.testIsVisible(obj)) {
|
823
|
-
hits.splice(i, 1);
|
824
|
-
i--;
|
825
|
-
continue;
|
826
|
-
}
|
827
|
-
hit.object = UIRaycastUtils.getObject(obj);
|
828
|
-
break;
|
829
|
-
}
|
830
|
-
// console.log(...hits);
|
831
|
-
return hits;
|
832
|
-
}
|
833
|
-
}
|
834
|
-
|
835
|
-
|
836
|
-
export enum AttachedObjectEvents {
|
837
|
-
WillTake = "WillTake",
|
838
|
-
DidTake = "DidTake",
|
839
|
-
WillFree = "WillFree",
|
840
|
-
DidFree = "DidFree",
|
841
|
-
}
|
842
|
-
|
843
|
-
export class AttachedObject {
|
844
|
-
|
845
|
-
public static Events: { [key: string]: Function[] } = {};
|
846
|
-
public static AddEventListener(event: AttachedObjectEvents, callback: Function): Function {
|
847
|
-
if (!AttachedObject.Events[event]) AttachedObject.Events[event] = [];
|
848
|
-
AttachedObject.Events[event].push(callback);
|
849
|
-
return callback;
|
850
|
-
}
|
851
|
-
public static RemoveEventListener(event: AttachedObjectEvents, callback: Function | null) {
|
852
|
-
if (!callback) return;
|
853
|
-
if (!AttachedObject.Events[event]) return;
|
854
|
-
const idx = AttachedObject.Events[event].indexOf(callback);
|
855
|
-
if (idx >= 0) AttachedObject.Events[event].splice(idx, 1);
|
856
|
-
}
|
857
|
-
|
858
|
-
|
859
|
-
public static Current: AttachedObject[] = [];
|
860
|
-
|
861
|
-
private static Register(obj: AttachedObject) {
|
862
|
-
|
863
|
-
if (!this.Current.find(x => x === obj)) {
|
864
|
-
this.Current.push(obj);
|
865
|
-
}
|
866
|
-
}
|
867
|
-
|
868
|
-
private static Remove(obj: AttachedObject) {
|
869
|
-
const i = this.Current.indexOf(obj);
|
870
|
-
if (i >= 0) {
|
871
|
-
this.Current.splice(i, 1);
|
872
|
-
}
|
873
|
-
}
|
874
|
-
|
875
|
-
public static TryTake(controller: WebXRController, candidate: Object3D, intersection: Intersection, closeGrab: boolean): AttachedObject | null {
|
876
|
-
const interactable = GameObject.getComponentInParent(candidate, Interactable);
|
877
|
-
if (!interactable) {
|
878
|
-
if (debug)
|
879
|
-
console.warn("Prevented taking object that is not interactable", candidate);
|
880
|
-
return null;
|
881
|
-
}
|
882
|
-
else candidate = interactable.gameObject;
|
883
|
-
|
884
|
-
|
885
|
-
let objectToAttach = candidate;
|
886
|
-
const sync = GameObject.getComponentInParent(candidate, SyncedTransform);
|
887
|
-
if (sync) {
|
888
|
-
sync.requestOwnership();
|
889
|
-
objectToAttach = sync.gameObject;
|
890
|
-
}
|
891
|
-
|
892
|
-
for (const o of this.Current) {
|
893
|
-
if (o.selected === objectToAttach) {
|
894
|
-
if (o.controller === controller) return o;
|
895
|
-
o.free();
|
896
|
-
o.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
|
897
|
-
return o;
|
898
|
-
}
|
899
|
-
}
|
900
|
-
|
901
|
-
const att = new AttachedObject();
|
902
|
-
att.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
|
903
|
-
return att;
|
904
|
-
}
|
905
|
-
|
906
|
-
|
907
|
-
public sync: SyncedTransform | null = null;
|
908
|
-
public selected: Object3D | null = null;
|
909
|
-
public selectedParent: Object3D | null = null;
|
910
|
-
public selectedMesh: Mesh | null = null;
|
911
|
-
public controller: WebXRController | null = null;
|
912
|
-
public grabTime: number = 0;
|
913
|
-
public grabUUID: string = "";
|
914
|
-
public isCloseGrab: boolean = false; // when taken via sphere cast with hands
|
915
|
-
|
916
|
-
private originalMaterial: Material | Material[] | null = null;
|
917
|
-
private usageMarker: UsageMarker | null = null;
|
918
|
-
private rigidbodies: Rigidbody[] | null = null;
|
919
|
-
private didReparent: boolean = false;
|
920
|
-
private grabDistance: number = 0;
|
921
|
-
private interactable: Interactable | null = null;
|
922
|
-
private positionSource: Object3D | null = null;
|
923
|
-
|
924
|
-
private Take(controller: WebXRController, take: Object3D, hit: Object3D, sync: SyncedTransform | null, _interactable: Interactable,
|
925
|
-
intersection: Intersection, closeGrab: boolean)
|
926
|
-
: AttachedObject {
|
927
|
-
console.assert(take !== null, "Expected object to be taken but was", take);
|
928
|
-
|
929
|
-
if (controller.isUsingHands) {
|
930
|
-
this.positionSource = closeGrab ? controller.wrist : controller.controller;
|
931
|
-
}
|
932
|
-
else {
|
933
|
-
this.positionSource = controller.controller;
|
934
|
-
}
|
935
|
-
if (!this.positionSource) {
|
936
|
-
console.warn("No position source");
|
937
|
-
return this;
|
938
|
-
}
|
939
|
-
|
940
|
-
const args = { controller, take, hit, sync, interactable: _interactable };
|
941
|
-
AttachedObject.Events.WillTake?.forEach(x => x(this, args));
|
942
|
-
|
943
|
-
|
944
|
-
const mesh = hit as Mesh;
|
945
|
-
if (mesh?.material) {
|
946
|
-
this.originalMaterial = mesh.material;
|
947
|
-
if (!Array.isArray(mesh.material)) {
|
948
|
-
mesh.material = (mesh.material as Material).clone();
|
949
|
-
if (mesh.material && mesh.material["emissive"])
|
950
|
-
mesh.material["emissive"].b = .2;
|
951
|
-
}
|
952
|
-
}
|
953
|
-
|
954
|
-
this.selected = take;
|
955
|
-
if (!this.selectedParent) {
|
956
|
-
this.selectedParent = take.parent;
|
957
|
-
}
|
958
|
-
this.selectedMesh = mesh;
|
959
|
-
this.controller = controller;
|
960
|
-
this.interactable = _interactable;
|
961
|
-
this.isCloseGrab = closeGrab;
|
962
|
-
// if (interactable.canGrab) {
|
963
|
-
// this.didReparent = true;
|
964
|
-
// this.device.controller.attach(take);
|
965
|
-
// }
|
966
|
-
// else
|
967
|
-
this.didReparent = false;
|
968
|
-
|
969
|
-
|
970
|
-
this.sync = sync;
|
971
|
-
this.grabTime = controller.context.time.time;
|
972
|
-
this.grabUUID = Date.now().toString();
|
973
|
-
this.usageMarker = GameObject.addNewComponent(this.selected, UsageMarker);
|
974
|
-
this.rigidbodies = GameObject.getComponentsInChildren(this.selected, Rigidbody);
|
975
|
-
getWorldPosition(this.positionSource, this.lastControllerWorldPos);
|
976
|
-
const getGrabPoint = () => closeGrab ? this.lastControllerWorldPos.clone() : intersection.point.clone();
|
977
|
-
this.grabDistance = getGrabPoint().distanceTo(this.lastControllerWorldPos);
|
978
|
-
this.totalChangeAlongDirection = 0.0;
|
979
|
-
|
980
|
-
// we're storing position relative to the grab point
|
981
|
-
// we're storing rotation relative to the ray
|
982
|
-
this.localPositionOffsetToGrab = this.selected.worldToLocal(getGrabPoint());
|
983
|
-
const rot = controller.isUsingHands && closeGrab ? this.controller.getWristQuaternion()!.clone() : controller.rayRotation.clone();
|
984
|
-
getWorldQuaternion(this.selected, this.localQuaternionToGrab).premultiply(rot.invert());
|
985
|
-
|
986
|
-
const rig = this.controller.webXR!.Rig;
|
987
|
-
if (rig)
|
988
|
-
this.rigPositionLastFrame.copy(getWorldPosition(rig))
|
989
|
-
|
990
|
-
Avatar_POI.Add(controller.context, this.selected);
|
991
|
-
AttachedObject.Register(this);
|
992
|
-
|
993
|
-
if (this.sync) {
|
994
|
-
this.sync.fastMode = true;
|
995
|
-
}
|
996
|
-
|
997
|
-
AttachedObject.Events.DidTake?.forEach(x => x(this, args));
|
998
|
-
|
999
|
-
return this;
|
1000
|
-
}
|
1001
|
-
|
1002
|
-
public free(): void {
|
1003
|
-
if (!this.selected) return;
|
1004
|
-
|
1005
|
-
const args = { controller: this.controller, take: this.selected, hit: this.selected, sync: this.sync, interactable: null };
|
1006
|
-
AttachedObject.Events.WillFree?.forEach(x => x(this, args));
|
1007
|
-
|
1008
|
-
Avatar_POI.Remove(this.controller!.context, this.selected);
|
1009
|
-
AttachedObject.Remove(this);
|
1010
|
-
|
1011
|
-
if (this.sync) {
|
1012
|
-
this.sync.fastMode = false;
|
1013
|
-
}
|
1014
|
-
|
1015
|
-
const mesh = this.selectedMesh;
|
1016
|
-
if (mesh && this.originalMaterial && mesh.material) {
|
1017
|
-
mesh.material = this.originalMaterial;
|
1018
|
-
}
|
1019
|
-
|
1020
|
-
const object = this.selected;
|
1021
|
-
// only attach the object back if it has a parent
|
1022
|
-
// no parent means it was destroyed while holding it!
|
1023
|
-
if (this.didReparent && object.parent) {
|
1024
|
-
const prevParent = this.selectedParent;
|
1025
|
-
if (prevParent) prevParent.attach(object);
|
1026
|
-
else this.controller?.context.scene.attach(object);
|
1027
|
-
}
|
1028
|
-
|
1029
|
-
this.usageMarker?.destroy();
|
1030
|
-
|
1031
|
-
if (this.controller)
|
1032
|
-
this.controller.grabbed = null;
|
1033
|
-
this.selected = null;
|
1034
|
-
this.selectedParent = null;
|
1035
|
-
this.selectedMesh = null;
|
1036
|
-
this.sync = null;
|
1037
|
-
|
1038
|
-
|
1039
|
-
// TODO: make throwing work again
|
1040
|
-
if (this.rigidbodies) {
|
1041
|
-
for (const rb of this.rigidbodies) {
|
1042
|
-
rb.wakeUp();
|
1043
|
-
rb.setVelocity(rb.smoothedVelocity);
|
1044
|
-
}
|
1045
|
-
}
|
1046
|
-
this.rigidbodies = null;
|
1047
|
-
|
1048
|
-
this.localPositionOffsetToGrab = null;
|
1049
|
-
this.quaternionLerp = null;
|
1050
|
-
|
1051
|
-
AttachedObject.Events.DidFree?.forEach(x => x(this, args));
|
1052
|
-
}
|
1053
|
-
|
1054
|
-
public grabPoint: Vector3 = new Vector3();
|
1055
|
-
|
1056
|
-
private localPositionOffsetToGrab: Vector3 | null = null;
|
1057
|
-
private localPositionOffsetToGrab_worldSpace: Vector3 = new Vector3();
|
1058
|
-
private localQuaternionToGrab: Quaternion = new Quaternion(0, 0, 0, 1);
|
1059
|
-
private targetDir: Vector3 | null = null;
|
1060
|
-
private quaternionLerp: Quaternion | null = null;
|
1061
|
-
|
1062
|
-
private controllerDir = new Vector3();
|
1063
|
-
private controllerWorldPos = new Vector3();
|
1064
|
-
private lastControllerWorldPos = new Vector3();
|
1065
|
-
private controllerPosDelta = new Vector3();
|
1066
|
-
private totalChangeAlongDirection = 0.0;
|
1067
|
-
private rigPositionLastFrame = new Vector3();
|
1068
|
-
|
1069
|
-
private controllerMovementSinceLastFrame() {
|
1070
|
-
if (!this.positionSource || !this.controller) return 0.0;
|
1071
|
-
|
1072
|
-
// controller direction
|
1073
|
-
this.controllerDir.set(0, 0, -1);
|
1074
|
-
this.controllerDir.applyQuaternion(this.controller.rayRotation);
|
1075
|
-
|
1076
|
-
// controller delta
|
1077
|
-
getWorldPosition(this.positionSource, this.controllerWorldPos);
|
1078
|
-
this.controllerPosDelta.copy(this.controllerWorldPos);
|
1079
|
-
this.controllerPosDelta.sub(this.lastControllerWorldPos);
|
1080
|
-
this.lastControllerWorldPos.copy(this.controllerWorldPos);
|
1081
|
-
const rig = this.controller.webXR!.Rig;
|
1082
|
-
if (rig) {
|
1083
|
-
const rigPos = getWorldPosition(rig);
|
1084
|
-
const rigDelta = this.rigPositionLastFrame.sub(rigPos);
|
1085
|
-
this.controllerPosDelta.add(rigDelta);
|
1086
|
-
this.rigPositionLastFrame.copy(rigPos);
|
1087
|
-
}
|
1088
|
-
|
1089
|
-
// calculate delta along direction
|
1090
|
-
const changeAlongControllerDirection = this.controllerDir.dot(this.controllerPosDelta);
|
1091
|
-
|
1092
|
-
return changeAlongControllerDirection;
|
1093
|
-
}
|
1094
|
-
|
1095
|
-
public update() {
|
1096
|
-
if (this.rigidbodies)
|
1097
|
-
for (const rb of this.rigidbodies)
|
1098
|
-
rb.resetVelocities();
|
1099
|
-
// TODO: add/use sync lost ownership event
|
1100
|
-
if (this.sync && this.controller && this.controller.context.connection.isInRoom) {
|
1101
|
-
const td = this.controller.context.time.time - this.grabTime;
|
1102
|
-
// if (time.frameCount % 60 === 0) {
|
1103
|
-
// console.log("ownership?", this.selected.name, this.sync.hasOwnership(), td)
|
1104
|
-
// }
|
1105
|
-
if (td > 3) {
|
1106
|
-
// if (time.frameCount % 60 === 0) {
|
1107
|
-
// console.log(this.sync.hasOwnership())
|
1108
|
-
// }
|
1109
|
-
if (this.sync.hasOwnership() === false) {
|
1110
|
-
console.log("no ownership, will leave", this.sync.guid);
|
1111
|
-
this.free();
|
1112
|
-
}
|
1113
|
-
}
|
1114
|
-
}
|
1115
|
-
if (this.interactable && !this.interactable.canGrab) return;
|
1116
|
-
|
1117
|
-
if (!this.didReparent && this.selected && this.controller) {
|
1118
|
-
|
1119
|
-
const rigScale = this.controller.webXR!.Rig?.scale.x ?? 1.0;
|
1120
|
-
|
1121
|
-
this.totalChangeAlongDirection += this.controllerMovementSinceLastFrame();
|
1122
|
-
// console.log(this.totalChangeAlongDirection);
|
1123
|
-
|
1124
|
-
// alert("yo: " + this.controller.webXR.Rig?.scale.x); // this is 0.1 on Hololens
|
1125
|
-
let currentDist = 1.0;
|
1126
|
-
if (this.controller.type === ControllerType.PhysicalDevice) // only for controllers and not on touch (AR touches are controllers)
|
1127
|
-
{
|
1128
|
-
currentDist = Math.max(0.0, 1 + this.totalChangeAlongDirection * 2.0 / rigScale);
|
1129
|
-
currentDist = currentDist * currentDist * currentDist;
|
1130
|
-
}
|
1131
|
-
if (this.grabDistance / rigScale < 0.8) currentDist = 1.0; // don't accelerate if this is a close grab, want full control
|
1132
|
-
|
1133
|
-
if (!this.targetDir) {
|
1134
|
-
this.targetDir = new Vector3();
|
1135
|
-
}
|
1136
|
-
this.targetDir.set(0, 0, -this.grabDistance * currentDist);
|
1137
|
-
const target = this.targetDir.applyQuaternion(this.controller.rayRotation).add(this.controllerWorldPos);
|
1138
|
-
|
1139
|
-
// apply rotation
|
1140
|
-
const targetQuat = this.controller.rayRotation.clone().multiplyQuaternions(this.controller.rayRotation, this.localQuaternionToGrab);
|
1141
|
-
if (!this.quaternionLerp) {
|
1142
|
-
this.quaternionLerp = targetQuat.clone();
|
1143
|
-
}
|
1144
|
-
this.quaternionLerp.slerp(targetQuat, this.controller.useSmoothing ? this.controller.context.time.deltaTime / .03 : 1.0);
|
1145
|
-
setWorldQuaternion(this.selected, this.quaternionLerp);
|
1146
|
-
this.selected.updateWorldMatrix(false, false); // necessary so that rotation is correct for the following position update
|
1147
|
-
|
1148
|
-
// apply position
|
1149
|
-
this.grabPoint.copy(target);
|
1150
|
-
// apply local grab offset
|
1151
|
-
if (this.localPositionOffsetToGrab) {
|
1152
|
-
this.localPositionOffsetToGrab_worldSpace.copy(this.localPositionOffsetToGrab);
|
1153
|
-
this.selected.localToWorld(this.localPositionOffsetToGrab_worldSpace).sub(getWorldPosition(this.selected));
|
1154
|
-
target.sub(this.localPositionOffsetToGrab_worldSpace);
|
1155
|
-
}
|
1156
|
-
setWorldPosition(this.selected, target);
|
1157
|
-
}
|
1158
|
-
|
1159
|
-
|
1160
|
-
if (this.rigidbodies != null) {
|
1161
|
-
for (const rb of this.rigidbodies) {
|
1162
|
-
rb.wakeUp();
|
1163
|
-
}
|
1164
|
-
}
|
1165
|
-
|
1166
|
-
InstancingUtil.markDirty(this.selected, true);
|
1167
|
-
}
|
1168
|
-
}
|
@@ -1,151 +0,0 @@
|
|
1
|
-
import { getWorldPosition, setWorldPosition, setWorldPositionXYZ } from "../../engine/engine_three_utils.js";
|
2
|
-
import { Behaviour, GameObject } from "../Component.js";
|
3
|
-
import { AttachedObject, AttachedObjectEvents } from "./WebXRController.js";
|
4
|
-
import { Object3D, Vector3 } from "three";
|
5
|
-
import { PlayerColor } from "../PlayerColor.js";
|
6
|
-
import { Context } from "../../engine/engine_setup.js";
|
7
|
-
import { type IModel, SendQueue } from "../../engine/engine_networking_types.js";
|
8
|
-
|
9
|
-
enum XRGrabEvent {
|
10
|
-
StartOrUpdate = "xr-grab-visual-start-or-update",
|
11
|
-
End = "xr-grab-visual-end",
|
12
|
-
}
|
13
|
-
|
14
|
-
export class XRGrabModel implements IModel {
|
15
|
-
guid!: any;
|
16
|
-
dontSave: boolean = true;
|
17
|
-
|
18
|
-
userId : string | null | undefined;
|
19
|
-
point: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
|
20
|
-
source: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
|
21
|
-
target: string | undefined;
|
22
|
-
|
23
|
-
update(context : Context, point: Vector3, source: Vector3, target: string | undefined = undefined) {
|
24
|
-
this.userId = context.connection.connectionId;
|
25
|
-
this.point.x = point.x;
|
26
|
-
this.point.y = point.y;
|
27
|
-
this.point.z = point.z;
|
28
|
-
this.source.x = source.x;
|
29
|
-
this.source.y = source.y;
|
30
|
-
this.source.z = source.z;
|
31
|
-
this.target = target;
|
32
|
-
}
|
33
|
-
}
|
34
|
-
|
35
|
-
// sends grab info to other users and creates rendering instances
|
36
|
-
export class XRGrabRendering extends Behaviour {
|
37
|
-
prefab: Object3D | null = null;
|
38
|
-
|
39
|
-
private _grabModels: Array<XRGrabModel> = [];
|
40
|
-
private _grabModelsUpdateTime: Array<number> = [];
|
41
|
-
private _addOrUpdateSub: Function | null = null;
|
42
|
-
private _endSub: Function | null = null;
|
43
|
-
private _freeSub: Function | null = null;
|
44
|
-
private _instances: { [key: string]: {instance:Object3D, model:XRGrabModel} } = {};
|
45
|
-
|
46
|
-
awake(): void {
|
47
|
-
if(this.prefab) this.prefab.visible = false;
|
48
|
-
}
|
49
|
-
|
50
|
-
onEnable(): void {
|
51
|
-
this._addOrUpdateSub = this.context.connection.beginListen(XRGrabEvent.StartOrUpdate, this.onRemoteGrabStartOrUpdate.bind(this));
|
52
|
-
this._endSub = this.context.connection.beginListen(XRGrabEvent.End, this.onRemoteGrabEnd.bind(this));
|
53
|
-
this._freeSub = AttachedObject.AddEventListener(AttachedObjectEvents.WillFree, this.onAttachedObjectFree.bind(this));
|
54
|
-
}
|
55
|
-
|
56
|
-
onDisable(): void {
|
57
|
-
this.context.connection.stopListen(XRGrabEvent.StartOrUpdate, this._addOrUpdateSub);
|
58
|
-
this.context.connection.stopListen(XRGrabEvent.End, this._endSub);
|
59
|
-
AttachedObject.RemoveEventListener(AttachedObjectEvents.WillFree, this._freeSub);
|
60
|
-
}
|
61
|
-
|
62
|
-
addOrUpdateGrab(model: XRGrabModel) {
|
63
|
-
this.context.connection.send(XRGrabEvent.StartOrUpdate, model, SendQueue.Queued);
|
64
|
-
}
|
65
|
-
|
66
|
-
endGrab(model: XRGrabModel) {
|
67
|
-
this.context.connection.send(XRGrabEvent.End, model, SendQueue.Queued);
|
68
|
-
}
|
69
|
-
|
70
|
-
private onRemoteGrabStartOrUpdate(data: XRGrabModel) {
|
71
|
-
if(!this.prefab) return;
|
72
|
-
const inst = this._instances[data.guid];
|
73
|
-
if(!inst)
|
74
|
-
{
|
75
|
-
const instance = GameObject.instantiate(this.prefab) as Object3D;
|
76
|
-
instance.visible = true;
|
77
|
-
this._instances[data.guid] = {instance, model:data};
|
78
|
-
if(data.userId){
|
79
|
-
const playerColor = GameObject.getComponentsInChildren(instance, PlayerColor);
|
80
|
-
if(playerColor?.length > 0)
|
81
|
-
{
|
82
|
-
for(const pl of playerColor){
|
83
|
-
pl.assignUserColor(data.userId)
|
84
|
-
}
|
85
|
-
}
|
86
|
-
}
|
87
|
-
return;
|
88
|
-
}
|
89
|
-
inst.model = data;
|
90
|
-
}
|
91
|
-
|
92
|
-
private onRemoteGrabEnd(data: XRGrabModel) {
|
93
|
-
if (!data) return;
|
94
|
-
const id = data.guid;
|
95
|
-
if(this._instances[id])
|
96
|
-
{
|
97
|
-
GameObject.destroy(this._instances[id].instance);
|
98
|
-
delete this._instances[id];
|
99
|
-
}
|
100
|
-
}
|
101
|
-
|
102
|
-
private onAttachedObjectFree(att: AttachedObject) {
|
103
|
-
if (this._grabModels.length <= 0) return;
|
104
|
-
const mod = this._grabModels[0];
|
105
|
-
this.updateModel(mod, att);
|
106
|
-
this.endGrab(mod);
|
107
|
-
}
|
108
|
-
|
109
|
-
onBeforeRender() {
|
110
|
-
this.updateRendering();
|
111
|
-
|
112
|
-
if (!this.prefab) return;
|
113
|
-
this.prefab.visible = false;
|
114
|
-
if (this.context.time.frameCount % 10 !== 0) return;
|
115
|
-
for (let i = 0; i < AttachedObject.Current.length; i++) {
|
116
|
-
const att = AttachedObject.Current[i];
|
117
|
-
|
118
|
-
if (!att.controller || !att.selected) continue;
|
119
|
-
|
120
|
-
if (this._grabModels.length <= i) {
|
121
|
-
this._grabModels.push(new XRGrabModel());
|
122
|
-
this._grabModelsUpdateTime.push(0);
|
123
|
-
}
|
124
|
-
this._grabModelsUpdateTime[i] = this.context.time.time;
|
125
|
-
const model = this._grabModels[i];
|
126
|
-
this.updateModel(model, att);
|
127
|
-
this.addOrUpdateGrab(model);
|
128
|
-
}
|
129
|
-
}
|
130
|
-
|
131
|
-
private updateModel(model: XRGrabModel, att: AttachedObject) {
|
132
|
-
if (!att.controller || !att.selected) return;
|
133
|
-
model.guid = att.grabUUID;
|
134
|
-
const targetObject = att.selected["guid"];
|
135
|
-
model.update(this.context, att.grabPoint, att.controller.worldPosition, targetObject);
|
136
|
-
}
|
137
|
-
|
138
|
-
private temp : Vector3 = new Vector3();
|
139
|
-
private updateRendering() {
|
140
|
-
const step = this.context.time.deltaTime / .5;
|
141
|
-
for(const key in this._instances){
|
142
|
-
const { instance, model } = this._instances[key];
|
143
|
-
if(!instance || !model) continue;
|
144
|
-
const { point } = model;
|
145
|
-
const wp = getWorldPosition(instance);
|
146
|
-
this.temp.set(point.x, point.y, point.z);
|
147
|
-
wp.lerp(this.temp, step);
|
148
|
-
setWorldPosition(instance, wp);
|
149
|
-
}
|
150
|
-
}
|
151
|
-
}
|
@@ -1,13 +1,14 @@
|
|
1
|
-
import {
|
1
|
+
import { Object3D, Quaternion, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { showBalloonMessage, showBalloonWarning } from "../../engine/debug/index.js";
|
4
|
+
import { AssetReference } from "../../engine/engine_addressables.js";
|
2
5
|
import { serializable } from "../../engine/engine_serialization.js";
|
3
|
-
import { Behaviour, GameObject } from "../Component.js";
|
4
|
-
import { Object3D, Quaternion, Vector3 } from "three";
|
5
6
|
import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
|
6
|
-
import {
|
7
|
-
import {
|
8
|
-
|
7
|
+
import { NeedleXREventArgs, NeedleXRSession } from "../../engine/xr/index.js";
|
8
|
+
import { imageToCanvas,USDWriter, USDZExporterContext } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
9
9
|
import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.js";
|
10
|
-
import {
|
10
|
+
import { Behaviour, GameObject } from "../Component.js";
|
11
|
+
import { InstancingUtil, Renderer } from "../Renderer.js";
|
11
12
|
|
12
13
|
// https://github.com/immersive-web/marker-tracking/blob/main/explainer.md
|
13
14
|
|
@@ -44,11 +45,13 @@
|
|
44
45
|
if (t01 === undefined || t01 >= 1 || haveChanged) {
|
45
46
|
object.position.copy(this._position);
|
46
47
|
object.quaternion.copy(this._rotation);
|
48
|
+
// InstancingUtil.markDirty(object);
|
47
49
|
}
|
48
50
|
else {
|
49
51
|
t01 = Math.max(0, Math.min(1, t01));
|
50
52
|
object.position.lerp(this._position, t01);
|
51
53
|
object.quaternion.slerp(this._rotation, t01);
|
54
|
+
// InstancingUtil.markDirty(object);
|
52
55
|
}
|
53
56
|
object.quaternion.multiply(WebXRTrackedImage.y180);
|
54
57
|
}
|
@@ -61,15 +64,10 @@
|
|
61
64
|
if (!this._position) {
|
62
65
|
this._position = WebXRTrackedImage._positionBuffer.get();
|
63
66
|
this._rotation = WebXRTrackedImage._rotationBuffer.get();
|
64
|
-
const t = this._pose.transform;
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
// this._rotation.set(-t.orientation.x, t.orientation.y, -t.orientation.z, t.orientation.w);
|
69
|
-
|
70
|
-
// for some reason when parented to the XRRig, we need the original data
|
71
|
-
this._position.set(t.position.x, t.position.y, t.position.z);
|
72
|
-
this._rotation.set(t.orientation.x, t.orientation.y, t.orientation.z, t.orientation.w);
|
67
|
+
const t = this._pose.transform as XRRigidTransform;
|
68
|
+
const converted = NeedleXRSession.active!.convertSpace(t);
|
69
|
+
this._position.copy(converted?.position);
|
70
|
+
this._rotation.copy(converted?.quaternion);
|
73
71
|
}
|
74
72
|
}
|
75
73
|
|
@@ -141,9 +139,7 @@
|
|
141
139
|
trackedImages?: WebXRImageTrackingModel[];
|
142
140
|
|
143
141
|
private readonly trackedImageIndexMap: Map<number, WebXRImageTrackingModel> = new Map();
|
144
|
-
|
145
142
|
private static _imageElements: Map<string, ImageBitmap | null> = new Map();
|
146
|
-
private webxr: WebXR | null = null;
|
147
143
|
|
148
144
|
awake(): void {
|
149
145
|
if (debug) console.log(this)
|
@@ -182,51 +178,35 @@
|
|
182
178
|
}
|
183
179
|
}
|
184
180
|
|
181
|
+
onBeforeXR(_mode: XRSessionMode, args: XRSessionInit & { trackedImages: Array<any> }): void {
|
182
|
+
// console.log("onXRRequested", args, this.trackedImages)
|
183
|
+
if (this.trackedImages) {
|
184
|
+
args.optionalFeatures = args.optionalFeatures || [];
|
185
|
+
if (!args.optionalFeatures.includes("image-tracking"))
|
186
|
+
args.optionalFeatures.push("image-tracking");
|
185
187
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
WebXR.removeEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
|
198
|
-
this.removeEventListener("image-tracking", this.onImageTrackingUpdate);
|
199
|
-
}
|
200
|
-
|
201
|
-
private onModifyAROptions = (event: any) => {
|
202
|
-
if (!this.trackedImages) return;
|
203
|
-
const options = event.detail;
|
204
|
-
const features = options.optionalFeatures || [];
|
205
|
-
if (!features.includes("image-tracking"))
|
206
|
-
features.push("image-tracking");
|
207
|
-
options.optionalFeatures = features;
|
208
|
-
|
209
|
-
options.trackedImages = [];
|
210
|
-
for (const trackedImage of this.trackedImages) {
|
211
|
-
if (trackedImage.image?.length && trackedImage.widthInMeters > 0) {
|
212
|
-
const bitmap = WebXRImageTracking._imageElements.get(trackedImage.image);
|
213
|
-
if (bitmap) {
|
214
|
-
this.trackedImageIndexMap.set(options.trackedImages.length, trackedImage);
|
215
|
-
options.trackedImages.push({
|
216
|
-
image: bitmap,
|
217
|
-
widthInMeters: trackedImage.widthInMeters
|
218
|
-
});
|
188
|
+
args.trackedImages = [];
|
189
|
+
for (const trackedImage of this.trackedImages) {
|
190
|
+
if (trackedImage.image?.length && trackedImage.widthInMeters > 0) {
|
191
|
+
const bitmap = WebXRImageTracking._imageElements.get(trackedImage.image);
|
192
|
+
if (bitmap) {
|
193
|
+
this.trackedImageIndexMap.set(args.trackedImages.length, trackedImage);
|
194
|
+
args.trackedImages.push({
|
195
|
+
image: bitmap,
|
196
|
+
widthInMeters: trackedImage.widthInMeters
|
197
|
+
});
|
198
|
+
}
|
219
199
|
}
|
220
200
|
}
|
221
201
|
}
|
222
202
|
}
|
223
203
|
|
224
|
-
|
204
|
+
onEnterXR(_args: NeedleXREventArgs): void {
|
225
205
|
if (this.trackedImages) {
|
226
206
|
for (const trackedImage of this.trackedImages) {
|
227
207
|
if (trackedImage.object?.asset) {
|
228
208
|
const obj = trackedImage.object.asset;
|
229
|
-
obj.visible = false;
|
209
|
+
// obj.visible = false;
|
230
210
|
}
|
231
211
|
}
|
232
212
|
}
|
@@ -236,17 +216,16 @@
|
|
236
216
|
}
|
237
217
|
};
|
238
218
|
|
239
|
-
private readonly imageToObjectMap = new Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number, lastTrackingTime:number }>();
|
219
|
+
private readonly imageToObjectMap = new Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number, lastTrackingTime: number }>();
|
240
220
|
private readonly currentImages: WebXRTrackedImage[] = [];
|
241
221
|
|
242
|
-
|
243
|
-
private onXRUpdate = (evt): void => {
|
222
|
+
onUpdateXR(args: NeedleXREventArgs): void {
|
244
223
|
this.currentImages.length = 0;
|
245
224
|
|
246
|
-
const frame =
|
225
|
+
const frame = args.xr.frame;
|
247
226
|
if (!frame) return;
|
248
227
|
|
249
|
-
if (
|
228
|
+
if (!("getImageTrackingResults" in frame)) {
|
250
229
|
const warning = "Image tracking is currently not supported on this device. On Chrome for Android, you can enable the <a target=\"_blank\" href=\"#\" onclick=\"() => console.log('I')\">chrome://flags/#webxr-incubations</a> flag.";
|
251
230
|
if (!this["didPrintWarning"]) {
|
252
231
|
this["didPrintWarning"] = true;
|
@@ -255,8 +234,7 @@
|
|
255
234
|
showBalloonWarning(warning);
|
256
235
|
return;
|
257
236
|
}
|
258
|
-
|
259
|
-
if (frame.session && typeof frame.getImageTrackingResults === "function") {
|
237
|
+
else if (frame.session && typeof frame.getImageTrackingResults === "function") {
|
260
238
|
const results = frame.getImageTrackingResults();
|
261
239
|
if (results.length > 0) {
|
262
240
|
const space = this.context.renderer.xr.getReferenceSpace();
|
@@ -279,9 +257,7 @@
|
|
279
257
|
if (this.currentImages.length > 0) {
|
280
258
|
try {
|
281
259
|
this.dispatchEvent(new CustomEvent("image-tracking", { detail: this.currentImages }));
|
282
|
-
|
283
|
-
this.webxr.allowARPlacementReticle = false;
|
284
|
-
}
|
260
|
+
this.onImageTrackingUpdate(this.currentImages);
|
285
261
|
}
|
286
262
|
catch (e) {
|
287
263
|
console.error(e);
|
@@ -314,9 +290,11 @@
|
|
314
290
|
}
|
315
291
|
|
316
292
|
|
317
|
-
private onImageTrackingUpdate = (
|
318
|
-
const
|
293
|
+
private onImageTrackingUpdate = (images: WebXRTrackedImage[]) => {
|
294
|
+
const xr = NeedleXRSession.active;
|
295
|
+
if (!xr) return;
|
319
296
|
|
297
|
+
|
320
298
|
for (const image of images) {
|
321
299
|
const model = image.model;
|
322
300
|
const isTracked = image.state === "tracked";
|
@@ -329,27 +307,38 @@
|
|
329
307
|
this.imageToObjectMap.set(model, trackedData);
|
330
308
|
|
331
309
|
model.object.loadAssetAsync().then((asset: GameObject | null) => {
|
332
|
-
if (model.createObjectInstance) {
|
310
|
+
if (model.createObjectInstance && asset) {
|
333
311
|
asset = GameObject.instantiate(asset);
|
334
312
|
}
|
335
313
|
|
336
314
|
if (asset) {
|
337
315
|
trackedData!.object = asset;
|
338
316
|
|
317
|
+
// workaround for instancing currently not properly updating
|
318
|
+
// instanced objects become visible when the image is recognized for the second time
|
319
|
+
// we need to look into this further https://linear.app/needle/issue/NE-3936
|
320
|
+
for (const rend of asset.getComponentsInChildren(Renderer)) {
|
321
|
+
rend.setInstancingEnabled(false);
|
322
|
+
}
|
323
|
+
|
339
324
|
// make sure to parent to the WebXR.rig
|
340
|
-
if (
|
341
|
-
|
325
|
+
if (xr.rig) {
|
326
|
+
xr.rig.gameObject.add(asset);
|
327
|
+
image.applyToObject(asset);
|
328
|
+
if (!asset.activeSelf)
|
329
|
+
GameObject.setActive(asset, true);
|
330
|
+
// InstancingUtil.markDirty(asset);
|
342
331
|
}
|
332
|
+
else {
|
333
|
+
console.warn("XRImageTracking: missing XRRig");
|
334
|
+
}
|
343
335
|
|
344
|
-
image.applyToObject(asset);
|
345
|
-
if (!asset.activeSelf)
|
346
|
-
GameObject.setActive(asset, true);
|
347
336
|
}
|
348
337
|
});
|
349
338
|
}
|
350
339
|
else {
|
351
340
|
trackedData.frames++;
|
352
|
-
if(isTracked)
|
341
|
+
if (isTracked)
|
353
342
|
trackedData.lastTrackingTime = Date.now();
|
354
343
|
|
355
344
|
// TODO we could do a bit more here: e.g. sample for the first 1s or so of getting pose data
|
@@ -359,13 +348,16 @@
|
|
359
348
|
|
360
349
|
if (!trackedData.object) continue;
|
361
350
|
|
362
|
-
if (
|
363
|
-
|
351
|
+
if (xr.rig) {
|
352
|
+
|
353
|
+
xr.rig.gameObject.add(trackedData.object);
|
354
|
+
|
355
|
+
image.applyToObject(trackedData.object);
|
356
|
+
if (!trackedData.object.activeSelf) {
|
357
|
+
GameObject.setActive(trackedData.object, true);
|
358
|
+
}
|
359
|
+
// InstancingUtil.markDirty(trackedData.object);
|
364
360
|
}
|
365
|
-
|
366
|
-
image.applyToObject(trackedData.object);
|
367
|
-
if (!trackedData.object.activeSelf)
|
368
|
-
GameObject.setActive(trackedData.object, true);
|
369
361
|
}
|
370
362
|
}
|
371
363
|
}
|
@@ -1,13 +1,14 @@
|
|
1
|
-
import { Box3, BufferAttribute, BufferGeometry, Group, Material, Mesh, Object3D, Vector3 } from "three";
|
1
|
+
import { Box3, BufferAttribute, BufferGeometry, Group, Material, Matrix4, Mesh, MeshBasicMaterial, MeshNormalMaterial, Object3D, Vector3 } from "three";
|
2
2
|
|
3
|
-
import {
|
4
|
-
import {
|
5
|
-
import {
|
3
|
+
import { AssetReference } from "../../engine/engine_addressables.js";
|
4
|
+
import { disposeObjectResources } from "../../engine/engine_assetdatabase.js";
|
5
|
+
import { destroy } from "../../engine/engine_gameobject.js";
|
6
6
|
import { serializable } from "../../engine/engine_serialization.js";
|
7
7
|
import type { Vec3 } from "../../engine/engine_types.js";
|
8
|
-
import { disposeObjectResources } from "../../engine/engine_assetdatabase.js";
|
9
8
|
import { getParam } from "../../engine/engine_utils.js";
|
10
|
-
import {
|
9
|
+
import { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
10
|
+
import { MeshCollider } from "../Collider.js";
|
11
|
+
import { Behaviour, GameObject } from "../Component.js";
|
11
12
|
// import { SimplifyModifier } from 'three/examples/jsm/modifiers/SimplifyModifier.js';
|
12
13
|
|
13
14
|
const debug = getParam("debugplanetracking");
|
@@ -41,8 +42,8 @@
|
|
41
42
|
export class WebXRPlaneTracking extends Behaviour {
|
42
43
|
|
43
44
|
/** Optional: if assigned it will be instantiated per tracked plane/tracked mesh */
|
44
|
-
@serializable(
|
45
|
-
dataTemplate?:
|
45
|
+
@serializable(AssetReference)
|
46
|
+
dataTemplate?: AssetReference;
|
46
47
|
|
47
48
|
@serializable()
|
48
49
|
initiateRoomCaptureIfNoData = true;
|
@@ -53,34 +54,25 @@
|
|
53
54
|
@serializable()
|
54
55
|
useMeshData: boolean = true;
|
55
56
|
|
57
|
+
/** when enabled mesh or plane tracking will also be used in VR */
|
58
|
+
@serializable()
|
59
|
+
runInVR = true;
|
60
|
+
|
56
61
|
get trackedPlanes() { return this._allPlanes.values(); }
|
57
62
|
get trackedMeshes() { return this._allMeshes.values(); }
|
58
63
|
|
59
|
-
onEnable(): void {
|
60
|
-
WebXR.addEventListener(WebXREvent.XRStarted, this.onXRStarted);
|
61
|
-
WebXR.addEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
|
62
|
-
WebXR.addEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
|
63
|
-
}
|
64
64
|
|
65
|
-
onDisable(): void {
|
66
|
-
WebXR.removeEventListener(WebXREvent.XRStarted, this.onXRStarted);
|
67
|
-
WebXR.removeEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
|
68
|
-
WebXR.removeEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
|
69
|
-
}
|
70
65
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
features.push("mesh-detection");
|
79
|
-
|
80
|
-
options.optionalFeatures = features;
|
66
|
+
onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
|
67
|
+
if (_mode === "immersive-vr" && !this.runInVR) return;
|
68
|
+
args.optionalFeatures = args.optionalFeatures || [];
|
69
|
+
if (this.usePlaneData && !args.optionalFeatures.includes("plane-detection"))
|
70
|
+
args.optionalFeatures.push("plane-detection");
|
71
|
+
if (this.useMeshData && !args.optionalFeatures.includes("mesh-detection"))
|
72
|
+
args.optionalFeatures.push("mesh-detection");
|
81
73
|
}
|
82
74
|
|
83
|
-
|
75
|
+
onEnterXR(_evt) {
|
84
76
|
// remove all previously added data from the scene again
|
85
77
|
for (const data of this._allPlanes.keys()) {
|
86
78
|
this.removeData(data, this._allPlanes);
|
@@ -90,18 +82,24 @@
|
|
90
82
|
}
|
91
83
|
}
|
92
84
|
|
93
|
-
|
94
|
-
|
85
|
+
onUpdateXR(args: NeedleXREventArgs): void {
|
86
|
+
|
87
|
+
if (!this.runInVR && args.xr.isVR) return;
|
88
|
+
|
95
89
|
// parenting tracked planes to the XR rig ensures that they synced with the real-world user data;
|
96
90
|
// otherwise they would "swim away" when the user rotates / moves / teleports and so on.
|
97
91
|
// There may be cases where we want that! E.g. a user walks around on their own table in castle builder
|
98
|
-
|
92
|
+
const rig = args.xr.rig;
|
93
|
+
if (!rig) {
|
94
|
+
console.warn("No XR rig found, cannot parent tracked planes to it");
|
95
|
+
return;
|
96
|
+
}
|
99
97
|
|
100
|
-
const frame =
|
98
|
+
const frame = args.xr.frame as XRFramePlanes;
|
101
99
|
const renderer = this.context.renderer;
|
102
100
|
const referenceSpace = renderer.xr.getReferenceSpace();
|
103
101
|
if (!referenceSpace) return;
|
104
|
-
|
102
|
+
|
105
103
|
const planes = frame.detectedPlanes;
|
106
104
|
const meshes = frame.detectedMeshes;
|
107
105
|
const hasAnyPlanes = planes !== undefined && planes.size > 0;
|
@@ -126,10 +124,10 @@
|
|
126
124
|
}
|
127
125
|
|
128
126
|
if (planes !== undefined)
|
129
|
-
this.processFrameData(
|
127
|
+
this.processFrameData(args.xr, rig.gameObject, frame, planes, this._allPlanes);
|
130
128
|
|
131
129
|
if (meshes !== undefined)
|
132
|
-
this.processFrameData(
|
130
|
+
this.processFrameData(args.xr, rig.gameObject, frame, meshes, this._allMeshes);
|
133
131
|
}
|
134
132
|
|
135
133
|
private removeData(data: XRPlane | XRMesh, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
|
@@ -156,11 +154,11 @@
|
|
156
154
|
private readonly _allMeshes = new Map<XRMesh, XRPlaneContext>();
|
157
155
|
private firstTimeNoPlanesDetected = -100;
|
158
156
|
|
159
|
-
private processFrameData(rig: Object3D, frame: XRFramePlanes, detected: Set<XRPlane | XRMesh>, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
|
157
|
+
private processFrameData(_xr: NeedleXRSession, rig: Object3D, frame: XRFramePlanes, detected: Set<XRPlane | XRMesh>, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
|
160
158
|
const renderer = this.context.renderer;
|
161
159
|
const referenceSpace = renderer.xr.getReferenceSpace();
|
162
160
|
if (!referenceSpace) return;
|
163
|
-
|
161
|
+
|
164
162
|
for (const data of _all.keys()) {
|
165
163
|
if (!detected.has(data)) {
|
166
164
|
this.removeData(data, _all);
|
@@ -170,7 +168,7 @@
|
|
170
168
|
for (const data of detected) {
|
171
169
|
const space = "planeSpace" in data ? data.planeSpace
|
172
170
|
: ("meshSpace" in data ? data.meshSpace
|
173
|
-
|
171
|
+
: undefined);
|
174
172
|
if (!space) continue;
|
175
173
|
const planePose = frame.getPose(space, referenceSpace);
|
176
174
|
|
@@ -243,12 +241,18 @@
|
|
243
241
|
|
244
242
|
// if we don't have any template assigned we just use a simple mesh object
|
245
243
|
if (!this.dataTemplate) {
|
246
|
-
|
244
|
+
const mesh = new Mesh();
|
245
|
+
if (debug) mesh.material = new MeshNormalMaterial();
|
246
|
+
else mesh.material = new MeshBasicMaterial({ wireframe: true, opacity: .33, transparent: true, color: 0x000000 });
|
247
|
+
this.dataTemplate = new AssetReference("", "", mesh);
|
247
248
|
}
|
248
249
|
|
249
|
-
if (this.dataTemplate) {
|
250
|
+
if (!this.dataTemplate.asset) {
|
251
|
+
this.dataTemplate.loadAssetAsync();
|
252
|
+
}
|
253
|
+
else {
|
250
254
|
// Create instance
|
251
|
-
const newPlane = GameObject.instantiate(this.dataTemplate) as GameObject;
|
255
|
+
const newPlane = GameObject.instantiate(this.dataTemplate.asset) as GameObject;
|
252
256
|
planeMesh = newPlane;
|
253
257
|
|
254
258
|
if (newPlane instanceof Mesh) {
|
@@ -265,7 +269,7 @@
|
|
265
269
|
}
|
266
270
|
}
|
267
271
|
}
|
268
|
-
|
272
|
+
|
269
273
|
const mc = newPlane.getComponent(MeshCollider) as MeshCollider;
|
270
274
|
if (mc) {
|
271
275
|
const mesh = newPlane as unknown as Mesh;
|
@@ -312,6 +316,7 @@
|
|
312
316
|
if (planePose) {
|
313
317
|
planeMesh.visible = true;
|
314
318
|
planeMesh.matrix.fromArray(planePose.transform.matrix);
|
319
|
+
planeMesh.matrix.premultiply(this._flipForwardMatrix);
|
315
320
|
} else {
|
316
321
|
planeMesh.visible = false;
|
317
322
|
}
|
@@ -319,9 +324,11 @@
|
|
319
324
|
};
|
320
325
|
}
|
321
326
|
|
327
|
+
private _flipForwardMatrix = new Matrix4().makeRotationY(Math.PI);
|
328
|
+
|
322
329
|
// heuristic to determine if a collider should be convex or not -
|
323
330
|
// the "global mesh" should be non-convex, other meshes should be
|
324
|
-
checkIfContextShouldBeConvex(mesh: Mesh | Group | undefined, xrData: XRPlane | XRMesh) {
|
331
|
+
private checkIfContextShouldBeConvex(mesh: Mesh | Group | undefined, xrData: XRPlane | XRMesh) {
|
325
332
|
if (!mesh) return true;
|
326
333
|
if (mesh) {
|
327
334
|
// get bounding box of the mesh
|
@@ -346,7 +353,7 @@
|
|
346
353
|
return true;
|
347
354
|
}
|
348
355
|
|
349
|
-
createGeometry(data: XRPlane | XRMesh) {
|
356
|
+
private createGeometry(data: XRPlane | XRMesh) {
|
350
357
|
if ("polygon" in data) {
|
351
358
|
return this.createPlaneGeometry(data.polygon);
|
352
359
|
}
|
@@ -359,7 +366,7 @@
|
|
359
366
|
// we cache vertices-to-geometry, because it looks like when we get an update sometimes the geometry stays the same.
|
360
367
|
// so we don't want to re-create the geometry every time.
|
361
368
|
private _verticesCache = new Map<string, BufferGeometry>();
|
362
|
-
createMeshGeometry(vertices: Float32Array, indices: Uint32Array) {
|
369
|
+
private createMeshGeometry(vertices: Float32Array, indices: Uint32Array) {
|
363
370
|
const key = vertices.toString() + "_" + indices.toString();
|
364
371
|
if (this._verticesCache.has(key)) {
|
365
372
|
return this._verticesCache.get(key)!;
|
@@ -369,7 +376,7 @@
|
|
369
376
|
geometry.setAttribute('position', new BufferAttribute(vertices, 3));
|
370
377
|
// set UVs in worldspace
|
371
378
|
const uvs = Array<number>();
|
372
|
-
for (let i = 0; i < vertices.length; i+=3) {
|
379
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
373
380
|
uvs.push(vertices[i], vertices[i + 2]);
|
374
381
|
}
|
375
382
|
geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2));
|
@@ -387,9 +394,9 @@
|
|
387
394
|
|
388
395
|
this._verticesCache.set(key, geometry);
|
389
396
|
return geometry;
|
390
|
-
|
397
|
+
}
|
391
398
|
|
392
|
-
createPlaneGeometry(polygon: Vec3[]) {
|
399
|
+
private createPlaneGeometry(polygon: Vec3[]) {
|
393
400
|
const geometry = new BufferGeometry();
|
394
401
|
|
395
402
|
const vertices: number[] = [];
|
@@ -1,22 +1,59 @@
|
|
1
|
-
import { Object3D } from "three";
|
1
|
+
import { AxesHelper, Euler, Object3D, Quaternion, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
2
4
|
import type { IGameObject } from "../../engine/engine_types.js";
|
3
5
|
import { getParam } from "../../engine/engine_utils.js";
|
6
|
+
import { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
7
|
+
import { IXRRig } from "../../engine/engine_xr.js";
|
4
8
|
import { Behaviour } from "../Component.js";
|
5
9
|
import { BoxGizmo } from "../Gizmos.js";
|
6
10
|
|
7
|
-
const debug = getParam("
|
11
|
+
const debug = getParam("debugwebxr");
|
8
12
|
|
9
|
-
export class XRRig extends Behaviour {
|
13
|
+
export class XRRig extends Behaviour implements IXRRig {
|
14
|
+
|
15
|
+
@serializable()
|
16
|
+
priority: number = 0;
|
17
|
+
|
18
|
+
get isActive() { return this.activeAndEnabled && this.gameObject.visible; }
|
19
|
+
|
20
|
+
/** Sets this rig to be the active XR rig (needs to be called during an active XR session) */
|
21
|
+
setAsActiveXRRig() {
|
22
|
+
NeedleXRSession.active?.setRigActive(this);
|
23
|
+
}
|
24
|
+
|
10
25
|
awake(): void {
|
11
|
-
// const helper = new AxesHelper(.1);
|
12
|
-
// this.gameObject.add(helper);
|
13
26
|
if (debug) {
|
14
27
|
const gizmoObj = new Object3D() as IGameObject;
|
15
28
|
gizmoObj.position.y += .5;
|
16
29
|
this.gameObject.add(gizmoObj);
|
17
|
-
const
|
18
|
-
if (
|
19
|
-
|
30
|
+
const box = gizmoObj.addNewComponent(BoxGizmo);
|
31
|
+
if (box)
|
32
|
+
box.isGizmo = false;
|
33
|
+
const axes = new AxesHelper(.5);
|
34
|
+
this.gameObject.add(axes)
|
20
35
|
}
|
21
36
|
}
|
37
|
+
|
38
|
+
isXRRig(): boolean {
|
39
|
+
return true;
|
40
|
+
}
|
41
|
+
|
42
|
+
supportsXR(_mode: XRSessionMode): boolean {
|
43
|
+
return true;
|
44
|
+
}
|
45
|
+
|
46
|
+
private _startScale?: Vector3;
|
47
|
+
|
48
|
+
onEnterXR(args: NeedleXREventArgs): void {
|
49
|
+
this._startScale = this.gameObject.scale.clone();
|
50
|
+
args.xr.addRig(this);
|
51
|
+
if(debug) console.log("WebXR: add Rig", this.name, this.priority)
|
52
|
+
}
|
53
|
+
onLeaveXR(args: NeedleXREventArgs): void {
|
54
|
+
args.xr.removeRig(this);
|
55
|
+
if (this._startScale && this.gameObject)
|
56
|
+
this.gameObject.scale.copy(this._startScale);
|
57
|
+
}
|
58
|
+
|
22
59
|
}
|
@@ -1,463 +0,0 @@
|
|
1
|
-
import { Behaviour, GameObject } from "../Component.js";
|
2
|
-
import { RoomEvents, OwnershipModel, NetworkConnection } from "../../engine/engine_networking.js";
|
3
|
-
import { WebXR, WebXREvent } from "./WebXR.js";
|
4
|
-
import { Group, Quaternion, Vector3, Vector4, WebXRManager } from "three";
|
5
|
-
import { getParam } from "../../engine/engine_utils.js";
|
6
|
-
import { Voip } from "../Voip.js";
|
7
|
-
import { Builder, Long } from "flatbuffers";
|
8
|
-
import { VrUserStateBuffer } from "../../engine-schemes/vr-user-state-buffer.js";
|
9
|
-
import { Vec3 } from "../../engine-schemes/vec3.js";
|
10
|
-
import { registerBinaryType } from "../../engine-schemes/schemes.js";
|
11
|
-
import { Vec4 } from "../../engine-schemes/vec4.js";
|
12
|
-
import { WebXRAvatar } from "./WebXRAvatar.js";
|
13
|
-
|
14
|
-
// for debug GUI
|
15
|
-
// import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
|
16
|
-
// import { HTMLMesh } from 'three/examples/jsm/interactive/HTMLMesh.js';
|
17
|
-
// import { InteractiveGroup } from 'three/examples/jsm/interactive/InteractiveGroup.js';
|
18
|
-
// import { renderer, sceneData } from "../engine/engine_setup.js";
|
19
|
-
|
20
|
-
const debugLogs = getParam("debugxr");
|
21
|
-
const debugAvatar = getParam("debugavatar");
|
22
|
-
// const debugAvatarVoip = getParam("debugavatarvoip");
|
23
|
-
|
24
|
-
enum WebXRSyncEvent {
|
25
|
-
WebXR_UserJoined = "webxr-user-joined",
|
26
|
-
WebXR_UserLeft = "webxr-user-left",
|
27
|
-
VRSessionStart = "vr-session-started",
|
28
|
-
VRSessionEnd = "vr-session-ended",
|
29
|
-
VRSessionUpdate = "vr-session-update",
|
30
|
-
}
|
31
|
-
|
32
|
-
enum XRMode {
|
33
|
-
VR = "vr",
|
34
|
-
AR = "ar",
|
35
|
-
}
|
36
|
-
|
37
|
-
const VRUserStateBufferIdentifier = "VRUS";
|
38
|
-
registerBinaryType(VRUserStateBufferIdentifier, VrUserStateBuffer.getRootAsVrUserStateBuffer);
|
39
|
-
|
40
|
-
function getTimeStampNow() {
|
41
|
-
return new Date().getTime(); // avoid sending millis in flatbuffer
|
42
|
-
}
|
43
|
-
|
44
|
-
function flatbuffers_long_from_number(num: number): Long {
|
45
|
-
const low = num & 0xffffffff
|
46
|
-
const high = (num / Math.pow(2, 32)) & 0xfffff
|
47
|
-
return Long.create(low, high);
|
48
|
-
}
|
49
|
-
|
50
|
-
export class VRUserState {
|
51
|
-
public guid: string;
|
52
|
-
public time!: number;
|
53
|
-
public avatarId!: string;
|
54
|
-
public position: Vector3 = new Vector3();
|
55
|
-
public rotation: Vector4 = new Vector4();
|
56
|
-
public scale: number = 1;
|
57
|
-
|
58
|
-
public posLeftHand = new Vector3();
|
59
|
-
public posRightHand = new Vector3();
|
60
|
-
|
61
|
-
public rotLeftHand = new Quaternion();
|
62
|
-
public rotRightHand = new Quaternion();
|
63
|
-
|
64
|
-
public constructor(guid: string) {
|
65
|
-
this.guid = guid;
|
66
|
-
}
|
67
|
-
|
68
|
-
private static invertRotation: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
69
|
-
|
70
|
-
public update(rig: Group, pos: DOMPointReadOnly, rot: DOMPointReadOnly, webXR: WebXR, avatarId: string) {
|
71
|
-
this.time = getTimeStampNow();
|
72
|
-
this.avatarId = avatarId;
|
73
|
-
this.position.set(pos.x, pos.y, pos.z);
|
74
|
-
if (rig)
|
75
|
-
this.position.applyMatrix4(rig.matrixWorld);
|
76
|
-
|
77
|
-
let q0 = VRUserState.quat0;
|
78
|
-
const q1 = VRUserState.quat1;
|
79
|
-
q0.set(rot.x, rot.y, rot.z, rot.w);
|
80
|
-
q0 = q0.multiplyQuaternions(q0, VRUserState.invertRotation);
|
81
|
-
|
82
|
-
if (rig) {
|
83
|
-
rig.getWorldQuaternion(q1);
|
84
|
-
q0.multiplyQuaternions(q1, q0);
|
85
|
-
}
|
86
|
-
|
87
|
-
this.rotation.set(q0.x, q0.y, q0.z, q0.w);
|
88
|
-
this.scale = rig.scale.x;
|
89
|
-
|
90
|
-
// for controllers, it seems we need grip pose
|
91
|
-
const ctrl0 = webXR.LeftController?.controllerGrip;
|
92
|
-
if (ctrl0) {
|
93
|
-
ctrl0.getWorldPosition(this.posLeftHand);
|
94
|
-
ctrl0.getWorldQuaternion(this.rotLeftHand);
|
95
|
-
}
|
96
|
-
const ctrl1 = webXR.RightController?.controllerGrip;
|
97
|
-
if (ctrl1) {
|
98
|
-
ctrl1.getWorldPosition(this.posRightHand);
|
99
|
-
ctrl1.getWorldQuaternion(this.rotRightHand);
|
100
|
-
}
|
101
|
-
|
102
|
-
// if this is a hand, we need to get the root bone of that / use that for position/rotation
|
103
|
-
if (webXR.LeftController?.hand?.visible) {
|
104
|
-
const wrist = webXR.LeftController.wrist;
|
105
|
-
if (wrist) {
|
106
|
-
wrist.getWorldPosition(this.posLeftHand);
|
107
|
-
wrist.getWorldQuaternion(this.rotLeftHand);
|
108
|
-
}
|
109
|
-
}
|
110
|
-
|
111
|
-
if (webXR.RightController?.hand?.visible) {
|
112
|
-
const wrist = webXR.RightController.wrist;
|
113
|
-
if (wrist) {
|
114
|
-
wrist.getWorldPosition(this.posRightHand);
|
115
|
-
wrist.getWorldQuaternion(this.rotRightHand);
|
116
|
-
}
|
117
|
-
}
|
118
|
-
}
|
119
|
-
|
120
|
-
private static quat0: Quaternion = new Quaternion();
|
121
|
-
private static quat1: Quaternion = new Quaternion();
|
122
|
-
|
123
|
-
public sendAsBuffer(builder: Builder, net: NetworkConnection) {
|
124
|
-
builder.clear();
|
125
|
-
const guid = builder.createString(this.guid);
|
126
|
-
const id = builder.createString(this.avatarId);
|
127
|
-
VrUserStateBuffer.startVrUserStateBuffer(builder);
|
128
|
-
VrUserStateBuffer.addGuid(builder, guid);
|
129
|
-
VrUserStateBuffer.addTime(builder, flatbuffers_long_from_number(this.time));
|
130
|
-
VrUserStateBuffer.addAvatarId(builder, id);
|
131
|
-
VrUserStateBuffer.addPosition(builder, Vec3.createVec3(builder, this.position.x, this.position.y, this.position.z));
|
132
|
-
VrUserStateBuffer.addRotation(builder, Vec4.createVec4(builder, this.rotation.x, this.rotation.y, this.rotation.z, this.rotation.w));
|
133
|
-
VrUserStateBuffer.addScale(builder, this.scale);
|
134
|
-
VrUserStateBuffer.addPosLeftHand(builder, Vec3.createVec3(builder, this.posLeftHand.x, this.posLeftHand.y, this.posLeftHand.z));
|
135
|
-
VrUserStateBuffer.addPosRightHand(builder, Vec3.createVec3(builder, this.posRightHand.x, this.posRightHand.y, this.posRightHand.z));
|
136
|
-
VrUserStateBuffer.addRotLeftHand(builder, Vec4.createVec4(builder, this.rotLeftHand.x, this.rotLeftHand.y, this.rotLeftHand.z, this.rotLeftHand.w));
|
137
|
-
VrUserStateBuffer.addRotRightHand(builder, Vec4.createVec4(builder, this.rotRightHand.x, this.rotRightHand.y, this.rotRightHand.z, this.rotRightHand.w));
|
138
|
-
const res = VrUserStateBuffer.endVrUserStateBuffer(builder);
|
139
|
-
builder.finish(res, VRUserStateBufferIdentifier);
|
140
|
-
const arr = builder.asUint8Array();
|
141
|
-
net.sendBinary(arr);
|
142
|
-
}
|
143
|
-
|
144
|
-
public setFromBuffer(guid: string, state: VrUserStateBuffer) {
|
145
|
-
if (!guid) return;
|
146
|
-
this.guid = guid;
|
147
|
-
this.time = state.time().toFloat64();
|
148
|
-
const id = state.avatarId();
|
149
|
-
if (id)
|
150
|
-
this.avatarId = id;
|
151
|
-
const pos = state.position();
|
152
|
-
if (pos)
|
153
|
-
this.position.set(pos.x(), pos.y(), pos.z());
|
154
|
-
// TODO: maybe just send one float more instead of converting back and forth
|
155
|
-
const rot = state.rotation();
|
156
|
-
if (rot)
|
157
|
-
this.rotation.set(rot.x(), rot.y(), rot.z(), rot.w());
|
158
|
-
const posLeftHand = state.posLeftHand();
|
159
|
-
if (posLeftHand)
|
160
|
-
this.posLeftHand.set(posLeftHand.x(), posLeftHand.y(), posLeftHand.z());
|
161
|
-
const posRightHand = state.posRightHand();
|
162
|
-
if (posRightHand)
|
163
|
-
this.posRightHand.set(posRightHand.x(), posRightHand.y(), posRightHand.z());
|
164
|
-
const rotLeftHand = state.rotLeftHand();
|
165
|
-
if (rotLeftHand)
|
166
|
-
this.rotLeftHand.set(rotLeftHand.x(), rotLeftHand.y(), rotLeftHand.z(), rotLeftHand.w());
|
167
|
-
const rotRightHand = state.rotRightHand();
|
168
|
-
if (rotRightHand)
|
169
|
-
this.rotRightHand.set(rotRightHand.x(), rotRightHand.y(), rotRightHand.z(), rotRightHand.w());
|
170
|
-
this.scale = state.scale();
|
171
|
-
}
|
172
|
-
}
|
173
|
-
|
174
|
-
export class WebXRSync extends Behaviour {
|
175
|
-
|
176
|
-
webXR: WebXR | null = null;
|
177
|
-
|
178
|
-
// private allowCustomAvatars: boolean | null = true;
|
179
|
-
|
180
|
-
private debugAvatarUser: WebXRAvatar | null = null;
|
181
|
-
private voip: Voip | null = null;
|
182
|
-
|
183
|
-
async awake() {
|
184
|
-
|
185
|
-
if(!this.webXR) this.webXR = GameObject.getComponent(this.gameObject, WebXR);
|
186
|
-
if(!this.webXR) this.webXR = GameObject.findObjectOfType(WebXR, this.context);
|
187
|
-
|
188
|
-
if(!this.webXR)
|
189
|
-
{
|
190
|
-
this.webXR = GameObject.findObjectOfType(WebXR, this.context);
|
191
|
-
if(!this.webXR) {
|
192
|
-
console.warn("WebXRSync: Could not find WebXR component, won't sync.");
|
193
|
-
return;
|
194
|
-
}
|
195
|
-
}
|
196
|
-
|
197
|
-
if (!this.voip) this.voip = GameObject.findObjectOfType(Voip, this.context);
|
198
|
-
|
199
|
-
if (debugAvatar) {
|
200
|
-
const debugGuid = "debug-avatar-" + debugAvatar;
|
201
|
-
const newUser = new WebXRAvatar(this.context, debugGuid, this.webXR);
|
202
|
-
// newUser.isLocalAvatar = true;
|
203
|
-
this.debugAvatarUser = newUser;
|
204
|
-
if (typeof debugAvatar === "string" && debugAvatar.length > 0) {
|
205
|
-
if (await newUser.setAvatarOverride(debugAvatar)) {
|
206
|
-
const debugState = new VRUserState(debugGuid);
|
207
|
-
debugState.position.y += 1;
|
208
|
-
const off = .5;
|
209
|
-
debugState.posLeftHand.y += off;
|
210
|
-
debugState.posLeftHand.x += off;
|
211
|
-
debugState.posRightHand.y += off;
|
212
|
-
debugState.posRightHand.x -= off;
|
213
|
-
newUser.tryUpdate(debugState, 0);
|
214
|
-
}
|
215
|
-
else {
|
216
|
-
newUser.destroy();
|
217
|
-
}
|
218
|
-
}
|
219
|
-
}
|
220
|
-
}
|
221
|
-
|
222
|
-
onEnable() {
|
223
|
-
// const debugUser = new WebXRAvatar(this.context, "sorry-no-guid", this.webXR!);
|
224
|
-
|
225
|
-
if (!this.webXR) {
|
226
|
-
this.webXR = GameObject.getComponent(this.gameObject, WebXR);
|
227
|
-
if (!this.webXR) {
|
228
|
-
console.warn("Missing webxr component on " + this.gameObject.name);
|
229
|
-
return;
|
230
|
-
}
|
231
|
-
}
|
232
|
-
|
233
|
-
this.eventSub_WebXRStartEvent = this.onXRSessionStart.bind(this);
|
234
|
-
WebXR.addEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
|
235
|
-
this.eventSub_WebXRUpdateEvent = this.onXRSessionUpdate.bind(this);
|
236
|
-
WebXR.addEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
|
237
|
-
this.eventSub_WebXREndEvent = this.onXRSessionEnded.bind(this);
|
238
|
-
WebXR.addEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
|
239
|
-
|
240
|
-
this.eventSub_ConnectionEvent = this.onConnected.bind(this);
|
241
|
-
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
|
242
|
-
this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserJoined, _evt => {
|
243
|
-
console.log("webxr user joined evt");
|
244
|
-
});
|
245
|
-
this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserLeft, evt => {
|
246
|
-
const hasId = evt.id !== null && evt.id !== undefined;
|
247
|
-
if (!hasId) return;
|
248
|
-
console.log("webxr user left evt");
|
249
|
-
if (hasId) {
|
250
|
-
const avatar = this.avatars[evt.id];
|
251
|
-
avatar?.destroy();
|
252
|
-
this.avatars[evt.id] = undefined;
|
253
|
-
}
|
254
|
-
});
|
255
|
-
this.context.connection.beginListenBinary(VRUserStateBufferIdentifier, (state: VrUserStateBuffer) => {
|
256
|
-
// console.log("BUFFER", state);
|
257
|
-
const guid = state.guid();
|
258
|
-
if (!guid) return;
|
259
|
-
const time = state.time().toFloat64();
|
260
|
-
const temp = this.tempState;
|
261
|
-
temp.setFromBuffer(guid, state);
|
262
|
-
// console.log(temp);
|
263
|
-
const user = this.onTryGetAvatar(guid, time);
|
264
|
-
user?.tryUpdate(temp, time);
|
265
|
-
});
|
266
|
-
this.context.connection.beginListen(WebXRSyncEvent.VRSessionUpdate, (state: VRUserState) => {
|
267
|
-
const guid = state.guid;
|
268
|
-
const time = state.time;
|
269
|
-
const user = this.onTryGetAvatar(guid, time);
|
270
|
-
user?.tryUpdate(state, time);
|
271
|
-
});
|
272
|
-
}
|
273
|
-
|
274
|
-
private tempState: VRUserState = new VRUserState("");
|
275
|
-
|
276
|
-
private onTryGetAvatar(guid: string, time: number) {
|
277
|
-
if (guid === this.context.connection.connectionId) return null; // ignore self in case we receive that also!
|
278
|
-
const timeDiff = new Date().getTime() - time;
|
279
|
-
if (timeDiff > 5000) {
|
280
|
-
if (debugLogs)
|
281
|
-
console.log("old data", timeDiff, guid)
|
282
|
-
return null;
|
283
|
-
}
|
284
|
-
if (!this.webXR) return null;
|
285
|
-
let user = this.avatars[guid];
|
286
|
-
if (user === undefined) {
|
287
|
-
try {
|
288
|
-
console.log("create new avatar");
|
289
|
-
const newUser = new WebXRAvatar(this.context, guid, this.webXR);
|
290
|
-
user = newUser;
|
291
|
-
this.avatars[guid] = newUser;
|
292
|
-
} catch (err) {
|
293
|
-
this.avatars[guid] = null;
|
294
|
-
console.error(err);
|
295
|
-
}
|
296
|
-
}
|
297
|
-
return user;
|
298
|
-
}
|
299
|
-
|
300
|
-
onDisable() {
|
301
|
-
if (this.eventSub_ConnectionEvent)
|
302
|
-
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
|
303
|
-
WebXR.removeEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
|
304
|
-
WebXR.removeEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
|
305
|
-
WebXR.removeEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
|
306
|
-
}
|
307
|
-
|
308
|
-
update(): void {
|
309
|
-
|
310
|
-
const now = getTimeStampNow();
|
311
|
-
|
312
|
-
if (this.debugAvatarUser) {
|
313
|
-
this.debugAvatarUser.lastUpdate = now;
|
314
|
-
}
|
315
|
-
|
316
|
-
this.detectPotentiallyDisconnectedAvatarsAndRemove();
|
317
|
-
|
318
|
-
for (const key in this.avatars) {
|
319
|
-
const avatar = this.avatars[key];
|
320
|
-
if (!avatar) continue;
|
321
|
-
avatar.update();
|
322
|
-
}
|
323
|
-
}
|
324
|
-
|
325
|
-
|
326
|
-
private _removeAvatarsList: string[] = [];
|
327
|
-
private detectPotentiallyDisconnectedAvatarsAndRemove() {
|
328
|
-
const utcnow = getTimeStampNow();
|
329
|
-
for (const key in this.avatars) {
|
330
|
-
const avatar = this.avatars[key];
|
331
|
-
if (!avatar) {
|
332
|
-
this._removeAvatarsList.push(key);
|
333
|
-
continue;
|
334
|
-
}
|
335
|
-
if (utcnow - avatar.lastUpdate > 10_000) {
|
336
|
-
console.log("avatar timed out (didnt receive any updates in a while) - destroying it now");
|
337
|
-
avatar.destroy();
|
338
|
-
this.avatars[key] = undefined;
|
339
|
-
}
|
340
|
-
}
|
341
|
-
for (const rem of this._removeAvatarsList) {
|
342
|
-
delete this.avatars[rem];
|
343
|
-
}
|
344
|
-
this._removeAvatarsList.length = 0;
|
345
|
-
}
|
346
|
-
|
347
|
-
private buildLocalAvatar() {
|
348
|
-
if (this.localAvatar || !this.webXR) return;
|
349
|
-
const connectionId = this.context.connection?.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
|
350
|
-
this.localAvatar = new WebXRAvatar(this.context, connectionId, this.webXR);
|
351
|
-
this.localAvatar.isLocalAvatar = true;
|
352
|
-
this.localAvatar.setAvatarOverride(this.getAvatarId());
|
353
|
-
this.avatars[this.localAvatar.guid] = this.localAvatar;
|
354
|
-
}
|
355
|
-
|
356
|
-
|
357
|
-
private eventSub_ConnectionEvent: Function | null = null;
|
358
|
-
private eventSub_WebXRStartEvent: Function | null = null;
|
359
|
-
private eventSub_WebXREndEvent: Function | null = null;
|
360
|
-
private eventSub_WebXRUpdateEvent: Function | null = null;
|
361
|
-
private avatars: { [key: string]: WebXRAvatar | undefined | null } = {}
|
362
|
-
private localAvatar: WebXRAvatar | null = null;
|
363
|
-
private k_LocalAvatarNoNetworkingGuid = "local";
|
364
|
-
|
365
|
-
private onConnected() {
|
366
|
-
// this event gets fired when we have joined a room and are ready to update
|
367
|
-
if (debugLogs)
|
368
|
-
console.log("Hey you are connected as " + this.context.connection.connectionId);
|
369
|
-
|
370
|
-
if (this.localAvatar?.guid === this.k_LocalAvatarNoNetworkingGuid) {
|
371
|
-
if (this.localAvatar) {
|
372
|
-
this.localAvatar?.destroy();
|
373
|
-
this.avatars[this.localAvatar.guid] = undefined;
|
374
|
-
}
|
375
|
-
this.localAvatar = null;
|
376
|
-
this.xrState = null;
|
377
|
-
this.ownership?.freeOwnership();
|
378
|
-
this.ownership = null;
|
379
|
-
}
|
380
|
-
}
|
381
|
-
|
382
|
-
private onXRSessionStart(_evt: { session: XRSession }) {
|
383
|
-
console.log("XR session started");
|
384
|
-
this.context.connection.send(WebXRSyncEvent.WebXR_UserJoined, { id: this.context.connection.connectionId, mode: XRMode.VR });
|
385
|
-
|
386
|
-
if (this.localAvatar) {
|
387
|
-
this.localAvatar?.destroy();
|
388
|
-
this.avatars[this.localAvatar.guid] = undefined;
|
389
|
-
this.localAvatar = null;
|
390
|
-
}
|
391
|
-
this.xrState = null;
|
392
|
-
this.ownership?.freeOwnership();
|
393
|
-
this.ownership = null;
|
394
|
-
|
395
|
-
if (this.avatars) {
|
396
|
-
for (const key in this.avatars) {
|
397
|
-
this.avatars[key]?.updateFlags();
|
398
|
-
}
|
399
|
-
}
|
400
|
-
}
|
401
|
-
|
402
|
-
private onXRSessionEnded(_evt: { session: XRSession }) {
|
403
|
-
console.log("XR session ended");
|
404
|
-
this.context.connection.send(WebXRSyncEvent.WebXR_UserLeft, { id: this.context.connection.connectionId, mode: XRMode.VR });
|
405
|
-
if(this.localAvatar){
|
406
|
-
this.localAvatar?.destroy();
|
407
|
-
this.avatars[this.localAvatar.guid] = undefined;
|
408
|
-
this.localAvatar = null;
|
409
|
-
}
|
410
|
-
}
|
411
|
-
|
412
|
-
private ownership: OwnershipModel | null = null;
|
413
|
-
private xrState: VRUserState | null = null;
|
414
|
-
private builder: Builder = new Builder(1024);
|
415
|
-
|
416
|
-
private onXRSessionUpdate(evt: { rig: Group, frame: XRFrame, xr: WebXRManager, input: XRInputSource[] }) {
|
417
|
-
|
418
|
-
this.xrState ??= new VRUserState(this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
|
419
|
-
this.ownership ??= new OwnershipModel(this.context.connection, this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
|
420
|
-
this.ownership.guid = this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
|
421
|
-
this.buildLocalAvatar();
|
422
|
-
|
423
|
-
|
424
|
-
const { frame, xr, rig } = evt;
|
425
|
-
const pose = frame.getViewerPose(xr.getReferenceSpace()!);
|
426
|
-
if (!pose) return; // e.g. if user is not wearing headset
|
427
|
-
const transform: XRRigidTransform = pose?.transform;
|
428
|
-
const pos = transform.position;
|
429
|
-
const rot = transform.orientation;
|
430
|
-
this.xrState.update(rig, pos, rot, this.webXR!, this.getAvatarId());
|
431
|
-
|
432
|
-
if (this.localAvatar) {
|
433
|
-
if (this.context.connection.connectionId) {
|
434
|
-
this.localAvatar.guid = this.context.connection.connectionId;
|
435
|
-
}
|
436
|
-
this.localAvatar.tryUpdate(this.xrState, 0);
|
437
|
-
}
|
438
|
-
|
439
|
-
if (this.ownership && !this.ownership.hasOwnership && this.context.connection.isConnected) {
|
440
|
-
if (this.context.time.frameCount % 120 === 0)
|
441
|
-
this.ownership.requestOwnership();
|
442
|
-
if (!this.ownership.hasOwnership) {
|
443
|
-
// console.log("NO OWNERSHIP", this.ownership.guid);
|
444
|
-
return;
|
445
|
-
}
|
446
|
-
}
|
447
|
-
|
448
|
-
if (!this.context.connection.isConnected || !this.context.connection.connectionId) {
|
449
|
-
return;
|
450
|
-
}
|
451
|
-
|
452
|
-
this.xrState.sendAsBuffer(this.builder, this.context.connection);
|
453
|
-
|
454
|
-
// this.context.connection.send(WebXRSyncEvent.VRSessionUpdate, this.xrState);
|
455
|
-
|
456
|
-
}
|
457
|
-
|
458
|
-
private getAvatarId() {
|
459
|
-
const urlAvatar = getParam("avatar") as string;
|
460
|
-
const avatarId = urlAvatar ?? null;
|
461
|
-
return avatarId;
|
462
|
-
}
|
463
|
-
}
|
@@ -1,139 +0,0 @@
|
|
1
|
-
import { Behaviour, GameObject } from "./Component.js";
|
2
|
-
import { getParam } from "../engine/engine_utils.js";
|
3
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
|
-
|
5
|
-
|
6
|
-
const debug = getParam("debugflags");
|
7
|
-
|
8
|
-
export enum XRStateFlag {
|
9
|
-
Never = 0,
|
10
|
-
Browser = 1 << 0,
|
11
|
-
AR = 1 << 1,
|
12
|
-
VR = 1 << 2,
|
13
|
-
FirstPerson = 1 << 3,
|
14
|
-
ThirdPerson = 1 << 4,
|
15
|
-
All = 0xffffffff
|
16
|
-
}
|
17
|
-
|
18
|
-
export class XRState {
|
19
|
-
|
20
|
-
public static Global: XRState = new XRState();
|
21
|
-
|
22
|
-
public Mask: XRStateFlag = XRStateFlag.Browser | XRStateFlag.ThirdPerson;
|
23
|
-
|
24
|
-
public Has(state: XRStateFlag) {
|
25
|
-
const res = (this.Mask & state);
|
26
|
-
return res !== 0;
|
27
|
-
}
|
28
|
-
|
29
|
-
public Set(state: number) {
|
30
|
-
if(debug) console.warn("Set XR flag state to", state)
|
31
|
-
this.Mask = state as number;
|
32
|
-
XRFlag.Apply();
|
33
|
-
}
|
34
|
-
|
35
|
-
public Enable(state: number) {
|
36
|
-
this.Mask |= state;
|
37
|
-
XRFlag.Apply();
|
38
|
-
}
|
39
|
-
|
40
|
-
public Disable(state: number) {
|
41
|
-
this.Mask &= ~state;
|
42
|
-
XRFlag.Apply();
|
43
|
-
}
|
44
|
-
|
45
|
-
public Toggle(state: number) {
|
46
|
-
this.Mask ^= state;
|
47
|
-
XRFlag.Apply();
|
48
|
-
}
|
49
|
-
|
50
|
-
public EnableAll() {
|
51
|
-
this.Mask = 0xffffffff | 0;
|
52
|
-
XRFlag.Apply();
|
53
|
-
}
|
54
|
-
|
55
|
-
public DisableAll() {
|
56
|
-
this.Mask = 0;
|
57
|
-
XRFlag.Apply();
|
58
|
-
}
|
59
|
-
}
|
60
|
-
|
61
|
-
export class XRFlag extends Behaviour {
|
62
|
-
|
63
|
-
private static registry: XRFlag[] = [];
|
64
|
-
|
65
|
-
public static Apply() {
|
66
|
-
for (const r of this.registry) r.UpdateVisible(XRState.Global);
|
67
|
-
}
|
68
|
-
|
69
|
-
private static firstApply: boolean;
|
70
|
-
private static buffer: XRState = new XRState();
|
71
|
-
|
72
|
-
@serializable()
|
73
|
-
public visibleIn!: number;
|
74
|
-
|
75
|
-
awake() {
|
76
|
-
XRFlag.registry.push(this);
|
77
|
-
}
|
78
|
-
|
79
|
-
onEnable(): void {
|
80
|
-
if (!XRFlag.firstApply) {
|
81
|
-
XRFlag.firstApply = true;
|
82
|
-
XRFlag.Apply();
|
83
|
-
}
|
84
|
-
else {
|
85
|
-
this.UpdateVisible(XRState.Global);
|
86
|
-
}
|
87
|
-
}
|
88
|
-
|
89
|
-
onDestroy(): void {
|
90
|
-
const i = XRFlag.registry.indexOf(this);
|
91
|
-
if (i >= 0)
|
92
|
-
XRFlag.registry.splice(i, 1);
|
93
|
-
}
|
94
|
-
|
95
|
-
public get isOn(): boolean { return this.gameObject.visible; }
|
96
|
-
|
97
|
-
public UpdateVisible(state: XRState | XRStateFlag | null = null) {
|
98
|
-
// XR flags set visibility of whole hierarchy which is like setting the whole object inactive
|
99
|
-
// so we need to ignore the enabled state of the XRFlag component
|
100
|
-
// if(!this.enabled) return;
|
101
|
-
let res: boolean | undefined = undefined;
|
102
|
-
|
103
|
-
const flag = state as number;
|
104
|
-
if (flag && typeof flag === "number") {
|
105
|
-
console.assert(typeof flag === "number", "XRFlag.UpdateVisible: state must be a number", flag);
|
106
|
-
if (debug)
|
107
|
-
console.log(flag);
|
108
|
-
XRFlag.buffer.Mask = flag;
|
109
|
-
state = XRFlag.buffer;
|
110
|
-
}
|
111
|
-
|
112
|
-
const st = state as XRState;
|
113
|
-
if (st) {
|
114
|
-
if (debug)
|
115
|
-
console.warn(this.name, "use passed in mask", st.Mask, this.visibleIn)
|
116
|
-
res = st.Has(this.visibleIn);
|
117
|
-
}
|
118
|
-
else {
|
119
|
-
if (debug)
|
120
|
-
console.log(this.name, "use global mask")
|
121
|
-
XRState.Global.Has(this.visibleIn);
|
122
|
-
}
|
123
|
-
if (res === undefined) return;
|
124
|
-
if (res) {
|
125
|
-
if (debug)
|
126
|
-
console.log(this.name, "is visible", this.gameObject.uuid)
|
127
|
-
// this.gameObject.visible = true;
|
128
|
-
GameObject.setActive(this.gameObject, true);
|
129
|
-
} else {
|
130
|
-
if (debug)
|
131
|
-
console.log(this.name, "is not visible", this.gameObject.uuid);
|
132
|
-
const isVisible = this.gameObject.visible;
|
133
|
-
if(!isVisible) return;
|
134
|
-
this.gameObject.visible = false;
|
135
|
-
// console.trace("DISABLE", this.name);
|
136
|
-
// GameObject.setActive(this.gameObject, false);
|
137
|
-
}
|
138
|
-
}
|
139
|
-
}
|
@@ -0,0 +1,56 @@
|
|
1
|
+
import fs from 'fs';
|
2
|
+
|
3
|
+
|
4
|
+
/** Create a file containing information about the build inside the build directory
|
5
|
+
* @param {String} buildDirectory
|
6
|
+
*/
|
7
|
+
export function createBuildInfoFile(buildDirectory) {
|
8
|
+
if (!buildDirectory) {
|
9
|
+
console.warn("WARN: Can not create build info file because \"buildDirectory\" is not defined");
|
10
|
+
return;
|
11
|
+
}
|
12
|
+
// start creating the buildinfo
|
13
|
+
const buildInfo = {
|
14
|
+
time: new Date().toISOString(),
|
15
|
+
totalsize: 0,
|
16
|
+
files: []
|
17
|
+
};
|
18
|
+
console.log("[needle-buildinfo] - Collect files in " + buildDirectory);
|
19
|
+
recursivelyCollectFiles(buildDirectory, "", buildInfo);
|
20
|
+
const buildInfoPath = `${buildDirectory}/needle.buildinfo.json`;
|
21
|
+
const totalSizeInMB = buildInfo.totalsize / 1024 / 1024;
|
22
|
+
console.log(`[needle-buildinfo] - Collected ${buildInfo.files.length} files (${totalSizeInMB.toFixed(2)} MB). Write info to \"${buildInfoPath}\"`);
|
23
|
+
fs.writeFileSync(buildInfoPath, JSON.stringify(buildInfo));
|
24
|
+
}
|
25
|
+
|
26
|
+
/** Recursively collect all files in a directory
|
27
|
+
* @param {String} directory to search
|
28
|
+
* @param {{ files: Array<string>, totalsize:number }} info
|
29
|
+
*/
|
30
|
+
function recursivelyCollectFiles(directory, path, info) {
|
31
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
32
|
+
for (const entry of entries) {
|
33
|
+
if (entry.isDirectory()) {
|
34
|
+
// make sure we never collect files inside node_modules
|
35
|
+
if (entry.name === "node_modules") {
|
36
|
+
console.warn("WARN: Skipping node_modules directory at " + path);
|
37
|
+
continue;
|
38
|
+
}
|
39
|
+
const newPath = path?.length <= 0 ? entry.name : `${path}/${entry.name}`;
|
40
|
+
const newDirectory = `${directory}/${entry.name}`;
|
41
|
+
console.log("[needle-buildinfo] - Collect files in " + newPath);
|
42
|
+
recursivelyCollectFiles(newDirectory, newPath, info);
|
43
|
+
} else {
|
44
|
+
const relpath = `${path}/${entry.name}`;
|
45
|
+
info.files.push(relpath);
|
46
|
+
try {
|
47
|
+
const fullpath = `${directory}/${entry.name}`;
|
48
|
+
const stats = fs.statSync(fullpath);
|
49
|
+
info.totalsize += stats.size;
|
50
|
+
}
|
51
|
+
catch {
|
52
|
+
//ignore
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { createBuildInfoFile } from '../common/buildinfo.js';
|
2
|
+
import { getOutputDirectory } from './config.js';
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
/** Create a buildinfo file in the build directory
|
7
|
+
* @param {import('../types').userSettings} userSettings
|
8
|
+
* @returns {import('vite').Plugin}
|
9
|
+
*/
|
10
|
+
export const needleBuildInfo = (command, config, userSettings) => {
|
11
|
+
|
12
|
+
if (userSettings?.noBuildInfo) return;
|
13
|
+
|
14
|
+
return {
|
15
|
+
name: 'needle-buildinfo',
|
16
|
+
apply: "build",
|
17
|
+
enforce: "post",
|
18
|
+
closeBundle: () => {
|
19
|
+
const buildDirectory = getOutputDirectory();
|
20
|
+
createBuildInfoFile(buildDirectory);
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
@@ -0,0 +1,2 @@
|
|
1
|
+
Using flatbuffer compiler 2.0
|
2
|
+
https://github.com/google/flatbuffers/releases/tag/v2.0.0
|
@@ -0,0 +1,232 @@
|
|
1
|
+
import { Object3D, Quaternion, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { AssetReference } from "../../engine/engine_addressables.js";
|
4
|
+
import { ObjectUtils, PrimitiveType } from "../../engine/engine_create_objects.js";
|
5
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
6
|
+
import { IGameObject } from "../../engine/engine_types.js";
|
7
|
+
import { getParam,PromiseAllWithErrors } from "../../engine/engine_utils.js";
|
8
|
+
import { NeedleXREventArgs, NeedleXRSession, NeedleXRUtils } from "../../engine/xr/index.js";
|
9
|
+
import { PlayerState } from "../../engine-components-experimental/networking/PlayerSync.js";
|
10
|
+
import { Behaviour, GameObject } from "../Component.js";
|
11
|
+
import { SyncedTransform } from "../SyncedTransform.js";
|
12
|
+
import { AvatarMarker } from "./WebXRAvatar.js";
|
13
|
+
import { XRFlag } from "./XRFlag.js";
|
14
|
+
|
15
|
+
const debug = getParam("debugwebxr");
|
16
|
+
|
17
|
+
const flipForwardQuaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
18
|
+
|
19
|
+
export class Avatar extends Behaviour {
|
20
|
+
|
21
|
+
@serializable(AssetReference)
|
22
|
+
head?: AssetReference;
|
23
|
+
|
24
|
+
@serializable(AssetReference)
|
25
|
+
leftHand?: AssetReference;
|
26
|
+
|
27
|
+
@serializable(AssetReference)
|
28
|
+
rightHand?: AssetReference;
|
29
|
+
|
30
|
+
private _syncTransforms?: SyncedTransform[];
|
31
|
+
|
32
|
+
async onEnterXR(_args: NeedleXREventArgs) {
|
33
|
+
if (!this.activeAndEnabled) return;
|
34
|
+
if (debug) console.warn("AVATAR ENTER XR", this.guid, this.sourceId, this, this.activeAndEnabled)
|
35
|
+
if (this._syncTransforms)
|
36
|
+
this._syncTransforms.length = 0;
|
37
|
+
await this.prepareAvatar();
|
38
|
+
|
39
|
+
const playerstate = PlayerState.getFor(this);
|
40
|
+
if (playerstate?.owner) {
|
41
|
+
const marker = this.gameObject.addNewComponent(AvatarMarker)!;
|
42
|
+
marker.avatar = this.gameObject;
|
43
|
+
marker.connectionId = playerstate.owner;
|
44
|
+
}
|
45
|
+
else if (this.context.connection.isConnected) console.error("No player state found for avatar", this);
|
46
|
+
// don't destroy the avatar when entering XR and not connected to a networking backend
|
47
|
+
else if (playerstate && !this.context.connection.isConnected) playerstate.dontDestroy = true;
|
48
|
+
}
|
49
|
+
|
50
|
+
onLeaveXR(_args: NeedleXREventArgs): void {
|
51
|
+
const marker = this.gameObject.getComponent(AvatarMarker);
|
52
|
+
if (marker) {
|
53
|
+
marker.destroy();
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
onUpdateXR(args: NeedleXREventArgs): void {
|
58
|
+
if (!this.activeAndEnabled) return;
|
59
|
+
|
60
|
+
const isLocalPlayer = PlayerState.isLocalPlayer(this);
|
61
|
+
if (!isLocalPlayer) return;
|
62
|
+
|
63
|
+
const xr = args.xr;
|
64
|
+
// make sure the avatar is inside the active rig
|
65
|
+
if (xr.rig && xr.rig.gameObject !== this.gameObject.parent) {
|
66
|
+
this.gameObject.position.set(0, 0, 0);
|
67
|
+
this.gameObject.rotation.set(0, 0, 0);
|
68
|
+
this.gameObject.scale.set(1, 1, 1);
|
69
|
+
xr.rig.gameObject.add(this.gameObject);
|
70
|
+
}
|
71
|
+
// this.gameObject.position.copy(xr.rig!.gameObject.position);
|
72
|
+
// this.gameObject.quaternion.copy(xr.rig!.gameObject.quaternion);
|
73
|
+
// this.gameObject.scale.set(1, 1, 1);
|
74
|
+
|
75
|
+
|
76
|
+
if (this._syncTransforms && isLocalPlayer) {
|
77
|
+
for (const sync of this._syncTransforms) {
|
78
|
+
sync.fastMode = true;
|
79
|
+
if (!sync.isOwned())
|
80
|
+
sync.requestOwnership();
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
|
85
|
+
// synchronize head
|
86
|
+
if (this.head && this.context.mainCamera) {
|
87
|
+
const headObj = this.head.asset as IGameObject;
|
88
|
+
headObj.position.copy(this.context.mainCamera.position);
|
89
|
+
headObj.quaternion.copy(this.context.mainCamera.quaternion);
|
90
|
+
headObj.quaternion.x *= -1;
|
91
|
+
|
92
|
+
// HACK: XRFlag limitation workaround to make sure first person user head is never rendered
|
93
|
+
if (this.context.time.frameCount % 10 === 0) {
|
94
|
+
const xrflags = GameObject.getComponentsInChildren(this.head.asset, XRFlag);
|
95
|
+
for (const flag of xrflags) {
|
96
|
+
flag.enabled = false;
|
97
|
+
flag.gameObject.visible = false;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
// synchronize hands
|
103
|
+
const leftCtrl = args.xr.leftController;
|
104
|
+
const leftObj = this.leftHand?.asset as Object3D;
|
105
|
+
if (leftCtrl && leftObj) {
|
106
|
+
leftObj.position.copy(leftCtrl.gripPosition);
|
107
|
+
leftObj.quaternion.copy(leftCtrl.gripQuaternion);
|
108
|
+
leftObj.quaternion.multiply(flipForwardQuaternion);
|
109
|
+
leftObj.visible = leftCtrl.isTracking;
|
110
|
+
}
|
111
|
+
|
112
|
+
const right = args.xr.rightController;
|
113
|
+
if (right && this.rightHand?.asset) {
|
114
|
+
const rightObj = this.rightHand.asset as Object3D;
|
115
|
+
rightObj.position.copy(right.gripPosition);
|
116
|
+
rightObj.quaternion.copy(right.gripQuaternion);
|
117
|
+
rightObj.quaternion.multiply(flipForwardQuaternion);
|
118
|
+
rightObj.visible = right.isTracking;
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
onBeforeRender(): void {
|
123
|
+
if (this.context.time.frame % 10 === 0)
|
124
|
+
this.updateRemoteAvatarVisibility();
|
125
|
+
}
|
126
|
+
|
127
|
+
|
128
|
+
private updateRemoteAvatarVisibility() {
|
129
|
+
if (this.context.connection.isConnected) {
|
130
|
+
const state = PlayerState.getFor(this);
|
131
|
+
if (state && state.isLocalPlayer == false) {
|
132
|
+
|
133
|
+
const sync = NeedleXRSession.getXRSync(this.context);
|
134
|
+
if (sync) {
|
135
|
+
if (sync.hasState(state.owner)) {
|
136
|
+
this.tryFindAvatarObjectsIfMissing();
|
137
|
+
|
138
|
+
const leftObj = this.leftHand?.asset as Object3D;
|
139
|
+
if (leftObj) {
|
140
|
+
leftObj.visible = sync?.isTracking(state.owner, "left") ?? false;
|
141
|
+
}
|
142
|
+
const rightObj = this.rightHand?.asset as Object3D;
|
143
|
+
if (rightObj) {
|
144
|
+
rightObj.visible = sync?.isTracking(state.owner, "right") ?? false;
|
145
|
+
}
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
// HACK: XRFlag limitation workaround to make sure first person user head of OTHER users is ALWAYS rendered
|
150
|
+
if (this.head?.asset) {
|
151
|
+
const xrflags = GameObject.getComponentsInChildren(this.head.asset, XRFlag);
|
152
|
+
for (const flag of xrflags) {
|
153
|
+
flag.enabled = false;
|
154
|
+
flag.gameObject.visible = true;
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
private tryFindAvatarObjectsIfMissing() {
|
164
|
+
// if no avatar objects are set, try to find them
|
165
|
+
if (!this.head || !this.leftHand || !this.rightHand) {
|
166
|
+
const res = { head: this.head, leftHand: this.leftHand, rightHand: this.rightHand };
|
167
|
+
NeedleXRUtils.tryFindAvatarObjects(this.gameObject, this.sourceId || "", res);
|
168
|
+
if (res.head) this.head = res.head;
|
169
|
+
if (res.leftHand) this.leftHand = res.leftHand;
|
170
|
+
if (res.rightHand) this.rightHand = res.rightHand;
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
private async prepareAvatar() {
|
175
|
+
// if no avatar objects are set, try to find them
|
176
|
+
this.tryFindAvatarObjectsIfMissing();
|
177
|
+
|
178
|
+
if (!this.head) {
|
179
|
+
const head = new Object3D();
|
180
|
+
head.name = "Head";
|
181
|
+
const cube = ObjectUtils.createPrimitive(PrimitiveType.Cube);
|
182
|
+
head.add(cube);
|
183
|
+
this.gameObject.add(head);
|
184
|
+
this.head = new AssetReference("", this.sourceId, head);
|
185
|
+
if (debug) console.log("Create head", head);
|
186
|
+
}
|
187
|
+
else if (this.head instanceof Object3D) {
|
188
|
+
this.head = new AssetReference("", this.sourceId, this.head);
|
189
|
+
}
|
190
|
+
|
191
|
+
if (!this.rightHand) {
|
192
|
+
const rightHand = new Object3D();
|
193
|
+
rightHand.name = "Right Hand";
|
194
|
+
this.gameObject.add(rightHand);
|
195
|
+
this.rightHand = new AssetReference("", this.sourceId, rightHand);
|
196
|
+
if (debug) console.log("Create right hand", rightHand);
|
197
|
+
}
|
198
|
+
else if (this.rightHand instanceof Object3D) {
|
199
|
+
this.rightHand = new AssetReference("", this.sourceId, this.rightHand);
|
200
|
+
}
|
201
|
+
|
202
|
+
if (!this.leftHand) {
|
203
|
+
const leftHand = new Object3D();
|
204
|
+
leftHand.name = "Left Hand";
|
205
|
+
this.gameObject.add(leftHand);
|
206
|
+
this.leftHand = new AssetReference("", this.sourceId, leftHand);
|
207
|
+
if (debug) console.log("Create left hand", leftHand);
|
208
|
+
}
|
209
|
+
else if (this.leftHand instanceof Object3D) {
|
210
|
+
this.leftHand = new AssetReference("", this.sourceId, this.leftHand);
|
211
|
+
}
|
212
|
+
|
213
|
+
await this.loadAvatarObjects(this.head, this.leftHand, this.rightHand);
|
214
|
+
|
215
|
+
if (PlayerState.isLocalPlayer(this.gameObject)) {
|
216
|
+
this._syncTransforms = GameObject.getComponentsInChildren(this.gameObject, SyncedTransform);
|
217
|
+
}
|
218
|
+
}
|
219
|
+
|
220
|
+
|
221
|
+
private async loadAvatarObjects(head: AssetReference, left: AssetReference, right: AssetReference) {
|
222
|
+
const pHead = head.loadAssetAsync();
|
223
|
+
const pHandLeft = left.loadAssetAsync();
|
224
|
+
const pHandRight = right.loadAssetAsync();
|
225
|
+
const promises = new Array<Promise<any>>();
|
226
|
+
if (pHead) promises.push(pHead);
|
227
|
+
if (pHandLeft) promises.push(pHandLeft);
|
228
|
+
if (pHandRight) promises.push(pHandRight);
|
229
|
+
const res = await PromiseAllWithErrors(promises);
|
230
|
+
if (debug) console.log("Avatar loaded results:", res);
|
231
|
+
}
|
232
|
+
}
|
@@ -0,0 +1,2 @@
|
|
1
|
+
|
2
|
+
export * from "./xr/index.js"
|
@@ -0,0 +1,5 @@
|
|
1
|
+
export * from "./NeedleXRController.js";
|
2
|
+
export * from "./NeedleXRSession.js";
|
3
|
+
export * from "./NeedleXRSync.js"
|
4
|
+
export * from "./utils.js"
|
5
|
+
export * from "./XRRig.js";
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import { Matrix4, Object3D, Quaternion, Scene, Vector3 } from 'three';
|
2
|
+
|
3
|
+
import { CreateWireCube, Gizmos } from '../engine_gizmos.js';
|
4
|
+
import { IGameObject } from '../engine_types.js';
|
5
|
+
import { getParam } from '../engine_utils.js';
|
6
|
+
import { IXRRig } from './XRRig.js';
|
7
|
+
|
8
|
+
export const flipForwardMatrix = new Matrix4().makeRotationY(Math.PI);
|
9
|
+
export const flipForwardQuaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
10
|
+
|
11
|
+
const debug = getParam("debugwebxr");
|
12
|
+
|
13
|
+
export class ImplictXRRig implements IXRRig {
|
14
|
+
|
15
|
+
priority = -100000;
|
16
|
+
gameObject: IGameObject;
|
17
|
+
|
18
|
+
isXRRig(): boolean {
|
19
|
+
return true;
|
20
|
+
}
|
21
|
+
|
22
|
+
get isActive(): boolean {
|
23
|
+
return this.gameObject.visible;
|
24
|
+
}
|
25
|
+
|
26
|
+
constructor() {
|
27
|
+
this.gameObject = new Object3D() as IGameObject;
|
28
|
+
this.gameObject.name = "Implicit XR Rig";
|
29
|
+
if (debug) {
|
30
|
+
const cube = CreateWireCube(0xff55dd);
|
31
|
+
cube.position.y += .5;
|
32
|
+
this.gameObject.add(cube);
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
@@ -0,0 +1,785 @@
|
|
1
|
+
import { fetchProfile, MotionController } from "@webxr-input-profiles/motion-controllers";
|
2
|
+
import { AxesHelper, Int8BufferAttribute, Object3D, Quaternion, Ray, Vector3 } from "three";
|
3
|
+
|
4
|
+
import { RGBAColor } from "../../engine-components/js-extensions/RGBAColor.js";
|
5
|
+
import { Context } from "../engine_context.js";
|
6
|
+
import { Gizmos } from "../engine_gizmos.js";
|
7
|
+
import { InputEventNames, InputEvents, NEPointerEvent, NEPointerEventInit, PointerType } from "../engine_input.js";
|
8
|
+
import { getTempQuaternion, getTempVector, getWorldQuaternion } from "../engine_three_utils.js";
|
9
|
+
import type { ButtonName, IGameObject, Vec3, XRControllerButtonName, XRGestureName } from "../engine_types.js";
|
10
|
+
import { getParam } from "../engine_utils.js";
|
11
|
+
import { flipForwardMatrix, flipForwardQuaternion } from "./internal.js";
|
12
|
+
import type { NeedleXRHitTestResult, NeedleXRSession } from "./NeedleXRSession.js";
|
13
|
+
|
14
|
+
const debug = getParam("debugwebxr");
|
15
|
+
/** when enabled we will not use the browser select event but instead
|
16
|
+
* we will emit the input event based on our own pinch detection
|
17
|
+
* this is a workaround for visionOS not emitting the select events, see https://linear.app/needle/issue/NE-4212
|
18
|
+
*/
|
19
|
+
const debugCustomGesture = getParam("debugcustomgesture");
|
20
|
+
|
21
|
+
/** true when selectstart was ever received */
|
22
|
+
let _didReceiveSelectStartEvent = false;
|
23
|
+
|
24
|
+
// https://github.com/immersive-web/webxr-input-profiles/blob/4484a05e30bcd43fe86bb4e06b7a707861a26796/packages/registry/profiles/meta/meta-quest-touch-plus.json
|
25
|
+
declare type ControllerAxes = "xr-standard-thumbstick";
|
26
|
+
declare type StickName = "xr-standard-thumbstick";
|
27
|
+
declare type Mapping = "xr-standard";
|
28
|
+
declare type ComponentType = "button" | "thumbstick" | "squeeze";
|
29
|
+
declare type GamepadKey = "button" | "xAxis" | "yAxis";
|
30
|
+
|
31
|
+
|
32
|
+
declare type ComponentMap = {
|
33
|
+
type: ComponentType,
|
34
|
+
rootNodeName?: string,
|
35
|
+
gamepadIndices?: { [key in GamepadKey]?: number },
|
36
|
+
visualResponses?: { [key: string]: { states: Array<string> } }
|
37
|
+
}
|
38
|
+
|
39
|
+
declare type InputDeviceLayout = {
|
40
|
+
selectComponentId: string,
|
41
|
+
components: { [key: string]: ComponentMap }
|
42
|
+
mapping: Mapping;
|
43
|
+
gamepad: Array<XRControllerButtonName>,
|
44
|
+
axes: Array<{
|
45
|
+
componentId: ControllerAxes,
|
46
|
+
axis: "x-axis" | "y-axis",
|
47
|
+
}>,
|
48
|
+
}
|
49
|
+
declare type InputDeviceProfile = {
|
50
|
+
profileId: string,
|
51
|
+
fallbackProfileIds: string[],
|
52
|
+
layouts: [
|
53
|
+
left: InputDeviceLayout,
|
54
|
+
right: InputDeviceLayout
|
55
|
+
]
|
56
|
+
}
|
57
|
+
|
58
|
+
// https://github.com/immersive-web/webxr-input-profiles/blob/4484a05e30bcd43fe86bb4e06b7a707861a26796/packages/registry/profiles/meta/meta-quest-touch-plus.json
|
59
|
+
const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles';
|
60
|
+
const DEFAULT_PROFILE = 'generic-trigger';
|
61
|
+
|
62
|
+
|
63
|
+
/**
|
64
|
+
* A NeedleXRController wraps a connected XRInputDevice that is either a physical controller or a hand
|
65
|
+
* You can access specific buttons using `getButton` and `getStick`
|
66
|
+
* To get spatial data in rig space (position, rotation) use the `gripPosition`, `gripQuaternion`, `rayPosition` and `rayQuaternion` properties
|
67
|
+
* To get spatial data in world space use the `gripWorldPosition`, `gripWorldQuaternion`, `rayWorldPosition` and `rayWorldQuaternion` properties
|
68
|
+
* Inputs will also be emitted as pointer events on `this.context.input` - so you can receive controller inputs on objects using the appropriate input events on your components (e.g. `onPointerDown`, `onPointerUp` etc) - use the `pointerType` property to check if the event is from a controller or not
|
69
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource
|
70
|
+
*/
|
71
|
+
export class NeedleXRController {
|
72
|
+
/** the Needle XR Session */
|
73
|
+
readonly xr: NeedleXRSession;
|
74
|
+
get context() { return this.xr.context; }
|
75
|
+
/**
|
76
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource
|
77
|
+
*/
|
78
|
+
readonly inputSource: XRInputSource;
|
79
|
+
/** the input source index */
|
80
|
+
readonly index: number = 0;
|
81
|
+
|
82
|
+
/** When enabled the controller will create input events in the Needle Engine input system (e.g. when a button is pressed or the controller is moved)
|
83
|
+
* You can disable this if you don't want inputs to go through the input system but be aware that this will result in `onPointerDown` component callbacks to not be invoked anymore for this XRController
|
84
|
+
*/
|
85
|
+
emitEvents = true;
|
86
|
+
|
87
|
+
/** Is the controller still connected? */
|
88
|
+
get connected() {
|
89
|
+
return this._connected;
|
90
|
+
}
|
91
|
+
private _connected: boolean = true;
|
92
|
+
|
93
|
+
get isTracking() { return this._isTracking; }
|
94
|
+
private _isTracking: boolean = false;
|
95
|
+
/** the input source gamepad giving raw access to the gamepad values
|
96
|
+
* You should usually use the `getButton` and `getStick` methods instead to get access to named buttons and sticks
|
97
|
+
*/
|
98
|
+
get gamepad() { return this.inputSource.gamepad; }
|
99
|
+
/** @returns true if this is a hand (otherwise this is a controller) */
|
100
|
+
get isHand() { return this.inputSource.hand != undefined; }
|
101
|
+
/**
|
102
|
+
* If this is a hand then this is the hand info (XRHand)
|
103
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRHand
|
104
|
+
*/
|
105
|
+
get hand() { return this.inputSource.hand; }
|
106
|
+
/** threejs XRHandSpace, shorthand for `context.renderer.xr.getHand(controllerIndex)`
|
107
|
+
* @link https://threejs.org/docs/#api/en/renderers/webxr/WebXRManager.getHand
|
108
|
+
*/
|
109
|
+
get handObject() { return this.context.renderer.xr.getHand(this.index); }
|
110
|
+
/** The input source profiles */
|
111
|
+
get profiles() { return this.inputSource.profiles; }
|
112
|
+
/** The device input layout */
|
113
|
+
get layout() { return this._layout; }
|
114
|
+
|
115
|
+
/** shorthand for `inputSource.targetRayMode` */
|
116
|
+
get targetRayMode() { return this.inputSource.targetRayMode; }
|
117
|
+
/** shorthand for `inputSource.targetRaySpace` */
|
118
|
+
get targetRaySpace() { return this.inputSource.targetRaySpace; }
|
119
|
+
/** shorthand for `inputSource.gripSpace` */
|
120
|
+
get gripSpace() { return this.inputSource.gripSpace; }
|
121
|
+
/**
|
122
|
+
* If the controller if held in the left or right hand (or if it's a left or right hand)
|
123
|
+
**/
|
124
|
+
get side() { return this.inputSource.handedness; }
|
125
|
+
/** is right side. shorthand for `side === 'right'` */
|
126
|
+
get isRight() { return this.side === 'right'; }
|
127
|
+
/** is left side. shorthand for `side === 'left'` */
|
128
|
+
get isLeft() { return this.side === 'left'; }
|
129
|
+
|
130
|
+
/** The XRTransientInputHitTestSource can be used to perform hit tests with the controller ray against the real world.
|
131
|
+
* see https://developer.mozilla.org/en-US/docs/Web/API/XRSession/requestHitTestSourceForTransientInput for more information
|
132
|
+
* Requires the hit-test feature to be enabled in the XRSession
|
133
|
+
*/
|
134
|
+
get hitTestSource() { return this._hitTestSource; }
|
135
|
+
private _hitTestSource: XRTransientInputHitTestSource | undefined = undefined;
|
136
|
+
|
137
|
+
/** Perform a hit test against the XR planes or meshes. shorthand for `xr.getHitTest(controller)`
|
138
|
+
* @returns the hit test result (with position and rotation in worldspace) or null if no hit was found
|
139
|
+
*/
|
140
|
+
getHitTest(): NeedleXRHitTestResult | null {
|
141
|
+
return this.xr.getHitTest(this);
|
142
|
+
}
|
143
|
+
|
144
|
+
/** This is cleared at the beginning of each frame */
|
145
|
+
private readonly _handJointPoses: Map<XRJointSpace, XRJointPose> = new Map();
|
146
|
+
/** Get the hand joint pose from the current XRFrame. Results are cached for a frame to avoid calling getJointPose multiple times */
|
147
|
+
getHandJointPose(joint: XRJointSpace) {
|
148
|
+
if (!this.hand || !this.xr.frame?.getJointPose || !this.xr.referenceSpace) return null;
|
149
|
+
let pose = this._handJointPoses?.get(joint);
|
150
|
+
if (pose) return pose;
|
151
|
+
pose = this.xr.frame.getJointPose(joint, this.xr.referenceSpace);
|
152
|
+
if (pose) this._handJointPoses.set(joint, pose);
|
153
|
+
return pose;
|
154
|
+
}
|
155
|
+
|
156
|
+
private readonly _gripPosition = new Vector3();
|
157
|
+
private readonly _gripQuaternion = new Quaternion();
|
158
|
+
private readonly _linearVelocity: Vector3 = new Vector3();
|
159
|
+
private readonly _rayPosition = new Vector3();
|
160
|
+
private readonly _rayQuaternion = new Quaternion();
|
161
|
+
|
162
|
+
/** Grip position in rig space */
|
163
|
+
get gripPosition() { return getTempVector(this._gripPosition).applyMatrix4(flipForwardMatrix) }
|
164
|
+
/** Grip rotation in rig space */
|
165
|
+
get gripQuaternion() { return getTempQuaternion(this._gripQuaternion).premultiply(flipForwardQuaternion) }
|
166
|
+
/** Grip linear velocity in rig space
|
167
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRPose/linearVelocity
|
168
|
+
*/
|
169
|
+
get gripLinearVelocity() {
|
170
|
+
return getTempVector(this._linearVelocity).applyQuaternion(flipForwardQuaternion);
|
171
|
+
}
|
172
|
+
/** Ray position in rig space */
|
173
|
+
get rayPosition() { return getTempVector(this._rayPosition).applyMatrix4(flipForwardMatrix) }
|
174
|
+
/** Ray rotation in rig space */
|
175
|
+
get rayQuaternion() { return getTempQuaternion(this._rayQuaternion).premultiply(flipForwardQuaternion) }
|
176
|
+
|
177
|
+
/** Controller grip position in worldspace */
|
178
|
+
get gripWorldPosition() {
|
179
|
+
return getTempVector(this._gripWorldPosition);
|
180
|
+
}
|
181
|
+
private readonly _gripWorldPosition: Vector3 = new Vector3();
|
182
|
+
|
183
|
+
/** Controller grip rotation in wordspace */
|
184
|
+
get gripWorldQuaternion() {
|
185
|
+
return getTempQuaternion(this._gripWorldQuaternion);
|
186
|
+
}
|
187
|
+
private readonly _gripWorldQuaternion: Quaternion = new Quaternion();
|
188
|
+
|
189
|
+
/** Controller ray position in worldspace (this value is calculated once per frame by default - call `updateRayWorldPosition` to force an update) */
|
190
|
+
get rayWorldPosition() {
|
191
|
+
return getTempVector(this._rayWorldPosition);
|
192
|
+
}
|
193
|
+
private readonly _rayWorldPosition: Vector3 = new Vector3();
|
194
|
+
/** Recalculates the ray world position */
|
195
|
+
updateRayWorldPosition() {
|
196
|
+
const parent = this.xr.context.mainCamera?.parent;
|
197
|
+
this._rayWorldPosition.copy(this._rayPosition);
|
198
|
+
if (parent) this._rayWorldPosition.applyMatrix4(parent.matrixWorld);
|
199
|
+
}
|
200
|
+
|
201
|
+
/** Controller ray rotation in wordspace (this value is calculated once per frame by default - call `updateRayWorldQuaternion` to force an update) */
|
202
|
+
get rayWorldQuaternion() {
|
203
|
+
return getTempQuaternion(this._rayWorldQuaternion);
|
204
|
+
}
|
205
|
+
private readonly _rayWorldQuaternion: Quaternion = new Quaternion();
|
206
|
+
/** Recalculates the ray world quaternion */
|
207
|
+
updateRayWorldQuaternion() {
|
208
|
+
const parent = this.xr.context.mainCamera?.parent;
|
209
|
+
const parentWorldQuaternion = parent ? getWorldQuaternion(parent) : undefined;
|
210
|
+
this._rayWorldQuaternion.copy(this._rayQuaternion)
|
211
|
+
// flip forward because we want +Z to be forward
|
212
|
+
.multiply(flipForwardQuaternion);
|
213
|
+
if (parentWorldQuaternion) this._rayWorldQuaternion.premultiply(parentWorldQuaternion)
|
214
|
+
}
|
215
|
+
|
216
|
+
/** The controller ray in worldspace */
|
217
|
+
get ray(): Ray {
|
218
|
+
this._ray.origin.copy(this.rayWorldPosition);
|
219
|
+
this._ray.direction.copy(getTempVector(0, 0, 1).applyQuaternion(this.rayWorldQuaternion));
|
220
|
+
return this._ray;
|
221
|
+
}
|
222
|
+
private readonly _ray;
|
223
|
+
|
224
|
+
|
225
|
+
/** The controller object space.
|
226
|
+
* You can use it to attach objects to the controller.
|
227
|
+
* Children will be automatically detached and put into the scene when the controller disconnects
|
228
|
+
*/
|
229
|
+
get object() { return this._object; }
|
230
|
+
private readonly _object: IGameObject;
|
231
|
+
|
232
|
+
private readonly _debugAxesHelper = new AxesHelper(.2);
|
233
|
+
|
234
|
+
/** returns the URL of the default controller model */
|
235
|
+
async getModelUrl(): Promise<string | null> {
|
236
|
+
return this.getMotionController?.then(res => res?.assetUrl || null);
|
237
|
+
}
|
238
|
+
|
239
|
+
constructor(session: NeedleXRSession, device: XRInputSource, index: number) {
|
240
|
+
this.xr = session;
|
241
|
+
this.inputSource = device;
|
242
|
+
this.index = index;
|
243
|
+
this._object = new Object3D() as unknown as IGameObject;
|
244
|
+
if (debug)
|
245
|
+
this._object.add(this._debugAxesHelper);
|
246
|
+
this.xr.context.scene.add(this._object);
|
247
|
+
this._ray = new Ray();
|
248
|
+
this.pointerInit = {
|
249
|
+
origin: this,
|
250
|
+
pointerType: this.hand ? "hand" : "controller",
|
251
|
+
deviceIndex: this.index,
|
252
|
+
pointerId: -1, // < this will be updated in the emitPointerEvent method
|
253
|
+
mode: this.inputSource.targetRayMode,
|
254
|
+
ray: this._ray,
|
255
|
+
device: this._object,
|
256
|
+
buttonName: "none",
|
257
|
+
}
|
258
|
+
this.initialize();
|
259
|
+
this.subscribeEvents();
|
260
|
+
|
261
|
+
// TODO: change this to check if we have hit-testing enabled instead of pass through.
|
262
|
+
if (this.xr.mode === "immersive-ar" && this.inputSource.targetRayMode === "tracked-pointer") {
|
263
|
+
// request hittest source
|
264
|
+
this.xr.session.requestHitTestSourceForTransientInput?.({
|
265
|
+
profile: this.inputSource.profiles[0],
|
266
|
+
offsetRay: new XRRay(),
|
267
|
+
})?.then(hitTestSource => {
|
268
|
+
return this._hitTestSource = hitTestSource;
|
269
|
+
});
|
270
|
+
}
|
271
|
+
}
|
272
|
+
|
273
|
+
onUpdate(frame: XRFrame) {
|
274
|
+
this.onUpdateFrame(frame);
|
275
|
+
this.updateInputEvents();
|
276
|
+
this.onUpdateMove();
|
277
|
+
}
|
278
|
+
|
279
|
+
onRenderDebug() {
|
280
|
+
Gizmos.DrawWireSphere(this.rayWorldPosition, .02);
|
281
|
+
Gizmos.DrawDirection(this.rayWorldPosition, getTempVector(0, 0, 10).applyQuaternion(this.rayWorldQuaternion));
|
282
|
+
const debugLabelPosition = this.gripWorldPosition.sub(this.object.worldForward.multiplyScalar(.1));
|
283
|
+
const profileStr = this.inputSource.profiles.join("\n");
|
284
|
+
let debugStr = `Controller[${this.index}] ${this.side}
|
285
|
+
C:${this.connected ? "yes" : "no"} T:${this.isTracking ? "yes" : "no"} Hand:${this.inputSource.hand ? "yes" : "no"}`;
|
286
|
+
if (this.inputSource.hand) debugStr += `\nPinch: ${this.getGesture("pinch")?.value.toFixed(3)}`;
|
287
|
+
debugStr += "\n" + profileStr;
|
288
|
+
Gizmos.DrawLabel(debugLabelPosition, debugStr, .01);
|
289
|
+
}
|
290
|
+
|
291
|
+
private onUpdateFrame(frame: XRFrame) {
|
292
|
+
// make sure this is cleared every frame
|
293
|
+
this._handJointPoses.clear();
|
294
|
+
|
295
|
+
if (!this.xr.referenceSpace) {
|
296
|
+
this._isTracking = false;
|
297
|
+
return;
|
298
|
+
}
|
299
|
+
|
300
|
+
const rayPose = frame.getPose(this.inputSource.targetRaySpace, this.xr.referenceSpace);
|
301
|
+
this._isTracking = rayPose != null;
|
302
|
+
|
303
|
+
if (rayPose) {
|
304
|
+
const t = rayPose.transform;
|
305
|
+
this._rayPosition.set(t.position.x, t.position.y, t.position.z);
|
306
|
+
this._rayQuaternion.set(t.orientation.x, t.orientation.y, t.orientation.z, t.orientation.w);
|
307
|
+
}
|
308
|
+
|
309
|
+
if (this.inputSource.gripSpace) {
|
310
|
+
const gripPose = frame.getPose(this.inputSource.gripSpace, this.xr.referenceSpace!);
|
311
|
+
if (gripPose) {
|
312
|
+
const t = gripPose.transform;
|
313
|
+
this._gripPosition.set(t.position.x, t.position.y, t.position.z);
|
314
|
+
this._gripQuaternion.set(t.orientation.x, t.orientation.y, t.orientation.z, t.orientation.w);
|
315
|
+
if (gripPose.linearVelocity)
|
316
|
+
this._linearVelocity.set(gripPose.linearVelocity.x, gripPose.linearVelocity.y, gripPose.linearVelocity.z);
|
317
|
+
}
|
318
|
+
}
|
319
|
+
|
320
|
+
// update controller object position
|
321
|
+
if (this.xr.context.mainCamera?.parent && this._object.parent !== this.xr.context.mainCamera?.parent)
|
322
|
+
this.xr.context.mainCamera.parent.add(this._object);
|
323
|
+
|
324
|
+
// for controllers, we set the position and rotation of the object to the ray position and rotation
|
325
|
+
// for hands, we take the wrist position and rotation
|
326
|
+
const hand = this.hand;
|
327
|
+
if (hand) {
|
328
|
+
// https://www.w3.org/TR/webxr-hand-input-1/#xrhand-interface
|
329
|
+
let gotWrist = false;
|
330
|
+
// TODO check why types are not correct here
|
331
|
+
// @ts-ignore
|
332
|
+
const wrist = hand.get("wrist");
|
333
|
+
const writePose = wrist && this.getHandJointPose(wrist);
|
334
|
+
if (writePose) {
|
335
|
+
gotWrist = true;
|
336
|
+
const p = writePose.transform.position;
|
337
|
+
const q = writePose.transform.orientation;
|
338
|
+
this._object.position.set(p.x, p.y, p.z);
|
339
|
+
this._object.quaternion.set(q.x, q.y, q.z, q.w).multiply(flipForwardQuaternion);
|
340
|
+
}
|
341
|
+
if (!gotWrist) {
|
342
|
+
this._object.position.copy(this._rayPosition);
|
343
|
+
this._object.quaternion.copy(this._rayQuaternion).multiply(flipForwardQuaternion);
|
344
|
+
}
|
345
|
+
|
346
|
+
//@ts-ignore
|
347
|
+
const middle = hand.get("middle-finger-metacarpal");
|
348
|
+
const middlePose = middle && this.getHandJointPose(middle);
|
349
|
+
if (middlePose) {
|
350
|
+
const p = middlePose.transform.position;
|
351
|
+
const q = middlePose.transform.orientation;
|
352
|
+
// for some reason the grip rotation is different from the wrist rotation
|
353
|
+
// but we want to use the wrist rotation for the grip
|
354
|
+
this._gripPosition.set(p.x, p.y, p.z);
|
355
|
+
this._gripQuaternion.set(q.x, q.y, q.z, q.w);
|
356
|
+
}
|
357
|
+
}
|
358
|
+
else {
|
359
|
+
this._object.position.copy(this._rayPosition);
|
360
|
+
this._object.quaternion.copy(this._rayQuaternion).multiply(flipForwardQuaternion);
|
361
|
+
}
|
362
|
+
|
363
|
+
|
364
|
+
// UPDATE WORLD TRANSFORM DATA
|
365
|
+
const parent = this.xr.context.mainCamera?.parent;
|
366
|
+
const parentWorldQuaternion = parent ? getWorldQuaternion(parent) : undefined;
|
367
|
+
|
368
|
+
// GRIP
|
369
|
+
this._gripWorldPosition.copy(this._gripPosition);
|
370
|
+
if (parent) this._gripWorldPosition.applyMatrix4(parent.matrixWorld);
|
371
|
+
this._gripWorldQuaternion.copy(this._gripQuaternion);
|
372
|
+
// flip forward because we want +Z to be forward
|
373
|
+
this._gripWorldQuaternion.multiply(flipForwardQuaternion);
|
374
|
+
if (parentWorldQuaternion) this._gripWorldQuaternion.premultiply(parentWorldQuaternion)
|
375
|
+
|
376
|
+
// RAY
|
377
|
+
this.updateRayWorldPosition();
|
378
|
+
this.updateRayWorldQuaternion();
|
379
|
+
}
|
380
|
+
|
381
|
+
/** Called when the input source disconnects */
|
382
|
+
onDisconnected() {
|
383
|
+
this._connected = false;
|
384
|
+
if (debug) console.warn("Controller disconnected", this.index);
|
385
|
+
// move all attached objects into the scene
|
386
|
+
for (const child of this._object.children) {
|
387
|
+
this.xr.context.scene.attach(child);
|
388
|
+
}
|
389
|
+
this._object.removeFromParent();
|
390
|
+
this._debugAxesHelper.removeFromParent();
|
391
|
+
this.unsubscribeEvents();
|
392
|
+
}
|
393
|
+
|
394
|
+
/**
|
395
|
+
* Get a gamepad button
|
396
|
+
* @link https://github.com/immersive-web/webxr-gamepads-module/blob/main/gamepads-module-explainer.md
|
397
|
+
* @param key the controller button name e.g. x-button
|
398
|
+
* @returns the gamepad button if it exists on the controller - otherwise undefined
|
399
|
+
*/
|
400
|
+
getButton(key: ButtonName | "primary-button" | "primary"): NeedleGamepadButton | undefined {
|
401
|
+
if (!this._layout) return undefined;
|
402
|
+
|
403
|
+
switch (key) {
|
404
|
+
case "primary-button":
|
405
|
+
if (this.isLeft) key = "x-button";
|
406
|
+
else if (this.isRight) key = "a-button";
|
407
|
+
else return undefined;
|
408
|
+
break;
|
409
|
+
case "primary":
|
410
|
+
return this.toNeedleGamepadButton(0);
|
411
|
+
}
|
412
|
+
|
413
|
+
|
414
|
+
if (this._buttonMap.has(key)) {
|
415
|
+
return this.toNeedleGamepadButton(this._buttonMap.get(key)!);
|
416
|
+
}
|
417
|
+
const componentModel = this._layout?.components[key];
|
418
|
+
if (componentModel?.gamepadIndices) {
|
419
|
+
switch (componentModel.type) {
|
420
|
+
case "button":
|
421
|
+
case "squeeze":
|
422
|
+
if (this.inputSource.gamepad) {
|
423
|
+
const index = componentModel.gamepadIndices!.button!;
|
424
|
+
this._buttonMap.set(key, index);
|
425
|
+
return this.toNeedleGamepadButton(index);
|
426
|
+
}
|
427
|
+
break;
|
428
|
+
default:
|
429
|
+
console.warn("Unsupported component type", componentModel.type);
|
430
|
+
break;
|
431
|
+
}
|
432
|
+
}
|
433
|
+
this._buttonMap.set(key, undefined!);
|
434
|
+
return undefined;
|
435
|
+
}
|
436
|
+
|
437
|
+
/** Get a gesture state */
|
438
|
+
getGesture(key: XRGestureName): NeedleGamepadButton | null {
|
439
|
+
const state = this.states[key];
|
440
|
+
if (!state) return null;
|
441
|
+
this.states[key] = state;
|
442
|
+
const needleButton = this._needleGamepadButtons[key] || new NeedleGamepadButton();
|
443
|
+
needleButton.pressed = state.pressed;
|
444
|
+
needleButton.value = state.value;
|
445
|
+
needleButton.isDown = state.isDown;
|
446
|
+
needleButton.isUp = state.isUp;
|
447
|
+
this._needleGamepadButtons[key] = needleButton;
|
448
|
+
return needleButton;
|
449
|
+
}
|
450
|
+
|
451
|
+
private readonly _needleGamepadButtons: { [key: number | string]: NeedleGamepadButton } = {};
|
452
|
+
/** combine the InputState information + the GamepadButton information (since GamepadButtons can not be extended) */
|
453
|
+
private toNeedleGamepadButton(index: number): NeedleGamepadButton {
|
454
|
+
const button = this.inputSource.gamepad?.buttons[index];
|
455
|
+
const state = this.states[index];
|
456
|
+
const needleButton = this._needleGamepadButtons[index] || new NeedleGamepadButton();
|
457
|
+
if (button) {
|
458
|
+
needleButton.pressed = button.pressed;
|
459
|
+
needleButton.value = button.value;
|
460
|
+
needleButton.touched = button.touched;
|
461
|
+
}
|
462
|
+
if (state) {
|
463
|
+
needleButton.isDown = state.isDown;
|
464
|
+
needleButton.isUp = state.isUp;
|
465
|
+
}
|
466
|
+
this._needleGamepadButtons[index] = needleButton;
|
467
|
+
return needleButton;
|
468
|
+
}
|
469
|
+
|
470
|
+
/**
|
471
|
+
* Get the values of a controller joystick
|
472
|
+
* @link https://github.com/immersive-web/webxr-gamepads-module/blob/main/gamepads-module-explainer.md
|
473
|
+
* @returns the stick values where x is left/right, y is up/down and z is the button value
|
474
|
+
*/
|
475
|
+
getStick(key: StickName | "primary"): Vec3 {
|
476
|
+
if (!this._layout) return { x: 0, y: 0, z: 0 };
|
477
|
+
|
478
|
+
if (key === "primary") {
|
479
|
+
const x = this.inputSource.gamepad?.axes[0] || 0;
|
480
|
+
const y = this.inputSource.gamepad?.axes[1] || 0;
|
481
|
+
// the primary thumbstick is button 3 (see gamepads module explainer)
|
482
|
+
const z = this.inputSource.gamepad?.buttons[3].value || 0;
|
483
|
+
return { x, y, z }
|
484
|
+
}
|
485
|
+
|
486
|
+
const componentModel = this._layout?.components[key];
|
487
|
+
if (componentModel?.gamepadIndices) {
|
488
|
+
switch (componentModel.type) {
|
489
|
+
case "thumbstick":
|
490
|
+
if (this.inputSource.gamepad) {
|
491
|
+
const xIndex = componentModel.gamepadIndices!.xAxis!;
|
492
|
+
const yIndex = componentModel.gamepadIndices!.yAxis!;
|
493
|
+
let x = this.inputSource.gamepad?.axes[xIndex];
|
494
|
+
let y = this.inputSource.gamepad?.axes[yIndex];
|
495
|
+
x *= -1;
|
496
|
+
y *= -1;
|
497
|
+
const buttonIndex = componentModel.gamepadIndices!.button!;
|
498
|
+
const z = this.inputSource.gamepad?.buttons[buttonIndex].value;
|
499
|
+
return { x, y, z }
|
500
|
+
}
|
501
|
+
}
|
502
|
+
}
|
503
|
+
return { x: 0, y: 0, z: 0 }
|
504
|
+
}
|
505
|
+
|
506
|
+
private readonly _buttonMap = new Map<ButtonName, number>();
|
507
|
+
|
508
|
+
// the motion controller contains the controller scheme, we use this to simplify button access
|
509
|
+
private _motioncontroller?: MotionController;
|
510
|
+
private _layout: InputDeviceLayout | undefined;
|
511
|
+
private getMotionController!: Promise<MotionController>;
|
512
|
+
private initialize() {
|
513
|
+
if (!this._layout) {
|
514
|
+
// TODO: we should fetch the profiles or better yet the profile list once and cache it
|
515
|
+
const fetchProfileCall = fetchProfile(this.inputSource, DEFAULT_PROFILES_PATH, DEFAULT_PROFILE);
|
516
|
+
/** @ts-ignore */
|
517
|
+
this.getMotionController = fetchProfileCall.then(res => {
|
518
|
+
|
519
|
+
if (!this.connected) return null;
|
520
|
+
|
521
|
+
this._motioncontroller = new MotionController(
|
522
|
+
this.inputSource,
|
523
|
+
res.profile,
|
524
|
+
res.assetPath || ""
|
525
|
+
);
|
526
|
+
|
527
|
+
const profile = res.profile as InputDeviceProfile;
|
528
|
+
const layout = profile.layouts[this.inputSource.handedness];
|
529
|
+
this._layout = layout;
|
530
|
+
if (this._layout) {
|
531
|
+
if (!this._layout.gamepad?.length) {
|
532
|
+
this._layout.gamepad = [];
|
533
|
+
for (const key in this._layout.components) {
|
534
|
+
const component = this._layout.components[key];
|
535
|
+
this._layout.gamepad[component.gamepadIndices!.button!] = key as XRControllerButtonName;
|
536
|
+
}
|
537
|
+
}
|
538
|
+
}
|
539
|
+
// if (debug) console.log(this._layout, this.inputSource);
|
540
|
+
// debugger;
|
541
|
+
// this.getButton("a-button")
|
542
|
+
return this._motioncontroller;
|
543
|
+
}).catch(err => {
|
544
|
+
console.error(err);
|
545
|
+
});
|
546
|
+
}
|
547
|
+
}
|
548
|
+
|
549
|
+
private subscribeEvents() {
|
550
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/XRSession/selectstart_event
|
551
|
+
this.xr.session.addEventListener("selectstart", this.onSelectStart);
|
552
|
+
this.xr.session.addEventListener("selectend", this.onSelectEnd);
|
553
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/XRSession/squeeze_event
|
554
|
+
this.xr.session.addEventListener("squeezestart", this.onSequeezeStart);
|
555
|
+
this.xr.session.addEventListener("squeezeend", this.onSequeezeEnd);
|
556
|
+
}
|
557
|
+
private unsubscribeEvents() {
|
558
|
+
this.xr.session.removeEventListener("selectstart", this.onSelectStart);
|
559
|
+
this.xr.session.removeEventListener("selectend", this.onSelectEnd);
|
560
|
+
this.xr.session.removeEventListener("squeezestart", this.onSequeezeStart);
|
561
|
+
this.xr.session.removeEventListener("squeezeend", this.onSequeezeEnd);
|
562
|
+
}
|
563
|
+
|
564
|
+
private _selectButtonIndex: number | undefined = undefined;
|
565
|
+
private _squeezeButtonIndex: number | undefined = undefined;
|
566
|
+
|
567
|
+
private onSelectStart = (evt: XRInputSourceEvent) => {
|
568
|
+
if (this.inputSource !== evt.inputSource) return;
|
569
|
+
const selectComponentId = this._layout?.selectComponentId;
|
570
|
+
const i = this._layout?.components[selectComponentId!]?.gamepadIndices?.button;
|
571
|
+
if (i !== undefined) this._selectButtonIndex = i;
|
572
|
+
if (debugCustomGesture) return;
|
573
|
+
if (!_didReceiveSelectStartEvent) {
|
574
|
+
_didReceiveSelectStartEvent = true;
|
575
|
+
// safeguard first pinch event - check if the pinch gesture is already down
|
576
|
+
const pinch = this.getGesture("pinch");
|
577
|
+
if (pinch?.pressed) {
|
578
|
+
console.warn("Select start event was received but the pinch gesture is already down. This might happen the first time you start pinching", this.index, this.side);
|
579
|
+
return;
|
580
|
+
}
|
581
|
+
}
|
582
|
+
if (debug) Gizmos.DrawDirection(this.rayWorldPosition, getTempVector(0, .01, 1).applyQuaternion(this.rayWorldQuaternion), 0xff0000, 10);
|
583
|
+
this.emitPointerEvent(InputEvents.PointerDown, this._selectButtonIndex || 0, "xr-standard-trigger", true, evt);
|
584
|
+
}
|
585
|
+
private onSelectEnd = (evt: XRInputSourceEvent) => {
|
586
|
+
if (debugCustomGesture) return;
|
587
|
+
if (this.inputSource !== evt.inputSource) return;
|
588
|
+
this.emitPointerEvent(InputEvents.PointerUp, this._selectButtonIndex || 0, "xr-standard-trigger", true, evt);
|
589
|
+
}
|
590
|
+
private onSequeezeStart = (evt: XRInputSourceEvent) => {
|
591
|
+
if (this.inputSource !== evt.inputSource) return;
|
592
|
+
this._squeezeButtonIndex = this._layout?.components["xr-standard-squeeze"]?.gamepadIndices?.button;
|
593
|
+
if (this._squeezeButtonIndex !== undefined) {
|
594
|
+
if (debug) Gizmos.DrawDirection(this.rayWorldPosition, getTempVector(0, .01, 1).applyQuaternion(this.rayWorldQuaternion), 0x0000ff, 10);
|
595
|
+
this.emitPointerEvent(InputEvents.PointerDown, this._squeezeButtonIndex || 0, "xr-standard-squeeze", true, evt);
|
596
|
+
}
|
597
|
+
};
|
598
|
+
private onSequeezeEnd = (evt: XRInputSourceEvent) => {
|
599
|
+
if (this.inputSource !== evt.inputSource) return;
|
600
|
+
if (this._squeezeButtonIndex !== undefined)
|
601
|
+
this.emitPointerEvent(InputEvents.PointerUp, this._squeezeButtonIndex || 0, "xr-standard-squeeze", true, evt);
|
602
|
+
};
|
603
|
+
|
604
|
+
/** Index = button index */
|
605
|
+
private readonly states: { [key: number | string]: InputState } = {};
|
606
|
+
// If we want to invoke button events for ALL buttons we need to keep track of the previous state
|
607
|
+
// instead of using XR input select start events which is only raised for the primary button
|
608
|
+
// we should probably do both but then we need to ignore the primary index in the following function (to not raise an event for the same button twice)
|
609
|
+
// and start with index = 1
|
610
|
+
private updateInputEvents() {
|
611
|
+
// https://immersive-web.github.io/webxr-gamepads-module/#xr-standard-heading
|
612
|
+
if (this.gamepad?.buttons) {
|
613
|
+
for (let k = 0; k < this.gamepad.buttons.length; k++) {
|
614
|
+
const button = this.gamepad.buttons[k];
|
615
|
+
const state = this.states[k] || new InputState();
|
616
|
+
let eventName: InputEventNames | null = null;
|
617
|
+
|
618
|
+
// is down
|
619
|
+
if (button.pressed && !state.pressed) {
|
620
|
+
eventName = "pointerdown";
|
621
|
+
state.isDown = true;
|
622
|
+
state.isUp = false;
|
623
|
+
}
|
624
|
+
// is up
|
625
|
+
else if (!button.pressed && state.pressed) {
|
626
|
+
eventName = "pointerup"
|
627
|
+
state.isDown = false;
|
628
|
+
state.isUp = true;
|
629
|
+
}
|
630
|
+
else {
|
631
|
+
state.isDown = false;
|
632
|
+
state.isUp = false;
|
633
|
+
}
|
634
|
+
|
635
|
+
state.value = button.value;
|
636
|
+
state.pressed = button.pressed;
|
637
|
+
this.states[k] = state;
|
638
|
+
|
639
|
+
// the selection event is handled in the "selectstart" callback
|
640
|
+
const emitEvent = k !== this._selectButtonIndex && k !== this._squeezeButtonIndex;
|
641
|
+
|
642
|
+
if (eventName != null && emitEvent) {
|
643
|
+
const name = this._layout?.gamepad[k];
|
644
|
+
if (debug || debugCustomGesture) console.log("Emitting pointer event", eventName, k, name, button.value, this.gamepad, this._layout);
|
645
|
+
this.emitPointerEvent(eventName, k, name ?? "none", false, null, button.value);
|
646
|
+
}
|
647
|
+
}
|
648
|
+
}
|
649
|
+
|
650
|
+
// update hand gesture states
|
651
|
+
if (this.hand) {
|
652
|
+
const handObject = this.handObject;
|
653
|
+
if (handObject) {
|
654
|
+
// update pinch state
|
655
|
+
const indexTip = handObject.joints["index-finger-tip"];
|
656
|
+
const thumbTip = handObject.joints["thumb-tip"];
|
657
|
+
if (indexTip && thumbTip) {
|
658
|
+
const pinchThreshold = .02;
|
659
|
+
const pinchHysteresis = .01;
|
660
|
+
const distance = indexTip.position.distanceTo(thumbTip.position);
|
661
|
+
const state = this.states["pinch"] || new InputState();
|
662
|
+
state.value = distance;
|
663
|
+
|
664
|
+
const isPressed = distance < (pinchThreshold - pinchHysteresis);
|
665
|
+
const isReleased = distance > (pinchThreshold + pinchHysteresis);
|
666
|
+
if (isPressed && !state.pressed) {
|
667
|
+
if (debugCustomGesture) console.log("pinch start", distance);
|
668
|
+
state.isDown = true;
|
669
|
+
state.isUp = false;
|
670
|
+
state.pressed = true;
|
671
|
+
}
|
672
|
+
else if (isReleased && state.pressed) {
|
673
|
+
state.isDown = false;
|
674
|
+
state.isUp = true;
|
675
|
+
state.pressed = false;
|
676
|
+
}
|
677
|
+
else {
|
678
|
+
state.isDown = false;
|
679
|
+
state.isUp = false;
|
680
|
+
}
|
681
|
+
this.states["pinch"] = state;
|
682
|
+
|
683
|
+
/** workaround for visionOS not emitting selectstart. See https://linear.app/needle/issue/NE-4212
|
684
|
+
* If a select start event was never received we do a manual check here if the user is pinching
|
685
|
+
*/
|
686
|
+
if (!_didReceiveSelectStartEvent && (state.isDown || state.isUp)) {
|
687
|
+
const eventName = isPressed ? "pointerdown" : "pointerup";
|
688
|
+
const pressure = distance / pinchThreshold;
|
689
|
+
if (debugCustomGesture) {
|
690
|
+
const p = this.rayWorldPosition.add(this.object.worldForward.multiplyScalar(.2));
|
691
|
+
p.y += .05;
|
692
|
+
p.y += Math.random() * .02;
|
693
|
+
Gizmos.DrawLabel(p, "pinch:" + eventName + ", " + this.index + ", " + this.side + "\n" + handObject.uuid, 0.01, 5, 0x000000, new RGBAColor(1, 1, 1, .1));
|
694
|
+
}
|
695
|
+
this.emitPointerEvent(eventName, 0, "pinch", false, null, pressure);
|
696
|
+
}
|
697
|
+
}
|
698
|
+
}
|
699
|
+
}
|
700
|
+
}
|
701
|
+
|
702
|
+
|
703
|
+
private _didMoveLastFrame = false;
|
704
|
+
private readonly _lastPointerMovePosition = new Vector3();
|
705
|
+
private readonly _lastPointerMoveQuaternion = new Quaternion();
|
706
|
+
|
707
|
+
private onUpdateMove() {
|
708
|
+
let didMove = false;
|
709
|
+
const dist = this._lastPointerMovePosition.distanceTo(this.gripWorldPosition);
|
710
|
+
if (dist > .02) didMove = true;
|
711
|
+
if (!didMove) {
|
712
|
+
const angle = this._lastPointerMoveQuaternion.angleTo(this.gripWorldQuaternion);
|
713
|
+
if (angle > .02) didMove = true;
|
714
|
+
}
|
715
|
+
|
716
|
+
if (didMove) {
|
717
|
+
this._didMoveLastFrame = true;
|
718
|
+
this._lastPointerMovePosition.copy(this.gripWorldPosition);
|
719
|
+
this._lastPointerMoveQuaternion.copy(this.gripWorldQuaternion);
|
720
|
+
if (debug) Gizmos.DrawLabel(this.rayWorldPosition.add(this.object.worldForward.multiplyScalar(.1)), "move", .01);
|
721
|
+
|
722
|
+
let button = this.xr.context.input.getFirstPressedButtonForPointer(this.index);
|
723
|
+
if (button === undefined) button = 0;
|
724
|
+
const pressure = this.gamepad?.buttons[button]?.value;
|
725
|
+
this.emitPointerEvent("pointermove", button, "none", false, null, pressure);
|
726
|
+
}
|
727
|
+
else {
|
728
|
+
this._didMoveLastFrame = false;
|
729
|
+
}
|
730
|
+
}
|
731
|
+
|
732
|
+
|
733
|
+
/** cached spatial pointer init object. We re-use it to not have */
|
734
|
+
private readonly pointerInit: NEPointerEventInit;
|
735
|
+
private emitPointerEvent(type: InputEventNames, button: number, buttonName: ButtonName | "none", primary: boolean, source: Event | null = null, pressure?: number) {
|
736
|
+
|
737
|
+
if (!this.emitEvents) {
|
738
|
+
if (debug && type !== InputEvents.PointerMove) console.warn("Pointer events are disabled for this controller", this.index, type, button);
|
739
|
+
return;
|
740
|
+
}
|
741
|
+
|
742
|
+
// Currently we do only want to emit pointer events for NON screen based events
|
743
|
+
// that means if the input device is spatial (AR touch on a screen should be handled via touchdown etc still)
|
744
|
+
// Not sure if *this* is enough to determine if the event is spatial or not
|
745
|
+
if (this.xr.mode === "immersive-vr" || this.xr.isPassThrough) {
|
746
|
+
this.pointerInit.origin = this;
|
747
|
+
this.pointerInit.pointerId = this.index * 10 + button;
|
748
|
+
this.pointerInit.pointerType = this.hand ? "hand" : "controller";
|
749
|
+
this.pointerInit.button = button;
|
750
|
+
this.pointerInit.buttonName = buttonName;
|
751
|
+
this.pointerInit.isPrimary = primary;
|
752
|
+
this.pointerInit.mode = this.inputSource.targetRayMode;
|
753
|
+
this.pointerInit.ray = this.ray;
|
754
|
+
this.pointerInit.device = this.object;
|
755
|
+
this.pointerInit.pressure = pressure;
|
756
|
+
|
757
|
+
const prevContext = Context.Current;
|
758
|
+
Context.Current = this.xr.context;
|
759
|
+
if (debug && type !== "pointermove") console.warn("Pointer event", type, button, buttonName, { ...this.pointerInit });
|
760
|
+
this.xr.context.input.createInputEvent(new NEPointerEvent(type, source, this.pointerInit));
|
761
|
+
Context.Current = prevContext;
|
762
|
+
}
|
763
|
+
}
|
764
|
+
}
|
765
|
+
|
766
|
+
class InputState {
|
767
|
+
/** if the button was pressed the last update */
|
768
|
+
isDown: boolean = false;
|
769
|
+
/** if the button was released the last update */
|
770
|
+
isUp: boolean = false;
|
771
|
+
|
772
|
+
pressed: boolean = false;
|
773
|
+
value: number = 0;
|
774
|
+
};
|
775
|
+
|
776
|
+
/** Enhanced GamepadButton with `isDown` and `isUp` information */
|
777
|
+
class NeedleGamepadButton {
|
778
|
+
touched: boolean = false;
|
779
|
+
pressed: boolean = false;
|
780
|
+
value: number = 0;
|
781
|
+
/** was the button just pressed down the last update */
|
782
|
+
isDown: boolean = false;
|
783
|
+
/** was the button just released the last update */
|
784
|
+
isUp: boolean = false;
|
785
|
+
}
|
@@ -0,0 +1,1290 @@
|
|
1
|
+
import { Camera, Object3D, PerspectiveCamera, Quaternion, Vector3 } from "three";
|
2
|
+
|
3
|
+
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../debug/index.js";
|
4
|
+
import { Context, FrameEvent } from "../engine_context.js";
|
5
|
+
import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
|
6
|
+
import { isDestroyed } from "../engine_gameobject.js";
|
7
|
+
import { Gizmos } from "../engine_gizmos.js";
|
8
|
+
import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
|
9
|
+
import { getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
|
10
|
+
import { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
|
11
|
+
import { delay, getParam, isDesktop, isQuest } from "../engine_utils.js";
|
12
|
+
import { flipForwardMatrix, flipForwardQuaternion, ImplictXRRig } from "./internal.js";
|
13
|
+
import { NeedleXRController } from "./NeedleXRController.js";
|
14
|
+
import { NeedleXRSync } from "./NeedleXRSync.js";
|
15
|
+
import { SceneTransition } from "./SceneTransition.js";
|
16
|
+
import { TemporaryXRContext } from "./TempXRContext.js";
|
17
|
+
import type { IXRRig } from "./XRRig.js";
|
18
|
+
|
19
|
+
|
20
|
+
/** @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame/fillPoses */
|
21
|
+
declare type FillPosesFunction = (spaces: IterableIterator<XRJointSpace>, referenceSpace: XRSpace, targetArray: Float32Array) => void;
|
22
|
+
declare type NeedleXRFrame = XRFrame & { fillPoses?: FillPosesFunction };
|
23
|
+
|
24
|
+
/** NeedleXRSession event argument.
|
25
|
+
* Use `args.xr` to access the NeedleXRSession */
|
26
|
+
export type NeedleXREventArgs = { readonly xr: NeedleXRSession }
|
27
|
+
export type SessionChangedEvt = (args: NeedleXREventArgs) => void;
|
28
|
+
export type SessionRequestedEvent = (args: { readonly mode: XRSessionMode, readonly init: XRSessionInit }) => void;
|
29
|
+
export type SessionRequestedEndEvent = (args: { readonly mode: XRSessionMode, readonly init: XRSessionInit, newSession: XRSession | null }) => void;
|
30
|
+
|
31
|
+
/** Result of a XR hit-test
|
32
|
+
* @property {XRHitTestResult} hit The original XRHitTestResult
|
33
|
+
* @property {Vector3} position The hit position in world space
|
34
|
+
* @property {Quaternion} quaternion The hit rotation in world space
|
35
|
+
*/
|
36
|
+
export type NeedleXRHitTestResult = { readonly hit: XRHitTestResult, readonly position: Vector3, readonly quaternion: Quaternion };
|
37
|
+
|
38
|
+
const debug = getParam("debugwebxr");
|
39
|
+
const debugFPS = getParam("stats");
|
40
|
+
|
41
|
+
// TODO: move this into the IComponent interface!?
|
42
|
+
export interface INeedleXRSessionEventReceiver extends Pick<IComponent, "destroyed"> {
|
43
|
+
get activeAndEnabled(): boolean;
|
44
|
+
supportsXR?(mode: XRSessionMode): boolean;
|
45
|
+
/** Called before requesting a XR session */
|
46
|
+
onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void;
|
47
|
+
onEnterXR?(args: NeedleXREventArgs): void;
|
48
|
+
onUpdateXR?(args: NeedleXREventArgs): void;
|
49
|
+
onLeaveXR?(args: NeedleXREventArgs): void;
|
50
|
+
onXRControllerAdded?(args: NeedleXRControllerEventArgs): void;
|
51
|
+
onXRControllerRemoved?(args: NeedleXRControllerEventArgs): void;
|
52
|
+
}
|
53
|
+
|
54
|
+
/** Contains a reference to the currently active webxr session and the controller that has changed */
|
55
|
+
export type NeedleXRControllerEventArgs = NeedleXREventArgs & { controller: NeedleXRController, change: "added" | "removed" }
|
56
|
+
/** Event Arguments when a controller changed event is invoked (added or removed)
|
57
|
+
* Access the controller via `args.controller`, the `args.change` property indicates if the controller was added or removed
|
58
|
+
*/
|
59
|
+
export type ControllerChangedEvt = (args: NeedleXRControllerEventArgs) => void;
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
function getDOMOverlayElement(domElement: HTMLElement) {
|
64
|
+
let arOverlayElement: HTMLElement | null = null;
|
65
|
+
// for react cases we dont have an Engine Element
|
66
|
+
const element: any = domElement;
|
67
|
+
if (element.getAROverlayContainer)
|
68
|
+
arOverlayElement = element.getAROverlayContainer();
|
69
|
+
else arOverlayElement = domElement;
|
70
|
+
return arOverlayElement;
|
71
|
+
}
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
registerSessionGranted();
|
76
|
+
function registerSessionGranted() {
|
77
|
+
if ('xr' in navigator) {
|
78
|
+
// WebXRViewer (based on Firefox) has a bug where addEventListener
|
79
|
+
// throws a silent exception and aborts execution entirely.
|
80
|
+
if (/WebXRViewer\//i.test(navigator.userAgent)) {
|
81
|
+
console.warn('WebXRViewer does not support addEventListener');
|
82
|
+
return;
|
83
|
+
}
|
84
|
+
|
85
|
+
navigator.xr?.addEventListener('sessiongranted', () => {
|
86
|
+
console.log("Received Session Granted...")
|
87
|
+
const lastSessionMode = sessionStorage.getItem("needle_xr_session_mode");
|
88
|
+
const lastSessionInit = sessionStorage.getItem("needle_xr_session_init");
|
89
|
+
if (lastSessionMode && lastSessionInit) {
|
90
|
+
console.log("Session Granted: Restore last session")
|
91
|
+
const init = JSON.parse(lastSessionInit);
|
92
|
+
NeedleXRSession.start(lastSessionMode as XRSessionMode, init).catch(e => console.warn(e));
|
93
|
+
}
|
94
|
+
else {
|
95
|
+
// if no session was found we start VR by default
|
96
|
+
NeedleXRSession.start("immersive-vr").catch(e => console.warn("Session Granted failed:", e));
|
97
|
+
}
|
98
|
+
});
|
99
|
+
}
|
100
|
+
}
|
101
|
+
function saveSessionInfo(mode: XRSessionMode, init: XRSessionInit) {
|
102
|
+
sessionStorage.setItem("needle_xr_session_mode", mode);
|
103
|
+
sessionStorage.setItem("needle_xr_session_init", JSON.stringify(init));
|
104
|
+
}
|
105
|
+
|
106
|
+
function deleteSessionInfo() {
|
107
|
+
sessionStorage.removeItem("needle_xr_session_mode");
|
108
|
+
sessionStorage.removeItem("needle_xr_session_init");
|
109
|
+
}
|
110
|
+
|
111
|
+
if (isDesktop() && isDevEnvironment()) {
|
112
|
+
window.addEventListener("keydown", (evt) => {
|
113
|
+
if (evt.key === "x") {
|
114
|
+
if (NeedleXRSession.active) {
|
115
|
+
NeedleXRSession.stop();
|
116
|
+
}
|
117
|
+
}
|
118
|
+
});
|
119
|
+
}
|
120
|
+
|
121
|
+
if (getParam("simulatewebxrloading")) {
|
122
|
+
ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, async _cb => {
|
123
|
+
await delay(3000);
|
124
|
+
setTimeout(async () => {
|
125
|
+
const info = await TemporaryXRContext.handoff();
|
126
|
+
if (info) NeedleXRSession.setSession(info.mode, info.session, info.init, Context.Current);
|
127
|
+
else
|
128
|
+
NeedleXRSession.start("immersive-vr")
|
129
|
+
}, 6000)
|
130
|
+
});
|
131
|
+
let triggered = false;
|
132
|
+
window.addEventListener("click", () => {
|
133
|
+
if (triggered) return;
|
134
|
+
triggered = true;
|
135
|
+
TemporaryXRContext.start("immersive-vr", NeedleXRSession.getDefaultSessionInit("immersive-vr"));
|
136
|
+
});
|
137
|
+
}
|
138
|
+
|
139
|
+
/**
|
140
|
+
* This class manages an XRSession to provide helper methods and events. It provides easy access to the XRInputSources (controllers and hands)
|
141
|
+
* - Start a XRSession with `NeedleXRSession.start(...)`
|
142
|
+
* - Stop a XRSession with `NeedleXRSession.stop()`
|
143
|
+
* - Access a running XRSession with `NeedleXRSession.active`
|
144
|
+
*
|
145
|
+
* If a XRSession is active you can use all XR-related event methods on your components to receive XR events e.g. `onEnterXR`, `onUpdateXR`, `onLeaveXR`
|
146
|
+
* ```ts
|
147
|
+
* export class MyComponent extends Behaviour {
|
148
|
+
* // callback invoked whenever the XRSession is started or your component is added to a scene with an active XRSession
|
149
|
+
* onEnterXR(args: NeedleXREventArgs) {
|
150
|
+
* console.log("Entered XR");
|
151
|
+
* // access the NeedleXRSession via args.xr
|
152
|
+
* }
|
153
|
+
* // callback invoked whenever a controller is added (or you switch from controller to hand tracking)
|
154
|
+
* onControllerAdded(args: NeedleXRControllerEventArgs) { }
|
155
|
+
* }
|
156
|
+
* ```
|
157
|
+
*
|
158
|
+
* ### XRRig
|
159
|
+
* The XRRig can be accessed via the `rig` property
|
160
|
+
* Set a custom XRRig via `NeedleXRSession.addRig(...)` or `NeedleXRSession.removeRig(...)`
|
161
|
+
* By default the active XRRig with the highest priority in the scene is used
|
162
|
+
*/
|
163
|
+
export class NeedleXRSession implements INeedleXRSession {
|
164
|
+
|
165
|
+
private static _sync: NeedleXRSync | null = null;
|
166
|
+
static getXRSync(context: Context) {
|
167
|
+
if (!this._sync) this._sync = new NeedleXRSync(context);
|
168
|
+
return this._sync;
|
169
|
+
}
|
170
|
+
|
171
|
+
static get currentSessionRequest(): XRSessionMode | null { return this._currentSessionRequestMode; }
|
172
|
+
private static _currentSessionRequestMode: XRSessionMode | null = null;
|
173
|
+
|
174
|
+
static get active(): NeedleXRSession | null { return this._activeSession; }
|
175
|
+
/** The active xr session mode (if any xr session is active) */
|
176
|
+
static get activeMode() { return this._activeSession?.mode ?? null; }
|
177
|
+
/** XRSystem via navigator.xr access
|
178
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSystem
|
179
|
+
*/
|
180
|
+
static get xrSystem(): XRSystem | undefined {
|
181
|
+
return ('xr' in navigator) ? navigator.xr : undefined;
|
182
|
+
}
|
183
|
+
static isXRSupported() { return Promise.all([this.isVRSupported(), this.isARSupported()]).then(res => res.some(e => e)).catch(() => false); }
|
184
|
+
static isVRSupported() { return this.xrSystem?.isSessionSupported('immersive-vr').catch(err => { if (debug) console.error(err); return false }) ?? Promise.resolve(false); }
|
185
|
+
static isARSupported() { return this.xrSystem?.isSessionSupported('immersive-ar').catch(err => { if (debug) console.error(err); return false }) ?? Promise.resolve(false); }
|
186
|
+
|
187
|
+
private static _currentSessionRequest?: Promise<XRSession>;
|
188
|
+
private static _activeSession: NeedleXRSession | null;
|
189
|
+
|
190
|
+
static onSessionRequestStart(evt: SessionRequestedEvent) {
|
191
|
+
this._sessionRequestStartListeners.push(evt);
|
192
|
+
}
|
193
|
+
static offSessionRequestStart(evt: SessionRequestedEvent) {
|
194
|
+
const index = this._sessionRequestStartListeners.indexOf(evt);
|
195
|
+
if (index >= 0) this._sessionRequestStartListeners.splice(index, 1);
|
196
|
+
}
|
197
|
+
private static readonly _sessionRequestStartListeners: SessionRequestedEvent[] = [];
|
198
|
+
|
199
|
+
/** Called after the session request has finished */
|
200
|
+
static onSessionRequestEnd(evt: SessionRequestedEndEvent) {
|
201
|
+
this._sessionRequestEndListeners.push(evt);
|
202
|
+
}
|
203
|
+
/** Unsubscribe from request end evt */
|
204
|
+
static offSessionRequestEnd(evt: SessionRequestedEndEvent) {
|
205
|
+
const index = this._sessionRequestEndListeners.indexOf(evt);
|
206
|
+
if (index >= 0) this._sessionRequestEndListeners.splice(index, 1);
|
207
|
+
}
|
208
|
+
private static readonly _sessionRequestEndListeners: SessionRequestedEndEvent[] = [];
|
209
|
+
|
210
|
+
/** Listen to XR session started. Unsubscribe with `offXRSessionStart` */
|
211
|
+
static onXRSessionStart(evt: SessionChangedEvt) {
|
212
|
+
this._xrStartListeners.push(evt);
|
213
|
+
};
|
214
|
+
/** Unsubscribe from XRSession started events */
|
215
|
+
static offXRSessionStart(evt: SessionChangedEvt) {
|
216
|
+
const index = this._xrStartListeners.indexOf(evt);
|
217
|
+
if (index >= 0) this._xrStartListeners.splice(index, 1);
|
218
|
+
}
|
219
|
+
private static readonly _xrStartListeners: SessionChangedEvt[] = [];
|
220
|
+
|
221
|
+
/** Listen to XR session ended. Unsubscribe with `offXRSessionEnd` */
|
222
|
+
static onXRSessionEnd(evt: SessionChangedEvt) {
|
223
|
+
this._xrEndListeners.push(evt);
|
224
|
+
};
|
225
|
+
/** Unsubscribe from XRSession started events */
|
226
|
+
static offXRSessionEnd(evt: SessionChangedEvt) {
|
227
|
+
const index = this._xrEndListeners.indexOf(evt);
|
228
|
+
if (index >= 0) this._xrEndListeners.splice(index, 1);
|
229
|
+
}
|
230
|
+
private static readonly _xrEndListeners: SessionChangedEvt[] = [];
|
231
|
+
|
232
|
+
/** Listen to controller added events.
|
233
|
+
* Events are cleared when starting a new session
|
234
|
+
**/
|
235
|
+
static onControllerAdded(evt: ControllerChangedEvt) {
|
236
|
+
this._controllerAddedListeners.push(evt);
|
237
|
+
}
|
238
|
+
/** Unsubscribe from controller added evts */
|
239
|
+
static offControllerAdded(evt: ControllerChangedEvt) {
|
240
|
+
const index = this._controllerAddedListeners.indexOf(evt);
|
241
|
+
if (index >= 0) this._controllerAddedListeners.splice(index, 1);
|
242
|
+
}
|
243
|
+
private static readonly _controllerAddedListeners: ControllerChangedEvt[] = [];
|
244
|
+
|
245
|
+
/** Listen to controller removed events
|
246
|
+
* Events are cleared when starting a new session
|
247
|
+
**/
|
248
|
+
static onControllerRemoved(evt: ControllerChangedEvt) {
|
249
|
+
this._controllerRemovedListeners.push(evt);
|
250
|
+
}
|
251
|
+
/** Unsubscribe from controller removed events */
|
252
|
+
static offControllerRemoved(evt: ControllerChangedEvt) {
|
253
|
+
const index = this._controllerRemovedListeners.indexOf(evt);
|
254
|
+
if (index >= 0) this._controllerRemovedListeners.splice(index, 1);
|
255
|
+
}
|
256
|
+
private static readonly _controllerRemovedListeners: ControllerChangedEvt[] = [];
|
257
|
+
|
258
|
+
/** If the browser supports offerSession - creating a VR or AR button in the browser navigation bar */
|
259
|
+
static offerSession(mode: XRSessionMode, init: XRSessionInit | "default", context: Context): boolean {
|
260
|
+
if ('xr' in navigator && navigator.xr && 'offerSession' in navigator.xr) {
|
261
|
+
if (typeof navigator.xr.offerSession === "function") {
|
262
|
+
console.log("WebXR offerSession is available - requesting mode: " + mode);
|
263
|
+
if (init == "default") {
|
264
|
+
init = this.getDefaultSessionInit(mode);
|
265
|
+
}
|
266
|
+
navigator.xr.offerSession(mode, {
|
267
|
+
...init
|
268
|
+
}).then((session) => {
|
269
|
+
return NeedleXRSession.setSession(mode, session, init as XRSessionInit, context);
|
270
|
+
}).catch(_ => {
|
271
|
+
console.log("XRSession offer rejected (perhaps because another call to offerSession was made or a call to requestSession was made)")
|
272
|
+
});
|
273
|
+
}
|
274
|
+
return true;
|
275
|
+
}
|
276
|
+
return false;
|
277
|
+
}
|
278
|
+
|
279
|
+
/** @returns a new XRSession init object with defaults */
|
280
|
+
static getDefaultSessionInit(mode: Omit<XRSessionMode, "inline">): XRSessionInit {
|
281
|
+
switch (mode) {
|
282
|
+
case "immersive-ar":
|
283
|
+
return {
|
284
|
+
optionalFeatures: ['anchors', 'local-floor', 'hand-tracking', 'layers', 'dom-overlay', 'hit-test'],
|
285
|
+
}
|
286
|
+
case "immersive-vr":
|
287
|
+
return {
|
288
|
+
optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'high-fixed-foveation-level', 'layers'],
|
289
|
+
}
|
290
|
+
default:
|
291
|
+
console.warn("No default session init for mode", mode);
|
292
|
+
return {};
|
293
|
+
}
|
294
|
+
}
|
295
|
+
|
296
|
+
/** start a new webXR session (make sure to stop already running sessions before calling this method)
|
297
|
+
* @param mode The XRSessionMode to start (e.g. `immersive-vr` or `immersive-ar`), docs: https://developer.mozilla.org/en-US/docs/Web/API/XRSessionMode
|
298
|
+
* @param init The XRSessionInit to use (optional), docs: https://developer.mozilla.org/en-US/docs/Web/API/XRSessionInit
|
299
|
+
* @param context The Needle Engine context to use
|
300
|
+
*/
|
301
|
+
static async start(mode: XRSessionMode, init?: XRSessionInit, context?: Context): Promise<NeedleXRSession | null> {
|
302
|
+
|
303
|
+
if (this._currentSessionRequest) {
|
304
|
+
console.warn("A XRSession is already being requested");
|
305
|
+
if (debug || isDevEnvironment()) showBalloonWarning("A XRSession is already being requested");
|
306
|
+
return this._currentSessionRequest.then(() => this._activeSession!);
|
307
|
+
}
|
308
|
+
|
309
|
+
if (this._activeSession) {
|
310
|
+
console.error("A XRSession is already running");
|
311
|
+
return this._activeSession;
|
312
|
+
}
|
313
|
+
|
314
|
+
// Make sure we have a context
|
315
|
+
if (!context) context = Context.Current;
|
316
|
+
if (!context) context = ContextRegistry.All[0] as Context;
|
317
|
+
if (!context) throw new Error("No Needle Engine Context found");
|
318
|
+
|
319
|
+
// setup session init args, make sure we have default values
|
320
|
+
if (!init) init = {};
|
321
|
+
switch (mode) {
|
322
|
+
|
323
|
+
// Setup VR initialization parameters
|
324
|
+
case "immersive-ar":
|
325
|
+
{
|
326
|
+
const supported = await this.xrSystem?.isSessionSupported('immersive-ar')
|
327
|
+
if (supported !== true) {
|
328
|
+
console.error(mode + ' is not supported by this browser.');
|
329
|
+
return null;
|
330
|
+
}
|
331
|
+
const defaultInit: XRSessionInit = this.getDefaultSessionInit(mode);
|
332
|
+
const domOverlayElement = getDOMOverlayElement(context.domElement);
|
333
|
+
if (domOverlayElement && !isQuest()) { // excluding quest since dom-overlay breaks sessiongranted
|
334
|
+
defaultInit.domOverlay = { root: domOverlayElement };
|
335
|
+
defaultInit.optionalFeatures!.push('dom-overlay');
|
336
|
+
}
|
337
|
+
init = {
|
338
|
+
...defaultInit,
|
339
|
+
...init,
|
340
|
+
}
|
341
|
+
}
|
342
|
+
break;
|
343
|
+
|
344
|
+
// Setup AR initialization parameters
|
345
|
+
case "immersive-vr":
|
346
|
+
{
|
347
|
+
const supported = await this.xrSystem?.isSessionSupported('immersive-vr')
|
348
|
+
if (supported !== true) {
|
349
|
+
console.error(mode + ' is not supported by this browser.');
|
350
|
+
return null;
|
351
|
+
}
|
352
|
+
const defaultInit: XRSessionInit = this.getDefaultSessionInit(mode);
|
353
|
+
init = {
|
354
|
+
...defaultInit,
|
355
|
+
...init,
|
356
|
+
}
|
357
|
+
}
|
358
|
+
break;
|
359
|
+
|
360
|
+
default:
|
361
|
+
console.warn("No default session init for mode", mode);
|
362
|
+
break;
|
363
|
+
}
|
364
|
+
|
365
|
+
// we stop a temporary session here (if any runs)
|
366
|
+
await TemporaryXRContext.stop();
|
367
|
+
|
368
|
+
const scripts = mode == "immersive-ar" ? context.scripts_immersive_ar : context.scripts_immersive_vr
|
369
|
+
|
370
|
+
if (debug)
|
371
|
+
console.log("%c" + `Requesting ${mode} session`, "font-weight:bold;", init, scripts);
|
372
|
+
else
|
373
|
+
console.log("%c" + `Requesting ${mode} session`, "font-weight:bold;");
|
374
|
+
for (const script of scripts) {
|
375
|
+
if (script.onBeforeXR) script.onBeforeXR(mode, init);
|
376
|
+
}
|
377
|
+
for (const listener of this._sessionRequestStartListeners) {
|
378
|
+
listener({ mode, init });
|
379
|
+
}
|
380
|
+
if (debug) showBalloonMessage("Requesting " + mode + " session (" + Date.now() + ")");
|
381
|
+
this._currentSessionRequest = navigator.xr?.requestSession(mode, init);
|
382
|
+
this._currentSessionRequestMode = mode;
|
383
|
+
/**@type {XRSystem} */
|
384
|
+
const newSession = await (this._currentSessionRequest)?.catch(e => {
|
385
|
+
console.error(e, "Code: " + e.code);
|
386
|
+
if (e.code === 9) showBalloonWarning("Make sure your device has the required permissions (e.g. camera access)")
|
387
|
+
console.log("If the specified XR configuration is not supported (e.g. entering AR doesnt work) - make sure you access the website on a secure connection (HTTPS) and your device has the required permissions (e.g. camera access)");
|
388
|
+
const notSecure = location.protocol === 'http:';
|
389
|
+
if (notSecure) showBalloonWarning("XR requires a secure connection (HTTPS)");
|
390
|
+
});
|
391
|
+
this._currentSessionRequest = undefined;
|
392
|
+
this._currentSessionRequestMode = null;
|
393
|
+
for (const listener of this._sessionRequestEndListeners) {
|
394
|
+
listener({ mode, init, newSession: newSession || null });
|
395
|
+
}
|
396
|
+
if (!newSession) {
|
397
|
+
console.warn("XR Session request was rejected");
|
398
|
+
return null;
|
399
|
+
}
|
400
|
+
return this.setSession(mode, newSession, init, context);
|
401
|
+
}
|
402
|
+
|
403
|
+
static setSession(mode: XRSessionMode, session: XRSession, init: XRSessionInit, context: Context) {
|
404
|
+
if (this._activeSession) {
|
405
|
+
console.error("A XRSession is already running");
|
406
|
+
return this._activeSession;
|
407
|
+
}
|
408
|
+
const scripts = mode == "immersive-ar" ? context.scripts_immersive_ar : context.scripts_immersive_vr;
|
409
|
+
this._activeSession = new NeedleXRSession(mode, session, context, {
|
410
|
+
scripts: scripts,
|
411
|
+
controller_added: this._controllerAddedListeners,
|
412
|
+
controller_removed: this._controllerRemovedListeners,
|
413
|
+
init: init
|
414
|
+
});
|
415
|
+
session.addEventListener("end", this.onEnd);
|
416
|
+
if (debug)
|
417
|
+
console.log("%c" + `Started ${mode} session`, "font-weight:bold;", scripts);
|
418
|
+
else
|
419
|
+
console.log("%c" + `Started ${mode} session`, "font-weight:bold;");
|
420
|
+
return this._activeSession;
|
421
|
+
}
|
422
|
+
/** stops the active XR session */
|
423
|
+
static stop() {
|
424
|
+
this._activeSession?.end();
|
425
|
+
}
|
426
|
+
private static onEnd = () => {
|
427
|
+
if (debug) console.log("XR Session ended");
|
428
|
+
this._activeSession = null;
|
429
|
+
}
|
430
|
+
|
431
|
+
|
432
|
+
/** The needle engine context this session was started from */
|
433
|
+
readonly context: Context;
|
434
|
+
|
435
|
+
get sync(): NeedleXRSync | null {
|
436
|
+
return NeedleXRSession._sync;
|
437
|
+
}
|
438
|
+
|
439
|
+
/** Returns true if the xr session is still active */
|
440
|
+
get running(): boolean { return !this._ended && this.session != null; }
|
441
|
+
|
442
|
+
/**
|
443
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession
|
444
|
+
*/
|
445
|
+
readonly session: XRSession;
|
446
|
+
|
447
|
+
/** XR Session Mode: AR or VR */
|
448
|
+
readonly mode: XRSessionMode;
|
449
|
+
|
450
|
+
/**
|
451
|
+
* The XRSession interface's read-only interactionMode property describes the best space (according to the user agent) for the application to draw an interactive UI for the current session.
|
452
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession/interactionMode
|
453
|
+
*/
|
454
|
+
get interactionMode(): "screen-space" | "world-space" { return this.session["interactionMode"]; }
|
455
|
+
|
456
|
+
/**
|
457
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession/visibilityState
|
458
|
+
*/
|
459
|
+
get visibilityState() { return this.session.visibilityState; }
|
460
|
+
|
461
|
+
/**
|
462
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession/environmentBlendMode
|
463
|
+
*/
|
464
|
+
get environmentBlendMode() { return this.session.environmentBlendMode; }
|
465
|
+
|
466
|
+
/**
|
467
|
+
* The current XR frame
|
468
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame
|
469
|
+
*/
|
470
|
+
get frame(): NeedleXRFrame { return this.context.xrFrame! as NeedleXRFrame; }
|
471
|
+
|
472
|
+
/** The currently active/connected controllers */
|
473
|
+
readonly controllers: NeedleXRController[] = [];
|
474
|
+
/** shorthand to query the left controller. Use `controllers` to get access to all connected controllers */
|
475
|
+
get leftController() { return this.controllers.find(c => c.isLeft); }
|
476
|
+
/** shorthand to query the right controller. Use `controllers` to get access to all connected controllers */
|
477
|
+
get rightController() { return this.controllers.find(c => c.isRight); }
|
478
|
+
/** @returns the given controller if it is connected */
|
479
|
+
getController(side: XRHandedness) { return this.controllers.find(c => c.side === side) || null; }
|
480
|
+
|
481
|
+
/** Returns true if running in pass through mode in immersive AR */
|
482
|
+
get isPassThrough() {
|
483
|
+
if (this.environmentBlendMode !== 'opaque' && this.interactionMode === "world-space") return true;
|
484
|
+
// since we can not rely on interactionMode check we check the controllers too
|
485
|
+
// https://linear.app/needle/issue/NE-4057
|
486
|
+
// the following is a workaround for the issue above
|
487
|
+
if (this.mode === "immersive-ar" && this.environmentBlendMode !== 'opaque') {
|
488
|
+
// if we have any tracked pointer controllers we're also in passthrough
|
489
|
+
if (this.controllers.some(c => c.inputSource.targetRayMode === "tracked-pointer"))
|
490
|
+
return true;
|
491
|
+
}
|
492
|
+
if (isDevEnvironment() && isDesktop() && this.mode === "immersive-ar") {
|
493
|
+
return true;
|
494
|
+
}
|
495
|
+
return false;
|
496
|
+
}
|
497
|
+
get isAR() { return this.mode === 'immersive-ar'; }
|
498
|
+
get isVR() { return this.mode === 'immersive-vr'; }
|
499
|
+
|
500
|
+
get posePosition() { return this._transformPosition; }
|
501
|
+
get poseOrientation() { return this._transformOrientation; }
|
502
|
+
/** @returns the context.renderer.xr.getReferenceSpace() result */
|
503
|
+
get referenceSpace(): XRSpace | null { return this.context.renderer.xr.getReferenceSpace(); }
|
504
|
+
/** @returns the XRFrame `viewerpose` using the xr `referenceSpace` */
|
505
|
+
get viewerPose(): XRViewerPose | undefined { return this._viewerPose; }
|
506
|
+
|
507
|
+
|
508
|
+
/** @returns `true` if any image is currently being tracked */
|
509
|
+
/** returns true if images are currently being tracked */
|
510
|
+
get isTrackingImages() {
|
511
|
+
if (this.frame && "getImageTrackingResults" in this.frame && typeof this.frame.getImageTrackingResults === "function") {
|
512
|
+
try {
|
513
|
+
const trackingResult = this.frame.getImageTrackingResults();
|
514
|
+
for (const result of trackingResult) {
|
515
|
+
const state = result.trackingState;
|
516
|
+
if (state === "tracked") return true;
|
517
|
+
}
|
518
|
+
}
|
519
|
+
catch {
|
520
|
+
// Looks like we get a NotSupportedException on Android since the method is known
|
521
|
+
// but the feature is not supported by the session
|
522
|
+
// TODO Can we check here if we even requested the image-tracking feature instead of catching?
|
523
|
+
return false;
|
524
|
+
}
|
525
|
+
}
|
526
|
+
return false;
|
527
|
+
}
|
528
|
+
|
529
|
+
|
530
|
+
/** The currently active XR rig */
|
531
|
+
get rig(): IXRRig | null {
|
532
|
+
const rig = this._rigs[0] ?? null;
|
533
|
+
if (rig?.gameObject && isDestroyed(rig.gameObject) || rig?.isActive === false) {
|
534
|
+
this.updateActiveXRRig();
|
535
|
+
return this._rigs[0] ?? null;
|
536
|
+
}
|
537
|
+
return rig;
|
538
|
+
}
|
539
|
+
private _rigScale: number = 1;
|
540
|
+
private _lastRigScaleUpdate: number = -1;
|
541
|
+
/** get the XR rig worldscale */
|
542
|
+
get rigScale() {
|
543
|
+
if (!this._rigs[0]) return 1;
|
544
|
+
if (this._lastRigScaleUpdate !== this.context.time.frame) {
|
545
|
+
this._lastRigScaleUpdate = this.context.time.frame;
|
546
|
+
this._rigScale = this._rigs[0].gameObject.worldScale.x;
|
547
|
+
}
|
548
|
+
return this._rigScale;
|
549
|
+
}
|
550
|
+
/** add a rig to the available XR rigs - if it's priority is higher than the currently active rig it will be enabled */
|
551
|
+
addRig(rig: IXRRig) {
|
552
|
+
const i = this._rigs.indexOf(rig);
|
553
|
+
if (i >= 0) return;
|
554
|
+
if (rig.priority === undefined) rig.priority = 0;
|
555
|
+
this._rigs.push(rig);
|
556
|
+
this.updateActiveXRRig();
|
557
|
+
}
|
558
|
+
/** Remove a rig from the available XR Rigs */
|
559
|
+
removeRig(rig: IXRRig) {
|
560
|
+
const i = this._rigs.indexOf(rig);
|
561
|
+
if (i === -1) return;
|
562
|
+
this._rigs.splice(i, 1);
|
563
|
+
this.updateActiveXRRig();
|
564
|
+
}
|
565
|
+
/** Sets a XRRig to be active which will parent the camera to this rig */
|
566
|
+
setRigActive(rig: IXRRig) {
|
567
|
+
const i = this._rigs.indexOf(rig);
|
568
|
+
this._rigs.splice(i, 1);
|
569
|
+
this._rigs.unshift(rig);
|
570
|
+
this.updateActiveXRRig();
|
571
|
+
}
|
572
|
+
private updateActiveXRRig() {
|
573
|
+
const previouslyActiveRig = this._rigs[0] ?? null;
|
574
|
+
|
575
|
+
// ensure that the default rig is in the scene
|
576
|
+
if (this._defaultRig.gameObject.parent !== this.context.scene)
|
577
|
+
this.context.scene.add(this._defaultRig.gameObject);
|
578
|
+
// ensure the fallback rig is always active!!!
|
579
|
+
this._defaultRig.gameObject.visible = true;
|
580
|
+
// ensure that the default rig is in the list of available rigs
|
581
|
+
if (!this._rigs.includes(this._defaultRig))
|
582
|
+
this._rigs.push(this._defaultRig);
|
583
|
+
|
584
|
+
// find the rig with the highest priority and make sure it's at the beginning of the array
|
585
|
+
let highestPriorityRig: IXRRig = this._rigs[0];
|
586
|
+
if (highestPriorityRig && highestPriorityRig.priority === undefined) highestPriorityRig.priority = 0;
|
587
|
+
|
588
|
+
for (let i = 1; i < this._rigs.length; i++) {
|
589
|
+
const rig = this._rigs[i];
|
590
|
+
if (!rig.isActive) continue;
|
591
|
+
if (isDestroyed(rig.gameObject)) {
|
592
|
+
this._rigs.splice(i, 1);
|
593
|
+
i--;
|
594
|
+
continue;
|
595
|
+
}
|
596
|
+
if (!highestPriorityRig || highestPriorityRig.isActive === false || (rig.priority !== undefined && rig.priority > highestPriorityRig.priority!)) {
|
597
|
+
highestPriorityRig = rig;
|
598
|
+
}
|
599
|
+
}
|
600
|
+
|
601
|
+
// make sure the highest priority rig is at the beginning if it isnt already
|
602
|
+
if (previouslyActiveRig !== highestPriorityRig) {
|
603
|
+
const index = this._rigs.indexOf(highestPriorityRig);
|
604
|
+
if (index >= 0) this._rigs.splice(index, 1);
|
605
|
+
this._rigs.unshift(highestPriorityRig);
|
606
|
+
}
|
607
|
+
|
608
|
+
if (debug) {
|
609
|
+
if (previouslyActiveRig === highestPriorityRig)
|
610
|
+
console.log("Updated Active XR Rig:", highestPriorityRig, "prev:", previouslyActiveRig);
|
611
|
+
else console.log("Updated Active XRRig:", highestPriorityRig, " (the same as before)");
|
612
|
+
}
|
613
|
+
}
|
614
|
+
private _rigs: IXRRig[] = [];
|
615
|
+
|
616
|
+
|
617
|
+
|
618
|
+
private _viewerHitTestSource: XRHitTestSource | null = null;
|
619
|
+
|
620
|
+
/** Returns a XR hit test result (if hit-testing is available) in rig space
|
621
|
+
* @param source If provided, the hit test will be performed for the given controller
|
622
|
+
*/
|
623
|
+
getHitTest(source?: NeedleXRController): NeedleXRHitTestResult | null {
|
624
|
+
if (source) {
|
625
|
+
return this.getControllerHitTest(source);
|
626
|
+
}
|
627
|
+
|
628
|
+
if (!this._viewerHitTestSource) return null;
|
629
|
+
const hitTestSource = this._viewerHitTestSource;
|
630
|
+
const hitTestResults = this.frame.getHitTestResults(hitTestSource);
|
631
|
+
if (hitTestResults.length > 0) {
|
632
|
+
const hit = hitTestResults[0];
|
633
|
+
return this.convertHitTestResult(hit);
|
634
|
+
}
|
635
|
+
return null;
|
636
|
+
}
|
637
|
+
private getControllerHitTest(controller: NeedleXRController): NeedleXRHitTestResult | null {
|
638
|
+
const hitTestSource = controller.hitTestSource;
|
639
|
+
if (!hitTestSource) return null;
|
640
|
+
const res = this.frame.getHitTestResultsForTransientInput(hitTestSource);
|
641
|
+
for (const result of res) {
|
642
|
+
if (result.inputSource === controller.inputSource) {
|
643
|
+
for (const hit of result.results) {
|
644
|
+
return this.convertHitTestResult(hit);
|
645
|
+
}
|
646
|
+
}
|
647
|
+
}
|
648
|
+
return null;
|
649
|
+
}
|
650
|
+
private convertHitTestResult(result: XRHitTestResult): NeedleXRHitTestResult | null {
|
651
|
+
const referenceSpace = this.context.renderer.xr.getReferenceSpace();
|
652
|
+
const pose = referenceSpace && result.getPose(referenceSpace);
|
653
|
+
if (pose) {
|
654
|
+
const pos = getTempVector(pose.transform.position);
|
655
|
+
const rot = getTempQuaternion(pose.transform.orientation);
|
656
|
+
const camera = this.context.mainCamera;
|
657
|
+
if (camera?.parent !== this._cameraRenderParent) {
|
658
|
+
pos.applyMatrix4(flipForwardMatrix);
|
659
|
+
}
|
660
|
+
if (camera?.parent) {
|
661
|
+
pos.applyMatrix4(camera.parent.matrixWorld);
|
662
|
+
rot.multiply(flipForwardQuaternion);
|
663
|
+
// apply parent quaternion (if parent is moved/rotated)
|
664
|
+
const parentRotation = getWorldQuaternion(camera.parent);
|
665
|
+
// ensure that "up" (y+) is pointing away from the wall
|
666
|
+
parentRotation.premultiply(flipForwardQuaternion);
|
667
|
+
rot.premultiply(parentRotation);
|
668
|
+
}
|
669
|
+
return { hit: result, position: pos, quaternion: rot };
|
670
|
+
}
|
671
|
+
return null;
|
672
|
+
}
|
673
|
+
|
674
|
+
|
675
|
+
/** convert a XRRigidTransform from XR session space to threejs / Needle Engine XR space */
|
676
|
+
convertSpace(transform: XRRigidTransform): { position: Vector3, quaternion: Quaternion } {
|
677
|
+
const pos = getTempVector(transform.position);
|
678
|
+
pos.applyMatrix4(flipForwardMatrix);
|
679
|
+
const rot = getTempQuaternion(transform.orientation);
|
680
|
+
rot.premultiply(flipForwardQuaternion);
|
681
|
+
return { position: pos, quaternion: rot };
|
682
|
+
}
|
683
|
+
|
684
|
+
/** this is the implictly created XR rig */
|
685
|
+
private readonly _defaultRig: IXRRig;
|
686
|
+
|
687
|
+
/** all scripts that receive some sort of XR update event */
|
688
|
+
private readonly _xr_scripts: INeedleXRSessionEventReceiver[];
|
689
|
+
/** scripts that have onUpdateXR event methods */
|
690
|
+
private readonly _xr_update_scripts: INeedleXRSessionEventReceiver[] = [];
|
691
|
+
/** scripts that are in the scene but inactive (e.g. disabled parent gameObject) */
|
692
|
+
private readonly _inactive_scripts: INeedleXRSessionEventReceiver[] = [];
|
693
|
+
private readonly _controllerAdded: ControllerChangedEvt[];
|
694
|
+
private readonly _controllerRemoved: ControllerChangedEvt[];
|
695
|
+
private readonly _originalCameraWorldPosition?: Vector3 | null;
|
696
|
+
private readonly _originalCameraWorldRotation?: Quaternion | null;
|
697
|
+
private readonly _originalCameraWorldScale?: Vector3 | null;
|
698
|
+
private readonly _originalCameraParent?: Object3D | null;
|
699
|
+
/** we store the main camera reference here each frame to make sure we have a rendering camera
|
700
|
+
* this e.g. the case when the XR rig with the camera gets disabled (and thus this.context.mainCamera is unassigned)
|
701
|
+
*/
|
702
|
+
private _mainCamera: ICamera | null = null;
|
703
|
+
|
704
|
+
private constructor(mode: XRSessionMode, session: XRSession, context: Context, extra: {
|
705
|
+
scripts: INeedleXRSessionEventReceiver[],
|
706
|
+
controller_added: ControllerChangedEvt[],
|
707
|
+
controller_removed: ControllerChangedEvt[],
|
708
|
+
/** the initialization arguments */
|
709
|
+
init: XRSessionInit,
|
710
|
+
}) {
|
711
|
+
saveSessionInfo(mode, extra.init);
|
712
|
+
this.session = session;
|
713
|
+
this.mode = mode;
|
714
|
+
this.context = context;
|
715
|
+
|
716
|
+
this.context.xr = this;
|
717
|
+
this.context.renderer.xr.setSession(this.session).then(this.onRendererSessionSet)
|
718
|
+
|
719
|
+
this._xr_scripts = [...extra.scripts];
|
720
|
+
this._xr_update_scripts = this._xr_scripts.filter(e => typeof e.onUpdateXR === "function");
|
721
|
+
this._controllerAdded = extra.controller_added;
|
722
|
+
this._controllerRemoved = extra.controller_removed;
|
723
|
+
|
724
|
+
registerFrameEventCallback(this.onBefore, FrameEvent.LateUpdate);
|
725
|
+
this.context.pre_render_callbacks.push(this.onBeforeRender);
|
726
|
+
this.context.post_render_callbacks.push(this.onAfterRender);
|
727
|
+
|
728
|
+
|
729
|
+
if (extra.init.optionalFeatures?.includes("hit-test") || extra.init.requiredFeatures?.includes("hit-test")) {
|
730
|
+
session.requestReferenceSpace('viewer').then((referenceSpace) => {
|
731
|
+
return session.requestHitTestSource?.call(session, { space: referenceSpace })?.then((source) => {
|
732
|
+
return this._viewerHitTestSource = source;
|
733
|
+
});
|
734
|
+
}).catch(e => console.warn(e));
|
735
|
+
}
|
736
|
+
|
737
|
+
if (this.context.mainCamera) {
|
738
|
+
this._originalCameraWorldPosition = getWorldPosition(this.context.mainCamera, new Vector3());
|
739
|
+
this._originalCameraWorldRotation = getWorldQuaternion(this.context.mainCamera, new Quaternion());
|
740
|
+
this._originalCameraWorldScale = getWorldScale(this.context.mainCamera, new Vector3());
|
741
|
+
this._originalCameraParent = this.context.mainCamera.parent;
|
742
|
+
}
|
743
|
+
|
744
|
+
this.context.mainCameraComponent?.applyClearFlags();
|
745
|
+
|
746
|
+
this._defaultRig = new ImplictXRRig();
|
747
|
+
this.context.scene.add(this._defaultRig.gameObject);
|
748
|
+
this.addRig(this._defaultRig);
|
749
|
+
|
750
|
+
// register already connected input sources
|
751
|
+
// this is for when the session is already running (via a temporary xr session)
|
752
|
+
// and the controllers are already connected
|
753
|
+
for (const sources of this.session.inputSources) {
|
754
|
+
this.onInputSourceAdded(sources);
|
755
|
+
}
|
756
|
+
|
757
|
+
// handle controller and input source changes changes
|
758
|
+
this.session.addEventListener('end', this.onEnd);
|
759
|
+
// handle input sources change
|
760
|
+
this.session.addEventListener("inputsourceschange", (evt: XRInputSourceChangeEvent) => {
|
761
|
+
// handle removed controllers
|
762
|
+
for (const removedInputSource of evt.removed) {
|
763
|
+
this.disconnectInputSource(removedInputSource);
|
764
|
+
}
|
765
|
+
for (const newInputSource of evt.added) {
|
766
|
+
this.onInputSourceAdded(newInputSource);
|
767
|
+
}
|
768
|
+
});
|
769
|
+
}
|
770
|
+
|
771
|
+
/** called when renderer.setSession is fulfilled */
|
772
|
+
private onRendererSessionSet = () => {
|
773
|
+
if (!this.running) return;
|
774
|
+
this.context.renderer.xr.enabled = true;
|
775
|
+
// calling update camera here once to prevent the xr camera left/right NaN in the first frame due to false near/far plane values, see NE-4126
|
776
|
+
this.context.renderer.xr.updateCamera(this.context.mainCamera as PerspectiveCamera);
|
777
|
+
}
|
778
|
+
|
779
|
+
private onInputSourceAdded = (newInputSource: XRInputSource) => {
|
780
|
+
// do not create XR controllers for screen input sources
|
781
|
+
if (newInputSource.targetRayMode === "screen") {
|
782
|
+
return;
|
783
|
+
}
|
784
|
+
let index = 0;
|
785
|
+
for (let i = 0; i < this.session.inputSources.length; i++) {
|
786
|
+
if (this.session.inputSources[i] === newInputSource) {
|
787
|
+
index = i;
|
788
|
+
break;
|
789
|
+
}
|
790
|
+
}
|
791
|
+
// check if an xr controller for this input source already exists
|
792
|
+
// in case we have both an event from inputsourceschange and from the construtor initial input sources
|
793
|
+
if (this.controllers.find(c => c.inputSource === newInputSource)) return;
|
794
|
+
|
795
|
+
const newController = new NeedleXRController(this, newInputSource, index);
|
796
|
+
this.controllers.push(newController);
|
797
|
+
this.controllers.sort((a, b) => a.index - b.index);
|
798
|
+
this._newControllers.push(newController);
|
799
|
+
this.invokeControllerEvent(newController, this._controllerAdded, "added");
|
800
|
+
|
801
|
+
}
|
802
|
+
|
803
|
+
/** End the XR Session */
|
804
|
+
end() {
|
805
|
+
// this can be called by external code to end the session
|
806
|
+
// the actual cleanup happens in onEnd which subscribes to the session end event
|
807
|
+
// so users can also just regularly call session.end() and the cleanup will happen automatically
|
808
|
+
if (this._ended) return;
|
809
|
+
this.session.end().catch(e => console.warn(e));
|
810
|
+
}
|
811
|
+
|
812
|
+
private _ended: boolean = false;
|
813
|
+
private readonly _newControllers: NeedleXRController[] = [];
|
814
|
+
|
815
|
+
private onEnd = (_evt: XRSessionEvent) => {
|
816
|
+
if (this._ended) return;
|
817
|
+
this._ended = true;
|
818
|
+
|
819
|
+
if (debug) console.log("XR Session ended");
|
820
|
+
|
821
|
+
deleteSessionInfo();
|
822
|
+
|
823
|
+
this.onAfterRender();
|
824
|
+
this.revertCustomForward();
|
825
|
+
this._didStart = false;
|
826
|
+
this._previousCameraParent = null;
|
827
|
+
|
828
|
+
unregisterFrameEventCallback(this.onBefore, FrameEvent.LateUpdate);
|
829
|
+
const index = this.context.pre_render_callbacks.indexOf(this.onBeforeRender);
|
830
|
+
if (index >= 0) this.context.pre_render_callbacks.splice(index, 1);
|
831
|
+
const index2 = this.context.post_render_callbacks.indexOf(this.onAfterRender);
|
832
|
+
if (index2 >= 0) this.context.post_render_callbacks.splice(index2, 1);
|
833
|
+
|
834
|
+
this.context.xr = null;
|
835
|
+
this.context.renderer.xr.enabled = false;
|
836
|
+
this.context.mainCameraComponent?.applyClearFlags();
|
837
|
+
|
838
|
+
for (const listener of NeedleXRSession._xrEndListeners) {
|
839
|
+
listener({ xr: this });
|
840
|
+
}
|
841
|
+
|
842
|
+
// make sure we disconnect all controllers
|
843
|
+
for (let i = 0; i < this.controllers.length; i++) {
|
844
|
+
this.disconnectInputSource(this.controllers[i].inputSource);
|
845
|
+
}
|
846
|
+
|
847
|
+
// we want to call leave XR for *all* scripts that are still registered
|
848
|
+
// even if they might already be destroyed e.g. by the WebXR component (it destroys the default controller scripts)
|
849
|
+
// they should still receive this callback to be properly cleaned up
|
850
|
+
for (const listener of this._xr_scripts) {
|
851
|
+
listener?.onLeaveXR?.({ xr: this });
|
852
|
+
}
|
853
|
+
|
854
|
+
this.sync?.onExitXR(this);
|
855
|
+
|
856
|
+
|
857
|
+
if (this.context.mainCamera) {
|
858
|
+
// if we have a main camera we want to move it back to it's original parent
|
859
|
+
this._originalCameraParent?.add(this.context.mainCamera);
|
860
|
+
|
861
|
+
if (this._originalCameraWorldPosition) {
|
862
|
+
setWorldPosition(this.context.mainCamera, this._originalCameraWorldPosition);
|
863
|
+
}
|
864
|
+
if (this._originalCameraWorldRotation) {
|
865
|
+
setWorldQuaternion(this.context.mainCamera, this._originalCameraWorldRotation);
|
866
|
+
}
|
867
|
+
if (this._originalCameraWorldScale) {
|
868
|
+
setWorldScale(this.context.mainCamera, this._originalCameraWorldScale);
|
869
|
+
}
|
870
|
+
}
|
871
|
+
|
872
|
+
// mark for size change since DPI might have changed
|
873
|
+
this.context.requestSizeUpdate();
|
874
|
+
|
875
|
+
this._defaultRig.gameObject.removeFromParent();
|
876
|
+
};
|
877
|
+
|
878
|
+
/** Disconnects the controller, invokes events and notifies previou controller (if any) */
|
879
|
+
private disconnectInputSource(inputSource: XRInputSource) {
|
880
|
+
for (let i = this.controllers.length - 1; i >= 0; i--) {
|
881
|
+
const oldController = this.controllers[i];
|
882
|
+
if (oldController.inputSource === inputSource) {
|
883
|
+
this.controllers.splice(i, 1);
|
884
|
+
this.invokeControllerEvent(oldController, this._controllerRemoved, "removed");
|
885
|
+
const args: NeedleXRControllerEventArgs = {
|
886
|
+
xr: this,
|
887
|
+
controller: oldController,
|
888
|
+
change: "removed"
|
889
|
+
};
|
890
|
+
for (const script of this._xr_scripts) {
|
891
|
+
if (script.onXRControllerRemoved) script.onXRControllerRemoved(args);
|
892
|
+
}
|
893
|
+
oldController.onDisconnected();
|
894
|
+
}
|
895
|
+
}
|
896
|
+
}
|
897
|
+
|
898
|
+
private _didStart: boolean = false;
|
899
|
+
|
900
|
+
/** Called every frame by the engine */
|
901
|
+
private onBefore = (context: Context) => {
|
902
|
+
const frame = context.xrFrame;
|
903
|
+
if (!frame) return;
|
904
|
+
|
905
|
+
// ensure that XR is always set to a running session
|
906
|
+
this.context.xr = this;
|
907
|
+
|
908
|
+
// ensure that we always have the correct main camera reference
|
909
|
+
// we need to store the main camera reference here because the main camera can be disabled and then it's removed from the context
|
910
|
+
// but in XR we want to ensure we always have a active camera (or at least parent the camera to the active rig)
|
911
|
+
if (this.context.mainCameraComponent && this.context.mainCameraComponent !== this._mainCamera) {
|
912
|
+
this._mainCamera = this.context.mainCameraComponent;
|
913
|
+
}
|
914
|
+
|
915
|
+
if (this.rig?.isActive == false) {
|
916
|
+
if (debug) console.warn("Latest rig is not active - trying to activate a different rig", this.rig);
|
917
|
+
this.updateActiveXRRig();
|
918
|
+
}
|
919
|
+
|
920
|
+
// make sure the camera is parented to the active rig
|
921
|
+
if (this.rig && this._mainCamera?.gameObject) {
|
922
|
+
const currentParent = this._mainCamera?.gameObject?.parent;
|
923
|
+
if (currentParent !== this.rig.gameObject) {
|
924
|
+
this.rig.gameObject.add(this._mainCamera?.gameObject);
|
925
|
+
}
|
926
|
+
}
|
927
|
+
|
928
|
+
this.internalUpdateState();
|
929
|
+
|
930
|
+
// we apply the flip immediately and keep it while in XR so that regular raycasts just work
|
931
|
+
// otherwise rendering would fool us
|
932
|
+
this.applyCustomForward();
|
933
|
+
|
934
|
+
const args: NeedleXREventArgs = { xr: this };
|
935
|
+
|
936
|
+
// we want to invoke ENTERXR for NEW component nad EXITXR for REMOVED components
|
937
|
+
// we want to invoke onControllerAdded to components joining a running XR session if controllers are already connected
|
938
|
+
//TODO: handle REMOVED components during a session (we want to call leave session events and controller removed events)
|
939
|
+
|
940
|
+
// deferred start because we need an XR frame
|
941
|
+
if (!this._didStart) {
|
942
|
+
this._didStart = true;
|
943
|
+
|
944
|
+
for (const listener of NeedleXRSession._xrStartListeners) {
|
945
|
+
listener(args);
|
946
|
+
}
|
947
|
+
|
948
|
+
// invoke session listeners start
|
949
|
+
// we need to make a copy because the array might be modified during the loop (could also use a for loop and iterate backwards perhaps but then order of invocation would be changed OR check if the size has changed...)
|
950
|
+
const copy = [...this._xr_scripts];
|
951
|
+
if (debug) console.log("NeedleXRSession start, handle scripts:", copy);
|
952
|
+
for (const script of copy) {
|
953
|
+
if (script.destroyed) {
|
954
|
+
this._script_to_remove.push(script);
|
955
|
+
continue;
|
956
|
+
}
|
957
|
+
if (!script.activeAndEnabled) {
|
958
|
+
this.markInactive(script);
|
959
|
+
continue;
|
960
|
+
}
|
961
|
+
// if ((script as IComponent).activeAndEnabled === false) continue;
|
962
|
+
this.invokeCallback_EnterXR(script);
|
963
|
+
// also invoke all events for currently (already) connected controllers
|
964
|
+
for (const controller of this.controllers) {
|
965
|
+
this.invokeCallback_ControllerAdded(script, controller);
|
966
|
+
}
|
967
|
+
}
|
968
|
+
}
|
969
|
+
else if (this.context.new_scripts_xr.length > 0) {
|
970
|
+
// invoke start on all new scripts that were added during the session and that support the current mode
|
971
|
+
const copy = [...this.context.new_scripts_xr];
|
972
|
+
for (let i = 0; i < copy.length; i++) {
|
973
|
+
const script = this.context.new_scripts_xr[i] as IComponent & INeedleXRSessionEventReceiver;
|
974
|
+
if (!script || script.destroyed || script.supportsXR?.(this.mode) == false) {
|
975
|
+
this.context.new_scripts_xr.splice(i, 1);
|
976
|
+
continue;
|
977
|
+
}
|
978
|
+
if (!script.activeAndEnabled) {
|
979
|
+
this.context.new_scripts_xr.splice(i, 1);
|
980
|
+
this.markInactive(script);
|
981
|
+
continue;
|
982
|
+
}
|
983
|
+
// ignore inactive scripts
|
984
|
+
// if (script.activeAndEnabled === false) continue;
|
985
|
+
if (this.addScript(script)) {
|
986
|
+
// invoke onEnterXR on those scripts because they joined a running session
|
987
|
+
this.invokeCallback_EnterXR(script);
|
988
|
+
// also invoke all events for currently (already) connected controllers
|
989
|
+
for (const controller of this.controllers) {
|
990
|
+
this.invokeCallback_ControllerAdded(script, controller);
|
991
|
+
}
|
992
|
+
}
|
993
|
+
}
|
994
|
+
}
|
995
|
+
|
996
|
+
// make sure camera layers are correct
|
997
|
+
// we do this every frame here but I think it would be enough to do it once after the first rendering
|
998
|
+
// since we want to override the settings in three's WebXRManager
|
999
|
+
// we also want to continuously sync the culling mask if a user changes it on the main cam while in XR
|
1000
|
+
this.syncCameraCullingMask();
|
1001
|
+
|
1002
|
+
// update controllers
|
1003
|
+
for (const controller of this.controllers) {
|
1004
|
+
controller.onUpdate(frame);
|
1005
|
+
}
|
1006
|
+
|
1007
|
+
// handle when new controllers have been added
|
1008
|
+
for (const controller of this._newControllers) {
|
1009
|
+
for (const script of this._xr_scripts) {
|
1010
|
+
if (script.destroyed) {
|
1011
|
+
this._script_to_remove.push(script);
|
1012
|
+
continue;
|
1013
|
+
}
|
1014
|
+
if (script.onXRControllerAdded) script.onXRControllerAdded({ xr: this, controller, change: "added" });
|
1015
|
+
}
|
1016
|
+
}
|
1017
|
+
this._newControllers.length = 0;
|
1018
|
+
|
1019
|
+
// invoke update on all scripts
|
1020
|
+
for (const script of this._xr_update_scripts) {
|
1021
|
+
if (script.destroyed === true) {
|
1022
|
+
this._script_to_remove.push(script);
|
1023
|
+
continue;
|
1024
|
+
}
|
1025
|
+
if (script.activeAndEnabled === false) {
|
1026
|
+
this.markInactive(script);
|
1027
|
+
continue;
|
1028
|
+
}
|
1029
|
+
if (script.onUpdateXR) script.onUpdateXR(args);
|
1030
|
+
}
|
1031
|
+
|
1032
|
+
// handle inactive scripts
|
1033
|
+
this.handleInactiveScripts();
|
1034
|
+
|
1035
|
+
// handle removed scripts
|
1036
|
+
if (this._script_to_remove.length > 0) {
|
1037
|
+
// make sure we have no duplicates
|
1038
|
+
const unique = [...new Set(this._script_to_remove)];
|
1039
|
+
this._script_to_remove.length = 0;
|
1040
|
+
for (const script of unique) {
|
1041
|
+
if (!script.destroyed && this.running) {
|
1042
|
+
script.onLeaveXR?.(args);
|
1043
|
+
}
|
1044
|
+
this.removeScript(script);
|
1045
|
+
}
|
1046
|
+
}
|
1047
|
+
|
1048
|
+
this.sync?.onUpdate(this);
|
1049
|
+
|
1050
|
+
this.onRenderDebug();
|
1051
|
+
}
|
1052
|
+
|
1053
|
+
private onRenderDebug() {
|
1054
|
+
if (debug) {
|
1055
|
+
for (const controller of this.controllers) {
|
1056
|
+
controller.onRenderDebug();
|
1057
|
+
}
|
1058
|
+
}
|
1059
|
+
if ((debug || debugFPS) && this.rig) {
|
1060
|
+
const pos = this.rig.gameObject.worldPosition;
|
1061
|
+
const forward = this.rig.gameObject.worldForward;
|
1062
|
+
pos.add(forward.multiplyScalar(1.5));
|
1063
|
+
const upwards = this.rig.gameObject.worldUp;
|
1064
|
+
pos.add(upwards.multiplyScalar(2.5));
|
1065
|
+
let debugLabel = "";
|
1066
|
+
debugLabel += this.context.time.smoothedFps.toFixed(1);
|
1067
|
+
if (debug) {
|
1068
|
+
for (const ctrl of this.controllers) {
|
1069
|
+
debugLabel += `\n${ctrl.hand ? "hand" : "ctrl"} ${ctrl.inputSource.handedness}[${ctrl.index}] con:${ctrl.connected} tr:${ctrl.isTracking}`;
|
1070
|
+
}
|
1071
|
+
}
|
1072
|
+
Gizmos.DrawLabel(pos, debugLabel);
|
1073
|
+
}
|
1074
|
+
}
|
1075
|
+
|
1076
|
+
private onBeforeRender = () => {
|
1077
|
+
if (this.context.mainCamera)
|
1078
|
+
this.updateFade(this.context.mainCamera);
|
1079
|
+
}
|
1080
|
+
|
1081
|
+
private onAfterRender = () => {
|
1082
|
+
this.onUpdateFade_PostRender();
|
1083
|
+
|
1084
|
+
// render spectator view if we're in VR using Link
|
1085
|
+
if (isDesktop()) {
|
1086
|
+
const renderer = this.context.renderer;
|
1087
|
+
if (renderer.xr.isPresenting && this.context.mainCamera) {
|
1088
|
+
const wasXr = renderer.xr.enabled;
|
1089
|
+
const previousRenderTarget = renderer.getRenderTarget();
|
1090
|
+
renderer.xr.enabled = false;
|
1091
|
+
renderer.setRenderTarget(null);
|
1092
|
+
renderer.render(this.context.scene, this.context.mainCamera);
|
1093
|
+
renderer.xr.enabled = wasXr;
|
1094
|
+
renderer.setRenderTarget(previousRenderTarget);
|
1095
|
+
}
|
1096
|
+
}
|
1097
|
+
}
|
1098
|
+
|
1099
|
+
/** register a new XR script if it hasnt added yet */
|
1100
|
+
private addScript(script: INeedleXRSessionEventReceiver) {
|
1101
|
+
if (this._xr_scripts.includes(script)) return false;
|
1102
|
+
if (debug) console.log("Register new XRScript", script);
|
1103
|
+
this._xr_scripts.push(script);
|
1104
|
+
if (typeof script.onUpdateXR === "function") {
|
1105
|
+
this._xr_update_scripts.push(script);
|
1106
|
+
}
|
1107
|
+
return true;
|
1108
|
+
}
|
1109
|
+
|
1110
|
+
/** mark a script as inactive and invokes callbacks */
|
1111
|
+
private markInactive(script: INeedleXRSessionEventReceiver) {
|
1112
|
+
if (this._inactive_scripts.indexOf(script) >= 0) return;
|
1113
|
+
// inactive scripts should not receive any regular callbacks anymore
|
1114
|
+
this.removeScript(script, false);
|
1115
|
+
this._inactive_scripts.push(script);
|
1116
|
+
// inactive scripts receive callbacks as if the XR session has ended
|
1117
|
+
for (const ctrl of this.controllers) this.invokeCallback_ControllerRemoved(script, ctrl);
|
1118
|
+
this.invokeCallback_LeaveXR(script);
|
1119
|
+
}
|
1120
|
+
private handleInactiveScripts() {
|
1121
|
+
if (this._inactive_scripts.length > 0) {
|
1122
|
+
for (let i = this._inactive_scripts.length - 1; i >= 0; i--) {
|
1123
|
+
const script = this._inactive_scripts[i];
|
1124
|
+
if (script.activeAndEnabled) {
|
1125
|
+
this._inactive_scripts.splice(i, 1);
|
1126
|
+
this.addScript(script);
|
1127
|
+
this.invokeCallback_EnterXR(script);
|
1128
|
+
for (const ctrl of this.controllers) this.invokeCallback_ControllerAdded(script, ctrl);
|
1129
|
+
}
|
1130
|
+
}
|
1131
|
+
}
|
1132
|
+
}
|
1133
|
+
|
1134
|
+
private readonly _script_to_remove: INeedleXRSessionEventReceiver[] = [];
|
1135
|
+
|
1136
|
+
private removeScript(script: INeedleXRSessionEventReceiver, removeCompletely: boolean = true) {
|
1137
|
+
if (debug) console.log("Remove XRScript", script);
|
1138
|
+
const index = this._xr_scripts.indexOf(script);
|
1139
|
+
if (index >= 0) this._xr_scripts.splice(index, 1);
|
1140
|
+
const index2 = this._xr_update_scripts.indexOf(script);
|
1141
|
+
if (index2 >= 0) this._xr_update_scripts.splice(index2, 1);
|
1142
|
+
if (removeCompletely) {
|
1143
|
+
const index3 = this._inactive_scripts.indexOf(script);
|
1144
|
+
if (index3 >= 0) this._inactive_scripts.splice(index3, 1);
|
1145
|
+
}
|
1146
|
+
}
|
1147
|
+
|
1148
|
+
private invokeCallback_EnterXR(script: INeedleXRSessionEventReceiver) {
|
1149
|
+
if (script.onEnterXR) {
|
1150
|
+
script.onEnterXR({ xr: this });
|
1151
|
+
}
|
1152
|
+
}
|
1153
|
+
private invokeCallback_ControllerAdded(script: INeedleXRSessionEventReceiver, controller: NeedleXRController) {
|
1154
|
+
if (script.onXRControllerAdded) {
|
1155
|
+
script.onXRControllerAdded({ xr: this, controller, change: "added" });
|
1156
|
+
}
|
1157
|
+
}
|
1158
|
+
private invokeCallback_ControllerRemoved(script: INeedleXRSessionEventReceiver, controller: NeedleXRController) {
|
1159
|
+
if (script.onXRControllerRemoved) {
|
1160
|
+
script.onXRControllerRemoved({ xr: this, controller, change: "removed" });
|
1161
|
+
}
|
1162
|
+
}
|
1163
|
+
private invokeCallback_LeaveXR(script: INeedleXRSessionEventReceiver) {
|
1164
|
+
if (script.onLeaveXR && !script.destroyed) {
|
1165
|
+
script.onLeaveXR({ xr: this });
|
1166
|
+
}
|
1167
|
+
}
|
1168
|
+
|
1169
|
+
private syncCameraCullingMask() {
|
1170
|
+
// when we set unity layers objects will only be rendered on one eye
|
1171
|
+
// we set layers to sync raycasting and have a similar behaviour to unity
|
1172
|
+
const cam = this.context.xrCamera;
|
1173
|
+
const cull = this.context.mainCameraComponent?.cullingMask;
|
1174
|
+
if (cam && cull !== undefined) {
|
1175
|
+
for (const c of cam.cameras) {
|
1176
|
+
c.layers.mask = cull;
|
1177
|
+
}
|
1178
|
+
cam.layers.mask = cull;
|
1179
|
+
}
|
1180
|
+
else if (cam) {
|
1181
|
+
for (const c of cam.cameras) {
|
1182
|
+
c.layers.enableAll();
|
1183
|
+
}
|
1184
|
+
cam.layers.enableAll();
|
1185
|
+
}
|
1186
|
+
}
|
1187
|
+
|
1188
|
+
private invokeControllerEvent(controller: NeedleXRController, listeners: ControllerChangedEvt[], change: "added" | "removed") {
|
1189
|
+
for (let i = listeners.length - 1; i >= 0; i--) {
|
1190
|
+
const listener = listeners[i];
|
1191
|
+
if (!listener) continue;
|
1192
|
+
try {
|
1193
|
+
listener({
|
1194
|
+
xr: this,
|
1195
|
+
controller,
|
1196
|
+
change
|
1197
|
+
});
|
1198
|
+
}
|
1199
|
+
catch (e) {
|
1200
|
+
console.error(e);
|
1201
|
+
}
|
1202
|
+
}
|
1203
|
+
}
|
1204
|
+
|
1205
|
+
|
1206
|
+
private _camera!: Object3D;
|
1207
|
+
private readonly _cameraRenderParent: Object3D = new Object3D().rotateY(Math.PI);
|
1208
|
+
private _previousCameraParent!: Object3D | null;
|
1209
|
+
private readonly _customforward: boolean = true;
|
1210
|
+
private originalCameraNearPlane?: number;
|
1211
|
+
/** This is used to have the XR system camera look into threejs Z forward direction (instead of -z) */
|
1212
|
+
private applyCustomForward() {
|
1213
|
+
if (this.context.mainCamera && this._customforward) {
|
1214
|
+
this._camera = this.context.mainCamera;
|
1215
|
+
if (this._camera.parent !== this._cameraRenderParent) {
|
1216
|
+
this._previousCameraParent = this._camera.parent;
|
1217
|
+
this._previousCameraParent?.add(this._cameraRenderParent);
|
1218
|
+
}
|
1219
|
+
this._cameraRenderParent.name = "XR Camera Render Parent";
|
1220
|
+
this._cameraRenderParent.add(this._camera);
|
1221
|
+
|
1222
|
+
let minNearPlane = .02;
|
1223
|
+
if (this.rig) {
|
1224
|
+
const rigWorldScale = getWorldScale(this.rig.gameObject);
|
1225
|
+
minNearPlane *= rigWorldScale.x;
|
1226
|
+
}
|
1227
|
+
if (this._camera instanceof PerspectiveCamera && this._camera.near > minNearPlane) {
|
1228
|
+
this.originalCameraNearPlane = this._camera.near;
|
1229
|
+
this._camera.near = minNearPlane;
|
1230
|
+
}
|
1231
|
+
}
|
1232
|
+
}
|
1233
|
+
private revertCustomForward() {
|
1234
|
+
if (this._camera && this._previousCameraParent) {
|
1235
|
+
this._previousCameraParent.add(this._camera);
|
1236
|
+
}
|
1237
|
+
this._previousCameraParent = null;
|
1238
|
+
|
1239
|
+
if (this._camera instanceof PerspectiveCamera && this.originalCameraNearPlane != undefined) {
|
1240
|
+
this._camera.near = this.originalCameraNearPlane;
|
1241
|
+
}
|
1242
|
+
}
|
1243
|
+
|
1244
|
+
|
1245
|
+
private _viewerPose?: XRViewerPose;
|
1246
|
+
private readonly _transformOrientation = new Quaternion();
|
1247
|
+
private readonly _transformPosition = new Vector3();
|
1248
|
+
|
1249
|
+
private internalUpdateState() {
|
1250
|
+
const referenceSpace = this.context.renderer.xr.getReferenceSpace();
|
1251
|
+
if (!referenceSpace) {
|
1252
|
+
this._viewerPose = undefined;
|
1253
|
+
return;
|
1254
|
+
}
|
1255
|
+
this._viewerPose = this.frame.getViewerPose(referenceSpace);
|
1256
|
+
if (this._viewerPose) {
|
1257
|
+
const transform: XRRigidTransform = this._viewerPose.transform;
|
1258
|
+
this._transformPosition.set(transform.position.x, transform.position.y, transform.position.z);
|
1259
|
+
this._transformOrientation.set(transform.orientation.x, transform.orientation.y, transform.orientation.z, transform.orientation.w);
|
1260
|
+
}
|
1261
|
+
}
|
1262
|
+
|
1263
|
+
// TODO: for scene transitions (e.g. SceneSwitcher) where creating the scene might take a few moments we might want more control over when/how this fading occurs and how long the scene stays black
|
1264
|
+
private _transition?: SceneTransition;
|
1265
|
+
|
1266
|
+
public get transition() {
|
1267
|
+
if (!this._transition) this._transition = new SceneTransition();
|
1268
|
+
return this._transition;
|
1269
|
+
}
|
1270
|
+
|
1271
|
+
/** Call to fade rendering to black for a short moment (the returned promise will be resolved when fully black)
|
1272
|
+
* This can be used to mask scene transitions or teleportation
|
1273
|
+
* @returns a promise that is resolved when the screen is fully black
|
1274
|
+
* @example `fadeTransition().then(() => { <fully_black> })`
|
1275
|
+
*/
|
1276
|
+
fadeTransition() {
|
1277
|
+
if (!this._transition) this._transition = new SceneTransition();
|
1278
|
+
return this._transition.fadeTransition();
|
1279
|
+
}
|
1280
|
+
|
1281
|
+
/** e.g. FadeToBlack */
|
1282
|
+
private updateFade(camera: Camera) {
|
1283
|
+
if (this._transition && camera instanceof PerspectiveCamera)
|
1284
|
+
this._transition.update(camera, this.context.time.deltaTime);
|
1285
|
+
}
|
1286
|
+
|
1287
|
+
private onUpdateFade_PostRender() {
|
1288
|
+
this._transition?.remove();
|
1289
|
+
}
|
1290
|
+
}
|
@@ -0,0 +1,221 @@
|
|
1
|
+
import type { Context } from "../engine_context.js";
|
2
|
+
import { RoomEvents, UserJoinedOrLeftRoomModel } from "../engine_networking.js";
|
3
|
+
import { getParam } from "../engine_utils.js";
|
4
|
+
import { NeedleXRController } from "./NeedleXRController.js";
|
5
|
+
import { NeedleXRSession } from "./NeedleXRSession.js";
|
6
|
+
|
7
|
+
const debug = getParam("debugwebxr");
|
8
|
+
|
9
|
+
|
10
|
+
declare type XRControllerType = "hand" | "controller";
|
11
|
+
|
12
|
+
declare type XRControllerState = {
|
13
|
+
// adding a guid so it's saved on the server, ideally we have a "room lifetime" store that doesnt save state forever on disc but just until the room is disposed (we have to add support for this in the networking backend tho)
|
14
|
+
guid: string;
|
15
|
+
index: number;
|
16
|
+
handedness: XRHandedness;
|
17
|
+
isTracking: boolean;
|
18
|
+
type: XRControllerType;
|
19
|
+
}
|
20
|
+
|
21
|
+
class XRUserState {
|
22
|
+
|
23
|
+
readonly controllerStates: XRControllerState[] = [];
|
24
|
+
|
25
|
+
readonly userId: string;
|
26
|
+
readonly context: Context;
|
27
|
+
|
28
|
+
private readonly userStateEvtName: string;
|
29
|
+
|
30
|
+
constructor(userId: string, context: Context) {
|
31
|
+
this.userId = userId;
|
32
|
+
this.context = context;
|
33
|
+
this.userStateEvtName = "xr-sync-user-state-" + userId;
|
34
|
+
this.context.connection.beginListen(this.userStateEvtName, this.onReceivedControllerState);
|
35
|
+
}
|
36
|
+
|
37
|
+
dispose() {
|
38
|
+
this.context.connection.stopListen(this.userStateEvtName, this.onReceivedControllerState);
|
39
|
+
}
|
40
|
+
|
41
|
+
onReceivedControllerState = (state: XRControllerState) => {
|
42
|
+
if (debug) console.log(`XRSync: Received change for ${this.userId}: ${state.type} ${state.handedness}; tracked=${state.isTracking}`);
|
43
|
+
|
44
|
+
let found = false;
|
45
|
+
for (let i = 0; i < this.controllerStates.length; i++) {
|
46
|
+
const ctrl = this.controllerStates[i];
|
47
|
+
if (ctrl.index === state.index) {
|
48
|
+
this.controllerStates[i] = state;
|
49
|
+
found = true;
|
50
|
+
break;
|
51
|
+
}
|
52
|
+
}
|
53
|
+
if (!found) {
|
54
|
+
this.controllerStates.push(state);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
update(session: NeedleXRSession) {
|
59
|
+
if (this.context.connection.isConnected == false) return;
|
60
|
+
|
61
|
+
for (let i = this.controllerStates.length - 1; i >= 0; i--) {
|
62
|
+
const state = this.controllerStates[i];
|
63
|
+
let foundController = false;
|
64
|
+
for (let i = 0; i < session.controllers.length; i++) {
|
65
|
+
const ctrl = session.controllers[i];
|
66
|
+
if (ctrl.index === state.index) {
|
67
|
+
foundController = true;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
if (!foundController) {
|
71
|
+
// controller was removed
|
72
|
+
if (debug) console.log(`XRSync: ${state.type} ${state.handedness} removed`, state.index);
|
73
|
+
this.controllerStates.splice(i, 1);
|
74
|
+
this.sendControllerRemoved(state);
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
for (const ctrl of session.controllers) {
|
79
|
+
this.updateControllerStates(ctrl);
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
onExitXR(_session: NeedleXRSession) {
|
84
|
+
for (const state of this.controllerStates) {
|
85
|
+
this.sendControllerRemoved(state);
|
86
|
+
}
|
87
|
+
this.controllerStates.length = 0;
|
88
|
+
}
|
89
|
+
|
90
|
+
private sendControllerRemoved(state: XRControllerState) {
|
91
|
+
state.isTracking = false;
|
92
|
+
state.guid = "";
|
93
|
+
this.context.connection.send(this.userStateEvtName, state);
|
94
|
+
this.context.connection.sendDeleteRemoteState(state.guid);
|
95
|
+
}
|
96
|
+
|
97
|
+
private updateControllerStates(ctrl: NeedleXRController) {
|
98
|
+
|
99
|
+
// this.context.connection.send(this.userStateEvtName, {});
|
100
|
+
const existing = this.controllerStates.find(x => x.index === ctrl.index);
|
101
|
+
if (existing) {
|
102
|
+
let hasChanged = false;
|
103
|
+
hasChanged ||= existing.isTracking != ctrl.isTracking;
|
104
|
+
if (hasChanged) {
|
105
|
+
existing.isTracking = ctrl.isTracking;
|
106
|
+
this.context.connection.send(this.userStateEvtName, existing);
|
107
|
+
}
|
108
|
+
}
|
109
|
+
else {
|
110
|
+
const state: XRControllerState = {
|
111
|
+
guid: this.userId + "-" + ctrl.index,
|
112
|
+
isTracking: ctrl.isTracking,
|
113
|
+
handedness: ctrl.side,
|
114
|
+
index: ctrl.index,
|
115
|
+
type: ctrl.hand ? "hand" : "controller"
|
116
|
+
}
|
117
|
+
this.controllerStates.push(state);
|
118
|
+
this.context.connection.send(this.userStateEvtName, state);
|
119
|
+
if (debug) console.log(`XRSync: ${state.type} ${state.handedness} added`, state.index);
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
|
124
|
+
}
|
125
|
+
|
126
|
+
export class NeedleXRSync {
|
127
|
+
|
128
|
+
hasState(userId: string | null | undefined) {
|
129
|
+
if (!userId) return false;
|
130
|
+
return this._states.has(userId);
|
131
|
+
}
|
132
|
+
|
133
|
+
/** Is the left controller or hand tracked */
|
134
|
+
isTracking(userId: string | null | undefined, handedness: XRHandedness): boolean | undefined {
|
135
|
+
if (!userId) return undefined;
|
136
|
+
const user = this._states.get(userId);
|
137
|
+
if (!user) return undefined;
|
138
|
+
const ctrl = user.controllerStates.find(x => x.handedness === handedness);
|
139
|
+
return ctrl?.isTracking || false;
|
140
|
+
}
|
141
|
+
|
142
|
+
/** Is it hand tracking or a controller */
|
143
|
+
getDeviceType(userId: string, handedness: XRHandedness): XRControllerType | undefined | "unknown" {
|
144
|
+
if (!userId) return undefined;
|
145
|
+
const user = this._states.get(userId);
|
146
|
+
if (!user) return undefined;
|
147
|
+
const ctrl = user.controllerStates.find(x => x.handedness === handedness);
|
148
|
+
return ctrl?.type || "unknown";
|
149
|
+
}
|
150
|
+
|
151
|
+
private readonly context: Context;
|
152
|
+
|
153
|
+
constructor(context: Context) {
|
154
|
+
this.context = context;
|
155
|
+
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
|
156
|
+
this.context.connection.beginListen(RoomEvents.LeftRoom, this.onLeftRoom)
|
157
|
+
this.context.connection.beginListen(RoomEvents.UserJoinedRoom, this.onOtherUserJoinedRoom);
|
158
|
+
this.context.connection.beginListen(RoomEvents.UserLeftRoom, this.onOtherUserLeftRoom);
|
159
|
+
}
|
160
|
+
destroy() {
|
161
|
+
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
|
162
|
+
this.context.connection.stopListen(RoomEvents.LeftRoom, this.onLeftRoom)
|
163
|
+
this.context.connection.stopListen(RoomEvents.UserJoinedRoom, this.onOtherUserJoinedRoom);
|
164
|
+
this.context.connection.stopListen(RoomEvents.UserLeftRoom, this.onOtherUserLeftRoom);
|
165
|
+
}
|
166
|
+
|
167
|
+
private onJoinedRoom = () => {
|
168
|
+
if (this.context.connection.connectionId) {
|
169
|
+
if (!this._states.has(this.context.connection.connectionId)) {
|
170
|
+
if (debug) console.log("XRSync: Local user joined room", this.context.connection.connectionId);
|
171
|
+
this._states.set(this.context.connection.connectionId, new XRUserState(this.context.connection.connectionId, this.context));
|
172
|
+
}
|
173
|
+
for (const user of this.context.connection.usersInRoom()) {
|
174
|
+
if (!this._states.has(user)) {
|
175
|
+
this._states.set(user, new XRUserState(user, this.context));
|
176
|
+
}
|
177
|
+
}
|
178
|
+
}
|
179
|
+
}
|
180
|
+
private onLeftRoom = () => {
|
181
|
+
if (this.context.connection.connectionId) {
|
182
|
+
if (!this._states.has(this.context.connection.connectionId)) {
|
183
|
+
const state = this._states.get(this.context.connection.connectionId);
|
184
|
+
state?.dispose();
|
185
|
+
this._states.delete(this.context.connection.connectionId);
|
186
|
+
}
|
187
|
+
}
|
188
|
+
}
|
189
|
+
private onOtherUserJoinedRoom = (evt: UserJoinedOrLeftRoomModel) => {
|
190
|
+
const userId = evt.userId;
|
191
|
+
if (!this._states.has(userId)) {
|
192
|
+
if (debug) console.log("XRSync: Remote user joined room", userId);
|
193
|
+
this._states.set(userId, new XRUserState(userId, this.context));
|
194
|
+
}
|
195
|
+
}
|
196
|
+
private onOtherUserLeftRoom = (evt: UserJoinedOrLeftRoomModel) => {
|
197
|
+
const userId = evt.userId;
|
198
|
+
if (!this._states.has(userId)) {
|
199
|
+
const state = this._states.get(userId);
|
200
|
+
state?.dispose();
|
201
|
+
this._states.delete(userId);
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
private _states: Map<string, XRUserState> = new Map();
|
206
|
+
|
207
|
+
onUpdate(session: NeedleXRSession) {
|
208
|
+
if (this.context.connection.isConnected && this.context.connection.connectionId) {
|
209
|
+
const localState = this._states.get(this.context.connection.connectionId);
|
210
|
+
localState?.update(session);
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
onExitXR(session: NeedleXRSession) {
|
215
|
+
if (this.context.connection.isConnected && this.context.connection.connectionId) {
|
216
|
+
const localState = this._states.get(this.context.connection.connectionId);
|
217
|
+
localState?.onExitXR(session);
|
218
|
+
}
|
219
|
+
}
|
220
|
+
|
221
|
+
}
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import { Camera, DoubleSide, Mesh, MeshBasicMaterial, PlaneGeometry } from "three";
|
2
|
+
|
3
|
+
import { Mathf } from "../engine_math.js";
|
4
|
+
|
5
|
+
export class SceneTransition {
|
6
|
+
|
7
|
+
private readonly _fadeToColorQuad: Mesh;
|
8
|
+
private readonly _fadeToColorMaterial: MeshBasicMaterial;
|
9
|
+
|
10
|
+
constructor() {
|
11
|
+
this._fadeToColorMaterial = new MeshBasicMaterial({
|
12
|
+
color: 0x000000,
|
13
|
+
transparent: true,
|
14
|
+
depthTest: false,
|
15
|
+
fog: false,
|
16
|
+
side: DoubleSide,
|
17
|
+
});
|
18
|
+
this._fadeToColorQuad = new Mesh(new PlaneGeometry(10, 10), this._fadeToColorMaterial);
|
19
|
+
}
|
20
|
+
|
21
|
+
dispose() {
|
22
|
+
this._fadeToColorQuad.geometry.dispose();
|
23
|
+
this._fadeToColorMaterial.dispose();
|
24
|
+
}
|
25
|
+
|
26
|
+
update(camera: Camera, dt: number) {
|
27
|
+
const quad = this._fadeToColorQuad;
|
28
|
+
const mat = this._fadeToColorMaterial;
|
29
|
+
|
30
|
+
// make sure the quad is in the scene
|
31
|
+
if (quad.parent !== camera && mat.opacity > 0) {
|
32
|
+
camera.add(quad);
|
33
|
+
}
|
34
|
+
else if (mat.opacity === 0) {
|
35
|
+
quad.removeFromParent();
|
36
|
+
}
|
37
|
+
quad.layers.set(2);
|
38
|
+
quad.material = this._fadeToColorMaterial!;
|
39
|
+
quad.position.z = -1;
|
40
|
+
// because of TMUI
|
41
|
+
quad.renderOrder = Infinity;
|
42
|
+
// perform the fade
|
43
|
+
const fadeValue = this._requestedFadeValue;
|
44
|
+
mat.opacity = Mathf.lerp(mat.opacity, fadeValue, dt / .03);
|
45
|
+
|
46
|
+
// check if we're close enough to the desired value:
|
47
|
+
if (Math.abs(mat.opacity - fadeValue) <= .01) {
|
48
|
+
if (this._transitionResolve) {
|
49
|
+
this._transitionResolve();
|
50
|
+
this._transitionResolve = null;
|
51
|
+
this._transitionPromise = null;
|
52
|
+
this._requestedFadeValue = 0;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
remove() {
|
57
|
+
this._fadeToColorQuad.removeFromParent();
|
58
|
+
}
|
59
|
+
|
60
|
+
/** Call to fade rendering to black for a short moment (the returned promise will be resolved when fully black)
|
61
|
+
* This can be used to mask scene transitions or teleportation
|
62
|
+
* @returns a promise that is resolved when the screen is fully black
|
63
|
+
* @example `fadeTransition().then(() => { <fully_black> })`
|
64
|
+
*/
|
65
|
+
fadeTransition() {
|
66
|
+
if (this._transitionPromise) return this._transitionPromise;
|
67
|
+
this._requestedFadeValue = 1;
|
68
|
+
const promise = new Promise<void>(resolve => {
|
69
|
+
this._transitionResolve = resolve;
|
70
|
+
});
|
71
|
+
this._transitionPromise = promise;
|
72
|
+
return promise;
|
73
|
+
}
|
74
|
+
|
75
|
+
|
76
|
+
private _requestedFadeValue: number = 0;
|
77
|
+
private _transitionPromise: Promise<void> | null = null;
|
78
|
+
private _transitionResolve: (() => void) | null = null;
|
79
|
+
}
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import { Behaviour } from "../Component.js";
|
2
|
+
|
3
|
+
/** This component is just used as a marker on objects for WebXR teleportation
|
4
|
+
* The default XRControllerMovement script checks if this component is on the object you are pointing at and if so it will teleport to that location if triggered
|
5
|
+
* If the component is not present it won't teleport
|
6
|
+
*/
|
7
|
+
export class TeleportTarget extends Behaviour {
|
8
|
+
|
9
|
+
}
|
@@ -0,0 +1,183 @@
|
|
1
|
+
import { AxesHelper, Camera, Color, DirectionalLight, GridHelper, Mesh, MeshBasicMaterial, MeshStandardMaterial, PerspectiveCamera, Scene, WebGLRenderer } from "three";
|
2
|
+
|
3
|
+
import { ObjectUtils, PrimitiveType } from "../engine_create_objects.js";
|
4
|
+
import { Mathf } from "../engine_math.js";
|
5
|
+
import { delay } from "../engine_utils.js";
|
6
|
+
|
7
|
+
declare type SessionInfo = { session: XRSession, mode: XRSessionMode, init: XRSessionInit };
|
8
|
+
|
9
|
+
/** Create with static `start`- used to start an XR session while waiting for session granted */
|
10
|
+
export class TemporaryXRContext {
|
11
|
+
|
12
|
+
private static _active: TemporaryXRContext | null = null;
|
13
|
+
static get active() {
|
14
|
+
return this._active;
|
15
|
+
}
|
16
|
+
|
17
|
+
private static _requestInFlight = false;
|
18
|
+
|
19
|
+
static async start(mode: XRSessionMode, init: XRSessionInit) {
|
20
|
+
if (this._active) {
|
21
|
+
console.error("Cannot start a new XR session while one is already active");
|
22
|
+
return null;
|
23
|
+
}
|
24
|
+
if (this._requestInFlight) {
|
25
|
+
console.error("Cannot start a new XR session while a request is already in flight");
|
26
|
+
return null;
|
27
|
+
}
|
28
|
+
|
29
|
+
if ('xr' in navigator && navigator.xr) {
|
30
|
+
if (!init) {
|
31
|
+
console.error("XRSessionInit must be provided");
|
32
|
+
return null;
|
33
|
+
}
|
34
|
+
this._requestInFlight = true;
|
35
|
+
const session = await navigator.xr.requestSession(mode, init);
|
36
|
+
session.addEventListener("end", () => {
|
37
|
+
this._active = null;
|
38
|
+
});
|
39
|
+
if (!this._requestInFlight) {
|
40
|
+
session.end();
|
41
|
+
return null;
|
42
|
+
}
|
43
|
+
this._requestInFlight = false;
|
44
|
+
this._active = new TemporaryXRContext(mode, init, session);
|
45
|
+
return this._active;
|
46
|
+
}
|
47
|
+
|
48
|
+
return null;
|
49
|
+
}
|
50
|
+
|
51
|
+
static async handoff(): Promise<SessionInfo | null> {
|
52
|
+
if (this._active) {
|
53
|
+
return this._active.handoff();
|
54
|
+
}
|
55
|
+
return null;
|
56
|
+
}
|
57
|
+
|
58
|
+
static async stop() {
|
59
|
+
this._requestInFlight = false;
|
60
|
+
if (this._active) {
|
61
|
+
await this._active.end();
|
62
|
+
await delay(100);
|
63
|
+
}
|
64
|
+
this._active = null;
|
65
|
+
}
|
66
|
+
|
67
|
+
private readonly _session: XRSession | null;
|
68
|
+
private readonly _mode: XRSessionMode;
|
69
|
+
private readonly _init: XRSessionInit;
|
70
|
+
|
71
|
+
private readonly _renderer: WebGLRenderer;
|
72
|
+
private readonly _camera: Camera;
|
73
|
+
private readonly _scene: Scene;
|
74
|
+
|
75
|
+
private constructor(mode: XRSessionMode, init: XRSessionInit, session: XRSession) {
|
76
|
+
this._mode = mode;
|
77
|
+
this._init = init;
|
78
|
+
this._session = session;
|
79
|
+
this._session.addEventListener("end", this.onEnd);
|
80
|
+
|
81
|
+
this._renderer = new WebGLRenderer({ alpha: true });
|
82
|
+
this._renderer.setAnimationLoop(this.onFrame);
|
83
|
+
this._renderer.xr.setSession(session);
|
84
|
+
this._renderer.xr.enabled = true;
|
85
|
+
this._camera = new PerspectiveCamera();
|
86
|
+
this._scene = new Scene();
|
87
|
+
this._scene.add(this._camera);
|
88
|
+
this.setupScene();
|
89
|
+
}
|
90
|
+
|
91
|
+
end() {
|
92
|
+
if (!this._session) return Promise.resolve();
|
93
|
+
return this._session.end();
|
94
|
+
}
|
95
|
+
|
96
|
+
/** returns the session and session info and stops the temporary rendering */
|
97
|
+
async handoff() {
|
98
|
+
if (!this._session) throw new Error("Cannot handoff a session that has already ended");
|
99
|
+
const info: SessionInfo = {
|
100
|
+
session: this._session,
|
101
|
+
mode: this._mode,
|
102
|
+
init: this._init
|
103
|
+
};
|
104
|
+
await this.onBeforeHandoff();
|
105
|
+
// calling onEnd here directly because we dont end the session
|
106
|
+
this.onEnd();
|
107
|
+
// set the session to null because we dont want this class to accidentaly end the session
|
108
|
+
//@ts-ignore
|
109
|
+
this._session = null;
|
110
|
+
return info;
|
111
|
+
}
|
112
|
+
|
113
|
+
private onEnd = () => {
|
114
|
+
this._session?.removeEventListener("end", this.onEnd);
|
115
|
+
this._renderer.setAnimationLoop(null);
|
116
|
+
this._renderer.dispose();
|
117
|
+
this._scene.clear();
|
118
|
+
}
|
119
|
+
|
120
|
+
private _lastTime = 0;
|
121
|
+
private onFrame = (time: DOMHighResTimeStamp, _frame: XRFrame) => {
|
122
|
+
const dt = time - this._lastTime;
|
123
|
+
this.update(time, dt);
|
124
|
+
if (this._camera.parent !== this._scene) {
|
125
|
+
this._scene.add(this._camera);
|
126
|
+
}
|
127
|
+
this._renderer.render(this._scene, this._camera);
|
128
|
+
}
|
129
|
+
|
130
|
+
/** can be used to prepare the user or fade to black */
|
131
|
+
private async onBeforeHandoff() {
|
132
|
+
const obj = ObjectUtils.createPrimitive(PrimitiveType.Cube);
|
133
|
+
obj.position.z = -3;
|
134
|
+
obj.position.y = .5;
|
135
|
+
this._scene.add(obj);
|
136
|
+
await delay(4000);
|
137
|
+
this._scene.clear();
|
138
|
+
await delay(100);
|
139
|
+
}
|
140
|
+
|
141
|
+
|
142
|
+
private _spheres: Mesh[] = [];
|
143
|
+
private setupScene() {
|
144
|
+
this._scene.background = new Color(0x000000);
|
145
|
+
this._scene.add(new GridHelper(5, 10, 0x111111, 0x222222));
|
146
|
+
|
147
|
+
const light = new DirectionalLight(0xffffff, 1);
|
148
|
+
light.position.set(2, 2, 2);
|
149
|
+
light.castShadow = false;
|
150
|
+
this._scene.add(light);
|
151
|
+
|
152
|
+
const light2 = new DirectionalLight(0xffffff, 1);
|
153
|
+
light2.position.set(-2, -2, -2);
|
154
|
+
light2.castShadow = false;
|
155
|
+
this._scene.add(light2);
|
156
|
+
|
157
|
+
const sphereRange = 50;
|
158
|
+
for (let i = 0; i < 100; i++) {
|
159
|
+
const sphere = ObjectUtils.createPrimitive(PrimitiveType.Sphere, {
|
160
|
+
material: new MeshStandardMaterial({
|
161
|
+
color: 0x222222,
|
162
|
+
metalness: 1,
|
163
|
+
roughness: .8,
|
164
|
+
})
|
165
|
+
});
|
166
|
+
sphere.position.x = Mathf.random(-sphereRange, sphereRange);
|
167
|
+
sphere.position.y = Mathf.random(3, 40);
|
168
|
+
sphere.position.z = Mathf.random(-sphereRange, sphereRange);
|
169
|
+
sphere.scale.multiplyScalar(2);
|
170
|
+
this._spheres.push(sphere);
|
171
|
+
this._scene.add(sphere);
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
private update(time: number, _deltaTime: number) {
|
176
|
+
|
177
|
+
const speed = time * .0004;
|
178
|
+
for (let i = 0; i < this._spheres.length; i++) {
|
179
|
+
const sphere = this._spheres[i];
|
180
|
+
sphere.position.y += Math.sin(speed + i * .5) * 0.002;
|
181
|
+
}
|
182
|
+
}
|
183
|
+
}
|
@@ -0,0 +1,4 @@
|
|
1
|
+
|
2
|
+
export interface XRMovementBehaviour {
|
3
|
+
isXRMovementHandler: true;
|
4
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import { Object3D } from "three";
|
2
|
+
|
3
|
+
import { AssetReference } from "../engine_addressables.js";
|
4
|
+
import type { SourceIdentifier } from "../engine_types.js";
|
5
|
+
import { getParam } from "../engine_utils.js";
|
6
|
+
|
7
|
+
const debug = getParam("debugwebxr");
|
8
|
+
|
9
|
+
export class NeedleXRUtils {
|
10
|
+
|
11
|
+
/** Searches the hierarchy for objects following a specific naming scheme */
|
12
|
+
static tryFindAvatarObjects(obj: Object3D, sourceId: SourceIdentifier, result: { head?: AssetReference, leftHand?: AssetReference, rightHand?: AssetReference }) {
|
13
|
+
if (result.head && result.leftHand && result.rightHand) return;
|
14
|
+
|
15
|
+
const name = obj.name.toLocaleLowerCase();
|
16
|
+
|
17
|
+
if (!result.head && name.includes("head")) {
|
18
|
+
if (debug) console.log("FOUND AVATAR HEAD", obj.name)
|
19
|
+
result.head = new AssetReference("", sourceId, obj);
|
20
|
+
}
|
21
|
+
if (name.includes("hand")) {
|
22
|
+
if (!result.leftHand && name.includes("left")) {
|
23
|
+
if (debug) console.log("FOUND AVATAR LEFT HAND", obj.name)
|
24
|
+
result.leftHand = new AssetReference("", sourceId, obj);
|
25
|
+
}
|
26
|
+
if (!result.rightHand && name.includes("right")) {
|
27
|
+
if (debug) console.log("FOUND AVATAR RIGHT HAND", obj.name)
|
28
|
+
result.rightHand = new AssetReference("", sourceId, obj);
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
for (let i = 0; i < obj.children.length; i++) {
|
33
|
+
if (result.head && result.leftHand && result.rightHand) return;
|
34
|
+
const child = obj.children[i];
|
35
|
+
this.tryFindAvatarObjects(child, sourceId, result);
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
|
40
|
+
}
|
@@ -0,0 +1,348 @@
|
|
1
|
+
import { isDevEnvironment } from "../../engine/debug/index.js";
|
2
|
+
import { Context } from "../../engine/engine_context.js";
|
3
|
+
import { generateQRCode } from "../../engine/engine_utils.js";
|
4
|
+
import { isMozillaXR } from "../../engine/engine_utils.js";
|
5
|
+
import { NeedleXRSession } from "../../engine/engine_xr.js";
|
6
|
+
import { GameObject } from "../Component.js";
|
7
|
+
import { USDZExporter } from "../export/usdz/USDZExporter.js";
|
8
|
+
|
9
|
+
const webXRElementName = "needle-webxr-buttons";
|
10
|
+
|
11
|
+
// TODO: add feedback process to the buttons when XR session is requested/pending... perhaps we need to expose an event on the NeedleXRSession class for this
|
12
|
+
|
13
|
+
export class NeedleWebXRHtmlElement extends HTMLElement {
|
14
|
+
|
15
|
+
static create() {
|
16
|
+
return document.createElement(webXRElementName) as NeedleWebXRHtmlElement;
|
17
|
+
}
|
18
|
+
|
19
|
+
static getOrCreate(context: Context) {
|
20
|
+
const domElement = context.domElement;
|
21
|
+
let el = domElement.querySelector(webXRElementName);
|
22
|
+
if (!el) {
|
23
|
+
el = NeedleWebXRHtmlElement.create();
|
24
|
+
domElement.appendChild(el);
|
25
|
+
};
|
26
|
+
return el as NeedleWebXRHtmlElement;
|
27
|
+
}
|
28
|
+
|
29
|
+
private readonly root: HTMLElement;
|
30
|
+
|
31
|
+
constructor() {
|
32
|
+
super();
|
33
|
+
this.attachShadow({ mode: 'open' });
|
34
|
+
const template = document.createElement('template');
|
35
|
+
template.innerHTML = `<style>
|
36
|
+
:host {
|
37
|
+
position: absolute;
|
38
|
+
display: flex;
|
39
|
+
flex-wrap: wrap;
|
40
|
+
justify-content: center;
|
41
|
+
/** increase z-index (nipplejs has 999 as default) */
|
42
|
+
z-index: 5000;
|
43
|
+
width: 100%;
|
44
|
+
bottom: 100px;
|
45
|
+
left: 50%;
|
46
|
+
transform: translateX(-50%);
|
47
|
+
}
|
48
|
+
:host button {
|
49
|
+
font-family: Roboto, sans-serif, Arial;
|
50
|
+
border: none;
|
51
|
+
color: black;
|
52
|
+
background: rgba(255, 255, 255, 1);
|
53
|
+
margin: 5px 5px;
|
54
|
+
padding: 0.5rem .7rem;
|
55
|
+
font-size: 1rem;
|
56
|
+
white-space: nowrap;
|
57
|
+
transition: all 0.2s ease-in-out;
|
58
|
+
border-radius: .2rem;
|
59
|
+
border: rgba(255, 255, 255, 0.2) solid 1px;
|
60
|
+
box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
|
61
|
+
font-weight: normal;
|
62
|
+
}
|
63
|
+
:host button:hover {
|
64
|
+
cursor: pointer;
|
65
|
+
background: rgba(255, 255, 255, 1);
|
66
|
+
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1), 0 0 15px rgba(255, 255, 255, 0.2);
|
67
|
+
transition: all 0.1s ease-in-out;
|
68
|
+
}
|
69
|
+
:host button:disabled {
|
70
|
+
background: rgba(200, 200, 200, 1);
|
71
|
+
color: rgba(100, 100, 100, 1);
|
72
|
+
border: rgba(0,0,0,0) 1px solid;
|
73
|
+
box-shadow: none;
|
74
|
+
cursor: initial;
|
75
|
+
}
|
76
|
+
:host button.this-mode-is-requested {
|
77
|
+
font-weight: bold;
|
78
|
+
background: repeating-linear-gradient(to right, #fff 0%, #fff 40%, #aaffff 55%, #fff 80%);
|
79
|
+
background-size: 200% auto;
|
80
|
+
background-position: 0 100%;
|
81
|
+
animation: AnimationName .7s ease infinite forwards;
|
82
|
+
}
|
83
|
+
:host button.other-mode-is-requested {
|
84
|
+
}
|
85
|
+
|
86
|
+
@keyframes AnimationName {
|
87
|
+
0% { background-position: 0% 0 }
|
88
|
+
100% { background-position: -200% 0 }
|
89
|
+
}
|
90
|
+
|
91
|
+
:host .qr-code-container {
|
92
|
+
position: absolute;
|
93
|
+
display: initial;
|
94
|
+
bottom: 100%;
|
95
|
+
left: 50%;
|
96
|
+
transform: translateX(-50%) translateY(-10px);
|
97
|
+
background-color: white;
|
98
|
+
padding: 1.2rem;
|
99
|
+
border-radius: 0.4rem;
|
100
|
+
pointer-events: all;
|
101
|
+
opacity: 1;
|
102
|
+
transition: opacity 0.2s ease-in-out;
|
103
|
+
box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
|
104
|
+
}
|
105
|
+
|
106
|
+
:host .qr-code-container img {
|
107
|
+
max-width: calc(min(100vw, 300px) - 20px);
|
108
|
+
}
|
109
|
+
|
110
|
+
:host .qr-code-container.hidden {
|
111
|
+
opacity: 0;
|
112
|
+
display: none; /* prevents the QR code from overflowing the body when it's actually disabled but breaks animation */
|
113
|
+
pointer-events: none;
|
114
|
+
}
|
115
|
+
</style>
|
116
|
+
`;
|
117
|
+
|
118
|
+
this.root = document.createElement("div");
|
119
|
+
if (window.location.protocol !== "https:") {
|
120
|
+
this.root.classList.add("needs-https");
|
121
|
+
}
|
122
|
+
if (this.shadowRoot) {
|
123
|
+
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
124
|
+
this.shadowRoot.appendChild(this.root);
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
private get isSecureConnection() { return window.location.protocol === "https:"; }
|
129
|
+
|
130
|
+
/** @returns the quicklook button if it was created */
|
131
|
+
get quicklookButton() { return this.shadowRoot?.querySelector("[data-needle='quicklook-button']") as HTMLButtonElement | null; }
|
132
|
+
/** get or create the quicklook button
|
133
|
+
* Behaviour of the button:
|
134
|
+
* - if the button is clicked a USDZExporter component will be searched for in the scene and if found, it will be used to export the scene to USDZ / Quicklook
|
135
|
+
*/
|
136
|
+
createQuicklookButton(): HTMLButtonElement {
|
137
|
+
const existingButton = this.shadowRoot?.querySelector("[data-needle='quicklook-button']") as HTMLButtonElement | null;
|
138
|
+
if (existingButton) return existingButton;
|
139
|
+
const button = document.createElement("button");
|
140
|
+
button.dataset["needle"] = "quicklook-button";
|
141
|
+
button.innerText = "Open in Quicklook";
|
142
|
+
button.addEventListener("click", () => {
|
143
|
+
const usdzExporter = GameObject.findObjectOfType(USDZExporter);
|
144
|
+
if (usdzExporter) {
|
145
|
+
usdzExporter.exportAsync();
|
146
|
+
}
|
147
|
+
else {
|
148
|
+
console.warn("No USDZExporter component found in the scene");
|
149
|
+
}
|
150
|
+
});
|
151
|
+
this.hideElementDuringXRSession(button);
|
152
|
+
this.root?.appendChild(button);
|
153
|
+
return button;
|
154
|
+
}
|
155
|
+
|
156
|
+
/** @returns the WebXR AR button if it was created */
|
157
|
+
get arButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-ar-button']") as HTMLButtonElement | null; }
|
158
|
+
/** get or create the WebXR AR button
|
159
|
+
* @param init optional session init options
|
160
|
+
* Behaviour of the button:
|
161
|
+
* - if the device supports AR, the button will be visible and clickable
|
162
|
+
* - if the device does not support AR, the button will be hidden
|
163
|
+
* - if the device changes and now supports AR, the button will be visible
|
164
|
+
*/
|
165
|
+
createARButton(init?: XRSessionInit): HTMLButtonElement {
|
166
|
+
const existingButton = this.shadowRoot?.querySelector("[data-needle='webxr-ar-button']") as HTMLButtonElement | null;
|
167
|
+
if (existingButton) return existingButton;
|
168
|
+
const mode: XRSessionMode = "immersive-ar";
|
169
|
+
const button = document.createElement("button");
|
170
|
+
button.classList.add("webxr-button");
|
171
|
+
button.dataset["needle"] = "webxr-ar-button";
|
172
|
+
button.innerText = "Enter AR";
|
173
|
+
button.title = "Click to start a WebXR session in AR";
|
174
|
+
button.addEventListener("click", () => NeedleXRSession.start(mode, init));
|
175
|
+
this.updateSessionSupported(button, mode);
|
176
|
+
this.listenToXRSessionState(button, mode);
|
177
|
+
this.hideElementDuringXRSession(button);
|
178
|
+
this.root?.appendChild(button);
|
179
|
+
|
180
|
+
if (!this.isSecureConnection) {
|
181
|
+
button.disabled = true;
|
182
|
+
button.title = "WebXR requires a secure connection (HTTPS)";
|
183
|
+
}
|
184
|
+
|
185
|
+
if (!isMozillaXR()) // WebXR Viewer can't attach events before session start
|
186
|
+
navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode));
|
187
|
+
|
188
|
+
return button;
|
189
|
+
}
|
190
|
+
|
191
|
+
/** @returns the WebXR VR button if it was created */
|
192
|
+
get vrButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-vr-button']") as HTMLButtonElement | null; }
|
193
|
+
/** get or create the WebXR VR button
|
194
|
+
* @param init optional session init options
|
195
|
+
* Behaviour of the button:
|
196
|
+
* - if the device supports VR, the button will be visible and clickable
|
197
|
+
* - if the device does not support VR, the button will be hidden
|
198
|
+
* - if the device changes and now supports VR, the button will be visible
|
199
|
+
*/
|
200
|
+
createVRButton(init?: XRSessionInit): HTMLButtonElement {
|
201
|
+
const hasButton = this.shadowRoot?.querySelector("[data-needle='webxr-vr-button']");
|
202
|
+
if (hasButton) return hasButton as HTMLButtonElement;
|
203
|
+
const mode: XRSessionMode = "immersive-vr";
|
204
|
+
const button = document.createElement("button");
|
205
|
+
button.classList.add("webxr-button");
|
206
|
+
button.dataset["needle"] = "webxr-vr-button";
|
207
|
+
button.innerText = "Enter VR";
|
208
|
+
button.title = "Click to start a WebXR session in VR";
|
209
|
+
button.addEventListener("click", () => NeedleXRSession.start(mode, init));
|
210
|
+
this.updateSessionSupported(button, mode);
|
211
|
+
this.listenToXRSessionState(button, mode);
|
212
|
+
this.hideElementDuringXRSession(button);
|
213
|
+
this.root?.appendChild(button);
|
214
|
+
|
215
|
+
if (!this.isSecureConnection) {
|
216
|
+
button.disabled = true;
|
217
|
+
button.title = "WebXR requires a secure connection (HTTPS)";
|
218
|
+
}
|
219
|
+
|
220
|
+
if (!isMozillaXR()) // WebXR Viewer can't attach events before session start
|
221
|
+
navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode));
|
222
|
+
|
223
|
+
return button;
|
224
|
+
}
|
225
|
+
|
226
|
+
/** @returns the Send to Quest button */
|
227
|
+
get sendToQuestButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-sendtoquest-button']") as HTMLButtonElement | null; }
|
228
|
+
/** get or create the Send To Quest button
|
229
|
+
* Behaviour of the button:
|
230
|
+
* - if the button is clicked, the current URL will be sent to the Oculus Browser on the Quest
|
231
|
+
*/
|
232
|
+
createSendToQuestButton(): HTMLButtonElement {
|
233
|
+
const hasButton = this.shadowRoot?.querySelector("[data-needle='webxr-sendtoquest-button']");
|
234
|
+
if (hasButton) return hasButton as HTMLButtonElement;
|
235
|
+
const baseUrl = `https://oculus.com/open_url/?url=`
|
236
|
+
const button = document.createElement("button");
|
237
|
+
button.dataset["needle"] = "webxr-sendtoquest-button";
|
238
|
+
button.innerText = "Open on Quest";
|
239
|
+
button.title = "Click to send this page to the Oculus Browser on your Quest";
|
240
|
+
button.addEventListener("click", () => {
|
241
|
+
const urlParameter = encodeURIComponent(window.location.href);
|
242
|
+
window.open(baseUrl + urlParameter);
|
243
|
+
});
|
244
|
+
this.listenToXRSessionState(button);
|
245
|
+
this.hideElementDuringXRSession(button);
|
246
|
+
// make sure to hide the button when we have VR support directly on the device
|
247
|
+
if (!isMozillaXR()) { // WebXR Viewer can't attach events before session start
|
248
|
+
navigator.xr?.addEventListener("devicechange", () => {
|
249
|
+
if (navigator.xr?.isSessionSupported("immersive-vr")) {
|
250
|
+
button.style.display = "none";
|
251
|
+
}
|
252
|
+
else {
|
253
|
+
button.style.display = "";
|
254
|
+
}
|
255
|
+
});
|
256
|
+
}
|
257
|
+
this.root?.appendChild(button);
|
258
|
+
return button;
|
259
|
+
}
|
260
|
+
|
261
|
+
async createQRCode() {
|
262
|
+
const wrapper = document.createElement("div");
|
263
|
+
wrapper.style.position = "relative";
|
264
|
+
wrapper.style.display = "inline-block";
|
265
|
+
this.hideElementDuringXRSession(wrapper);
|
266
|
+
|
267
|
+
const qrCodeContainer = document.createElement("div");
|
268
|
+
qrCodeContainer.classList.add("qr-code-container");
|
269
|
+
qrCodeContainer.classList.add("hidden");
|
270
|
+
generateAndInsertQRCode();
|
271
|
+
|
272
|
+
const qrCodeButton = document.createElement("button");
|
273
|
+
qrCodeButton.innerText = "QR Code";
|
274
|
+
qrCodeButton.title = "Scan this QR code with your phone to open this page";
|
275
|
+
|
276
|
+
qrCodeButton.addEventListener("click", () => {
|
277
|
+
qrCodeContainer.classList.toggle("hidden");
|
278
|
+
if (qrCodeContainer.classList.contains("hidden")) return;
|
279
|
+
// generate the qr code when the button is clicked - this ensures that we get the QRcode with the latest URL
|
280
|
+
generateAndInsertQRCode();
|
281
|
+
});
|
282
|
+
async function generateAndInsertQRCode() {
|
283
|
+
const size = 200;
|
284
|
+
const code = await generateQRCode({
|
285
|
+
text: window.location.href,
|
286
|
+
width: size,
|
287
|
+
height: size,
|
288
|
+
});
|
289
|
+
qrCodeContainer.innerHTML = "";
|
290
|
+
qrCodeContainer.appendChild(code);
|
291
|
+
}
|
292
|
+
|
293
|
+
wrapper.appendChild(qrCodeButton);
|
294
|
+
wrapper.appendChild(qrCodeContainer);
|
295
|
+
|
296
|
+
this.root?.appendChild(wrapper);
|
297
|
+
}
|
298
|
+
|
299
|
+
private updateSessionSupported(button: HTMLButtonElement, mode: XRSessionMode) {
|
300
|
+
if (!navigator.xr) {
|
301
|
+
button.style.display = "none";
|
302
|
+
return;
|
303
|
+
}
|
304
|
+
navigator.xr.isSessionSupported(mode).then(supported => {
|
305
|
+
button.style.display = !supported ? "none" : "";
|
306
|
+
if (isDevEnvironment() && !supported) console.warn(mode + " is not supported on this device - make sure your server runs using HTTPS and you have a device connected that supports " + mode);
|
307
|
+
});
|
308
|
+
}
|
309
|
+
|
310
|
+
private hideElementDuringXRSession(element: HTMLElement) {
|
311
|
+
NeedleXRSession.onXRSessionStart(_ => {
|
312
|
+
element["previous-display"] = element.style.display;
|
313
|
+
element.style.display = "none";
|
314
|
+
});
|
315
|
+
NeedleXRSession.onXRSessionEnd(_ => {
|
316
|
+
if (element["previous-display"] != undefined)
|
317
|
+
element.style.display = element["previous-display"];
|
318
|
+
});
|
319
|
+
}
|
320
|
+
|
321
|
+
private listenToXRSessionState(button: HTMLButtonElement, mode?: XRSessionMode) {
|
322
|
+
|
323
|
+
if (mode) {
|
324
|
+
NeedleXRSession.onSessionRequestStart(args => {
|
325
|
+
if (args.mode === mode) {
|
326
|
+
button.classList.add("this-mode-is-requested");
|
327
|
+
// button["original-text"] = button.innerText;
|
328
|
+
// let modeText = mode === "immersive-vr" ? "VR" : "AR";
|
329
|
+
// button.innerText = "Starting " + modeText + "...";
|
330
|
+
}
|
331
|
+
else {
|
332
|
+
button["was-disabled"] = button.disabled;
|
333
|
+
button.disabled = true;
|
334
|
+
button.classList.add("other-mode-is-requested");
|
335
|
+
}
|
336
|
+
});
|
337
|
+
NeedleXRSession.onSessionRequestEnd(_ => {
|
338
|
+
button.classList.remove("this-mode-is-requested");
|
339
|
+
button.classList.remove("other-mode-is-requested");
|
340
|
+
button.disabled = button["was-disabled"];
|
341
|
+
// button.innerText = button["original-text"];
|
342
|
+
});
|
343
|
+
}
|
344
|
+
}
|
345
|
+
}
|
346
|
+
|
347
|
+
if (!customElements.get(webXRElementName))
|
348
|
+
customElements.define(webXRElementName, NeedleWebXRHtmlElement);
|
@@ -0,0 +1,67 @@
|
|
1
|
+
|
2
|
+
import { serializable } from "../../../engine/engine_serialization_decorator.js";
|
3
|
+
import { NeedleXREventArgs } from "../../../engine/engine_xr.js";
|
4
|
+
import { Behaviour } from "../../Component.js";
|
5
|
+
|
6
|
+
|
7
|
+
/** Add this script to an object and set `side` to make the object follow a specific controller */
|
8
|
+
export class XRControllerFollow extends Behaviour {
|
9
|
+
|
10
|
+
// override active and enabled here so that we always receive xr update events
|
11
|
+
get activeAndEnabled() {
|
12
|
+
return true;
|
13
|
+
}
|
14
|
+
|
15
|
+
/** should this object follow a right hand/controller or left hand/controller */
|
16
|
+
@serializable()
|
17
|
+
side: XRHandedness = "none";
|
18
|
+
|
19
|
+
/** should it follow controllers (the physics controller) */
|
20
|
+
@serializable()
|
21
|
+
controller: boolean = true;
|
22
|
+
|
23
|
+
/** should it follow hands (when using hand tracking in WebXR) */
|
24
|
+
hands: boolean = false;
|
25
|
+
|
26
|
+
/** Disable if you don't want this script to modify the object's visibility
|
27
|
+
* If enabled the object will be hidden when the configured controller or hand is not available
|
28
|
+
* If disabled this script will not modify the object's visibility
|
29
|
+
*/
|
30
|
+
controlVisibility: boolean = true;
|
31
|
+
|
32
|
+
/** when true it will use the grip space, otherwise the ray space */
|
33
|
+
useGripSpace = false;
|
34
|
+
|
35
|
+
onUpdateXR(args: NeedleXREventArgs): void {
|
36
|
+
|
37
|
+
// try to get the controller
|
38
|
+
const ctrl = args.xr.getController(this.side);
|
39
|
+
if (ctrl) {
|
40
|
+
// check if this is a hand and hands are allowed
|
41
|
+
if (ctrl.hand && !this.hands) {
|
42
|
+
if (this.controlVisibility)
|
43
|
+
this.gameObject.visible = false;
|
44
|
+
return;
|
45
|
+
}
|
46
|
+
// check if this is a controller and controllers are allowed
|
47
|
+
else if (!this.controller) {
|
48
|
+
if (this.controlVisibility)
|
49
|
+
this.gameObject.visible = false;
|
50
|
+
return;
|
51
|
+
}
|
52
|
+
// we're following a controller (or hand)
|
53
|
+
if (this.controlVisibility)
|
54
|
+
this.gameObject.visible = true;
|
55
|
+
if (this.useGripSpace) {
|
56
|
+
this.gameObject.worldPosition = ctrl.gripWorldPosition;
|
57
|
+
this.gameObject.worldQuaternion = ctrl.gripWorldQuaternion;
|
58
|
+
}
|
59
|
+
else {
|
60
|
+
this.gameObject.worldPosition = ctrl.rayWorldPosition;
|
61
|
+
this.gameObject.worldQuaternion = ctrl.rayWorldQuaternion;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
}
|
66
|
+
|
67
|
+
}
|
@@ -0,0 +1,307 @@
|
|
1
|
+
import { AxesHelper, Group, Material, Mesh, Object3D, XRHandSpace } from "three";
|
2
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
|
+
import { XRControllerModelFactory } from "three/examples/jsm/webxr/XRControllerModelFactory.js";
|
4
|
+
import { XRHandMeshModel } from "three/examples/jsm/webxr/XRHandMeshModel.js";
|
5
|
+
|
6
|
+
import { showBalloonWarning } from "../../../engine/debug/index.js";
|
7
|
+
import { AssetReference } from "../../../engine/engine_addressables.js";
|
8
|
+
import { setDontDestroy } from "../../../engine/engine_gameobject.js";
|
9
|
+
import { Gizmos } from "../../../engine/engine_gizmos.js";
|
10
|
+
import { addDracoAndKTX2Loaders } from "../../../engine/engine_loaders.js";
|
11
|
+
import { serializable } from "../../../engine/engine_serialization_decorator.js";
|
12
|
+
import { IGameObject } from "../../../engine/engine_types.js";
|
13
|
+
import { getParam } from "../../../engine/engine_utils.js";
|
14
|
+
import { NeedleXRController, NeedleXRControllerEventArgs, NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
|
15
|
+
import { Behaviour, GameObject } from "../../Component.js"
|
16
|
+
|
17
|
+
const debug = getParam("debugwebxr");
|
18
|
+
|
19
|
+
const handsJointBuffer = new Float32Array(16 * 25);
|
20
|
+
const renderingUpdateTimings = new Array<number>();
|
21
|
+
|
22
|
+
export class XRControllerModel extends Behaviour {
|
23
|
+
|
24
|
+
@serializable()
|
25
|
+
createControllerModel: boolean = true;
|
26
|
+
|
27
|
+
@serializable()
|
28
|
+
createHandModel: boolean = true;
|
29
|
+
|
30
|
+
/** assign a model or model url to create custom hand models */
|
31
|
+
@serializable(AssetReference)
|
32
|
+
customLeftHand?: AssetReference;
|
33
|
+
/** assign a model or model url to create custom hand models */
|
34
|
+
@serializable(AssetReference)
|
35
|
+
customRightHand?: AssetReference;
|
36
|
+
|
37
|
+
|
38
|
+
static readonly factory: XRControllerModelFactory = new XRControllerModelFactory();
|
39
|
+
|
40
|
+
supportsXR(mode: XRSessionMode): boolean {
|
41
|
+
return mode === "immersive-vr" || mode === "immersive-ar";
|
42
|
+
}
|
43
|
+
|
44
|
+
private _models = new Array<{ model?: IGameObject, controller: NeedleXRController, handmesh?: XRHandMeshModel }>();
|
45
|
+
|
46
|
+
|
47
|
+
async onXRControllerAdded(args: NeedleXRControllerEventArgs) {
|
48
|
+
// TODO we may want to treat controllers differently in AR/Passthrough mode
|
49
|
+
const isSupportedSession = args.xr.isVR || args.xr.isPassThrough;
|
50
|
+
if (!isSupportedSession) return;
|
51
|
+
|
52
|
+
const { controller } = args;
|
53
|
+
|
54
|
+
if (debug) console.warn("Add Controller Model for", controller.side, controller.index)
|
55
|
+
|
56
|
+
if (this.createControllerModel) {
|
57
|
+
if (controller.hand) {
|
58
|
+
if (this.createHandModel) {
|
59
|
+
const res = await this.loadHandModel(controller);
|
60
|
+
if (!res || !controller.connected) {
|
61
|
+
res?.handObject?.removeFromParent();
|
62
|
+
res?.handmesh?.controller?.removeFromParent();
|
63
|
+
return;
|
64
|
+
}
|
65
|
+
this._models.push({ controller: controller, model: res.handObject, handmesh: res.handmesh });
|
66
|
+
this._models.sort((a, b) => a.controller.index - b.controller.index);
|
67
|
+
this.scene.add(res.handObject);
|
68
|
+
}
|
69
|
+
}
|
70
|
+
else {
|
71
|
+
if (this.createControllerModel) {
|
72
|
+
const assetUrl = await controller.getModelUrl();
|
73
|
+
if (assetUrl) {
|
74
|
+
const model = await this.loadModel(controller, assetUrl);
|
75
|
+
if (!model || !controller.connected) return;
|
76
|
+
this._models.push({ controller: controller, model });
|
77
|
+
this._models.sort((a, b) => a.controller.index - b.controller.index);
|
78
|
+
this.scene.add(model);
|
79
|
+
// The controller mesh should by default inherit layers.
|
80
|
+
model.traverse(child => {
|
81
|
+
child.layers.set(2);
|
82
|
+
});
|
83
|
+
}
|
84
|
+
else {
|
85
|
+
console.warn("XRControllerModel: no model found for " + controller.side);
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
}
|
91
|
+
onXRControllerRemoved(args: NeedleXRControllerEventArgs): void {
|
92
|
+
// we need to find the index by the controller because if controller 0 is removed first then args.controller.index 1 will be at index 0
|
93
|
+
const indexInArray = this._models.findIndex(m => m?.controller === args.controller);
|
94
|
+
const entry = this._models[indexInArray];
|
95
|
+
if (!entry) return;
|
96
|
+
this._models.splice(indexInArray, 1);
|
97
|
+
|
98
|
+
if (entry.handmesh) {
|
99
|
+
entry.handmesh.handModel?.removeFromParent();
|
100
|
+
}
|
101
|
+
if (entry.model) {
|
102
|
+
entry.model.removeFromParent();
|
103
|
+
}
|
104
|
+
}
|
105
|
+
onBeforeRender() {
|
106
|
+
if (!NeedleXRSession.active) return;
|
107
|
+
|
108
|
+
if (debug) renderingUpdateTimings[0] = Date.now();
|
109
|
+
// update model
|
110
|
+
this.updateRendering(NeedleXRSession.active);
|
111
|
+
|
112
|
+
if (debug) {
|
113
|
+
const dt = Date.now() - renderingUpdateTimings[0];
|
114
|
+
renderingUpdateTimings.push(dt);
|
115
|
+
if (renderingUpdateTimings.length >= 30) {
|
116
|
+
renderingUpdateTimings[0] = 0;
|
117
|
+
const avrg = renderingUpdateTimings.reduce((a, b) => a + b, 0) / renderingUpdateTimings.length;
|
118
|
+
renderingUpdateTimings.length = 0;
|
119
|
+
console.log("[XRControllerModel] " + avrg.toFixed(2) + " ms");
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
onLeaveXR(_args: NeedleXREventArgs): void {
|
124
|
+
for (const entry of this._models) {
|
125
|
+
if (!entry) continue;
|
126
|
+
entry.model?.removeFromParent();
|
127
|
+
}
|
128
|
+
this._models = [];
|
129
|
+
}
|
130
|
+
|
131
|
+
private updateRendering(xr: NeedleXRSession) {
|
132
|
+
|
133
|
+
for (let i = 0; i < this._models.length; i++) {
|
134
|
+
const entry = this._models[i];
|
135
|
+
if (!entry) continue;
|
136
|
+
const ctrl = entry.controller;
|
137
|
+
if (!ctrl.connected) {
|
138
|
+
// the actual removal of the model happens in onXRControllerRemoved
|
139
|
+
if (debug) console.warn("XRControllerModel.onUpdateXR: controller is not connected anymore", ctrl.side, ctrl.hand);
|
140
|
+
continue;
|
141
|
+
}
|
142
|
+
|
143
|
+
// do we have a controller model?
|
144
|
+
if (entry.model && !entry.handmesh) {
|
145
|
+
// TODO: if the model would just be in the XR rig, we could just set grip position and we would not need to override the scale
|
146
|
+
// entry.model.position.copy(ctrl.gripWorldPosition);
|
147
|
+
entry.model.position.copy(ctrl.gripPosition);
|
148
|
+
// entry.model.quaternion.copy(ctrl.gripWorldQuaternion);
|
149
|
+
entry.model.quaternion.copy(ctrl.gripQuaternion);
|
150
|
+
entry.model.visible = ctrl.isTracking;
|
151
|
+
// ensure that controller models are in rig space
|
152
|
+
xr.rig?.gameObject.add(entry.model);
|
153
|
+
}
|
154
|
+
// do we have a hand mesh?
|
155
|
+
else if (ctrl.inputSource.hand && entry.handmesh) {
|
156
|
+
const referenceSpace = xr.referenceSpace;
|
157
|
+
const hand = this.context.renderer.xr.getHand(ctrl.index);
|
158
|
+
// if (referenceSpace && xr.frame.fillPoses) {
|
159
|
+
// xr.frame.fillPoses(ctrl.inputSource.hand.values(), referenceSpace, handsJointBuffer);
|
160
|
+
// let j = 0;
|
161
|
+
// for (const space of ctrl.inputSource.hand.values()) {
|
162
|
+
// const joint = hand.joints[space.jointName];
|
163
|
+
// if (joint) {
|
164
|
+
// joint.matrix.fromArray(handsJointBuffer, j * 16);
|
165
|
+
// joint.matrix.decompose(joint.position, joint.quaternion, joint.scale);
|
166
|
+
// joint.visible = true;
|
167
|
+
// }
|
168
|
+
// j++;
|
169
|
+
// }
|
170
|
+
// }
|
171
|
+
// else
|
172
|
+
if (referenceSpace && xr.frame.getJointPose) {
|
173
|
+
for (const inputjoint of ctrl.inputSource.hand.values()) {
|
174
|
+
// The transform of this joint will be updated with the joint pose on each frame
|
175
|
+
const joint = hand.joints[inputjoint.jointName];
|
176
|
+
if (joint) {
|
177
|
+
// Update the joints groups with the XRJoint poses
|
178
|
+
const jointPose = ctrl.getHandJointPose(inputjoint);
|
179
|
+
if (jointPose) {
|
180
|
+
// joint.matrixAutoUpdate = false;
|
181
|
+
// joint.matrix.fromArray(jointPose.transform.matrix);
|
182
|
+
// joint.matrix.decompose(joint.position, joint.quaternion, joint.scale);
|
183
|
+
const { position, quaternion } = xr.convertSpace(jointPose.transform);
|
184
|
+
joint.position.copy(position);
|
185
|
+
joint.quaternion.copy(quaternion);
|
186
|
+
joint.matrixWorldAutoUpdate = false;
|
187
|
+
}
|
188
|
+
joint.visible = jointPose != null;
|
189
|
+
}
|
190
|
+
}
|
191
|
+
// ensure that the hand renders in rig space
|
192
|
+
if (entry.model) {
|
193
|
+
entry.model.visible = ctrl.isTracking;
|
194
|
+
if (entry.model.visible && entry.model.parent !== xr.rig?.gameObject) {
|
195
|
+
xr.rig?.gameObject.add(entry.model);
|
196
|
+
}
|
197
|
+
entry.model.position.set(0, 0, 0);
|
198
|
+
}
|
199
|
+
|
200
|
+
if (entry.model?.visible) entry.handmesh?.updateMesh();
|
201
|
+
}
|
202
|
+
}
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
protected async loadModel(controller: NeedleXRController, url: string): Promise<IGameObject | null> {
|
207
|
+
if (!controller.connected) {
|
208
|
+
console.warn("XRControllerModel.onXRControllerAdded: controller is not connected anymore", controller.side);
|
209
|
+
return null;
|
210
|
+
}
|
211
|
+
const assetReference = AssetReference.getOrCreate("", url);
|
212
|
+
const model = await assetReference.instantiate() as GameObject;
|
213
|
+
setDontDestroy(model);
|
214
|
+
|
215
|
+
if (NeedleXRSession.active?.isPassThrough) {
|
216
|
+
model.traverseVisible((obj: Object3D) => {
|
217
|
+
this.makeOccluder(obj);
|
218
|
+
})
|
219
|
+
}
|
220
|
+
return model as IGameObject;
|
221
|
+
}
|
222
|
+
|
223
|
+
protected async loadHandModel(controller: NeedleXRController): Promise<{ handObject: IGameObject, handmesh: XRHandMeshModel } | null> {
|
224
|
+
|
225
|
+
const context = this.context;
|
226
|
+
const hand = context.renderer.xr.getHand(controller.index);
|
227
|
+
if (!hand) {
|
228
|
+
if (debug) Gizmos.DrawLabel(controller.rayWorldPosition, "No hand found for index " + controller.index, .05, 5);
|
229
|
+
else console.warn("No hand found for index " + controller.index);
|
230
|
+
}
|
231
|
+
|
232
|
+
const loader = new GLTFLoader();
|
233
|
+
addDracoAndKTX2Loaders(loader, context);
|
234
|
+
loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/generic-hand/');
|
235
|
+
|
236
|
+
// TODO: we should handle the loading here ourselves to not have this requirement of a specific model name
|
237
|
+
const expectedHandModelName = controller.side === "left" ? "left." : "right.";
|
238
|
+
const customHand = controller.side === "left" ? this.customLeftHand : this.customRightHand;
|
239
|
+
if (customHand) {
|
240
|
+
if (!customHand.uri.includes(expectedHandModelName)) {
|
241
|
+
console.warn("XRControllerModel: custom hand model must be named " + expectedHandModelName);
|
242
|
+
showBalloonWarning("Custom Hand: unexpected name, please see the console for details");
|
243
|
+
}
|
244
|
+
else {
|
245
|
+
const basePath = customHand.uri.substring(0, customHand.uri.indexOf(expectedHandModelName));
|
246
|
+
loader.setPath(basePath);
|
247
|
+
if (debug) console.log("XRControllerModel: loading custom hand model from " + basePath);
|
248
|
+
}
|
249
|
+
}
|
250
|
+
|
251
|
+
|
252
|
+
const handObject = new Object3D();
|
253
|
+
setDontDestroy(handObject);
|
254
|
+
// @ts-ignore
|
255
|
+
const handmesh = new XRHandMeshModel(handObject, hand, loader.path, controller.inputSource.handedness, loader, (object: Object3D) => {
|
256
|
+
// The hand mesh should not receive raycasts
|
257
|
+
object.traverseVisible(child => {
|
258
|
+
child.layers.set(2);
|
259
|
+
if (NeedleXRSession.active?.isPassThrough)
|
260
|
+
this.makeOccluder(child);
|
261
|
+
});
|
262
|
+
if (!controller.connected) {
|
263
|
+
if (debug) Gizmos.DrawLabel(controller.rayWorldPosition, "Hand is loaded but not connected anymore", .05, 5);
|
264
|
+
object.removeFromParent();
|
265
|
+
}
|
266
|
+
});
|
267
|
+
|
268
|
+
if (debug) handObject.add(new AxesHelper(.5));
|
269
|
+
|
270
|
+
if (controller.inputSource.hand) {
|
271
|
+
if (debug) console.log(controller.inputSource.hand);
|
272
|
+
for (const inputjoint of controller.inputSource.hand.values()) {
|
273
|
+
if (hand.joints[inputjoint.jointName] === undefined) {
|
274
|
+
const joint = new Group();
|
275
|
+
joint.matrixAutoUpdate = false;
|
276
|
+
joint.visible = true;
|
277
|
+
// joint.jointRadius = 0.01;
|
278
|
+
// @ts-ignore
|
279
|
+
hand.joints[inputjoint.jointName] = joint;
|
280
|
+
hand.add(joint);
|
281
|
+
|
282
|
+
}
|
283
|
+
}
|
284
|
+
}
|
285
|
+
else {
|
286
|
+
if (debug) {
|
287
|
+
Gizmos.DrawLabel(controller.rayWorldPosition, "No inputSource.hand found for index " + controller.index, .05, 5);
|
288
|
+
}
|
289
|
+
}
|
290
|
+
|
291
|
+
return { handObject: handObject as IGameObject, handmesh: handmesh };
|
292
|
+
}
|
293
|
+
|
294
|
+
private makeOccluder(obj: Object3D) {
|
295
|
+
if (obj instanceof Mesh) {
|
296
|
+
let mat = obj.material;
|
297
|
+
if (mat instanceof Material) {
|
298
|
+
mat = obj.material = mat.clone();
|
299
|
+
// depth only
|
300
|
+
mat.depthWrite = true;
|
301
|
+
mat.depthTest = true;
|
302
|
+
mat.colorWrite = false;
|
303
|
+
obj.renderOrder = -100;
|
304
|
+
}
|
305
|
+
}
|
306
|
+
}
|
307
|
+
}
|
@@ -0,0 +1,340 @@
|
|
1
|
+
import { AdditiveBlending, BufferAttribute, Color, DoubleSide, Line, Line3, Mesh, MeshBasicMaterial, Object3D, RingGeometry, SubtractiveBlending, Vector3 } from "three";
|
2
|
+
import { Line2 } from "three/examples/jsm/lines/Line2.js";
|
3
|
+
import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
|
4
|
+
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
|
5
|
+
|
6
|
+
import { Gizmos } from "../../../engine/engine_gizmos.js";
|
7
|
+
import { Mathf } from "../../../engine/engine_math.js";
|
8
|
+
import { RaycastTestObjectCallback } from "../../../engine/engine_physics.js";
|
9
|
+
import { serializable } from "../../../engine/engine_serialization.js"
|
10
|
+
import { getTempVector, getWorldQuaternion, getWorldScale } from "../../../engine/engine_three_utils.js";
|
11
|
+
import { IGameObject } from "../../../engine/engine_types.js";
|
12
|
+
import { getParam } from "../../../engine/engine_utils.js";
|
13
|
+
import { NeedleXRController, NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
|
14
|
+
import { Behaviour, GameObject } from "../../Component.js"
|
15
|
+
import { TeleportTarget } from "../TeleportTarget.js";
|
16
|
+
import { XRMovementBehaviour } from "../types.js";
|
17
|
+
|
18
|
+
const debug = getParam("debugwebxr");
|
19
|
+
|
20
|
+
export class XRControllerMovement extends Behaviour implements XRMovementBehaviour {
|
21
|
+
|
22
|
+
/** Movement speed in meters per second */
|
23
|
+
@serializable()
|
24
|
+
movementSpeed = 1;
|
25
|
+
|
26
|
+
/** How many degrees to rotate the XR rig when using the rotation trigger */
|
27
|
+
@serializable()
|
28
|
+
rotationStep = 60;
|
29
|
+
|
30
|
+
/** When enabled you can teleport using the right XR controller's thumbstick by pressing forward */
|
31
|
+
@serializable()
|
32
|
+
useTeleport: boolean = true;
|
33
|
+
|
34
|
+
/** Enable to only allow teleporting on objects with a teleport target component */
|
35
|
+
@serializable()
|
36
|
+
useTeleportTarget = false;
|
37
|
+
|
38
|
+
/** Enable to fade out the scene when teleporting */
|
39
|
+
@serializable()
|
40
|
+
useTeleportFade = false;
|
41
|
+
|
42
|
+
/** enable to visualize controller rays in the 3D scene */
|
43
|
+
@serializable()
|
44
|
+
showRays: boolean = true;
|
45
|
+
|
46
|
+
/** enable to visualize pointer targets in the 3D scene */
|
47
|
+
@serializable()
|
48
|
+
showHits: boolean = true;
|
49
|
+
|
50
|
+
readonly isXRMovementHandler: true = true;
|
51
|
+
|
52
|
+
readonly xrSessionMode = "immersive-vr";
|
53
|
+
|
54
|
+
private _didApplyRotation = false;
|
55
|
+
private _didTeleport = false;
|
56
|
+
|
57
|
+
onUpdateXR(args: NeedleXREventArgs): void {
|
58
|
+
const rig = args.xr.rig;
|
59
|
+
if (!rig?.gameObject) return;
|
60
|
+
|
61
|
+
// in AR pass through mode we dont want to move the rig
|
62
|
+
if (args.xr.isPassThrough) {
|
63
|
+
return;
|
64
|
+
}
|
65
|
+
|
66
|
+
const movementController = args.xr.leftController;
|
67
|
+
const teleportController = args.xr.rightController;
|
68
|
+
|
69
|
+
if (movementController)
|
70
|
+
this.onHandleMovement(movementController, rig.gameObject);
|
71
|
+
if (teleportController) {
|
72
|
+
this.onHandleRotation(teleportController, rig.gameObject);
|
73
|
+
if (this.useTeleport)
|
74
|
+
this.onHandleTeleport(teleportController, rig.gameObject);
|
75
|
+
}
|
76
|
+
|
77
|
+
}
|
78
|
+
onLeaveXR(_: NeedleXREventArgs): void {
|
79
|
+
for (const line of this._lines) {
|
80
|
+
line.removeFromParent();
|
81
|
+
}
|
82
|
+
for (const disc of this._hitDiscs) {
|
83
|
+
disc?.removeFromParent();
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
onBeforeRender(): void {
|
88
|
+
if (this.context.xr?.running) {
|
89
|
+
if (this.showRays)
|
90
|
+
this.renderRays(this.context.xr);
|
91
|
+
if (this.showHits)
|
92
|
+
this.renderHits(this.context.xr);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
protected onHandleMovement(controller: NeedleXRController, rig: IGameObject) {
|
97
|
+
const stick = controller.getStick("xr-standard-thumbstick");
|
98
|
+
const vec = new Vector3(stick.x, 0, stick.y);
|
99
|
+
vec.multiplyScalar(this.context.time.deltaTime * this.movementSpeed);
|
100
|
+
const scale = getWorldScale(rig);
|
101
|
+
vec.multiplyScalar(scale.x);
|
102
|
+
vec.applyQuaternion(controller.xr.poseOrientation);
|
103
|
+
vec.y = 0;
|
104
|
+
vec.applyQuaternion(rig.worldQuaternion);
|
105
|
+
rig.position.add(vec);
|
106
|
+
|
107
|
+
// TODO: if we dont do this here the XRControllerModel will be frame-delayed - maybe we need to introduce a priority order for XR components?
|
108
|
+
rig.updateMatrixWorld();
|
109
|
+
}
|
110
|
+
|
111
|
+
|
112
|
+
protected onHandleRotation(controller: NeedleXRController, rig: IGameObject) {
|
113
|
+
const stick = controller.getStick("xr-standard-thumbstick");
|
114
|
+
const rotationInput = stick.x;
|
115
|
+
if (this._didApplyRotation) {
|
116
|
+
if (Math.abs(rotationInput) < .3) {
|
117
|
+
this._didApplyRotation = false;
|
118
|
+
}
|
119
|
+
}
|
120
|
+
else if (Math.abs(rotationInput) > .5) {
|
121
|
+
this._didApplyRotation = true;
|
122
|
+
const dir = rotationInput > 0 ? 1 : -1;
|
123
|
+
rig.rotateY(dir * Mathf.toRadians(this.rotationStep));
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
protected onHandleTeleport(controller: NeedleXRController, rig: IGameObject) {
|
128
|
+
const teleportInput = controller.getStick("xr-standard-thumbstick")
|
129
|
+
if (this._didTeleport) {
|
130
|
+
if (teleportInput.y < .2) {
|
131
|
+
this._didTeleport = false;
|
132
|
+
}
|
133
|
+
}
|
134
|
+
else if (teleportInput.y > .8) {
|
135
|
+
this._didTeleport = true;
|
136
|
+
const hit = this.context.physics.raycastFromRay(controller.ray)[0];
|
137
|
+
if (hit) {
|
138
|
+
if (this.useTeleportTarget) {
|
139
|
+
const teleportTarget = GameObject.getComponentInParent(hit.object, TeleportTarget);
|
140
|
+
if (!teleportTarget) return;
|
141
|
+
}
|
142
|
+
if (debug) Gizmos.DrawSphere(hit.point, .025, 0xff0000, 5);
|
143
|
+
const point = hit.point.clone();
|
144
|
+
if (this.useTeleportFade) {
|
145
|
+
controller.xr.fadeTransition()?.then(() => {
|
146
|
+
rig.worldPosition = point;
|
147
|
+
})
|
148
|
+
}
|
149
|
+
else {
|
150
|
+
rig.worldPosition = point;
|
151
|
+
}
|
152
|
+
}
|
153
|
+
else {
|
154
|
+
// TODO: add option to allow teleportation on current ground plane
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
private readonly _lines: Object3D[] = [];
|
160
|
+
private readonly _hitDiscs: Object3D[] = [];
|
161
|
+
private readonly _hitDistances: number[] = [];
|
162
|
+
|
163
|
+
protected renderRays(session: NeedleXRSession) {
|
164
|
+
|
165
|
+
for (let i = 0; i < this._lines.length; i++) {
|
166
|
+
const line = this._lines[i];
|
167
|
+
line.visible = false;
|
168
|
+
}
|
169
|
+
|
170
|
+
for (let i = 0; i < session.controllers.length; i++) {
|
171
|
+
const ctrl = session.controllers[i];
|
172
|
+
let line = this._lines[i];
|
173
|
+
if (!ctrl.connected || !ctrl.isTracking) {
|
174
|
+
if (line) line.visible = false;
|
175
|
+
continue;
|
176
|
+
}
|
177
|
+
if (!line) {
|
178
|
+
line = this.createRayLineObject();
|
179
|
+
line.scale.z = .5;
|
180
|
+
this._lines[i] = line;
|
181
|
+
}
|
182
|
+
|
183
|
+
ctrl.updateRayWorldPosition();
|
184
|
+
ctrl.updateRayWorldQuaternion();
|
185
|
+
const pos = ctrl.rayWorldPosition;
|
186
|
+
const rot = ctrl.rayWorldQuaternion;
|
187
|
+
line.position.copy(pos);
|
188
|
+
line.quaternion.copy(rot);
|
189
|
+
const scale = session.rigScale;
|
190
|
+
const dist = this._hitDistances[i] ?? 1;
|
191
|
+
line.scale.set(scale, scale, scale * dist);
|
192
|
+
line.visible = true;
|
193
|
+
line.layers.disableAll();
|
194
|
+
line.layers.enable(2);
|
195
|
+
if (line.parent !== this.context.scene)
|
196
|
+
this.context.scene.add(line);
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
protected renderHits(session: NeedleXRSession) {
|
201
|
+
for (const disc of this._hitDiscs) {
|
202
|
+
if (disc) disc.visible = false;
|
203
|
+
}
|
204
|
+
for (let i = 0; i < session.controllers.length; i++) {
|
205
|
+
const ctrl = session.controllers[i];
|
206
|
+
if (!ctrl.connected || !ctrl.isTracking) continue;
|
207
|
+
|
208
|
+
// save performance by only raycasting every nth frame
|
209
|
+
if (this.context.time.frame % 2 !== 0) {
|
210
|
+
const disc = this._hitDiscs[i];
|
211
|
+
// if the disc had a hit last frame, we can show it again
|
212
|
+
if (disc && disc["hit"]) disc.visible = true;
|
213
|
+
continue;
|
214
|
+
}
|
215
|
+
|
216
|
+
const hit = this.context.physics.raycastFromRay(ctrl.ray, { testObject: this.hitPointRaycastFilter })[0];
|
217
|
+
this._hitDistances[i] = hit?.distance;
|
218
|
+
|
219
|
+
let disc = this._hitDiscs[i];
|
220
|
+
if (disc) // save the hit object on the disc
|
221
|
+
disc["hit"] = hit;
|
222
|
+
|
223
|
+
if (hit) {
|
224
|
+
const rigScale = (session.rigScale ?? 1);
|
225
|
+
if (debug) {
|
226
|
+
Gizmos.DrawWireSphere(hit.point, .025 * rigScale, 0xff0000);
|
227
|
+
Gizmos.DrawLabel(getTempVector(0, .2, 0).add(hit.point), hit.object.name, .02, 0);
|
228
|
+
}
|
229
|
+
|
230
|
+
if (!disc) {
|
231
|
+
disc = this.createHitPointObject();
|
232
|
+
this._hitDiscs[i] = disc;
|
233
|
+
}
|
234
|
+
disc.visible = true;
|
235
|
+
const size = (.01 * (1 + hit.distance));
|
236
|
+
disc.scale.set(size, size, size);
|
237
|
+
disc.layers.disableAll();
|
238
|
+
disc.layers.enable(2);
|
239
|
+
disc["hit"] = hit;
|
240
|
+
|
241
|
+
if (hit.normal) {
|
242
|
+
const factor = 0.02 * rigScale;
|
243
|
+
disc.position.set(0, 0, -.1 * factor).applyQuaternion(ctrl.rayWorldQuaternion);
|
244
|
+
disc.position.add(hit.point);
|
245
|
+
const worldNormal = hit.normal.applyQuaternion(getWorldQuaternion(hit.object));
|
246
|
+
disc.quaternion.setFromUnitVectors(up, worldNormal);
|
247
|
+
}
|
248
|
+
else {
|
249
|
+
disc.position.add(hit.point);
|
250
|
+
}
|
251
|
+
|
252
|
+
if (disc.parent !== this.context.scene) {
|
253
|
+
this.context.scene.add(disc);
|
254
|
+
}
|
255
|
+
}
|
256
|
+
else {
|
257
|
+
if (this._hitDiscs[i]) {
|
258
|
+
this._hitDiscs[i].visible = false;
|
259
|
+
}
|
260
|
+
}
|
261
|
+
}
|
262
|
+
}
|
263
|
+
|
264
|
+
protected hitPointRaycastFilter: RaycastTestObjectCallback = (obj: Object3D) => {
|
265
|
+
// by default dont raycast ont skinned meshes using the hit point raycast (because it is a big performance hit and only a visual indicator)
|
266
|
+
if (obj.type === "SkinnedMesh") return "continue in children";
|
267
|
+
return true;
|
268
|
+
}
|
269
|
+
|
270
|
+
/** create an object to visualize hit points in the scene */
|
271
|
+
protected createHitPointObject(): Object3D {
|
272
|
+
var container = new Object3D();
|
273
|
+
const disc = new Mesh(
|
274
|
+
new RingGeometry(.3, 0.5, 32).rotateX(- Math.PI / 2),
|
275
|
+
new MeshBasicMaterial({
|
276
|
+
color: 0xeeeeee,
|
277
|
+
opacity: .7,
|
278
|
+
transparent: true,
|
279
|
+
side: DoubleSide,
|
280
|
+
})
|
281
|
+
);
|
282
|
+
disc.layers.disableAll();
|
283
|
+
disc.layers.enable(2);
|
284
|
+
container.add(disc);
|
285
|
+
|
286
|
+
const disc2 = new Mesh(
|
287
|
+
new RingGeometry(.43, 0.5, 32).rotateX(- Math.PI / 2),
|
288
|
+
new MeshBasicMaterial({
|
289
|
+
color: 0x000000,
|
290
|
+
opacity: .2,
|
291
|
+
transparent: true,
|
292
|
+
side: DoubleSide,
|
293
|
+
})
|
294
|
+
);
|
295
|
+
disc2.layers.disableAll();
|
296
|
+
disc2.layers.enable(2);
|
297
|
+
disc2.position.z -= .01;
|
298
|
+
container.add(disc2);
|
299
|
+
return container;
|
300
|
+
}
|
301
|
+
|
302
|
+
/** create an object to visualize controller rays */
|
303
|
+
protected createRayLineObject() {
|
304
|
+
const line = new Line2();
|
305
|
+
line.layers.disableAll();
|
306
|
+
line.layers.enable(2);
|
307
|
+
|
308
|
+
const geometry = new LineGeometry();
|
309
|
+
line.geometry = geometry;
|
310
|
+
|
311
|
+
const positions = new Float32Array(9);
|
312
|
+
positions.set([0, 0, .02, 0, 0, .4, 0, 0, 1]);
|
313
|
+
geometry.setPositions(positions)
|
314
|
+
|
315
|
+
const colors = new Float32Array(9);
|
316
|
+
colors.set([1, 1, 1, .1, .1, .1, 0, 0, 0]);
|
317
|
+
geometry.setColors(colors);
|
318
|
+
|
319
|
+
const mat = new LineMaterial({
|
320
|
+
color: 0xffffff,
|
321
|
+
vertexColors: true,
|
322
|
+
worldUnits: true,
|
323
|
+
linewidth: .004,
|
324
|
+
|
325
|
+
transparent: true,
|
326
|
+
// TODO: this doesnt work with passthrough
|
327
|
+
blending: AdditiveBlending,
|
328
|
+
dashed: false,
|
329
|
+
alphaToCoverage: true,
|
330
|
+
|
331
|
+
});
|
332
|
+
line.material = mat;
|
333
|
+
|
334
|
+
return line;
|
335
|
+
}
|
336
|
+
}
|
337
|
+
|
338
|
+
|
339
|
+
const up = new Vector3(0, 1, 0);
|
340
|
+
|
@@ -0,0 +1,143 @@
|
|
1
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
2
|
+
import { getParam } from "../../engine/engine_utils.js";
|
3
|
+
import { Behaviour, GameObject } from "../Component.js";
|
4
|
+
|
5
|
+
|
6
|
+
const debug = getParam("debugxrflags");
|
7
|
+
const disable = getParam("disablexrflags");
|
8
|
+
if (disable) { console.warn("XRFlags are disabled") }
|
9
|
+
|
10
|
+
export enum XRStateFlag {
|
11
|
+
Never = 0,
|
12
|
+
Browser = 1 << 0,
|
13
|
+
AR = 1 << 1,
|
14
|
+
VR = 1 << 2,
|
15
|
+
FirstPerson = 1 << 3,
|
16
|
+
ThirdPerson = 1 << 4,
|
17
|
+
All = 0xffffffff
|
18
|
+
}
|
19
|
+
|
20
|
+
export class XRState {
|
21
|
+
|
22
|
+
public static Global: XRState = new XRState();
|
23
|
+
|
24
|
+
public Mask: XRStateFlag = XRStateFlag.Browser | XRStateFlag.ThirdPerson;
|
25
|
+
|
26
|
+
public Has(state: XRStateFlag) {
|
27
|
+
const res = (this.Mask & state);
|
28
|
+
return res !== 0;
|
29
|
+
}
|
30
|
+
|
31
|
+
public Set(state: number) {
|
32
|
+
if (debug) console.warn("Set XR flag state to", state)
|
33
|
+
this.Mask = state as number;
|
34
|
+
XRFlag.Apply();
|
35
|
+
}
|
36
|
+
|
37
|
+
public Enable(state: number) {
|
38
|
+
this.Mask |= state;
|
39
|
+
XRFlag.Apply();
|
40
|
+
}
|
41
|
+
|
42
|
+
public Disable(state: number) {
|
43
|
+
this.Mask &= ~state;
|
44
|
+
XRFlag.Apply();
|
45
|
+
}
|
46
|
+
|
47
|
+
public Toggle(state: number) {
|
48
|
+
this.Mask ^= state;
|
49
|
+
XRFlag.Apply();
|
50
|
+
}
|
51
|
+
|
52
|
+
public EnableAll() {
|
53
|
+
this.Mask = 0xffffffff | 0;
|
54
|
+
XRFlag.Apply();
|
55
|
+
}
|
56
|
+
|
57
|
+
public DisableAll() {
|
58
|
+
this.Mask = 0;
|
59
|
+
XRFlag.Apply();
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
export class XRFlag extends Behaviour {
|
64
|
+
|
65
|
+
private static registry: XRFlag[] = [];
|
66
|
+
|
67
|
+
public static Apply() {
|
68
|
+
for (const r of this.registry) r.UpdateVisible(XRState.Global);
|
69
|
+
}
|
70
|
+
|
71
|
+
private static firstApply: boolean;
|
72
|
+
private static buffer: XRState = new XRState();
|
73
|
+
|
74
|
+
@serializable()
|
75
|
+
public visibleIn!: number;
|
76
|
+
|
77
|
+
awake() {
|
78
|
+
XRFlag.registry.push(this);
|
79
|
+
}
|
80
|
+
|
81
|
+
onEnable(): void {
|
82
|
+
if (!XRFlag.firstApply) {
|
83
|
+
XRFlag.firstApply = true;
|
84
|
+
XRFlag.Apply();
|
85
|
+
}
|
86
|
+
else {
|
87
|
+
this.UpdateVisible(XRState.Global);
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
onDestroy(): void {
|
92
|
+
const i = XRFlag.registry.indexOf(this);
|
93
|
+
if (i >= 0)
|
94
|
+
XRFlag.registry.splice(i, 1);
|
95
|
+
}
|
96
|
+
|
97
|
+
public get isOn(): boolean { return this.gameObject.visible; }
|
98
|
+
|
99
|
+
public UpdateVisible(state: XRState | XRStateFlag | null = null) {
|
100
|
+
if (disable) {
|
101
|
+
return;
|
102
|
+
}
|
103
|
+
// XR flags set visibility of whole hierarchy which is like setting the whole object inactive
|
104
|
+
// so we need to ignore the enabled state of the XRFlag component
|
105
|
+
// if(!this.enabled) return;
|
106
|
+
let res: boolean | undefined = undefined;
|
107
|
+
|
108
|
+
const flag = state as number;
|
109
|
+
if (flag && typeof flag === "number") {
|
110
|
+
console.assert(typeof flag === "number", "XRFlag.UpdateVisible: state must be a number", flag);
|
111
|
+
if (debug)
|
112
|
+
console.log(flag);
|
113
|
+
XRFlag.buffer.Mask = flag;
|
114
|
+
state = XRFlag.buffer;
|
115
|
+
}
|
116
|
+
|
117
|
+
if (state instanceof XRState) {
|
118
|
+
if (debug)
|
119
|
+
console.warn(this.name, "use passed in mask", state.Mask, this.visibleIn)
|
120
|
+
res = state.Has(this.visibleIn);
|
121
|
+
}
|
122
|
+
else {
|
123
|
+
if (debug)
|
124
|
+
console.log(this.name, "use global mask")
|
125
|
+
XRState.Global.Has(this.visibleIn);
|
126
|
+
}
|
127
|
+
if (res === undefined) return;
|
128
|
+
if (res) {
|
129
|
+
if (debug)
|
130
|
+
console.log(this.name, "is visible", this.gameObject.uuid)
|
131
|
+
// this.gameObject.visible = true;
|
132
|
+
GameObject.setActive(this.gameObject, true);
|
133
|
+
} else {
|
134
|
+
if (debug)
|
135
|
+
console.log(this.name, "is not visible", this.gameObject.uuid);
|
136
|
+
const isVisible = this.gameObject.visible;
|
137
|
+
if (!isVisible) return;
|
138
|
+
this.gameObject.visible = false;
|
139
|
+
// console.trace("DISABLE", this.name);
|
140
|
+
// GameObject.setActive(this.gameObject, false);
|
141
|
+
}
|
142
|
+
}
|
143
|
+
}
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import { IComponent } from "../engine_types.js";
|
2
|
+
|
3
|
+
|
4
|
+
export interface IXRRig extends Pick<IComponent, "gameObject"> {
|
5
|
+
isXRRig(): boolean;
|
6
|
+
get isActive(): boolean;
|
7
|
+
/** The rig with the highest priority will be chosen */
|
8
|
+
priority?: number;
|
9
|
+
}
|