File without changes
|
@@ -1,56 +0,0 @@
|
|
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
|
-
}
|
@@ -1,23 +0,0 @@
|
|
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
|
-
}
|
@@ -68,7 +68,6 @@
|
|
68
68
|
return "assets";
|
69
69
|
}
|
70
70
|
|
71
|
-
/** @returns the fullpath of the build */
|
72
71
|
export function getOutputDirectory() {
|
73
72
|
const projectConfig = tryLoadProjectConfig();
|
74
73
|
return process.cwd() + "/" + (projectConfig?.buildDirectory || "dist");
|
@@ -36,10 +36,7 @@
|
|
36
36
|
const needleConfig = tryLoadProjectConfig();
|
37
37
|
if (needleConfig) {
|
38
38
|
assetsDirName = needleConfig.assetsDirectory;
|
39
|
-
while
|
40
|
-
|
41
|
-
if (needleConfig.buildDirectory)
|
42
|
-
outdirName = needleConfig.buildDirectory;
|
39
|
+
while(assetsDirName.startsWith('/')) assetsDirName = assetsDirName.substring(1);
|
43
40
|
}
|
44
41
|
|
45
42
|
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);
|
30
30
|
if (version)
|
31
31
|
viteConfig.define.NEEDLE_ENGINE_VERSION = "\"" + version + "\"";
|
32
32
|
if (needleEngineConfig)
|
@@ -42,9 +42,6 @@
|
|
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() + "\"";
|
48
45
|
}
|
49
46
|
}
|
50
47
|
}
|
@@ -39,14 +39,11 @@
|
|
39
39
|
});
|
40
40
|
}
|
41
41
|
|
42
|
-
|
43
|
-
log(
|
44
|
-
for (const client of currentClients) {
|
45
|
-
|
46
|
-
}
|
47
|
-
return new Promise((resolve) => {
|
48
|
-
setTimeout(resolve, 100);
|
49
|
-
});
|
42
|
+
function triggerReloadOnClients() {
|
43
|
+
log("Triggering reload on clients (todo)", currentClients.size)
|
44
|
+
// for (const client of currentClients) {
|
45
|
+
// client.send(JSON.stringify({ type: "full-reload" }));
|
46
|
+
// }
|
50
47
|
}
|
51
48
|
|
52
49
|
|
@@ -84,6 +81,9 @@
|
|
84
81
|
modified = true;
|
85
82
|
}
|
86
83
|
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("Dependency added", key)
|
99
99
|
requireReload = true;
|
100
100
|
requireInstall = true;
|
101
101
|
}
|
@@ -104,16 +104,13 @@
|
|
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("DevDependency added", 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
|
-
}
|
117
114
|
|
118
115
|
packageJsonSize = packageJsonStat.size;
|
119
116
|
lastEditTime = packageJsonStat.mtime;
|
@@ -122,7 +119,7 @@
|
|
122
119
|
restart(server, projectDir, cachePath);
|
123
120
|
}
|
124
121
|
}
|
125
|
-
},
|
122
|
+
}, 1000);
|
126
123
|
}
|
127
124
|
|
128
125
|
function testIfInstallIsRequired(projectDir, packageJson) {
|
@@ -145,7 +142,7 @@
|
|
145
142
|
}
|
146
143
|
}
|
147
144
|
}
|
148
|
-
log("Dependency not installed
|
145
|
+
log("Dependency not installed", key)
|
149
146
|
return true;
|
150
147
|
}
|
151
148
|
else {
|
@@ -164,11 +161,9 @@
|
|
164
161
|
const isRange = expectedVersion.includes("^") || expectedVersion.includes(">") || expectedVersion.includes("<");
|
165
162
|
if (!isRange) {
|
166
163
|
const packageJsonPath = path.join(depPath, "package.json");
|
167
|
-
/** @type {String} */
|
168
164
|
const installedVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
|
169
|
-
|
170
|
-
|
171
|
-
log(`Dependency ${key} is installed but version is not the right one. Expected \"${expectedVersion}/" but got \"${installedVersion}\"`)
|
165
|
+
if (expectedVersion !== installedVersion) {
|
166
|
+
log(`Dependency ${key} is installed but version is not correct. Expected ${expectedVersion} but got ${installedVersion}`)
|
172
167
|
return true;
|
173
168
|
}
|
174
169
|
}
|
@@ -199,13 +194,13 @@
|
|
199
194
|
}
|
200
195
|
|
201
196
|
if (id !== restartId) return;
|
202
|
-
if (Date.now() - lastRestartTime <
|
197
|
+
if (Date.now() - lastRestartTime < 1000) return;
|
203
198
|
log("Restarting server...")
|
204
199
|
lastRestartTime = Date.now();
|
205
200
|
requireInstall = false;
|
206
201
|
if (existsSync(cachePath))
|
207
202
|
rmSync(cachePath, { recursive: true, force: true });
|
208
|
-
|
203
|
+
triggerReloadOnClients();
|
209
204
|
|
210
205
|
// touch vite config to trigger reload
|
211
206
|
// const viteConfigPath = path.join(projectDir, "vite.config.js");
|
@@ -217,9 +212,8 @@
|
|
217
212
|
// }
|
218
213
|
|
219
214
|
// check if server is running
|
220
|
-
if (server.httpServer.listening)
|
215
|
+
if (server.httpServer.listening)
|
221
216
|
server.restart();
|
222
|
-
}
|
223
217
|
isRunningRestart = false;
|
224
218
|
console.log("-----------------------------------------------")
|
225
219
|
}
|
@@ -42,7 +42,6 @@
|
|
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";
|
46
45
|
|
47
46
|
|
48
47
|
export * from "./gzip.js";
|
@@ -58,8 +57,6 @@
|
|
58
57
|
*/
|
59
58
|
export const needlePlugins = async (command, config, userSettings) => {
|
60
59
|
|
61
|
-
if(!config) config = {}
|
62
|
-
|
63
60
|
// ensure we have user settings initialized with defaults
|
64
61
|
userSettings = { ...defaultUserSettings, ...userSettings }
|
65
62
|
const array = [
|
@@ -70,7 +67,6 @@
|
|
70
67
|
needlePoster(command, config, userSettings),
|
71
68
|
needleReload(command, config, userSettings),
|
72
69
|
needleBuild(command, config, userSettings),
|
73
|
-
needleBuildInfo(command, config, userSettings),
|
74
70
|
needleCopyFiles(command, config, userSettings),
|
75
71
|
needleTransformCodegen(command, config, userSettings),
|
76
72
|
needleDrop(command, config, userSettings),
|
@@ -109,8 +109,6 @@
|
|
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
|
-
|
114
112
|
return { html, tags }
|
115
113
|
},
|
116
114
|
}
|
@@ -1,2 +0,0 @@
|
|
1
|
-
Using flatbuffer compiler 2.0
|
2
|
-
https://github.com/google/flatbuffers/releases/tag/v2.0.0
|
@@ -1,7 +1,6 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import { USDDocument,USDObject } from "../../ThreeUSDZExporter.js";
|
1
|
+
import { Object3D, Matrix4, Material, BufferGeometry } from "three";
|
4
2
|
import { ActionBuilder, ActionModel } from "./BehavioursBuilder.js";
|
3
|
+
import { USDObject, USDDocument } from "../../ThreeUSDZExporter.js";
|
5
4
|
|
6
5
|
export abstract class DocumentAction {
|
7
6
|
|
@@ -1,8 +1,7 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
2
|
+
import * as utils from "./../engine/engine_three_utils.js";
|
1
3
|
import { Vector3 } from "three";
|
2
|
-
|
3
4
|
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";
|
6
5
|
|
7
6
|
export class AlignmentConstraint extends Behaviour {
|
8
7
|
|
@@ -1,11 +1,10 @@
|
|
1
|
+
import { Behaviour } from "./Component.js";
|
1
2
|
import { AnimationAction, AnimationClip, AnimationMixer, LoopOnce, LoopRepeat } from "three";
|
2
|
-
|
3
|
+
import { MixerEvent } from "./Animator.js";
|
4
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
3
5
|
import { Mathf } from "../engine/engine_math.js";
|
4
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
6
|
import type { Vec2 } from "../engine/engine_types.js";
|
6
7
|
import { getParam } from "../engine/engine_utils.js";
|
7
|
-
import { MixerEvent } from "./Animator.js";
|
8
|
-
import { Behaviour } from "./Component.js";
|
9
8
|
|
10
9
|
const debug = getParam("debuganimation");
|
11
10
|
|
@@ -1,10 +1,9 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { GameObject } from "../../../Component.js";
|
3
2
|
import { getParam } from "../../../../engine/engine_utils.js";
|
3
|
+
import { USDObject, buildMatrix, findStructuralNodesInBoneHierarchy, usdNumberFormatting as fn, getPathToSkeleton } from "../ThreeUSDZExporter.js";
|
4
|
+
import type { IUSDExporterExtension } from "../Extension.js";
|
5
|
+
import { Object3D, Matrix4, Vector3, Quaternion, Interpolant, AnimationClip, KeyframeTrack, PropertyBinding, Bone } from "three";
|
4
6
|
import { Animator } from "../../../Animator.js";
|
5
|
-
import { GameObject } from "../../../Component.js";
|
6
|
-
import type { IUSDExporterExtension } from "../Extension.js";
|
7
|
-
import { buildMatrix, findStructuralNodesInBoneHierarchy, getPathToSkeleton,usdNumberFormatting as fn, USDObject } from "../ThreeUSDZExporter.js";
|
8
7
|
|
9
8
|
const debug = getParam("debugusdzanimation");
|
10
9
|
const debugSerialization = getParam("debugusdzanimationserialization");
|
@@ -23,22 +23,6 @@
|
|
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
|
-
|
42
26
|
get duration(): number {
|
43
27
|
if (!this.keys || this.keys.length == 0) return 0;
|
44
28
|
return this.keys[this.keys.length - 1].time;
|
@@ -54,9 +38,9 @@
|
|
54
38
|
for (let i = 0; i < this.keys.length; i++) {
|
55
39
|
const kf = this.keys[i];
|
56
40
|
if (kf.time <= time) {
|
57
|
-
const hasNextKeyframe = i
|
41
|
+
const hasNextKeyframe = i+1 < this.keys.length;
|
58
42
|
if (hasNextKeyframe) {
|
59
|
-
const nextKf = this.keys[i
|
43
|
+
const nextKf = this.keys[i+1];
|
60
44
|
// if the next
|
61
45
|
if (nextKf.time < time) continue;
|
62
46
|
// tangents are set to Infinity if interpolation is set to constant - in that case we should always return the floored value
|
@@ -1,12 +1,11 @@
|
|
1
|
-
import { Object3D } from "three";
|
2
1
|
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
|
-
|
2
|
+
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
4
3
|
import { addNewComponent } from "../engine/engine_components.js";
|
5
|
-
import {
|
4
|
+
import { Animator } from "./Animator.js";
|
6
5
|
import { Animation } from "./Animation.js";
|
7
|
-
import { Animator } from "./Animator.js";
|
8
6
|
import { GameObject } from "./Component.js";
|
9
7
|
import { PlayableDirector } from "./timeline/PlayableDirector.js";
|
8
|
+
import { Object3D } from "three";
|
10
9
|
|
11
10
|
|
12
11
|
const $objectAnimationKey = Symbol("objectIsAnimatedData");
|
@@ -1,10 +1,9 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import { getParam } from "../../../../engine/engine_utils.js";
|
1
|
+
import { Animator } from "../../../Animator.js";
|
4
2
|
import { Animation } from "../../../Animation.js";
|
5
|
-
import {
|
3
|
+
import { Object3D, AnimationClip } from "three";
|
4
|
+
import { AnimationExtension } from "../extensions/Animation.js";
|
6
5
|
import { Behaviour, GameObject } from "../../../Component.js";
|
7
|
-
import {
|
6
|
+
import { getParam } from "../../../../engine/engine_utils.js";
|
8
7
|
import { PlayAnimationOnClick } from "../extensions/behavior/BehaviourComponents.js";
|
9
8
|
|
10
9
|
const debug = getParam("debugusdz");
|
@@ -1,12 +1,11 @@
|
|
1
|
-
import
|
2
|
-
|
3
|
-
import { Mathf } from "../engine/engine_math.js";
|
4
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
1
|
+
import { Behaviour } from "./Component.js";
|
2
|
+
import type { AnimationActionLoopStyles, AnimationAction, AnimationMixer } from "three";
|
5
3
|
import { getParam } from "../engine/engine_utils.js";
|
6
4
|
import type { AnimatorControllerModel } from "../engine/extensions/NEEDLE_animator_controller_model.js";
|
5
|
+
import { AnimatorController } from "./AnimatorController.js";
|
6
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
7
|
+
import { Mathf } from "../engine/engine_math.js";
|
7
8
|
import { getObjectAnimated } from "./AnimationUtils.js";
|
8
|
-
import { AnimatorController } from "./AnimatorController.js";
|
9
|
-
import { Behaviour } from "./Component.js";
|
10
9
|
|
11
10
|
const debug = getParam("debuganimator");
|
12
11
|
|
@@ -1,16 +1,15 @@
|
|
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";
|
1
4
|
import { AnimationAction, AnimationClip, AnimationMixer, AxesHelper, Euler, KeyframeTrack, LoopOnce, Object3D, Quaternion, Vector3 } from "three";
|
2
|
-
|
5
|
+
import { deepClone, getParam } from "../engine/engine_utils.js";
|
6
|
+
import { Context } from "../engine/engine_setup.js";
|
7
|
+
import { TypeStore } from "../engine/engine_typestore.js";
|
8
|
+
import { SerializationContext, TypeSerializer, assign } from "../engine/engine_serialization_core.js";
|
9
|
+
import { Mathf } from "../engine/engine_math.js";
|
10
|
+
import { isAnimationAction } from "../engine/engine_three_utils.js";
|
3
11
|
import { isDevEnvironment } from "../engine/debug/index.js";
|
4
|
-
import { Mathf } from "../engine/engine_math.js";
|
5
12
|
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
6
|
-
import { assign,SerializationContext, TypeSerializer } from "../engine/engine_serialization_core.js";
|
7
|
-
import { Context } from "../engine/engine_setup.js";
|
8
|
-
import { isAnimationAction } from "../engine/engine_three_utils.js";
|
9
|
-
import { TypeStore } from "../engine/engine_typestore.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";
|
14
13
|
|
15
14
|
const debug = getParam("debuganimatorcontroller");
|
16
15
|
const debugRootMotion = getParam("debugrootmotion");
|
@@ -213,7 +212,6 @@
|
|
213
212
|
console.warn("AnimatorController has not been resolved, can not create model from string", this.model);
|
214
213
|
return null;
|
215
214
|
}
|
216
|
-
if (debug) console.warn("AnimatorController clone()", this.model);
|
217
215
|
// clone runtime controller but dont clone clip or action
|
218
216
|
const clonedModel = deepClone(this.model, (_owner, _key, _value) => {
|
219
217
|
if (_value === null || _value === undefined) return true;
|
@@ -226,8 +224,6 @@
|
|
226
224
|
}
|
227
225
|
// dont clone AnimationClip
|
228
226
|
if (_value["tracks"] !== undefined) return false;
|
229
|
-
// when assigned __concreteInstance during serialization
|
230
|
-
if (_value instanceof AnimatorController) return false;
|
231
227
|
return true;
|
232
228
|
}) as AnimatorControllerModel;
|
233
229
|
console.assert(clonedModel !== this.model);
|
@@ -585,7 +581,7 @@
|
|
585
581
|
}
|
586
582
|
|
587
583
|
private createActions(_animator: Animator) {
|
588
|
-
|
584
|
+
// console.trace(this.model, _animator);
|
589
585
|
for (const layer of this.model.layers) {
|
590
586
|
const sm = layer.stateMachine;
|
591
587
|
for (let index = 0; index < sm.states.length; index++) {
|
@@ -612,13 +608,8 @@
|
|
612
608
|
if (this.animator && state.motion.clips) {
|
613
609
|
// TODO: we have to compare by name because on instantiate we clone objects but not the node object
|
614
610
|
const mapping = state.motion.clips?.find(e => e.node.name === this.animator?.gameObject?.name);
|
615
|
-
|
616
|
-
|
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;
|
611
|
+
// console.log(state.name, mapping?.clip);
|
612
|
+
state.motion.clip = mapping?.clip;
|
622
613
|
}
|
623
614
|
|
624
615
|
// ensure we have a clip to blend to
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { EdgeDetectionMode, SMAAEffect, SMAAPreset } from "postprocessing";
|
2
|
-
|
3
2
|
import { serializable } from "../../../engine/engine_serialization.js";
|
4
3
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
5
4
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,20 +1,22 @@
|
|
1
|
+
export { Behaviour, Component, GameObject } from "./Component.js"
|
1
2
|
export * from "./codegen/components.js";
|
2
|
-
export { Behaviour, Component, GameObject } from "./Component.js"
|
3
3
|
|
4
4
|
// We dont want to export everything in the extensions
|
5
|
-
export
|
5
|
+
export * from "./js-extensions/RGBAColor.js";
|
6
|
+
export * from "./js-extensions/Object3D.js";
|
7
|
+
export * from "./XRFlag.js"
|
8
|
+
|
6
9
|
export * from "./export/index.js"
|
7
|
-
export * from "./js-extensions/Object3D.js";
|
8
|
-
export * from "./js-extensions/RGBAColor.js";
|
9
10
|
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"
|
15
14
|
|
15
|
+
export { ClearFlags } from "./Camera.js"
|
16
|
+
export { type ISceneEventListener } from "./SceneSwitcher.js";
|
17
|
+
|
16
18
|
import "./CameraUtils.js"
|
17
19
|
import "./AnimationUtils.js"
|
18
20
|
|
19
21
|
export { ParticleSystemBaseBehaviour, type QParticle, type QParticleBehaviour } from "./ParticleSystem.js"
|
20
|
-
|
22
|
+
|
@@ -1,42 +1,42 @@
|
|
1
1
|
|
2
|
-
export * from "./
|
2
|
+
export * from "./extensions/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";
|
6
8
|
export * from "./engine_components.js";
|
7
9
|
export * from "./engine_components_internal.js";
|
8
|
-
export * from "./
|
9
|
-
export * from "./engine_constants.js";
|
10
|
+
export * from "./engine_context_registry.js";
|
10
11
|
export * from "./engine_context.js";
|
11
|
-
export * from "./engine_context_registry.js";
|
12
12
|
export * from "./engine_coroutine.js"
|
13
|
-
export * from "./
|
13
|
+
export * from "./engine_constants.js";
|
14
|
+
export * from "./debug/index.js";
|
14
15
|
export * from "./engine_element.js";
|
16
|
+
export * from "./engine_element_loading.js";
|
15
17
|
export * from "./engine_element_attributes.js";
|
16
|
-
export * from "./engine_element_loading.js";
|
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 "./
|
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";
|
21
|
+
export * from "./engine_gameobject.js";
|
26
22
|
export * from "./engine_networking.js";
|
23
|
+
export * from "./engine_networking_types.js";
|
27
24
|
export { syncField } from "./engine_networking_auto.js";
|
28
25
|
export * from "./engine_networking_files.js";
|
29
26
|
export * from "./engine_networking_instantiate.js";
|
30
|
-
export * from "./engine_networking_peer.js";
|
31
27
|
export * from "./engine_networking_streams.js";
|
32
|
-
export * from "./engine_networking_types.js";
|
33
28
|
export * from "./engine_networking_utils.js";
|
29
|
+
export * from "./engine_networking_peer.js";
|
34
30
|
export * from "./engine_patcher.js";
|
31
|
+
export * from "./engine_playerview.js";
|
35
32
|
export * from "./engine_physics.js";
|
36
33
|
export * from "./engine_physics.types.js";
|
37
34
|
export * from "./engine_physics_rapier.js";
|
38
|
-
export * from "./engine_playerview.js";
|
39
35
|
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,11 +44,12 @@
|
|
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";
|
50
47
|
export * from "./engine_utils_screenshot.js";
|
51
48
|
export * from "./engine_web_api.js";
|
52
|
-
export * from "./
|
53
|
-
|
54
|
-
export
|
49
|
+
export * from "./engine_utils.js";
|
50
|
+
|
51
|
+
export { TypeStore, registerType } from "./engine_typestore.js";
|
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";
|
@@ -1,9 +1,8 @@
|
|
1
|
-
import { Object3D } from "three";
|
2
|
-
|
3
|
-
import { AudioSource } from "../../../../AudioSource.js";
|
4
1
|
import { GameObject } from "../../../../Component.js";
|
5
2
|
import type { IUSDExporterExtension } from "../../Extension.js";
|
6
3
|
import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
4
|
+
import { Object3D } from "three";
|
5
|
+
import { AudioSource } from "../../../../AudioSource.js";
|
7
6
|
|
8
7
|
export class AudioExtension implements IUSDExporterExtension {
|
9
8
|
|
@@ -1,8 +1,7 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
1
2
|
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";
|
6
5
|
|
7
6
|
|
8
7
|
export class AudioListener extends Behaviour {
|
@@ -1,14 +1,11 @@
|
|
1
|
-
import {
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
2
2
|
import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudioHelper.js';
|
3
|
-
|
3
|
+
import { AudioListener } from "./AudioListener.js";
|
4
|
+
import * as utils from "../engine/engine_utils.js";
|
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";
|
4
8
|
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";
|
10
|
-
import { AudioListener } from "./AudioListener.js";
|
11
|
-
import { Behaviour, GameObject } from "./Component.js";
|
12
9
|
|
13
10
|
|
14
11
|
const debug = utils.getParam("debugaudio");
|
@@ -68,9 +65,6 @@
|
|
68
65
|
playOnAwake: boolean = false;
|
69
66
|
|
70
67
|
@serializable()
|
71
|
-
preload: boolean = false;
|
72
|
-
|
73
|
-
@serializable()
|
74
68
|
get loop(): boolean {
|
75
69
|
if (this.sound) this._loop = this.sound.getLoop();
|
76
70
|
return this._loop;
|
@@ -146,69 +140,23 @@
|
|
146
140
|
if (!listener && this.context.mainCamera) listener = GameObject.addNewComponent(this.context.mainCamera, AudioListener);
|
147
141
|
if (listener?.listener) {
|
148
142
|
this.sound = new PositionalAudio(listener.listener);
|
149
|
-
this.gameObject
|
150
|
-
|
151
|
-
// this._listener = listener;
|
152
|
-
// this._originalSoundMatrixWorldFunction = this.sound.updateMatrixWorld;
|
153
|
-
// this.sound.updateMatrixWorld = this._onSoundMatrixWorld;
|
143
|
+
this.gameObject.add(this.sound);
|
154
144
|
}
|
155
145
|
else if (debug) console.warn("No audio listener found in scene - can not play audio");
|
156
146
|
}
|
157
147
|
return this.sound;
|
158
148
|
}
|
159
149
|
|
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
|
-
|
189
150
|
public get ShouldPlay(): boolean { return this.shouldPlay; }
|
190
151
|
|
191
|
-
/** Get the audio context from the Sound */
|
192
|
-
public get audioContext() {
|
193
|
-
return this.sound?.context;
|
194
|
-
}
|
195
152
|
|
196
153
|
awake() {
|
197
|
-
if
|
154
|
+
if(debug) console.log(this);
|
198
155
|
this.audioLoader = new AudioLoader();
|
199
156
|
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
|
-
}
|
206
157
|
}
|
207
158
|
|
208
159
|
onEnable(): void {
|
209
|
-
if (this.sound)
|
210
|
-
this.gameObject.add(this.sound);
|
211
|
-
|
212
160
|
if (!AudioSource.userInteractionRegistered) {
|
213
161
|
AudioSource.registerWaitForAllowAudio(() => {
|
214
162
|
if (this.enabled && !this.destroyed && this.shouldPlay)
|
@@ -254,56 +202,50 @@
|
|
254
202
|
this.sound?.setVolume(this.volume);
|
255
203
|
}
|
256
204
|
|
205
|
+
private lerp = (x, y, a) => x * (1 - a) + y * a;
|
206
|
+
|
257
207
|
private createAudio = (buffer?: AudioBuffer) => {
|
258
|
-
if (debug) console.log("
|
208
|
+
if (debug) console.log("audio buffer loaded");
|
209
|
+
AudioSource.registerWaitForAllowAudio(() => {
|
210
|
+
if (debug)
|
211
|
+
console.log("finished loading", buffer);
|
259
212
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
213
|
+
const sound = this.Sound;
|
214
|
+
if (!sound) {
|
215
|
+
console.warn("Failed getting sound", this.name);
|
216
|
+
return;
|
217
|
+
}
|
218
|
+
if (sound.isPlaying)
|
219
|
+
sound.stop();
|
265
220
|
|
266
|
-
|
267
|
-
|
221
|
+
if (buffer)
|
222
|
+
sound.setBuffer(buffer);
|
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();
|
268
233
|
|
269
|
-
|
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;
|
234
|
+
if (debug) console.log(this.name, this.shouldPlay, AudioSource.userInteractionRegistered, this);
|
274
235
|
|
275
|
-
|
276
|
-
|
277
|
-
|
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);
|
236
|
+
if (this.shouldPlay && AudioSource.userInteractionRegistered)
|
237
|
+
this.play();
|
238
|
+
});
|
290
239
|
}
|
291
|
-
private __onAllowAudioCallback = () => {
|
292
|
-
if (this.shouldPlay)
|
293
|
-
this.play();
|
294
|
-
}
|
295
240
|
|
296
241
|
private applySpatialDistanceSettings() {
|
297
242
|
const sound = this.sound;
|
298
243
|
if (!sound) return;
|
299
244
|
this._needUpdateSpatialDistanceSettings = false;
|
300
|
-
const dist =
|
245
|
+
const dist = this.lerp(10 * this._maxDistance / Math.max(0.0001, this.spatialBlend), this._minDistance, this.spatialBlend);
|
301
246
|
if (debug) console.log(this.name, this._minDistance, this._maxDistance, this.spatialBlend, "Ref distance=" + dist);
|
302
247
|
sound.setRefDistance(dist);
|
303
248
|
sound.setMaxDistance(Math.max(0.01, this._maxDistance));
|
304
|
-
// sound.setRolloffFactor(this.spatialBlend);
|
305
|
-
// sound.panner.positionZ.automationRate
|
306
|
-
|
307
249
|
// https://developer.mozilla.org/en-US/docs/Web/API/PannerNode/distanceModel
|
308
250
|
switch (this.rollOffMode) {
|
309
251
|
case AudioRolloffMode.Logarithmic:
|
@@ -327,7 +269,7 @@
|
|
327
269
|
}
|
328
270
|
}
|
329
271
|
|
330
|
-
private
|
272
|
+
private onNewClip(clip?: string | MediaStream) {
|
331
273
|
if (clip) this.clip = clip;
|
332
274
|
if (typeof clip === "string") {
|
333
275
|
if (debug)
|
@@ -343,10 +285,7 @@
|
|
343
285
|
this._lastClipStartedLoading = clip;
|
344
286
|
if (debug)
|
345
287
|
console.log("load audio", clip);
|
346
|
-
|
347
|
-
this._lastClipStartedLoading = null;
|
348
|
-
if (buffer)
|
349
|
-
this.createAudio(buffer);
|
288
|
+
this.audioLoader.load(clip, this.createAudio, () => { }, console.error);
|
350
289
|
}
|
351
290
|
else console.warn("Unsupported audio clip type", clip)
|
352
291
|
}
|
@@ -389,7 +328,6 @@
|
|
389
328
|
if (this.sound && !this.sound.isPlaying) {
|
390
329
|
const muted = this.context.application.muted;
|
391
330
|
if (muted) this.sound.setVolume(0);
|
392
|
-
this.gameObject?.add(this.sound);
|
393
331
|
|
394
332
|
if (this.clip instanceof MediaStream) {
|
395
333
|
|
@@ -473,7 +411,7 @@
|
|
473
411
|
this._hasEnded = true;
|
474
412
|
if (debug)
|
475
413
|
console.log("Audio clip ended", this.clip);
|
476
|
-
this.dispatchEvent(
|
414
|
+
this.sound.dispatchEvent({ type: 'ended', target: this });
|
477
415
|
}
|
478
416
|
|
479
417
|
// this.gameObject.position.x = Math.sin(time.time) * 2;
|
@@ -1,12 +1,11 @@
|
|
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";
|
7
2
|
import { TypeStore } from "../../engine/engine_typestore.js";
|
8
3
|
import { Behaviour, GameObject } from "../Component.js";
|
9
4
|
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";
|
10
9
|
|
11
10
|
export class Avatar_POI {
|
12
11
|
|
@@ -1,10 +1,9 @@
|
|
1
|
-
import { Object3D } from "three";
|
2
|
-
|
3
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
4
|
-
import * as utils from "../../engine/engine_utils.js";
|
5
1
|
import { Behaviour, GameObject } from "../Component.js";
|
6
2
|
import { Voip } from "../Voip.js";
|
7
3
|
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";
|
8
7
|
|
9
8
|
const debug = utils.getParam("debugmouth");
|
10
9
|
|
@@ -1,232 +0,0 @@
|
|
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
|
-
}
|
@@ -1,8 +1,7 @@
|
|
1
1
|
import { Object3D } from "three";
|
2
|
-
|
2
|
+
import { Behaviour, GameObject } from "../Component.js";
|
3
|
+
import { XRFlag, XRState } from "../XRFlag.js";
|
3
4
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
4
|
-
import { Behaviour, GameObject } from "../Component.js";
|
5
|
-
import { XRFlag, XRState } from "../webxr/XRFlag.js";
|
6
5
|
|
7
6
|
|
8
7
|
export class AvatarBlink_Simple extends Behaviour {
|
@@ -1,11 +1,10 @@
|
|
1
|
+
import { Behaviour, GameObject } from "../Component.js";
|
2
|
+
import * as utils from "../../engine/engine_three_utils.js"
|
1
3
|
import * as THREE from "three";
|
4
|
+
import { Avatar_Brain_LookAt } from "./Avatar_Brain_LookAt.js";
|
5
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
2
6
|
import { Object3D } from "three";
|
3
7
|
|
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
|
-
|
9
8
|
export class AvatarEyeLook_Rotation extends Behaviour {
|
10
9
|
|
11
10
|
@serializable(Object3D)
|
@@ -1,13 +1,12 @@
|
|
1
|
-
import { Box3, Object3D, Vector3 } from "three";
|
2
1
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
|
-
|
4
|
-
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
5
|
-
import { getLoader } from "../engine/engine_gltf.js";
|
2
|
+
import * as utils from "../engine/engine_utils.js"
|
6
3
|
import * as loaders from "../engine/engine_loaders.js"
|
7
4
|
import { Context } from "../engine/engine_setup.js";
|
8
|
-
import
|
5
|
+
import { GameObject } from "./Component.js";
|
9
6
|
import { download_file } from "../engine/engine_web_api.js";
|
10
|
-
import {
|
7
|
+
import { getLoader } from "../engine/engine_gltf.js";
|
8
|
+
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
9
|
+
import { Box3, Object3D, Vector3 } from "three";
|
11
10
|
|
12
11
|
const debug = utils.getParam("debugavatar");
|
13
12
|
|
@@ -1,8 +1,7 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { Behaviour } from "./Component.js";
|
3
2
|
import * as params from "../engine/engine_default_parameters.js";
|
4
3
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
-
import {
|
4
|
+
import { AxesHelper as _AxesHelper } from "three";
|
6
5
|
|
7
6
|
export class AxesHelper extends Behaviour {
|
8
7
|
@serializable()
|
@@ -1,12 +1,11 @@
|
|
1
1
|
// import { Canvas } from './Canvas.js';
|
2
|
-
import { AxesHelper, Object3D } from 'three';
|
3
2
|
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';
|
7
3
|
import { Behaviour, GameObject } from "../Component.js";
|
8
4
|
import { EventSystem } from "./EventSystem.js";
|
5
|
+
import { showGizmos } from '../../engine/engine_default_parameters.js';
|
6
|
+
import { AxesHelper, Object3D } from 'three';
|
9
7
|
import type { ICanvas } from './Interfaces.js';
|
8
|
+
import { getParam } from '../../engine/engine_utils.js';
|
10
9
|
export const includesDir = "./include";
|
11
10
|
|
12
11
|
const debug = getParam("debugshadowcomponents");
|
@@ -25,38 +24,22 @@
|
|
25
24
|
|
26
25
|
export const $shadowDomOwner = Symbol("shadowDomOwner");
|
27
26
|
|
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
|
-
*/
|
31
27
|
export class BaseUIComponent extends Behaviour {
|
32
28
|
|
33
|
-
/** Is this object on the root of the UI hierarchy ? */
|
34
29
|
isRoot() { return this.Root?.gameObject === this.gameObject; }
|
35
30
|
|
36
|
-
/** Access the parent canvas component */
|
37
31
|
get canvas() {
|
38
32
|
const cv = this.Root as any as ICanvas;
|
39
33
|
if (cv?.isCanvas) return cv;
|
40
34
|
return null;
|
41
35
|
}
|
42
|
-
/** @deprecated use `canvas` */
|
43
|
-
protected get Canvas() {
|
44
|
-
return this.canvas;
|
45
|
-
}
|
46
36
|
|
47
|
-
/** Mark the UI dirty which will trigger an THREE-Mesh-UI update */
|
48
37
|
markDirty() {
|
49
38
|
EventSystem.markUIDirty(this.context);
|
50
39
|
}
|
51
40
|
|
52
|
-
|
53
|
-
get shadowComponent() { return this._shadowComponent }
|
54
|
-
private set shadowComponent(val: Object3D | null) {
|
55
|
-
this._shadowComponent = val;
|
56
|
-
}
|
41
|
+
shadowComponent: ThreeMeshUI.Block | null = null;
|
57
42
|
|
58
|
-
private _shadowComponent: Object3D | null = null;
|
59
|
-
|
60
43
|
private _controlsChildLayout = true;
|
61
44
|
get controlsChildLayout(): boolean { return this._controlsChildLayout; }
|
62
45
|
set controlsChildLayout(val: boolean) {
|
@@ -75,6 +58,11 @@
|
|
75
58
|
return this._root;
|
76
59
|
}
|
77
60
|
|
61
|
+
// TODO: rename to canvas
|
62
|
+
protected get Canvas() {
|
63
|
+
return this.canvas;
|
64
|
+
}
|
65
|
+
|
78
66
|
// private _intermediate?: Object3D;
|
79
67
|
protected _parentComponent?: BaseUIComponent | null = undefined;
|
80
68
|
|
@@ -89,10 +77,7 @@
|
|
89
77
|
super.onEnable();
|
90
78
|
}
|
91
79
|
|
92
|
-
|
93
|
-
* @param container the three-mesh-ui object to add
|
94
|
-
* @param parent the parent component to add the object to
|
95
|
-
*/
|
80
|
+
//@ts-ignore
|
96
81
|
protected addShadowComponent(container: any, parent?: BaseUIComponent) {
|
97
82
|
|
98
83
|
this.removeShadowComponent();
|
@@ -149,7 +134,21 @@
|
|
149
134
|
if(debug) console.log(this.shadowComponent)
|
150
135
|
}
|
151
136
|
|
152
|
-
|
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) {
|
153
152
|
if (!current) return;
|
154
153
|
// TODO: only traverse our own hierarchy, we can stop if we find another owner
|
155
154
|
if (current[$shadowDomOwner] === undefined || current[$shadowDomOwner] === this) {
|
@@ -172,7 +171,6 @@
|
|
172
171
|
}
|
173
172
|
}
|
174
173
|
|
175
|
-
/** Remove the underlying UI object from the hierarchy */
|
176
174
|
protected removeShadowComponent() {
|
177
175
|
if (this.shadowComponent) {
|
178
176
|
this.shadowComponent.removeFromParent();
|
@@ -1,8 +1,7 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
2
|
+
import * as utils from "./../engine/engine_three_utils.js";
|
1
3
|
import { Vector3 } from "three";
|
2
4
|
|
3
|
-
import * as utils from "./../engine/engine_three_utils.js";
|
4
|
-
import { Behaviour, GameObject } from "./Component.js";
|
5
|
-
|
6
5
|
export class BasicIKConstraint extends Behaviour {
|
7
6
|
|
8
7
|
private from!: GameObject;
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import { getParam } from "../../../../../engine/engine_utils.js";
|
2
1
|
import { GameObject } from "../../../../Component.js";
|
3
2
|
import type { IUSDExporterExtension } from "../../Extension.js";
|
4
3
|
import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
5
4
|
import { BehaviorModel } from "./BehavioursBuilder.js";
|
5
|
+
import { getParam } from "../../../../../engine/engine_utils.js";
|
6
6
|
|
7
7
|
const debug = getParam("debugusdz");
|
8
8
|
|
@@ -1,20 +1,21 @@
|
|
1
|
-
import {
|
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";
|
1
|
+
import { Behaviour, GameObject } from "../../../../Component.js";
|
8
2
|
import { Animator } from "../../../../Animator.js";
|
9
|
-
import { AudioSource } from "../../../../AudioSource.js";
|
10
|
-
import { Behaviour, GameObject } from "../../../../Component.js";
|
11
3
|
import { Renderer } from "../../../../Renderer.js";
|
4
|
+
import { serializable } from "../../../../../engine/engine_serialization_decorator.js";
|
12
5
|
import type { IPointerClickHandler, PointerEventData } from "../../../../ui/PointerEvents.js";
|
13
|
-
import {
|
6
|
+
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";
|
14
10
|
import { USDDocument, USDObject, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
15
|
-
|
11
|
+
|
16
12
|
import type { BehaviorExtension, UsdzBehaviour } from "./Behaviour.js";
|
17
|
-
import { ActionBuilder, ActionModel, AuralMode, BehaviorModel,
|
13
|
+
import { ActionBuilder, ActionModel, AuralMode, BehaviorModel, type IBehaviorElement, MotionType, PlayAction, Space, TriggerBuilder, GroupActionModel, MultiplePerformOperation } from "./BehavioursBuilder.js";
|
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";
|
18
19
|
|
19
20
|
function ensureRaycaster(obj: GameObject) {
|
20
21
|
if (!obj) return;
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import { Object3D } from "three";
|
2
|
+
import { USDDocument, USDObject, USDWriter, makeNameSafeForUSD } from "../../ThreeUSDZExporter.js";
|
2
3
|
|
4
|
+
import { BehaviorExtension } from "./Behaviour.js";
|
3
5
|
import { getParam } from "../../../../../engine/engine_utils.js";
|
4
|
-
import { makeNameSafeForUSD,USDDocument, USDObject, USDWriter } from "../../ThreeUSDZExporter.js";
|
5
|
-
import { BehaviorExtension } from "./Behaviour.js";
|
6
6
|
|
7
7
|
const debug = getParam("debugusdz");
|
8
8
|
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { BlendFunction, BloomEffect, SelectiveBloomEffect } from "postprocessing";
|
2
|
-
|
3
2
|
import { serializable } from "../../../engine/engine_serialization.js";
|
4
3
|
import { PostProcessingEffect } from "../PostProcessingEffect.js";
|
5
4
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,9 +1,8 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { Behaviour } from "./Component.js";
|
2
|
+
import { getParam } from "../engine/engine_utils.js";
|
3
3
|
import { CreateWireCube, Gizmos } from "../engine/engine_gizmos.js";
|
4
4
|
import { getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js";
|
5
|
-
import {
|
6
|
-
import { Behaviour } from "./Component.js";
|
5
|
+
import { Box3, Color, type ColorRepresentation, LineSegments, Object3D, Vector3 } from "three";
|
7
6
|
|
8
7
|
const gizmos = getParam("gizmos");
|
9
8
|
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";
|
7
1
|
import { Behaviour, GameObject } from "../Component.js";
|
8
2
|
import { EventList } from "../EventList.js";
|
3
|
+
import type { IPointerClickHandler, IPointerEnterHandler, IPointerEventHandler, IPointerExitHandler, PointerEventData } from "./PointerEvents.js";
|
4
|
+
import { Image } from "./Image.js";
|
9
5
|
import { RGBAColor } from "../js-extensions/RGBAColor.js";
|
10
|
-
import {
|
11
|
-
import
|
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";
|
12
10
|
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: boolean = false;
|
69
69
|
|
70
70
|
onPointerEnter(_) {
|
71
|
-
this._isHovered += 1;
|
72
71
|
if (debug)
|
73
|
-
console.
|
72
|
+
console.log("Button Enter", this.animationTriggers?.highlightedTrigger, this.animator);
|
73
|
+
this._isHovered = true;
|
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,12 +82,10 @@
|
|
82
82
|
}
|
83
83
|
|
84
84
|
onPointerExit() {
|
85
|
-
this._isHovered -= 1;
|
86
85
|
if (debug)
|
87
|
-
console.log("Button Exit", this.
|
86
|
+
console.log("Button Exit", this.animationTriggers?.highlightedTrigger, this.animator);
|
87
|
+
this._isHovered = false;
|
88
88
|
if (!this.interactable) return;
|
89
|
-
if (this._isHovered > 0) return;
|
90
|
-
this._isHovered = 0;
|
91
89
|
if (this.transition == Transition.Animation && this.animationTriggers && this.animator) {
|
92
90
|
this.animator.setTrigger(this.animationTriggers.normalTrigger);
|
93
91
|
}
|
@@ -122,10 +120,10 @@
|
|
122
120
|
}
|
123
121
|
|
124
122
|
onPointerClick(args: PointerEventData) {
|
125
|
-
if (!this.interactable) return;
|
123
|
+
if (!this.interactable || args.pointerId !== 0) return;
|
126
124
|
|
127
|
-
if (args.button !== 0 && args.event.pointerType === PointerType.Mouse) return;
|
128
125
|
// Button clicks should only run with left mouse button while using mouse
|
126
|
+
if(args.pointerId !== 0 && this.context.input.getIsMouse(args.pointerId)) return;
|
129
127
|
if (debug) {
|
130
128
|
console.warn("Button Click", this.onClick);
|
131
129
|
showBalloonMessage("CLICKED button " + this.name + " at " + this.context.time.frameCount);
|
@@ -1,17 +1,17 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
3
|
-
|
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
|
+
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"
|
4
7
|
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
|
8
|
+
import { getWorldPosition } from "../engine/engine_three_utils.js";
|
5
9
|
import { Gizmos } from "../engine/engine_gizmos.js";
|
6
|
-
|
7
|
-
import {
|
10
|
+
|
11
|
+
import { EquirectangularReflectionMapping, OrthographicCamera, PerspectiveCamera, Ray, SRGBColorSpace, Vector3 } from "three";
|
12
|
+
import { OrbitControls } from "./OrbitControls.js";
|
8
13
|
import { RenderTexture } from "../engine/engine_texture.js";
|
9
|
-
import {
|
10
|
-
import type { ICamera } from "../engine/engine_types.js"
|
11
|
-
import { getParam } from "../engine/engine_utils.js";
|
12
|
-
import { Behaviour, GameObject } from "./Component.js";
|
13
|
-
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
14
|
-
import { OrbitControls } from "./OrbitControls.js";
|
14
|
+
import { Texture } from "three";
|
15
15
|
|
16
16
|
export enum ClearFlags {
|
17
17
|
Skybox = 1,
|
@@ -350,6 +350,7 @@
|
|
350
350
|
if (this._backgroundBlurriness !== undefined)
|
351
351
|
this.context.scene.backgroundBlurriness = this._backgroundBlurriness;
|
352
352
|
if (this._backgroundIntensity !== undefined)
|
353
|
+
//@ts-ignore
|
353
354
|
this.context.scene.backgroundIntensity = this._backgroundIntensity;
|
354
355
|
|
355
356
|
break;
|
@@ -391,7 +392,7 @@
|
|
391
392
|
if (debug)
|
392
393
|
showBalloonMessage("Environment blend mode: " + environmentBlendMode + " on " + navigator.userAgent);
|
393
394
|
let transparent = environmentBlendMode === 'additive' || environmentBlendMode === 'alpha-blend';
|
394
|
-
if (context.
|
395
|
+
if (context.xrSessionMode === XRSessionMode.ImmersiveAR) {
|
395
396
|
if (environmentBlendMode === "opaque") {
|
396
397
|
// workaround for Quest 2 returning opaque when it should be alpha-blend
|
397
398
|
// check user agent if this is the Quest browser and return true if so
|
@@ -1,15 +1,14 @@
|
|
1
|
+
import { OrbitControls } from "./OrbitControls.js";
|
2
|
+
import { addNewComponent, getOrAddComponent } from "../engine/engine_components.js";
|
1
3
|
import { Object3D } from "three";
|
2
|
-
|
4
|
+
import type { ICamera, IContext } from "../engine/engine_types.js";
|
5
|
+
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
6
|
+
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
3
7
|
import { getCameraController } from "../engine/engine_camera.js";
|
4
|
-
import {
|
5
|
-
import { Context } from "../engine/engine_context.js";
|
6
|
-
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
8
|
+
import { Camera, ClearFlags } from "./Camera.js";
|
7
9
|
import { NeedleEngineHTMLElement } from "../engine/engine_element.js";
|
8
|
-
import type { ICamera, IContext } from "../engine/engine_types.js";
|
9
10
|
import { getParam } from "../engine/engine_utils.js";
|
10
|
-
import {
|
11
|
-
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
12
|
-
import { OrbitControls } from "./OrbitControls.js";
|
11
|
+
import { Context } from "../engine/engine_context.js";
|
13
12
|
|
14
13
|
const debug = getParam("debugmissingcamera");
|
15
14
|
|
@@ -1,19 +1,17 @@
|
|
1
|
-
import {
|
2
|
-
import * as ThreeMeshUI from 'three-mesh-ui'
|
3
|
-
|
4
|
-
import { Mathf } from "../../engine/engine_math.js";
|
1
|
+
import { updateRenderSettings as updateRenderSettingsRecursive } from "./Utils.js";
|
5
2
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
6
3
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
7
|
-
import {
|
8
|
-
import {
|
4
|
+
import { BaseUIComponent, UIRootComponent } from "./BaseUIComponent.js";
|
5
|
+
import { GameObject } from "../Component.js";
|
6
|
+
import { Matrix4, Object3D } from "three";
|
7
|
+
import { RectTransform } from "./RectTransform.js";
|
8
|
+
import type { ICanvas, ICanvasEventReceiver, ILayoutGroup, IRectTransform } from "./Interfaces.js";
|
9
9
|
import { Camera } from "../Camera.js";
|
10
|
-
import { GameObject } from "../Component.js";
|
11
|
-
import { BaseUIComponent, UIRootComponent } from "./BaseUIComponent.js";
|
12
10
|
import { EventSystem } from "./EventSystem.js";
|
13
|
-
import
|
11
|
+
import * as ThreeMeshUI from 'three-mesh-ui'
|
12
|
+
import { getParam } from "../../engine/engine_utils.js";
|
14
13
|
import { LayoutGroup } from "./Layout.js";
|
15
|
-
import {
|
16
|
-
import { updateRenderSettings as updateRenderSettingsRecursive } from "./Utils.js";
|
14
|
+
import { Mathf } from "../../engine/engine_math.js";
|
17
15
|
|
18
16
|
export enum RenderMode {
|
19
17
|
ScreenSpaceOverlay = 0,
|
@@ -202,37 +200,19 @@
|
|
202
200
|
}
|
203
201
|
}
|
204
202
|
|
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
|
-
|
227
203
|
onBeforeRenderRoutine = () => {
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
this
|
232
|
-
this.
|
204
|
+
if (this.context.isInVR) {
|
205
|
+
this.onUpdateRenderMode();
|
206
|
+
this.handleLayoutUpdates();
|
207
|
+
// TODO TMUI @swingingtom - For VR this is so we don't have text clipping
|
208
|
+
this.shadowComponent?.updateMatrixWorld(true);
|
209
|
+
this.shadowComponent?.updateWorldMatrix(true, true);
|
210
|
+
this.invokeBeforeRenderEvents();
|
211
|
+
EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context, true);
|
233
212
|
return;
|
234
213
|
}
|
235
214
|
|
215
|
+
this.previousParent = this.gameObject.parent;
|
236
216
|
// console.log(this.previousParent?.name + "/" + this.gameObject.name);
|
237
217
|
|
238
218
|
if (this.renderOnTop || this.screenspace) {
|
@@ -251,12 +231,7 @@
|
|
251
231
|
}
|
252
232
|
|
253
233
|
onAfterRenderRoutine = () => {
|
254
|
-
if
|
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
|
-
}
|
234
|
+
if(this.context.isInVR) return;
|
260
235
|
if ((this.screenspace || this.renderOnTop) && this.previousParent && this.context.mainCamera) {
|
261
236
|
if (this.screenspace) {
|
262
237
|
const camObj = this.context.mainCamera;
|
@@ -301,7 +276,7 @@
|
|
301
276
|
for (const ch of this._rectTransforms) {
|
302
277
|
if (matrixWorldChanged) ch.markDirty();
|
303
278
|
let layout = this._layoutGroups.get(ch.gameObject);
|
304
|
-
if
|
279
|
+
if(ch.isDirty && !layout){
|
305
280
|
layout = ch.gameObject.getComponentInParent(LayoutGroup) as LayoutGroup;
|
306
281
|
}
|
307
282
|
if (ch.isDirty || layout?.isDirty) {
|
@@ -1,9 +1,9 @@
|
|
1
|
-
import {
|
1
|
+
import { Graphic } from "./Graphic.js";
|
2
2
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
3
3
|
import { Behaviour, GameObject } from "../Component.js";
|
4
|
+
import { type ICanvasGroup, type IHasAlphaFactor } from "./Interfaces.js";
|
5
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
4
6
|
import { BaseUIComponent } from "./BaseUIComponent.js";
|
5
|
-
import { Graphic } from "./Graphic.js";
|
6
|
-
import { type ICanvasGroup, type IHasAlphaFactor } from "./Interfaces.js";
|
7
7
|
|
8
8
|
|
9
9
|
export class CanvasGroup extends Behaviour implements ICanvasGroup {
|
@@ -41,6 +41,7 @@
|
|
41
41
|
|
42
42
|
private _buffer: Graphic[] = [];
|
43
43
|
private applyChangesNow() {
|
44
|
+
this._buffer.length = 0;
|
44
45
|
for (const ch of GameObject.getComponentsInChildren(this.gameObject, BaseUIComponent, this._buffer)) {
|
45
46
|
const hasAlphaFactor = ch as any as IHasAlphaFactor;
|
46
47
|
if (hasAlphaFactor.setAlphaFactor)
|
@@ -1,15 +1,14 @@
|
|
1
1
|
import { Quaternion, Ray, Vector2, Vector3 } from "three";
|
2
|
-
|
3
2
|
import { Mathf } from "../engine/engine_math.js";
|
4
|
-
import { RaycastOptions } from "../engine/engine_physics.js";
|
5
3
|
import { serializable } from "../engine/engine_serialization.js";
|
6
|
-
import { getWorldPosition } from "../engine/engine_three_utils.js";
|
7
4
|
import { Collision } from "../engine/engine_types.js";
|
8
|
-
import { getParam } from "../engine/engine_utils.js";
|
9
|
-
import { Animator } from "./Animator.js"
|
10
5
|
import { CapsuleCollider } from "./Collider.js";
|
11
6
|
import { Behaviour, GameObject } from "./Component.js";
|
12
7
|
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";
|
13
12
|
|
14
13
|
const debug = getParam("debugcharactercontroller");
|
15
14
|
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { ChromaticAberrationEffect } from "postprocessing";
|
2
2
|
import { Vector2 } from "three";
|
3
|
-
|
4
3
|
import { serializable } from "../../../engine/engine_serialization.js";
|
5
4
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
6
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,14 +1,13 @@
|
|
1
|
+
import { Behaviour } from "./Component.js";
|
2
|
+
import { Rigidbody } from "./RigidBody.js";
|
3
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
1
4
|
import { Group, Mesh, Vector3 } from "three"
|
2
|
-
|
3
|
-
import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
|
4
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
-
import { getWorldScale } from "../engine/engine_three_utils.js";
|
6
5
|
// import { IColliderProvider, registerColliderProvider } from "../engine/engine_physics.js";
|
7
6
|
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";
|
8
9
|
import { validate } from "../engine/engine_util_decorator.js";
|
9
10
|
import { unwatchWrite, watchWrite } from "../engine/engine_utils.js";
|
10
|
-
import { Behaviour } from "./Component.js";
|
11
|
-
import { Rigidbody } from "./RigidBody.js";
|
12
11
|
|
13
12
|
|
14
13
|
export class Collider extends Behaviour implements ICollider {
|
@@ -106,14 +105,8 @@
|
|
106
105
|
onEnable() {
|
107
106
|
super.onEnable();
|
108
107
|
this.context.physics.engine?.addBoxCollider(this, this.size);
|
109
|
-
watchWrite(this.gameObject.scale, this.updateProperties);
|
110
108
|
}
|
111
109
|
|
112
|
-
onDisable(): void {
|
113
|
-
super.onDisable();
|
114
|
-
unwatchWrite(this.gameObject.scale, this.updateProperties);
|
115
|
-
}
|
116
|
-
|
117
110
|
onValidate(): void {
|
118
111
|
this.updateProperties();
|
119
112
|
}
|
@@ -1,10 +1,9 @@
|
|
1
1
|
import { BrightnessContrastEffect, HueSaturationEffect } from "postprocessing";
|
2
|
-
import { LinearToneMapping, NoToneMapping } from "three";
|
3
|
-
|
4
2
|
import { serializable } from "../../../engine/engine_serialization.js";
|
5
3
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
6
4
|
import { VolumeParameter } from "../VolumeParameter.js";
|
7
5
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
6
|
+
import { LinearToneMapping, NoToneMapping } from "three";
|
8
7
|
|
9
8
|
|
10
9
|
export class ColorAdjustments extends PostProcessingEffect {
|
@@ -1,17 +1,15 @@
|
|
1
|
-
import {
|
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";
|
1
|
+
import { Mathf } from "../engine/engine_math.js";
|
2
|
+
import * as threeutils from "../engine/engine_three_utils.js";
|
5
3
|
import { activeInHierarchyFieldName } from "../engine/engine_constants.js";
|
6
|
-
import {
|
4
|
+
import { Context, FrameEvent } from "../engine/engine_setup.js";
|
7
5
|
import * as main from "../engine/engine_mainloop_utils.js";
|
8
6
|
import { syncDestroy, syncInstantiate } from "../engine/engine_networking_instantiate.js";
|
9
|
-
import {
|
10
|
-
import
|
11
|
-
import
|
12
|
-
import { INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs } from "../engine/engine_xr.js";
|
13
|
-
import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
7
|
+
import type { ConstructorConcrete, SourceIdentifier, IComponent, IGameObject, Constructor, GuidsMap, Collision, ICollider } from "../engine/engine_types.js";
|
8
|
+
import { addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../engine/engine_components.js";
|
9
|
+
import { findByGuid, destroy, InstantiateOptions, instantiate, HideFlags, foreachComponent, markAsInstancedRendered, isActiveInHierarchy, isActiveSelf, isUsingInstancing, setActive, isDestroyed, IInstantiateOptions } from "../engine/engine_gameobject.js";
|
14
10
|
|
11
|
+
import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
|
12
|
+
import { showBalloonWarning, isDevEnvironment } from "../engine/debug/index.js";
|
15
13
|
|
16
14
|
// export interface ISerializationCallbackReceiver {
|
17
15
|
// onBeforeSerialize?(): object | void;
|
@@ -83,8 +81,8 @@
|
|
83
81
|
* @param instance object to instantiate
|
84
82
|
* @param opts options for the instantiation (e.g. with what parent, position, etc.)
|
85
83
|
*/
|
86
|
-
public static instantiate(instance: GameObject | Object3D, opts: IInstantiateOptions | null = null): GameObject {
|
87
|
-
return instantiate(instance, opts) as GameObject;
|
84
|
+
public static instantiate(instance: GameObject | Object3D | null, opts: IInstantiateOptions | null = null): GameObject | null {
|
85
|
+
return instantiate(instance, opts) as GameObject | null;
|
88
86
|
}
|
89
87
|
|
90
88
|
/** Destroys a object on all connected clients (if you are in a networked session)
|
@@ -125,7 +123,7 @@
|
|
125
123
|
main.addScriptToArrays(comp, context!);
|
126
124
|
if (comp.__internalDidAwakeAndStart) return;
|
127
125
|
if (context!.new_script_start.includes(comp) === false) {
|
128
|
-
context!.new_script_start.push(comp as
|
126
|
+
context!.new_script_start.push(comp as Behaviour);
|
129
127
|
}
|
130
128
|
}, true);
|
131
129
|
}
|
@@ -256,7 +254,7 @@
|
|
256
254
|
return getComponentsInParent(go, typeName, arr);
|
257
255
|
}
|
258
256
|
|
259
|
-
public static getAllComponents(go: IGameObject | Object3D):
|
257
|
+
public static getAllComponents(go: IGameObject | Object3D): Behaviour[] {
|
260
258
|
const componentsList = go.userData?.components;
|
261
259
|
const newList = [...componentsList];
|
262
260
|
return newList;
|
@@ -296,7 +294,7 @@
|
|
296
294
|
abstract set worldQuaternion(val: Quaternion);
|
297
295
|
abstract get worldQuaternion(): Quaternion;
|
298
296
|
abstract set worldRotation(val: Vector3);
|
299
|
-
abstract get worldRotation(): Vector3;
|
297
|
+
abstract get worldRotation(): Vector3;
|
300
298
|
abstract set worldScale(val: Vector3);
|
301
299
|
abstract get worldScale(): Vector3;
|
302
300
|
|
@@ -307,28 +305,17 @@
|
|
307
305
|
|
308
306
|
|
309
307
|
|
310
|
-
|
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
|
-
{
|
308
|
+
export class Component implements IComponent, EventTarget {
|
320
309
|
|
321
310
|
get isComponent(): boolean { return true; }
|
322
311
|
|
323
312
|
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 */
|
325
313
|
get context(): Context {
|
326
314
|
return this.__context ?? Context.Current;
|
327
315
|
}
|
328
316
|
set context(context: Context) {
|
329
317
|
this.__context = context;
|
330
318
|
}
|
331
|
-
/** shorthand for `this.context.scene` */
|
332
319
|
get scene(): Scene { return this.context.scene; }
|
333
320
|
|
334
321
|
get layer(): number {
|
@@ -368,7 +355,7 @@
|
|
368
355
|
return this.gameObject?.userData.hideFlags;
|
369
356
|
}
|
370
357
|
|
371
|
-
|
358
|
+
|
372
359
|
get activeAndEnabled(): boolean {
|
373
360
|
if (this.destroyed) return false;
|
374
361
|
if (this.__isEnabled === false) return false;
|
@@ -398,27 +385,19 @@
|
|
398
385
|
this.gameObject[activeInHierarchyFieldName] = val;
|
399
386
|
}
|
400
387
|
|
401
|
-
/** the object this component is attached to. Note that this is a threejs Object3D with some additional features */
|
402
388
|
gameObject!: GameObject;
|
403
|
-
/** the unique identifier for this component */
|
404
389
|
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) */
|
406
390
|
sourceId?: SourceIdentifier;
|
407
391
|
// transform: Object3D = nullObject;
|
408
392
|
|
409
393
|
/** 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) */
|
410
394
|
resolveGuids?(guidsMap: GuidsMap): void;
|
411
395
|
|
412
|
-
/** called once when the component becomes active for the first time
|
413
|
-
* This is the first callback to be called */
|
396
|
+
/** called once when the component becomes active for the first time */
|
414
397
|
awake() { }
|
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
|
-
*/
|
398
|
+
/** called every time when the component gets enabled (this is invoked after awake and before start) */
|
418
399
|
onEnable() { }
|
419
|
-
/** called every time the component gets disabled or if a parent object (or this.gameObject) gets set to invisible */
|
420
400
|
onDisable() { }
|
421
|
-
/** Called when the component gets destroyed */
|
422
401
|
onDestroy() {
|
423
402
|
this.__destroyed = true;
|
424
403
|
}
|
@@ -430,17 +409,11 @@
|
|
430
409
|
/** Called for all scripts when the context gets paused or unpaused */
|
431
410
|
onPausedChanged?(isPaused: boolean, wasPaused: boolean): void;
|
432
411
|
|
433
|
-
/** called at the beginning of a frame (once per component) */
|
434
412
|
start?(): void;
|
435
|
-
/** first callback in a frame (called every frame when implemented) */
|
436
413
|
earlyUpdate?(): void;
|
437
|
-
/** regular callback in a frame (called every frame when implemented) */
|
438
414
|
update?(): void;
|
439
|
-
/** late callback in a frame (called every frame when implemented) */
|
440
415
|
lateUpdate?(): void;
|
441
|
-
/** called before the scene gets rendered in the main update loop */
|
442
416
|
onBeforeRender?(frame: XRFrame | null): void;
|
443
|
-
/** called after the scene was rendered */
|
444
417
|
onAfterRender?(): void;
|
445
418
|
|
446
419
|
onCollisionEnter?(col: Collision);
|
@@ -451,79 +424,18 @@
|
|
451
424
|
onTriggerStay?(col: ICollider);
|
452
425
|
onTriggerExit?(col: ICollider);
|
453
426
|
|
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
|
-
*/
|
507
427
|
startCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): Generator {
|
508
428
|
return this.context.registerCoroutineUpdate(this, routine, evt);
|
509
429
|
}
|
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
|
-
*/
|
430
|
+
|
515
431
|
stopCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): void {
|
516
432
|
this.context.unregisterCoroutineUpdate(routine, evt);
|
517
433
|
}
|
518
434
|
|
519
|
-
/** @returns true if this component was destroyed (`this.destroy()`) or the whole object this component was part of */
|
520
435
|
public get destroyed(): boolean {
|
521
436
|
return this.__destroyed;
|
522
437
|
}
|
523
438
|
|
524
|
-
/**
|
525
|
-
* Destroys this component (and removes it from the object)
|
526
|
-
*/
|
527
439
|
public destroy() {
|
528
440
|
if (this.__destroyed) return;
|
529
441
|
this.__internalDestroy();
|
@@ -552,11 +464,7 @@
|
|
552
464
|
|
553
465
|
/** @internal */
|
554
466
|
constructor() {
|
555
|
-
this.
|
556
|
-
this.__didStart = false;
|
557
|
-
this.__didEnable = false;
|
558
|
-
this.__isEnabled = undefined;
|
559
|
-
this.__destroyed = false;
|
467
|
+
this.__internalNewInstanceCreated();
|
560
468
|
}
|
561
469
|
|
562
470
|
|
@@ -758,6 +666,5 @@
|
|
758
666
|
}
|
759
667
|
}
|
760
668
|
|
761
|
-
|
762
|
-
|
763
|
-
export { Component as Behaviour };
|
669
|
+
export class Behaviour extends Component {
|
670
|
+
}
|
@@ -1,4 +1,3 @@
|
|
1
|
-
/* eslint-disable */
|
2
1
|
// Export types
|
3
2
|
export class __Ignore {}
|
4
3
|
export { ActionBuilder } from "../export/usdz/extensions/behavior/BehavioursBuilder.js";
|
@@ -12,11 +11,11 @@
|
|
12
11
|
export { Animator } from "../Animator.js";
|
13
12
|
export { AnimatorController } from "../AnimatorController.js";
|
14
13
|
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";
|
20
19
|
export { Avatar_Brain_LookAt } from "../avatar/Avatar_Brain_LookAt.js";
|
21
20
|
export { Avatar_MouthShapes } from "../avatar/Avatar_MouthShapes.js";
|
22
21
|
export { Avatar_MustacheShake } from "../avatar/Avatar_MustacheShake.js";
|
@@ -31,6 +30,7 @@
|
|
31
30
|
export { BasicIKConstraint } from "../BasicIKConstraint.js";
|
32
31
|
export { BehaviorExtension } from "../export/usdz/extensions/behavior/Behaviour.js";
|
33
32
|
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,6 +51,7 @@
|
|
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";
|
54
55
|
export { ContactShadows } from "../ContactShadows.js";
|
55
56
|
export { ControlTrackHandler } from "../timeline/TimelineTracks.js";
|
56
57
|
export { CustomBranding } from "../export/usdz/USDZExporter.js";
|
@@ -87,6 +88,7 @@
|
|
87
88
|
export { Image } from "../ui/Image.js";
|
88
89
|
export { InheritVelocityModule } from "../ParticleSystemModules.js";
|
89
90
|
export { InputField } from "../ui/InputField.js";
|
91
|
+
export { Interactable } from "../Interactable.js";
|
90
92
|
export { Light } from "../Light.js";
|
91
93
|
export { LimitVelocityOverLifetimeModule } from "../ParticleSystemModules.js";
|
92
94
|
export { LODGroup } from "../LODGroup.js";
|
@@ -100,7 +102,6 @@
|
|
100
102
|
export { MeshRenderer } from "../Renderer.js";
|
101
103
|
export { MinMaxCurve } from "../ParticleSystemModules.js";
|
102
104
|
export { MinMaxGradient } from "../ParticleSystemModules.js";
|
103
|
-
export { NeedleWebXRHtmlElement } from "../webxr/WebXRButtons.js";
|
104
105
|
export { NestedGltf } from "../NestedGltf.js";
|
105
106
|
export { Networking } from "../Networking.js";
|
106
107
|
export { NoiseModule } from "../ParticleSystemModules.js";
|
@@ -124,6 +125,7 @@
|
|
124
125
|
export { PreliminaryAction } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
|
125
126
|
export { PreliminaryTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
|
126
127
|
export { RawImage } from "../ui/Image.js";
|
128
|
+
export { Raycaster } from "../ui/Raycaster.js";
|
127
129
|
export { Rect } from "../ui/RectTransform.js";
|
128
130
|
export { RectTransform } from "../ui/RectTransform.js";
|
129
131
|
export { ReflectionProbe } from "../ReflectionProbe.js";
|
@@ -151,7 +153,6 @@
|
|
151
153
|
export { SizeOverLifetimeModule } from "../ParticleSystemModules.js";
|
152
154
|
export { SkinnedMeshRenderer } from "../Renderer.js";
|
153
155
|
export { SmoothFollow } from "../SmoothFollow.js";
|
154
|
-
export { SpatialGrabRaycaster } from "../ui/Raycaster.js";
|
155
156
|
export { SpatialHtml } from "../ui/SpatialHtml.js";
|
156
157
|
export { SpatialTrigger } from "../SpatialTrigger.js";
|
157
158
|
export { SpatialTriggerReceiver } from "../SpatialTrigger.js";
|
@@ -166,7 +167,7 @@
|
|
166
167
|
export { SyncedRoom } from "../SyncedRoom.js";
|
167
168
|
export { SyncedTransform } from "../SyncedTransform.js";
|
168
169
|
export { TapGestureTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
|
169
|
-
export { TeleportTarget } from "../webxr/
|
170
|
+
export { TeleportTarget } from "../webxr/WebXRController.js";
|
170
171
|
export { TestRunner } from "../TestRunner.js";
|
171
172
|
export { TestSimulateUserData } from "../TestRunner.js";
|
172
173
|
export { Text } from "../ui/Text.js";
|
@@ -196,16 +197,20 @@
|
|
196
197
|
export { Volume } from "../postprocessing/Volume.js";
|
197
198
|
export { VolumeParameter } from "../postprocessing/VolumeParameter.js";
|
198
199
|
export { VolumeProfile } from "../postprocessing/VolumeProfile.js";
|
200
|
+
export { VRUserState } from "../webxr/WebXRSync.js";
|
201
|
+
export { WebAR } from "../webxr/WebXR.js";
|
199
202
|
export { WebARCameraBackground } from "../webxr/WebARCameraBackground.js";
|
200
203
|
export { WebARSessionRoot } from "../webxr/WebARSessionRoot.js";
|
201
204
|
export { WebXR } from "../webxr/WebXR.js";
|
205
|
+
export { WebXRAvatar } from "../webxr/WebXRAvatar.js";
|
206
|
+
export { WebXRController } from "../webxr/WebXRController.js";
|
202
207
|
export { WebXRImageTracking } from "../webxr/WebXRImageTracking.js";
|
203
208
|
export { WebXRImageTrackingModel } from "../webxr/WebXRImageTracking.js";
|
204
209
|
export { WebXRPlaneTracking } from "../webxr/WebXRPlaneTracking.js";
|
210
|
+
export { WebXRSync } from "../webxr/WebXRSync.js";
|
205
211
|
export { WebXRTrackedImage } from "../webxr/WebXRImageTracking.js";
|
206
|
-
export {
|
207
|
-
export {
|
208
|
-
export {
|
209
|
-
export { XRFlag } from "../webxr/XRFlag.js";
|
212
|
+
export { XRFlag } from "../XRFlag.js";
|
213
|
+
export { XRGrabModel } from "../webxr/WebXRGrabRendering.js";
|
214
|
+
export { XRGrabRendering } from "../webxr/WebXRGrabRendering.js";
|
210
215
|
export { XRRig } from "../webxr/WebXRRig.js";
|
211
|
-
export { XRState } from "../
|
216
|
+
export { XRState } from "../XRFlag.js";
|
@@ -1,11 +1,11 @@
|
|
1
|
+
import { Behaviour } from "./Component.js";
|
2
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
3
|
+
|
1
4
|
import { CustomBlending, DoubleSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, OrthographicCamera, PlaneGeometry, ShaderMaterial, WebGLRenderTarget } from "three";
|
2
5
|
import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js';
|
3
6
|
import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js';
|
4
|
-
|
5
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
6
7
|
import { getParam } from "../engine/engine_utils.js"
|
7
8
|
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,7 +1,6 @@
|
|
1
|
+
import { getErrorCount } from "./debug_overlay.js";
|
2
|
+
import { getParam, isMobileDevice } from "../engine_utils.js";
|
1
3
|
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";
|
5
4
|
|
6
5
|
let consoleInstance: any = null;
|
7
6
|
let consoleHtmlElement: HTMLElement | null = null;
|
@@ -23,11 +22,8 @@
|
|
23
22
|
currentUrl.searchParams.set("console", "1");
|
24
23
|
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());
|
25
24
|
}
|
26
|
-
const isMobile = isMobileDevice()
|
25
|
+
const isMobile = isMobileDevice();
|
27
26
|
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();
|
31
27
|
beginWatchingLogs();
|
32
28
|
createConsole(true);
|
33
29
|
if (isMobile) {
|
@@ -195,7 +191,7 @@
|
|
195
191
|
}
|
196
192
|
`;
|
197
193
|
consoleHtmlElement?.prepend(styles);
|
198
|
-
if (startHidden === true
|
194
|
+
if (startHidden === true)
|
199
195
|
hideDebugConsole();
|
200
196
|
console.log("🌵 Debug console has loaded");
|
201
197
|
}
|
@@ -1,6 +1,6 @@
|
|
1
|
+
import { getParam } from "../engine_utils.js";
|
2
|
+
import { isLocalNetwork } from "../engine_networking_utils.js";
|
1
3
|
import { ContextRegistry } from "../engine_context_registry.js";
|
2
|
-
import { isLocalNetwork } from "../engine_networking_utils.js";
|
3
|
-
import { getParam } from "../engine_utils.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,10 +37,9 @@
|
|
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)
|
41
|
-
{
|
40
|
+
if (isLocal) {
|
42
41
|
if (debug)
|
43
|
-
console.
|
42
|
+
console.log(window.location.hostname);
|
44
43
|
console.error = patchedConsoleError;
|
45
44
|
window.addEventListener("error", (event) => {
|
46
45
|
if (hide) return;
|
@@ -67,10 +66,10 @@
|
|
67
66
|
}
|
68
67
|
|
69
68
|
|
70
|
-
let
|
69
|
+
let errorCount = 0;
|
71
70
|
|
72
71
|
function onReceivedError() {
|
73
|
-
|
72
|
+
errorCount += 1;
|
74
73
|
}
|
75
74
|
|
76
75
|
function onParseError(args: Array<any>) {
|
@@ -1,13 +1,10 @@
|
|
1
|
+
import { addLog, LogType, setAllowOverlayMessages } from "./debug_overlay.js";
|
2
|
+
import { showDebugConsole } from "./debug_console.js";
|
1
3
|
import { isLocalNetwork } from "../engine_networking_utils.js";
|
2
|
-
import { getParam } from "../engine_utils.js";
|
3
|
-
import { showDebugConsole } from "./debug_console.js";
|
4
|
-
import { addLog, LogType, setAllowOverlayMessages } from "./debug_overlay.js";
|
5
4
|
|
6
5
|
export { showDebugConsole }
|
7
6
|
export { LogType, setAllowOverlayMessages };
|
8
7
|
|
9
|
-
const noDevLogs = getParam("nodevlogs");
|
10
|
-
|
11
8
|
/** Displays a debug message on screen for a certain amount of time */
|
12
9
|
export function showBalloonMessage(text: string, logType: LogType = LogType.Log): void {
|
13
10
|
addLog(logType, text);
|
@@ -25,7 +22,6 @@
|
|
25
22
|
|
26
23
|
/** True when the application runs on a local url */
|
27
24
|
export function isDevEnvironment(): boolean {
|
28
|
-
if (noDevLogs) return false;
|
29
25
|
if (_manuallySetDevEnvironment !== undefined) return _manuallySetDevEnvironment;
|
30
26
|
return isLocalNetwork();
|
31
27
|
}
|
@@ -1,6 +1,5 @@
|
|
1
1
|
|
2
2
|
import * as THREE from "three";
|
3
|
-
|
4
3
|
import { syncDestroy } from "../engine/engine_networking_instantiate.js";
|
5
4
|
import { getParam } from "../engine/engine_utils.js";
|
6
5
|
import { BoxHelperComponent } from "./BoxHelperComponent.js";
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import { DepthOfFieldEffect } from "postprocessing";
|
2
|
-
|
2
|
+
import { serializable } from "../../../engine/engine_serialization.js";
|
3
3
|
import { Mathf } from "../../../engine/engine_math.js";
|
4
|
-
import { serializable } from "../../../engine/engine_serialization.js";
|
5
4
|
import { getParam, isMobileDevice } from "../../../engine/engine_utils.js";
|
6
5
|
import { PostProcessingEffect } from "../PostProcessingEffect.js";
|
7
6
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
|
+
import { isMobileDevice } from "../engine/engine_utils.js";
|
2
3
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
3
|
-
import { isMobileDevice } from "../engine/engine_utils.js";
|
4
4
|
import { Behaviour, GameObject } from "./Component.js";
|
5
5
|
|
6
6
|
|
@@ -1,126 +1,104 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import {
|
4
|
-
import {
|
5
|
-
import {
|
1
|
+
import { GameObject } from "./Component.js";
|
2
|
+
import { SyncedTransform } from "./SyncedTransform.js";
|
3
|
+
import type { IPointerDownHandler, IPointerEnterHandler, IPointerEventHandler, IPointerExitHandler, IPointerUpHandler, PointerEventData } from "./ui/PointerEvents.js";
|
4
|
+
import { Context } from "../engine/engine_setup.js";
|
5
|
+
import { Interactable, UsageMarker } from "./Interactable.js";
|
6
|
+
import { Rigidbody } from "./RigidBody.js";
|
7
|
+
import { WebXR } from "./webxr/WebXR.js";
|
8
|
+
import { Avatar_POI } from "./avatar/Avatar_Brain_LookAt.js";
|
6
9
|
import { RaycastOptions } from "../engine/engine_physics.js";
|
7
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
|
-
import { Context } from "../engine/engine_setup.js";
|
9
10
|
import { getWorldPosition, setWorldPosition } from "../engine/engine_three_utils.js";
|
10
|
-
import {
|
11
|
-
import {
|
12
|
-
import {
|
13
|
-
import { Avatar_POI } from "./avatar/Avatar_Brain_LookAt.js";
|
14
|
-
import { Behaviour, GameObject } from "./Component.js";
|
15
|
-
import { UsageMarker } from "./Interactable.js";
|
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";
|
16
14
|
import { OrbitControls } from "./OrbitControls.js";
|
17
|
-
import {
|
18
|
-
import { SyncedTransform } from "./SyncedTransform.js";
|
19
|
-
import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
15
|
+
import { BufferGeometry, Camera, Color, Line, LineBasicMaterial, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Ray, Raycaster, SphereGeometry, Vector2, Vector3 } from "three";
|
20
16
|
import { ObjectRaycaster } from "./ui/Raycaster.js";
|
17
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
21
18
|
|
22
|
-
const debug =
|
19
|
+
const debug = false;
|
23
20
|
|
24
|
-
export enum
|
25
|
-
|
26
|
-
|
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,
|
21
|
+
export enum DragEvents {
|
22
|
+
SelectStart = "selectstart",
|
23
|
+
SelectEnd = "selectend",
|
35
24
|
}
|
36
25
|
|
37
|
-
|
26
|
+
interface SelectArgs {
|
27
|
+
selected: Object3D;
|
28
|
+
attached: Object3D | GameObject | null;
|
29
|
+
}
|
38
30
|
|
39
|
-
// dragPlane (floor, object, view)
|
40
|
-
// snap to surface (snap orientation?)
|
41
|
-
// two-handed drag (scale, rotate, move)
|
42
|
-
// keep upright (no tilt)
|
43
31
|
|
44
|
-
|
45
|
-
|
46
|
-
|
32
|
+
export interface IDragEventListener {
|
33
|
+
onDragStart?();
|
34
|
+
onDragEnd?();
|
35
|
+
}
|
47
36
|
|
48
|
-
|
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;
|
37
|
+
export class DragControls extends Interactable implements IPointerEventHandler {
|
59
38
|
|
60
|
-
|
61
|
-
|
62
|
-
public xrKeepRotation: boolean = false;
|
39
|
+
private static _active: number = 0;
|
40
|
+
public static get HasAnySelected(): boolean { return this._active > 0; }
|
63
41
|
|
64
|
-
/**
|
42
|
+
/** Show's drag gizmos when enabled */
|
65
43
|
@serializable()
|
66
|
-
public
|
44
|
+
public showGizmo: boolean = true;
|
67
45
|
|
68
|
-
/** When enabled
|
46
|
+
/** When enabled DragControls will drag vertically when the object is viewed from a low angle */
|
69
47
|
@serializable()
|
70
|
-
public
|
48
|
+
public useViewAngle: boolean = true;
|
71
49
|
|
72
|
-
|
73
|
-
//
|
50
|
+
public transformSelf: boolean = true;
|
51
|
+
// public transformGroup: boolean = true;
|
52
|
+
// public targets: Object3D[] | null = null;
|
74
53
|
|
75
|
-
|
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;
|
54
|
+
// private controls: Control | null = null;
|
80
55
|
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;
|
91
56
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
57
|
+
private selectStartEventListener: ((controls: DragControls, args: SelectArgs) => void)[] = [];
|
58
|
+
private selectEndEventListener: Array<Function> = [];
|
59
|
+
private _dragHelper: DragHelper | null = null;
|
60
|
+
|
61
|
+
constructor() {
|
62
|
+
super();
|
63
|
+
this.selectStartEventListener = [];
|
64
|
+
this.selectEndEventListener = [];
|
65
|
+
this._dragDelta = new Vector2();
|
97
66
|
}
|
98
67
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
68
|
+
|
69
|
+
// TODO: Update DragEventListener code
|
70
|
+
addDragEventListener(type: DragEvents, cb: (ctrls: DragControls, args: SelectArgs) => void | Function) {
|
71
|
+
switch (type) {
|
72
|
+
case DragEvents.SelectStart:
|
73
|
+
this.selectStartEventListener.push(cb);
|
74
|
+
break;
|
75
|
+
case DragEvents.SelectEnd:
|
76
|
+
this.selectEndEventListener.push(cb);
|
77
|
+
break;
|
78
|
+
}
|
109
79
|
}
|
110
80
|
|
81
|
+
|
82
|
+
|
111
83
|
start() {
|
112
84
|
this.orbit = GameObject.findObjectOfType(OrbitControls, this.context);
|
113
|
-
if (!this.gameObject.getComponentInParent(ObjectRaycaster))
|
85
|
+
if (!this.gameObject.getComponentInParent(ObjectRaycaster)) {
|
114
86
|
this.gameObject.addNewComponent(ObjectRaycaster);
|
87
|
+
}
|
115
88
|
}
|
116
89
|
|
90
|
+
private static lastHovered: Object3D;
|
91
|
+
private _draggingRigidbodies: Rigidbody[] = [];
|
92
|
+
|
117
93
|
private allowEdit(_obj: Object3D | null = null) {
|
118
94
|
return this.context.connection.allowEditing;
|
119
95
|
}
|
120
96
|
|
121
97
|
onPointerEnter(evt: PointerEventData) {
|
122
98
|
if (!this.allowEdit(this.gameObject)) return;
|
123
|
-
if (
|
99
|
+
if (WebXR.IsInWebXR) return;
|
100
|
+
// const interactable = GameObject.getComponentInParent(evt.object, Interactable);
|
101
|
+
// if (!interactable) return;
|
124
102
|
const dc = GameObject.getComponentInParent(evt.object, DragControls);
|
125
103
|
if (!dc || dc !== this) return;
|
126
104
|
DragControls.lastHovered = evt.object;
|
@@ -129,121 +107,83 @@
|
|
129
107
|
|
130
108
|
onPointerExit(evt: PointerEventData) {
|
131
109
|
if (!this.allowEdit(this.gameObject)) return;
|
132
|
-
if (
|
110
|
+
if (WebXR.IsInWebXR) return;
|
133
111
|
if (DragControls.lastHovered !== evt.object) return;
|
112
|
+
// const interactable = GameObject.getComponentInParent(evt.object, Interactable);
|
113
|
+
// if (!interactable) return;
|
134
114
|
this.context.domElement.style.cursor = 'auto';
|
135
115
|
}
|
136
116
|
|
117
|
+
private _waitingForDragStart: PointerEventData | null = null;
|
118
|
+
|
137
119
|
onPointerDown(args: PointerEventData) {
|
138
120
|
if (!this.allowEdit(this.gameObject)) return;
|
139
|
-
if (
|
140
|
-
DragControls.
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
-
}
|
121
|
+
if (WebXR.IsInWebXR) return;
|
122
|
+
DragControls._active += 1;
|
123
|
+
this._dragDelta.set(0, 0);
|
124
|
+
this._didDrag = false;
|
125
|
+
// Clone to not modify the original event (and this event is used in the actual onDragStart method)
|
126
|
+
this._waitingForDragStart = args.clone();
|
127
|
+
args.stopPropagation();
|
128
|
+
// disabling pointer controls here already, otherwise we get a few frames of movement event in orbit controls and this will rotate the camera sligthly AFTER drag controls dragging ends.
|
129
|
+
if (this.orbit) this.orbit.enabled = false;
|
172
130
|
}
|
173
131
|
|
174
132
|
onPointerMove(args: PointerEventData) {
|
175
|
-
if
|
133
|
+
if(this._isDragging || this._waitingForDragStart !== null) args.use();
|
176
134
|
}
|
177
135
|
|
178
136
|
onPointerUp(args: PointerEventData) {
|
179
|
-
|
180
|
-
if(debug) Gizmos.DrawLabel(args.point ?? this.gameObject.worldPosition, "POINTERUP:" + args.pointerId + ", " + args.button, .03, 3);
|
181
|
-
|
137
|
+
this._waitingForDragStart = null;
|
182
138
|
if (!this.allowEdit(this.gameObject)) return;
|
183
|
-
if (
|
184
|
-
|
139
|
+
if (DragControls._active > 0)
|
140
|
+
DragControls._active -= 1;
|
141
|
+
if (WebXR.IsInWebXR) return;
|
142
|
+
this.onDragEnd(args);
|
143
|
+
args.stopPropagation();
|
144
|
+
if (this.orbit) this.orbit.enabled = true;
|
145
|
+
}
|
185
146
|
|
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
147
|
|
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
|
-
}
|
210
|
-
}
|
211
|
-
|
212
148
|
update(): void {
|
149
|
+
if (WebXR.IsInWebXR) return;
|
213
150
|
|
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
|
-
|
221
151
|
// drag start only after having dragged for some pixels
|
222
|
-
if (this.
|
152
|
+
if (this._waitingForDragStart) {
|
223
153
|
if (!this._didDrag) {
|
224
|
-
// this is so we can e.g. process clicks without having a drag change the position
|
225
|
-
//
|
226
|
-
|
154
|
+
// this is so we can e.g. process clicks without having a drag change the position
|
155
|
+
// e.g. a click to rotate the object
|
156
|
+
const delta = this.context.input.getPointerPositionDelta(0);
|
157
|
+
if (delta)
|
158
|
+
this._dragDelta.add(delta);
|
159
|
+
if (this._dragDelta.length() > 2)
|
227
160
|
this._didDrag = true;
|
228
161
|
else return;
|
229
162
|
}
|
230
|
-
const args = this.
|
231
|
-
this.
|
232
|
-
this.
|
163
|
+
const args = this._waitingForDragStart;
|
164
|
+
this._waitingForDragStart = null;
|
165
|
+
this.onDragStart(args);
|
233
166
|
}
|
234
167
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
168
|
+
if (this._dragHelper && this._dragHelper.hasSelected) {
|
169
|
+
this.onUpdateDrag();
|
170
|
+
}
|
171
|
+
|
172
|
+
if (this._dragHelper?.hasSelected === false || (this._activePointerId !== undefined && this.context.input.getPointerPressed(this._activePointerId) === false)) {
|
173
|
+
this.onDragEnd(null);
|
174
|
+
}
|
240
175
|
}
|
241
176
|
|
242
|
-
|
243
|
-
private
|
177
|
+
private _isDragging: boolean = false;
|
178
|
+
private _marker: UsageMarker | null = null;
|
179
|
+
private _dragDelta!: Vector2;
|
180
|
+
private _didDrag: boolean = false;
|
181
|
+
private _activePointerId?: number;
|
182
|
+
|
183
|
+
private onDragStart(evt: PointerEventData) {
|
244
184
|
if (!this._dragHelper) {
|
245
185
|
if (this.context.mainCamera)
|
246
|
-
this._dragHelper = new
|
186
|
+
this._dragHelper = new DragHelper(this.context.mainCamera);
|
247
187
|
else
|
248
188
|
return;
|
249
189
|
}
|
@@ -252,17 +192,46 @@
|
|
252
192
|
const dc = GameObject.getComponentInParent(evt.object, DragControls);
|
253
193
|
if (!dc || dc !== this) return;
|
254
194
|
|
255
|
-
const object = this.targetObject || this.gameObject;
|
256
195
|
|
257
|
-
|
196
|
+
let object: Object3D = evt.object;
|
258
197
|
|
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;
|
259
228
|
this._isDragging = true;
|
260
229
|
this._dragHelper.setSelected(object, this.context);
|
261
230
|
if (this.orbit) this.orbit.enabled = false;
|
262
231
|
|
263
232
|
const sync = GameObject.getComponentInChildren(object, SyncedTransform);
|
264
|
-
if (debug)
|
265
|
-
|
233
|
+
if (debug)
|
234
|
+
console.log("DRAG START", sync, object);
|
266
235
|
if (sync) {
|
267
236
|
sync.fastMode = true;
|
268
237
|
sync?.requestOwnership();
|
@@ -270,31 +239,30 @@
|
|
270
239
|
|
271
240
|
this._marker = GameObject.addNewComponent(object, UsageMarker);
|
272
241
|
|
242
|
+
// console.log(object, this._marker);
|
243
|
+
|
273
244
|
this._draggingRigidbodies.length = 0;
|
274
245
|
const rbs = GameObject.getComponentsInChildren(object, Rigidbody);
|
275
246
|
if (rbs)
|
276
247
|
this._draggingRigidbodies.push(...rbs);
|
248
|
+
|
249
|
+
const l = nameofFactory<IDragEventListener>();
|
250
|
+
GameObject.invokeOnChildren(this._dragHelper.selected, l("onDragStart"));
|
277
251
|
}
|
278
252
|
|
279
|
-
|
280
|
-
private onAnyDragUpdate() {
|
253
|
+
private onUpdateDrag() {
|
281
254
|
if (!this._dragHelper) return;
|
282
255
|
this._dragHelper.showGizmo = this.showGizmo;
|
256
|
+
this._dragHelper.useViewAngle = this.useViewAngle;
|
283
257
|
|
284
258
|
this._dragHelper.onUpdate(this.context);
|
285
259
|
for (const rb of this._draggingRigidbodies) {
|
286
260
|
rb.wakeUp();
|
287
261
|
rb.resetVelocities();
|
288
|
-
rb.resetForcesAndTorques();
|
289
262
|
}
|
290
|
-
|
291
|
-
const object = this.targetObject || this.gameObject;
|
292
|
-
|
293
|
-
InstancingUtil.markDirty(object);
|
294
263
|
}
|
295
264
|
|
296
|
-
|
297
|
-
private onLastDragEnd(evt: PointerEventData | null) {
|
265
|
+
private onDragEnd(evt: PointerEventData | null) {
|
298
266
|
if (!this || !this._isDragging) return;
|
299
267
|
this._isDragging = false;
|
300
268
|
if (!this._dragHelper) return;
|
@@ -303,7 +271,8 @@
|
|
303
271
|
}
|
304
272
|
this._draggingRigidbodies.length = 0;
|
305
273
|
const selected = this._dragHelper.selected;
|
306
|
-
if (debug)
|
274
|
+
if (debug)
|
275
|
+
console.log("DRAG END", selected, selected?.visible)
|
307
276
|
this._dragHelper.setSelected(null, this.context);
|
308
277
|
if (this.orbit) this.orbit.enabled = true;
|
309
278
|
if (evt?.object) {
|
@@ -313,761 +282,23 @@
|
|
313
282
|
// sync?.requestOwnership();
|
314
283
|
}
|
315
284
|
}
|
316
|
-
if (this._marker)
|
285
|
+
if (this._marker) {
|
317
286
|
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;
|
355
287
|
}
|
356
|
-
|
357
|
-
|
358
|
-
|
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);
|
288
|
+
// raise event
|
289
|
+
for (const listener of this.selectEndEventListener) {
|
290
|
+
listener(this);
|
364
291
|
}
|
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);
|
380
292
|
|
381
|
-
|
382
|
-
|
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
|
-
}
|
293
|
+
const l = nameofFactory<IDragEventListener>();
|
294
|
+
GameObject.invokeOnChildren(selected, l("onDragEnd"));
|
402
295
|
}
|
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
|
-
}
|
559
296
|
}
|
560
297
|
|
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
|
-
}
|
575
298
|
|
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 {
|
580
299
|
|
581
|
-
|
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; }
|
300
|
+
class DragHelper {
|
586
301
|
|
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
|
-
|
1071
302
|
showGizmo: boolean = true;
|
1072
303
|
useViewAngle: boolean = true;
|
1073
304
|
|
@@ -1105,12 +336,13 @@
|
|
1105
336
|
constructor(camera: Camera) {
|
1106
337
|
this._camera = camera;
|
1107
338
|
|
1108
|
-
const line = new Line(
|
339
|
+
const line = new Line(DragHelper.geometry);
|
1109
340
|
const mat = line.material as LineBasicMaterial;
|
1110
341
|
mat.color = new Color(.4, .4, .4);
|
1111
342
|
line.layers.set(2);
|
1112
343
|
line.name = 'line';
|
1113
344
|
line.scale.y = 1;
|
345
|
+
// line.matrixAutoUpdate = false;
|
1114
346
|
this._groundLine = line;
|
1115
347
|
|
1116
348
|
const geometry = new SphereGeometry(.5, 22, 22);
|
@@ -1125,12 +357,13 @@
|
|
1125
357
|
if (this._selected && context) {
|
1126
358
|
for (const rb of this._rbs) {
|
1127
359
|
rb.wakeUp();
|
360
|
+
// if (!rb.smoothedVelocity) continue;
|
1128
361
|
rb.setVelocity(0, 0, 0);
|
1129
362
|
}
|
1130
363
|
}
|
1131
364
|
|
1132
365
|
if (this._selected) {
|
1133
|
-
|
366
|
+
|
1134
367
|
Avatar_POI.Remove(context, this._selected);
|
1135
368
|
}
|
1136
369
|
|
@@ -1152,8 +385,6 @@
|
|
1152
385
|
console.error("DragHelper: no context");
|
1153
386
|
return;
|
1154
387
|
}
|
1155
|
-
|
1156
|
-
// TODO move somewhere else
|
1157
388
|
Avatar_POI.Add(context, this._selected, null);
|
1158
389
|
|
1159
390
|
this._groundOffsetFactor = 0;
|
@@ -1161,6 +392,7 @@
|
|
1161
392
|
this._groundOffset.set(0, 0, 0);
|
1162
393
|
this._requireUpdateGroundPlane = true;
|
1163
394
|
|
395
|
+
// this._rbs = GameObject.getComponentsInChildren(this._selected, Rigidbody);
|
1164
396
|
this.onUpdateScreenSpacePlane();
|
1165
397
|
}
|
1166
398
|
}
|
@@ -1170,16 +402,6 @@
|
|
1170
402
|
private _didDragOnGroundPlaneLastFrame: boolean = false;
|
1171
403
|
|
1172
404
|
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
|
-
/*
|
1183
405
|
if (!this._context) return;
|
1184
406
|
|
1185
407
|
const mainKey: KeyCode = "Space";
|
@@ -1266,7 +488,6 @@
|
|
1266
488
|
this.onDidUpdate();
|
1267
489
|
}
|
1268
490
|
}
|
1269
|
-
*/
|
1270
491
|
}
|
1271
492
|
|
1272
493
|
private onUpdateWorldPosition(wp: Vector3, pointOnPlane: Vector3 | null, heightOnly: boolean) {
|
@@ -1328,6 +549,18 @@
|
|
1328
549
|
this._groundOffset.copy(this._intersection).sub(wp);
|
1329
550
|
}
|
1330
551
|
|
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
|
+
|
1331
564
|
private contains(obj: Object3D, toSearch: Object3D): boolean {
|
1332
565
|
if (obj === toSearch) return true;
|
1333
566
|
if (obj.children) {
|
@@ -1,11 +1,10 @@
|
|
1
|
-
import
|
2
|
-
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
2
|
+
import { RaycastOptions } from "../engine/engine_physics.js";
|
3
3
|
import * as files from "../engine/engine_networking_files.js";
|
4
|
-
import { RaycastOptions } from "../engine/engine_physics.js";
|
5
4
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
+
import { Networking } from "../engine-components/Networking.js";
|
6
|
+
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
6
7
|
import { getParam } from "../engine/engine_utils.js";
|
7
|
-
import { Networking } from "../engine-components/Networking.js";
|
8
|
-
import { Behaviour, GameObject } from "./Component.js";
|
9
8
|
|
10
9
|
const debug = getParam("debugdroplistener");
|
11
10
|
|
@@ -1,27 +1,22 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import {
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
2
|
+
import { WebXRController, ControllerEvents } from "./webxr/WebXRController.js";
|
3
|
+
import { DragControls, DragEvents } from "./DragControls.js";
|
4
|
+
import { Interactable } from "./Interactable.js";
|
5
|
+
import { Animation } from "./Animation.js";
|
6
|
+
import { Vector3, Quaternion, Object3D } from "three";
|
7
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
8
|
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
5
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
6
|
-
import { Behaviour, GameObject } from "./Component.js";
|
7
|
-
import { DragControls } from "./DragControls.js";
|
8
|
-
import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
9
|
-
import { ObjectRaycaster } from "./ui/Raycaster.js";
|
10
9
|
|
11
|
-
export class Duplicatable extends
|
10
|
+
export class Duplicatable extends Interactable {
|
12
11
|
|
13
|
-
/** Duplicates will be parented into the set object. If not defined, this GameObject will be used as parent. */
|
14
12
|
@serializable(Object3D)
|
15
13
|
parent: GameObject | null = null;
|
16
|
-
|
17
|
-
/** The object to be duplicated */
|
18
14
|
@serializable(Object3D)
|
19
15
|
object: GameObject | null = null;
|
20
16
|
|
21
17
|
// limit max object spawn count per interval
|
22
18
|
@serializable()
|
23
19
|
limitCount = 10;
|
24
|
-
|
25
20
|
@serializable()
|
26
21
|
limitInterval = 60;
|
27
22
|
|
@@ -29,7 +24,17 @@
|
|
29
24
|
private _startPosition: THREE.Vector3 | null = null;
|
30
25
|
private _startQuaternion: THREE.Quaternion | null = null;
|
31
26
|
|
32
|
-
|
27
|
+
awake(): void {
|
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);
|
33
38
|
if (this.object) {
|
34
39
|
if (this.object as any === this.gameObject) {
|
35
40
|
console.error("Can not duplicate self");
|
@@ -43,45 +48,34 @@
|
|
43
48
|
this._startQuaternion = this.object.quaternion?.clone() ?? new Quaternion(0, 0, 0, 1);
|
44
49
|
}
|
45
50
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
+
const drag = GameObject.getComponentInParent(this.gameObject, DragControls);
|
52
|
+
if (drag) {
|
53
|
+
drag.addDragEventListener(DragEvents.SelectStart, (_ctrls, args) => {
|
54
|
+
if (this._currentCount >= this.limitCount) {
|
55
|
+
args.attached = null;
|
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
|
+
});
|
51
64
|
}
|
65
|
+
else console.warn("Could no find drag controls in parent", this.name);
|
52
66
|
|
53
|
-
|
54
|
-
this.
|
67
|
+
WebXRController.addEventListener(ControllerEvents.SelectStart, (_controller: WebXRController, args: { selected: THREE.Object3D, grab: THREE.Object3D | GameObject | null }) => {
|
68
|
+
if (this._currentCount >= this.limitCount) {
|
69
|
+
args.grab = null;
|
70
|
+
return;
|
71
|
+
}
|
72
|
+
const res = this.handleDuplication(args.selected);
|
73
|
+
if (res) args.grab = res;
|
74
|
+
});
|
55
75
|
|
56
76
|
this.cloneLimitIntervalFn();
|
57
77
|
}
|
58
78
|
|
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
|
-
|
85
79
|
private cloneLimitIntervalFn() {
|
86
80
|
if (this.destroyed) return;
|
87
81
|
if (this._currentCount > 0) {
|
@@ -92,39 +86,62 @@
|
|
92
86
|
}, (this.limitInterval / this.limitCount) * 1000);
|
93
87
|
}
|
94
88
|
|
95
|
-
private handleDuplication(): THREE.Object3D | null {
|
89
|
+
private handleDuplication(selected: THREE.Object3D): THREE.Object3D | null {
|
90
|
+
if (this._currentCount >= this.limitCount) return null;
|
96
91
|
if (!this.object) return null;
|
97
|
-
if (this.
|
98
|
-
if (this.object as any === this.gameObject) return null;
|
92
|
+
if (selected === this.gameObject || this.handleMultiObject(selected)) {
|
99
93
|
|
100
|
-
|
94
|
+
if (this.object as any === this.gameObject) return null;
|
95
|
+
this.object.visible = true;
|
101
96
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
97
|
+
if (this._startPosition)
|
98
|
+
this.object.position.copy(this._startPosition);
|
99
|
+
if (this._startQuaternion)
|
100
|
+
this.object.quaternion.copy(this._startQuaternion);
|
106
101
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
102
|
+
const opts = new InstantiateOptions();
|
103
|
+
if (!this.parent) this.parent = this.gameObject.parent as GameObject;
|
104
|
+
if (this.parent) {
|
105
|
+
opts.parent = this.parent.guid ?? this.parent.userData?.guid;
|
106
|
+
opts.keepWorldPosition = true;
|
107
|
+
}
|
108
|
+
opts.position = this.worldPosition;
|
109
|
+
opts.rotation = this.worldQuaternion;
|
110
|
+
opts.context = this.context;
|
111
|
+
this._currentCount += 1;
|
112
|
+
|
113
|
+
const newInstance = GameObject.instantiateSynced(this.object as GameObject, opts) as GameObject;
|
114
|
+
console.assert(newInstance !== this.object, "Duplicated object is original");
|
115
|
+
this.object.visible = false;
|
116
|
+
|
117
|
+
// see if this fixes object being offset when duplicated and dragged - it looks like three clone has shared position/quaternion objects?
|
118
|
+
if (this._startPosition)
|
119
|
+
this.object.position.clone().copy(this._startPosition);
|
120
|
+
if (this._startQuaternion)
|
121
|
+
this.object.quaternion.clone().copy(this._startQuaternion);
|
122
|
+
|
123
|
+
return newInstance;
|
112
124
|
}
|
113
|
-
|
114
|
-
|
115
|
-
opts.context = this.context;
|
116
|
-
this._currentCount += 1;
|
125
|
+
return null;
|
126
|
+
}
|
117
127
|
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
+
}
|
121
133
|
|
122
|
-
|
123
|
-
if (
|
124
|
-
|
125
|
-
if (
|
126
|
-
|
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
|
+
}
|
127
146
|
|
128
|
-
return newInstance;
|
129
|
-
}
|
130
147
|
}
|
@@ -1,14 +1,13 @@
|
|
1
|
+
import { deepClone, getParam, resolveUrl } from "../engine/engine_utils.js";
|
2
|
+
import { SerializationContext, TypeSerializer, assign } from "./engine_serialization_core.js";
|
3
|
+
import { Context } from "./engine_setup.js";
|
1
4
|
import { Group, Object3D, Texture, TextureLoader } from "three";
|
2
|
-
|
3
|
-
import { deepClone, getParam, resolveUrl } from "../engine/engine_utils.js";
|
4
|
-
import { destroy, IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
|
5
|
-
import { getLoader } from "./engine_gltf.js";
|
6
5
|
import { processNewScripts } from "./engine_mainloop_utils.js";
|
7
6
|
import { registerPrefabProvider, syncInstantiate } from "./engine_networking_instantiate.js";
|
8
|
-
import {
|
9
|
-
import {
|
7
|
+
import { download } from "./engine_web_api.js";
|
8
|
+
import { getLoader } from "./engine_gltf.js";
|
10
9
|
import type { IComponent, IGameObject, SourceIdentifier } from "./engine_types.js";
|
11
|
-
import {
|
10
|
+
import { destroy, IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
|
12
11
|
|
13
12
|
const debug = getParam("debugaddressables");
|
14
13
|
|
@@ -1,8 +1,7 @@
|
|
1
|
+
import { InternalUsageTrackerPlugin } from "./extensions/usage_tracker.js";
|
1
2
|
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";
|
6
5
|
|
7
6
|
|
8
7
|
export class AssetDatabase {
|
@@ -1,8 +1,7 @@
|
|
1
|
+
import type { ICameraController } from "./engine_types.js";
|
1
2
|
import { Camera, Object3D } from "three";
|
2
3
|
|
3
|
-
import type { ICameraController } from "./engine_types.js";
|
4
4
|
|
5
|
-
|
6
5
|
const $cameraController = Symbol("cameraController");
|
7
6
|
|
8
7
|
export function getCameraController(cam: Camera): ICameraController | null {
|
@@ -1,13 +1,12 @@
|
|
1
1
|
import { Object3D, Scene } from "three";
|
2
|
-
|
2
|
+
import type { Constructor, ConstructorConcrete, IComponent, IGameObject } from "./engine_types.js";
|
3
|
+
import { Context, registerComponent } from "./engine_setup.js";
|
4
|
+
import { getParam } from "./engine_utils.js";
|
5
|
+
import { removeScriptFromContext, updateActiveInHierarchyWithoutEventCall } from "./engine_mainloop_utils.js";
|
6
|
+
import { activeInHierarchyFieldName } from "./engine_constants.js";
|
3
7
|
import { apply } from "../engine-components/js-extensions/Object3D.js";
|
8
|
+
import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
|
4
9
|
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";
|
9
|
-
import type { Constructor, ConstructorConcrete, IComponent, IGameObject } from "./engine_types.js";
|
10
|
-
import { getParam } from "./engine_utils.js";
|
11
10
|
|
12
11
|
const debug = getParam("debuggetcomponent");
|
13
12
|
|
@@ -8,22 +8,19 @@
|
|
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";`)
|
12
11
|
|
13
12
|
declare const NEEDLE_ENGINE_VERSION: string
|
14
13
|
declare const NEEDLE_ENGINE_GENERATOR: string;
|
15
|
-
declare const NEEDLE_PROJECT_BUILD_TIME: string;
|
16
14
|
|
17
15
|
// Make sure to wrap the new global this define in underscores to prevent the bundler from replacing it with the actual value
|
18
16
|
tryEval(`globalThis["__NEEDLE_ENGINE_VERSION__"] = "` + NEEDLE_ENGINE_VERSION + `";`)
|
19
17
|
tryEval(`globalThis["__NEEDLE_ENGINE_GENERATOR__"] = "` + NEEDLE_ENGINE_GENERATOR + `";`)
|
20
|
-
tryEval(`globalThis["__NEEDLE_PROJECT_BUILD_TIME__"] = "` + NEEDLE_PROJECT_BUILD_TIME + `";`)
|
21
18
|
|
22
19
|
|
20
|
+
|
23
21
|
export const VERSION = NEEDLE_ENGINE_VERSION;
|
24
22
|
export const GENERATOR = NEEDLE_ENGINE_GENERATOR;
|
25
|
-
|
26
|
-
if (debug) console.log(`Engine version: ${VERSION} (generator: ${GENERATOR})\nProject built at ${BUILD_TIME}`);
|
23
|
+
if (debug) console.log(`Engine version: ${VERSION} (generator: ${GENERATOR})`);
|
27
24
|
|
28
25
|
export const activeInHierarchyFieldName = "needle_isActiveInHierarchy";
|
29
26
|
export const builtinComponentKeyName = "builtin_components";
|
@@ -1,36 +1,41 @@
|
|
1
|
-
import { EffectComposer, RenderPass } from "postprocessing";
|
2
1
|
import {
|
3
2
|
BufferGeometry, Cache, Camera, Clock, Color, DepthTexture, Group,
|
4
3
|
Material, NearestFilter, NoToneMapping, Object3D, PCFSoftShadowMap,
|
5
4
|
PerspectiveCamera, RGBAFormat, Scene, SRGBColorSpace,
|
6
5
|
Texture, WebGLRenderer, type WebGLRendererParameters, WebGLRenderTarget, type WebXRArrayCamera
|
7
6
|
} from 'three';
|
7
|
+
|
8
|
+
import { Input } from './engine_input.js';
|
9
|
+
import { Physics } from './engine_physics.js';
|
10
|
+
import { Time } from './engine_time.js';
|
11
|
+
import { NetworkConnection } from './engine_networking.js';
|
12
|
+
|
13
|
+
import * as looputils from './engine_mainloop_utils.js';
|
14
|
+
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
|
+
|
8
22
|
import * as Stats from 'three/examples/jsm/libs/stats.module.js';
|
9
|
-
|
10
|
-
import { isDevEnvironment, LogType, showBalloonError, showBalloonMessage, showBalloonWarning } from './debug/index.js';
|
23
|
+
import { RendererData as SceneLighting } from './engine_scenelighting.js';
|
11
24
|
import { Addressables } from './engine_addressables.js';
|
12
25
|
import { Application } from './engine_application.js';
|
13
|
-
import {
|
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
|
+
import { delay, getParam } from './engine_utils.js';
|
14
33
|
import { VERSION } from './engine_constants.js';
|
15
|
-
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';
|
16
37
|
import { WaitForPromise } from './engine_coroutine.js';
|
17
|
-
import { destroy, foreachComponent } from './engine_gameobject.js';
|
18
|
-
import { getLoader } from './engine_gltf.js';
|
19
|
-
import { Input } from './engine_input.js';
|
20
38
|
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';
|
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';
|
29
|
-
import { Time } from './engine_time.js';
|
30
|
-
import { type CoroutineData, type GLTF, type ICamera, type IComponent, type IContext, type ILight, INeedleXRSession, type LoadedGLTF } from "./engine_types.js";
|
31
|
-
import * as utils from "./engine_utils.js";
|
32
|
-
import { delay, getParam } from './engine_utils.js';
|
33
|
-
import type { INeedleXRSessionEventReceiver, NeedleXRSession } from './engine_xr.js';
|
34
39
|
|
35
40
|
|
36
41
|
const debug = utils.getParam("debugcontext");
|
@@ -96,6 +101,11 @@
|
|
96
101
|
Undefined = -1,
|
97
102
|
}
|
98
103
|
|
104
|
+
export enum XRSessionMode {
|
105
|
+
ImmersiveVR = "immersive-vr",
|
106
|
+
ImmersiveAR = "immersive-ar",
|
107
|
+
}
|
108
|
+
|
99
109
|
/** threejs callback event signature */
|
100
110
|
export declare type OnRenderCallback = (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void
|
101
111
|
|
@@ -203,7 +213,6 @@
|
|
203
213
|
private _boundingClientRectFrame: number = -1;
|
204
214
|
private _boundingClientRect: DOMRect | null = null;
|
205
215
|
private _domX; private _domY;
|
206
|
-
/** update bounding rects + domX, domY */
|
207
216
|
private calculateBoundingClientRect() {
|
208
217
|
// workaround for mozilla webXR viewer
|
209
218
|
if (this.isInAR) {
|
@@ -218,46 +227,30 @@
|
|
218
227
|
this._domY = this._boundingClientRect.y;
|
219
228
|
}
|
220
229
|
|
221
|
-
/** The width of the `<needle-engine>` element on the website */
|
222
230
|
get domWidth(): number {
|
223
231
|
// for mozilla XR
|
224
232
|
if (this.isInAR) return window.innerWidth;
|
225
233
|
return this.domElement.clientWidth;
|
226
234
|
}
|
227
|
-
/** The height of the `<needle-engine>` element on the website */
|
228
235
|
get domHeight(): number {
|
229
236
|
// for mozilla XR
|
230
237
|
if (this.isInAR) return window.innerHeight;
|
231
238
|
return this.domElement.clientHeight;
|
232
239
|
}
|
233
|
-
/** the X position of the Needle Engine element on the website */
|
234
240
|
get domX(): number {
|
235
241
|
this.calculateBoundingClientRect();
|
236
242
|
return this._domX;
|
237
243
|
}
|
238
|
-
/** the Y position of the Needlee Engine element on the website */
|
239
244
|
get domY(): number {
|
240
245
|
this.calculateBoundingClientRect();
|
241
246
|
return this._domY;
|
242
247
|
}
|
243
248
|
get isInXR() { return this.renderer?.xr?.isPresenting || false; }
|
244
|
-
|
245
|
-
|
246
|
-
|
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` */
|
249
|
+
xrSessionMode: XRSessionMode | undefined = undefined;
|
250
|
+
get isInVR() { return this.xrSessionMode === XRSessionMode.ImmersiveVR; }
|
251
|
+
get isInAR() { return this.xrSessionMode === XRSessionMode.ImmersiveAR; }
|
255
252
|
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
|
-
*/
|
259
253
|
get xrFrame() { return this._xrFrame }
|
260
|
-
/** @returns the current WebXR camera (shorthand for `context.renderer.xr.getCamera()`) */
|
261
254
|
get xrCamera(): WebXRArrayCamera | undefined { return this.renderer?.xr?.getCamera(); }
|
262
255
|
private _xrFrame: XRFrame | null = null;
|
263
256
|
get arOverlayElement(): HTMLElement {
|
@@ -277,37 +270,17 @@
|
|
277
270
|
composer: EffectComposer | null = null;
|
278
271
|
|
279
272
|
// all scripts
|
280
|
-
|
281
|
-
|
273
|
+
scripts: IComponent[] = [];
|
274
|
+
scripts_pausedChanged: IComponent[] = [];
|
282
275
|
// scripts with update event
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
readonly scripts_immersive_ar: INeedleXRSessionEventReceiver[] = [];
|
291
|
-
readonly coroutines: { [FrameEvent: number]: Array<CoroutineData> } = {}
|
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
|
+
coroutines: { [FrameEvent: number]: Array<CoroutineData> } = {}
|
292
283
|
|
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
|
-
|
311
284
|
mainCameraComponent: ICamera | undefined;
|
312
285
|
|
313
286
|
private _camera: Camera | null = null;
|
@@ -327,13 +300,20 @@
|
|
327
300
|
this._camera = cam;
|
328
301
|
}
|
329
302
|
|
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
|
+
|
330
313
|
application: Application;
|
331
|
-
/** access timings (current frame number, deltaTime, timeScale, ...) */
|
332
314
|
time: Time;
|
333
315
|
input: Input;
|
334
|
-
/** access physics related methods (e.g. raycasting). To access the phyiscs engine use `context.physics.engine` */
|
335
316
|
physics: Physics;
|
336
|
-
/** access networking methods (use it to send or listen to messages or join a networking backend) */
|
337
317
|
connection: NetworkConnection;
|
338
318
|
/**
|
339
319
|
* @deprecated AssetDataBase is deprecated
|
@@ -399,10 +379,11 @@
|
|
399
379
|
ContextRegistry.register(this);
|
400
380
|
}
|
401
381
|
|
402
|
-
|
382
|
+
/** calling this function will dispose the current renderer and create a new one */
|
383
|
+
createNewRenderer(params?: WebGLRendererParameters) {
|
403
384
|
this.renderer?.dispose();
|
404
385
|
|
405
|
-
|
386
|
+
params = { ...Context.DefaultWebGLRendererParameters, ...params };
|
406
387
|
if (!params.canvas) {
|
407
388
|
// get canvas already configured in the Needle Engine Web Component
|
408
389
|
const canvas = this.domElement?.shadowRoot?.querySelector("canvas");
|
@@ -413,7 +394,7 @@
|
|
413
394
|
}
|
414
395
|
}
|
415
396
|
}
|
416
|
-
if
|
397
|
+
if(debug) console.log("Using Renderer Parameters:", params, this.domElement)
|
417
398
|
|
418
399
|
this.renderer = new WebGLRenderer(params);
|
419
400
|
|
@@ -432,8 +413,6 @@
|
|
432
413
|
this.renderer.outputColorSpace = SRGBColorSpace;
|
433
414
|
// https://github.com/mrdoob/three.js/pull/25556
|
434
415
|
this.renderer.useLegacyLights = false;
|
435
|
-
|
436
|
-
this.input.bindEvents();
|
437
416
|
}
|
438
417
|
|
439
418
|
|
@@ -445,13 +424,10 @@
|
|
445
424
|
|
446
425
|
private _disposeCallbacks: Function[] = [];
|
447
426
|
|
427
|
+
// private _requestSizeUpdate : boolean = false;
|
448
428
|
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
/** update the renderer and canvas size */
|
453
|
-
updateSize(force: boolean = false) {
|
454
|
-
if (force || (!this.isManagedExternally && this.renderer.xr?.isPresenting === false)) {
|
429
|
+
updateSize() {
|
430
|
+
if (!this.isManagedExternally && this.renderer.xr?.isPresenting === false) {
|
455
431
|
this._sizeChanged = false;
|
456
432
|
const scaleFactor = this.resolutionScaleFactor;
|
457
433
|
const width = this.domWidth * scaleFactor;
|
@@ -502,7 +478,7 @@
|
|
502
478
|
async create(opts?: ContextCreateArgs) {
|
503
479
|
try {
|
504
480
|
this._isCreating = true;
|
505
|
-
if
|
481
|
+
if(opts !== this._originalCreationArgs)
|
506
482
|
this._originalCreationArgs = utils.deepClone(opts);
|
507
483
|
window.addEventListener("unhandledrejection", this.onUnhandledRejection)
|
508
484
|
const res = await this.internalOnCreate(opts);
|
@@ -555,11 +531,11 @@
|
|
555
531
|
if (this.renderer) {
|
556
532
|
this.renderer.setClearAlpha(0);
|
557
533
|
this.renderer.clear();
|
558
|
-
if (!this.isManagedExternally) {
|
559
|
-
if (debug) console.log("Disposing renderer");
|
560
|
-
this.renderer.dispose();
|
561
|
-
}
|
562
534
|
}
|
535
|
+
if (!this.isManagedExternally) {
|
536
|
+
if(debug) console.log("Disposing renderer");
|
537
|
+
this.renderer.dispose();
|
538
|
+
}
|
563
539
|
this.scene = null!;
|
564
540
|
this.renderer = null!;
|
565
541
|
this.input.dispose();
|
@@ -577,10 +553,6 @@
|
|
577
553
|
this._isCreated = false;
|
578
554
|
ContextRegistry.dispatchCallback(ContextEvent.ContextDestroyed, this);
|
579
555
|
ContextRegistry.unregister(this);
|
580
|
-
if (Context.Current === this) {
|
581
|
-
//@ts-ignore
|
582
|
-
Context.Current = null;
|
583
|
-
}
|
584
556
|
}
|
585
557
|
|
586
558
|
registerCoroutineUpdate(script: IComponent, coroutine: Generator, evt: FrameEvent): Generator {
|
@@ -732,7 +704,7 @@
|
|
732
704
|
private async internalOnCreate(opts?: ContextCreateArgs) {
|
733
705
|
const createId = ++this._createId;
|
734
706
|
|
735
|
-
if
|
707
|
+
if(debug) console.log("Creating context", this.name, opts);
|
736
708
|
|
737
709
|
this.clear();
|
738
710
|
// stop the animation loop if its running during creation
|
@@ -839,8 +811,6 @@
|
|
839
811
|
}
|
840
812
|
}
|
841
813
|
|
842
|
-
this.input.bindEvents();
|
843
|
-
|
844
814
|
Context.Current = this;
|
845
815
|
looputils.processNewScripts(this);
|
846
816
|
|
@@ -883,7 +853,7 @@
|
|
883
853
|
this._dispatchReadyAfterFrame = true;
|
884
854
|
const res = ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this, { files: loadedFiles });
|
885
855
|
if (res) {
|
886
|
-
if
|
856
|
+
if("internalSetLoadingMessage" in this.domElement && typeof this.domElement.internalSetLoadingMessage === "function")
|
887
857
|
this.domElement?.internalSetLoadingMessage("finish loading");
|
888
858
|
await res;
|
889
859
|
}
|
@@ -927,7 +897,7 @@
|
|
927
897
|
}
|
928
898
|
|
929
899
|
args?.onLoadingStart?.call(this, i, file);
|
930
|
-
if
|
900
|
+
if(debug) console.log("Context Load " + file);
|
931
901
|
const res = await loader.loadSync(this, file, file, loadingHash, prog => {
|
932
902
|
progressArg.name = file;
|
933
903
|
progressArg.progress = prog;
|
@@ -1003,9 +973,9 @@
|
|
1003
973
|
catch (err) {
|
1004
974
|
this._renderlooperrors += 1;
|
1005
975
|
if ((isDevEnvironment() || debug) && (err instanceof Error || err instanceof TypeError))
|
1006
|
-
showBalloonMessage(
|
1007
|
-
console.error(
|
1008
|
-
if (this._renderlooperrors
|
976
|
+
showBalloonMessage("Caught unhandled exception during render-loop.<br/>Stopping renderloop...<br/>See console for details.", LogType.Error);
|
977
|
+
console.error(err);
|
978
|
+
if (this._renderlooperrors > 10) {
|
1009
979
|
console.warn("Stopping render loop due to error")
|
1010
980
|
this.renderer.setAnimationLoop(null);
|
1011
981
|
}
|
@@ -1038,11 +1008,7 @@
|
|
1038
1008
|
|
1039
1009
|
private internalOnBeforeRender(timestamp: DOMHighResTimeStamp, frame: XRFrame | null) {
|
1040
1010
|
|
1041
|
-
const sessionStarted = frame !== null && this._xrFrame === null;
|
1042
1011
|
this._xrFrame = frame;
|
1043
|
-
if (sessionStarted) {
|
1044
|
-
this.domElement.dispatchEvent(new CustomEvent("xr-session-started", { detail: { context: this, session: this.xrSession, frame: frame } }));
|
1045
|
-
}
|
1046
1012
|
|
1047
1013
|
this._currentFrameEvent = FrameEvent.Undefined;
|
1048
1014
|
|
@@ -1081,13 +1047,6 @@
|
|
1081
1047
|
this.setCurrentCamera(last);
|
1082
1048
|
}
|
1083
1049
|
|
1084
|
-
if (this.pre_update_oneshot_callbacks) {
|
1085
|
-
for (const i in this.pre_update_oneshot_callbacks) {
|
1086
|
-
this.pre_update_oneshot_callbacks[i]();
|
1087
|
-
}
|
1088
|
-
this.pre_update_oneshot_callbacks.length = 0;
|
1089
|
-
}
|
1090
|
-
|
1091
1050
|
if (this.pre_update_callbacks) {
|
1092
1051
|
for (const i in this.pre_update_callbacks) {
|
1093
1052
|
this.pre_update_callbacks[i]();
|
@@ -1170,7 +1129,7 @@
|
|
1170
1129
|
|
1171
1130
|
if (this.pre_render_callbacks) {
|
1172
1131
|
for (const i in this.pre_render_callbacks) {
|
1173
|
-
this.pre_render_callbacks[i](
|
1132
|
+
this.pre_render_callbacks[i]();
|
1174
1133
|
}
|
1175
1134
|
}
|
1176
1135
|
|
@@ -1231,7 +1190,7 @@
|
|
1231
1190
|
if (this._stats) {
|
1232
1191
|
this._stats.end();
|
1233
1192
|
if (this.time.frameCount % 150 === 0)
|
1234
|
-
console.log({ ...this.renderer.info.
|
1193
|
+
console.log(this.renderer.info.render.calls + " DrawCalls", "\nRender:", { ...this.renderer.info.render }, "\nMemory:", { ...this.renderer.info.memory });
|
1235
1194
|
}
|
1236
1195
|
|
1237
1196
|
if (this._dispatchReadyAfterFrame) {
|
@@ -1248,8 +1207,8 @@
|
|
1248
1207
|
}
|
1249
1208
|
this._isRendering = true;
|
1250
1209
|
this.renderRequiredTextures();
|
1210
|
+
|
1251
1211
|
|
1252
|
-
|
1253
1212
|
if (this.composer && !this.isInXR) {
|
1254
1213
|
this.composer.render(this.time.deltaTime);
|
1255
1214
|
}
|
@@ -1,7 +1,5 @@
|
|
1
|
-
import {
|
1
|
+
import { PlaneGeometry, MeshBasicMaterial, DoubleSide, Mesh, Material, MeshStandardMaterial, BoxGeometry, SphereGeometry, ColorRepresentation } from "three"
|
2
2
|
|
3
|
-
import { Vec3 } from "./engine_types.js";
|
4
|
-
|
5
3
|
export enum PrimitiveType {
|
6
4
|
Quad = 0,
|
7
5
|
Cube = 1,
|
@@ -11,10 +9,6 @@
|
|
11
9
|
export type ObjectOptions = {
|
12
10
|
name?: string,
|
13
11
|
material?: Material,
|
14
|
-
position?: Vec3,
|
15
|
-
/** euler */
|
16
|
-
rotation?: Vec3,
|
17
|
-
scale?: Vec3,
|
18
12
|
}
|
19
13
|
|
20
14
|
export class ObjectUtils {
|
@@ -41,12 +35,6 @@
|
|
41
35
|
}
|
42
36
|
if (opts?.name)
|
43
37
|
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);
|
50
38
|
return obj;
|
51
39
|
}
|
52
40
|
}
|
@@ -1,9 +1,9 @@
|
|
1
|
-
import { logoSVG } from "./assets/index.js"
|
2
1
|
import { showBalloonWarning } from "./debug/index.js";
|
3
|
-
import { hasCommercialLicense, hasProLicense, runtimeLicenseCheckPromise } from "./engine_license.js";
|
4
2
|
import { Mathf } from "./engine_math.js";
|
5
3
|
import { LoadingProgressArgs } from "./engine_setup.js";
|
6
4
|
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,24 +228,12 @@
|
|
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);
|
243
231
|
|
244
232
|
const loadingBarContainer = document.createElement("div");
|
245
233
|
const maxWidth = 30;
|
246
234
|
loadingBarContainer.style.display = "flex";
|
247
235
|
loadingBarContainer.style.width = maxWidth + "%";
|
248
|
-
loadingBarContainer.style.height = "
|
236
|
+
loadingBarContainer.style.height = "2px";
|
249
237
|
if (loadingStyle === "light")
|
250
238
|
loadingBarContainer.style.backgroundColor = "rgba(0,0,0,.2)"
|
251
239
|
else
|
@@ -259,15 +247,6 @@
|
|
259
247
|
logo.style.marginBottom = "20px";
|
260
248
|
logo.style.userSelect = "none";
|
261
249
|
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
|
-
}
|
271
250
|
logo.src = logoSVG;
|
272
251
|
let isUsingCustomLogo = false;
|
273
252
|
if (hasLicense && this._element) {
|
@@ -282,8 +261,8 @@
|
|
282
261
|
logo.style.pointerEvents = "all";
|
283
262
|
logo.addEventListener("click", () => window.open("https://needle.tools", "_blank"));
|
284
263
|
}
|
285
|
-
|
286
|
-
|
264
|
+
this._loadingElement.appendChild(logo);
|
265
|
+
this._loadingElement.appendChild(loadingBarContainer);
|
287
266
|
|
288
267
|
|
289
268
|
this._loadingBar = document.createElement("div");
|
@@ -314,7 +293,7 @@
|
|
314
293
|
this._loadingTextContainer.style.display = "flex";
|
315
294
|
this._loadingTextContainer.style.justifyContent = "center";
|
316
295
|
this._loadingTextContainer.style.marginTop = "1.2em";
|
317
|
-
|
296
|
+
this._loadingElement.appendChild(this._loadingTextContainer);
|
318
297
|
|
319
298
|
const messageContainer = document.createElement("div");
|
320
299
|
this._messageContainer = messageContainer;
|
@@ -324,7 +303,7 @@
|
|
324
303
|
messageContainer.style.fontWeight = "200";
|
325
304
|
// messageContainer.style.border = "1px solid rgba(255,255,255,.1)";
|
326
305
|
messageContainer.style.justifyContent = "center";
|
327
|
-
|
306
|
+
this._loadingElement.appendChild(messageContainer);
|
328
307
|
|
329
308
|
if (hasLicense && this._element) {
|
330
309
|
const loadingTextColor = this._element.getAttribute("loading-text-color");
|
@@ -333,7 +312,7 @@
|
|
333
312
|
}
|
334
313
|
}
|
335
314
|
|
336
|
-
this.handleRuntimeLicense(
|
315
|
+
this.handleRuntimeLicense(this._loadingElement);
|
337
316
|
|
338
317
|
return this._loadingElement;
|
339
318
|
}
|
@@ -344,16 +323,6 @@
|
|
344
323
|
// if it's the case then we don't need to perform a runtime check
|
345
324
|
if (commercialLicense) return;
|
346
325
|
|
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
|
-
|
357
326
|
// Use the runtime license check
|
358
327
|
if (runtimeLicenseCheckPromise) {
|
359
328
|
if (debugLicense) console.log("Waiting for runtime license check");
|
@@ -361,7 +330,13 @@
|
|
361
330
|
commercialLicense = hasCommercialLicense();
|
362
331
|
}
|
363
332
|
if (commercialLicense) return;
|
364
|
-
|
365
|
-
|
333
|
+
|
334
|
+
// If we don't have a commercial license, then we need to display our message
|
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);
|
366
341
|
}
|
367
342
|
}
|
@@ -16,7 +16,6 @@
|
|
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;
|
20
19
|
|
21
20
|
requestEndAR = () => {
|
22
21
|
this.onRequestedEndAR();
|
@@ -35,22 +34,6 @@
|
|
35
34
|
this._reparentedObjects.push({ el: el, previousParent: el.parentElement });
|
36
35
|
this.arContainer?.appendChild(el);
|
37
36
|
}
|
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
|
-
}
|
54
37
|
}
|
55
38
|
this.ensureQuitARButton(this.arContainer);
|
56
39
|
}
|
@@ -1,15 +1,15 @@
|
|
1
|
-
import {
|
1
|
+
import { Context, ContextCreateArgs, LoadingProgressArgs } from "./engine_setup.js";
|
2
|
+
import { AROverlayHandler, arContainerClassName } from "./engine_element_overlay.js";
|
2
3
|
import { GameObject } from "../engine-components/Component.js";
|
3
|
-
import { isDevEnvironment, showBalloonWarning } from "./debug/index.js";
|
4
|
-
import { VERSION } from "./engine_constants.js";
|
5
4
|
import { calculateProgress01, EngineLoadingView, type ILoadingViewHandler } from "./engine_element_loading.js";
|
6
|
-
import {
|
7
|
-
import { hasCommercialLicense } from "./engine_license.js";
|
5
|
+
import { getParam } from "./engine_utils.js";
|
8
6
|
import { setDracoDecoderPath, setDracoDecoderType, setKtx2TranscoderPath } from "./engine_loaders.js";
|
7
|
+
import { getLoader, registerLoader } from "../engine/engine_gltf.js";
|
9
8
|
import { NeedleGltfLoader } from "./engine_scenetools.js";
|
10
|
-
import { Context, ContextCreateArgs, LoadingProgressArgs } from "./engine_setup.js";
|
11
9
|
import { type INeedleEngineComponent, type LoadedGLTF } from "./engine_types.js";
|
12
|
-
import {
|
10
|
+
import { isDevEnvironment, showBalloonWarning } from "./debug/index.js";
|
11
|
+
import { hasCommercialLicense } from "./engine_license.js";
|
12
|
+
import { VERSION } from "./engine_constants.js";
|
13
13
|
|
14
14
|
//
|
15
15
|
// registering loader here too to make sure it's imported when using engine via vanilla js
|
@@ -143,15 +143,12 @@
|
|
143
143
|
}
|
144
144
|
:host .quit-ar-button {
|
145
145
|
position: absolute;
|
146
|
-
|
147
|
-
top: 60px; /** camera access needs a bit more space **/
|
146
|
+
top: 40px;
|
148
147
|
right: 20px;
|
149
148
|
z-index: 9999;
|
150
149
|
}
|
151
150
|
</style>
|
152
|
-
<
|
153
|
-
<canvas></canvas>
|
154
|
-
</div>
|
151
|
+
<canvas></canvas>
|
155
152
|
<div class="content">
|
156
153
|
<slot class="overlay-content"></slot>
|
157
154
|
</div>
|
@@ -170,7 +167,6 @@
|
|
170
167
|
console.log("<needle-engine> connected");
|
171
168
|
}
|
172
169
|
|
173
|
-
this.addEventListener("xr-session-started", this.onXRSessionStarted);
|
174
170
|
this.onSetupDesktop();
|
175
171
|
|
176
172
|
if (!this.getAttribute("src")) {
|
@@ -200,8 +196,6 @@
|
|
200
196
|
}
|
201
197
|
|
202
198
|
disconnectedCallback() {
|
203
|
-
this.removeEventListener("xr-session-started", this.onXRSessionStarted);
|
204
|
-
|
205
199
|
this._didFullyLoad = false;
|
206
200
|
const keepAlive = this.getAttribute("keep-alive");
|
207
201
|
const dispose = keepAlive == undefined || (keepAlive?.length > 0 && keepAlive !== "true" && keepAlive !== "1");
|
@@ -346,15 +340,10 @@
|
|
346
340
|
totalProgress01: this._loadingProgress01
|
347
341
|
};
|
348
342
|
const progressEvent = new CustomEvent("progress", { detail: progressEventDetail });
|
349
|
-
const displayNames = new Array<string>();
|
350
343
|
const args: ContextCreateArgs = {
|
351
344
|
files: filesToLoad,
|
352
345
|
onLoadingProgress: evt => {
|
353
|
-
|
354
|
-
if (!displayNames[index] && evt.name) {
|
355
|
-
displayNames[index] = getDisplayName(evt.name);
|
356
|
-
}
|
357
|
-
evt.name = displayNames[index];
|
346
|
+
evt.name = getNameFromUrl(evt.name);
|
358
347
|
if (useDefaultLoading) this._loadingView?.onLoadingUpdate(evt);
|
359
348
|
progressEventDetail.name = evt.name;
|
360
349
|
progressEventDetail.progress = evt.progress;
|
@@ -395,23 +384,6 @@
|
|
395
384
|
}));
|
396
385
|
}
|
397
386
|
|
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
|
-
|
415
387
|
/** called by the context when the first frame has been rendered */
|
416
388
|
private onReady = () => this._loadingView?.onLoadingFinished();
|
417
389
|
private onError = () => this._loadingView?.setMessage("Loading failed!");
|
@@ -502,9 +474,8 @@
|
|
502
474
|
return null;
|
503
475
|
}
|
504
476
|
|
505
|
-
onEnterAR(session: XRSession) {
|
477
|
+
onEnterAR(session: XRSession, overlayContainer: HTMLElement) {
|
506
478
|
this.onSetupAR();
|
507
|
-
const overlayContainer = this.getAROverlayContainer();
|
508
479
|
this._overlay_ar.onBegin(this._context!, overlayContainer, session);
|
509
480
|
this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
|
510
481
|
}
|
@@ -619,36 +590,12 @@
|
|
619
590
|
return hash;
|
620
591
|
}
|
621
592
|
|
622
|
-
function
|
593
|
+
function getNameFromUrl(str: string) {
|
623
594
|
const parts = str.split("/");
|
624
595
|
let name = parts[parts.length - 1];
|
625
596
|
// Remove params
|
626
597
|
const index = name.indexOf("?")
|
627
598
|
if (index > 0)
|
628
599
|
name = name.substring(0, index);
|
629
|
-
|
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;
|
600
|
+
return decodeURIComponent(name);
|
654
601
|
}
|
@@ -1,18 +1,17 @@
|
|
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";
|
9
2
|
import { processNewScripts } from "./engine_mainloop_utils.js";
|
10
3
|
import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
|
11
|
-
import { assign } from "./engine_serialization_core.js";
|
12
4
|
import { Context, registerComponent } from "./engine_setup.js";
|
13
5
|
import { logHierarchy, setWorldPosition, setWorldQuaternion } from "./engine_three_utils.js";
|
14
|
-
import { type
|
6
|
+
import { type GuidsMap, type IComponent as Component, type IComponent, type IGameObject as GameObject, type UIDProvider, type Constructor } from "./engine_types.js";
|
15
7
|
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";
|
16
15
|
|
17
16
|
const debug = getParam("debuggetcomponent");
|
18
17
|
const debugInstantiate = getParam("debuginstantiate");
|
@@ -33,11 +32,9 @@
|
|
33
32
|
idProvider?: UIDProvider;
|
34
33
|
//** parent guid or object */
|
35
34
|
parent?: string | Object3D;
|
36
|
-
/** position in local space. Set `keepWorldPosition` to true if this is world space */
|
37
35
|
position?: Vector3;
|
38
36
|
/** for duplicatable parenting */
|
39
37
|
keepWorldPosition?: boolean;
|
40
|
-
/** rotation in local space. Set `keepWorldPosition` to true if this is world space */
|
41
38
|
rotation?: Quaternion;
|
42
39
|
scale?: Vector3;
|
43
40
|
/** if the instantiated object should be visible */
|
@@ -140,49 +137,23 @@
|
|
140
137
|
go[$isDestroyed] = value;
|
141
138
|
}
|
142
139
|
|
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> = [];
|
155
140
|
export function destroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false) {
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
for (const comp of destroyed_components) {
|
141
|
+
const allComponents: IComponent[] = [];
|
142
|
+
internalDestroy(instance, recursive, dispose, true, allComponents);
|
143
|
+
for (const comp of allComponents) {
|
160
144
|
comp.gameObject = null!;
|
161
145
|
//@ts-ignore
|
162
146
|
comp.context = null;
|
163
147
|
}
|
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;
|
175
148
|
}
|
176
149
|
|
177
|
-
function internalDestroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false, isRoot: boolean = true) {
|
150
|
+
function internalDestroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false, isRoot: boolean = true, allComponents: IComponent[]) {
|
178
151
|
if (instance === null || instance === undefined)
|
179
152
|
return;
|
180
153
|
|
181
154
|
const comp = instance as Component;
|
182
155
|
if (comp.isComponent) {
|
183
|
-
|
184
|
-
if (comp[$isDontDestroy]) return;
|
185
|
-
destroyed_components.push(comp);
|
156
|
+
allComponents.push(comp);
|
186
157
|
const go = comp.gameObject;
|
187
158
|
comp.__internalDisable();
|
188
159
|
comp.__internalDestroy();
|
@@ -190,34 +161,44 @@
|
|
190
161
|
return;
|
191
162
|
}
|
192
163
|
|
193
|
-
// handle Object3D
|
194
|
-
if (instance[$isDontDestroy]) return;
|
195
164
|
|
196
165
|
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
|
+
|
197
173
|
if (debug) console.log(obj);
|
198
|
-
destroyed_objects.push(obj);
|
199
174
|
|
200
|
-
|
175
|
+
if (recursive && obj.children) {
|
176
|
+
for (const ch of obj.children) {
|
177
|
+
internalDestroy(ch, recursive, dispose, false, allComponents);
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
201
181
|
const components = obj.userData.components;
|
202
182
|
if (components) {
|
203
183
|
let lastLength = components.length;
|
204
184
|
for (let i = 0; i < components.length; i++) {
|
205
185
|
const comp: Component = components[i];
|
206
|
-
|
207
|
-
|
186
|
+
allComponents.push(comp);
|
187
|
+
const go = comp.gameObject;
|
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
|
208
196
|
if (components.length < lastLength) {
|
209
197
|
lastLength = components.length;
|
210
198
|
i--;
|
211
199
|
}
|
212
200
|
}
|
213
201
|
}
|
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
|
-
|
221
202
|
if (isRoot)
|
222
203
|
obj.removeFromParent();
|
223
204
|
}
|
@@ -285,7 +266,9 @@
|
|
285
266
|
clone: Object3D;
|
286
267
|
}
|
287
268
|
|
288
|
-
export function instantiate(instance: GameObject | Object3D, opts: IInstantiateOptions | null = null): GameObject {
|
269
|
+
export function instantiate(instance: GameObject | Object3D | null, opts: IInstantiateOptions | null = null): GameObject | null {
|
270
|
+
if (instance === null) return null;
|
271
|
+
|
289
272
|
let options: InstantiateOptions | null = null;
|
290
273
|
if (opts !== null) {
|
291
274
|
// 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
|
@@ -296,8 +279,13 @@
|
|
296
279
|
else {
|
297
280
|
// if (opts instanceof InstantiateOptions)
|
298
281
|
options = opts as InstantiateOptions;
|
282
|
+
// else {
|
283
|
+
// options = new InstantiateOptions();
|
284
|
+
// Object.assign(options, opts);
|
285
|
+
// }
|
299
286
|
}
|
300
287
|
}
|
288
|
+
console.log(options?.position)
|
301
289
|
|
302
290
|
let context = Context.Current;
|
303
291
|
if (options?.context) context = options.context;
|
@@ -1,13 +1,11 @@
|
|
1
|
-
import {
|
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';
|
1
|
+
import { BufferAttribute, Line, BoxGeometry, EdgesGeometry, Color, LineSegments, LineBasicMaterial, Object3D, Mesh, SphereGeometry, type ColorRepresentation, Vector3, Box3, Quaternion, CylinderGeometry, AxesHelper } from 'three';
|
6
2
|
import { Context } from './engine_setup.js';
|
7
3
|
import { getWorldPosition, lookAtObject, setWorldPositionXYZ } from './engine_three_utils.js';
|
8
4
|
import type { Vec3, Vec4 } from './engine_types.js';
|
5
|
+
import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
|
9
6
|
import { getParam } from './engine_utils.js';
|
10
|
-
import {
|
7
|
+
import { type Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
|
8
|
+
import { isDestroyed } from './engine_gameobject.js';
|
11
9
|
|
12
10
|
const _tmp = new Vector3();
|
13
11
|
const _tmp2 = new Vector3();
|
@@ -23,15 +21,6 @@
|
|
23
21
|
|
24
22
|
export class Gizmos {
|
25
23
|
|
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
|
-
*/
|
35
24
|
static isGizmo(obj: Object3D) {
|
36
25
|
return obj[$cacheSymbol] !== undefined;
|
37
26
|
}
|
@@ -40,12 +29,10 @@
|
|
40
29
|
* Draw a label in the scene or attached to an object (if a parent is provided)
|
41
30
|
* @returns a handle to the label that can be used to change the text
|
42
31
|
*/
|
43
|
-
static DrawLabel(position: Vec3, text: string, size: number = .
|
44
|
-
if (!Gizmos.enabled) return null;
|
32
|
+
static DrawLabel(position: Vec3, text: string, size: number = .1, duration: number = 9999, color?: ColorRepresentation, backgroundColor?: ColorRepresentation | ColorWithAlpha, parent?: Object3D,) {
|
45
33
|
if (!color) color = defaultColor;
|
46
|
-
const
|
47
|
-
|
48
|
-
if (parent instanceof Object3D) parent.add(element as any);
|
34
|
+
const element = Internal.getTextLabel(duration, text, size, color, backgroundColor);
|
35
|
+
if (parent instanceof Object3D) parent.add(element);
|
49
36
|
element.position.x = position.x;
|
50
37
|
element.position.y = position.y;
|
51
38
|
element.position.z = position.z;
|
@@ -53,7 +40,6 @@
|
|
53
40
|
}
|
54
41
|
|
55
42
|
static DrawRay(origin: Vec3, dir: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
56
|
-
if (!Gizmos.enabled) return;
|
57
43
|
const obj = Internal.getLine(duration);
|
58
44
|
const positions = obj.geometry.getAttribute("position");
|
59
45
|
positions.setXYZ(0, origin.x, origin.y, origin.z);
|
@@ -66,7 +52,6 @@
|
|
66
52
|
}
|
67
53
|
|
68
54
|
static DrawDirection(pt: Vec3, direction: Vec3 | Vec4, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true, lengthFactor: number = 1) {
|
69
|
-
if (!Gizmos.enabled) return;
|
70
55
|
const obj = Internal.getLine(duration);
|
71
56
|
const positions = obj.geometry.getAttribute("position");
|
72
57
|
positions.setXYZ(0, pt.x, pt.y, pt.z);
|
@@ -88,8 +73,8 @@
|
|
88
73
|
}
|
89
74
|
|
90
75
|
static DrawLine(pt0: Vec3, pt1: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
91
|
-
if (!Gizmos.enabled) return;
|
92
76
|
const obj = Internal.getLine(duration);
|
77
|
+
|
93
78
|
const positions = obj.geometry.getAttribute("position");
|
94
79
|
positions.setXYZ(0, pt0.x, pt0.y, pt0.z);
|
95
80
|
positions.setXYZ(1, pt1.x, pt1.y, pt1.z);
|
@@ -100,7 +85,6 @@
|
|
100
85
|
}
|
101
86
|
|
102
87
|
static DrawWireSphere(center: Vec3, radius: number, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
103
|
-
if (!Gizmos.enabled) return;
|
104
88
|
const obj = Internal.getSphere(radius, duration, true);
|
105
89
|
setWorldPositionXYZ(obj, center.x, center.y, center.z);
|
106
90
|
obj.material["color"].set(color);
|
@@ -109,7 +93,6 @@
|
|
109
93
|
}
|
110
94
|
|
111
95
|
static DrawSphere(center: Vec3, radius: number, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
112
|
-
if (!Gizmos.enabled) return;
|
113
96
|
const obj = Internal.getSphere(radius, duration, false);
|
114
97
|
setWorldPositionXYZ(obj, center.x, center.y, center.z);
|
115
98
|
obj.material["color"].set(color);
|
@@ -118,7 +101,6 @@
|
|
118
101
|
}
|
119
102
|
|
120
103
|
static DrawWireBox(center: Vec3, size: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
121
|
-
if (!Gizmos.enabled) return;
|
122
104
|
const obj = Internal.getBox(duration);
|
123
105
|
obj.position.set(center.x, center.y, center.z);
|
124
106
|
obj.scale.set(size.x, size.y, size.z);
|
@@ -129,7 +111,6 @@
|
|
129
111
|
}
|
130
112
|
|
131
113
|
static DrawWireBox3(box: Box3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
132
|
-
if (!Gizmos.enabled) return;
|
133
114
|
const obj = Internal.getBox(duration);
|
134
115
|
obj.position.copy(box.getCenter(_tmp));
|
135
116
|
obj.scale.copy(box.getSize(_tmp));
|
@@ -141,7 +122,6 @@
|
|
141
122
|
|
142
123
|
private static _up = new Vector3(0, 1, 0);
|
143
124
|
static DrawArrow(pt0: Vec3, pt1: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true, wireframe: boolean = false) {
|
144
|
-
if (!Gizmos.enabled) return;
|
145
125
|
const obj = Internal.getArrowHead(duration);
|
146
126
|
obj.position.set(pt1.x, pt1.y, pt1.z);
|
147
127
|
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());
|
@@ -208,13 +188,12 @@
|
|
208
188
|
width: "auto",
|
209
189
|
fontSize: size,
|
210
190
|
color: color,
|
211
|
-
lineHeight:
|
191
|
+
lineHeight: .75,
|
212
192
|
backgroundColor: backgroundColor ?? undefined,
|
213
193
|
backgroundOpacity: opacity,
|
214
194
|
textContent: text,
|
215
|
-
borderRadius:
|
216
|
-
padding:
|
217
|
-
whiteSpace: 'pre',
|
195
|
+
borderRadius: 1 * size,
|
196
|
+
padding: 1 * size,
|
218
197
|
};
|
219
198
|
|
220
199
|
if (!element) {
|
@@ -222,7 +201,7 @@
|
|
222
201
|
const global = this;
|
223
202
|
const labelHandle = element as LabelHandle & Text;
|
224
203
|
labelHandle.setText = function (str: string) {
|
225
|
-
this.set({ textContent: str });
|
204
|
+
this.set({ textContent: str, whiteSpace: 'pre' });
|
226
205
|
global.tmuiNeedsUpdate = true;
|
227
206
|
};
|
228
207
|
}
|
@@ -232,7 +211,9 @@
|
|
232
211
|
// handle.setText(text);
|
233
212
|
}
|
234
213
|
this.tmuiNeedsUpdate = true;
|
235
|
-
|
214
|
+
element.layers.disableAll();
|
215
|
+
element.layers.enable(2);
|
216
|
+
this.registerTimedObject(Context.Current, element, duration, this.textLabelCache);
|
236
217
|
return element as Text & LabelHandle;
|
237
218
|
}
|
238
219
|
|
@@ -288,43 +269,20 @@
|
|
288
269
|
private static textLabelCache: Array<Text> = [];
|
289
270
|
|
290
271
|
private static registerTimedObject(context: Context, object: Object3D, duration: number, cache: Array<Object3D>) {
|
291
|
-
|
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) {
|
272
|
+
if (!this.contextPostRenderCallbacks.get(context)) {
|
309
273
|
const cb = () => { this.onPostRender(context, this.timedObjectsBuffer, this.timesBuffer) };
|
310
274
|
this.contextPostRenderCallbacks.set(context, cb);
|
311
275
|
context.post_render_callbacks.push(cb);
|
312
276
|
}
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
context.post_render_callbacks.splice(index, 1);
|
318
|
-
}
|
319
|
-
context.post_render_callbacks.push(postRender);
|
277
|
+
if (!this.contextBeforeRenderCallbacks.get(context)) {
|
278
|
+
const cb = () => { this.onBeforeRender(context, this.timedObjectsBuffer) };
|
279
|
+
this.contextBeforeRenderCallbacks.set(context, cb);
|
280
|
+
context.pre_render_callbacks.push(cb);
|
320
281
|
}
|
321
282
|
|
322
|
-
object.traverse(obj => {
|
323
|
-
obj.layers.disableAll();
|
324
|
-
obj.layers.enable(2);
|
325
|
-
});
|
326
|
-
|
327
283
|
object.renderOrder = 999999;
|
284
|
+
object.layers.disableAll();
|
285
|
+
object.layers.enable(2);
|
328
286
|
object[$cacheSymbol] = cache;
|
329
287
|
this.timedObjectsBuffer.push(object);
|
330
288
|
this.timesBuffer.push(Context.Current.time.realtimeSinceStartup + duration);
|
@@ -346,13 +304,13 @@
|
|
346
304
|
for (let i = 0; i < objects.length; i++) {
|
347
305
|
const obj = objects[i];
|
348
306
|
if (ctx.mainCamera && obj instanceof ThreeMeshUI.MeshUIBaseElement) {
|
349
|
-
if (isDestroyed(obj
|
307
|
+
if (isDestroyed(obj)) {
|
350
308
|
continue;
|
351
309
|
}
|
352
310
|
const isInXR = ctx.isInVR;
|
353
|
-
const keepUp =
|
311
|
+
const keepUp = isInXR;
|
354
312
|
const copyRotation = !isInXR;
|
355
|
-
lookAtObject(obj
|
313
|
+
lookAtObject(obj, ctx.mainCamera, keepUp, copyRotation);
|
356
314
|
}
|
357
315
|
}
|
358
316
|
}
|
@@ -365,7 +323,7 @@
|
|
365
323
|
objects.splice(i, 1);
|
366
324
|
times.splice(i, 1);
|
367
325
|
obj.removeFromParent();
|
368
|
-
if (isDestroyed(obj)
|
326
|
+
if (isDestroyed(obj) == false) {
|
369
327
|
const cache = obj[$cacheSymbol];
|
370
328
|
cache.push(obj);
|
371
329
|
}
|
@@ -1,20 +1,18 @@
|
|
1
1
|
import "./codegen/register_types.js";
|
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";
|
2
|
+
import { TypeStore } from "./engine_typestore.js";
|
9
3
|
import { InstantiateIdProvider } from "./engine_networking_instantiate.js"
|
10
|
-
import {
|
4
|
+
import { Context } from "./engine_setup.js";
|
11
5
|
import { deserializeObject, serializeObject } from "./engine_serialization.js";
|
12
6
|
import { assign, ImplementationInformation, type ISerializable, SerializationContext } from "./engine_serialization_core.js";
|
13
|
-
import {
|
7
|
+
import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
|
8
|
+
import { debugExtension } from "./engine_default_parameters.js";
|
9
|
+
import { editorGuidKeyName, builtinComponentKeyName } from "./engine_constants.js";
|
14
10
|
import type { GuidsMap, ICamera, IComponent, IGameObject, SourceIdentifier, UIDProvider } from "./engine_types.js";
|
15
|
-
import {
|
11
|
+
import { addNewComponent } from "./engine_components.js";
|
16
12
|
import { getParam } from "./engine_utils.js";
|
17
|
-
import {
|
13
|
+
import { LogType, showBalloonMessage } from "./debug/index.js";
|
14
|
+
import { isLocalNetwork } from "./engine_networking_utils.js";
|
15
|
+
import { Object3D } from "three";
|
18
16
|
|
19
17
|
|
20
18
|
const debug = debugExtension;
|
@@ -1,9 +1,8 @@
|
|
1
|
-
import type {
|
2
|
-
|
3
|
-
import { SerializationContext } from "./engine_serialization_core.js";
|
1
|
+
import type { ConstructorConcrete, SourceIdentifier, UIDProvider } from "./engine_types.js";
|
4
2
|
import { Context } from "./engine_setup.js";
|
5
|
-
import type { ConstructorConcrete, SourceIdentifier, UIDProvider } from "./engine_types.js";
|
6
3
|
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'
|
7
6
|
|
8
7
|
|
9
8
|
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"
|
3
1
|
import type { IComponent } from "./engine_types.js";
|
4
2
|
import { TypeStore } from "./engine_typestore.js";
|
3
|
+
import { addScriptToArrays, removeScriptFromContext } from "./engine_mainloop_utils.js"
|
5
4
|
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,137 +1,22 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { Vector2 } from 'three';
|
3
2
|
import { showBalloonMessage, showBalloonWarning } from './debug/debug.js';
|
4
3
|
import { Context } from './engine_setup.js';
|
5
|
-
import type {
|
6
|
-
import {
|
4
|
+
import type { IInput, Vec2 } from './engine_types.js';
|
5
|
+
import { getParam } from './engine_utils.js';
|
7
6
|
|
8
7
|
const debug = getParam("debuginput");
|
9
8
|
|
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
|
-
|
59
9
|
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) */
|
70
10
|
readonly source: Event | null;
|
71
11
|
|
72
|
-
|
73
|
-
|
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;
|
12
|
+
constructor(type: InputEvents, source: Event | null, init: PointerEventInit) {
|
13
|
+
super(type, init)
|
110
14
|
this.source = source;
|
111
|
-
this.mode = init.mode;
|
112
|
-
this.ray = init.ray;
|
113
|
-
this.space = init.device;
|
114
15
|
}
|
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
|
-
|
125
16
|
stopImmediatePropagation(): void {
|
126
|
-
this._immediatePropagationStopped = true;
|
127
17
|
super.stopImmediatePropagation();
|
128
18
|
this.source?.stopImmediatePropagation();
|
129
19
|
}
|
130
|
-
stopPropagation(): void {
|
131
|
-
this._propagationStopped = true;
|
132
|
-
super.stopPropagation();
|
133
|
-
this.source?.stopPropagation();
|
134
|
-
}
|
135
20
|
}
|
136
21
|
export class NEKeyboardEvent extends KeyboardEvent {
|
137
22
|
source?: Event
|
@@ -156,49 +41,22 @@
|
|
156
41
|
}
|
157
42
|
}
|
158
43
|
|
44
|
+
export enum InputEvents {
|
45
|
+
PointerDown = "pointerdown",
|
46
|
+
PointerUp = "pointerup",
|
47
|
+
PointerMove = "pointermove",
|
48
|
+
KeyDown = "keydown",
|
49
|
+
KeyUp = "keyup",
|
50
|
+
KeyPressed = "keypress"
|
51
|
+
}
|
159
52
|
|
53
|
+
export enum PointerType {
|
54
|
+
Mouse = "mouse",
|
55
|
+
Touch = "touch",
|
56
|
+
}
|
160
57
|
|
161
|
-
|
162
|
-
declare type KeyboardEventListener = (evt: NEKeyboardEvent) => void;
|
163
|
-
declare type InputEventListener = PointerEventListener | KeyboardEventListener;
|
58
|
+
export class Input extends EventTarget implements IInput {
|
164
59
|
|
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
|
-
|
202
60
|
_doubleClickTimeThreshold = .2;
|
203
61
|
_longPressTimeThreshold = 1;
|
204
62
|
|
@@ -385,41 +243,7 @@
|
|
385
243
|
private _mouseWheelDeltaY: number[] = [0];
|
386
244
|
private _pointerEvent: Event[] = [];
|
387
245
|
private _pointerUsed: boolean[] = [];
|
388
|
-
/** This is added/updated for pointers. screenspace pointers set this to the camera near plane */
|
389
|
-
private _pointerSpace: IGameObject[] = [];
|
390
246
|
|
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
|
-
|
423
247
|
getKeyDown(): string | null {
|
424
248
|
for (const key in this.keysPressed) {
|
425
249
|
const k = this.keysPressed[key];
|
@@ -489,58 +313,39 @@
|
|
489
313
|
return null;
|
490
314
|
}
|
491
315
|
|
492
|
-
|
493
|
-
|
494
|
-
|
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
|
-
}
|
316
|
+
createPointerDown(args: NEPointerEvent) {
|
317
|
+
if (debug) showBalloonMessage("Create Pointer down");
|
318
|
+
this.onDown(args);
|
510
319
|
}
|
511
320
|
|
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
|
+
|
512
331
|
convertScreenspaceToRaycastSpace(vec2: Vec2) {
|
513
332
|
vec2.x = (vec2.x - this.context.domX) / this.context.domWidth * 2 - 1;
|
514
333
|
vec2.y = -((vec2.y - this.context.domY) / this.context.domHeight) * 2 + 1;
|
515
334
|
}
|
516
335
|
|
517
336
|
constructor(context: Context) {
|
337
|
+
super();
|
518
338
|
this.context = context;
|
519
339
|
this.context.post_render_callbacks.push(this.onEndOfFrame);
|
520
|
-
}
|
521
340
|
|
522
|
-
|
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);
|
341
|
+
window.addEventListener('touchstart', this.onTouchStart, false);
|
536
342
|
window.addEventListener('touchmove', this.onTouchMove, { passive: true });
|
537
343
|
window.addEventListener('touchend', this.onTouchUp, false);
|
538
|
-
window.addEventListener("touchcancel", this.onTouchCancel, false);
|
539
344
|
|
540
|
-
|
345
|
+
window.addEventListener('mousedown', this.onMouseDown, false);
|
541
346
|
window.addEventListener('mousemove', this.onMouseMove, false);
|
542
347
|
window.addEventListener('mouseup', this.onMouseUp, false);
|
543
|
-
|
348
|
+
window.addEventListener('wheel', this.onMouseWheel, { passive: true });
|
544
349
|
|
545
350
|
window.addEventListener("keydown", this.onKeyDown, false);
|
546
351
|
window.addEventListener("keypress", this.onKeyPressed, false);
|
@@ -550,19 +355,18 @@
|
|
550
355
|
window.addEventListener('blur', this.onLostFocus);
|
551
356
|
}
|
552
357
|
|
553
|
-
|
554
|
-
|
358
|
+
dispose() {
|
359
|
+
const index = this.context.post_render_callbacks.indexOf(this.onEndOfFrame);
|
360
|
+
if (index >= 0) this.context.post_render_callbacks.splice(index, 1);
|
555
361
|
|
556
|
-
|
557
|
-
window.removeEventListener('touchstart', this.onTouchStartWindow);
|
362
|
+
window.removeEventListener('touchstart', this.onTouchStart, false);
|
558
363
|
window.removeEventListener('touchmove', this.onTouchMove, false);
|
559
364
|
window.removeEventListener('touchend', this.onTouchUp, false);
|
560
|
-
window.removeEventListener("touchcancel", this.onTouchCancel, false);
|
561
365
|
|
562
|
-
|
366
|
+
window.removeEventListener('mousedown', this.onMouseDown, false);
|
563
367
|
window.removeEventListener('mousemove', this.onMouseMove, false);
|
564
368
|
window.removeEventListener('mouseup', this.onMouseUp, false);
|
565
|
-
|
369
|
+
window.removeEventListener('wheel', this.onMouseWheel, false);
|
566
370
|
|
567
371
|
window.removeEventListener("keydown", this.onKeyDown, false);
|
568
372
|
window.removeEventListener("keypress", this.onKeyPressed, false);
|
@@ -571,12 +375,6 @@
|
|
571
375
|
window.removeEventListener('blur', this.onLostFocus);
|
572
376
|
}
|
573
377
|
|
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
|
-
|
580
378
|
private onLostFocus = () => {
|
581
379
|
for (const kp in this.keysPressed) {
|
582
380
|
this.keysPressed[kp].pressed = false;
|
@@ -605,41 +403,17 @@
|
|
605
403
|
// if(evt.target === this.context.renderer.domElement) return true;
|
606
404
|
// const css = window.getComputedStyle(evt.target as HTMLElement);
|
607
405
|
// if(css.pointerEvents === "all") return false;
|
406
|
+
|
608
407
|
// We only check the target elements here since the canvas may be overlapped by other elements
|
609
408
|
// in which case we do not want to use the input (e.g. if a HTML element is being triggered)
|
610
|
-
if
|
611
|
-
if
|
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
|
-
|
409
|
+
if(evt.target === this.context.renderer?.domElement) return true;
|
410
|
+
if(evt.target === this.context.domElement) return true;
|
616
411
|
return false;
|
617
412
|
}
|
618
413
|
|
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
|
-
|
639
414
|
private keysPressed: { [key: KeyCode | string]: { pressed: boolean, frame: number, startFrame: number, key: string, code: KeyCode | string } } = {};
|
640
415
|
|
641
416
|
private onKeyDown = (evt: KeyboardEvent) => {
|
642
|
-
if (debug) console.log(`key down ${evt.code}, ${this.context.application.hasFocus}`, evt);
|
643
417
|
if (!this.context.application.hasFocus)
|
644
418
|
return;
|
645
419
|
const ex = this.keysPressed[evt.code];
|
@@ -679,12 +453,6 @@
|
|
679
453
|
this._mouseWheelDeltaY[0] = current + evt.deltaY;
|
680
454
|
}
|
681
455
|
|
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
|
-
};
|
688
456
|
private onTouchStart = (evt: TouchEvent) => {
|
689
457
|
if (evt.changedTouches.length <= 0) return;
|
690
458
|
if (this.canReceiveInput(evt) === false) return;
|
@@ -692,8 +460,7 @@
|
|
692
460
|
const touch = evt.changedTouches[i];
|
693
461
|
const id = this.getPointerIndex(touch.identifier)
|
694
462
|
if (debug) showBalloonMessage(`touch start #${id}, identifier:${touch.identifier}`);
|
695
|
-
const
|
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 });
|
463
|
+
const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { button: id, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch });
|
697
464
|
this.onDown(ne);
|
698
465
|
}
|
699
466
|
}
|
@@ -702,9 +469,8 @@
|
|
702
469
|
if (evt.changedTouches.length <= 0) return;
|
703
470
|
for (let i = 0; i < evt.changedTouches.length; i++) {
|
704
471
|
const touch = evt.changedTouches[i];
|
705
|
-
const id = this.getPointerIndex(touch.identifier)
|
706
|
-
const
|
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 });
|
472
|
+
const id = this.getPointerIndex(touch.identifier)
|
473
|
+
const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { button: id, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch });
|
708
474
|
this.onMove(ne);
|
709
475
|
}
|
710
476
|
}
|
@@ -714,96 +480,38 @@
|
|
714
480
|
for (let i = 0; i < evt.changedTouches.length; i++) {
|
715
481
|
const touch = evt.changedTouches[i];
|
716
482
|
const id = this.getPointerIndex(touch.identifier);
|
483
|
+
|
717
484
|
if (!this.isNewEvent(evt.timeStamp, id, this._pointerUpTimestamp)) continue;
|
718
|
-
|
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 });
|
719
488
|
this.onUp(ne);
|
720
489
|
}
|
721
490
|
}
|
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
|
-
}
|
729
491
|
|
730
|
-
private onTouchCancel = (_evt: Event) => {
|
731
|
-
};
|
732
|
-
|
733
492
|
private onMouseDown = (evt: MouseEvent) => {
|
734
|
-
this.onDownButton(0, evt.button);
|
735
|
-
if (this.context.isInVR) return;
|
736
493
|
if (evt.defaultPrevented) return;
|
737
494
|
if (this.canReceiveInput(evt) === false) return;
|
738
|
-
|
739
|
-
const
|
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 });
|
495
|
+
const id = evt.button;
|
496
|
+
const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse });
|
749
497
|
this.onDown(ne);
|
750
498
|
}
|
751
499
|
|
752
500
|
private onMouseMove = (evt: MouseEvent) => {
|
753
|
-
if (this.context.isInVR) return;
|
754
501
|
if (evt.defaultPrevented) return;
|
755
|
-
|
756
|
-
const
|
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 });
|
502
|
+
const id = evt.button;
|
503
|
+
const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse });
|
762
504
|
this.onMove(ne);
|
763
505
|
}
|
764
506
|
|
765
507
|
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;
|
777
508
|
if (evt.defaultPrevented) return;
|
778
|
-
const
|
779
|
-
|
509
|
+
const id = evt.button;
|
510
|
+
if (!this.isNewEvent(evt.timeStamp, id, this._pointerUpTimestamp)) return;
|
511
|
+
const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse });
|
780
512
|
this.onUp(ne);
|
781
513
|
}
|
782
514
|
|
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
|
-
|
807
515
|
// Prevent the same event being handled twice (e.g. on touch we get a mouseUp and touchUp evt with the same timestamp)
|
808
516
|
private isNewEvent(timestamp: number, index: number, arr: number[]): boolean {
|
809
517
|
while (arr.length <= index) arr.push(-1);
|
@@ -824,18 +532,12 @@
|
|
824
532
|
}
|
825
533
|
|
826
534
|
private onDown(evt: NEPointerEvent) {
|
827
|
-
|
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);
|
535
|
+
if (debug) console.log(evt.pointerType, "DOWN", evt.button);
|
832
536
|
if (!this.isInRect(evt)) return;
|
833
537
|
|
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
|
-
|
836
538
|
// check if we received an mouse UP event for a touch (for some reason we get a mouse.down for touch.up)
|
837
539
|
if (evt.pointerType === PointerType.Mouse) {
|
838
|
-
const upTime = this._pointerUpTimestamp[
|
540
|
+
const upTime = this._pointerUpTimestamp[evt.button];
|
839
541
|
if (upTime > 0 && evt.source?.timeStamp !== undefined) {
|
840
542
|
const diff = (evt.source.timeStamp - upTime);
|
841
543
|
// on android touch up and mouse up have the exact same value
|
@@ -848,20 +550,20 @@
|
|
848
550
|
}
|
849
551
|
}
|
850
552
|
|
851
|
-
this.setPointerState(
|
852
|
-
this.setPointerState(
|
853
|
-
this.setPointerStateT(
|
553
|
+
this.setPointerState(evt.button, this._pointerPressed, true);
|
554
|
+
this.setPointerState(evt.button, this._pointerDown, true);
|
555
|
+
this.setPointerStateT(evt.button, this._pointerEvent, evt.source);
|
854
556
|
|
855
|
-
while (
|
856
|
-
this._pointerTypes[
|
557
|
+
while (evt.button >= this._pointerTypes.length) this._pointerTypes.push(evt.pointerType);
|
558
|
+
this._pointerTypes[evt.button] = evt.pointerType;
|
857
559
|
|
858
|
-
while (
|
859
|
-
this._pointerPositionDown[
|
860
|
-
while (
|
861
|
-
this._pointerPositions[
|
560
|
+
while (evt.button >= this._pointerPositionDown.length) this._pointerPositionDown.push(new Vector2());
|
561
|
+
this._pointerPositionDown[evt.button].set(evt.clientX, evt.clientY);
|
562
|
+
while (evt.button >= this._pointerPositions.length) this._pointerPositions.push(new Vector2());
|
563
|
+
this._pointerPositions[evt.button].set(evt.clientX, evt.clientY);
|
862
564
|
|
863
|
-
if (
|
864
|
-
this._pointerDownTime[
|
565
|
+
if (evt.button >= this._pointerDownTime.length) this._pointerDownTime.push(0);
|
566
|
+
this._pointerDownTime[evt.button] = this.context.time.time;
|
865
567
|
|
866
568
|
this.updatePointerPosition(evt);
|
867
569
|
|
@@ -869,60 +571,63 @@
|
|
869
571
|
}
|
870
572
|
// moveEvent?: Event;
|
871
573
|
private onMove(evt: NEPointerEvent) {
|
872
|
-
const index = evt.
|
873
|
-
|
574
|
+
const index = evt.button;
|
575
|
+
|
874
576
|
const isDown = this.getPointerPressed(index);
|
875
577
|
if (isDown === false && !this.isInRect(evt)) return;
|
876
578
|
if (evt.pointerType === PointerType.Touch && !isDown) return;
|
877
|
-
if (debug) console.log(evt.pointerType, "MOVE", index
|
878
|
-
|
579
|
+
if (debug) console.log(evt.pointerType, "MOVE", index);
|
580
|
+
|
879
581
|
this.updatePointerPosition(evt);
|
880
582
|
this.setPointerStateT(index, this._pointerEvent, evt.source);
|
881
583
|
this.onDispatchEvent(evt);
|
882
584
|
}
|
883
585
|
private onUp(evt: NEPointerEvent) {
|
884
|
-
|
885
|
-
|
586
|
+
if (this._pointerIds?.length >= evt.button)
|
587
|
+
this._pointerIds[evt.button] = -1;
|
588
|
+
const wasDown = this._pointerPressed[evt.button];
|
886
589
|
if (!wasDown) {
|
887
|
-
if (debug) console.log(evt.pointerType, "UP",
|
590
|
+
if (debug) console.log(evt.pointerType, "UP", evt.button, "was not down");
|
888
591
|
return;
|
889
592
|
}
|
890
|
-
if (debug) console.log(evt.pointerType, "UP",
|
891
|
-
this.setPointerState(
|
892
|
-
this.setPointerStateT(
|
893
|
-
this.setPointerState(index, this._pointerUp, true);
|
593
|
+
if (debug) console.log(evt.pointerType, "UP", evt.button);
|
594
|
+
this.setPointerState(evt.button, this._pointerPressed, false);
|
595
|
+
this.setPointerStateT(evt.button, this._pointerEvent, evt.source);
|
894
596
|
|
895
|
-
|
896
|
-
|
597
|
+
// if (!this.isInRect(evt)) {
|
598
|
+
// if (debug) showBalloonWarning("Pointer out of bounds: " + evt.clientX + ", " + evt.clientY);
|
599
|
+
// return;
|
600
|
+
// }
|
601
|
+
this.setPointerState(evt.button, this._pointerUp, true);
|
897
602
|
|
603
|
+
while (evt.button >= this._pointerUsed.length) this._pointerUsed.push(false);
|
604
|
+
this.setPointerState(evt.button, this._pointerUsed, false);
|
605
|
+
|
898
606
|
this.updatePointerPosition(evt);
|
899
607
|
|
900
|
-
if (!this._pointerPositionDown[
|
901
|
-
if (debug) showBalloonWarning("Received pointer up event without matching down event for button: " +
|
902
|
-
console.warn("Received pointer up event without matching down event for button: " +
|
608
|
+
if (!this._pointerPositionDown[evt.button]) {
|
609
|
+
if (debug) showBalloonWarning("Received pointer up event without matching down event for button: " + evt.button);
|
610
|
+
console.warn("Received pointer up event without matching down event for button: " + evt.button)
|
903
611
|
return;
|
904
612
|
}
|
905
|
-
const dx = evt.clientX - this._pointerPositionDown[
|
906
|
-
const dy = evt.clientY - this._pointerPositionDown[
|
613
|
+
const dx = evt.clientX - this._pointerPositionDown[evt.button].x;
|
614
|
+
const dy = evt.clientY - this._pointerPositionDown[evt.button].y;
|
907
615
|
|
908
|
-
if (
|
616
|
+
if (evt.button >= this._pointerUpTime.length) this._pointerUpTime.push(-99);
|
909
617
|
|
910
|
-
|
618
|
+
// console.log(dx, dy);
|
911
619
|
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) {
|
912
|
-
|
913
|
-
this.setPointerState(index, this._pointerClick, true);
|
914
|
-
evt.isClick = true;
|
620
|
+
this.setPointerState(evt.button, this._pointerClick, true);
|
915
621
|
|
916
622
|
// handle double click
|
917
|
-
const lastUp = this._pointerUpTime[
|
623
|
+
const lastUp = this._pointerUpTime[evt.button];
|
918
624
|
const dt = this.context.time.time - lastUp;
|
919
625
|
// console.log(dt);
|
920
626
|
if (dt < this._doubleClickTimeThreshold && dt > 0) {
|
921
|
-
this.setPointerState(
|
922
|
-
evt.isDoubleClick = true;
|
627
|
+
this.setPointerState(evt.button, this._pointerDoubleClick, true);
|
923
628
|
}
|
924
629
|
}
|
925
|
-
this._pointerUpTime[
|
630
|
+
this._pointerUpTime[evt.button] = this.context.time.time;
|
926
631
|
|
927
632
|
this.onDispatchEvent(evt);
|
928
633
|
}
|
@@ -940,11 +645,11 @@
|
|
940
645
|
let dx = evt.clientX - lf.x;
|
941
646
|
let dy = evt.clientY - lf.y;
|
942
647
|
// if pointer is locked, clientX and Y are not changed, but Movement is.
|
943
|
-
if
|
648
|
+
if(evt.source instanceof MouseEvent || evt.source instanceof TouchEvent) {
|
944
649
|
const source = evt.source as PointerEvent;
|
945
|
-
if
|
650
|
+
if(dx === 0 && source.movementX !== 0)
|
946
651
|
dx = source.movementX || 0;
|
947
|
-
if
|
652
|
+
if(dy === 0 && source.movementY !== 0)
|
948
653
|
dy = source.movementY || 0;
|
949
654
|
}
|
950
655
|
delta.x += dx;
|
@@ -986,16 +691,16 @@
|
|
986
691
|
}
|
987
692
|
|
988
693
|
private setPointerState(index: number, arr: boolean[], value: boolean) {
|
694
|
+
while (arr.length <= index) arr.push(false);
|
989
695
|
arr[index] = value;
|
990
696
|
}
|
991
697
|
|
992
698
|
private setPointerStateT<T>(index: number, arr: T[], value: T) {
|
993
|
-
|
699
|
+
while (arr.length <= index) arr.push(null as any);
|
994
700
|
arr[index] = value;
|
995
|
-
return value;
|
996
701
|
}
|
997
702
|
|
998
|
-
private onDispatchEvent(evt: NEPointerEvent |
|
703
|
+
private onDispatchEvent(evt: NEPointerEvent | KeyboardEvent) {
|
999
704
|
const prevContext = Context.Current;
|
1000
705
|
try {
|
1001
706
|
Context.Current = this.context;
|
@@ -1095,81 +800,81 @@
|
|
1095
800
|
| "F11"
|
1096
801
|
| "F12";
|
1097
802
|
|
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
|
803
|
+
// KEY_1 = 49,
|
804
|
+
// KEY_2 = 50,
|
805
|
+
// KEY_3 = 51,
|
806
|
+
// KEY_4 = 52,
|
807
|
+
// KEY_5 = 53,
|
808
|
+
// KEY_6 = 54,
|
809
|
+
// KEY_7 = 55,
|
810
|
+
// KEY_8 = 56,
|
811
|
+
// KEY_9 = 57,
|
812
|
+
// KEY_A = 65,
|
813
|
+
// KEY_B = 66,
|
814
|
+
// KEY_C = 67,
|
815
|
+
// KEY_D = "d",
|
816
|
+
// KEY_E = 69,
|
817
|
+
// KEY_F = 70,
|
818
|
+
// KEY_G = 71,
|
819
|
+
// KEY_H = 72,
|
820
|
+
// KEY_I = 73,
|
821
|
+
// KEY_J = 74,
|
822
|
+
// KEY_K = 75,
|
823
|
+
// KEY_L = 76,
|
824
|
+
// KEY_M = 77,
|
825
|
+
// KEY_N = 78,
|
826
|
+
// KEY_O = 79,
|
827
|
+
// KEY_P = 80,
|
828
|
+
// KEY_Q = 81,
|
829
|
+
// KEY_R = 82,
|
830
|
+
// KEY_S = 83,
|
831
|
+
// KEY_T = 84,
|
832
|
+
// KEY_U = 85,
|
833
|
+
// KEY_V = 86,
|
834
|
+
// KEY_W = 87,
|
835
|
+
// KEY_X = 88,
|
836
|
+
// KEY_Y = 89,
|
837
|
+
// KEY_Z = 90,
|
838
|
+
// LEFT_META = 91,
|
839
|
+
// RIGHT_META = 92,
|
840
|
+
// SELECT = 93,
|
841
|
+
// NUMPAD_0 = 96,
|
842
|
+
// NUMPAD_1 = 97,
|
843
|
+
// NUMPAD_2 = 98,
|
844
|
+
// NUMPAD_3 = 99,
|
845
|
+
// NUMPAD_4 = 100,
|
846
|
+
// NUMPAD_5 = 101,
|
847
|
+
// NUMPAD_6 = 102,
|
848
|
+
// NUMPAD_7 = 103,
|
849
|
+
// NUMPAD_8 = 104,
|
850
|
+
// NUMPAD_9 = 105,
|
851
|
+
// MULTIPLY = 106,
|
852
|
+
// ADD = 107,
|
853
|
+
// SUBTRACT = 109,
|
854
|
+
// DECIMAL = 110,
|
855
|
+
// DIVIDE = 111,
|
856
|
+
// F1 = 112,
|
857
|
+
// F2 = 113,
|
858
|
+
// F3 = 114,
|
859
|
+
// F4 = 115,
|
860
|
+
// F5 = 116,
|
861
|
+
// F6 = 117,
|
862
|
+
// F7 = 118,
|
863
|
+
// F8 = 119,
|
864
|
+
// F9 = 120,
|
865
|
+
// F10 = 121,
|
866
|
+
// F11 = 122,
|
867
|
+
// F12 = 123,
|
868
|
+
// NUM_LOCK = 144,
|
869
|
+
// SCROLL_LOCK = 145,
|
870
|
+
// SEMICOLON = 186,
|
871
|
+
// EQUALS = 187,
|
872
|
+
// COMMA = 188,
|
873
|
+
// DASH = 189,
|
874
|
+
// PERIOD = 190,
|
875
|
+
// FORWARD_SLASH = 191,
|
876
|
+
// GRAVE_ACCENT = 192,
|
877
|
+
// OPEN_BRACKET = 219,
|
878
|
+
// BACK_SLASH = 220,
|
879
|
+
// CLOSE_BRACKET = 221,
|
880
|
+
// SINGLE_QUOTE = 222
|
@@ -1,8 +1,8 @@
|
|
1
|
+
import { getParam, isMobileDevice } from "./engine_utils.js";
|
2
|
+
import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
|
3
|
+
import type { IContext } from "./engine_types.js";
|
1
4
|
import { logoSVG } from "./assets/index.js";
|
2
5
|
import { GENERATOR, VERSION } from "./engine_constants.js";
|
3
|
-
import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
|
4
|
-
import type { IContext } from "./engine_types.js";
|
5
|
-
import { getParam, isMobileDevice } from "./engine_utils.js";
|
6
6
|
|
7
7
|
const debug = getParam("debuglicense");
|
8
8
|
|
@@ -50,21 +50,18 @@
|
|
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(
|
54
|
-
if (debug) console.error("License check failed", _err);
|
55
|
-
return undefined;
|
56
|
-
});
|
53
|
+
}).catch();
|
57
54
|
if (res?.status === 200) {
|
58
55
|
applicationIsForbidden = false;
|
59
56
|
if (debug) console.log("License check succeeded");
|
60
57
|
NEEDLE_ENGINE_LICENSE_TYPE = "pro";
|
61
58
|
}
|
62
|
-
else if (res
|
59
|
+
else if (res.status === 403) {
|
63
60
|
applicationIsForbidden = true;
|
64
61
|
applicationForbiddenText = await res.text();
|
65
62
|
}
|
66
63
|
else {
|
67
|
-
if (debug) console.log("License check failed with status " + res
|
64
|
+
if (debug) console.log("License check failed with status " + res.status);
|
68
65
|
}
|
69
66
|
}
|
70
67
|
catch (err) {
|
@@ -139,34 +136,23 @@
|
|
139
136
|
const licenseDelay = 1200;
|
140
137
|
|
141
138
|
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
|
-
|
146
139
|
await runtimeLicenseCheckPromise?.catch(() => { });
|
147
140
|
if (hasCommercialLicense()) return;
|
148
141
|
logNonCommercialUse();
|
149
|
-
|
150
|
-
// check if the engine is already ready (meaning has finished loading)
|
151
|
-
if (isReady) {
|
142
|
+
ctx.domElement.addEventListener("ready", () => {
|
152
143
|
insertNonCommercialUseHint(ctx);
|
153
|
-
}
|
154
|
-
else {
|
155
|
-
ctx.domElement.addEventListener("ready", () => {
|
156
|
-
insertNonCommercialUseHint(ctx);
|
157
|
-
});
|
158
|
-
}
|
144
|
+
});
|
159
145
|
}
|
160
146
|
|
161
147
|
function insertNonCommercialUseHint(ctx: IContext) {
|
148
|
+
|
162
149
|
const licenseElement = createLicenseElement();
|
163
150
|
const style = createLicenseStyle();
|
164
151
|
|
165
152
|
const imgElement = document.createElement("img");
|
166
153
|
imgElement.src = logoSVG;
|
167
154
|
imgElement.classList.add("logo");
|
168
|
-
|
169
|
-
imgElement.style.cssText = imageElementCssText;
|
155
|
+
imgElement.style.cssText = `width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;`;
|
170
156
|
licenseElement.appendChild(imgElement);
|
171
157
|
|
172
158
|
const setAndUpdateStyle = () => {
|
@@ -179,9 +165,9 @@
|
|
179
165
|
if (imgElement.parentElement !== licenseElement) {
|
180
166
|
licenseElement.appendChild(imgElement);
|
181
167
|
}
|
182
|
-
if (imgElement.src !== logoSVG
|
168
|
+
if (imgElement.src !== logoSVG) {
|
183
169
|
imgElement.setAttribute("src", logoSVG);
|
184
|
-
imgElement.style.cssText =
|
170
|
+
imgElement.style.cssText = `width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;`;
|
185
171
|
}
|
186
172
|
};
|
187
173
|
|
@@ -1,54 +1,30 @@
|
|
1
|
+
import { ContextEvent } from "./engine_context_registry.js";
|
1
2
|
import { FrameEvent } from "./engine_context.js";
|
2
|
-
import { ContextEvent } from "./engine_context_registry.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
|
-
|
10
|
-
* onInitialized((ctx : Context) => {
|
11
|
-
* // do something
|
12
|
-
* }
|
13
|
-
* ```
|
14
|
-
* */
|
9
|
+
*/
|
15
10
|
export function onInitialized(cb: LifecycleMethod) {
|
16
11
|
registerFrameEventCallback(cb, ContextEvent.ContextCreated);
|
17
12
|
}
|
18
13
|
|
19
14
|
/** Register a callback in the engine start event.
|
20
|
-
* This happens at the beginning of each frame
|
21
|
-
* ```ts
|
22
|
-
* onStart((ctx : Context) => {
|
23
|
-
* // do something
|
24
|
-
* }
|
25
|
-
* ```
|
26
|
-
* */
|
15
|
+
* This happens at the beginning of each frame */
|
27
16
|
export function onStart(cb: LifecycleMethod) {
|
28
17
|
registerFrameEventCallback(cb, FrameEvent.Start);
|
29
18
|
}
|
30
19
|
|
31
20
|
|
32
21
|
/** Register a callback in the engine update event
|
33
|
-
* This is called every frame
|
34
|
-
* ```ts
|
35
|
-
* onUpdate((ctx : Context) => {
|
36
|
-
* // do something
|
37
|
-
* }
|
38
|
-
* ```
|
22
|
+
* This is called every frame
|
39
23
|
* */
|
40
24
|
export function onUpdate(cb: LifecycleMethod) {
|
41
25
|
registerFrameEventCallback(cb, FrameEvent.Update);
|
42
26
|
}
|
43
27
|
|
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
|
-
* */
|
52
28
|
export function onBeforeRender(cb: LifecycleMethod) {
|
53
29
|
registerFrameEventCallback(cb, FrameEvent.OnBeforeRender);
|
54
30
|
}
|
@@ -1,6 +1,6 @@
|
|
1
|
-
import {
|
1
|
+
import { safeInvoke } from "./engine_generic_utils.js";
|
2
|
+
import { FrameEvent, type Context } from "./engine_context.js";
|
2
3
|
import type { ContextEvent } from "./engine_context_registry.js";
|
3
|
-
import { safeInvoke } from "./engine_generic_utils.js";
|
4
4
|
|
5
5
|
export declare type LifecycleMethod = (ctx: Context) => void;
|
6
6
|
export declare type Event = ContextEvent | FrameEvent;
|
@@ -1,9 +1,8 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { LightmapType } from "./extensions/NEEDLE_lightmaps.js";
|
2
|
+
import { Texture, ShaderChunk, UniformsLib, Vector4 } from "three";
|
3
3
|
import { Context } from "./engine_setup.js";
|
4
|
+
import { getParam } from "./engine_utils.js";
|
4
5
|
import type { SourceIdentifier } from "./engine_types.js";
|
5
|
-
import { getParam } from "./engine_utils.js";
|
6
|
-
import { LightmapType } from "./extensions/NEEDLE_lightmaps.js";
|
7
6
|
|
8
7
|
const debugLightmap = getParam("debuglightmaps") ? true : false;
|
9
8
|
|
@@ -1,10 +1,10 @@
|
|
1
1
|
|
2
|
-
import {
|
2
|
+
import { Context } from "./engine_setup.js"
|
3
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
3
4
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
4
|
-
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
5
5
|
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
6
|
+
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
6
7
|
|
7
|
-
import { Context } from "./engine_setup.js"
|
8
8
|
import { getParam } from "./engine_utils.js";
|
9
9
|
|
10
10
|
const debug = getParam("debugdecoders");
|
@@ -1,13 +1,11 @@
|
|
1
|
+
import { safeInvoke } from "./engine_generic_utils.js";
|
2
|
+
import * as constants from "./engine_constants.js";
|
3
|
+
import { getParam } from './engine_utils.js';
|
1
4
|
import { CubeCamera, Object3D, Scene, WebGLCubeRenderTarget } from 'three';
|
2
|
-
|
5
|
+
import type { IComponent, IContext } from './engine_types.js';
|
6
|
+
import { isActiveSelf } from './engine_gameobject.js';
|
7
|
+
import { ContextRegistry } from "./engine_context_registry.js";
|
3
8
|
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';
|
7
|
-
import { safeInvoke } from "./engine_generic_utils.js";
|
8
|
-
import type { IComponent, IContext } from './engine_types.js';
|
9
|
-
import { getParam } from './engine_utils.js';
|
10
|
-
import type { INeedleXRSessionEventReceiver } from "./engine_xr.js";
|
11
9
|
|
12
10
|
const debug = getParam("debugnewscripts");
|
13
11
|
const debugHierarchy = getParam("debughierarchy");
|
@@ -210,12 +208,9 @@
|
|
210
208
|
if (script.onBeforeRender) context.scripts_onBeforeRender.push(script);
|
211
209
|
if (script.onAfterRender) context.scripts_onAfterRender.push(script);
|
212
210
|
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);
|
217
211
|
}
|
218
212
|
|
213
|
+
|
219
214
|
export function removeScriptFromContext(script: any, context: IContext) {
|
220
215
|
removeFromArray(script, context.new_scripts);
|
221
216
|
removeFromArray(script, context.new_script_start);
|
@@ -226,9 +221,6 @@
|
|
226
221
|
removeFromArray(script, context.scripts_onBeforeRender);
|
227
222
|
removeFromArray(script, context.scripts_onAfterRender);
|
228
223
|
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);
|
232
224
|
context.stopAllCoroutinesFrom(script);
|
233
225
|
}
|
234
226
|
|
@@ -237,26 +229,7 @@
|
|
237
229
|
if (index >= 0) array.splice(index, 1);
|
238
230
|
}
|
239
231
|
|
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
|
-
}
|
258
232
|
|
259
|
-
|
260
233
|
export function updateIsActive(obj?: Object3D) {
|
261
234
|
if (!obj) obj = ContextRegistry.Current.scene;
|
262
235
|
if (!obj) {
|
@@ -1,6 +1,6 @@
|
|
1
|
+
import { getParam } from "./engine_utils.js";
|
1
2
|
import { isDevEnvironment } from "./debug/index.js";
|
2
3
|
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,9 +1,8 @@
|
|
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 { UIDProvider } from "./engine_types.js";
|
4
5
|
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
5
|
-
|
6
|
-
import type { UIDProvider } from "./engine_types.js";
|
7
6
|
// import { Animation } from "../engine-components/Animation.js";
|
8
7
|
|
9
8
|
|
@@ -1,16 +1,15 @@
|
|
1
|
-
import {
|
2
|
-
import
|
3
|
-
|
4
|
-
import { getLoader } from "../engine/engine_gltf.js";
|
1
|
+
import { Context } from "../engine/engine_setup.js";
|
2
|
+
import * as web from "../engine/engine_web_api.js";
|
5
3
|
import { NetworkConnection } from "../engine/engine_networking.js";
|
6
4
|
import { generateSeed, InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
7
|
-
import { Context } from "../engine/engine_setup.js";
|
8
|
-
import * as web from "../engine/engine_web_api.js";
|
9
|
-
import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
|
10
|
-
import { findByGuid } from "./engine_gameobject.js";
|
11
5
|
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";
|
12
8
|
import type { IModel } from "./engine_networking_types.js";
|
13
9
|
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";
|
14
13
|
|
15
14
|
export enum File_Event {
|
16
15
|
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 {
|
3
|
+
import { Context } from "./engine_setup.js"
|
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
|
+
|
4
8
|
// https://github.com/uuidjs/uuid
|
5
9
|
// v5 takes string and namespace
|
6
10
|
import { v5 } from 'uuid';
|
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";
|
11
|
+
import type { UIDProvider } from "./engine_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 { IInstantiateOptions, destroy, findByGuid, instantiate } from "./engine_gameobject.js";
|
15
|
+
import { Object3D } from "three";
|
16
|
+
import { InstantiateOptions } from "./engine_gameobject.js";
|
17
|
+
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
|
18
18
|
|
19
19
|
|
20
20
|
|
@@ -163,7 +163,7 @@
|
|
163
163
|
}
|
164
164
|
}
|
165
165
|
|
166
|
-
|
166
|
+
class NewInstanceModel implements IModel {
|
167
167
|
guid: string;
|
168
168
|
originalGuid: string;
|
169
169
|
seed: number | undefined;
|
@@ -176,9 +176,6 @@
|
|
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
|
-
|
182
179
|
constructor(originalGuid: string, newGuid: string) {
|
183
180
|
this.originalGuid = originalGuid;
|
184
181
|
this.guid = newGuid;
|
@@ -252,13 +249,11 @@
|
|
252
249
|
export function beginListenInstantiate(context: Context) {
|
253
250
|
context.connection.beginListen(InstantiateEvent.NewInstanceCreated, async (model: NewInstanceModel) => {
|
254
251
|
const obj: GameObject | null = await tryResolvePrefab(model.originalGuid, context.scene) as GameObject;
|
255
|
-
if (model.preventCreation === true) {
|
256
|
-
return;
|
257
|
-
}
|
258
252
|
if (!obj) {
|
259
253
|
console.warn("could not find object that was instantiated: " + model.guid);
|
260
254
|
return;
|
261
255
|
}
|
256
|
+
// console.log(model);
|
262
257
|
const options = new InstantiateOptions();
|
263
258
|
if (model.position)
|
264
259
|
options.position = new THREE.Vector3(model.position.x, model.position.y, model.position.z);
|
@@ -1,6 +1,5 @@
|
|
1
|
+
import Peer, { type PeerConnectOption } from "peerjs";
|
1
2
|
import type { DataConnection, PeerJSOption } from "peerjs";
|
2
|
-
import Peer, { type PeerConnectOption } from "peerjs";
|
3
|
-
|
4
3
|
import { type ConstructorConcrete } from "./engine_types.js";
|
5
4
|
|
6
5
|
let peerOptions: PeerJSOption | undefined = undefined;
|
@@ -1,13 +1,12 @@
|
|
1
|
+
import { type Context } from "./engine_context.js";
|
1
2
|
import Peer, { MediaConnection } from "peerjs"
|
2
|
-
import { EventDispatcher } from "three";
|
3
|
-
|
4
3
|
import { RoomEvents } from "../engine/engine_networking.js";
|
5
4
|
import { UserJoinedOrLeftRoomModel } from "../engine/engine_networking.js";
|
5
|
+
import type { IModel } from "./engine_networking_types.js";
|
6
6
|
import { getPeerjsInstance } from "../engine/engine_networking_peer.js";
|
7
|
-
import {
|
8
|
-
import
|
7
|
+
import { EventDispatcher } from "three";
|
8
|
+
import { getParam } from "./engine_utils.js";
|
9
9
|
import { type IComponent } from "./engine_types.js";
|
10
|
-
import { getParam } from "./engine_utils.js";
|
11
10
|
|
12
11
|
|
13
12
|
|
@@ -57,7 +56,7 @@
|
|
57
56
|
Outgoing = "outgoing",
|
58
57
|
}
|
59
58
|
|
60
|
-
class CallHandle extends EventDispatcher
|
59
|
+
class CallHandle extends EventDispatcher {
|
61
60
|
readonly userId: string;
|
62
61
|
readonly direction: CallDirection;
|
63
62
|
readonly call: MediaConnection;
|
@@ -106,7 +105,7 @@
|
|
106
105
|
}
|
107
106
|
}
|
108
107
|
|
109
|
-
export class PeerHandle extends EventDispatcher
|
108
|
+
export class PeerHandle extends EventDispatcher {
|
110
109
|
|
111
110
|
private static readonly instances: Map<string, PeerHandle> = new Map();
|
112
111
|
|
@@ -306,7 +305,7 @@
|
|
306
305
|
// userId: string;
|
307
306
|
// }
|
308
307
|
|
309
|
-
export class NetworkedStreams extends EventDispatcher
|
308
|
+
export class NetworkedStreams extends EventDispatcher {
|
310
309
|
|
311
310
|
static create(comp: IComponent) {
|
312
311
|
const peer = PeerHandle.getOrCreate(comp.context, comp.context.connection.connectionId!);
|
@@ -1,21 +1,19 @@
|
|
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";
|
4
8
|
import * as flatbuffers from 'flatbuffers';
|
5
|
-
import { type Websocket } from 'websocket-ts';
|
6
|
-
|
7
9
|
import * as schemes from "../engine-schemes/schemes.js";
|
8
|
-
import { isDevEnvironment } from './debug/debug.js';
|
9
10
|
import { PeerNetworking } from './engine_networking_peer.js';
|
10
11
|
import { type IModel, type INetworkConnection, SendQueue } from './engine_networking_types.js';
|
11
12
|
import { isHostedOnGlitch } from './engine_networking_utils.js';
|
12
|
-
|
13
|
-
import { Context } from './engine_setup.js';
|
14
|
-
import * as utils from "./engine_utils.js";
|
13
|
+
import { isDevEnvironment } from './debug/debug.js';
|
15
14
|
|
16
15
|
export const debugNet = utils.getParam("debugnet") ? true : false;
|
17
16
|
export const debugOwner = debugNet || utils.getParam("debugowner") ? true : false;
|
18
|
-
const debugnetBin = utils.getParam("debugnetbin");
|
19
17
|
|
20
18
|
export interface INetworkingWebsocketUrlProvider {
|
21
19
|
getWebsocketUrl(): string | null;
|
@@ -391,7 +389,7 @@
|
|
391
389
|
|
392
390
|
/** Send a binary message to the server (broadcasted to all connected users) */
|
393
391
|
public sendBinary(bin: Uint8Array) {
|
394
|
-
if (
|
392
|
+
if (debugNet) console.log("<< bin", bin.length);
|
395
393
|
this._ws?.send(bin);
|
396
394
|
}
|
397
395
|
|
@@ -549,11 +547,10 @@
|
|
549
547
|
console.error("⊠Websocket error", i, ev);
|
550
548
|
resolve(false);
|
551
549
|
})
|
550
|
+
.onMessage(this.onMessage.bind(this))
|
552
551
|
.onRetry(() => { console.log("Retry connecting to networking websocket") })
|
553
552
|
.build();
|
554
|
-
|
555
|
-
this.onMessage(socket, msg);
|
556
|
-
});
|
553
|
+
|
557
554
|
});
|
558
555
|
}
|
559
556
|
|
@@ -584,7 +581,6 @@
|
|
584
581
|
}
|
585
582
|
|
586
583
|
private async handleIncomingBinaryMessage(blob: Blob) {
|
587
|
-
if (debugnetBin) console.log("<< bin", this.context.time.frame);
|
588
584
|
const buf = await blob.arrayBuffer();
|
589
585
|
var data = new Uint8Array(buf);
|
590
586
|
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';
|
2
1
|
import { BufferAttribute, BufferGeometry, LineBasicMaterial, LineSegments, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from 'three'
|
3
2
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
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';
|
3
|
+
import { CircularBuffer, getParam } from "./engine_utils.js"
|
11
4
|
import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPositionXYZ, setWorldQuaternionXYZW } from "./engine_three_utils.js"
|
12
5
|
import type {
|
13
|
-
|
6
|
+
IPhysicsEngine,
|
7
|
+
IComponent,
|
14
8
|
ICollider,
|
15
|
-
|
9
|
+
IRigidbody,
|
10
|
+
Vec3,
|
11
|
+
IGameObject,
|
12
|
+
Vec2,
|
16
13
|
IContext,
|
17
|
-
IGameObject,
|
18
|
-
IPhysicsEngine,
|
19
|
-
IRigidbody,
|
20
14
|
ISphereCollider,
|
21
|
-
|
22
|
-
Vec3,
|
15
|
+
IBoxCollider,
|
23
16
|
} from './engine_types.js';
|
24
|
-
import {
|
17
|
+
import { ContactPoint, Collision } from './engine_types.js';
|
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';
|
25
24
|
import { SphereOverlapResult } from './engine_types.js';
|
26
|
-
import {
|
25
|
+
import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
|
26
|
+
import { isDevEnvironment } from './debug/debug.js';
|
27
27
|
|
28
28
|
const debugPhysics = getParam("debugphysics");
|
29
29
|
const debugColliderPlacement = getParam("debugcolliderplacement");
|
@@ -166,14 +166,12 @@
|
|
166
166
|
addForce(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
|
167
167
|
this.validate();
|
168
168
|
const body = this.internal_getRigidbody(rigidbody);
|
169
|
-
|
170
|
-
else console.warn("Rigidbody doesn't exist: can not apply force");
|
169
|
+
body?.addForce(force, wakeup)
|
171
170
|
}
|
172
171
|
addImpulse(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
|
173
172
|
this.validate();
|
174
173
|
const body = this.internal_getRigidbody(rigidbody);
|
175
|
-
|
176
|
-
else console.warn("Rigidbody doesn't exist: can not apply impulse");
|
174
|
+
body?.applyImpulse(force, wakeup)
|
177
175
|
}
|
178
176
|
getLinearVelocity(comp: IRigidbody | ICollider): Vec3 | null {
|
179
177
|
this.validate();
|
@@ -206,15 +204,13 @@
|
|
206
204
|
applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
|
207
205
|
this.validate();
|
208
206
|
const body = this.internal_getRigidbody(rb);
|
209
|
-
|
210
|
-
else console.warn("Rigidbody doesn't exist: can not apply impulse");
|
207
|
+
body?.applyImpulse(vec, wakeup);
|
211
208
|
}
|
212
209
|
|
213
210
|
wakeup(rb: IRigidbody) {
|
214
211
|
this.validate();
|
215
212
|
const body = this.internal_getRigidbody(rb);
|
216
|
-
|
217
|
-
else console.warn("Rigidbody doesn't exist: can not wake up");
|
213
|
+
body?.wakeUp();
|
218
214
|
}
|
219
215
|
isSleeping(rb: IRigidbody) {
|
220
216
|
this.validate();
|
@@ -224,14 +220,12 @@
|
|
224
220
|
setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
|
225
221
|
this.validate();
|
226
222
|
const body = this.internal_getRigidbody(rb);
|
227
|
-
|
228
|
-
else console.warn("Rigidbody doesn't exist: can not set angular velocity");
|
223
|
+
body?.setAngvel(vec, wakeup);
|
229
224
|
}
|
230
225
|
setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
|
231
226
|
this.validate();
|
232
227
|
const body = this.internal_getRigidbody(rb);
|
233
|
-
|
234
|
-
else console.warn("Rigidbody doesn't exist: can not set linear velocity");
|
228
|
+
body?.setLinvel(vec, wakeup);
|
235
229
|
}
|
236
230
|
|
237
231
|
private context?: IContext;
|
@@ -898,11 +892,9 @@
|
|
898
892
|
case ShapeType.Cuboid:
|
899
893
|
const cuboid = shape as Cuboid;
|
900
894
|
const sc = col as IBoxCollider;
|
901
|
-
const
|
902
|
-
const
|
903
|
-
const
|
904
|
-
const newY = sc.size.y * 0.5 * scale.y;
|
905
|
-
const newZ = sc.size.z * 0.5 * scale.z;
|
895
|
+
const newX = sc.size.x * 0.5;
|
896
|
+
const newY = sc.size.y * 0.5;
|
897
|
+
const newZ = sc.size.z * 0.5;
|
906
898
|
sizeHasChanged = cuboid.halfExtents.x !== newX || cuboid.halfExtents.y !== newY || cuboid.halfExtents.z !== newZ;
|
907
899
|
cuboid.halfExtents.x = newX;
|
908
900
|
cuboid.halfExtents.y = newY;
|
@@ -996,22 +988,6 @@
|
|
996
988
|
}
|
997
989
|
this.world.step(this.eventQueue);
|
998
990
|
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
|
-
}
|
1015
991
|
this.updateDebugRendering(this.world);
|
1016
992
|
}
|
1017
993
|
|
@@ -1019,7 +995,7 @@
|
|
1019
995
|
if (debugPhysics || debugColliderPlacement || showColliders || this.debugRenderColliders === true) {
|
1020
996
|
if (!this.lines) {
|
1021
997
|
const material = new LineBasicMaterial({
|
1022
|
-
color:
|
998
|
+
color: 0x227700,
|
1023
999
|
fog: false,
|
1024
1000
|
// vertexColors: THREE.VertexColors
|
1025
1001
|
});
|
@@ -1041,6 +1017,22 @@
|
|
1041
1017
|
}
|
1042
1018
|
}
|
1043
1019
|
|
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
|
+
|
1044
1036
|
/** sync rendered objects with physics world (except for colliders without rigidbody) */
|
1045
1037
|
private syncObjects() {
|
1046
1038
|
if (debugColliderPlacement) return;
|
@@ -1077,8 +1069,8 @@
|
|
1077
1069
|
if (center && center.isVector3) {
|
1078
1070
|
this._tempQuaternion.set(rot.x, rot.y, rot.z, rot.w);
|
1079
1071
|
const offset = this._tempPosition.copy(center).applyQuaternion(this._tempQuaternion);
|
1080
|
-
const scale = getWorldScale(obj.gameObject);
|
1081
|
-
offset.multiply(scale);
|
1072
|
+
// const scale = getWorldScale(obj.gameObject);
|
1073
|
+
// offset.multiply(scale);
|
1082
1074
|
pos.x -= offset.x;
|
1083
1075
|
pos.y -= offset.y;
|
1084
1076
|
pos.z -= offset.z;
|
@@ -1175,14 +1167,8 @@
|
|
1175
1167
|
this._tempCenterPos.z = center.z;
|
1176
1168
|
getWorldScale(collider.gameObject, this._tempCenterVec);
|
1177
1169
|
this._tempCenterPos.multiply(this._tempCenterVec);
|
1178
|
-
|
1179
|
-
|
1180
|
-
getWorldQuaternion(collider.gameObject, this._tempCenterQuaternion);
|
1181
|
-
this._tempCenterPos.applyQuaternion(this._tempCenterQuaternion);
|
1182
|
-
}
|
1183
|
-
else {
|
1184
|
-
this._tempCenterPos.applyQuaternion(collider.gameObject.quaternion);
|
1185
|
-
}
|
1170
|
+
const rot = getWorldQuaternion(collider.gameObject, this._tempCenterQuaternion);
|
1171
|
+
this._tempCenterPos.applyQuaternion(rot);
|
1186
1172
|
targetVector.x += this._tempCenterPos.x;
|
1187
1173
|
targetVector.y += this._tempCenterPos.y;
|
1188
1174
|
targetVector.z += this._tempCenterPos.z;
|
@@ -1296,7 +1282,6 @@
|
|
1296
1282
|
this.eventQueue.drainCollisionEvents((handle1, handle2, started) => {
|
1297
1283
|
const col1 = this.world!.getCollider(handle1);
|
1298
1284
|
const col2 = this.world!.getCollider(handle2);
|
1299
|
-
if (!col1 || !col2) return;
|
1300
1285
|
const colliderComponent1 = col1[$componentKey];
|
1301
1286
|
const colliderComponent2 = col2[$componentKey];
|
1302
1287
|
if (debugCollisions)
|
@@ -1,11 +1,10 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import { Gizmos } from './engine_gizmos.js';
|
1
|
+
import { Box3, Camera, type Intersection, Layers, Mesh, Object3D, Ray, Raycaster, Sphere, Vector2, Vector3, AxesHelper, Line } from 'three'
|
4
2
|
import { Context } from './engine_setup.js';
|
3
|
+
import { getParam } from "./engine_utils.js"
|
5
4
|
import { getWorldPosition } from "./engine_three_utils.js"
|
6
5
|
import type { Vec2, Vec3, } from './engine_types.js';
|
7
6
|
import type { IPhysicsEngine } from './engine_types.js';
|
8
|
-
import {
|
7
|
+
import { Gizmos } from './engine_gizmos.js';
|
9
8
|
|
10
9
|
const debugPhysics = getParam("debugphysics");
|
11
10
|
const layerMaskHelper: Layers = new Layers();
|
@@ -13,7 +12,7 @@
|
|
13
12
|
export declare type RaycastTestObjectReturnType = void | boolean | "continue in children";
|
14
13
|
export declare type RaycastTestObjectCallback = (obj: Object3D) => RaycastTestObjectReturnType;
|
15
14
|
|
16
|
-
|
15
|
+
declare interface IRaycastOptions {
|
17
16
|
/** Optionally a custom raycaster can be provided. Other properties will then be set on this raycaster */
|
18
17
|
raycaster?: Raycaster;
|
19
18
|
/** Optional ray that can be used for raycasting
|
@@ -166,19 +165,17 @@
|
|
166
165
|
if (obj.type === "Mesh" && obj.layers.test(mask) && !Gizmos.isGizmo(obj)) {
|
167
166
|
const mesh = obj as Mesh;
|
168
167
|
const geo = mesh.geometry;
|
169
|
-
if (geo)
|
170
|
-
|
171
|
-
|
172
|
-
if (
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
if (!traverseChildsAfterHit) return;
|
181
|
-
}
|
168
|
+
if (!geo.boundingBox)
|
169
|
+
geo.computeBoundingBox();
|
170
|
+
if (geo.boundingBox) {
|
171
|
+
if (mesh.matrixWorldNeedsUpdate) mesh.updateMatrixWorld();
|
172
|
+
const test = this.tempBoundingBox.copy(geo.boundingBox).applyMatrix4(mesh.matrixWorld);
|
173
|
+
if (sp.intersectsBox(test)) {
|
174
|
+
const wp = getWorldPosition(obj);
|
175
|
+
const dist = wp.distanceTo(sp.center);
|
176
|
+
const int = new SphereIntersection(obj, dist, wp);
|
177
|
+
results.push(int);
|
178
|
+
if (!traverseChildsAfterHit) return;
|
182
179
|
}
|
183
180
|
}
|
184
181
|
}
|
@@ -191,7 +188,7 @@
|
|
191
188
|
}
|
192
189
|
}
|
193
190
|
|
194
|
-
public raycastFromRay(ray: Ray, options: IRaycastOptions | null = null): Array<Intersection> {
|
191
|
+
public raycastFromRay(ray: Ray, options: IRaycastOptions | RaycastOptions | null = null): Array<Intersection> {
|
195
192
|
const opts = options ?? this.defaultRaycastOptions;
|
196
193
|
opts.ray = ray;
|
197
194
|
const res = this.raycast(opts);
|
@@ -206,7 +203,7 @@
|
|
206
203
|
* Use raycastPhysics for raycasting against physic colliders only. Depending on your scenario this might be faster.
|
207
204
|
* @param options raycast options. If null, default options will be used.
|
208
205
|
*/
|
209
|
-
public raycast(options: IRaycastOptions | null = null): Array<Intersection> {
|
206
|
+
public raycast(options: IRaycastOptions | RaycastOptions | null = null): Array<Intersection> {
|
210
207
|
if (!options) options = this.defaultRaycastOptions;
|
211
208
|
const mp = options.screenPoint ?? this.context.input.mousePositionRC;
|
212
209
|
const rc = options.raycaster ?? this.raycaster;
|
@@ -274,10 +271,8 @@
|
|
274
271
|
|
275
272
|
private intersect(raycaster: Raycaster, objects: Object3D[], results: Intersection[], options: IRaycastOptions) {
|
276
273
|
for (const obj of objects) {
|
277
|
-
// dont raycast invisible objects
|
278
|
-
if (obj.visible === false) continue;
|
279
|
-
|
280
274
|
if (Gizmos.isGizmo(obj)) continue;
|
275
|
+
|
281
276
|
// dont raycast object if it's a line and the line threshold is < 0
|
282
277
|
if (options.lineThreshold !== undefined && options.lineThreshold < 0) {
|
283
278
|
if (obj instanceof Line) {
|
@@ -1,7 +1,6 @@
|
|
1
|
+
import { getParam } from "./engine_utils.js";
|
1
2
|
import { Object3D } from "three";
|
2
|
-
|
3
3
|
import { Context } from "./engine_setup.js";
|
4
|
-
import { getParam } from "./engine_utils.js";
|
5
4
|
|
6
5
|
const debug = getParam("debugplayerview");
|
7
6
|
|
@@ -1,11 +1,10 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import { AssetReference } from "./engine_addressables.js";
|
1
|
+
import { Vector4, EquirectangularReflectionMapping, WebGLCubeRenderTarget, Texture, LightProbe, SphericalHarmonics3, SRGBColorSpace } from "three";
|
4
2
|
import { Context } from "./engine_setup.js";
|
3
|
+
import { SceneLightSettings } from "./extensions/NEEDLE_lighting_settings.js";
|
5
4
|
import { createFlatTexture, createTrilightTexture } from "./engine_shaders.js";
|
5
|
+
import { getParam } from "./engine_utils.js";
|
6
6
|
import { type SourceIdentifier } from "./engine_types.js";
|
7
|
-
import {
|
8
|
-
import { SceneLightSettings } from "./extensions/NEEDLE_lighting_settings.js";
|
7
|
+
import { AssetReference } from "./engine_addressables.js";
|
9
8
|
// import { LightProbeGenerator } from "three/examples/jsm/lights/LightProbeGenerator.js"
|
10
9
|
|
11
10
|
const debug = getParam("debugenvlight");
|
@@ -1,18 +1,17 @@
|
|
1
|
-
import {
|
1
|
+
import { Context } from "./engine_setup.js"
|
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";
|
7
3
|
// import * as object from "./engine_gltf_builtin_components.js";
|
8
4
|
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";
|
13
5
|
import * as utils from "./engine_utils.js";
|
14
6
|
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";
|
15
11
|
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";
|
16
15
|
|
17
16
|
|
18
17
|
export class NeedleGltfLoader implements INeedleGltfLoader {
|
@@ -1,15 +1,14 @@
|
|
1
1
|
import * as THREE from "three";
|
2
|
-
import {
|
3
|
-
|
4
|
-
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
|
2
|
+
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
3
|
+
import { SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
|
5
4
|
import { Behaviour, Component, GameObject } from "../engine-components/Component.js";
|
5
|
+
import { debugExtension } from "./engine_default_parameters.js";
|
6
6
|
import { CallInfo, EventList } from "../engine-components/EventList.js";
|
7
|
-
import {
|
8
|
-
import { AssetReference } from "./engine_addressables.js";
|
9
|
-
import { debugExtension } from "./engine_default_parameters.js";
|
10
|
-
import { SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
|
7
|
+
import { Color, CompressedTexture, Object3D, Texture, WebGLRenderTarget } from "three";
|
11
8
|
import { RenderTexture } from "./engine_texture.js";
|
9
|
+
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
|
12
10
|
import { resolveUrl } from "./engine_utils.js";
|
11
|
+
import { AssetReference } from "./engine_addressables.js";
|
13
12
|
|
14
13
|
// export class SourcePath {
|
15
14
|
// src?:string
|
@@ -157,14 +156,6 @@
|
|
157
156
|
|
158
157
|
onDeserialize(data: any, context: SerializationContext) {
|
159
158
|
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;
|
168
159
|
// TODO: need to serialize some identifier for referenced components as well, maybe just guid?
|
169
160
|
// because here the components are created but dont have their former guid assigned
|
170
161
|
// and will later in the stack just get a newly generated guid
|
@@ -182,9 +173,8 @@
|
|
182
173
|
res = this.findObjectForGuid(data.guid, context.context?.scene);
|
183
174
|
if (res) return res;
|
184
175
|
}
|
185
|
-
if (isDevEnvironment() || debugExtension)
|
186
|
-
console.warn("Could not resolve component reference
|
187
|
-
}
|
176
|
+
if (isDevEnvironment() || debugExtension)
|
177
|
+
console.warn("Could not resolve component reference", context.path, data, context.target);
|
188
178
|
data["could_not_resolve"] = true;
|
189
179
|
return undefined;
|
190
180
|
}
|
@@ -1,14 +1,13 @@
|
|
1
|
+
import { type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
|
+
import { getParam } from "./engine_utils.js";
|
1
3
|
import { AnimationClip, Material, Mesh, Object3D, Texture } from "three";
|
2
|
-
import {
|
3
|
-
|
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";
|
4
7
|
import { debugExtension } from "../engine/engine_default_parameters.js";
|
5
|
-
import {
|
8
|
+
import { LogType, addLog } from "./debug/debug_overlay.js";
|
6
9
|
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";
|
9
10
|
import { $BuiltInTypeFlag } from "./engine_typestore.js";
|
10
|
-
import { getParam } from "./engine_utils.js";
|
11
|
-
import { isPersistentAsset } from "./extensions/NEEDLE_persistent_assets.js";
|
12
11
|
|
13
12
|
const debug = getParam("debugserializer");
|
14
13
|
|
@@ -125,7 +124,7 @@
|
|
125
124
|
// }
|
126
125
|
// }
|
127
126
|
|
128
|
-
constructor(type:
|
127
|
+
constructor(type: ConstructorConcrete<any> | ConstructorConcrete<any>[]) {
|
129
128
|
if (Array.isArray(type)) {
|
130
129
|
for (const key of type)
|
131
130
|
helper.register(key.name, this);
|
@@ -360,6 +359,7 @@
|
|
360
359
|
obj.onAfterDeserialize(serializedData, context);
|
361
360
|
}
|
362
361
|
|
362
|
+
context.path = undefined;
|
363
363
|
return true;
|
364
364
|
}
|
365
365
|
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import {
|
1
|
+
import { serializeObject, deserializeObject } from "./engine_serialization_core.js";
|
2
2
|
|
3
|
-
export {
|
3
|
+
export { serializeObject, deserializeObject };
|
4
4
|
|
5
|
-
export
|
6
|
-
|
5
|
+
export { serializable, serializeable } from "./engine_serialization_decorator.js"
|
6
|
+
|
7
|
+
export * from "./engine_serialization_builtin_serializer.js";
|
@@ -1,10 +1,9 @@
|
|
1
1
|
|
2
|
-
import
|
3
|
-
|
2
|
+
import * as loader from "./engine_fileloader.js"
|
3
|
+
import * as SHADERDATA from "./shaders/shaderData.js"
|
4
|
+
import { Vector4, FileLoader, DataTexture, RGBAFormat, Color } from "three";
|
4
5
|
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
5
|
-
import * as loader from "./engine_fileloader.js"
|
6
6
|
import { Mathf } from "./engine_math.js";
|
7
|
-
import * as SHADERDATA from "./shaders/shaderData.js"
|
8
7
|
|
9
8
|
|
10
9
|
const white = new Uint8Array(4);
|
@@ -1,6 +1,5 @@
|
|
1
|
+
import { Camera, Mesh, Object3D, Texture, WebGLRenderer, WebGLRenderTarget } from "three";
|
1
2
|
import { EffectComposer } from "postprocessing";
|
2
|
-
import { Camera, Mesh, Object3D, Texture, WebGLRenderer, WebGLRenderTarget } from "three";
|
3
|
-
|
4
3
|
import { findResourceUsers } from "./engine_assetdatabase.js";
|
5
4
|
|
6
5
|
|
@@ -1,7 +1,6 @@
|
|
1
|
-
import { AnimationAction, Euler, Mesh,Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, Texture, Uniform, Vector3 } from "three";
|
2
|
-
import { ShaderMaterial,WebGLRenderer } from "three";
|
3
|
-
|
4
1
|
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";
|
5
4
|
import { CircularBuffer } from "./engine_utils.js";
|
6
5
|
|
7
6
|
|
@@ -48,24 +47,11 @@
|
|
48
47
|
|
49
48
|
|
50
49
|
const _tempVecs = new CircularBuffer(() => new Vector3(), 100);
|
51
|
-
export function getTempVector(
|
50
|
+
export function getTempVector(value?: Vector3) {
|
52
51
|
const vec = _tempVecs.get();
|
53
|
-
if
|
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
|
-
}
|
52
|
+
if(value instanceof Vector3) vec.copy(value);
|
60
53
|
return vec;
|
61
54
|
}
|
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
|
-
}
|
69
55
|
|
70
56
|
|
71
57
|
const _worldPositions = new CircularBuffer(() => new Vector3(), 100);
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import { Clock } from 'three'
|
2
|
-
|
2
|
+
import { getParam } from './engine_utils.js';
|
3
3
|
import { type ITime } from './engine_types.js';
|
4
|
-
import { getParam } from './engine_utils.js';
|
5
4
|
|
6
5
|
const timescaleUrl = getParam("timescale");
|
7
6
|
let timeScale = 1;
|
@@ -46,8 +45,8 @@
|
|
46
45
|
this.frame += 1;
|
47
46
|
this.time += this.deltaTime;
|
48
47
|
|
49
|
-
if (this._fpsSamples.length <
|
50
|
-
else this._fpsSamples[(this._fpsSampleIndex++) %
|
48
|
+
if (this._fpsSamples.length < 30) this._fpsSamples.push(this.deltaTime);
|
49
|
+
else this._fpsSamples[(this._fpsSampleIndex++) % 30] = this.deltaTime;
|
51
50
|
let sum = 0;
|
52
51
|
for (let i = 0; i < this._fpsSamples.length; i++)
|
53
52
|
sum += this._fpsSamples[i];
|
@@ -1,12 +1,10 @@
|
|
1
|
-
import
|
1
|
+
import { RenderTexture } from "./engine_texture.js";
|
2
|
+
import type { Camera, Color, Material, Object3D, Quaternion, Ray, Scene, WebGLRenderer, Mesh } from "three";
|
2
3
|
import { Vector3 } from "three";
|
3
|
-
import { type GLTF as GLTF3 } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
|
-
|
5
4
|
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
6
5
|
import { CollisionDetectionMode, type PhysicsMaterial, RigidbodyConstraints } from "./engine_physics.types.js";
|
7
|
-
import { RenderTexture } from "./engine_texture.js";
|
8
6
|
import { CircularBuffer } from "./engine_utils.js";
|
9
|
-
import type
|
7
|
+
import { type GLTF as GLTF3 } from "three/examples/jsm/loaders/GLTFLoader.js";
|
10
8
|
|
11
9
|
export type GLTF = GLTF3 & {
|
12
10
|
// asset: { generator: string, version: string }
|
@@ -74,14 +72,13 @@
|
|
74
72
|
|
75
73
|
scripts: IComponent[];
|
76
74
|
scripts_pausedChanged: IComponent[];
|
75
|
+
// scripts with update event
|
77
76
|
scripts_earlyUpdate: IComponent[];
|
78
77
|
scripts_update: IComponent[];
|
79
78
|
scripts_lateUpdate: IComponent[];
|
80
79
|
scripts_onBeforeRender: IComponent[];
|
81
80
|
scripts_onAfterRender: IComponent[];
|
82
81
|
scripts_WithCorroutines: IComponent[];
|
83
|
-
scripts_immersive_vr: INeedleXRSessionEventReceiver[];
|
84
|
-
scripts_immersive_ar: INeedleXRSessionEventReceiver[];
|
85
82
|
coroutines: { [FrameEvent: number]: Array<CoroutineData> };
|
86
83
|
|
87
84
|
post_setup_callbacks: Function[];
|
@@ -93,13 +90,10 @@
|
|
93
90
|
new_script_start: IComponent[];
|
94
91
|
new_scripts_pre_setup_callbacks: Function[];
|
95
92
|
new_scripts_post_setup_callbacks: Function[];
|
96
|
-
new_scripts_xr: INeedleXRSessionEventReceiver[];
|
97
93
|
|
98
94
|
stopAllCoroutinesFrom(script: IComponent);
|
99
95
|
}
|
100
96
|
|
101
|
-
export type INeedleXRSession = NeedleXRSession;
|
102
|
-
|
103
97
|
export declare interface INeedleEngineComponent extends HTMLElement {
|
104
98
|
getAROverlayContainer(): HTMLElement;
|
105
99
|
onEnterAR(session: XRSession, overlayContainer: HTMLElement);
|
@@ -513,20 +507,3 @@
|
|
513
507
|
/** Enable to visualize raycasts in the scene with gizmos */
|
514
508
|
debugRenderRaycasts: boolean;
|
515
509
|
}
|
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,8 +1,7 @@
|
|
1
|
-
import { Quaternion, Vector2, Vector3, Vector4 } from "three";
|
2
|
-
|
3
|
-
import { isDevEnvironment, LogType, showBalloonMessage } from "./debug/index.js";
|
4
1
|
import { $isAssigningProperties } from "./engine_serialization_core.js";
|
2
|
+
import { LogType, isDevEnvironment, showBalloonMessage } from "./debug/index.js";
|
5
3
|
import { type Constructor, type IComponent } from "./engine_types.js";
|
4
|
+
import { Quaternion, Vector2, Vector3, Vector4 } from "three";
|
6
5
|
import { watchWrite } from "./engine_utils.js";
|
7
6
|
|
8
7
|
|
@@ -1,7 +1,6 @@
|
|
1
|
-
import { Camera,PerspectiveCamera } from "three";
|
2
|
-
|
3
1
|
import { ContextRegistry } from "./engine_context_registry.js";
|
4
2
|
import { Context } from "./engine_setup.js";
|
3
|
+
import { PerspectiveCamera, Camera } from "three";
|
5
4
|
|
6
5
|
declare type ImageMimeType = "image/webp" | "image/png";
|
7
6
|
|
@@ -1,8 +1,5 @@
|
|
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";
|
6
3
|
import { type SourceIdentifier } from "./engine_types.js";
|
7
4
|
|
8
5
|
// https://schneidenbach.gitbooks.io/typescript-cookbook/content/nameof-operator.html
|
@@ -11,8 +8,6 @@
|
|
11
8
|
return nameofFactory<T>()(name);
|
12
9
|
}
|
13
10
|
|
14
|
-
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
|
15
|
-
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
|
16
11
|
|
17
12
|
export function isDebugMode(): boolean {
|
18
13
|
return getParam("debug") ? true : false;
|
@@ -212,37 +207,12 @@
|
|
212
207
|
return obj;
|
213
208
|
}
|
214
209
|
|
215
|
-
/** @returns a promise that resolves after a certain amount of milliseconds
|
216
|
-
* e.g. `await delay(1000)` will wait for 1 second
|
217
|
-
*/
|
218
210
|
export function delay(milliseconds: number): Promise<void> {
|
219
|
-
return new Promise((
|
220
|
-
setTimeout(
|
211
|
+
return new Promise((res, _) => {
|
212
|
+
setTimeout(res, milliseconds);
|
221
213
|
});
|
222
214
|
}
|
223
215
|
|
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
|
-
|
246
216
|
// 1) if a timeline is exported via menu item the audio clip path is relative to the glb (same folder)
|
247
217
|
// we need to detect that here and build the new audio source path relative to the new glb location
|
248
218
|
// the same is/might be true for any file that is/will be exported via menu item
|
@@ -546,6 +516,10 @@
|
|
546
516
|
return json;
|
547
517
|
}
|
548
518
|
|
519
|
+
|
520
|
+
|
521
|
+
|
522
|
+
|
549
523
|
declare type AttributeChangeCallback = (value: string | null) => void;
|
550
524
|
declare type HtmlElementExtra = {
|
551
525
|
observer: MutationObserver,
|
@@ -637,42 +611,4 @@
|
|
637
611
|
anyFailed: anyFailed,
|
638
612
|
results: res,
|
639
613
|
};
|
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;
|
678
614
|
}
|
@@ -1,2 +0,0 @@
|
|
1
|
-
|
2
|
-
export * from "./xr/index.js"
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import "./engine_hot_reload.js";
|
2
|
+
|
3
|
+
import * as engine_setup from "./engine_setup.js";
|
4
|
+
import * as engine_scenetools from "./engine_scenetools.js";
|
2
5
|
import "./tests/test_utils.js";
|
3
|
-
|
4
6
|
import { RGBAColor } from "../engine-components/js-extensions/RGBAColor.js";
|
5
|
-
import * as engine_scenetools from "./engine_scenetools.js";
|
6
|
-
import * as engine_setup from "./engine_setup.js";
|
7
7
|
|
8
8
|
const engine : any = {
|
9
9
|
...engine_setup,
|
@@ -1,18 +1,19 @@
|
|
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";
|
6
1
|
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'
|
7
6
|
import { Context } from "../../engine/engine_setup.js";
|
8
|
-
import {
|
7
|
+
import { type IPointerEventHandler, PointerEventData, hasPointerEventComponent } from "./PointerEvents.js";
|
8
|
+
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";
|
9
12
|
import { getParam } from "../../engine/engine_utils.js";
|
10
|
-
import {
|
13
|
+
import { UIRaycastUtils } from "./RaycastUtils.js";
|
11
14
|
import { $shadowDomOwner } from "./BaseUIComponent.js";
|
12
|
-
import
|
13
|
-
import {
|
14
|
-
import { ObjectRaycaster, Raycaster } from "./Raycaster.js";
|
15
|
-
import { UIRaycastUtils } from "./RaycastUtils.js";
|
15
|
+
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
16
|
+
import { Mathf } from "../../engine/engine_math.js";
|
16
17
|
import { isUIObject } from "./Utils.js";
|
17
18
|
|
18
19
|
const debug = getParam("debugeventsystem");
|
@@ -28,8 +29,6 @@
|
|
28
29
|
hasActiveUI: boolean
|
29
30
|
}
|
30
31
|
|
31
|
-
declare type IComponentCanMaybeReceiveEvents = IPointerEventHandler & IComponent & { interactable?: boolean };
|
32
|
-
|
33
32
|
export class EventSystem extends Behaviour {
|
34
33
|
private static _eventSystemMap = new Map<Context, EventSystem[]>();
|
35
34
|
|
@@ -94,9 +93,10 @@
|
|
94
93
|
const res = GameObject.findObjectOfType(Raycaster, this.context);
|
95
94
|
if (!res) {
|
96
95
|
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. Skinnedmeshes will be ignored for better performance");
|
100
100
|
}
|
101
101
|
}
|
102
102
|
}
|
@@ -112,16 +112,89 @@
|
|
112
112
|
}
|
113
113
|
}
|
114
114
|
|
115
|
+
private _selectStartFn?: any;
|
116
|
+
private _selectEndFn?: any;
|
117
|
+
private _selectUpdateFn?: any;
|
118
|
+
private _handleEventCycleFn?: any;
|
115
119
|
private _handleInputFn?: any;
|
116
120
|
|
117
121
|
onEnable(): void {
|
118
|
-
|
122
|
+
const grabbed: Map<any, Object3D | null> = new Map();
|
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
|
+
|
119
188
|
this.context.input.addEventListener(InputEvents.PointerDown, this._handleInputFn);
|
120
189
|
this.context.input.addEventListener(InputEvents.PointerUp, this._handleInputFn);
|
121
190
|
this.context.input.addEventListener(InputEvents.PointerMove, this._handleInputFn);
|
122
191
|
}
|
123
192
|
|
124
193
|
onDisable(): void {
|
194
|
+
WebXRController.removeEventListener(ControllerEvents.SelectStart, this._selectStartFn);
|
195
|
+
WebXRController.removeEventListener(ControllerEvents.SelectEnd, this._selectEndFn);
|
196
|
+
WebXRController.removeEventListener(ControllerEvents.Update, this._selectUpdateFn);
|
197
|
+
|
125
198
|
this.context.input.removeEventListener(InputEvents.PointerDown, this._handleInputFn);
|
126
199
|
this.context.input.removeEventListener(InputEvents.PointerUp, this._handleInputFn);
|
127
200
|
this.context.input.removeEventListener(InputEvents.PointerMove, this._handleInputFn);
|
@@ -151,39 +224,30 @@
|
|
151
224
|
*/
|
152
225
|
private onPointerEvent(pointerEvent: NEPointerEvent) {
|
153
226
|
if (pointerEvent === undefined) return;
|
154
|
-
if (pointerEvent.propagationStopped) return;
|
155
227
|
|
156
|
-
//
|
228
|
+
// On mouse input has to be always 0 regardless of the button user pressed
|
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;
|
157
231
|
const data = new PointerEventData(this.context.input, pointerEvent);
|
158
|
-
this._currentPointerEventName = pointerEvent.type;
|
159
232
|
|
160
233
|
data.inputSource = this.context.input;
|
161
|
-
data.
|
162
|
-
data.
|
234
|
+
data.pointerId = pointerEvent.button;
|
235
|
+
data.isClicked = pointerEvent.type == InputEvents.PointerUp && this.context.input.getPointerClicked(pointerEvent.button)
|
163
236
|
// using the input type directly instead of input API -> otherwise onMove events can sometimes be getPointerUp() == true
|
164
237
|
data.isDown = pointerEvent.type == InputEvents.PointerDown;
|
165
238
|
data.isUp = pointerEvent.type == InputEvents.PointerUp;
|
166
|
-
data.isPressed = this.context.input.getPointerPressed(pointerEvent.
|
239
|
+
data.isPressed = this.context.input.getPointerPressed(pointerEvent.button);
|
167
240
|
|
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
|
-
}
|
241
|
+
if (debug && data.isClicked) console.log("CLICK", data.pointerId);
|
173
242
|
|
174
243
|
// raycast
|
175
244
|
const options = new RaycastOptions();
|
176
|
-
|
177
|
-
options.ray = pointerEvent.ray;
|
178
|
-
}
|
179
|
-
else {
|
180
|
-
options.screenPoint = this.context.input.getPointerPositionRC(pointerEvent.pointerId)!;
|
181
|
-
}
|
245
|
+
options.screenPoint = this.context.input.getPointerPositionRC(id)!;
|
182
246
|
|
183
|
-
|
184
247
|
const hits = this.performRaycast(options);
|
248
|
+
if (!hits) return;
|
185
249
|
|
186
|
-
if (debug && data.
|
250
|
+
if (debug && data.isClicked) {
|
187
251
|
showBalloonMessage("EventSystem: " + data.pointerId + " - " + this.context.time.frame + " - Up:" + data.isUp + ", Down:" + data.isDown)
|
188
252
|
}
|
189
253
|
|
@@ -193,12 +257,12 @@
|
|
193
257
|
hasActiveUI: this.currentActiveMeshUIComponents.length > 0,
|
194
258
|
}
|
195
259
|
|
196
|
-
this.dispatchEvent(new CustomEvent(EventSystemEvents.BeforeHandleInput, { detail: evt }))
|
260
|
+
this.dispatchEvent(new CustomEvent(EventSystemEvents.BeforeHandleInput, { detail: evt }))
|
197
261
|
|
198
|
-
//
|
199
|
-
this.handleIntersections(hits, data)
|
262
|
+
// handle hit objects
|
263
|
+
this.handleIntersections(id, hits, data)
|
200
264
|
|
201
|
-
this.dispatchEvent(new CustomEvent<AfterHandleInputEvent>(EventSystemEvents.AfterHandleInput, { detail: evt }))
|
265
|
+
this.dispatchEvent(new CustomEvent<AfterHandleInputEvent>(EventSystemEvents.AfterHandleInput, { detail: evt }))
|
202
266
|
}
|
203
267
|
|
204
268
|
private readonly _sortedHits: THREE.Intersection[] = [];
|
@@ -207,10 +271,6 @@
|
|
207
271
|
* cache for objects that we want to raycast against. It's cleared before each call to performRaycast invoking raycasters
|
208
272
|
*/
|
209
273
|
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
|
-
|
214
274
|
/**
|
215
275
|
* Checks if an object that we encounter has an event component and if it does, we add it to our objects cache
|
216
276
|
* If it doesnt we tell our raycasting system to ignore it and continue in the child hierarchy
|
@@ -223,72 +283,57 @@
|
|
223
283
|
* */
|
224
284
|
private shouldRaycastObject = (obj: Object3D): RaycastTestObjectReturnType => {
|
225
285
|
// check if this object is actually a UI shadow hierarchy object
|
226
|
-
let
|
286
|
+
let shadowComponent: Object3D | null = null;
|
227
287
|
const isUI = isUIObject(obj);
|
228
288
|
// if yes we want to grab the actual object that is the owner of the shadow dom
|
229
289
|
// and check that object for the event component
|
230
290
|
if (isUI) {
|
231
|
-
|
291
|
+
shadowComponent = obj[$shadowDomOwner]?.gameObject;
|
232
292
|
}
|
233
293
|
|
234
294
|
// check if the object was seen previously
|
235
|
-
if (this._testObjectsCache.has(obj) || (
|
295
|
+
if (this._testObjectsCache.has(obj) || (shadowComponent && this._testObjectsCache.has(shadowComponent))) {
|
236
296
|
// if yes we check if it was previously stored as "YES WE NEED TO RAYCAST THIS"
|
237
297
|
const prev = this._testObjectsCache.get(obj)!;
|
238
298
|
if (prev === false) return "continue in children"
|
239
299
|
return true;
|
240
300
|
}
|
241
301
|
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
|
-
|
248
302
|
// the object was not yet seen so we test if it has an event component
|
249
|
-
let hasEventComponent = hasPointerEventComponent(obj
|
250
|
-
if (!hasEventComponent &&
|
303
|
+
let hasEventComponent = hasPointerEventComponent(obj);
|
304
|
+
if (!hasEventComponent && shadowComponent) hasEventComponent = hasPointerEventComponent(shadowComponent);
|
251
305
|
|
252
306
|
if (hasEventComponent) {
|
253
307
|
// it has an event component: we add it and all its children to the cache
|
254
308
|
// we don't need to do the same for the shadow component hierarchy
|
255
309
|
// because the next object that will be detecting that the shadow owner was already seen
|
256
310
|
this._testObjectsCache.set(obj, true);
|
257
|
-
|
311
|
+
obj.traverse((o) => {
|
312
|
+
this._testObjectsCache.set(o, true);
|
313
|
+
})
|
258
314
|
return true;
|
259
315
|
}
|
260
316
|
this._testObjectsCache.set(obj, false);
|
261
317
|
return "continue in children"
|
262
318
|
}
|
263
319
|
}
|
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;
|
269
320
|
|
270
|
-
this._testObjectsCache.set(obj, true);
|
271
|
-
for (const ch of obj.children) {
|
272
|
-
this.shouldRaycastObject_AddToYesCache(ch);
|
273
|
-
}
|
274
|
-
}
|
275
|
-
|
276
321
|
/** the raycast filter is always overriden */
|
277
322
|
private performRaycast(opts: RaycastOptions | null): THREE.Intersection[] | null {
|
278
323
|
if (!this.raycaster) return null;
|
279
|
-
|
280
|
-
this._testObjectsCache.clear();
|
324
|
+
|
281
325
|
this._sortedHits.length = 0;
|
282
326
|
|
283
327
|
if (!opts) opts = new RaycastOptions();
|
328
|
+
|
329
|
+
// we clear the cache of previously seen objects
|
330
|
+
this._testObjectsCache.clear();
|
284
331
|
opts.testObject = this.shouldRaycastObject;
|
285
332
|
|
286
333
|
for (const rc of this.raycaster) {
|
287
334
|
if (!rc.activeAndEnabled) continue;
|
288
335
|
|
289
|
-
this._currentlyActiveRaycaster = rc;
|
290
336
|
const res = rc.performRaycast(opts);
|
291
|
-
this._currentlyActiveRaycaster = null;
|
292
337
|
|
293
338
|
if (res && res.length > 0) {
|
294
339
|
// console.log(res.length, res.map(r => r.object.name));
|
@@ -301,55 +346,36 @@
|
|
301
346
|
return this._sortedHits;
|
302
347
|
}
|
303
348
|
|
304
|
-
private
|
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
|
-
|
349
|
+
private handleIntersections(id:number, hits: THREE.Intersection[], args: PointerEventData): boolean {
|
323
350
|
if (hits?.length) {
|
324
351
|
hits = this.sortCandidates(hits);
|
325
352
|
for (const hit of hits) {
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
353
|
+
const { object } = hit;
|
354
|
+
args.point = hit.point;
|
355
|
+
args.normal = hit.normal;
|
356
|
+
args.face = hit.face;
|
357
|
+
args.distance = hit.distance;
|
358
|
+
args.instanceId = hit.instanceId;
|
359
|
+
if (this.handleEventOnObject(object, args)) {
|
331
360
|
return true;
|
332
361
|
}
|
333
362
|
}
|
334
363
|
}
|
335
364
|
|
336
|
-
// first invoke captured pointers
|
337
|
-
this.assignHitInformation(args, hits?.[0]);
|
338
|
-
this.invokePointerCapture(args);
|
339
|
-
|
340
365
|
// pointer has not hit any object to handle
|
341
366
|
|
342
367
|
// thus is not hovering over anything
|
343
|
-
const hoveredData = this.hoveredByID.get(
|
368
|
+
const hoveredData = this.hoveredByID.get(id);
|
344
369
|
if (hoveredData) {
|
345
|
-
this.
|
370
|
+
this.triggerOnExit(hoveredData.obj, hoveredData.data);
|
346
371
|
}
|
347
|
-
this.hoveredByID.delete(
|
372
|
+
this.hoveredByID.delete(id);
|
348
373
|
|
349
374
|
// if it was up, it means it doesn't should notify things that it down on before
|
350
375
|
if (args.isUp) {
|
351
|
-
this.pressedByID.get(
|
352
|
-
|
376
|
+
const pressedData = this.pressedByID.get(id);
|
377
|
+
pressedData?.handlers.forEach(h => h.onPointerUp?.call(h, args));
|
378
|
+
this.pressedByID.delete(id);
|
353
379
|
}
|
354
380
|
|
355
381
|
return false;
|
@@ -390,29 +416,34 @@
|
|
390
416
|
private handleEventOnObject(object: THREE.Object3D, args: PointerEventData): boolean {
|
391
417
|
// ensures that invisible objects are ignored
|
392
418
|
if (!this.testIsVisible(object)) {
|
393
|
-
if (args.
|
419
|
+
if (args.isClicked && debug)
|
394
420
|
console.log("not allowed", object);
|
395
421
|
return false;
|
396
422
|
}
|
397
423
|
|
398
424
|
// Event without pointer can't be handled
|
399
425
|
if (args.pointerId === undefined) {
|
400
|
-
if
|
426
|
+
if(debug) console.warn("Event without pointer can't be handled", args);
|
401
427
|
return false;
|
402
428
|
}
|
403
429
|
|
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
|
+
|
404
434
|
// Correct the handled object to match the relevant object in shadow dom (?)
|
435
|
+
const originalObject = object;
|
405
436
|
args.object = object;
|
406
437
|
|
407
438
|
const parent = object.parent as any;
|
408
439
|
let isShadow = false;
|
409
|
-
const clicked = args.
|
440
|
+
const clicked = args.isClicked ?? false;
|
410
441
|
|
411
442
|
let canvasGroup: ICanvasGroup | null = null;
|
412
443
|
|
413
444
|
// handle potential shadow dom built from three mesh ui
|
414
445
|
if (parent && parent.isUI) {
|
415
|
-
const pressedOrClicked = (args.isPressed || args.
|
446
|
+
const pressedOrClicked = (args.isPressed || args.isClicked) ?? false;
|
416
447
|
if (parent[$shadowDomOwner]) {
|
417
448
|
const actualGo = parent[$shadowDomOwner].gameObject;
|
418
449
|
if (actualGo) {
|
@@ -441,12 +472,11 @@
|
|
441
472
|
// Handle OnPointerExit -> in case when we are about to hover something new
|
442
473
|
// 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)
|
443
474
|
const hovering = this.hoveredByID.get(args.pointerId);
|
444
|
-
const
|
445
|
-
const isNewlyHovering = prevHovering !== object;
|
475
|
+
const isNewlyHovering = hovering?.obj !== object;
|
446
476
|
|
447
477
|
// trigger onPointerExit
|
448
|
-
if (isNewlyHovering &&
|
449
|
-
this.
|
478
|
+
if (isNewlyHovering && hovering?.obj) {
|
479
|
+
this.triggerOnExit(hovering.obj, hovering.data);
|
450
480
|
}
|
451
481
|
|
452
482
|
// save hovered object
|
@@ -469,7 +499,7 @@
|
|
469
499
|
}
|
470
500
|
}
|
471
501
|
if (canvasGroup === null || canvasGroup.interactable) {
|
472
|
-
this.handleMainInteraction(object, args,
|
502
|
+
this.handleMainInteraction(object, args, isNewlyHovering);
|
473
503
|
}
|
474
504
|
|
475
505
|
return true;
|
@@ -478,17 +508,22 @@
|
|
478
508
|
/**
|
479
509
|
* Propagate up in hiearchy and call the callback for each component that is possibly a handler
|
480
510
|
*/
|
481
|
-
private propagate(object: Object3D
|
511
|
+
private propagate(object: THREE.Object3D, _args: PointerEventData, onComponent: (behaviour: Behaviour) => void) {
|
482
512
|
|
483
513
|
while (true) {
|
514
|
+
// Propagate up the hierarchy
|
484
515
|
|
485
|
-
if
|
516
|
+
if(_args.used) return;
|
486
517
|
|
487
518
|
GameObject.foreachComponent(object, comp => {
|
488
519
|
// TODO: implement Stop Immediate Propagation
|
520
|
+
|
489
521
|
onComponent(comp);
|
522
|
+
// return undefined to continue iterating
|
523
|
+
return undefined;
|
490
524
|
}, false);
|
491
525
|
|
526
|
+
if (!object.parent) break;
|
492
527
|
// walk up
|
493
528
|
object = object.parent;
|
494
529
|
}
|
@@ -498,59 +533,43 @@
|
|
498
533
|
/**
|
499
534
|
* Propagate up in hiearchy and call handlers based on the pointer event data
|
500
535
|
*/
|
501
|
-
private handleMainInteraction(object: Object3D, args: PointerEventData,
|
536
|
+
private handleMainInteraction(object: THREE.Object3D, args: PointerEventData, isNewlyHovering: boolean) {
|
537
|
+
if (args.pointerId === undefined) return;
|
502
538
|
const pressedEvent = this.pressedByID.get(args.pointerId);
|
503
|
-
const hoveredObjectChanged = prevHovering !== object;
|
504
539
|
|
505
|
-
|
506
|
-
|
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
|
-
}
|
540
|
+
this.propagate(object, args, (behaviour) => {
|
541
|
+
const comp = behaviour as any;
|
521
542
|
|
522
|
-
this.propagate(object, (behaviour) => {
|
523
|
-
const comp = behaviour as IComponentCanMaybeReceiveEvents;
|
524
|
-
|
525
543
|
if (comp.interactable === false) return;
|
526
|
-
if (!comp.activeAndEnabled || !comp.enabled) return;
|
527
544
|
|
528
545
|
if (comp.onPointerEnter) {
|
529
|
-
if (
|
530
|
-
|
546
|
+
if (isNewlyHovering) {
|
547
|
+
comp.onPointerEnter(args);
|
531
548
|
}
|
532
549
|
}
|
533
550
|
|
534
551
|
if (args.isDown) {
|
535
552
|
if (comp.onPointerDown) {
|
536
553
|
comp.onPointerDown(args);
|
554
|
+
|
537
555
|
// Set the handler that we called the down event on
|
538
556
|
// So we can call the up event on the same handler
|
539
557
|
// In a scenario where we Down on one object and Up on another
|
540
558
|
pressedEvent?.handlers.add(comp);
|
541
|
-
this.handlePointerCapture(args, comp);
|
542
559
|
}
|
543
560
|
}
|
544
561
|
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
562
|
+
const posLastFrame = this.context.input.getPointerPositionLastFrame(args.pointerId!)!;
|
563
|
+
const posThisFrame = this.context.input.getPointerPosition(args.pointerId!)!;
|
564
|
+
const isMoving = posLastFrame && !Mathf.approximately(posLastFrame, posThisFrame);
|
565
|
+
|
566
|
+
if (isMoving && comp.onPointerMove) {
|
567
|
+
comp.onPointerMove(args);
|
549
568
|
}
|
550
569
|
|
551
570
|
if (args.isUp) {
|
552
571
|
if (comp.onPointerUp) {
|
553
|
-
|
572
|
+
comp.onPointerUp(args);
|
554
573
|
|
555
574
|
// We don't want to call Up twice if we Down and Up on the same object
|
556
575
|
// But if we Down on one and Up on another we want to call Up on the first one as well
|
@@ -558,9 +577,16 @@
|
|
558
577
|
// The original component that received the down event SHOULD also receive the up event
|
559
578
|
pressedEvent?.handlers.delete(comp);
|
560
579
|
}
|
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
|
+
}
|
561
587
|
}
|
562
588
|
|
563
|
-
if (args.
|
589
|
+
if (args.isClicked) {
|
564
590
|
if (comp.onPointerClick) {
|
565
591
|
comp.onPointerClick(args);
|
566
592
|
}
|
@@ -571,153 +597,31 @@
|
|
571
597
|
// If user drags away from the object, then it doesn't get the UP event
|
572
598
|
if (args.isUp) {
|
573
599
|
pressedEvent?.handlers.forEach((handler) => {
|
574
|
-
|
600
|
+
if (handler.onPointerUp) {
|
601
|
+
handler.onPointerUp(args);
|
602
|
+
}
|
575
603
|
});
|
576
604
|
|
577
605
|
this.pressedByID.delete(args.pointerId);
|
578
606
|
}
|
579
607
|
}
|
580
608
|
|
581
|
-
/**
|
582
|
-
|
583
|
-
|
609
|
+
/**
|
610
|
+
* Propagate up in hiearchy and call OnExit regardless of the pointer event data
|
611
|
+
*/
|
612
|
+
private triggerOnExit(object: THREE.Object3D, args: PointerEventData) {
|
613
|
+
args.used = false;
|
614
|
+
|
615
|
+
this.propagate(object, args, (behaviour) => {
|
584
616
|
if (!behaviour.gameObject || behaviour.destroyed) return;
|
585
617
|
|
586
618
|
const inst: any = behaviour;
|
587
619
|
if (inst.onPointerExit) {
|
588
|
-
|
589
|
-
if (newObject && this.isChild(newObject, behaviour.gameObject)) {
|
590
|
-
return;
|
591
|
-
}
|
592
|
-
this.handlePointerExit(inst, args);
|
620
|
+
inst.onPointerExit(args);
|
593
621
|
}
|
594
622
|
});
|
595
623
|
}
|
596
624
|
|
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
|
-
|
721
625
|
private handleMeshUiObjectWithoutShadowDom(obj: any, pressed: boolean) {
|
722
626
|
if (!obj || !obj.isUI) return true;
|
723
627
|
const hit = this.handleMeshUIIntersection(obj, pressed);
|
@@ -725,7 +629,7 @@
|
|
725
629
|
return hit;
|
726
630
|
}
|
727
631
|
|
728
|
-
private currentActiveMeshUIComponents:
|
632
|
+
private currentActiveMeshUIComponents: ThreeMeshUI.Block[] = [];
|
729
633
|
|
730
634
|
private handleMeshUIIntersection(meshUiObject: THREE.Object3D, pressed: boolean): boolean {
|
731
635
|
const res = MeshUIHelper.updateState(meshUiObject, pressed);
|
@@ -793,8 +697,8 @@
|
|
793
697
|
threeMeshUI.update();
|
794
698
|
}
|
795
699
|
|
796
|
-
static updateState(intersect: THREE.Object3D, _selectState: boolean):
|
797
|
-
let foundBlock:
|
700
|
+
static updateState(intersect: THREE.Object3D, _selectState: boolean): ThreeMeshUI.Block | null {
|
701
|
+
let foundBlock: ThreeMeshUI.Block | null = null;
|
798
702
|
|
799
703
|
if (intersect) {
|
800
704
|
foundBlock = this.findBlockInParent(intersect);
|
@@ -821,7 +725,7 @@
|
|
821
725
|
this.needsUpdate = true;
|
822
726
|
}
|
823
727
|
|
824
|
-
static findBlockInParent(elem: any):
|
728
|
+
static findBlockInParent(elem: any): ThreeMeshUI.Block | null {
|
825
729
|
if (!elem) return null;
|
826
730
|
if (elem.isBlock) {
|
827
731
|
// @TODO : Replace states managements
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import { serializable } from "../engine/engine_serialization.js";
|
2
|
+
import { EventList } from "./EventList.js";
|
3
|
+
import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js"
|
2
4
|
import { Behaviour } from "./Component.js"
|
3
|
-
import { EventList } from "./EventList.js";
|
4
5
|
import { EventType } from "./EventType.js"
|
5
|
-
import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js"
|
6
6
|
|
7
7
|
class TriggerEvent {
|
8
8
|
@serializable()
|
@@ -1,10 +1,9 @@
|
|
1
|
+
import { getParam } from "../engine_utils.js";
|
2
|
+
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
|
1
3
|
import { Texture } from "three";
|
2
|
-
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
|
3
4
|
import { type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
5
|
|
5
|
-
import { getParam } from "../engine_utils.js";
|
6
6
|
|
7
|
-
|
8
7
|
const debug = getParam("debugexr");
|
9
8
|
|
10
9
|
export class EXT_texture_exr implements GLTFLoaderPlugin {
|
@@ -1,8 +1,7 @@
|
|
1
|
+
import { type IExtensionReferenceResolver } from "./extension_resolver.js";
|
1
2
|
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";
|
6
5
|
|
7
6
|
const debug = getParam("debugresolvedependencies");
|
8
7
|
|
@@ -1,7 +1,6 @@
|
|
1
|
+
import { USDObject, USDZExporterContext } from "./ThreeUSDZExporter.js";
|
1
2
|
import { Object3D } from "three";
|
2
3
|
|
3
|
-
import { USDObject, USDZExporterContext } from "./ThreeUSDZExporter.js";
|
4
|
-
|
5
4
|
export interface IUSDExporterExtension {
|
6
5
|
|
7
6
|
get extensionName(): string;
|
@@ -1,21 +1,20 @@
|
|
1
|
-
import {
|
1
|
+
import { NEEDLE_techniques_webgl } from "./NEEDLE_techniques_webgl.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";
|
3
|
+
import { NEEDLE_components } from "./NEEDLE_components.js";
|
10
4
|
import { EXT_texture_exr } from "./EXT_texture_exr.js";
|
11
|
-
import { NEEDLE_components } from "./NEEDLE_components.js";
|
12
5
|
import { NEEDLE_gameobject_data } from "./NEEDLE_gameobject_data.js";
|
6
|
+
import { NEEDLE_persistent_assets } from "./NEEDLE_persistent_assets.js";
|
7
|
+
import { NEEDLE_lightmaps } from "../extensions/NEEDLE_lightmaps.js";
|
8
|
+
import { type ConstructorConcrete, type SourceIdentifier } from "../engine_types.js";
|
9
|
+
import { Context } from "../engine_setup.js";
|
13
10
|
import { NEEDLE_lighting_settings } from "./NEEDLE_lighting_settings.js";
|
14
|
-
import {
|
11
|
+
import { NEEDLE_render_objects } from "./NEEDLE_render_objects.js";
|
15
12
|
import { NEEDLE_progressive } from "./NEEDLE_progressive.js";
|
16
|
-
import { NEEDLE_render_objects } from "./NEEDLE_render_objects.js";
|
17
|
-
import { NEEDLE_techniques_webgl } from "./NEEDLE_techniques_webgl.js";
|
18
13
|
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";
|
19
18
|
|
20
19
|
const debug = getParam("debugextensions");
|
21
20
|
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { Object3D } from "three";
|
2
|
-
|
3
2
|
import type { Constructor } from "../../engine/engine_types.js";
|
4
3
|
|
5
4
|
const handlers: Map<any, ApplyPrototypeExtension> = new Map();
|
@@ -1,7 +1,6 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
1
2
|
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";
|
5
4
|
|
6
5
|
export class FlyControls extends Behaviour {
|
7
6
|
private _controls: ThreeFlyControls | null = null;
|
@@ -1,7 +1,6 @@
|
|
1
|
+
import { Behaviour } from "./Component.js";
|
1
2
|
import { Color, Fog as Fog3 } from "three";
|
2
|
-
|
3
3
|
import { serializable } from "../engine/engine_serialization.js";
|
4
|
-
import { Behaviour } from "./Component.js";
|
5
4
|
|
6
5
|
|
7
6
|
export enum FogMode {
|
@@ -1,11 +1,10 @@
|
|
1
|
+
import { Behaviour } from "./Component.js";
|
1
2
|
import * as THREE from "three";
|
3
|
+
import * as Gizmos from "../engine/engine_gizmos.js";
|
4
|
+
import * as params from "../engine/engine_default_parameters.js";
|
5
|
+
import { FrameEvent } from "../engine/engine_setup.js";
|
2
6
|
import { BoxHelper, Color } from "three";
|
3
|
-
|
4
|
-
import * as params from "../engine/engine_default_parameters.js";
|
5
|
-
import * as Gizmos from "../engine/engine_gizmos.js";
|
6
7
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
7
|
-
import { FrameEvent } from "../engine/engine_setup.js";
|
8
|
-
import { Behaviour } from "./Component.js";
|
9
8
|
|
10
9
|
|
11
10
|
export class BoxGizmo extends Behaviour {
|
@@ -1,17 +1,17 @@
|
|
1
1
|
import { Object3D, Vector3 } from "three";
|
2
|
-
import { AnimationClip } from "three";
|
3
2
|
import { GLTFExporter, type GLTFExporterOptions } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
4
3
|
|
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";
|
5
7
|
import { SerializationContext } from "../../../engine/engine_serialization_core.js";
|
6
8
|
import { serializable } from "../../../engine/engine_serialization_decorator.js";
|
9
|
+
import { NEEDLE_components } from "../../../engine/extensions/NEEDLE_components.js";
|
7
10
|
import { getWorldPosition } from "../../../engine/engine_three_utils.js";
|
11
|
+
import { BoxHelperComponent } from "../../BoxHelperComponent.js";
|
12
|
+
import { AnimationClip } from "three";
|
8
13
|
import { getParam } from "../../../engine/engine_utils.js";
|
9
14
|
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,15 +1,14 @@
|
|
1
|
-
import {
|
1
|
+
import { type IGraphic, type IRectTransformChangedReceiver } from './Interfaces.js';
|
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';
|
7
3
|
import { RGBAColor } from "../js-extensions/RGBAColor.js"
|
8
4
|
import { BaseUIComponent } from "./BaseUIComponent.js";
|
9
|
-
import {
|
10
|
-
import {
|
5
|
+
import { serializable } from '../../engine/engine_serialization_decorator.js';
|
6
|
+
import { Color, LinearSRGBColorSpace, SRGBColorSpace, Texture } from 'three';
|
11
7
|
import { RectTransform } from './RectTransform.js';
|
12
8
|
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';
|
13
12
|
|
14
13
|
const _colorStateObject: { backgroundColor: Color, backgroundOpacity: number, borderColor: Color, borderOpacity: number } = {
|
15
14
|
backgroundColor: new Color(1, 1, 1),
|
@@ -138,7 +137,7 @@
|
|
138
137
|
onEnable(): void {
|
139
138
|
super.onEnable();
|
140
139
|
if (this.uiObject) {
|
141
|
-
this.rectTransform.shadowComponent?.add(this.uiObject
|
140
|
+
this.rectTransform.shadowComponent?.add(this.uiObject);
|
142
141
|
this.addShadowComponent(this.uiObject, this.rectTransform);
|
143
142
|
}
|
144
143
|
|
@@ -1,9 +1,8 @@
|
|
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";
|
1
4
|
import { Color, GridHelper as _GridHelper } from "three";
|
2
5
|
|
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
|
-
|
7
6
|
export class GridHelper extends Behaviour {
|
8
7
|
|
9
8
|
@serializable()
|
@@ -1,10 +1,9 @@
|
|
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";
|
1
5
|
import { Texture } from "three";
|
2
|
-
import { GroundedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundedSkybox.js';
|
3
6
|
|
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
|
-
|
8
7
|
const debug = getParam("debuggroundprojection");
|
9
8
|
|
10
9
|
export class GroundProjectedEnv extends Behaviour {
|
@@ -82,19 +81,14 @@
|
|
82
81
|
if (!this.env || this.context.scene.environment !== this._lastEnvironment) {
|
83
82
|
if (debug)
|
84
83
|
console.log("Create/Update Ground Projection", this.context.scene.environment.name);
|
85
|
-
this.env = new GroundProjection(this.context.scene.environment
|
86
|
-
this.env.position.y = this._height;
|
84
|
+
this.env = new GroundProjection(this.context.scene.environment);
|
87
85
|
}
|
88
86
|
this._lastEnvironment = this.context.scene.environment;
|
89
87
|
if (!this.env.parent)
|
90
88
|
this.gameObject.add(this.env);
|
91
|
-
|
92
|
-
/* TODO realtime adjustments aren't possible anymore with GroundedSkybox (mesh generation)
|
93
89
|
this.env.scale.setScalar(this._scale);
|
94
90
|
this.env.radius = this._radius;
|
95
91
|
this.env.height = this._height;
|
96
|
-
*/
|
97
|
-
|
98
92
|
// dont make the ground projection raycastable by default
|
99
93
|
if (this.env.isObject3D === true) {
|
100
94
|
this.env.layers.set(2);
|
@@ -1,6 +1,5 @@
|
|
1
|
+
import { serializable } from '../../engine/engine_serialization_decorator.js';
|
1
2
|
import { Color, Texture } from 'three';
|
2
|
-
|
3
|
-
import { serializable } from '../../engine/engine_serialization_decorator.js';
|
4
3
|
import { MaskableGraphic } from './Graphic.js';
|
5
4
|
|
6
5
|
|
@@ -1,3 +1,3 @@
|
|
1
|
-
export {
|
2
|
-
export {
|
3
|
-
export {
|
1
|
+
export { USDZExporter } from "./USDZExporter.js";
|
2
|
+
export { USDObject, imageToCanvas } from "./ThreeUSDZExporter.js";
|
3
|
+
export { type UsdzBehaviour } from "./extensions/behavior/Behaviour.js";
|
@@ -1,4 +1,4 @@
|
|
1
|
+
export * from "./VolumeParameter.js"
|
2
|
+
export * from "./PostProcessingHandler.js"
|
1
3
|
export * from "./PostProcessingEffect.js";
|
2
|
-
export * from "./PostProcessingHandler.js"
|
3
|
-
export * from "./VolumeParameter.js"
|
4
4
|
export * from "./VolumeProfile.js";
|
@@ -1,4 +1,4 @@
|
|
1
|
-
export { type ITimelineAnimationCallbacks as ITimelineAnimationOverride } from "./PlayableDirector.js"
|
2
1
|
export * from "./SignalAsset.js"
|
2
|
+
export * from "./TimelineTracks.js"
|
3
3
|
export * from "./TimelineModels.js"
|
4
|
-
export
|
4
|
+
export { type ITimelineAnimationCallbacks as ITimelineAnimationOverride } from "./PlayableDirector.js"
|
@@ -1,3 +1,4 @@
|
|
1
|
-
export
|
1
|
+
export * from "./WebXR.js";
|
2
|
+
export * from "./WebXRPlaneTracking.js";
|
2
3
|
export * from "./WebXRImageTracking.js";
|
3
|
-
export * from "./
|
4
|
+
export * from "./WebXRController.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"
|
4
3
|
export * from "./NEEDLE_progressive.js"
|
5
|
-
export { CustomShader } from "./NEEDLE_techniques_webgl.js"
|
4
|
+
export { CustomShader } from "./NEEDLE_techniques_webgl.js"
|
5
|
+
export { SceneLightSettings } from "./NEEDLE_lighting_settings.js"
|
@@ -1,5 +0,0 @@
|
|
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";
|
@@ -1,10 +1,10 @@
|
|
1
|
+
import { Behaviour, GameObject } from "../Component.js";
|
2
|
+
import { type IPointerEventHandler } from "./PointerEvents.js";
|
3
|
+
import { FrameEvent } from "../../engine/engine_setup.js";
|
1
4
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
2
|
-
import {
|
5
|
+
import { Text } from "./Text.js";
|
3
6
|
import { getParam, isiOS } from "../../engine/engine_utils.js";
|
4
|
-
import { Behaviour, GameObject } from "../Component.js";
|
5
7
|
import { EventList } from "../EventList.js";
|
6
|
-
import { type IPointerEventHandler } from "./PointerEvents.js";
|
7
|
-
import { Text } from "./Text.js";
|
8
8
|
import { tryGetUIComponent } from "./Utils.js";
|
9
9
|
|
10
10
|
const debug = getParam("debuginputfield");
|
@@ -1,11 +1,19 @@
|
|
1
1
|
import { Behaviour } from "./Component.js";
|
2
|
+
import type { IPointerClickHandler, PointerEventData } from "./ui/PointerEvents.js";
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
|
5
|
+
export class Interactable extends Behaviour implements IPointerClickHandler {
|
6
|
+
|
7
|
+
canGrab : boolean = true;
|
8
|
+
|
9
|
+
onPointerClick(_args: PointerEventData) {
|
10
|
+
}
|
11
|
+
}
|
12
|
+
|
13
|
+
|
14
|
+
// TODO: how do we sync things like that...
|
7
15
|
export class UsageMarker extends Behaviour
|
8
16
|
{
|
9
|
-
public isUsed: boolean = true;
|
10
|
-
public usedBy: any = null;
|
17
|
+
public isUsed : boolean = true;
|
18
|
+
public usedBy : any = null;
|
11
19
|
}
|
@@ -1,35 +0,0 @@
|
|
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
|
-
}
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { Vector3 } from "three";
|
2
|
-
|
3
2
|
import { serializable } from "../engine/engine_serialization.js";
|
4
3
|
import { Behaviour } from "./Component.js";
|
5
4
|
import { Rigidbody } from "./RigidBody.js";
|
@@ -1,9 +1,9 @@
|
|
1
|
+
import { type ILayoutGroup, type IRectTransform } from "./Interfaces.js";
|
2
|
+
import { Behaviour, GameObject } from "../Component.js";
|
1
3
|
import { serializable } from "../../engine/engine_serialization.js";
|
2
|
-
import { getParam } from "../../engine/engine_utils.js";
|
3
|
-
import { Behaviour, GameObject } from "../Component.js";
|
4
4
|
import { Canvas } from "./Canvas.js";
|
5
|
-
import { type ILayoutGroup, type IRectTransform } from "./Interfaces.js";
|
6
5
|
import { RectTransform } from "./RectTransform.js";
|
6
|
+
import { getParam } from "../../engine/engine_utils.js";
|
7
7
|
|
8
8
|
const debug = getParam("debuguilayout");
|
9
9
|
|
@@ -1,14 +1,13 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
1
2
|
import * as THREE from "three";
|
3
|
+
import { getParam, isMobileDevice } from "../engine/engine_utils.js";
|
4
|
+
import { setWorldPositionXYZ } from "../engine/engine_three_utils.js";
|
5
|
+
import { FrameEvent } from "../engine/engine_setup.js";
|
6
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
2
7
|
import { Color, DirectionalLight, OrthographicCamera } from "three";
|
3
|
-
|
4
|
-
import {
|
5
|
-
import { FrameEvent } from "../engine/engine_setup.js";
|
6
|
-
import { setWorldPositionXYZ } from "../engine/engine_three_utils.js";
|
8
|
+
import { WebXR, WebXREvent } from "./webxr/WebXR.js";
|
9
|
+
import { WebARSessionRoot } from "./webxr/WebARSessionRoot.js";
|
7
10
|
import type { ILight } from "../engine/engine_types.js";
|
8
|
-
import { getParam, isMobileDevice } from "../engine/engine_utils.js";
|
9
|
-
import { NeedleXREventArgs } from "../engine/xr/index.js";
|
10
|
-
import { Behaviour, GameObject } from "./Component.js";
|
11
|
-
import { WebARSessionRoot } from "./webxr/WebARSessionRoot.js";
|
12
11
|
|
13
12
|
// https://threejs.org/examples/webgl_shadowmap_csm.html
|
14
13
|
|
@@ -271,6 +270,8 @@
|
|
271
270
|
}
|
272
271
|
if (this.type === LightType.Directional)
|
273
272
|
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));
|
274
275
|
}
|
275
276
|
|
276
277
|
onDisable() {
|
@@ -281,13 +282,15 @@
|
|
281
282
|
else
|
282
283
|
this.light.visible = false;
|
283
284
|
}
|
285
|
+
WebXR.removeEventListener(WebXREvent.XRStarted, this._webXRStartedListener);
|
286
|
+
WebXR.removeEventListener(WebXREvent.XRStopped, this._webXREndedListener);
|
284
287
|
}
|
285
288
|
|
286
289
|
private _webXRStartedListener?: Function;
|
287
290
|
private _webXREndedListener?: Function;
|
288
291
|
private _webARRoot?: WebARSessionRoot;
|
289
292
|
|
290
|
-
|
293
|
+
private onWebXRStarted() {
|
291
294
|
this._webARRoot = GameObject.getComponentInParent(this.gameObject, WebARSessionRoot) ?? undefined;
|
292
295
|
// this.startCoroutine(this._updateLightIntensityInARRoutine());
|
293
296
|
}
|
@@ -300,7 +303,7 @@
|
|
300
303
|
// }
|
301
304
|
// }
|
302
305
|
|
303
|
-
|
306
|
+
private onWebXREnded() {
|
304
307
|
// this.updateIntensity();
|
305
308
|
}
|
306
309
|
|
@@ -1,11 +1,10 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
1
2
|
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";
|
2
6
|
import { Vector3 } from "three";
|
3
7
|
|
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
|
-
|
9
8
|
const debug = getParam("debuglods");
|
10
9
|
const noLods = getParam("nolods");
|
11
10
|
|
@@ -1,6 +1,6 @@
|
|
1
|
+
import { Behaviour } from "../../engine-components/Component.js";
|
1
2
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
2
3
|
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 { serializable } from "../../engine/engine_serialization.js";
|
2
|
+
import { Behaviour } from "../Component.js";
|
1
3
|
import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
|
4
|
+
import { getWorldPosition, getWorldQuaternion, lookAtObject, setWorldQuaternion } from "../../engine/engine_three_utils.js";
|
2
5
|
|
3
|
-
import {
|
4
|
-
import { getWorldPosition, getWorldQuaternion, lookAtObject, setWorldQuaternion } from "../../engine/engine_three_utils.js";
|
6
|
+
import { USDObject } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
5
7
|
import { type UsdzBehaviour } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
|
6
8
|
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,9 +1,8 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
1
2
|
import * as THREE from "three";
|
3
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
2
4
|
import { Object3D } from "three";
|
3
5
|
|
4
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
|
-
import { Behaviour, GameObject } from "./Component.js";
|
6
|
-
|
7
6
|
export class LookAtConstraint extends Behaviour {
|
8
7
|
|
9
8
|
constraintActive: boolean = true;
|
@@ -1,8 +1,7 @@
|
|
1
|
+
import { Animator } from "../../engine-components/Animator.js";
|
1
2
|
import { AnimationAction, AnimationClip, MathUtils, Object3D } from "three"
|
2
|
-
|
3
|
+
import { Context } from "../engine_setup.js";
|
3
4
|
import { InstantiateIdProvider } from "../../engine/engine_networking_instantiate.js";
|
4
|
-
import { Animator } from "../../engine-components/Animator.js";
|
5
|
-
import { Context } from "../engine_setup.js";
|
6
5
|
|
7
6
|
|
8
7
|
export declare type AnimatorControllerModel = {
|
@@ -1,13 +1,12 @@
|
|
1
|
-
import {
|
1
|
+
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
|
+
import { type NodeToObjectMap, type ObjectToNodeMap, SerializationContext } from "../engine_serialization_core.js";
|
2
3
|
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
3
|
-
import {
|
4
|
-
|
4
|
+
import { debugExtension } from "../engine_default_parameters.js";
|
5
|
+
import { builtinComponentKeyName } from "../engine_constants.js";
|
6
|
+
import { resolveReferences } from "./extension_utils.js";
|
5
7
|
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
8
|
import { getLoader } from "../engine_gltf.js";
|
9
|
-
import {
|
10
|
-
import { resolveReferences } from "./extension_utils.js";
|
9
|
+
import { Object3D } from "three";
|
11
10
|
|
12
11
|
export const debug = debugExtension
|
13
12
|
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<void> | 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(() => { });
|
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,15 +1,14 @@
|
|
1
1
|
import { AmbientLight, Color, HemisphereLight, Object3D } from "three";
|
2
|
-
import { LightProbe } from "three";
|
3
2
|
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
|
-
|
3
|
+
import { type SourceIdentifier } from "../engine_types.js";
|
5
4
|
import { Behaviour, GameObject } from "../../engine-components/Component.js";
|
5
|
+
import { AmbientMode, DefaultReflectionMode } from "../engine_scenelighting.js";
|
6
|
+
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";
|
6
10
|
import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
|
7
11
|
import { Mathf } from "../engine_math.js";
|
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";
|
12
|
-
import { LightmapType } from "./NEEDLE_lightmaps.js";
|
13
12
|
|
14
13
|
export const EXTENSION_NAME = "NEEDLE_lighting_settings";
|
15
14
|
const debug = getParam("debugenvlight");
|
@@ -1,13 +1,12 @@
|
|
1
|
+
import { type ILightDataRegistry } from "../engine_lightdata.js";
|
1
2
|
import { LinearSRGBColorSpace, SRGBColorSpace, Texture, TextureLoader } from "three";
|
3
|
+
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
|
+
import { type SourceIdentifier } from "../engine_types.js";
|
5
|
+
import { resolveReferences } from "./extension_utils.js";
|
6
|
+
import { getParam, PromiseAllWithErrors, resolveUrl } from "../engine_utils.js";
|
2
7
|
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
|
3
|
-
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
8
|
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
|
5
|
-
|
6
9
|
import { isDevEnvironment } from "../debug/debug.js";
|
7
|
-
import { type ILightDataRegistry } from "../engine_lightdata.js";
|
8
|
-
import { type SourceIdentifier } from "../engine_types.js";
|
9
|
-
import { getParam, PromiseAllWithErrors, resolveUrl } from "../engine_utils.js";
|
10
|
-
import { resolveReferences } from "./extension_utils.js";
|
11
10
|
|
12
11
|
// the lightmap extension is aimed to also export export skyboxes and custom reflection maps
|
13
12
|
// should we rename it?
|
@@ -61,7 +60,7 @@
|
|
61
60
|
if (debug)
|
62
61
|
console.log(ext);
|
63
62
|
|
64
|
-
return new Promise(async (
|
63
|
+
return new Promise(async (res, _rej) => {
|
65
64
|
|
66
65
|
const dependencies: Array<Promise<any>> = [];
|
67
66
|
for (const entry of arr) {
|
@@ -98,7 +97,7 @@
|
|
98
97
|
if (isDevEnvironment())
|
99
98
|
console.error("Failed to load lightmap extension", results);
|
100
99
|
}
|
101
|
-
|
100
|
+
res();
|
102
101
|
});
|
103
102
|
}
|
104
103
|
}
|
@@ -1,9 +1,8 @@
|
|
1
|
+
import { resolveReferences } from "./extension_utils.js";
|
1
2
|
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
|
-
|
3
|
+
import { type IExtensionReferenceResolver } from "./extension_resolver.js";
|
3
4
|
import { debugExtension } from "../engine_default_parameters.js";
|
4
5
|
import { TypeStore } from "../engine_typestore.js";
|
5
|
-
import { type IExtensionReferenceResolver } from "./extension_resolver.js";
|
6
|
-
import { resolveReferences } from "./extension_utils.js";
|
7
6
|
|
8
7
|
export const EXTENSION_NAME = "NEEDLE_persistent_assets";
|
9
8
|
|
@@ -1,10 +1,9 @@
|
|
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
|
-
|
3
|
+
import { type SourceIdentifier } from "../engine_types.js";
|
4
|
+
import { Context } from "../engine_setup.js";
|
4
5
|
import { addDracoAndKTX2Loaders } from "../engine_loaders.js";
|
5
|
-
import {
|
6
|
-
import { type SourceIdentifier } from "../engine_types.js";
|
7
|
-
import { delay, getParam, PromiseAllWithErrors, PromiseErrorResult, resolveUrl } from "../engine_utils.js";
|
6
|
+
import { PromiseAllWithErrors, PromiseErrorResult, delay, getParam, resolveUrl } from "../engine_utils.js";
|
8
7
|
|
9
8
|
export const EXTENSION_NAME = "NEEDLE_progressive";
|
10
9
|
|
@@ -133,7 +132,6 @@
|
|
133
132
|
if (t.source)
|
134
133
|
t.source[$progressiveTextureExtension] = ext;
|
135
134
|
NEEDLE_progressive.cache.set(t.uuid, ext);
|
136
|
-
return t;
|
137
135
|
});
|
138
136
|
}
|
139
137
|
}
|
@@ -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
|
+
|
2
6
|
import {
|
3
|
-
|
4
|
-
|
5
|
-
|
7
|
+
// stencil funcs
|
8
|
+
NeverStencilFunc,
|
9
|
+
LessStencilFunc,
|
6
10
|
EqualStencilFunc,
|
11
|
+
LessEqualStencilFunc,
|
12
|
+
GreaterStencilFunc,
|
13
|
+
NotEqualStencilFunc,
|
7
14
|
GreaterEqualStencilFunc,
|
8
|
-
|
15
|
+
AlwaysStencilFunc,
|
16
|
+
// stencil ops
|
17
|
+
ZeroStencilOp,
|
18
|
+
KeepStencilOp,
|
19
|
+
ReplaceStencilOp,
|
9
20
|
IncrementStencilOp,
|
21
|
+
DecrementStencilOp,
|
10
22
|
IncrementWrapStencilOp,
|
23
|
+
DecrementWrapStencilOp,
|
11
24
|
InvertStencilOp,
|
12
|
-
KeepStencilOp,
|
13
|
-
LessEqualStencilFunc,
|
14
|
-
LessStencilFunc,
|
15
|
-
// stencil funcs
|
16
|
-
NeverStencilFunc,
|
17
|
-
NotEqualStencilFunc,
|
18
|
-
ReplaceStencilOp,
|
19
25
|
type StencilFunc,
|
20
26
|
type StencilOp as ThreeStencilOp,
|
21
|
-
// stencil ops
|
22
|
-
ZeroStencilOp,
|
23
27
|
} from "three";
|
24
|
-
import {
|
25
|
-
|
28
|
+
import { getParam } from "../engine_utils.js";
|
26
29
|
import { showBalloonWarning } from "../debug/index.js";
|
27
30
|
import { isUsingInstancing } from "../engine_gameobject.js";
|
28
31
|
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,13 +1,12 @@
|
|
1
|
+
import { type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
|
+
import { FindShaderTechniques, whiteDefaultTexture, ToUnityMatrixArray, SetUnitySphericalHarmonics } from '../engine_shaders.js';
|
1
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';
|
2
|
-
import { type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
|
-
|
4
4
|
import { Context } from '../engine_setup.js';
|
5
|
-
import {
|
6
|
-
import
|
5
|
+
import { getParam } from "../engine_utils.js";
|
6
|
+
import * as SHADERDATA from "../shaders/shaderData.js"
|
7
7
|
import { type SourceIdentifier } from "../engine_types.js";
|
8
8
|
import { type ILight } from "../engine_types.js";
|
9
|
-
import {
|
10
|
-
import * as SHADERDATA from "../shaders/shaderData.js"
|
9
|
+
import { getWorldPosition } from "../engine_three_utils.js";
|
11
10
|
|
12
11
|
const debug = getParam("debugcustomshader");
|
13
12
|
|
@@ -89,9 +88,7 @@
|
|
89
88
|
if (debug)
|
90
89
|
console.log(this);
|
91
90
|
|
92
|
-
//@ts-ignore - TODO: how to override and do we even need this?
|
93
91
|
this.type = "NEEDLE_CUSTOM_SHADER";
|
94
|
-
|
95
92
|
if (!this.uniforms[this._objToWorldName])
|
96
93
|
this.uniforms[this._objToWorldName] = { value: [] };
|
97
94
|
if (!this.uniforms[this._worldToObjectName])
|
@@ -1,3 +1,6 @@
|
|
1
|
+
import { makeErrorsVisibleForDevelopment } from "./engine/debug/debug_overlay.js";
|
2
|
+
makeErrorsVisibleForDevelopment();
|
3
|
+
|
1
4
|
import "./engine/engine_element.js";
|
2
5
|
import "./engine/engine_setup.js";
|
3
6
|
export * from "./engine/api.js";
|
@@ -1,785 +0,0 @@
|
|
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
|
-
}
|
@@ -1,1290 +0,0 @@
|
|
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
|
-
}
|
@@ -1,221 +0,0 @@
|
|
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
|
-
}
|
@@ -1,9 +1,9 @@
|
|
1
|
+
import { getParam } from "../engine/engine_utils.js";
|
2
|
+
import { Behaviour } from "../engine-components/Component.js";
|
1
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";
|
2
6
|
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";
|
5
|
-
import { getParam } from "../engine/engine_utils.js";
|
6
|
-
import { Behaviour } from "../engine-components/Component.js";
|
7
7
|
|
8
8
|
const debug = getParam("debugnestedgltf");
|
9
9
|
|
@@ -1,6 +1,6 @@
|
|
1
|
+
import { serializable } from "../engine/engine_serialization.js";
|
1
2
|
import type { INetworkingWebsocketUrlProvider } from "../engine/engine_networking.js";
|
2
3
|
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,23 +1,24 @@
|
|
1
|
+
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
|
1
2
|
import { Object3D, Quaternion, Vector3 } from "three";
|
2
|
-
import {
|
3
|
-
|
4
|
-
import {
|
5
|
-
import { destroy,isActiveSelf, setActive } from "../../engine/engine_gameobject.js";
|
3
|
+
import type { Constructor, ConstructorConcrete, IComponent, IComponent as Component } from "../../engine/engine_types.js";
|
4
|
+
import { moveComponentInstance, addNewComponent, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, removeComponent } from "../../engine/engine_components.js";
|
5
|
+
import { isActiveSelf, setActive, destroy } from "../../engine/engine_gameobject.js";
|
6
6
|
import {
|
7
|
-
|
7
|
+
setWorldPosition,
|
8
8
|
getWorldPosition,
|
9
|
+
setWorldQuaternion,
|
9
10
|
getWorldQuaternion,
|
10
|
-
getWorldRotation,
|
11
11
|
getWorldScale,
|
12
|
-
|
13
|
-
setWorldQuaternion,
|
12
|
+
setWorldScale,
|
14
13
|
setWorldRotation,
|
15
|
-
|
14
|
+
getWorldRotation,
|
15
|
+
getTempVector
|
16
|
+
}
|
16
17
|
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";
|
19
18
|
|
19
|
+
import { TransformControlsGizmo } from "three/examples/jsm/controls/TransformControls.js";
|
20
20
|
|
21
|
+
|
21
22
|
// used to decorate cloned object3D objects with the same added components defined above
|
22
23
|
export function apply(object: Object3D) {
|
23
24
|
if (object && object.isObject3D === true) {
|
@@ -1,8 +1,7 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
2
|
+
import * as utils from "./../engine/engine_three_utils.js";
|
3
|
+
import { Quaternion, Euler, Vector3, Plane } from "three";
|
3
4
|
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";
|
6
5
|
|
7
6
|
export class OffsetConstraint extends Behaviour {
|
8
7
|
|
@@ -1,9 +1,9 @@
|
|
1
1
|
|
2
|
+
import { type IPointerClickHandler, PointerEventData } from "../ui/index.js";
|
3
|
+
import { Behaviour } from "../Component.js";
|
4
|
+
import { serializable } from "../../engine/engine_serialization.js";
|
2
5
|
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
3
|
-
import {
|
4
|
-
import { isiOS,isSafari } from "../../engine/engine_utils.js";
|
5
|
-
import { Behaviour } from "../Component.js";
|
6
|
-
import { type IPointerClickHandler, PointerEventData } from "../ui/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,6 +34,7 @@
|
|
34
34
|
|
35
35
|
if (isDevEnvironment()) showBalloonMessage("Open URL: " + this.url)
|
36
36
|
|
37
|
+
|
37
38
|
switch (this.mode) {
|
38
39
|
case OpenURLMode.NewTab:
|
39
40
|
if (isSafari()) {
|
@@ -43,12 +44,10 @@
|
|
43
44
|
globalThis.open(this.url, "_blank");
|
44
45
|
break;
|
45
46
|
case OpenURLMode.SameTab:
|
46
|
-
|
47
|
-
if (isSafari() && isiOS()) {
|
47
|
+
if (isSafari()) {
|
48
48
|
globalThis.open(this.url, "_top");
|
49
49
|
}
|
50
|
-
else
|
51
|
-
globalThis.open(this.url, "_self");
|
50
|
+
else globalThis.open(this.url, "_self");
|
52
51
|
break;
|
53
52
|
case OpenURLMode.NewWindow:
|
54
53
|
if (isSafari()) {
|
@@ -59,10 +58,19 @@
|
|
59
58
|
|
60
59
|
}
|
61
60
|
}
|
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
|
+
|
66
74
|
onPointerEnter(args) {
|
67
75
|
if (!args.used && this.clickable)
|
68
76
|
this.context.input.setCursorPointer();
|
@@ -75,6 +83,30 @@
|
|
75
83
|
if (this.clickable && !args.used && this.url?.length)
|
76
84
|
this.open();
|
77
85
|
}
|
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
|
+
|
78
110
|
private _validateUrl() {
|
79
111
|
if (!this.url) return;
|
80
112
|
if (this.url.startsWith("www.")) {
|
@@ -1,21 +1,21 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
3
|
-
import {
|
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";
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
2
|
+
import { Camera } from "./Camera.js";
|
3
|
+
import { LookAtConstraint } from "./LookAtConstraint.js";
|
4
|
+
import { getWorldDirection, getWorldPosition, getWorldRotation, setWorldRotation } from "../engine/engine_three_utils.js";
|
8
5
|
import { RaycastOptions } from "../engine/engine_physics.js";
|
9
6
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
10
|
-
import {
|
7
|
+
import { getParam, isMobileDevice } from "../engine/engine_utils.js";
|
8
|
+
|
9
|
+
import { Box3, Object3D, PerspectiveCamera, Vector2, Vector3, Box3Helper, GridHelper, Mesh, ShadowMaterial, Ray } from "three";
|
10
|
+
import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
11
|
+
import { type AfterHandleInputEvent, EventSystem, EventSystemEvents } from "./ui/EventSystem.js";
|
11
12
|
import type { ICameraController } from "../engine/engine_types.js";
|
12
|
-
import {
|
13
|
-
import { Camera } from "./Camera.js";
|
14
|
-
import { Behaviour, GameObject } from "./Component.js";
|
15
|
-
import { LookAtConstraint } from "./LookAtConstraint.js";
|
13
|
+
import { setCameraController, useForAutoFit } from "../engine/engine_camera.js";
|
16
14
|
import { SyncedTransform } from "./SyncedTransform.js";
|
17
|
-
import { type AfterHandleInputEvent, EventSystem, EventSystemEvents } from "./ui/EventSystem.js";
|
18
15
|
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
|
+
//@ts-ignore
|
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 GroundProjectedSkybox) 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,8 +1,7 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { RGBAColor } from "../js-extensions/index.js";
|
3
2
|
import { serializable } from "../../engine/engine_serialization.js";
|
4
3
|
import { Behaviour } from "../Component.js";
|
5
|
-
import {
|
4
|
+
import { Color, Vector2 } from "three"
|
6
5
|
|
7
6
|
export class Outline extends Behaviour {
|
8
7
|
|
@@ -1,31 +1,32 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
1
2
|
import * as THREE from "three";
|
2
|
-
import {
|
3
|
-
import
|
4
|
-
import { BatchedParticleRenderer, ConstantColor, ConstantValue, ParticleSystem as _ParticleSystem, RenderMode, TrailBatch, TrailParticle } from "three.quarks";
|
3
|
+
import { MainModule, EmissionModule, ShapeModule, ParticleSystemShapeType, MinMaxCurve, MinMaxGradient, ColorOverLifetimeModule, SizeOverLifetimeModule, NoiseModule, ParticleSystemSimulationSpace, ParticleBurst, type IParticleSystem, ParticleSystemRenderMode, TrailModule, VelocityOverLifetimeModule, TextureSheetAnimationModule, RotationOverLifetimeModule, LimitVelocityOverLifetimeModule, RotationBySpeedModule, InheritVelocityModule, SizeBySpeedModule, ColorBySpeedModule, ParticleSystemScalingMode } from "./ParticleSystemModules.js"
|
4
|
+
import { getParam } from "../engine/engine_utils.js";
|
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";
|
9
6
|
// https://github.dev/creativelifeform/three-nebula
|
10
7
|
// import System, { Emitter, Position, Life, SpriteRenderer, Particle, Body, MeshRenderer, } from 'three-nebula.js';
|
8
|
+
|
11
9
|
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";
|
12
13
|
import { assign } from "../engine/engine_serialization_core.js";
|
14
|
+
import { ParticleSystem as _ParticleSystem, ConstantValue, ConstantColor, BatchedParticleRenderer, TrailBatch, TrailParticle, RenderMode } from "three.quarks";
|
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";
|
16
|
+
import { createFlatTexture } from "../engine/engine_shaders.js";
|
17
|
+
import { Mathf } from "../engine/engine_math.js";
|
13
18
|
import { Context } from "../engine/engine_setup.js";
|
14
|
-
import {
|
15
|
-
import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldScale } from "../engine/engine_three_utils.js";
|
16
|
-
import { getParam } from "../engine/engine_utils.js";
|
19
|
+
import { ParticleSubEmitter } from "./ParticleSystemSubEmitter.js";
|
17
20
|
import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
|
18
|
-
import {
|
19
|
-
import {
|
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"
|
21
|
-
import { ParticleSubEmitter } from "./ParticleSystemSubEmitter.js";
|
21
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
22
|
+
import { isDevEnvironment, showBalloonWarning } from "../engine/debug/index.js";
|
22
23
|
|
23
24
|
const debug = getParam("debugparticles");
|
24
25
|
const suppressProgressiveLoading = getParam("noprogressive");
|
25
26
|
const debugProgressiveLoading = getParam("debugprogressive");
|
26
27
|
|
27
28
|
|
28
|
-
export type {
|
29
|
+
export type { Behavior as QParticleBehaviour, Particle as QParticle } from "three.quarks"
|
29
30
|
|
30
31
|
|
31
32
|
|
@@ -80,22 +81,23 @@
|
|
80
81
|
return res;
|
81
82
|
}
|
82
83
|
|
84
|
+
private static _havePatchedQuarkShaders = false;
|
85
|
+
|
83
86
|
getMaterial(trailEnabled: boolean = false) {
|
84
|
-
let material = (trailEnabled === true && this.trailMaterial) ? this.trailMaterial : this.particleMaterial;
|
85
87
|
|
86
|
-
if (
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
else this.particleMaterial = material;
|
95
|
-
}
|
88
|
+
if (!ParticleSystemRenderer._havePatchedQuarkShaders) {
|
89
|
+
ParticleSystemRenderer._havePatchedQuarkShaders = true;
|
90
|
+
|
91
|
+
// HACK patch three.quarks fo three152+, see https://github.com/Alchemist0823/three.quarks/issues/56#issuecomment-1560825038
|
92
|
+
const _rebuild = TrailBatch.prototype.rebuildMaterial;
|
93
|
+
TrailBatch.prototype.rebuildMaterial = function () {
|
94
|
+
_rebuild.call(this);
|
95
|
+
this.material.defines.MAP_UV = "uv";
|
96
96
|
}
|
97
97
|
}
|
98
98
|
|
99
|
+
const material = (trailEnabled === true && this.trailMaterial) ? this.trailMaterial : this.particleMaterial;
|
100
|
+
|
99
101
|
// progressive load on start
|
100
102
|
// TODO: figure out how to do this before particle system rendering so we only load textures for visible materials
|
101
103
|
if (material && !suppressProgressiveLoading && material["_didRequestTextureLOD"] === undefined) {
|
@@ -396,7 +398,7 @@
|
|
396
398
|
let size = particle.size;
|
397
399
|
if (size <= 0 && !this.system.trails.sizeAffectsWidth) {
|
398
400
|
// Not sure where we get to 100* from, tested in SOC trong com
|
399
|
-
size =
|
401
|
+
size = 100 * this.system.trails.widthOverTrail.evaluate(.5, trailParticle[$trailWidthRandom]);
|
400
402
|
}
|
401
403
|
state.size = this.system.trails.getWidth(size, age01, pos01, trailParticle[$trailWidthRandom]);
|
402
404
|
state.color.copy(particle.color);
|
@@ -428,8 +430,9 @@
|
|
428
430
|
initialize(particle: Particle): void {
|
429
431
|
const simulationSpeed = this.system.main.simulationSpeed;
|
430
432
|
|
431
|
-
|
432
|
-
particle.
|
433
|
+
const factor = 1;
|
434
|
+
particle.startSpeed = this.system.main.startSpeed.evaluate(Math.random(), Math.random()) * factor;
|
435
|
+
particle.velocity.copy(this.system.shape.getDirection(particle.position)).multiplyScalar(particle.startSpeed);
|
433
436
|
if (this.system.inheritVelocity?.enabled) {
|
434
437
|
this.system.inheritVelocity.applyInitial(particle.velocity);
|
435
438
|
}
|
@@ -613,7 +616,8 @@
|
|
613
616
|
if (mat && mat["map"]) {
|
614
617
|
const original = mat["map"]! as THREE.Texture;
|
615
618
|
// cache the last original one so we're not creating tons of clones
|
616
|
-
if (this.clonedTexture.original !== original || !this.clonedTexture.clone)
|
619
|
+
if (this.clonedTexture.original !== original || !this.clonedTexture.clone)
|
620
|
+
{
|
617
621
|
const tex = original.clone();
|
618
622
|
tex.premultiplyAlpha = false;
|
619
623
|
tex.colorSpace = THREE.LinearSRGBColorSpace;
|
@@ -752,7 +756,7 @@
|
|
752
756
|
readonly limitVelocityOverLifetime!: LimitVelocityOverLifetimeModule;
|
753
757
|
|
754
758
|
@serializable(InheritVelocityModule)
|
755
|
-
inheritVelocity!: InheritVelocityModule;
|
759
|
+
readonly inheritVelocity!: InheritVelocityModule;
|
756
760
|
|
757
761
|
@serializable(ColorBySpeedModule)
|
758
762
|
readonly colorBySpeed!: ColorBySpeedModule;
|
@@ -931,8 +935,6 @@
|
|
931
935
|
}
|
932
936
|
|
933
937
|
awake(): void {
|
934
|
-
this._worldPositionFrame = -1;
|
935
|
-
|
936
938
|
this._renderer = this.gameObject.getComponent(ParticleSystemRenderer) as ParticleSystemRenderer;
|
937
939
|
|
938
940
|
if (!this.main) {
|
@@ -966,12 +968,6 @@
|
|
966
968
|
const emitter = this._particleSystem.emitter;
|
967
969
|
this.context.scene.add(emitter);
|
968
970
|
|
969
|
-
if (this.inheritVelocity.system && this.inheritVelocity.system !== this) {
|
970
|
-
this.inheritVelocity = this.inheritVelocity.clone();
|
971
|
-
}
|
972
|
-
this.inheritVelocity.awake(this);
|
973
|
-
|
974
|
-
|
975
971
|
if (debug) {
|
976
972
|
console.log(this);
|
977
973
|
this.gameObject.add(new AxesHelper(1))
|
@@ -1114,7 +1110,6 @@
|
|
1114
1110
|
this._interface.update();
|
1115
1111
|
this.shape.update(this, this.context, this.main.simulationSpace, this.gameObject);
|
1116
1112
|
this.noise.update(this.context);
|
1117
|
-
|
1118
1113
|
this.inheritVelocity?.update(this.context);
|
1119
1114
|
this.velocityOverLifetime.update(this);
|
1120
1115
|
}
|
@@ -1,18 +1,14 @@
|
|
1
|
-
import {
|
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";
|
1
|
+
import { Matrix4, Object3D, Quaternion, Vector3, Vector2, Euler, Vector4 } from "three";
|
7
2
|
import { Mathf } from "../engine/engine_math.js";
|
8
3
|
import { serializable } from "../engine/engine_serialization.js";
|
4
|
+
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
5
|
+
import { AnimationCurve } from "./AnimationCurve.js";
|
6
|
+
import type { Vec2, Vec3 } from "../engine/engine_types.js";
|
9
7
|
import { Context } from "../engine/engine_setup.js";
|
10
|
-
import {
|
11
|
-
import
|
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";
|
12
11
|
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';
|
16
12
|
|
17
13
|
const debug = getParam("debugparticles");
|
18
14
|
|
@@ -183,19 +179,6 @@
|
|
183
179
|
@serializable()
|
184
180
|
curveMultiplier?: number;
|
185
181
|
|
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
|
-
|
199
182
|
evaluate(t01: number, lerpFactor?: number): number {
|
200
183
|
const t = lerpFactor === undefined ? Math.random() : lerpFactor;
|
201
184
|
switch (this.mode) {
|
@@ -503,13 +486,6 @@
|
|
503
486
|
}
|
504
487
|
}
|
505
488
|
|
506
|
-
|
507
|
-
export enum ParticleSystemMeshShapeType {
|
508
|
-
Vertex = 0,
|
509
|
-
Edge = 1,
|
510
|
-
Triangle = 2,
|
511
|
-
}
|
512
|
-
|
513
489
|
export class ShapeModule implements EmitterShape {
|
514
490
|
|
515
491
|
// Emittershape start
|
@@ -517,7 +493,7 @@
|
|
517
493
|
return ParticleSystemShapeType[this.shapeType];
|
518
494
|
}
|
519
495
|
initialize(particle: Particle): void {
|
520
|
-
this.
|
496
|
+
this.getPosition();
|
521
497
|
particle.position.copy(this._vector);
|
522
498
|
}
|
523
499
|
toJSON(): ShapeJSON {
|
@@ -567,30 +543,6 @@
|
|
567
543
|
@serializable()
|
568
544
|
randomPositionAmount!: number;
|
569
545
|
|
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
|
-
|
594
546
|
private system!: IParticleSystem;
|
595
547
|
private _space?: ParticleSystemSimulationSpace;
|
596
548
|
private readonly _worldSpaceMatrix: Matrix4 = new Matrix4();
|
@@ -641,14 +593,12 @@
|
|
641
593
|
/** initializer implementation */
|
642
594
|
private _vector: Vector3 = new Vector3(0, 0, 0);
|
643
595
|
private _temp: Vector3 = new Vector3(0, 0, 0);
|
644
|
-
|
645
|
-
|
646
|
-
|
596
|
+
/** called by nebula on initialize */
|
597
|
+
get vector() {
|
598
|
+
return this._vector;
|
599
|
+
}
|
600
|
+
getPosition(): void {
|
647
601
|
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
|
-
|
652
602
|
const pos = this._temp.copy(this.position);
|
653
603
|
const isWorldSpace = this._space === ParticleSystemSimulationSpace.World;
|
654
604
|
if (isWorldSpace) {
|
@@ -674,64 +624,8 @@
|
|
674
624
|
case ParticleSystemShapeType.Circle:
|
675
625
|
this.randomCirclePoint(this.position, radius, this.radiusThickness, this.arc, this._vector);
|
676
626
|
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;
|
729
627
|
default:
|
730
628
|
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
|
-
}
|
735
629
|
break;
|
736
630
|
// case ParticleSystemShapeType.Hemisphere:
|
737
631
|
// randomSpherePoint(this.position.x, this.position.y, this.position.z, this.radius, this.radiusThickness, 180, this._vector);
|
@@ -757,7 +651,7 @@
|
|
757
651
|
|
758
652
|
private _dir: Vector3 = new Vector3();
|
759
653
|
|
760
|
-
getDirection(
|
654
|
+
getDirection(pos: Vec3): Vector3 {
|
761
655
|
if (!this.enabled) {
|
762
656
|
this._dir.set(0, 0, 1);
|
763
657
|
return this._dir;
|
@@ -782,47 +676,6 @@
|
|
782
676
|
else
|
783
677
|
this._dir.sub(this.position)
|
784
678
|
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;
|
826
679
|
default:
|
827
680
|
this._dir.set(0, 0, 1);
|
828
681
|
break;
|
@@ -888,7 +741,7 @@
|
|
888
741
|
vec.z = z;
|
889
742
|
}
|
890
743
|
|
891
|
-
private randomCirclePoint(pos:
|
744
|
+
private randomCirclePoint(pos:Vec3, radius:number, thickness:number, arg:number, vec:Vec3){
|
892
745
|
const u = Math.random();
|
893
746
|
const theta = 2 * Math.PI * u * (arg / 360);
|
894
747
|
const r = Mathf.lerp(1, 1 - (Math.pow(1 - Math.random(), Math.PI)), thickness) * (radius);
|
@@ -1119,7 +972,7 @@
|
|
1119
972
|
@serializable()
|
1120
973
|
worldSpace: boolean = false;
|
1121
974
|
|
1122
|
-
getWidth(size: number, _life01: number, pos01: number, t: number) {
|
975
|
+
getWidth(size: number, _life01: number, pos01: number, t : number) {
|
1123
976
|
const res = this.widthOverTrail.evaluate(pos01, t);
|
1124
977
|
size *= res;
|
1125
978
|
return size;
|
@@ -1531,54 +1384,22 @@
|
|
1531
1384
|
@serializable()
|
1532
1385
|
mode!: ParticleSystemInheritVelocityMode;
|
1533
1386
|
|
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
|
-
|
1543
1387
|
system!: IParticleSystem;
|
1388
|
+
private _lastWorldPosition!: Vector3;
|
1389
|
+
private _velocity: Vector3 = new Vector3();
|
1390
|
+
private _temp: Vector3 = new Vector3();
|
1544
1391
|
|
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
|
-
|
1570
1392
|
update(_context: Context) {
|
1571
1393
|
if (!this.enabled) return;
|
1572
1394
|
if (this.system.worldspace === false) return;
|
1573
|
-
if (this.
|
1574
|
-
this._firstUpdate = false;
|
1575
|
-
this._velocity.set(0, 0, 0);
|
1576
|
-
this._lastWorldPosition.copy(this.system.worldPos);
|
1577
|
-
}
|
1578
|
-
else if (this._lastWorldPosition) {
|
1395
|
+
if (this._lastWorldPosition) {
|
1579
1396
|
this._velocity.copy(this.system.worldPos).sub(this._lastWorldPosition).multiplyScalar(1 / this.system.deltaTime);
|
1580
1397
|
this._lastWorldPosition.copy(this.system.worldPos);
|
1581
1398
|
}
|
1399
|
+
else {
|
1400
|
+
this._velocity.set(0, 0, 0);
|
1401
|
+
this._lastWorldPosition = this.system.worldPos.clone();
|
1402
|
+
}
|
1582
1403
|
}
|
1583
1404
|
|
1584
1405
|
// TODO: make work for subsystems
|
@@ -1592,10 +1413,8 @@
|
|
1592
1413
|
}
|
1593
1414
|
}
|
1594
1415
|
|
1595
|
-
private _frames = 0;
|
1596
1416
|
applyCurrent(vel: Vector3, t01: number, lerpFactor: number) {
|
1597
1417
|
if (!this.enabled) return;
|
1598
|
-
if (!this.system) return;
|
1599
1418
|
if (this.system.worldspace === false) return;
|
1600
1419
|
if (this.mode === ParticleSystemInheritVelocityMode.Current) {
|
1601
1420
|
const factor = this.curve.evaluate(t01, lerpFactor);
|
@@ -1,9 +1,8 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
3
|
-
|
1
|
+
import { type Behavior, type Particle, type EmissionState, type ParticleSystem } from "three.quarks";
|
2
|
+
import { Vector3, Quaternion, Matrix4 } from "three";
|
3
|
+
import type { IParticleSystem } from "./ParticleSystemModules.js";
|
4
4
|
import { CircularBuffer } from "../engine/engine_utils.js";
|
5
5
|
import { $particleLife, SubEmitterType } from "./ParticleSystem.js";
|
6
|
-
import type { IParticleSystem } from "./ParticleSystemModules.js";
|
7
6
|
|
8
7
|
const VECTOR_ONE = new Vector3(1, 1, 1);
|
9
8
|
const VECTOR_Z = new Vector3(0, 0, 1);
|
@@ -1,9 +1,8 @@
|
|
1
|
+
import { registerCustomEffectType } from "../VolumeProfile.js";
|
2
|
+
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
1
3
|
import { PixelationEffect as PixelationEffectPP } from "postprocessing";
|
2
|
-
|
4
|
+
import { VolumeParameter } from "../VolumeParameter.js";
|
3
5
|
import { serializable } from "../../../engine/engine_serialization.js";
|
4
|
-
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
5
|
-
import { VolumeParameter } from "../VolumeParameter.js";
|
6
|
-
import { registerCustomEffectType } from "../VolumeProfile.js";
|
7
6
|
|
8
7
|
export class PixelationEffect extends PostProcessingEffect {
|
9
8
|
get typeName(): string {
|
@@ -1,17 +1,16 @@
|
|
1
|
+
import { Animator } from '../Animator.js';
|
2
|
+
import { Behaviour, GameObject } from '../Component.js';
|
1
3
|
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';
|
8
|
-
import { Animator } from '../Animator.js';
|
9
4
|
import { AudioListener } from '../AudioListener.js';
|
10
5
|
import { AudioSource } from '../AudioSource.js';
|
11
|
-
import { Behaviour, GameObject } from '../Component.js';
|
12
6
|
import { SignalReceiver } from './SignalAsset.js';
|
13
7
|
import * as Models from "./TimelineModels.js";
|
14
8
|
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';
|
15
14
|
|
16
15
|
const debug = getParam("debugtimeline");
|
17
16
|
|
@@ -165,9 +164,9 @@
|
|
165
164
|
if (!this.isValid()) return;
|
166
165
|
const pauseChanged = this._isPaused == true;
|
167
166
|
this._isPaused = false;
|
167
|
+
if (pauseChanged) this.invokePauseChangedMethodsOnTracks();
|
168
168
|
if (this._isPlaying) return;
|
169
169
|
this._isPlaying = true;
|
170
|
-
if (pauseChanged) this.invokePauseChangedMethodsOnTracks();
|
171
170
|
if (this.waitForAudio) {
|
172
171
|
// Make sure audio tracks have loaded at the current time
|
173
172
|
const promises: Array<Promise<any>> = [];
|
@@ -519,7 +518,7 @@
|
|
519
518
|
const clipModel = track.clips[i];
|
520
519
|
const animModel = clipModel.asset as Models.AnimationClipModel;
|
521
520
|
if (!animModel) {
|
522
|
-
console.error(
|
521
|
+
console.error("MISSING anim model?", "clip#" + i, clipModel, track, this.playableAsset, this.name);
|
523
522
|
continue;
|
524
523
|
}
|
525
524
|
// console.log(clipModel, track);
|
@@ -1,45 +1,40 @@
|
|
1
|
-
import * as THREE from "three";
|
2
|
-
|
3
|
-
import { WaitForSeconds } from "../engine/engine_coroutine.js";
|
4
1
|
import { RoomEvents } from "../engine/engine_networking.js";
|
5
|
-
import { PlayerState } from "../engine-components-experimental/networking/PlayerSync.js";
|
6
2
|
import { Behaviour, GameObject } from "./Component.js";
|
3
|
+
import * as THREE from "three";
|
7
4
|
import { AvatarMarker } from "./webxr/WebXRAvatar.js";
|
5
|
+
import { WaitForSeconds } from "../engine/engine_coroutine.js";
|
8
6
|
|
9
7
|
|
10
8
|
export class PlayerColor extends Behaviour {
|
11
9
|
|
10
|
+
awake(): void {
|
11
|
+
// console.log("AWAKE", this.name);
|
12
|
+
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.tryAssignColor.bind(this));
|
13
|
+
}
|
14
|
+
|
12
15
|
private _didAssignPlayerColor: boolean = false;
|
13
16
|
|
14
17
|
onEnable(): void {
|
15
|
-
|
18
|
+
// console.log("ENABLE", this.name);
|
16
19
|
if (!this._didAssignPlayerColor)
|
17
20
|
this.startCoroutine(this.waitForConnection());
|
18
21
|
}
|
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.enabled) {
|
25
25
|
yield WaitForSeconds(.2);
|
26
26
|
if (this.tryAssignColor()) break;
|
27
27
|
}
|
28
|
+
// console.log("STOP WAITING", this.name, this.destroyed);
|
28
29
|
}
|
29
30
|
|
30
|
-
private tryAssignColor
|
31
|
-
const marker = GameObject.getComponentInParent(this.gameObject,
|
32
|
-
if (marker && marker.
|
31
|
+
private tryAssignColor(): boolean {
|
32
|
+
const marker = GameObject.getComponentInParent(this.gameObject, AvatarMarker);
|
33
|
+
if (marker && marker.connectionId) {
|
33
34
|
this._didAssignPlayerColor = true;
|
34
|
-
this.assignUserColor(marker.
|
35
|
+
this.assignUserColor(marker.connectionId);
|
35
36
|
return true;
|
36
37
|
}
|
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
|
-
}
|
43
38
|
return false;
|
44
39
|
}
|
45
40
|
|
@@ -1,69 +1,39 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import { Behaviour, Component, GameObject } from "../../engine-components/Component.js";
|
3
2
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
3
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
4
|
+
import { syncField } from "../../engine/engine_networking_auto.js"
|
4
5
|
import { RoomEvents } from "../../engine/engine_networking.js";
|
5
|
-
import { syncField } from "../../engine/engine_networking_auto.js"
|
6
6
|
import { syncDestroy } from "../../engine/engine_networking_instantiate.js";
|
7
|
-
import {
|
8
|
-
|
9
|
-
import {
|
10
|
-
import { Behaviour, Component, GameObject } from "../../engine-components/Component.js";
|
7
|
+
import { getParam } from "../../engine/engine_utils.js";
|
8
|
+
|
9
|
+
import { Object3D } from "three";
|
11
10
|
import { EventList } from "../../engine-components/EventList.js";
|
12
11
|
|
13
12
|
|
14
13
|
const debug = getParam("debugplayersync");
|
15
14
|
|
16
15
|
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 */
|
23
16
|
@serializable(AssetReference)
|
24
17
|
asset?: AssetReference;
|
25
18
|
|
26
|
-
/** Event called when */
|
27
19
|
@serializable(EventList)
|
28
20
|
onPlayerSpawned?: EventList;
|
29
21
|
|
30
|
-
|
31
|
-
private _localInstance?: Promise<IGameObject>;
|
32
|
-
|
33
22
|
awake(): void {
|
34
23
|
this.watchTabVisible();
|
35
|
-
if (!this.onPlayerSpawned) this.onPlayerSpawned = new EventList();
|
36
24
|
}
|
37
25
|
|
38
26
|
onEnable(): void {
|
39
27
|
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
|
-
}
|
44
28
|
}
|
45
29
|
onDisable(): void {
|
46
|
-
this.context.connection.stopListen(RoomEvents.RoomStateSent, this.onJoinedRoom)
|
47
|
-
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
|
30
|
+
this.context.connection.stopListen(RoomEvents.RoomStateSent, this.onJoinedRoom)
|
48
31
|
}
|
49
32
|
|
50
|
-
private onJoinedRoom = () => {
|
51
|
-
if (debug) console.log("PlayerSync.
|
52
|
-
if (this.autoSync) this.getInstance();
|
53
|
-
}
|
33
|
+
private onJoinedRoom = async (_model) => {
|
34
|
+
if (debug) console.log("PlayerSync.onUserJoined", _model);
|
54
35
|
|
55
|
-
|
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;
|
36
|
+
const instance = await this.asset?.instantiateSynced({ parent: this.gameObject }, true);
|
67
37
|
if (instance) {
|
68
38
|
const pl = GameObject.getComponent(instance, PlayerState);
|
69
39
|
if (pl) {
|
@@ -71,29 +41,15 @@
|
|
71
41
|
this.onPlayerSpawned?.invoke(instance);
|
72
42
|
}
|
73
43
|
else {
|
74
|
-
this._localInstance = undefined;
|
75
44
|
console.error("<strong>Failed finding PlayerState on " + this.asset?.uri + "</strong>: please make sure the asset has a PlayerState component!");
|
76
45
|
GameObject.destroySynced(instance);
|
77
46
|
}
|
78
47
|
}
|
79
|
-
else
|
80
|
-
this._localInstance = undefined;
|
48
|
+
else{
|
81
49
|
console.warn("PlayerSync: failed instantiating asset!")
|
82
50
|
}
|
83
|
-
|
84
|
-
return this._localInstance;
|
85
51
|
}
|
86
52
|
|
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
|
-
|
97
53
|
private watchTabVisible() {
|
98
54
|
window.addEventListener("visibilitychange", _ => {
|
99
55
|
if (document.visibilityState === "visible") {
|
@@ -134,22 +90,19 @@
|
|
134
90
|
return PlayerState._local;
|
135
91
|
}
|
136
92
|
|
137
|
-
|
93
|
+
//** use to check if a component or gameobject is part of a instance owned by the local player */
|
94
|
+
static isLocalPlayer(obj: Object3D | Component): boolean {
|
138
95
|
if (obj instanceof Object3D) {
|
139
|
-
|
96
|
+
const state = GameObject.getComponentInParent(obj, PlayerState);
|
97
|
+
return state?.isLocalPlayer ?? false;
|
140
98
|
}
|
141
99
|
else if (obj instanceof Component) {
|
142
|
-
|
100
|
+
const state = GameObject.getComponentInParent(obj.gameObject, PlayerState);
|
101
|
+
return state?.isLocalPlayer ?? false;
|
143
102
|
}
|
144
|
-
return
|
103
|
+
return false;
|
145
104
|
}
|
146
105
|
|
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
|
-
|
153
106
|
// static Callback
|
154
107
|
private static _callbacks: { [key: string]: PlayerStateEventCallback[] } = {};
|
155
108
|
/**
|
@@ -180,9 +133,6 @@
|
|
180
133
|
@syncField(PlayerState.prototype.onOwnerChange)
|
181
134
|
owner?: string;
|
182
135
|
|
183
|
-
/** when enabled PlayerSync will not destroy itself when not connected anymore */
|
184
|
-
dontDestroy: boolean = false;
|
185
|
-
|
186
136
|
get isLocalPlayer(): boolean {
|
187
137
|
return this.owner === this.context.connection.connectionId;
|
188
138
|
}
|
@@ -202,13 +152,13 @@
|
|
202
152
|
}
|
203
153
|
|
204
154
|
// call local events
|
205
|
-
if
|
155
|
+
if(!this.hasOwner) {
|
206
156
|
this.hasOwner = true;
|
207
157
|
this.onFirstOwnerChangeEvent?.invoke(detail);
|
208
158
|
}
|
209
159
|
|
210
160
|
this.onOwnerChangeEvent?.invoke(detail);
|
211
|
-
|
161
|
+
|
212
162
|
// call remote events
|
213
163
|
if (this.owner === this.context.connection.connectionId) {
|
214
164
|
PlayerState._local.push(this);
|
@@ -238,63 +188,20 @@
|
|
238
188
|
}
|
239
189
|
|
240
190
|
|
241
|
-
|
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
|
-
|
191
|
+
start() {
|
256
192
|
// If a player is spawned but not in the room anymore we want to destroy it
|
257
193
|
// this might happen in a case where all users get disconnected at once and the server
|
258
194
|
// still has the syncInstantiate messages that are sent to all clients
|
259
|
-
if (this.owner) {
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
-
}
|
195
|
+
if (this.owner && !this.context.connection.userIsInRoom(this.owner)) {
|
196
|
+
if (debug) console.log("PlayerSync.start → doDestroy because user is not in room anymore...", this)
|
197
|
+
this.doDestroy();
|
198
|
+
return;
|
266
199
|
}
|
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
|
-
}
|
282
200
|
}
|
283
201
|
|
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
|
-
|
295
202
|
/** this tells the server that this client has been destroyed and the networking message for the instantiate will be removed */
|
296
203
|
doDestroy() {
|
297
|
-
if (debug) console.log("PlayerSync.doDestroy → syncDestroy", this
|
204
|
+
if (debug) console.log("PlayerSync.doDestroy → syncDestroy", this);
|
298
205
|
syncDestroy(this.gameObject, this.context.connection);
|
299
206
|
}
|
300
207
|
|
@@ -1,171 +1,102 @@
|
|
1
|
+
import { GameObject } from "../Component.js";
|
2
|
+
import { Input, NEPointerEvent } from "../../engine/engine_input.js";
|
1
3
|
import { Face, Object3D, Vector3 } from "three";
|
2
4
|
|
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
|
-
|
7
5
|
export interface IInputEventArgs {
|
8
6
|
get used(): boolean;
|
9
|
-
|
10
|
-
|
7
|
+
Use(): void;
|
8
|
+
StopPropagation?(): void;
|
11
9
|
}
|
12
10
|
|
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
|
-
*/
|
18
11
|
export class PointerEventData implements IInputEventArgs {
|
19
12
|
|
20
|
-
|
21
|
-
|
13
|
+
// TODO: should we make this a getter and return the input used state instead? -> this.context.getPointerUsed(this.pointerId);
|
14
|
+
used: boolean = false;
|
22
15
|
|
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 */
|
45
16
|
use() {
|
46
|
-
|
47
|
-
this._used = true;
|
17
|
+
this.used = true;
|
48
18
|
if (this.pointerId !== undefined)
|
49
19
|
this.input.setPointerUsed(this.pointerId);
|
50
20
|
}
|
51
21
|
|
52
|
-
|
53
|
-
|
54
|
-
return this._propagationStopped;
|
22
|
+
stopPropagation() {
|
23
|
+
this._event?.stopImmediatePropagation();
|
55
24
|
}
|
56
25
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
stopPropagation() {
|
61
|
-
// we currently don't have a distinction between stopPropagation and stopImmediatePropagation
|
62
|
-
this._propagationStopped = true;
|
63
|
-
this.event.stopImmediatePropagation();
|
26
|
+
/**@deprecated use use() */
|
27
|
+
Use() {
|
28
|
+
this.use();
|
64
29
|
}
|
65
|
-
/** Call this method to stop immediate propagation on the `event` object.
|
66
|
-
*/
|
67
|
-
stopImmediatePropagation() {
|
68
|
-
this._propagationStopped = true;
|
69
|
-
this.event.stopImmediatePropagation();
|
70
|
-
}
|
71
30
|
|
72
|
-
/**@
|
73
|
-
|
74
|
-
|
75
|
-
*/
|
76
|
-
setPointerCapture() {
|
77
|
-
this.z__pointer_ctured = true;
|
31
|
+
/**@deprecated use stopPropagation() */
|
32
|
+
StopPropagation() {
|
33
|
+
this._event?.stopImmediatePropagation();
|
78
34
|
}
|
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
|
-
}
|
85
35
|
|
86
|
-
|
87
36
|
/** Who initiated this event */
|
88
37
|
inputSource: Input | any;
|
89
38
|
|
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
|
-
|
93
39
|
/** The object this event hit or interacted with */
|
94
40
|
object!: THREE.Object3D;
|
95
41
|
/** The world position of this event */
|
96
42
|
point?: Vector3;
|
97
|
-
/** The
|
43
|
+
/** The world normal of this event */
|
98
44
|
normal?: Vector3;
|
99
|
-
/** */
|
100
45
|
face?: Face | null;
|
101
|
-
/** The distance of the hit point from the origin */
|
102
46
|
distance?: number;
|
103
|
-
/** The instance ID of an object hit by a raycast (if a instanced object was hit) */
|
104
47
|
instanceId?: number;
|
105
48
|
|
49
|
+
pointerId: number | undefined;
|
106
50
|
isDown: boolean | undefined;
|
107
51
|
isUp: boolean | undefined;
|
108
52
|
isPressed: boolean | undefined;
|
109
|
-
|
110
|
-
isDoubleClick: boolean | undefined;
|
53
|
+
isClicked: boolean | undefined;
|
111
54
|
|
55
|
+
/** mouse button 0 === LEFT, 1 === MIDDLE, 2 === RIGHT */
|
56
|
+
readonly button: number | string;
|
112
57
|
|
113
58
|
private input: Input;
|
114
59
|
|
115
|
-
|
116
|
-
|
60
|
+
private _event?: NEPointerEvent;
|
61
|
+
get event() { return this._event; }
|
62
|
+
|
63
|
+
constructor(input: Input, event?: NEPointerEvent) {
|
64
|
+
this._event = event;
|
117
65
|
this.input = input;
|
118
|
-
this.button = event
|
66
|
+
this.button = event?.button ?? 0;
|
119
67
|
}
|
120
68
|
|
121
69
|
clone() {
|
122
|
-
const clone = new PointerEventData(this.input, this.
|
70
|
+
const clone = new PointerEventData(this.input, this._event);
|
123
71
|
Object.assign(clone, this);
|
124
72
|
return clone;
|
125
73
|
}
|
126
|
-
|
127
|
-
/**@deprecated use use() */
|
128
|
-
Use() {
|
129
|
-
this.use();
|
130
|
-
}
|
131
|
-
|
132
|
-
/**@deprecated use stopPropagation() */
|
133
|
-
StopPropagation() {
|
134
|
-
this.event.stopImmediatePropagation();
|
135
|
-
}
|
136
74
|
}
|
137
75
|
|
138
76
|
export interface IPointerDownHandler {
|
139
|
-
/** Called when a button is started to being pressed on an object (or a child object) */
|
140
77
|
onPointerDown?(args: PointerEventData);
|
141
78
|
}
|
142
79
|
|
143
80
|
export interface IPointerUpHandler {
|
144
|
-
/** Called when a button is released (which was previously pressed in `onPointerDown`) */
|
145
81
|
onPointerUp?(args: PointerEventData);
|
146
82
|
}
|
147
83
|
|
148
84
|
export interface IPointerEnterHandler {
|
149
|
-
/** Called when a pointer (mouse, touch, xr controller) starts pointing on/hovering an object (or a child object) */
|
150
85
|
onPointerEnter?(args: PointerEventData);
|
151
86
|
}
|
152
87
|
|
153
88
|
export interface IPointerMoveHandler {
|
154
|
-
/** Called when a pointer (mouse, touch, xr controller) is moving over an object (or a child object) */
|
155
89
|
onPointerMove?(args: PointerEventData);
|
156
90
|
}
|
157
91
|
|
158
92
|
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) */
|
160
93
|
onPointerExit?(args: PointerEventData);
|
161
94
|
}
|
162
95
|
|
163
96
|
export interface IPointerClickHandler {
|
164
|
-
/** Called when an object (or any child object) is clicked (needs a EventSystem in the scene) */
|
165
97
|
onPointerClick?(args: PointerEventData);
|
166
98
|
}
|
167
99
|
|
168
|
-
/** Implement on your component to receive input events via the `EventSystem` component */
|
169
100
|
export interface IPointerEventHandler extends IPointerDownHandler,
|
170
101
|
IPointerUpHandler, IPointerEnterHandler, IPointerMoveHandler, IPointerExitHandler, IPointerClickHandler { }
|
171
102
|
|
@@ -175,30 +106,11 @@
|
|
175
106
|
* @internal tests if the object has any PointerEventComponent used by the EventSystem
|
176
107
|
* This is used to skip raycasting on objects that have no components that use pointer events
|
177
108
|
*/
|
178
|
-
export function hasPointerEventComponent(obj: Object3D
|
109
|
+
export function hasPointerEventComponent(obj: Object3D) {
|
179
110
|
const res = GameObject.foreachComponent(obj, comp => {
|
180
|
-
// ignore disabled components
|
181
|
-
if (!comp.enabled) return undefined;
|
182
|
-
|
183
111
|
const handler = comp as IPointerEventHandler;
|
184
|
-
|
185
|
-
|
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
|
-
}
|
112
|
+
if (handler.onPointerDown || handler.onPointerUp || handler.onPointerEnter || handler.onPointerExit || handler.onPointerClick)
|
113
|
+
return true;
|
202
114
|
// undefined means continue
|
203
115
|
return undefined;
|
204
116
|
}, false);
|
@@ -1,11 +1,10 @@
|
|
1
|
+
import { serializable } from "../../engine/engine_serialization.js";
|
1
2
|
import { Effect, Pass } from "postprocessing";
|
2
|
-
|
3
|
+
import { VolumeParameter } from "./VolumeParameter.js";
|
4
|
+
import { Component } from "../Component.js";
|
5
|
+
import type { ISerializable, SerializationContext } from "../../engine/engine_serialization_core.js";
|
3
6
|
import type { EditorModification, IEditorModification } from "../../engine/engine_editor-sync.js";
|
4
|
-
import { serializable } from "../../engine/engine_serialization.js";
|
5
|
-
import type { ISerializable, SerializationContext } from "../../engine/engine_serialization_core.js";
|
6
7
|
import { getParam } from "../../engine/engine_utils.js";
|
7
|
-
import { Component } from "../Component.js";
|
8
|
-
import { VolumeParameter } from "./VolumeParameter.js";
|
9
8
|
|
10
9
|
const debug = getParam("debugpost");
|
11
10
|
|
@@ -1,13 +1,12 @@
|
|
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";
|
3
1
|
import { HalfFloatType } from "three";
|
4
|
-
|
5
|
-
import { showBalloonWarning } from "../../engine/debug/index.js";
|
6
2
|
import { Context } from "../../engine/engine_setup.js";
|
7
|
-
import type { Constructor } from "../../engine/engine_types.js";
|
8
3
|
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";
|
9
6
|
import { Camera } from "../Camera.js";
|
10
7
|
import { PostProcessingEffect } from "./PostProcessingEffect.js";
|
8
|
+
import type { Constructor } from "../../engine/engine_types.js";
|
9
|
+
import { N8AOPostPass } from "n8ao";
|
11
10
|
|
12
11
|
const debug = getParam("debugpost");
|
13
12
|
|
@@ -1,5 +1,5 @@
|
|
1
|
+
import { Behaviour } from "../engine-components/Component.js";
|
1
2
|
import type { KeyCode } from "../engine/engine_input.js";
|
2
|
-
import { Behaviour } from "../engine-components/Component.js";
|
3
3
|
|
4
4
|
export class PresentationMode extends Behaviour {
|
5
5
|
|
@@ -1,17 +1,11 @@
|
|
1
|
-
import { SkinnedMesh } from "three";
|
2
|
-
|
3
|
-
import { IRaycastOptions, RaycastOptions } from "../../engine/engine_physics.js";
|
4
1
|
import { serializable } from "../../engine/engine_serialization.js";
|
5
|
-
import {
|
6
|
-
import { Behaviour } from "../Component.js";
|
2
|
+
import { RaycastOptions } from "../../engine/engine_physics.js";
|
3
|
+
import { Behaviour, Component } from "../Component.js";
|
7
4
|
import { EventSystem } from "./EventSystem.js";
|
5
|
+
import { SkinnedMesh } from "three";
|
8
6
|
|
9
7
|
|
10
|
-
|
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 {
|
8
|
+
export class Raycaster extends Behaviour {
|
15
9
|
awake(): void {
|
16
10
|
EventSystem.createIfNoneExists(this.context);
|
17
11
|
}
|
@@ -24,7 +18,9 @@
|
|
24
18
|
EventSystem.get(this.context)?.unregister(this);
|
25
19
|
}
|
26
20
|
|
27
|
-
|
21
|
+
performRaycast(_opts: RaycastOptions | null = null): THREE.Intersection[] | null {
|
22
|
+
return null;
|
23
|
+
}
|
28
24
|
}
|
29
25
|
|
30
26
|
|
@@ -39,7 +35,7 @@
|
|
39
35
|
this.targets = [this.gameObject];
|
40
36
|
}
|
41
37
|
|
42
|
-
performRaycast(opts:
|
38
|
+
performRaycast(opts: RaycastOptions | null = null): THREE.Intersection[] | null {
|
43
39
|
if (!this.targets) return null;
|
44
40
|
opts ??= new RaycastOptions();
|
45
41
|
opts.targets = this.targets;
|
@@ -74,19 +70,4 @@
|
|
74
70
|
}
|
75
71
|
}
|
76
72
|
|
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;
|
82
73
|
|
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,9 +1,8 @@
|
|
1
|
-
import { Object3D } from "three";
|
2
|
-
|
3
1
|
import { foreachComponent } from "../../engine/engine_gameobject.js";
|
4
2
|
import { type IComponent } from "../../engine/engine_types.js";
|
5
3
|
import { $shadowDomOwner } from "./BaseUIComponent.js";
|
6
4
|
import { type ICanvasGroup, type IGraphic } from "./Interfaces.js";
|
5
|
+
import { Object3D } from "three";
|
7
6
|
|
8
7
|
|
9
8
|
export class UIRaycastUtils {
|
@@ -1,14 +1,13 @@
|
|
1
|
-
import { Matrix4, Object3D, Quaternion, Vector2, Vector3 } from "three";
|
2
1
|
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";
|
6
4
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
5
|
+
import { Matrix4, Object3D, Quaternion, Vector2, Vector3 } from "three";
|
7
6
|
import { getParam } from "../../engine/engine_utils.js";
|
7
|
+
import { onChange } from "./Utils.js";
|
8
|
+
import { foreachComponentEnumerator } from "../../engine/engine_gameobject.js";
|
9
|
+
import { type ICanvas, type IRectTransform, type IRectTransformChangedReceiver } from "./Interfaces.js";
|
8
10
|
import { GameObject } from '../Component.js';
|
9
|
-
import { BaseUIComponent } from "./BaseUIComponent.js";
|
10
|
-
import { type ICanvas, type IRectTransform, type IRectTransformChangedReceiver } from "./Interfaces.js";
|
11
|
-
import { onChange } from "./Utils.js";
|
12
11
|
|
13
12
|
const debug = getParam("debugui");
|
14
13
|
const debugLayout = getParam("debuguilayout");
|
@@ -1,11 +1,10 @@
|
|
1
|
+
import { Behaviour } from "./Component.js";
|
1
2
|
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 { BoxHelperComponent } from "./BoxHelperComponent.js";
|
6
7
|
import { getParam } from "../engine/engine_utils.js";
|
7
|
-
import { BoxHelperComponent } from "./BoxHelperComponent.js";
|
8
|
-
import { Behaviour } from "./Component.js";
|
9
8
|
|
10
9
|
export const debug = getParam("debugreflectionprobe");
|
11
10
|
const disable = getParam("noreflectionprobe");
|
@@ -1,5 +1,4 @@
|
|
1
|
-

|
2
|
-
import { TypeStore } from "./../engine_typestore.js"
|
1
|
+
import { TypeStore } from "./../engine_typestore.js"
|
3
2
|
|
4
3
|
// Import types
|
5
4
|
import { __Ignore } from "../../engine-components/codegen/components.js";
|
@@ -14,11 +13,11 @@
|
|
14
13
|
import { Animator } from "../../engine-components/Animator.js";
|
15
14
|
import { AnimatorController } from "../../engine-components/AnimatorController.js";
|
16
15
|
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";
|
22
21
|
import { Avatar_Brain_LookAt } from "../../engine-components/avatar/Avatar_Brain_LookAt.js";
|
23
22
|
import { Avatar_MouthShapes } from "../../engine-components/avatar/Avatar_MouthShapes.js";
|
24
23
|
import { Avatar_MustacheShake } from "../../engine-components/avatar/Avatar_MustacheShake.js";
|
@@ -33,6 +32,7 @@
|
|
33
32
|
import { BasicIKConstraint } from "../../engine-components/BasicIKConstraint.js";
|
34
33
|
import { BehaviorExtension } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
|
35
34
|
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,6 +53,7 @@
|
|
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";
|
56
57
|
import { ContactShadows } from "../../engine-components/ContactShadows.js";
|
57
58
|
import { ControlTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
|
58
59
|
import { CustomBranding } from "../../engine-components/export/usdz/USDZExporter.js";
|
@@ -89,6 +90,7 @@
|
|
89
90
|
import { Image } from "../../engine-components/ui/Image.js";
|
90
91
|
import { InheritVelocityModule } from "../../engine-components/ParticleSystemModules.js";
|
91
92
|
import { InputField } from "../../engine-components/ui/InputField.js";
|
93
|
+
import { Interactable } from "../../engine-components/Interactable.js";
|
92
94
|
import { Light } from "../../engine-components/Light.js";
|
93
95
|
import { LimitVelocityOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
|
94
96
|
import { LODGroup } from "../../engine-components/LODGroup.js";
|
@@ -102,7 +104,6 @@
|
|
102
104
|
import { MeshRenderer } from "../../engine-components/Renderer.js";
|
103
105
|
import { MinMaxCurve } from "../../engine-components/ParticleSystemModules.js";
|
104
106
|
import { MinMaxGradient } from "../../engine-components/ParticleSystemModules.js";
|
105
|
-
import { NeedleWebXRHtmlElement } from "../../engine-components/webxr/WebXRButtons.js";
|
106
107
|
import { NestedGltf } from "../../engine-components/NestedGltf.js";
|
107
108
|
import { Networking } from "../../engine-components/Networking.js";
|
108
109
|
import { NoiseModule } from "../../engine-components/ParticleSystemModules.js";
|
@@ -129,6 +130,7 @@
|
|
129
130
|
import { PreliminaryTrigger } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents.js";
|
130
131
|
import { PresentationMode } from "../../engine-components-experimental/Presentation.js";
|
131
132
|
import { RawImage } from "../../engine-components/ui/Image.js";
|
133
|
+
import { Raycaster } from "../../engine-components/ui/Raycaster.js";
|
132
134
|
import { Rect } from "../../engine-components/ui/RectTransform.js";
|
133
135
|
import { RectTransform } from "../../engine-components/ui/RectTransform.js";
|
134
136
|
import { ReflectionProbe } from "../../engine-components/ReflectionProbe.js";
|
@@ -156,7 +158,6 @@
|
|
156
158
|
import { SizeOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
|
157
159
|
import { SkinnedMeshRenderer } from "../../engine-components/Renderer.js";
|
158
160
|
import { SmoothFollow } from "../../engine-components/SmoothFollow.js";
|
159
|
-
import { SpatialGrabRaycaster } from "../../engine-components/ui/Raycaster.js";
|
160
161
|
import { SpatialHtml } from "../../engine-components/ui/SpatialHtml.js";
|
161
162
|
import { SpatialTrigger } from "../../engine-components/SpatialTrigger.js";
|
162
163
|
import { SpatialTriggerReceiver } from "../../engine-components/SpatialTrigger.js";
|
@@ -171,7 +172,7 @@
|
|
171
172
|
import { SyncedRoom } from "../../engine-components/SyncedRoom.js";
|
172
173
|
import { SyncedTransform } from "../../engine-components/SyncedTransform.js";
|
173
174
|
import { TapGestureTrigger } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents.js";
|
174
|
-
import { TeleportTarget } from "../../engine-components/webxr/
|
175
|
+
import { TeleportTarget } from "../../engine-components/webxr/WebXRController.js";
|
175
176
|
import { TestRunner } from "../../engine-components/TestRunner.js";
|
176
177
|
import { TestSimulateUserData } from "../../engine-components/TestRunner.js";
|
177
178
|
import { Text } from "../../engine-components/ui/Text.js";
|
@@ -201,19 +202,23 @@
|
|
201
202
|
import { Volume } from "../../engine-components/postprocessing/Volume.js";
|
202
203
|
import { VolumeParameter } from "../../engine-components/postprocessing/VolumeParameter.js";
|
203
204
|
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";
|
204
207
|
import { WebARCameraBackground } from "../../engine-components/webxr/WebARCameraBackground.js";
|
205
208
|
import { WebARSessionRoot } from "../../engine-components/webxr/WebARSessionRoot.js";
|
206
209
|
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";
|
207
212
|
import { WebXRImageTracking } from "../../engine-components/webxr/WebXRImageTracking.js";
|
208
213
|
import { WebXRImageTrackingModel } from "../../engine-components/webxr/WebXRImageTracking.js";
|
209
214
|
import { WebXRPlaneTracking } from "../../engine-components/webxr/WebXRPlaneTracking.js";
|
215
|
+
import { WebXRSync } from "../../engine-components/webxr/WebXRSync.js";
|
210
216
|
import { WebXRTrackedImage } from "../../engine-components/webxr/WebXRImageTracking.js";
|
211
|
-
import {
|
212
|
-
import {
|
213
|
-
import {
|
214
|
-
import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
|
217
|
+
import { XRFlag } from "../../engine-components/XRFlag.js";
|
218
|
+
import { XRGrabModel } from "../../engine-components/webxr/WebXRGrabRendering.js";
|
219
|
+
import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering.js";
|
215
220
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
216
|
-
import { XRState } from "../../engine-components/
|
221
|
+
import { XRState } from "../../engine-components/XRFlag.js";
|
217
222
|
|
218
223
|
// Register types
|
219
224
|
TypeStore.add("__Ignore", __Ignore);
|
@@ -228,11 +233,11 @@
|
|
228
233
|
TypeStore.add("Animator", Animator);
|
229
234
|
TypeStore.add("AnimatorController", AnimatorController);
|
230
235
|
TypeStore.add("Antialiasing", Antialiasing);
|
236
|
+
TypeStore.add("AttachedObject", AttachedObject);
|
231
237
|
TypeStore.add("AudioExtension", AudioExtension);
|
232
238
|
TypeStore.add("AudioListener", AudioListener);
|
233
239
|
TypeStore.add("AudioSource", AudioSource);
|
234
240
|
TypeStore.add("AudioTrackHandler", AudioTrackHandler);
|
235
|
-
TypeStore.add("Avatar", Avatar);
|
236
241
|
TypeStore.add("Avatar_Brain_LookAt", Avatar_Brain_LookAt);
|
237
242
|
TypeStore.add("Avatar_MouthShapes", Avatar_MouthShapes);
|
238
243
|
TypeStore.add("Avatar_MustacheShake", Avatar_MustacheShake);
|
@@ -247,6 +252,7 @@
|
|
247
252
|
TypeStore.add("BasicIKConstraint", BasicIKConstraint);
|
248
253
|
TypeStore.add("BehaviorExtension", BehaviorExtension);
|
249
254
|
TypeStore.add("BehaviorModel", BehaviorModel);
|
255
|
+
TypeStore.add("Behaviour", Behaviour);
|
250
256
|
TypeStore.add("Bloom", Bloom);
|
251
257
|
TypeStore.add("BoxCollider", BoxCollider);
|
252
258
|
TypeStore.add("BoxGizmo", BoxGizmo);
|
@@ -267,6 +273,7 @@
|
|
267
273
|
TypeStore.add("ColorAdjustments", ColorAdjustments);
|
268
274
|
TypeStore.add("ColorBySpeedModule", ColorBySpeedModule);
|
269
275
|
TypeStore.add("ColorOverLifetimeModule", ColorOverLifetimeModule);
|
276
|
+
TypeStore.add("Component", Component);
|
270
277
|
TypeStore.add("ContactShadows", ContactShadows);
|
271
278
|
TypeStore.add("ControlTrackHandler", ControlTrackHandler);
|
272
279
|
TypeStore.add("CustomBranding", CustomBranding);
|
@@ -303,6 +310,7 @@
|
|
303
310
|
TypeStore.add("Image", Image);
|
304
311
|
TypeStore.add("InheritVelocityModule", InheritVelocityModule);
|
305
312
|
TypeStore.add("InputField", InputField);
|
313
|
+
TypeStore.add("Interactable", Interactable);
|
306
314
|
TypeStore.add("Light", Light);
|
307
315
|
TypeStore.add("LimitVelocityOverLifetimeModule", LimitVelocityOverLifetimeModule);
|
308
316
|
TypeStore.add("LODGroup", LODGroup);
|
@@ -316,7 +324,6 @@
|
|
316
324
|
TypeStore.add("MeshRenderer", MeshRenderer);
|
317
325
|
TypeStore.add("MinMaxCurve", MinMaxCurve);
|
318
326
|
TypeStore.add("MinMaxGradient", MinMaxGradient);
|
319
|
-
TypeStore.add("NeedleWebXRHtmlElement", NeedleWebXRHtmlElement);
|
320
327
|
TypeStore.add("NestedGltf", NestedGltf);
|
321
328
|
TypeStore.add("Networking", Networking);
|
322
329
|
TypeStore.add("NoiseModule", NoiseModule);
|
@@ -343,6 +350,7 @@
|
|
343
350
|
TypeStore.add("PreliminaryTrigger", PreliminaryTrigger);
|
344
351
|
TypeStore.add("PresentationMode", PresentationMode);
|
345
352
|
TypeStore.add("RawImage", RawImage);
|
353
|
+
TypeStore.add("Raycaster", Raycaster);
|
346
354
|
TypeStore.add("Rect", Rect);
|
347
355
|
TypeStore.add("RectTransform", RectTransform);
|
348
356
|
TypeStore.add("ReflectionProbe", ReflectionProbe);
|
@@ -370,7 +378,6 @@
|
|
370
378
|
TypeStore.add("SizeOverLifetimeModule", SizeOverLifetimeModule);
|
371
379
|
TypeStore.add("SkinnedMeshRenderer", SkinnedMeshRenderer);
|
372
380
|
TypeStore.add("SmoothFollow", SmoothFollow);
|
373
|
-
TypeStore.add("SpatialGrabRaycaster", SpatialGrabRaycaster);
|
374
381
|
TypeStore.add("SpatialHtml", SpatialHtml);
|
375
382
|
TypeStore.add("SpatialTrigger", SpatialTrigger);
|
376
383
|
TypeStore.add("SpatialTriggerReceiver", SpatialTriggerReceiver);
|
@@ -415,16 +422,20 @@
|
|
415
422
|
TypeStore.add("Volume", Volume);
|
416
423
|
TypeStore.add("VolumeParameter", VolumeParameter);
|
417
424
|
TypeStore.add("VolumeProfile", VolumeProfile);
|
425
|
+
TypeStore.add("VRUserState", VRUserState);
|
426
|
+
TypeStore.add("WebAR", WebAR);
|
418
427
|
TypeStore.add("WebARCameraBackground", WebARCameraBackground);
|
419
428
|
TypeStore.add("WebARSessionRoot", WebARSessionRoot);
|
420
429
|
TypeStore.add("WebXR", WebXR);
|
430
|
+
TypeStore.add("WebXRAvatar", WebXRAvatar);
|
431
|
+
TypeStore.add("WebXRController", WebXRController);
|
421
432
|
TypeStore.add("WebXRImageTracking", WebXRImageTracking);
|
422
433
|
TypeStore.add("WebXRImageTrackingModel", WebXRImageTrackingModel);
|
423
434
|
TypeStore.add("WebXRPlaneTracking", WebXRPlaneTracking);
|
435
|
+
TypeStore.add("WebXRSync", WebXRSync);
|
424
436
|
TypeStore.add("WebXRTrackedImage", WebXRTrackedImage);
|
425
|
-
TypeStore.add("XRControllerFollow", XRControllerFollow);
|
426
|
-
TypeStore.add("XRControllerModel", XRControllerModel);
|
427
|
-
TypeStore.add("XRControllerMovement", XRControllerMovement);
|
428
437
|
TypeStore.add("XRFlag", XRFlag);
|
438
|
+
TypeStore.add("XRGrabModel", XRGrabModel);
|
439
|
+
TypeStore.add("XRGrabRendering", XRGrabRendering);
|
429
440
|
TypeStore.add("XRRig", XRRig);
|
430
441
|
TypeStore.add("XRState", XRState);
|
@@ -1,22 +1,21 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
1
2
|
import * as THREE from "three";
|
3
|
+
// import { RendererCustomShader } from "./RendererCustomShader.js";
|
4
|
+
import { RendererLightmap } from "./RendererLightmap.js";
|
5
|
+
import { Context, FrameEvent } from "../engine/engine_setup.js";
|
6
|
+
import { getParam } from "../engine/engine_utils.js";
|
7
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
2
8
|
import { AxesHelper, Material, Matrix4, Mesh, Object3D, SkinnedMesh, Texture, Vector4 } from "three";
|
3
|
-
|
9
|
+
import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects.js";
|
10
|
+
import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
|
11
|
+
import { $instancingAutoUpdateBounds, $instancingRenderer, InstancingUtil, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
|
12
|
+
import type { IRenderer, ISharedMaterials } from "../engine/engine_types.js";
|
13
|
+
import { ReflectionProbe } from "./ReflectionProbe.js";
|
14
|
+
import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
|
15
|
+
import { isLocalNetwork } from "../engine/engine_networking_utils.js";
|
4
16
|
import { showBalloonWarning } from "../engine/debug/index.js";
|
5
17
|
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";
|
9
|
-
import { Context, FrameEvent } from "../engine/engine_setup.js";
|
10
18
|
import { getTempVector } from "../engine/engine_three_utils.js";
|
11
|
-
import type { IRenderer, ISharedMaterials } from "../engine/engine_types.js";
|
12
|
-
import { getParam } from "../engine/engine_utils.js";
|
13
|
-
import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
|
14
|
-
import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects.js";
|
15
|
-
import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
|
16
|
-
import { Behaviour, GameObject } from "./Component.js";
|
17
|
-
import { ReflectionProbe } from "./ReflectionProbe.js";
|
18
|
-
// import { RendererCustomShader } from "./RendererCustomShader.js";
|
19
|
-
import { RendererLightmap } from "./RendererLightmap.js";
|
20
19
|
|
21
20
|
// for staying compatible with old code
|
22
21
|
export { InstancingUtil } from "../engine/engine_instancing.js";
|
@@ -241,11 +240,7 @@
|
|
241
240
|
// private _materialProperties: Array<MaterialProperties> | undefined = undefined;
|
242
241
|
private _lightmaps?: RendererLightmap[];
|
243
242
|
|
244
|
-
|
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 {
|
243
|
+
get sharedMesh(): Mesh | undefined {
|
249
244
|
if (this.gameObject.type === "Mesh") {
|
250
245
|
return this.gameObject as unknown as Mesh
|
251
246
|
}
|
@@ -258,31 +253,11 @@
|
|
258
253
|
return undefined;
|
259
254
|
}
|
260
255
|
|
261
|
-
|
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 {
|
256
|
+
get sharedMaterial(): THREE.Material {
|
282
257
|
return this.sharedMaterials[0];
|
283
258
|
}
|
284
259
|
|
285
|
-
set sharedMaterial(mat: Material) {
|
260
|
+
set sharedMaterial(mat: THREE.Material) {
|
286
261
|
const cur = this.sharedMaterials[0];
|
287
262
|
if (cur === mat) return;
|
288
263
|
this.sharedMaterials[0] = mat;
|
@@ -290,12 +265,12 @@
|
|
290
265
|
}
|
291
266
|
|
292
267
|
/**@deprecated please use sharedMaterial */
|
293
|
-
get material(): Material {
|
268
|
+
get material(): THREE.Material {
|
294
269
|
return this.sharedMaterials[0];
|
295
270
|
}
|
296
271
|
|
297
272
|
/**@deprecated please use sharedMaterial */
|
298
|
-
set material(mat: Material) {
|
273
|
+
set material(mat: THREE.Material) {
|
299
274
|
this.sharedMaterial = mat;
|
300
275
|
}
|
301
276
|
|
@@ -480,10 +455,12 @@
|
|
480
455
|
|
481
456
|
private _isInstancingEnabled: boolean = false;
|
482
457
|
private handles: InstanceHandle[] | null | undefined = undefined;
|
458
|
+
private prevLayers: number[] | null | undefined = undefined;
|
483
459
|
|
484
460
|
private clearInstancingState() {
|
485
461
|
this._isInstancingEnabled = false;
|
486
462
|
this.handles = undefined;
|
463
|
+
this.prevLayers = undefined;
|
487
464
|
}
|
488
465
|
|
489
466
|
setInstancingEnabled(enabled: boolean): boolean {
|
@@ -542,9 +519,6 @@
|
|
542
519
|
}
|
543
520
|
|
544
521
|
onEnable() {
|
545
|
-
// ensure shared meshes are initialized
|
546
|
-
const _ = this.sharedMeshes;
|
547
|
-
|
548
522
|
this.setVisibility(true);
|
549
523
|
|
550
524
|
if (this._isInstancingEnabled) {
|
@@ -632,7 +606,11 @@
|
|
632
606
|
if (this._isInstancingEnabled && this.handles) {
|
633
607
|
for (let i = 0; i < this.handles.length; i++) {
|
634
608
|
const handle = this.handles[i];
|
635
|
-
|
609
|
+
if (!this.prevLayers) this.prevLayers = [];
|
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();
|
636
614
|
}
|
637
615
|
}
|
638
616
|
|
@@ -699,10 +677,10 @@
|
|
699
677
|
}
|
700
678
|
|
701
679
|
onAfterRender() {
|
702
|
-
if (this._isInstancingEnabled && this.handles) {
|
680
|
+
if (this._isInstancingEnabled && this.handles && this.prevLayers && this.prevLayers.length >= this.handles.length) {
|
703
681
|
for (let i = 0; i < this.handles.length; i++) {
|
704
682
|
const handle = this.handles[i];
|
705
|
-
|
683
|
+
handle.object.layers.mask = this.prevLayers[i];
|
706
684
|
}
|
707
685
|
}
|
708
686
|
|
@@ -1021,8 +999,8 @@
|
|
1021
999
|
this.inst = new THREE.InstancedMesh(geo, material, count);
|
1022
1000
|
this.inst[$instancingAutoUpdateBounds] = true;
|
1023
1001
|
this.inst.count = 0;
|
1002
|
+
this.inst.layers.set(2);
|
1024
1003
|
this.inst.visible = true;
|
1025
|
-
this.context.scene.add(this.inst);
|
1026
1004
|
|
1027
1005
|
// Not handled by RawShaderMaterial, so we need to set the define explicitly.
|
1028
1006
|
// Edge case: theoretically some users of the material could use it in an
|
@@ -1037,24 +1015,25 @@
|
|
1037
1015
|
material.needsUpdate = true;
|
1038
1016
|
}
|
1039
1017
|
|
1018
|
+
// this.inst.castShadow = true;
|
1019
|
+
// this.inst.receiveShadow = true;
|
1020
|
+
this.context.scene.add(this.inst);
|
1040
1021
|
context.pre_render_callbacks.push(this.onBeforeRender);
|
1041
|
-
|
1022
|
+
// console.log(this.inst);
|
1023
|
+
// this.context.pre_render_callbacks.push(this.onPreRender.bind(this));
|
1024
|
+
|
1025
|
+
// setInterval(() => {
|
1026
|
+
// this.inst.visible = !this.inst.visible;
|
1027
|
+
// }, 500);
|
1042
1028
|
}
|
1043
1029
|
|
1044
1030
|
private onBeforeRender = () => {
|
1045
|
-
// ensure the instanced mesh is rendered / has correct layers
|
1046
|
-
this.inst.layers.enableAll();
|
1047
|
-
|
1048
1031
|
if (this._needUpdateBounds && this.inst[$instancingAutoUpdateBounds] === true) {
|
1049
1032
|
if (debugInstancing)
|
1050
1033
|
console.log("Update instancing bounds", this.name, this.inst.matrixWorldNeedsUpdate);
|
1051
1034
|
this.updateBounds();
|
1052
1035
|
}
|
1053
1036
|
}
|
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
|
-
}
|
1058
1037
|
|
1059
1038
|
private randomColor() {
|
1060
1039
|
return new THREE.Color(Math.random(), Math.random(), Math.random());
|
@@ -1097,7 +1076,7 @@
|
|
1097
1076
|
if (this.inst.count > 0)
|
1098
1077
|
this.inst.visible = true;
|
1099
1078
|
|
1100
|
-
|
1079
|
+
// console.log("Added", this.name, this.inst.count, this.handles);
|
1101
1080
|
}
|
1102
1081
|
|
1103
1082
|
remove(handle: InstanceHandle) {
|
@@ -1137,7 +1116,6 @@
|
|
1137
1116
|
this.inst.visible = false;
|
1138
1117
|
|
1139
1118
|
this.inst.instanceMatrix.needsUpdate = true;
|
1140
|
-
if (debugInstancing) console.log("Removed", this.name, this.inst.count);
|
1141
1119
|
}
|
1142
1120
|
|
1143
1121
|
updateInstance(mat: THREE.Matrix4, index: number) {
|
@@ -1,5 +1,4 @@
|
|
1
|
-
import { Material, Mesh, ShaderMaterial, Texture, Vector4
|
2
|
-
|
1
|
+
import { Material, Mesh, type Shader, ShaderMaterial, Texture, Vector4 } from "three";
|
3
2
|
import type { Context, OnRenderCallback } from "../engine/engine_setup.js";
|
4
3
|
import { getParam } from "../engine/engine_utils.js";
|
5
4
|
|
@@ -100,7 +99,7 @@
|
|
100
99
|
}
|
101
100
|
}
|
102
101
|
|
103
|
-
private onBeforeCompile = (shader:
|
102
|
+
private onBeforeCompile = (shader: Shader, _) => {
|
104
103
|
if (debug) console.log("Lightmaps, before compile", shader)
|
105
104
|
//@ts-ignore
|
106
105
|
shader.lightMapUv = "uv1";
|
@@ -1,7 +1,6 @@
|
|
1
|
+
import { Mathf } from "../../engine/engine_math.js";
|
1
2
|
import { Color } from "three";
|
2
3
|
|
3
|
-
import { Mathf } from "../../engine/engine_math.js";
|
4
|
-
|
5
4
|
export class RGBAColor extends Color {
|
6
5
|
alpha: number = 1;
|
7
6
|
|
@@ -1,14 +1,13 @@
|
|
1
|
+
import { Behaviour } from "./Component.js";
|
1
2
|
import * as THREE from 'three'
|
3
|
+
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";
|
2
6
|
import { Matrix4, Object3D, Vector3 } from "three";
|
3
|
-
|
7
|
+
import type { IRigidbody, Vec3 } from "../engine/engine_types.js";
|
4
8
|
import { CollisionDetectionMode, RigidbodyConstraints } from "../engine/engine_physics.types.js";
|
5
|
-
import {
|
9
|
+
import { validate } from "../engine/engine_util_decorator.js";
|
6
10
|
import { Context, FrameEvent } from "../engine/engine_setup.js";
|
7
|
-
import { getWorldPosition } from "../engine/engine_three_utils.js";
|
8
|
-
import type { IRigidbody, Vec3 } from "../engine/engine_types.js";
|
9
|
-
import { validate } from "../engine/engine_util_decorator.js";
|
10
|
-
import { Watch } from "../engine/engine_utils.js";
|
11
|
-
import { Behaviour } from "./Component.js";
|
12
11
|
|
13
12
|
class TransformWatch {
|
14
13
|
|
@@ -362,17 +361,10 @@
|
|
362
361
|
return this.context.physics.engine?.isSleeping(this);
|
363
362
|
}
|
364
363
|
|
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
|
-
|
371
364
|
/** Forces affect the rigid-body's acceleration whereas impulses affect the rigid-body's velocity
|
372
365
|
* the acceleration change is equal to the force divided by the mass:
|
373
366
|
* @link see https://rapier.rs/docs/user_guides/javascript/rigid_bodies#forces-and-impulses */
|
374
367
|
public applyForce(vec: Vector3 | Vec3, _rel?: THREE.Vector3, wakeup: boolean = true) {
|
375
|
-
if (this._propertiesChanged) this.updateProperties();
|
376
368
|
this.context.physics.engine?.addForce(this, vec, wakeup);
|
377
369
|
}
|
378
370
|
|
@@ -380,7 +372,6 @@
|
|
380
372
|
* the velocity change is equal to the impulse divided by the mass
|
381
373
|
* @link see https://rapier.rs/docs/user_guides/javascript/rigid_bodies#forces-and-impulses */
|
382
374
|
public applyImpulse(vec: Vector3 | Vec3, wakeup: boolean = true) {
|
383
|
-
if (this._propertiesChanged) this.updateProperties();
|
384
375
|
this.context.physics.engine?.applyImpulse(this, vec, wakeup);
|
385
376
|
}
|
386
377
|
|
@@ -1,12 +1,11 @@
|
|
1
|
-
import { Object3D } from "three";
|
2
|
-
|
3
1
|
import { AssetReference } from "../engine/engine_addressables.js";
|
4
|
-
import { registerObservableAttribute } from "../engine/engine_element_extras.js";
|
5
2
|
import { InputEvents } from "../engine/engine_input.js";
|
6
3
|
import { isLocalNetwork } from "../engine/engine_networking_utils.js";
|
4
|
+
import { delay, getParam, setParamWithoutReload } from "../engine/engine_utils.js";
|
7
5
|
import { serializable } from "../engine/engine_serialization.js";
|
8
|
-
import { delay, getParam, setParamWithoutReload } from "../engine/engine_utils.js";
|
9
6
|
import { Behaviour, GameObject } from "./Component.js";
|
7
|
+
import { registerObservableAttribute } from "../engine/engine_element_extras.js";
|
8
|
+
import { Object3D } from "three";
|
10
9
|
|
11
10
|
const debug = getParam("debugsceneswitcher");
|
12
11
|
|
@@ -126,9 +125,9 @@
|
|
126
125
|
|
127
126
|
async onEnable() {
|
128
127
|
globalThis.addEventListener("popstate", this.onPopState);
|
129
|
-
this.context.input.addEventListener(InputEvents.KeyDown, this.
|
130
|
-
this.context.input.addEventListener(InputEvents.PointerMove, this.
|
131
|
-
this.context.input.addEventListener(InputEvents.PointerUp, this.
|
128
|
+
this.context.input.addEventListener(InputEvents.KeyDown, this.onKeyDown);
|
129
|
+
this.context.input.addEventListener(InputEvents.PointerMove, this.onPointerMove);
|
130
|
+
this.context.input.addEventListener(InputEvents.PointerUp, this.onPointerUp);
|
132
131
|
|
133
132
|
if (!this._engineElementOverserver) {
|
134
133
|
this._engineElementOverserver = new MutationObserver((mutations) => {
|
@@ -173,9 +172,9 @@
|
|
173
172
|
|
174
173
|
onDisable(): void {
|
175
174
|
globalThis.removeEventListener("popstate", this.onPopState);
|
176
|
-
this.context.input.removeEventListener(InputEvents.KeyDown, this.
|
177
|
-
this.context.input.removeEventListener(InputEvents.PointerMove, this.
|
178
|
-
this.context.input.removeEventListener(InputEvents.PointerUp, this.
|
175
|
+
this.context.input.removeEventListener(InputEvents.KeyDown, this.onKeyDown);
|
176
|
+
this.context.input.removeEventListener(InputEvents.PointerMove, this.onPointerMove);
|
177
|
+
this.context.input.removeEventListener(InputEvents.PointerUp, this.onPointerUp);
|
179
178
|
this._preloadScheduler?.stop();
|
180
179
|
}
|
181
180
|
|
@@ -203,7 +202,7 @@
|
|
203
202
|
|
204
203
|
private normalizedSwipeThresholdX = 0.1;
|
205
204
|
private _didSwipe: boolean = false;
|
206
|
-
private
|
205
|
+
private onPointerMove = (e: any) => {
|
207
206
|
if (!this.useSwipe) return;
|
208
207
|
if (!this._didSwipe && e.button === 0 && e.pointerType === "touch" && this.context.input.getPointerPressedCount() === 1) {
|
209
208
|
const delta = this.context.input.getPointerPositionDelta(e.button);
|
@@ -221,13 +220,13 @@
|
|
221
220
|
}
|
222
221
|
}
|
223
222
|
|
224
|
-
private
|
223
|
+
private onPointerUp = (e: any) => {
|
225
224
|
if (e.button === 0) {
|
226
225
|
this._didSwipe = false;
|
227
226
|
}
|
228
227
|
};
|
229
228
|
|
230
|
-
private
|
229
|
+
private onKeyDown = (e: any) => {
|
231
230
|
if (!this.useKeyboard) return;
|
232
231
|
if (!this.scenes) return;
|
233
232
|
const key = e.key.toLowerCase();
|
@@ -1,79 +0,0 @@
|
|
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
|
-
}
|
@@ -1,8 +1,7 @@
|
|
1
1
|
|
2
2
|
import * as flatbuffers from "flatbuffers"
|
3
|
-
|
3
|
+
import { Transform } from "./transform.js";
|
4
4
|
import { SyncedTransformModel } from "./synced-transform-model.js";
|
5
|
-
import { Transform } from "./transform.js";
|
6
5
|
|
7
6
|
// registry
|
8
7
|
export const binaryIdentifierCasts : {[key:string] : (bin:flatbuffers.ByteBuffer) => object} = {};
|
@@ -1,12 +1,12 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
2
|
+
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";
|
1
7
|
import { showBalloonWarning } from "../engine/debug/index.js";
|
8
|
+
import { NetworkedStreams, disposeStream, StreamReceivedEvent, StreamEndedEvent, PeerHandle, NetworkedStreamEvents } from "../engine/engine_networking_streams.js";
|
2
9
|
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";
|
7
|
-
import { Behaviour, GameObject } from "./Component.js";
|
8
|
-
import type { IPointerClickHandler, PointerEventData } from "./ui/PointerEvents.js";
|
9
|
-
import { AspectMode, VideoPlayer } from "./VideoPlayer.js";
|
10
10
|
|
11
11
|
const debug = getParam("debugscreensharing");
|
12
12
|
|
@@ -146,7 +146,6 @@
|
|
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;
|
150
149
|
});
|
151
150
|
}
|
152
151
|
}
|
@@ -182,7 +181,7 @@
|
|
182
181
|
if (this._activeShareRequest) return this._activeShareRequest;
|
183
182
|
this._activeShareRequest = this.internalShare(opts);
|
184
183
|
return this._activeShareRequest.then(() =>{
|
185
|
-
|
184
|
+
this._activeShareRequest = null;
|
186
185
|
})
|
187
186
|
}
|
188
187
|
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { BlendFunction, DepthDownsamplingPass, NormalPass, SSAOEffect } from "postprocessing";
|
2
2
|
import { Color, PerspectiveCamera } from "three";
|
3
|
-
|
4
3
|
import { serializable } from "../../../engine/engine_serialization.js";
|
5
4
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
6
5
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,11 +1,10 @@
|
|
1
|
-
import { N8AOPostPass } from "n8ao";
|
2
1
|
import { Color, NeverDepth, PerspectiveCamera } from "three";
|
3
|
-
|
4
2
|
import { serializable } from "../../../engine/engine_serialization.js";
|
5
|
-
import { validate } from "../../../engine/engine_util_decorator.js";
|
6
3
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
7
4
|
import { VolumeParameter } from "../VolumeParameter.js";
|
8
5
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
6
|
+
import { N8AOPostPass } from "n8ao";
|
7
|
+
import { validate } from "../../../engine/engine_util_decorator.js";
|
9
8
|
|
10
9
|
// https://github.com/N8python/n8ao
|
11
10
|
|
@@ -1,9 +1,8 @@
|
|
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";
|
5
1
|
import { Behaviour, GameObject } from "./Component.js";
|
6
2
|
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";
|
7
6
|
import { Renderer } from "./Renderer.js";
|
8
7
|
|
9
8
|
enum ShadowMode {
|
@@ -1,7 +1,7 @@
|
|
1
|
+
import { EventList } from "../EventList.js";
|
2
|
+
import { Behaviour } from "../Component.js";
|
1
3
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
2
4
|
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,16 +1,15 @@
|
|
1
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
2
|
+
import { Behaviour, GameObject } from "./Component.js";
|
3
|
+
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
|
4
|
+
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
|
1
5
|
import { EquirectangularRefractionMapping, NeverDepth, SRGBColorSpace, sRGBEncoding, Texture, TextureLoader } from "three"
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
|
5
|
-
import { disposeObjectResources, setDisposable } from "../engine/engine_assetdatabase.js";
|
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";
|
6
9
|
import { ContextRegistry } from "../engine/engine_context_registry.js";
|
7
10
|
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";
|
10
11
|
import { type IContext } from "../engine/engine_types.js";
|
11
|
-
import {
|
12
|
-
import { Camera, ClearFlags } from "./Camera.js";
|
13
|
-
import { Behaviour, GameObject } from "./Component.js";
|
12
|
+
import { disposeObjectResources, setDisposable } from "../engine/engine_assetdatabase.js";
|
14
13
|
|
15
14
|
const debug = getParam("debugskybox");
|
16
15
|
|
@@ -93,7 +92,7 @@
|
|
93
92
|
const entry = cache.shift();
|
94
93
|
if (entry) { disposeCachedTexture(entry.texture); }
|
95
94
|
}
|
96
|
-
texture.then(t =>
|
95
|
+
texture.then(t => setDisposable(t, false));
|
97
96
|
cache.push({ src, texture });
|
98
97
|
}
|
99
98
|
|
@@ -1,12 +1,11 @@
|
|
1
|
+
import { Camera } from "./Camera.js";
|
2
|
+
import { Behaviour, GameObject } from "./Component.js";
|
1
3
|
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";
|
6
5
|
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 {
|
9
|
-
import { Behaviour, GameObject } from "./Component.js";
|
8
|
+
import { Axes } from "../engine/engine_physics.types.js";
|
10
9
|
|
11
10
|
export class SmoothFollow extends Behaviour {
|
12
11
|
|
@@ -1,7 +1,6 @@
|
|
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
|
-
|
5
4
|
import { getWorldEuler, getWorldRotation, setWorldRotationXYZ } from '../../engine/engine_three_utils.js';
|
6
5
|
import { Behaviour } from '../Component.js';
|
7
6
|
|
@@ -1,10 +1,9 @@
|
|
1
1
|
import { BoxHelper, Layers } from "three";
|
2
|
-
|
2
|
+
import { Behaviour, GameObject } from "./Component.js";
|
3
|
+
import { BoxHelperComponent } from "./BoxHelperComponent.js"
|
4
|
+
import { EventList } from "./EventList.js";
|
3
5
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
4
6
|
import { getParam } from "../engine/engine_utils.js";
|
5
|
-
import { BoxHelperComponent } from "./BoxHelperComponent.js"
|
6
|
-
import { Behaviour, GameObject } from "./Component.js";
|
7
|
-
import { EventList } from "./EventList.js";
|
8
7
|
|
9
8
|
const debug = getParam("debugspatialtrigger");
|
10
9
|
|
@@ -1,21 +1,21 @@
|
|
1
|
+
import { Behaviour, Component, GameObject } from "./Component.js";
|
2
|
+
import { Camera } from "./Camera.js";
|
1
3
|
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";
|
2
9
|
import { Object3D } from "three";
|
3
|
-
|
4
10
|
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";
|
5
15
|
import { RoomEvents } from "../engine/engine_networking.js";
|
16
|
+
import type { ICamera } from "../engine/engine_types.js";
|
6
17
|
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";
|
9
18
|
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,11 +145,23 @@
|
|
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
|
+
|
148
157
|
this.orbit = GameObject.getComponent(this.context.mainCamera, OrbitControls);
|
149
158
|
}
|
150
159
|
|
151
160
|
onDestroy(): void {
|
152
161
|
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);
|
153
165
|
this._handler?.destroy();
|
154
166
|
this._networking?.destroy();
|
155
167
|
}
|
@@ -161,13 +173,13 @@
|
|
161
173
|
return standalone && !isHololens;
|
162
174
|
}
|
163
175
|
|
164
|
-
|
176
|
+
private onXRSessionRequestStart(_evt) {
|
165
177
|
if (!this.isSupportedPlatform()) return;
|
166
178
|
GameObject.setActive(this.gameObject, true);
|
167
179
|
}
|
168
180
|
|
169
181
|
|
170
|
-
|
182
|
+
private onXRSessionStart(_evt) {
|
171
183
|
if (!this.isSupportedPlatform()) return;
|
172
184
|
if (debug) console.log(this.context.mainCamera);
|
173
185
|
if (this.context.mainCamera) {
|
@@ -175,7 +187,7 @@
|
|
175
187
|
}
|
176
188
|
}
|
177
189
|
|
178
|
-
|
190
|
+
private onXRSessionEnded(_evt) {
|
179
191
|
this.context.removeCamera(this.cam as ICamera);
|
180
192
|
GameObject.setActive(this.gameObject, false);
|
181
193
|
if (this.orbit) this.orbit.enabled = true;
|
@@ -212,16 +224,14 @@
|
|
212
224
|
const previousRenderTarget = renderer.getRenderTarget();
|
213
225
|
let oldFramebuffer: WebGLFramebuffer | null = null;
|
214
226
|
|
215
|
-
const webglState = renderer.state as THREE.WebGLState & { bindXRFramebuffer?: Function };
|
216
|
-
|
217
227
|
// seems that in some cases, renderer.getRenderTarget returns null
|
218
228
|
// even when we're rendering to a headset.
|
219
229
|
if (!previousRenderTarget) {
|
220
|
-
if (!renderer.state.bindFramebuffer || !
|
230
|
+
if (!renderer.state.bindFramebuffer || !renderer.state.bindXRFramebuffer)
|
221
231
|
return;
|
222
232
|
|
223
233
|
oldFramebuffer = renderer["_framebuffer"];
|
224
|
-
|
234
|
+
renderer.state.bindXRFramebuffer(null);
|
225
235
|
}
|
226
236
|
|
227
237
|
this.setAvatarFlagsBeforeRender();
|
@@ -269,8 +279,8 @@
|
|
269
279
|
|
270
280
|
if (previousRenderTarget)
|
271
281
|
renderer.setRenderTarget(previousRenderTarget);
|
272
|
-
else
|
273
|
-
|
282
|
+
else
|
283
|
+
renderer.state.bindXRFramebuffer(oldFramebuffer);
|
274
284
|
|
275
285
|
this.resetAvatarFlags();
|
276
286
|
}
|
@@ -279,7 +289,7 @@
|
|
279
289
|
const isFirstPersonMode = this._mode === SpectatorMode.FirstPerson;
|
280
290
|
|
281
291
|
for (const av of AvatarMarker.instances) {
|
282
|
-
if (av.avatar && "isLocalAvatar" in av.avatar
|
292
|
+
if (av.avatar && "isLocalAvatar" in av.avatar) {
|
283
293
|
let mask = XRStateFlag.All;
|
284
294
|
if (this.isSpectatingSelf)
|
285
295
|
mask = isFirstPersonMode && av.avatar.isLocalAvatar ? XRStateFlag.FirstPerson : XRStateFlag.ThirdPerson;
|
@@ -298,7 +308,7 @@
|
|
298
308
|
const flags = av.avatar.flags;
|
299
309
|
if (!flags) continue;
|
300
310
|
for (const flag of flags) {
|
301
|
-
if (
|
311
|
+
if (av.avatar?.isLocalAvatar) {
|
302
312
|
flag.UpdateVisible(XRStateFlag.FirstPerson);
|
303
313
|
}
|
304
314
|
else {
|
@@ -1,10 +1,9 @@
|
|
1
|
+
import { Behaviour } from "./Component.js";
|
1
2
|
import * as THREE from "three";
|
3
|
+
import { serializable, serializeable } from "../engine/engine_serialization_decorator.js";
|
2
4
|
import { Material, NearestFilter, Texture } from "three";
|
3
|
-
|
4
|
-
import { serializable, serializeable } from "../engine/engine_serialization_decorator.js";
|
5
|
+
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
5
6
|
import { getParam } from "../engine/engine_utils.js";
|
6
|
-
import { Behaviour } from "./Component.js";
|
7
|
-
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
8
7
|
|
9
8
|
const debug = getParam("debugspriterenderer");
|
10
9
|
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";
|
8
1
|
import { NetworkConnection } from "../engine/engine_networking.js";
|
9
|
-
import {
|
10
|
-
import {
|
2
|
+
import { Behaviour, GameObject } from "./Component.js";
|
3
|
+
import { Camera } from "./Camera.js";
|
11
4
|
import * as utils from "../engine/engine_three_utils.js"
|
12
|
-
import {
|
5
|
+
import { WebXR } from "./webxr/WebXR.js";
|
6
|
+
import { Builder } from "flatbuffers";
|
13
7
|
import { SyncedCameraModel } from "../engine-schemes/synced-camera-model.js";
|
14
8
|
import { Vec3 } from "../engine-schemes/vec3.js";
|
15
|
-
import {
|
16
|
-
import {
|
9
|
+
import { registerBinaryType } from "../engine-schemes/schemes.js";
|
10
|
+
import { InstancingUtil } from "../engine/engine_instancing.js";
|
11
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
12
|
+
import { Object3D } from "three";
|
17
13
|
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 (WebXR.IsInWebXR) return;
|
134
134
|
|
135
135
|
const cam = this.context.mainCamera
|
136
136
|
if (cam === null) {
|
@@ -1,7 +1,7 @@
|
|
1
|
+
import { Behaviour } from "./Component.js";
|
2
|
+
import * as utils from "../engine/engine_utils.js"
|
1
3
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
2
|
-
import * as utils from "../engine/engine_utils.js"
|
3
4
|
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,17 +1,15 @@
|
|
1
|
-
import * as flatbuffers from "flatbuffers";
|
2
1
|
import * as THREE from 'three'
|
3
|
-
|
4
|
-
import { InstancingUtil } from "../engine/engine_instancing.js";
|
5
|
-
import { onUpdate } from '../engine/engine_lifecycle_api.js';
|
6
2
|
import { OwnershipModel, RoomEvents } from "../engine/engine_networking.js"
|
3
|
+
import { Behaviour, GameObject } from "./Component.js";
|
4
|
+
import { Rigidbody } from "./RigidBody.js";
|
5
|
+
import * as utils from "../engine/engine_utils.js"
|
7
6
|
import { sendDestroyed } from '../engine/engine_networking_instantiate.js';
|
8
|
-
import {
|
9
|
-
import * as utils from "../engine/engine_utils.js"
|
10
|
-
import { registerBinaryType } from '../engine-schemes/schemes.js';
|
7
|
+
import { InstancingUtil } from "../engine/engine_instancing.js";
|
11
8
|
import { SyncedTransformModel } from '../engine-schemes/synced-transform-model.js';
|
9
|
+
import * as flatbuffers from "flatbuffers";
|
12
10
|
import { Transform } from '../engine-schemes/transform.js';
|
13
|
-
import {
|
14
|
-
import {
|
11
|
+
import { registerBinaryType } from '../engine-schemes/schemes.js';
|
12
|
+
import { setWorldEuler } from '../engine/engine_three_utils.js';
|
15
13
|
|
16
14
|
const debug = utils.getParam("debugsync");
|
17
15
|
export const SyncedTransformIdentifier = "STRS";
|
@@ -37,19 +35,8 @@
|
|
37
35
|
}
|
38
36
|
|
39
37
|
|
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
|
-
|
50
38
|
export class SyncedTransform extends Behaviour {
|
51
39
|
|
52
|
-
|
53
40
|
// public autoOwnership: boolean = true;
|
54
41
|
public overridePhysics: boolean = true
|
55
42
|
public interpolatePosition: boolean = true;
|
@@ -70,7 +57,6 @@
|
|
70
57
|
private _receivedFastUpdate: boolean = false;
|
71
58
|
private _shouldRequestOwnership: boolean = false;
|
72
59
|
|
73
|
-
/** Request ownership of an object - you need to be connected to a room */
|
74
60
|
public requestOwnership() {
|
75
61
|
if (debug)
|
76
62
|
console.log("Request ownership");
|
@@ -306,12 +292,8 @@
|
|
306
292
|
|
307
293
|
const updateInterval = 10;
|
308
294
|
const fastUpdate = this.rb || this.fastMode;
|
309
|
-
|
310
295
|
if (this._needsUpdate && (updateInterval <= 0 || updateInterval > 0 && this.context.time.frameCount % updateInterval === 0 || fastUpdate)) {
|
311
296
|
|
312
|
-
FAST_ACTIVE_SYNCTRANSFORMS++;
|
313
|
-
if (fastUpdate && FAST_INTERVAL > 0 && this.context.time.frameCount % FAST_INTERVAL !== 0) return;
|
314
|
-
|
315
297
|
if (debug)
|
316
298
|
console.log("send update", this.context.connection.connectionId, this.guid, this.gameObject.name, this.gameObject.guid);
|
317
299
|
|
@@ -1,9 +0,0 @@
|
|
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
|
-
}
|
@@ -1,183 +0,0 @@
|
|
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
|
-
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
|
+
import * as utils from "../engine_utils.js";
|
2
3
|
import { noVoip } from "../../engine-components/Voip.js";
|
3
|
-
import * as utils from "../engine_utils.js";
|
4
4
|
|
5
5
|
|
6
6
|
export function detect_run_tests(){
|
@@ -1,12 +1,11 @@
|
|
1
|
+
import { Behaviour } from "./Component.js";
|
2
|
+
import * as tests from "../engine/tests/test_utils.js";
|
3
|
+
import { createTransformModel, SyncedTransform, SyncedTransformIdentifier } from "./SyncedTransform.js";
|
1
4
|
import * as flatbuffers from 'flatbuffers';
|
5
|
+
import { SyncedTransformModel } from "../engine-schemes/synced-transform-model.js";
|
6
|
+
import { Rigidbody } from "./RigidBody.js";
|
2
7
|
import { Vector3 } from "three";
|
3
|
-
|
4
8
|
import type { IModel } from "../engine/engine_networking_types.js";
|
5
|
-
import * as tests from "../engine/tests/test_utils.js";
|
6
|
-
import { SyncedTransformModel } from "../engine-schemes/synced-transform-model.js";
|
7
|
-
import { Behaviour } from "./Component.js";
|
8
|
-
import { Rigidbody } from "./RigidBody.js";
|
9
|
-
import { createTransformModel, SyncedTransform, SyncedTransformIdentifier } from "./SyncedTransform.js";
|
10
9
|
|
11
10
|
export class TestRunner extends Behaviour {
|
12
11
|
awake(): void {
|
@@ -1,13 +1,12 @@
|
|
1
|
-
import {
|
1
|
+
import { Graphic } from './Graphic.js';
|
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
|
-
|
4
|
+
import { Color } from 'three';
|
5
|
+
import { updateRenderSettings } from './Utils.js';
|
6
|
+
import { Canvas } from './Canvas.js';
|
5
7
|
import { serializable } from '../../engine/engine_serialization_decorator.js';
|
6
8
|
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';
|
11
10
|
|
12
11
|
const debug = getParam("debugtext");
|
13
12
|
|
@@ -314,12 +313,12 @@
|
|
314
313
|
const child = this.uiObject.children[i];
|
315
314
|
// @ts-ignore
|
316
315
|
if (child.isUI) {
|
317
|
-
this.uiObject.remove(child
|
316
|
+
this.uiObject.remove(child);
|
318
317
|
child.clear();
|
319
318
|
}
|
320
319
|
}
|
321
320
|
const el = new ThreeMeshUI.Inline({ textContent: text.substring(0, currentTag.startIndex), color: 'inherit' });
|
322
|
-
this.uiObject.add(el
|
321
|
+
this.uiObject.add(el);
|
323
322
|
}
|
324
323
|
|
325
324
|
const stackArray: Array<TagStackEntry> = [];
|
@@ -336,13 +335,13 @@
|
|
336
335
|
opts.textContent = this.getText(text, currentTag, next);
|
337
336
|
this.handleTag(currentTag, opts, stackArray);
|
338
337
|
const el = new ThreeMeshUI.Inline(opts);
|
339
|
-
this.uiObject?.add(el
|
338
|
+
this.uiObject?.add(el)
|
340
339
|
|
341
340
|
} else {
|
342
341
|
opts.textContent = text.substring(currentTag.endIndex);
|
343
342
|
this.handleTag(currentTag, opts, stackArray);
|
344
343
|
const el = new ThreeMeshUI.Inline(opts);
|
345
|
-
this.uiObject?.add(el
|
344
|
+
this.uiObject?.add(el);
|
346
345
|
}
|
347
346
|
currentTag = next;
|
348
347
|
}
|
@@ -1,37 +1,36 @@
|
|
1
|
+
import { Renderer } from '../../Renderer.js';
|
2
|
+
import { GameObject } from '../../Component.js';
|
3
|
+
import type { OffscreenCanvasExt } from '../../../engine/engine_shims.js';
|
1
4
|
import '../../../engine/engine_shims.js';
|
2
|
-
|
3
5
|
import {
|
4
|
-
|
5
|
-
|
6
|
-
|
6
|
+
PlaneGeometry,
|
7
|
+
Texture,
|
8
|
+
Uniform,
|
9
|
+
PerspectiveCamera,
|
10
|
+
Scene,
|
11
|
+
Mesh,
|
12
|
+
ShaderMaterial,
|
13
|
+
WebGLRenderer,
|
14
|
+
MathUtils,
|
15
|
+
Matrix4,
|
16
|
+
DoubleSide,
|
7
17
|
BufferGeometry,
|
18
|
+
Material,
|
8
19
|
Color,
|
9
|
-
|
10
|
-
Material,
|
11
|
-
MathUtils,
|
12
|
-
Matrix4,
|
13
|
-
Mesh,
|
14
|
-
MeshBasicMaterial,
|
20
|
+
MeshStandardMaterial,
|
15
21
|
MeshPhysicalMaterial,
|
16
|
-
MeshStandardMaterial,
|
17
22
|
Object3D,
|
18
|
-
|
19
|
-
|
20
|
-
PlaneGeometry,
|
21
|
-
Scene,
|
22
|
-
ShaderMaterial,
|
23
|
+
MeshBasicMaterial,
|
24
|
+
Bone,
|
23
25
|
SkinnedMesh,
|
24
26
|
SRGBColorSpace,
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
27
|
+
AnimationClip,
|
28
|
+
OrthographicCamera,
|
29
|
+
BufferAttribute,
|
30
|
+
Vector4
|
31
|
+
} from 'three';
|
29
32
|
import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
|
30
33
|
|
31
|
-
import type { OffscreenCanvasExt } from '../../../engine/engine_shims.js';
|
32
|
-
import { GameObject } from '../../Component.js';
|
33
|
-
import { Renderer } from '../../Renderer.js';
|
34
|
-
|
35
34
|
function makeNameSafe( str ) {
|
36
35
|
str = str.replace( /[^a-zA-Z0-9_]/g, '' );
|
37
36
|
|
@@ -1081,9 +1080,7 @@
|
|
1081
1080
|
|
1082
1081
|
if ( geometry ) {
|
1083
1082
|
writer.beginBlock( `def ${objType} "${name}"`, "(", false );
|
1084
|
-
|
1085
|
-
// also emit extra data for jointIndices etc., so we're skipping skinned meshes here.
|
1086
|
-
if (context.quickLookCompatible && material && material.side === DoubleSide && !isSkinnedMesh)
|
1083
|
+
if (context.quickLookCompatible && material && material.side === DoubleSide)
|
1087
1084
|
writer.appendLine(`prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry_doubleSided>`);
|
1088
1085
|
else
|
1089
1086
|
writer.appendLine(`prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>`);
|
@@ -1761,17 +1758,17 @@
|
|
1761
1758
|
];
|
1762
1759
|
|
1763
1760
|
export {
|
1761
|
+
USDZExporter,
|
1762
|
+
USDZExporterContext,
|
1763
|
+
USDWriter,
|
1764
|
+
USDObject,
|
1764
1765
|
buildMatrix,
|
1765
|
-
decompressGpuTexture,
|
1766
|
-
findStructuralNodesInBoneHierarchy,
|
1767
1766
|
getBoneName,
|
1768
1767
|
getPathToSkeleton,
|
1768
|
+
fn as usdNumberFormatting,
|
1769
|
+
USDDocument,
|
1770
|
+
makeNameSafe as makeNameSafeForUSD,
|
1769
1771
|
imageToCanvas,
|
1770
|
-
|
1771
|
-
|
1772
|
-
fn as usdNumberFormatting,
|
1773
|
-
USDObject,
|
1774
|
-
USDWriter,
|
1775
|
-
USDZExporter,
|
1776
|
-
USDZExporterContext,
|
1772
|
+
decompressGpuTexture,
|
1773
|
+
findStructuralNodesInBoneHierarchy,
|
1777
1774
|
};
|
@@ -1,9 +1,8 @@
|
|
1
|
+
import { registerCustomEffectType } from "../VolumeProfile.js";
|
2
|
+
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
1
3
|
import { KernelSize, TiltShiftEffect as TiltShift } from "postprocessing";
|
2
|
-
|
4
|
+
import { VolumeParameter } from "../VolumeParameter.js";
|
3
5
|
import { serializable } from "../../../engine/engine_serialization.js";
|
4
|
-
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
5
|
-
import { VolumeParameter } from "../VolumeParameter.js";
|
6
|
-
import { registerCustomEffectType } from "../VolumeProfile.js";
|
7
6
|
|
8
7
|
|
9
8
|
export class TiltShiftEffect extends PostProcessingEffect {
|
@@ -1,15 +1,14 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import {
|
1
|
+
import { PlayableDirector } from "./PlayableDirector.js";
|
2
|
+
import * as Models from "./TimelineModels.js";
|
3
|
+
import { GameObject } from "../Component.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";
|
5
7
|
import { getParam, resolveUrl } from "../../engine/engine_utils.js";
|
8
|
+
import { AudioSource } from "../AudioSource.js";
|
9
|
+
import { Animator } from "../Animator.js"
|
6
10
|
import { setObjectAnimated } from "../AnimationUtils.js";
|
7
|
-
import {
|
8
|
-
import { AudioSource } from "../AudioSource.js";
|
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";
|
11
|
+
import { isDevEnvironment } from "../../engine/debug/index.js";
|
13
12
|
|
14
13
|
const debug = getParam("debugtimeline");
|
15
14
|
|
@@ -564,16 +563,14 @@
|
|
564
563
|
|
565
564
|
const muteAudioTracks = getParam("mutetimeline");
|
566
565
|
|
567
|
-
declare type AudioClipModel = Models.ClipModel & { _didTriggerPlay: boolean };
|
568
|
-
|
569
566
|
export class AudioTrackHandler extends TrackHandler {
|
570
567
|
|
571
|
-
models: Array<
|
568
|
+
models: Array<Models.ClipModel> = [];
|
572
569
|
listener!: AudioListener;
|
573
570
|
audio: Array<Audio> = [];
|
574
571
|
audioContextTimeOffset: Array<number> = [];
|
575
572
|
lastTime: number = 0;
|
576
|
-
audioSource?:
|
573
|
+
audioSource?:AudioSource;
|
577
574
|
|
578
575
|
private _audioLoader: AudioLoader | null = null;
|
579
576
|
|
@@ -594,9 +591,7 @@
|
|
594
591
|
addModel(model: Models.ClipModel) {
|
595
592
|
const audio = new Audio(this.listener as any);
|
596
593
|
this.audio.push(audio);
|
597
|
-
|
598
|
-
audioClipModel._didTriggerPlay = false;
|
599
|
-
this.models.push(audioClipModel);
|
594
|
+
this.models.push(model);
|
600
595
|
}
|
601
596
|
|
602
597
|
onDisable() {
|
@@ -604,9 +599,6 @@
|
|
604
599
|
if (audio.isPlaying)
|
605
600
|
audio.stop();
|
606
601
|
}
|
607
|
-
for (const model of this.models) {
|
608
|
-
model._didTriggerPlay = false;
|
609
|
-
}
|
610
602
|
}
|
611
603
|
|
612
604
|
onDestroy() {
|
@@ -634,23 +626,8 @@
|
|
634
626
|
if (audio?.isPlaying)
|
635
627
|
audio.stop();
|
636
628
|
}
|
637
|
-
for (const model of this.models) {
|
638
|
-
model._didTriggerPlay = false;
|
639
|
-
}
|
640
629
|
}
|
641
630
|
|
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
|
-
|
654
631
|
evaluate(time: number) {
|
655
632
|
if (muteAudioTracks) return;
|
656
633
|
if (this.track.muted) return;
|
@@ -659,8 +636,6 @@
|
|
659
636
|
return;
|
660
637
|
}
|
661
638
|
const isMuted = this.director.context.application.muted;
|
662
|
-
const resumePlay = this._playableDirectorResumed;
|
663
|
-
this._playableDirectorResumed = false;
|
664
639
|
// this is just so that we dont hear the very first beat when the audio starts but is muted
|
665
640
|
// if we dont add a delay we hear a little bit of the audio before it shuts down
|
666
641
|
// MAYBE instead of doing it like this we should connect a custom audio node (or disconnect the output node?)
|
@@ -678,24 +653,15 @@
|
|
678
653
|
audio.playbackRate = this.director.context.time.timeScale * this.director.speed;
|
679
654
|
audio.loop = asset.loop;
|
680
655
|
if (time >= model.start && time <= model.end && time < this.director.duration) {
|
681
|
-
if (
|
682
|
-
|
683
|
-
|
684
|
-
|
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
|
-
}
|
656
|
+
if (this.director.isPlaying == false) {
|
657
|
+
if (audio.isPlaying)
|
658
|
+
audio.stop();
|
659
|
+
if (this.lastTime === time) continue;
|
698
660
|
}
|
661
|
+
else if (!audio.isPlaying) {
|
662
|
+
audio.offset = model.clipIn + (time - model.start) * model.timeScale;
|
663
|
+
audio.play(playTimeOffset);
|
664
|
+
}
|
699
665
|
else {
|
700
666
|
const targetOffset = model.clipIn + (time - model.start) * model.timeScale;
|
701
667
|
// seems it's non-trivial to get the right time from audio sources;
|
@@ -711,7 +677,7 @@
|
|
711
677
|
}
|
712
678
|
let vol = asset.volume as number;
|
713
679
|
|
714
|
-
if
|
680
|
+
if(this.track.volume !== undefined)
|
715
681
|
vol *= this.track.volume;
|
716
682
|
|
717
683
|
if (isMuted) vol = 0;
|
@@ -726,12 +692,8 @@
|
|
726
692
|
audio.setVolume(vol * this.director.weight);
|
727
693
|
}
|
728
694
|
else {
|
729
|
-
|
730
|
-
|
731
|
-
if (audio.isPlaying) {
|
732
|
-
audio.stop();
|
733
|
-
}
|
734
|
-
}
|
695
|
+
if (audio.isPlaying)
|
696
|
+
audio.stop();
|
735
697
|
}
|
736
698
|
}
|
737
699
|
this.lastTime = time;
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { ACESFilmicToneMapping, LinearToneMapping, NoToneMapping, ReinhardToneMapping } from "three";
|
2
|
-
|
3
2
|
import { serializable } from "../../../engine/engine_serialization.js";
|
4
3
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
5
4
|
import { VolumeParameter } from "../VolumeParameter.js";
|
@@ -1,11 +1,10 @@
|
|
1
|
-
import {
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
2
|
+
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";
|
2
6
|
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";
|
6
|
-
import { Behaviour, GameObject } from "./Component.js";
|
7
7
|
import { OrbitControls } from "./OrbitControls.js";
|
8
|
-
import { SyncedTransform } from "./SyncedTransform.js";
|
9
8
|
|
10
9
|
export class TransformGizmo extends Behaviour {
|
11
10
|
|
@@ -1,4 +0,0 @@
|
|
1
|
-
|
2
|
-
export interface XRMovementBehaviour {
|
3
|
-
isXRMovementHandler: true;
|
4
|
-
}
|
@@ -1,7 +1,6 @@
|
|
1
1
|
|
2
|
+
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
2
3
|
import { Mesh, Object3D } from "three";
|
3
|
-
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
4
|
-
|
5
4
|
import { getParam } from "../engine_utils.js";
|
6
5
|
|
7
6
|
|
@@ -1,25 +1,24 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
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
|
+
import { Behaviour, GameObject } from "../../Component.js";
|
10
|
+
import { WebXR } from "../../webxr/WebXR.js"
|
11
|
+
import { serializable } from "../../../engine/engine_serialization.js";
|
3
12
|
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
13
|
import { Context } from "../../../engine/engine_setup.js";
|
7
|
-
import { delay, getParam, isiOS, isMobileDevice, isSafari } from "../../../engine/engine_utils.js";
|
8
|
-
import { Behaviour, GameObject } from "../../Component.js";
|
9
|
-
import { Renderer } from "../../Renderer.js"
|
10
14
|
import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
|
11
|
-
import {
|
12
|
-
import {
|
13
|
-
import type { IUSDExporterExtension } from "./Extension.js";
|
14
|
-
import { AnimationExtension } from "./extensions/Animation.js"
|
15
|
+
import { hasProLicense } from "../../../engine/engine_license.js";
|
16
|
+
import { BehaviorExtension } from "./extensions/behavior/Behaviour.js";
|
15
17
|
import { AudioExtension } from "./extensions/behavior/AudioExtension.js";
|
16
|
-
import { BehaviorExtension } from "./extensions/behavior/Behaviour.js";
|
17
18
|
import { TextExtension } from "./extensions/USDZText.js";
|
18
19
|
import { USDZUIExtension } from "./extensions/USDZUI.js";
|
19
|
-
import {
|
20
|
-
import {
|
21
|
-
import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js";
|
22
|
-
import { getFormattedDate } from "./utils/timeutils.js";
|
20
|
+
import { Renderer } from "../../Renderer.js"
|
21
|
+
import { XRFlag, XRState, XRStateFlag } from "../../XRFlag.js";
|
23
22
|
|
24
23
|
const debug = getParam("debugusdz");
|
25
24
|
|
@@ -77,7 +76,7 @@
|
|
77
76
|
extensions: IUSDExporterExtension[] = [];
|
78
77
|
|
79
78
|
private link!: HTMLAnchorElement;
|
80
|
-
private
|
79
|
+
private webxr?: WebXR;
|
81
80
|
|
82
81
|
start() {
|
83
82
|
if (debug) {
|
@@ -115,9 +114,8 @@
|
|
115
114
|
const ios = isiOS()
|
116
115
|
const safari = isSafari();
|
117
116
|
if (debug || (ios && safari)) {
|
118
|
-
if (this.allowCreateQuicklookButton)
|
119
|
-
this.
|
120
|
-
|
117
|
+
if (debug || this.allowCreateQuicklookButton)
|
118
|
+
this.addQuicklookButton();
|
121
119
|
this.lastCallback = this.quicklookCallback.bind(this);
|
122
120
|
this.link = ensureQuicklookLinkIsCreated(this.context);
|
123
121
|
this.link.addEventListener('message', this.lastCallback);
|
@@ -129,13 +127,12 @@
|
|
129
127
|
}
|
130
128
|
|
131
129
|
onDisable() {
|
132
|
-
this.button?.remove();
|
133
130
|
this.link?.removeEventListener('message', this.lastCallback);
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
131
|
+
const ios = isiOS()
|
132
|
+
const safari = isSafari();
|
133
|
+
if (debug || (ios && safari)) {
|
134
|
+
this.removeQuicklookButton();
|
135
|
+
}
|
139
136
|
if (debug)
|
140
137
|
showBalloonMessage("USDZ Exporter disabled: " + this.name);
|
141
138
|
|
@@ -384,7 +381,76 @@
|
|
384
381
|
return obj;
|
385
382
|
}
|
386
383
|
|
387
|
-
|
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
|
+
|
388
454
|
private applyWebARSessionRoot() {
|
389
455
|
if (!this.objectToExport) return;
|
390
456
|
|
@@ -409,20 +475,7 @@
|
|
409
475
|
if (debug) console.log("AR Session Root scale", scale, target);
|
410
476
|
target.matrix.makeScale(scale, scale, scale);
|
411
477
|
if (sessionRoot.invertForward) {
|
412
|
-
target.matrix.multiply(
|
478
|
+
target.matrix.multiply(new Matrix4().makeRotationY(Math.PI));
|
413
479
|
}
|
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?
|
419
480
|
}
|
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
|
-
}
|
428
481
|
}
|
@@ -1,12 +1,11 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import type { IUSDExporterExtension } from "../Extension.js";
|
2
|
+
import type { IBehaviorElement } from "../extensions/behavior/BehavioursBuilder.js";
|
3
|
+
import { USDDocument, USDObject, USDWriter, USDZExporterContext } from "../ThreeUSDZExporter.js";
|
3
4
|
import { GameObject } from "../../../Component.js";
|
5
|
+
import { Text } from "../../../ui/Text.js"
|
4
6
|
import { RectTransform } from "../../../ui/RectTransform.js";
|
5
|
-
import {
|
7
|
+
import { Color, Material, Matrix4, MeshStandardMaterial, Object3D, Vector3 } from "three";
|
6
8
|
import { TextAnchor } from "../../../ui/Text.js";
|
7
|
-
import type { IUSDExporterExtension } from "../Extension.js";
|
8
|
-
import type { IBehaviorElement } from "../extensions/behavior/BehavioursBuilder.js";
|
9
|
-
import { USDDocument, USDObject, USDWriter, USDZExporterContext } from "../ThreeUSDZExporter.js";
|
10
9
|
|
11
10
|
|
12
11
|
export enum TextWrapMode {
|
@@ -1,14 +1,13 @@
|
|
1
|
-
import {
|
2
|
-
|
1
|
+
import type { IUSDExporterExtension } from "../Extension.js";
|
2
|
+
import { USDObject, USDZExporterContext } from "../ThreeUSDZExporter.js";
|
3
3
|
import { GameObject } from "../../../Component.js";
|
4
|
-
import { $shadowDomOwner } from "../../../ui/BaseUIComponent.js";
|
5
4
|
import { Canvas } from "../../../ui/Canvas.js";
|
6
|
-
import { RenderMode } from "../../../ui/Canvas.js";
|
7
5
|
import { CanvasGroup } from "../../../ui/CanvasGroup.js";
|
6
|
+
import { $shadowDomOwner } from "../../../ui/BaseUIComponent.js";
|
8
7
|
import { RectTransform } from "../../../ui/RectTransform.js";
|
9
|
-
import
|
10
|
-
import { USDObject, USDZExporterContext } from "../ThreeUSDZExporter.js";
|
8
|
+
import { Color, Mesh, MeshBasicMaterial, Object3D } from "three";
|
11
9
|
import { TextExtension } from "./USDZText.js";
|
10
|
+
import { RenderMode } from "../../../ui/Canvas.js";
|
12
11
|
|
13
12
|
export class USDZUIExtension implements IUSDExporterExtension {
|
14
13
|
get extensionName(): string {
|
@@ -32,7 +31,7 @@
|
|
32
31
|
height = rt.height;
|
33
32
|
|
34
33
|
const shadowRootModel = USDObject.createEmpty();
|
35
|
-
const shadowComponent = rt.shadowComponent
|
34
|
+
const shadowComponent = rt.shadowComponent;
|
36
35
|
model.add(shadowRootModel);
|
37
36
|
|
38
37
|
if (shadowComponent) {
|
@@ -53,7 +52,7 @@
|
|
53
52
|
childModel.matrix.copy(child.matrix);
|
54
53
|
|
55
54
|
const childParent = child.parent;
|
56
|
-
const isText =
|
55
|
+
const isText = childParent && typeof childParent["textContent"] === "string" && childParent["textContent"].length;
|
57
56
|
let hierarchyOpacity = opacityMap.get(childParent!) || 1;
|
58
57
|
|
59
58
|
// TODO CanvasGroup doesn't render something but modifies opacity
|
@@ -65,11 +64,8 @@
|
|
65
64
|
|
66
65
|
if (child instanceof Mesh && isText) {
|
67
66
|
// get shadoDomOwner so we can export Text from the text extension directly
|
68
|
-
const shadowDomOwner = child[$shadowDomOwner];
|
69
|
-
|
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);
|
67
|
+
const shadowDomOwner = child[$shadowDomOwner].gameObject;
|
68
|
+
textExt.exportText(shadowDomOwner, childModel, _context);
|
73
69
|
}
|
74
70
|
|
75
71
|
if (child instanceof Mesh && !isText)
|
@@ -25,9 +25,6 @@
|
|
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
|
-
|
31
28
|
/** required for @serializable https://github.com/vitejs/vite/issues/13736 */
|
32
29
|
vite44Hack: boolean;
|
33
30
|
|
@@ -1,7 +1,5 @@
|
|
1
1
|
|
2
|
-
import {
|
3
|
-
import ThreeMeshUI from "three-mesh-ui";
|
4
|
-
|
2
|
+
import { FrontSide, DoubleSide, Object3D } from "three"
|
5
3
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
6
4
|
import { Behaviour } from "../Component.js";
|
7
5
|
import { $shadowDomOwner, BaseUIComponent } from "./BaseUIComponent.js";
|
@@ -29,7 +27,7 @@
|
|
29
27
|
receiveShadows?: boolean;
|
30
28
|
}
|
31
29
|
|
32
|
-
export function updateRenderSettings(shadowComponent: Object3D
|
30
|
+
export function updateRenderSettings(shadowComponent: Object3D, settings: RenderSettings) {
|
33
31
|
if (!shadowComponent) return;
|
34
32
|
// const owner = shadowComponent[$shadowDomOwner];
|
35
33
|
// if (!owner)
|
@@ -1,40 +0,0 @@
|
|
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
|
-
}
|
@@ -1,7 +1,6 @@
|
|
1
|
+
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
|
1
2
|
import { Vector3 } from "three";
|
2
|
-
|
3
3
|
import { slerp } from "../../engine/engine_three_utils.js";
|
4
|
-
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
|
5
4
|
|
6
5
|
export function apply(object: Vector3) {
|
7
6
|
if (object && object.isVector3 === true) {
|
@@ -1,14 +1,13 @@
|
|
1
|
+
import { Behaviour, GameObject } from "./Component.js";
|
2
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
1
3
|
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";
|
5
4
|
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";
|
9
5
|
import { getParam } from "../engine/engine_utils.js";
|
10
|
-
import { Behaviour, GameObject } from "./Component.js";
|
11
6
|
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";
|
12
11
|
|
13
12
|
const debug = getParam("debugvideo");
|
14
13
|
|
@@ -185,13 +184,11 @@
|
|
185
184
|
private _isPlaying: boolean = false;
|
186
185
|
private wasPlaying: boolean = false;
|
187
186
|
|
188
|
-
/** ensure's the video
|
189
|
-
|
187
|
+
/** ensure's the video elemnent has been created and will start loading the clip */
|
188
|
+
preload() {
|
190
189
|
if (debug) console.log("Video Preload: " + this.name, this.clip);
|
191
190
|
this.create(false);
|
192
191
|
}
|
193
|
-
/** @deprecated use `preloadVideo()` */
|
194
|
-
preload() { this.preloadVideo(); }
|
195
192
|
|
196
193
|
/** Set a new video stream
|
197
194
|
* starts to play automatically if the videoplayer hasnt been active before and playOnAwake is true */
|
@@ -237,7 +234,7 @@
|
|
237
234
|
this.create(true);
|
238
235
|
}
|
239
236
|
else {
|
240
|
-
this.
|
237
|
+
this.preload();
|
241
238
|
}
|
242
239
|
|
243
240
|
if (this.screenspace) {
|
@@ -1,9 +1,8 @@
|
|
1
|
-
import { VignetteEffect } from "postprocessing";
|
2
|
-
|
3
1
|
import { serializable } from "../../../engine/engine_serialization.js";
|
2
|
+
import { VolumeParameter } from "../VolumeParameter.js";
|
4
3
|
import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
|
5
|
-
import { VolumeParameter } from "../VolumeParameter.js";
|
6
4
|
import { registerCustomEffectType } from "../VolumeProfile.js";
|
5
|
+
import { VignetteEffect } from "postprocessing";
|
7
6
|
|
8
7
|
|
9
8
|
export class Vignette extends PostProcessingEffect {
|
@@ -1,12 +1,11 @@
|
|
1
|
-
import {
|
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"
|
1
|
+
import { Behaviour } from "./Component.js";
|
2
|
+
import { StreamEndedEvent, NetworkedStreamEvents, NetworkedStreams, StreamReceivedEvent, disposeStream } from "../engine/engine_networking_streams.js"
|
6
3
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
7
4
|
import { getParam, microphonePermissionsGranted } from "../engine/engine_utils.js";
|
5
|
+
import { RoomEvents } from "../engine/engine_networking.js";
|
8
6
|
import { delay } from "../engine/engine_utils.js";
|
9
|
-
import {
|
7
|
+
import { isDevEnvironment, showBalloonError, showBalloonWarning } from "../engine/debug/index.js";
|
8
|
+
import { AudioAnalyser } from "three";
|
10
9
|
|
11
10
|
export const noVoip = "noVoip";
|
12
11
|
const debugParam = getParam("debugvoip");
|
@@ -1,14 +1,13 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import { isDevEnvironment } from "../../engine/debug/index.js";
|
4
|
-
import type { EditorModification, IEditorModification as IEditorModificationReceiver } from "../../engine/engine_editor-sync.js";
|
1
|
+
import { Behaviour } from "../Component.js";
|
5
2
|
import { serializeable } from "../../engine/engine_serialization_decorator.js";
|
6
3
|
import { getParam } from "../../engine/engine_utils.js";
|
7
|
-
import {
|
4
|
+
import { VolumeProfile } from "./VolumeProfile.js";
|
5
|
+
import type { EditorModification, IEditorModification as IEditorModificationReceiver } from "../../engine/engine_editor-sync.js";
|
6
|
+
import { PostProcessingHandler } from "./PostProcessingHandler.js";
|
8
7
|
import { PostProcessingEffect } from "./PostProcessingEffect.js";
|
9
|
-
import { PostProcessingHandler } from "./PostProcessingHandler.js";
|
10
8
|
import { VolumeParameter } from "./VolumeParameter.js";
|
11
|
-
import {
|
9
|
+
import { isDevEnvironment } from "../../engine/debug/index.js";
|
10
|
+
import { EffectComposer } from "postprocessing";
|
12
11
|
|
13
12
|
const debug = getParam("debugpost");
|
14
13
|
|
@@ -24,109 +24,102 @@
|
|
24
24
|
return (obj || new VrUserStateBuffer()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
25
25
|
}
|
26
26
|
|
27
|
+
guid():string|null
|
28
|
+
guid(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
29
|
+
guid(optionalEncoding?:any):string|Uint8Array|null {
|
30
|
+
const offset = this.bb!.__offset(this.bb_pos, 4);
|
31
|
+
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
32
|
+
}
|
33
|
+
|
27
34
|
time():flatbuffers.Long {
|
28
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
35
|
+
const offset = this.bb!.__offset(this.bb_pos, 6);
|
29
36
|
return offset ? this.bb!.readInt64(this.bb_pos + offset) : this.bb!.createLong(0, 0);
|
30
37
|
}
|
31
38
|
|
32
39
|
avatarId():string|null
|
33
40
|
avatarId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
34
41
|
avatarId(optionalEncoding?:any):string|Uint8Array|null {
|
35
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
42
|
+
const offset = this.bb!.__offset(this.bb_pos, 8);
|
36
43
|
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
37
44
|
}
|
38
45
|
|
39
46
|
position(obj?:Vec3):Vec3|null {
|
40
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
47
|
+
const offset = this.bb!.__offset(this.bb_pos, 10);
|
41
48
|
return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
|
42
49
|
}
|
43
50
|
|
44
51
|
rotation(obj?:Vec4):Vec4|null {
|
45
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
52
|
+
const offset = this.bb!.__offset(this.bb_pos, 12);
|
46
53
|
return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
|
47
54
|
}
|
48
55
|
|
49
56
|
scale():number {
|
50
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
57
|
+
const offset = this.bb!.__offset(this.bb_pos, 14);
|
51
58
|
return offset ? this.bb!.readFloat32(this.bb_pos + offset) : 0.0;
|
52
59
|
}
|
53
60
|
|
54
|
-
|
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 {
|
61
|
+
posLeftHand(obj?:Vec3):Vec3|null {
|
60
62
|
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
|
-
|
64
|
-
posLeftHand(obj?:Vec3):Vec3|null {
|
65
|
-
const offset = this.bb!.__offset(this.bb_pos, 18);
|
66
63
|
return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
|
67
64
|
}
|
68
65
|
|
69
66
|
posRightHand(obj?:Vec3):Vec3|null {
|
70
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
67
|
+
const offset = this.bb!.__offset(this.bb_pos, 18);
|
71
68
|
return offset ? (obj || new Vec3()).__init(this.bb_pos + offset, this.bb!) : null;
|
72
69
|
}
|
73
70
|
|
74
71
|
rotLeftHand(obj?:Vec4):Vec4|null {
|
75
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
72
|
+
const offset = this.bb!.__offset(this.bb_pos, 20);
|
76
73
|
return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
|
77
74
|
}
|
78
75
|
|
79
76
|
rotRightHand(obj?:Vec4):Vec4|null {
|
80
|
-
const offset = this.bb!.__offset(this.bb_pos,
|
77
|
+
const offset = this.bb!.__offset(this.bb_pos, 22);
|
81
78
|
return offset ? (obj || new Vec4()).__init(this.bb_pos + offset, this.bb!) : null;
|
82
79
|
}
|
83
80
|
|
84
81
|
static startVrUserStateBuffer(builder:flatbuffers.Builder) {
|
85
|
-
builder.startObject(
|
82
|
+
builder.startObject(10);
|
86
83
|
}
|
87
84
|
|
85
|
+
static addGuid(builder:flatbuffers.Builder, guidOffset:flatbuffers.Offset) {
|
86
|
+
builder.addFieldOffset(0, guidOffset, 0);
|
87
|
+
}
|
88
|
+
|
88
89
|
static addTime(builder:flatbuffers.Builder, time:flatbuffers.Long) {
|
89
|
-
builder.addFieldInt64(
|
90
|
+
builder.addFieldInt64(1, time, builder.createLong(0, 0));
|
90
91
|
}
|
91
92
|
|
92
93
|
static addAvatarId(builder:flatbuffers.Builder, avatarIdOffset:flatbuffers.Offset) {
|
93
|
-
builder.addFieldOffset(
|
94
|
+
builder.addFieldOffset(2, avatarIdOffset, 0);
|
94
95
|
}
|
95
96
|
|
96
97
|
static addPosition(builder:flatbuffers.Builder, positionOffset:flatbuffers.Offset) {
|
97
|
-
builder.addFieldStruct(
|
98
|
+
builder.addFieldStruct(3, positionOffset, 0);
|
98
99
|
}
|
99
100
|
|
100
101
|
static addRotation(builder:flatbuffers.Builder, rotationOffset:flatbuffers.Offset) {
|
101
|
-
builder.addFieldStruct(
|
102
|
+
builder.addFieldStruct(4, rotationOffset, 0);
|
102
103
|
}
|
103
104
|
|
104
105
|
static addScale(builder:flatbuffers.Builder, scale:number) {
|
105
|
-
builder.addFieldFloat32(
|
106
|
+
builder.addFieldFloat32(5, scale, 0.0);
|
106
107
|
}
|
107
108
|
|
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
|
-
|
116
109
|
static addPosLeftHand(builder:flatbuffers.Builder, posLeftHandOffset:flatbuffers.Offset) {
|
117
|
-
builder.addFieldStruct(
|
110
|
+
builder.addFieldStruct(6, posLeftHandOffset, 0);
|
118
111
|
}
|
119
112
|
|
120
113
|
static addPosRightHand(builder:flatbuffers.Builder, posRightHandOffset:flatbuffers.Offset) {
|
121
|
-
builder.addFieldStruct(
|
114
|
+
builder.addFieldStruct(7, posRightHandOffset, 0);
|
122
115
|
}
|
123
116
|
|
124
117
|
static addRotLeftHand(builder:flatbuffers.Builder, rotLeftHandOffset:flatbuffers.Offset) {
|
125
|
-
builder.addFieldStruct(
|
118
|
+
builder.addFieldStruct(8, rotLeftHandOffset, 0);
|
126
119
|
}
|
127
120
|
|
128
121
|
static addRotRightHand(builder:flatbuffers.Builder, rotRightHandOffset:flatbuffers.Offset) {
|
129
|
-
builder.addFieldStruct(
|
122
|
+
builder.addFieldStruct(9, rotRightHandOffset, 0);
|
130
123
|
}
|
131
124
|
|
132
125
|
static endVrUserStateBuffer(builder:flatbuffers.Builder):flatbuffers.Offset {
|
@@ -1,54 +1,49 @@
|
|
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";
|
1
5
|
import {
|
2
|
-
|
6
|
+
Scene,
|
7
|
+
Texture,
|
3
8
|
Mesh, MeshBasicMaterial,
|
4
|
-
|
9
|
+
UniformsUtils,
|
5
10
|
PlaneGeometry,
|
6
|
-
Scene,
|
7
11
|
ShaderLib,
|
8
12
|
ShaderMaterial,
|
9
|
-
|
10
|
-
|
13
|
+
DoubleSide,
|
14
|
+
PerspectiveCamera,
|
11
15
|
} from "three";
|
12
16
|
|
13
|
-
|
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"
|
17
|
+
export class WebARCameraBackground extends Behaviour {
|
18
18
|
|
19
|
-
|
19
|
+
awake(): void {
|
20
|
+
WebXR.OptionalFeatures_AR.push('camera-access');
|
21
|
+
}
|
20
22
|
|
21
|
-
|
23
|
+
@serializable()
|
24
|
+
public backgroundTint: RGBAColor = new RGBAColor(1,1,1,1);
|
22
25
|
|
23
|
-
|
24
|
-
|
25
|
-
args.optionalFeatures.push('camera-access');
|
26
|
-
|
27
|
-
if (debug) console.warn("Requesting camera-access");
|
26
|
+
public get background() {
|
27
|
+
return this.backgroundPlane;
|
28
28
|
}
|
29
29
|
|
30
|
-
|
30
|
+
private _preRender;
|
31
|
+
|
32
|
+
onEnable(): void {
|
33
|
+
this._preRender = this.preRender.bind(this);
|
34
|
+
this.context.pre_render_callbacks.push(this._preRender);
|
35
|
+
|
31
36
|
if (this.backgroundPlane) {
|
32
|
-
this.
|
37
|
+
this.gameObject.add(this.backgroundPlane);
|
33
38
|
this.backgroundPlane.visible = false;
|
34
39
|
}
|
35
|
-
|
36
|
-
if (this.backgroundPlane) this.context.scene.add(this.backgroundPlane);
|
37
|
-
this.context.pre_render_callbacks.push(this.preRender);
|
38
40
|
}
|
39
41
|
|
40
|
-
|
41
|
-
|
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
|
-
}
|
42
|
+
onDisable(): void {
|
43
|
+
this.context.pre_render_callbacks = this.context.pre_render_callbacks.filter(cb => cb !== this._preRender);
|
46
44
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
public get background() {
|
51
|
-
return this.backgroundPlane;
|
45
|
+
if (this.backgroundPlane)
|
46
|
+
this.gameObject.remove(this.backgroundPlane);
|
52
47
|
}
|
53
48
|
|
54
49
|
private backgroundPlane?: Mesh;
|
@@ -63,13 +58,11 @@
|
|
63
58
|
return function forceTextureInitialization(renderer, texture) {
|
64
59
|
material.map = texture;
|
65
60
|
renderer.render(scene, camera);
|
66
|
-
if (debug) console.warn("Force texture initialization");
|
67
61
|
};
|
68
62
|
}();
|
69
63
|
|
70
|
-
|
71
|
-
|
72
|
-
private preRender = () => {
|
64
|
+
// TODO should only attach on session start, and detach on session end
|
65
|
+
private preRender() {
|
73
66
|
if (!this || !this.gameObject) return;
|
74
67
|
|
75
68
|
const xr = this.context.renderer.xr;
|
@@ -88,14 +81,19 @@
|
|
88
81
|
// from three: WebGLBackground
|
89
82
|
if (this.backgroundPlane === undefined) {
|
90
83
|
this.backgroundPlane = makeFullscreenPlane(this.backgroundTint);
|
84
|
+
this.gameObject.add(this.backgroundPlane);
|
91
85
|
}
|
92
|
-
if(this.backgroundPlane.parent !== this.scene)
|
93
|
-
this.scene.add(this.backgroundPlane);
|
94
86
|
|
95
87
|
// WebXR Raw Camera Access -
|
96
88
|
// we composite the camera texture into the scene background by rendering it first.
|
97
89
|
this.updateFromFrame(frame);
|
98
90
|
}
|
91
|
+
|
92
|
+
/*
|
93
|
+
if (this.planeMesh) {
|
94
|
+
this.planeMesh.visible = frame != null;
|
95
|
+
}
|
96
|
+
*/
|
99
97
|
}
|
100
98
|
|
101
99
|
onBeforeRender(frame: XRFrame | null) {
|
@@ -133,9 +131,17 @@
|
|
133
131
|
this.backgroundPlane.setTexture(this.threeTexture);
|
134
132
|
this.backgroundPlane.visible = true;
|
135
133
|
}
|
136
|
-
|
137
|
-
|
134
|
+
|
135
|
+
// TODO this would be a lot better but currently
|
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;
|
138
143
|
}
|
144
|
+
*/
|
139
145
|
}
|
140
146
|
}
|
141
147
|
else {
|
@@ -169,14 +175,15 @@
|
|
169
175
|
gl_FragColor = texColor * <backgroundTint>;
|
170
176
|
|
171
177
|
#include <tonemapping_fragment>
|
172
|
-
#include <
|
178
|
+
#include <encodings_fragment>
|
179
|
+
|
173
180
|
}
|
174
181
|
`;
|
175
182
|
|
176
183
|
// not sure where we want to move this and in which form is best (extends Object3D?)
|
177
184
|
export function makeFullscreenPlane(tint: RGBAColor ) {
|
178
185
|
const replacementTint = "vec4(" + tint.r.toFixed(3) + "," + tint.g.toFixed(3) + "," + tint.b.toFixed(3) + "," + tint.a.toFixed(3) + ")";
|
179
|
-
|
186
|
+
console.log(replacementTint);
|
180
187
|
const planeMesh = new Mesh(
|
181
188
|
new PlaneGeometry(2, 2),
|
182
189
|
// @ts-ignore
|
@@ -184,7 +191,7 @@
|
|
184
191
|
name: 'BackgroundMaterial',
|
185
192
|
uniforms: UniformsUtils.clone( ShaderLib.background.uniforms ),
|
186
193
|
vertexShader: ShaderLib.background.vertexShader,
|
187
|
-
fragmentShader: backgroundFragment.
|
194
|
+
fragmentShader: backgroundFragment.replace("<backgroundTint>", replacementTint), // ShaderLib.background.fragmentShader,
|
188
195
|
side: DoubleSide,
|
189
196
|
depthTest: false,
|
190
197
|
depthWrite: false,
|
@@ -204,8 +211,8 @@
|
|
204
211
|
// Option 1: add the planeMesh to our scene for rendering.
|
205
212
|
// This is useful for applying custom shader effects on the background (instead of using the system composite)
|
206
213
|
planeMesh.renderOrder = -10000; // render first
|
207
|
-
|
208
|
-
planeMesh.layers.
|
214
|
+
planeMesh.layers.disableAll();
|
215
|
+
planeMesh.layers.enable(2); // ignore raycasts
|
209
216
|
planeMesh.frustumCulled = false;
|
210
217
|
|
211
218
|
// should be a class, for now lets just define a method for the weird way the texture needs to be set
|
@@ -1,424 +1,224 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import {
|
4
|
-
import {
|
1
|
+
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";
|
5
6
|
import { Context } from "../../engine/engine_context.js";
|
6
|
-
import {
|
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";
|
13
|
-
import { Behaviour, GameObject } from "../Component.js";
|
7
|
+
import { isQuest } from "../../engine/engine_utils.js";
|
14
8
|
|
15
9
|
// https://github.com/takahirox/takahirox.github.io/blob/master/js.mmdeditor/examples/js/controls/DeviceOrientationControls.js
|
16
10
|
|
17
|
-
const
|
11
|
+
const tempMatrix = new Matrix4();
|
18
12
|
|
19
|
-
|
13
|
+
export class WebARSessionRoot extends Behaviour {
|
20
14
|
|
21
|
-
|
15
|
+
webAR: WebAR | null = null;
|
22
16
|
|
23
|
-
|
17
|
+
get rig(): Object3D | undefined {
|
18
|
+
return this.webAR?.webxr.Rig;
|
19
|
+
}
|
24
20
|
|
25
|
-
/** The scale of a user in AR:
|
26
|
-
* a large value makes the scene appear smaller
|
27
|
-
* default is 1
|
28
|
-
*/
|
29
21
|
@serializable()
|
22
|
+
invertForward: boolean = false;
|
23
|
+
|
24
|
+
/** Preview feature: enable touch transform */
|
25
|
+
@serializable()
|
26
|
+
arTouchTransform: boolean = false;
|
27
|
+
|
28
|
+
@serializable()
|
30
29
|
get arScale(): number {
|
31
30
|
return this._arScale;
|
32
31
|
}
|
33
32
|
set arScale(val: number) {
|
34
33
|
if (val === this._arScale) return;
|
35
34
|
this._arScale = val;
|
36
|
-
this.
|
35
|
+
this.setScale(val);
|
37
36
|
}
|
38
|
-
private _arScale: number = 1;
|
39
37
|
|
40
|
-
|
41
|
-
|
42
|
-
|
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 */
|
38
|
+
private readonly _initalMatrix = new Matrix4();
|
39
|
+
private readonly _selectStartFn = this.onSelectStart.bind(this);
|
40
|
+
private readonly _selectEndFn = this.onSelectEnd.bind(this);
|
83
41
|
private userInput?: WebXRSessionRootUserInput;
|
84
42
|
|
85
|
-
|
86
|
-
|
43
|
+
start() {
|
44
|
+
const xr = GameObject.findObjectOfType(WebXR);
|
45
|
+
if (xr) {
|
46
|
+
xr.Rig.updateMatrix();
|
47
|
+
this._initalMatrix.copy(xr.Rig.matrix);
|
48
|
+
}
|
87
49
|
}
|
88
50
|
|
89
|
-
|
90
|
-
|
91
|
-
|
51
|
+
private _arScale: number = 1;
|
52
|
+
private _rig: Object3D | null = null;
|
53
|
+
private _startPose: Matrix4 | null = null;
|
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;
|
92
59
|
|
93
|
-
|
94
|
-
if (debug) console.log("ENTER WEBXR: SessionRoot start...");
|
60
|
+
onBegin(session: XRSession) {
|
95
61
|
|
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;
|
96
68
|
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
|
97
72
|
|
98
|
-
//
|
99
|
-
|
100
|
-
// return;
|
101
|
-
// }
|
73
|
+
// console.log(this.rig?.position, this.rig?.quaternion, this.rig?.scale);
|
74
|
+
this.gameObject.visible = false;
|
102
75
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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);
|
76
|
+
if (this.rig) {
|
77
|
+
// reset rig to initial pose, this is helping the mix of immersive AR and immersive VR that we now have on quest
|
78
|
+
// where the rig can be moved and scaled by the user in VR mode and we use the rig position when entering
|
79
|
+
// immersive Ar right now to place the user/offset the session
|
80
|
+
this.rig.matrixAutoUpdate = true;
|
81
|
+
this._initalMatrix.decompose(this.rig.position, this.rig.quaternion, this.rig.scale);
|
117
82
|
}
|
118
|
-
this.context.scene.add(rootObject);
|
119
83
|
|
120
|
-
//
|
121
|
-
|
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);
|
84
|
+
// TODO this is duplicate to WebXR events AND engine events, would be better in one place
|
85
|
+
this.dispatchEvent(new CustomEvent('onBeginSession'));
|
139
86
|
}
|
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 {
|
149
87
|
|
150
|
-
|
151
|
-
if (args.xr.isTrackingImages) {
|
152
|
-
for (const ret of this._reticle)
|
153
|
-
ret.visible = false;
|
154
|
-
return;
|
155
|
-
}
|
88
|
+
onUpdate(rig: Object3D | null, _session: XRSession, hit: XRHitTestResult | null, pose: XRPose | null | undefined): boolean {
|
156
89
|
|
157
|
-
if (this.
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
if (rigObject && rigObject.parent !== this.context.scene) {
|
162
|
-
this.context.scene.add(rigObject);
|
90
|
+
if (pose && !this._placementPose) {
|
91
|
+
if (!this._gotFirstHitTestResult) {
|
92
|
+
this._gotFirstHitTestResult = true;
|
93
|
+
this.dispatchEvent(new CustomEvent('canPlaceSession', { detail: { pose: pose } }));
|
163
94
|
}
|
164
95
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
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
|
-
}
|
96
|
+
if (this._isTouching) {
|
97
|
+
// callbacks
|
98
|
+
const poseMatrix = new Matrix4().fromArray(pose.transform.matrix).invert();
|
99
|
+
this.dispatchEvent(new CustomEvent('placedSession', { detail: { pose, poseMatrix } }));
|
186
100
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
}
|
101
|
+
if (this.webAR) this.webAR.setReticleActive(false);
|
102
|
+
this.placeAt(rig, poseMatrix);
|
103
|
+
if (hit && pose && !isQuest()) // TODO anchors seem to behave differently with an XRRig
|
104
|
+
this.onCreatePlacementAnchor(hit, pose);
|
202
105
|
|
203
|
-
|
204
|
-
if (this.arTouchTransform) {
|
205
|
-
if (!this.userInput) this.userInput = new WebXRSessionRootUserInput(this.context);
|
206
|
-
this.userInput?.enable();
|
106
|
+
return true;
|
207
107
|
}
|
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
|
-
}
|
219
108
|
}
|
109
|
+
return false;
|
220
110
|
}
|
221
111
|
|
222
|
-
private
|
223
|
-
|
224
|
-
|
112
|
+
private onCreatePlacementAnchor(hit: XRHitTestResult, pose: XRPose) {
|
113
|
+
this._anchor = null;
|
114
|
+
hit?.createAnchor?.call(hit, pose.transform)?.then(anchor => {
|
115
|
+
if (this.context.isInAR)
|
116
|
+
this._anchor = anchor;
|
117
|
+
}).catch(ex => {
|
118
|
+
console.warn("Failed to create anchor", ex);
|
119
|
+
});
|
120
|
+
}
|
225
121
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
122
|
+
private _anchorMatrix: Matrix4 = new Matrix4();
|
123
|
+
|
124
|
+
onBeforeRender(frame: XRFrame | null): void {
|
125
|
+
if (frame && this._rig) {
|
126
|
+
if (this._anchor) {
|
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
|
+
}
|
231
135
|
}
|
232
|
-
else {
|
233
|
-
this.customReticle.loadAssetAsync();
|
234
|
-
return;
|
235
|
-
}
|
236
136
|
}
|
237
|
-
else {
|
238
|
-
|
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";
|
137
|
+
else if (this._placementPose) {
|
138
|
+
this.placeAt(this._rig, this._placementPose!);
|
243
139
|
}
|
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;
|
252
140
|
}
|
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
|
-
}
|
274
141
|
}
|
275
142
|
|
276
|
-
private
|
277
|
-
|
143
|
+
private _invertedSessionRootMatrix: Matrix4 = new Matrix4();
|
144
|
+
private _invertForwardMatrix: Matrix4 = new Matrix4().makeRotationY(Math.PI);
|
278
145
|
|
279
|
-
|
280
|
-
|
146
|
+
placeAt(rig: Object3D | null, mat: Matrix4) {
|
147
|
+
if (!this._placementPose) this._placementPose = new Matrix4();
|
148
|
+
this._placementPose.copy(mat);
|
281
149
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
150
|
+
// apply session root offset
|
151
|
+
const invertedSessionRoot = this._invertedSessionRootMatrix.copy(this.gameObject.matrixWorld).invert();
|
152
|
+
this._placementPose.premultiply(invertedSessionRoot);
|
153
|
+
if (rig) {
|
154
|
+
if (this.invertForward) {
|
155
|
+
this._placementPose.premultiply(this._invertForwardMatrix);
|
288
156
|
}
|
289
|
-
}
|
290
157
|
|
291
|
-
|
292
|
-
|
293
|
-
return;
|
294
|
-
}
|
158
|
+
if (!this.userInput) this.userInput = new WebXRSessionRootUserInput(this.context);
|
159
|
+
this.userInput.enable();
|
295
160
|
|
296
|
-
|
297
|
-
|
298
|
-
return;
|
161
|
+
this._rig = rig;
|
162
|
+
this.setScale(this.arScale);
|
299
163
|
}
|
164
|
+
else this._rig = null;
|
165
|
+
this.gameObject.visible = true;
|
166
|
+
}
|
300
167
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
}
|
168
|
+
onEnd(rig: Object3D | null, _session: XRSession) {
|
169
|
+
this.userInput?.disable();
|
170
|
+
this.userInput?.reset();
|
305
171
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
this.
|
310
|
-
|
311
|
-
|
312
|
-
this.onRevertSceneChanges();
|
313
|
-
|
314
|
-
this.onApplyPose(reticle);
|
315
|
-
|
316
|
-
if (this.useXRAnchor) {
|
317
|
-
this.onCreateAnchor(NeedleXRSession.active!, hit);
|
172
|
+
this._placementPose = null;
|
173
|
+
this.gameObject.visible = false;
|
174
|
+
this.gameObject.matrixAutoUpdate = false;
|
175
|
+
this._anchor = null;
|
176
|
+
if (this._startPose) {
|
177
|
+
this.gameObject.matrix.copy(this._startPose);
|
318
178
|
}
|
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);
|
319
193
|
}
|
320
194
|
|
321
|
-
|
322
|
-
|
195
|
+
|
196
|
+
private onSelectStart() {
|
197
|
+
this._isTouching = true;
|
323
198
|
}
|
324
199
|
|
325
|
-
private
|
326
|
-
|
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
|
-
}
|
200
|
+
private onSelectEnd() {
|
201
|
+
this._isTouching = false;
|
342
202
|
}
|
343
203
|
|
344
|
-
private
|
345
|
-
|
346
|
-
|
347
|
-
if (isDevEnvironment()) showBalloonWarning("Hit does not support creating an anchor");
|
204
|
+
private setScale(scale) {
|
205
|
+
const rig = this._rig;
|
206
|
+
if (!rig || !this._placementPose) {
|
348
207
|
return;
|
349
208
|
}
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
if (session.running && anchor) {
|
354
|
-
this._anchor = anchor;
|
355
|
-
}
|
209
|
+
// Capture the rig position before the first time we move it during a session
|
210
|
+
if (!this._rigStartPose) {
|
211
|
+
this._rigStartPose = rig.matrix.clone();
|
356
212
|
}
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
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);
|
213
|
+
// we apply the transform to the rig because we want to move the user's position for easy networking
|
214
|
+
rig.matrixAutoUpdate = false;
|
215
|
+
if (this.arTouchTransform && this.userInput) {
|
216
|
+
this.userInput.applyMatrixTo(this._placementPose);
|
217
|
+
// rig.matrix.premultiply(this.userInput.offset);
|
421
218
|
}
|
219
|
+
rig.matrix.multiplyMatrices(tempMatrix.makeScale(scale, scale, scale), this._placementPose);
|
220
|
+
rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
|
221
|
+
rig.updateMatrixWorld();
|
422
222
|
}
|
423
223
|
}
|
424
224
|
|
@@ -434,14 +234,11 @@
|
|
434
234
|
twoFingerRotate: boolean = true;
|
435
235
|
twoFingerScale: boolean = true;
|
436
236
|
|
437
|
-
factor: number = 1;
|
438
|
-
|
439
237
|
readonly context: Context;
|
440
238
|
readonly offset: Matrix4;
|
441
239
|
readonly plane: Plane;
|
442
240
|
|
443
241
|
private _scale: number = 1;
|
444
|
-
private _hasChanged: boolean = false;
|
445
242
|
|
446
243
|
// readonly translate: Vector3 = new Vector3();
|
447
244
|
// readonly rotation: Quaternion = new Quaternion();
|
@@ -473,21 +270,8 @@
|
|
473
270
|
this._scale = 1;
|
474
271
|
this.offset.identity();
|
475
272
|
}
|
476
|
-
|
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);
|
273
|
+
applyMatrixTo(matrix: Matrix4) {
|
274
|
+
matrix.premultiply(this.offset);
|
491
275
|
// if (this._needsUpdate)
|
492
276
|
// this.updateMatrix();
|
493
277
|
// matrix.premultiply(this._rotationMatrix);
|
@@ -540,7 +324,7 @@
|
|
540
324
|
}
|
541
325
|
private touchMove = (evt: TouchEvent) => {
|
542
326
|
if (evt.defaultPrevented) return;
|
543
|
-
|
327
|
+
|
544
328
|
if (evt.touches.length === 1) {
|
545
329
|
// if we had multiple touches before due to e.g. pinching / rotating
|
546
330
|
// and stopping one of the touches, we don't want to move the scene suddenly
|
@@ -621,26 +405,21 @@
|
|
621
405
|
// this.translate.z -= dz;
|
622
406
|
// this._needsUpdate = true;
|
623
407
|
// return
|
624
|
-
|
408
|
+
// some arbitrary factor
|
409
|
+
dx *= .75;
|
410
|
+
dz *= .75;
|
625
411
|
// increase diff if the scene is scaled small
|
626
412
|
dx /= this._scale;
|
627
413
|
dz /= this._scale;
|
628
|
-
|
629
|
-
dx *= this.factor;
|
630
|
-
dz *= this.factor;
|
631
|
-
|
632
414
|
// apply it
|
633
|
-
this.offset.elements[12]
|
634
|
-
this.offset.elements[14]
|
635
|
-
if (dx !== 0 || dz !== 0)
|
636
|
-
this._hasChanged = true;
|
415
|
+
this.offset.elements[12] -= dx;
|
416
|
+
this.offset.elements[14] -= dz;
|
637
417
|
};
|
638
418
|
|
639
419
|
private readonly _tempMatrix: Matrix4 = new Matrix4();
|
640
420
|
|
641
421
|
private addScale(diff: number) {
|
642
422
|
diff /= window.innerWidth
|
643
|
-
diff *= -1;
|
644
423
|
|
645
424
|
// this.scale.x *= 1 + diff;
|
646
425
|
// this.scale.y *= 1 + diff;
|
@@ -654,19 +433,14 @@
|
|
654
433
|
// apply the scale
|
655
434
|
this._tempMatrix.makeScale(1 - diff, 1 - diff, 1 - diff);
|
656
435
|
this.offset.premultiply(this._tempMatrix);
|
657
|
-
if (diff !== 0)
|
658
|
-
this._hasChanged = true;
|
659
436
|
}
|
660
437
|
|
661
438
|
|
662
439
|
private addRotation(rot: number) {
|
663
|
-
rot *= -1;
|
664
440
|
// this.rotation.multiply(new Quaternion().setFromAxisAngle(WebXRSessionRootUserInput.up, rot));
|
665
441
|
// this._needsUpdate = true;
|
666
442
|
// return;
|
667
443
|
this._tempMatrix.makeRotationY(rot);
|
668
444
|
this.offset.premultiply(this._tempMatrix);
|
669
|
-
if (rot !== 0)
|
670
|
-
this._hasChanged = true;
|
671
445
|
}
|
672
446
|
}
|
@@ -1,301 +1,762 @@
|
|
1
|
-
import { Object3D } from
|
1
|
+
import { Color, Euler, EventDispatcher, Group, Matrix4, Mesh, MeshBasicMaterial, Object3D, Quaternion, RingGeometry, Texture, Vector3, type WebXRArrayCamera } from 'three';
|
2
|
+
import { ARButton } from '../../include/three/ARButton.js';
|
3
|
+
import { VRButton } from '../../include/three/VRButton.js';
|
2
4
|
|
3
|
-
import { showBalloonWarning } from "../../engine/debug/index.js";
|
4
5
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
5
|
-
import { serializable } from "../../engine/
|
6
|
-
import {
|
7
|
-
import {
|
8
|
-
import {
|
6
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
7
|
+
import { XRSessionMode } from "../../engine/engine_setup.js";
|
8
|
+
import { getWorldPosition, getWorldQuaternion, setWorldPosition, setWorldQuaternion } from "../../engine/engine_three_utils.js";
|
9
|
+
import type { INeedleEngineComponent } from "../../engine/engine_types.js";
|
10
|
+
import { getParam, isMozillaXR, isQuest, setOrAddParamsToUrl } from "../../engine/engine_utils.js";
|
11
|
+
|
9
12
|
import { Behaviour, GameObject } from "../Component.js";
|
10
|
-
import {
|
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";
|
13
|
+
import { noVoip } from "../Voip.js";
|
15
14
|
import { WebARSessionRoot } from "./WebARSessionRoot.js";
|
16
|
-
import {
|
17
|
-
import {
|
15
|
+
import { ControllerType, WebXRController } from "./WebXRController.js";
|
16
|
+
import { XRRig } from "./WebXRRig.js";
|
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';
|
18
21
|
|
19
|
-
const
|
20
|
-
const debugQuicklook = getParam("debugusdz");
|
22
|
+
const debugWebXR = getParam("debugwebxr");
|
21
23
|
|
22
|
-
export
|
24
|
+
export async function detectARSupport() {
|
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
|
+
}
|
23
39
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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;
|
40
|
+
let arSupported = false;
|
41
|
+
let vrSupported = false;
|
42
|
+
detectARSupport().then(res => arSupported = res);
|
43
|
+
detectVRSupport().then(res => vrSupported = res);
|
33
44
|
|
34
|
-
|
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;
|
45
|
+
// import TeleportVR from "teleportvr.js";
|
41
46
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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;
|
47
|
+
export enum WebXREvent {
|
48
|
+
XRStarted = "xrStarted",
|
49
|
+
XRStopped = "xrStopped",
|
50
|
+
XRUpdate = "xrUpdate",
|
51
|
+
RequestVRSession = "requestVRSession",
|
52
|
+
ModifyAROptions = "modify-ar-options",
|
53
|
+
}
|
51
54
|
|
52
|
-
|
53
|
-
|
55
|
+
export declare type CreateButtonOptions = {
|
56
|
+
registerClick: boolean
|
57
|
+
};
|
54
58
|
|
59
|
+
export class WebXR extends Behaviour {
|
55
60
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
61
|
+
@serializable()
|
62
|
+
enableVR = true;
|
63
|
+
@serializable()
|
64
|
+
enableAR = true;
|
60
65
|
|
61
|
-
|
62
|
-
/** This avatar representation will be spawned when you enter a webxr session */
|
63
66
|
@serializable(AssetReference)
|
64
67
|
defaultAvatar?: AssetReference;
|
68
|
+
@serializable()
|
69
|
+
handModelPath: string = "";
|
65
70
|
|
66
|
-
|
67
|
-
|
68
|
-
|
71
|
+
@serializable()
|
72
|
+
createVRButton: boolean = true;
|
73
|
+
@serializable()
|
74
|
+
createARButton: boolean = true;
|
69
75
|
|
70
|
-
private
|
76
|
+
private static _isInXr: boolean = false;
|
77
|
+
private static events: EventDispatcher = new EventDispatcher();
|
71
78
|
|
72
|
-
|
73
|
-
|
74
|
-
|
79
|
+
public static get IsInWebXR(): boolean { return this._isInXr; }
|
80
|
+
public static get XRSupported(): boolean { return 'xr' in navigator && (arSupported || vrSupported); }
|
81
|
+
public static get IsARSupported(): boolean { return arSupported; }
|
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;
|
75
92
|
}
|
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
|
+
}
|
76
100
|
|
77
|
-
|
78
|
-
|
79
|
-
|
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).");
|
101
|
+
public static createVRButton(webXR: WebXR, opts?: CreateButtonOptions): HTMLButtonElement | HTMLAnchorElement {
|
102
|
+
if (!WebXR.XRSupported) {
|
103
|
+
console.warn("WebXR is not supported on this device");
|
81
104
|
}
|
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
|
+
}
|
82
117
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
118
|
+
public static createARButton(webXR: WebXR, opts?: CreateButtonOptions): HTMLButtonElement | HTMLAnchorElement {
|
119
|
+
webXR.__internalAwake();
|
120
|
+
const domOverlayRoot = webXR.webAR?.getAROverlayContainer();
|
121
|
+
const options: any = { optionalFeatures: [...this.OptionalFeatures_AR] };
|
122
|
+
if (domOverlayRoot) {
|
123
|
+
options.domOverlay = { root: domOverlayRoot };
|
124
|
+
options.optionalFeatures.push('dom-overlay')
|
125
|
+
options.optionalFeatures.push('hit-test');
|
126
|
+
options.optionalFeatures.push('anchors');
|
91
127
|
}
|
128
|
+
else {
|
129
|
+
console.warn("No dom overlay root found, HTML overlays on top of screen-based AR will not work.");
|
130
|
+
}
|
92
131
|
|
93
|
-
this.
|
94
|
-
|
132
|
+
const arButton = ARButton.createButton(webXR.context.renderer, options, this.onModifyAROptions.bind(this));
|
133
|
+
arButton.classList.add('webxr-ar-button');
|
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
|
+
}
|
95
140
|
|
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
|
96
211
|
if (this.defaultAvatar) {
|
97
|
-
|
98
|
-
|
212
|
+
if (typeof (this.defaultAvatar) === "string") {
|
213
|
+
this.defaultAvatar = AssetReference.getOrCreate(this.sourceId ?? "/", this.defaultAvatar, this.context);
|
214
|
+
}
|
99
215
|
}
|
100
|
-
if (this.
|
101
|
-
|
102
|
-
|
103
|
-
this._playerSync.onPlayerSpawned?.addEventListener(this.onAvatarSpawned);
|
216
|
+
if (!GameObject.findObjectOfType(WebXRSync, this.context)) {
|
217
|
+
const sync = GameObject.addNewComponent(this.gameObject, WebXRSync, false) as WebXRSync;
|
218
|
+
sync.webXR = this;
|
104
219
|
}
|
220
|
+
this.webAR = new WebAR(this);
|
105
221
|
|
106
|
-
|
107
|
-
|
108
|
-
|
222
|
+
if (location.protocol == 'http:' && location.host.indexOf('localhost') < 0) {
|
223
|
+
showBalloonWarning("WebXR only works on https");
|
224
|
+
console.warn("WebXR only works on https. https://engine.needle.tools/docs/xr.html");
|
109
225
|
}
|
110
226
|
}
|
111
227
|
|
112
|
-
|
113
|
-
|
114
|
-
this.
|
115
|
-
this.
|
228
|
+
onEnable() {
|
229
|
+
if (this.isInit) return;
|
230
|
+
if (!this.enableAR && !this.enableVR) return;
|
231
|
+
this.isInit = true;
|
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);
|
116
277
|
}
|
117
278
|
|
118
|
-
private
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
279
|
+
private _transformOrientation: Quaternion = new Quaternion();
|
280
|
+
public get TransformOrientation(): Quaternion { return this._transformOrientation; }
|
281
|
+
|
282
|
+
private _currentHeadPose: XRViewerPose | null = null;
|
283
|
+
public get HeadPose(): XRViewerPose | null { return this._currentHeadPose; }
|
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);
|
123
301
|
}
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
if (hasARSupport && this.createARButton) {
|
128
|
-
return NeedleXRSession.offerSession("immersive-ar", "default", this.context);
|
302
|
+
|
303
|
+
if (WebXR._isInXr === false && session) {
|
304
|
+
this.onEnterXR(session, frame);
|
129
305
|
}
|
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
|
+
}
|
130
319
|
}
|
131
|
-
|
320
|
+
|
321
|
+
WebXR.events.dispatchEvent({ type: WebXREvent.XRUpdate, frame: frame, xr: this.context.renderer.xr, rig: this.rig });
|
132
322
|
}
|
133
323
|
|
134
|
-
|
135
|
-
|
136
|
-
|
324
|
+
private onClickedARButton() {
|
325
|
+
if (!this._isInAR) {
|
326
|
+
this._requestedAR = true;
|
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
|
+
}
|
137
333
|
}
|
138
|
-
|
139
|
-
|
140
|
-
|
334
|
+
|
335
|
+
private onClickedVRButton() {
|
336
|
+
if (!this._isInVR) {
|
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
|
+
}
|
141
356
|
}
|
142
357
|
|
143
|
-
|
144
|
-
|
145
|
-
|
358
|
+
private captureStateBeforeXR() {
|
359
|
+
if (this.context.mainCamera) {
|
360
|
+
this._originalCameraPosition.copy(getWorldPosition(this.context.mainCamera));
|
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
|
+
}
|
146
369
|
}
|
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
|
-
}
|
155
370
|
|
156
|
-
private
|
371
|
+
private ensureRig() {
|
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
|
+
}
|
157
389
|
|
158
|
-
|
159
|
-
if (
|
160
|
-
|
161
|
-
|
390
|
+
// Make sure the webxr camera is parented to the xr rig
|
391
|
+
if (this.context.isInXR && this.context.mainCamera && this.context.mainCamera.parent !== this.rig) {
|
392
|
+
this.rig.add(this.context.mainCamera);
|
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
|
+
}
|
162
405
|
}
|
163
406
|
}
|
164
407
|
|
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);
|
171
408
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
409
|
+
private _originalCameraParent: Object3D | null = null;
|
410
|
+
private _originalCameraPosition: Vector3 = new Vector3();
|
411
|
+
private _originalCameraRotation: Quaternion = new Quaternion();
|
412
|
+
|
413
|
+
private _originalXRRigParent: Object3D | null = null;
|
414
|
+
private _originalXRRigPosition: Vector3 = new Vector3();
|
415
|
+
private _originalXRRigRotation: Quaternion = new Quaternion();
|
416
|
+
|
417
|
+
private onEnterXR(session: XRSession, frame: XRFrame) {
|
418
|
+
console.log("[XR] session begin", session, frame);
|
419
|
+
WebXR._isInXr = true;
|
420
|
+
|
421
|
+
this.ensureRig();
|
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);
|
185
432
|
}
|
186
|
-
else if(debug) console.log("WebXR: WebARSessionRoot already exists, not creating a new one")
|
187
433
|
}
|
188
434
|
|
189
|
-
//
|
190
|
-
|
191
|
-
|
435
|
+
// when we set unity layers objects will only be rendered on one eye
|
436
|
+
// we set layers to sync raycasting and have a similar behaviour to unity
|
437
|
+
const xr = this.context.renderer.xr;
|
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
|
+
}
|
192
457
|
}
|
193
|
-
if (this.showControllerModels || this.showHandModels) {
|
194
|
-
this.setDefaultControllerRenderingEnabled(true);
|
195
|
-
}
|
196
458
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
459
|
+
const flag = this._requestedAR ? XRStateFlag.AR : XRStateFlag.VR;
|
460
|
+
|
461
|
+
XRState.Global.Set(flag);
|
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;
|
201
474
|
}
|
202
475
|
|
203
|
-
|
476
|
+
session.addEventListener('end', () => {
|
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 });
|
204
485
|
}
|
205
486
|
|
206
|
-
|
207
|
-
// revert XR flags
|
208
|
-
XRState.Global.Set(this._previousXRState);
|
487
|
+
private onExitXR(session: XRSession | null) {
|
209
488
|
|
210
|
-
this.
|
489
|
+
const wasInAR = this._isInAR;
|
211
490
|
|
212
|
-
|
213
|
-
|
491
|
+
if (session) {
|
492
|
+
if (this._isInAR) {
|
493
|
+
this.webAR?.onEnd(session);
|
494
|
+
}
|
495
|
+
else {
|
496
|
+
// if in VR we want to restore the FOV
|
497
|
+
this.context.mainCameraComponent?.applyClearFlagsIfIsActiveCamera();
|
498
|
+
}
|
214
499
|
}
|
215
|
-
this._createdComponentsInSession.length = 0;
|
216
500
|
|
217
|
-
this.
|
218
|
-
|
501
|
+
this._isInAR = false;
|
502
|
+
this._isInVR = false;
|
503
|
+
this._requestedAR = false;
|
504
|
+
this._requestedVR = false;
|
505
|
+
this.context.xrSessionMode = undefined;
|
219
506
|
|
507
|
+
if (this.xrMirrorWindow) {
|
508
|
+
this.xrMirrorWindow.close();
|
509
|
+
this.xrMirrorWindow = null;
|
510
|
+
}
|
220
511
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
this.
|
512
|
+
this.destroyControllers();
|
513
|
+
|
514
|
+
if (this.context.mainCamera) {
|
515
|
+
this._originalCameraParent?.add(this.context.mainCamera);
|
516
|
+
setWorldPosition(this.context.mainCamera, this._originalCameraPosition);
|
517
|
+
setWorldQuaternion(this.context.mainCamera, this._originalCameraRotation);
|
518
|
+
this.context.mainCamera.scale.set(1, 1, 1);
|
227
519
|
}
|
228
|
-
|
229
|
-
|
520
|
+
|
521
|
+
if (wasInAR) {
|
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 });
|
230
529
|
}
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
models.createHandModel == this.showHandModels;
|
530
|
+
|
531
|
+
private onEnterVR(_session: XRSession) {
|
532
|
+
}
|
533
|
+
|
534
|
+
private destroyControllers() {
|
535
|
+
for (let i = this.controllers.length - 1; i >= 0; i -= 1) {
|
536
|
+
this.controllers[i]?.destroy();
|
239
537
|
}
|
240
|
-
|
241
|
-
return models;
|
538
|
+
this.controllers.length = 0;
|
242
539
|
}
|
243
540
|
|
541
|
+
private xrMirrorWindow: Window | null = null;
|
244
542
|
|
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
|
+
}
|
245
564
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
565
|
+
|
566
|
+
// not sure if this should be a behaviour.
|
567
|
+
// for now we dont really need it to go through the usual update loop
|
568
|
+
export class WebAR {
|
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;
|
251
594
|
}
|
252
595
|
|
253
|
-
private
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
};
|
596
|
+
private arDomOverlay: HTMLElement | null = null;
|
597
|
+
private arOverlayElement: INeedleEngineComponent | HTMLElement | null = null;
|
598
|
+
private noHitTestAvailable: boolean = false;
|
599
|
+
private didPlaceARSessionRoot: boolean = false;
|
258
600
|
|
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
|
+
}
|
259
610
|
|
611
|
+
setReticleActive(active: boolean) {
|
612
|
+
this.reticleActive = active;
|
613
|
+
}
|
260
614
|
|
615
|
+
async onBegin(session: XRSession) {
|
616
|
+
const context = this.webxr.context;
|
617
|
+
this.reticleActive = true;
|
618
|
+
this.didPlaceARSessionRoot = false;
|
619
|
+
this.getAROverlayContainer();
|
261
620
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
if (!this._container) {
|
267
|
-
this._container = NeedleWebXRHtmlElement.getOrCreate(this.context);
|
621
|
+
const deviceType = isQuest() ? ControllerType.PhysicalDevice : ControllerType.Touch;
|
622
|
+
const controllerCount = deviceType === ControllerType.Touch ? 4 : 2;
|
623
|
+
for (let i = 0; i < controllerCount; i++) {
|
624
|
+
WebXRController.Create(this.webxr, i, this.webxr.gameObject as GameObject, deviceType)
|
268
625
|
}
|
269
|
-
return this._container;
|
270
|
-
}
|
271
626
|
|
272
|
-
|
273
|
-
|
627
|
+
if (!this.sessionRoot || this.sessionRoot.destroyed || !this.sessionRoot.activeAndEnabled)
|
628
|
+
this.sessionRoot = GameObject.findObjectOfType(WebARSessionRoot, context);
|
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
|
+
}
|
274
635
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
636
|
+
this.previousBackground = context.scene.background;
|
637
|
+
this.previousEnvironment = context.scene.environment;
|
638
|
+
context.scene.background = null;
|
639
|
+
|
640
|
+
session.requestReferenceSpace('viewer').then((referenceSpace) => {
|
641
|
+
session.requestHitTestSource?.call(session, { space: referenceSpace })?.then((source) => {
|
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;
|
281
669
|
}
|
282
|
-
|
283
|
-
if (this.createARButton) this.getButtonsContainer().createARButton();
|
284
|
-
if (this.createVRButton) this.getButtonsContainer().createVRButton();
|
670
|
+
else console.warn("Could not found WebXR Rig");
|
285
671
|
}
|
286
672
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
673
|
+
this._previousParent = this.webxr.gameObject;
|
674
|
+
if (!WebAR.tempWebXRObject) WebAR.tempWebXRObject = new Object3D();
|
675
|
+
this.context.scene.add(WebAR.tempWebXRObject);
|
676
|
+
GameObject.addComponent(WebAR.tempWebXRObject as GameObject, this.webxr);
|
677
|
+
|
678
|
+
if (this.sessionRoot) {
|
679
|
+
this.sessionRoot.webAR = this;
|
680
|
+
this.sessionRoot?.onBegin(session);
|
291
681
|
}
|
682
|
+
else console.warn("No WebARSessionRoot found in scene")
|
292
683
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
684
|
+
const eng = this.context.domElement as INeedleEngineComponent;
|
685
|
+
eng?.onEnterAR?.call(eng, session, this.arOverlayElement!);
|
686
|
+
|
687
|
+
this.context.mainCameraComponent?.applyClearFlagsIfIsActiveCamera();
|
688
|
+
}
|
689
|
+
|
690
|
+
onEnd(session: XRSession) {
|
691
|
+
if (this._previousParent) {
|
692
|
+
GameObject.addComponent(this._previousParent as GameObject, this.webxr);
|
693
|
+
this._previousParent = null;
|
297
694
|
}
|
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();
|
298
707
|
}
|
299
708
|
|
709
|
+
onUpdate(session: XRSession, frame: XRFrame) {
|
300
710
|
|
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
|
+
}
|
301
760
|
}
|
761
|
+
|
762
|
+
const arPlacementWithoutHitTestMatrix = new Matrix4().identity().makeTranslation(0, 0, 0);
|
@@ -1,8 +1,16 @@
|
|
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";
|
1
9
|
import { Object3D } from "three";
|
2
|
-
|
10
|
+
import { VRUserState } from "./WebXRSync.js";
|
3
11
|
import { getParam } from "../../engine/engine_utils.js";
|
4
|
-
import {
|
5
|
-
import {
|
12
|
+
import { ViewDevice } from "../../engine/engine_playerview.js";
|
13
|
+
import { InstancingUtil } from "../../engine/engine_instancing.js";
|
6
14
|
|
7
15
|
export const debug = getParam("debugavatar");
|
8
16
|
|
@@ -11,12 +19,6 @@
|
|
11
19
|
gameObject: Object3D;
|
12
20
|
}
|
13
21
|
|
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
|
20
22
|
export class AvatarMarker extends Behaviour {
|
21
23
|
|
22
24
|
public static getAvatar(index: number): AvatarMarker | null {
|
@@ -42,7 +44,7 @@
|
|
42
44
|
|
43
45
|
|
44
46
|
public connectionId!: string;
|
45
|
-
public avatar?:
|
47
|
+
public avatar?: WebXRAvatar | Object3D;
|
46
48
|
|
47
49
|
awake() {
|
48
50
|
AvatarMarker.instances.push(this);
|
@@ -63,4 +65,292 @@
|
|
63
65
|
isLocalAvatar() {
|
64
66
|
return this.connectionId === this.context.connection.connectionId;
|
65
67
|
}
|
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
|
+
}
|
66
78
|
}
|
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,348 +0,0 @@
|
|
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);
|
@@ -1,14 +1,13 @@
|
|
1
|
+
import { WebXR, WebXREvent } from "./WebXR.js";
|
2
|
+
import { serializable } from "../../engine/engine_serialization.js";
|
3
|
+
import { Behaviour, GameObject } from "../Component.js";
|
1
4
|
import { Object3D, Quaternion, Vector3 } from "three";
|
5
|
+
import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
|
6
|
+
import { AssetReference } from "../../engine/engine_addressables.js";
|
7
|
+
import { showBalloonMessage, showBalloonWarning } from "../../engine/debug/index.js";
|
2
8
|
|
3
|
-
import { showBalloonMessage, showBalloonWarning } from "../../engine/debug/index.js";
|
4
|
-
import { AssetReference } from "../../engine/engine_addressables.js";
|
5
|
-
import { serializable } from "../../engine/engine_serialization.js";
|
6
|
-
import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
|
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 {
|
11
|
-
import { InstancingUtil, Renderer } from "../Renderer.js";
|
10
|
+
import { USDZExporterContext, USDWriter, imageToCanvas } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
12
11
|
|
13
12
|
// https://github.com/immersive-web/marker-tracking/blob/main/explainer.md
|
14
13
|
|
@@ -45,13 +44,11 @@
|
|
45
44
|
if (t01 === undefined || t01 >= 1 || haveChanged) {
|
46
45
|
object.position.copy(this._position);
|
47
46
|
object.quaternion.copy(this._rotation);
|
48
|
-
// InstancingUtil.markDirty(object);
|
49
47
|
}
|
50
48
|
else {
|
51
49
|
t01 = Math.max(0, Math.min(1, t01));
|
52
50
|
object.position.lerp(this._position, t01);
|
53
51
|
object.quaternion.slerp(this._rotation, t01);
|
54
|
-
// InstancingUtil.markDirty(object);
|
55
52
|
}
|
56
53
|
object.quaternion.multiply(WebXRTrackedImage.y180);
|
57
54
|
}
|
@@ -64,10 +61,15 @@
|
|
64
61
|
if (!this._position) {
|
65
62
|
this._position = WebXRTrackedImage._positionBuffer.get();
|
66
63
|
this._rotation = WebXRTrackedImage._rotationBuffer.get();
|
67
|
-
const t = this._pose.transform
|
68
|
-
|
69
|
-
|
70
|
-
this.
|
64
|
+
const t = this._pose.transform;
|
65
|
+
|
66
|
+
// when parented to the world, we need to flip data here
|
67
|
+
//this._position.set(-t.position.x, t.position.y, -t.position.z);
|
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);
|
71
73
|
}
|
72
74
|
}
|
73
75
|
|
@@ -139,7 +141,9 @@
|
|
139
141
|
trackedImages?: WebXRImageTrackingModel[];
|
140
142
|
|
141
143
|
private readonly trackedImageIndexMap: Map<number, WebXRImageTrackingModel> = new Map();
|
144
|
+
|
142
145
|
private static _imageElements: Map<string, ImageBitmap | null> = new Map();
|
146
|
+
private webxr: WebXR | null = null;
|
143
147
|
|
144
148
|
awake(): void {
|
145
149
|
if (debug) console.log(this)
|
@@ -178,35 +182,51 @@
|
|
178
182
|
}
|
179
183
|
}
|
180
184
|
|
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");
|
187
185
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
186
|
+
onEnable(): void {
|
187
|
+
this.webxr = GameObject.findObjectOfType(WebXR);
|
188
|
+
WebXR.addEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
|
189
|
+
WebXR.addEventListener(WebXREvent.XRStarted, this.onXRStarted);
|
190
|
+
WebXR.addEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
|
191
|
+
this.addEventListener("image-tracking", this.onImageTrackingUpdate);
|
192
|
+
}
|
193
|
+
|
194
|
+
onDisable(): void {
|
195
|
+
WebXR.removeEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
|
196
|
+
WebXR.removeEventListener(WebXREvent.XRStarted, this.onXRStarted);
|
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
|
+
});
|
199
219
|
}
|
200
220
|
}
|
201
221
|
}
|
202
222
|
}
|
203
223
|
|
204
|
-
|
224
|
+
private onXRStarted = (_: any) => {
|
205
225
|
if (this.trackedImages) {
|
206
226
|
for (const trackedImage of this.trackedImages) {
|
207
227
|
if (trackedImage.object?.asset) {
|
208
228
|
const obj = trackedImage.object.asset;
|
209
|
-
|
229
|
+
obj.visible = false;
|
210
230
|
}
|
211
231
|
}
|
212
232
|
}
|
@@ -216,16 +236,17 @@
|
|
216
236
|
}
|
217
237
|
};
|
218
238
|
|
219
|
-
private readonly imageToObjectMap = new Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number, lastTrackingTime:
|
239
|
+
private readonly imageToObjectMap = new Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number, lastTrackingTime:number }>();
|
220
240
|
private readonly currentImages: WebXRTrackedImage[] = [];
|
221
241
|
|
222
|
-
|
242
|
+
|
243
|
+
private onXRUpdate = (evt): void => {
|
223
244
|
this.currentImages.length = 0;
|
224
245
|
|
225
|
-
const frame =
|
246
|
+
const frame = evt.frame;
|
226
247
|
if (!frame) return;
|
227
248
|
|
228
|
-
if (!("getImageTrackingResults" in frame)) {
|
249
|
+
if (frame.session && !("getImageTrackingResults" in frame)) {
|
229
250
|
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.";
|
230
251
|
if (!this["didPrintWarning"]) {
|
231
252
|
this["didPrintWarning"] = true;
|
@@ -234,7 +255,8 @@
|
|
234
255
|
showBalloonWarning(warning);
|
235
256
|
return;
|
236
257
|
}
|
237
|
-
|
258
|
+
|
259
|
+
if (frame.session && typeof frame.getImageTrackingResults === "function") {
|
238
260
|
const results = frame.getImageTrackingResults();
|
239
261
|
if (results.length > 0) {
|
240
262
|
const space = this.context.renderer.xr.getReferenceSpace();
|
@@ -257,7 +279,9 @@
|
|
257
279
|
if (this.currentImages.length > 0) {
|
258
280
|
try {
|
259
281
|
this.dispatchEvent(new CustomEvent("image-tracking", { detail: this.currentImages }));
|
260
|
-
this.
|
282
|
+
if (this.webxr && this.webxr.allowARPlacementReticle) {
|
283
|
+
this.webxr.allowARPlacementReticle = false;
|
284
|
+
}
|
261
285
|
}
|
262
286
|
catch (e) {
|
263
287
|
console.error(e);
|
@@ -290,11 +314,9 @@
|
|
290
314
|
}
|
291
315
|
|
292
316
|
|
293
|
-
private onImageTrackingUpdate = (
|
294
|
-
const
|
295
|
-
if (!xr) return;
|
317
|
+
private onImageTrackingUpdate = (event: any) => {
|
318
|
+
const images = event.detail as WebXRTrackedImage[];
|
296
319
|
|
297
|
-
|
298
320
|
for (const image of images) {
|
299
321
|
const model = image.model;
|
300
322
|
const isTracked = image.state === "tracked";
|
@@ -307,38 +329,27 @@
|
|
307
329
|
this.imageToObjectMap.set(model, trackedData);
|
308
330
|
|
309
331
|
model.object.loadAssetAsync().then((asset: GameObject | null) => {
|
310
|
-
if (model.createObjectInstance
|
332
|
+
if (model.createObjectInstance) {
|
311
333
|
asset = GameObject.instantiate(asset);
|
312
334
|
}
|
313
335
|
|
314
336
|
if (asset) {
|
315
337
|
trackedData!.object = asset;
|
316
338
|
|
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
|
-
|
324
339
|
// make sure to parent to the WebXR.rig
|
325
|
-
if (
|
326
|
-
|
327
|
-
image.applyToObject(asset);
|
328
|
-
if (!asset.activeSelf)
|
329
|
-
GameObject.setActive(asset, true);
|
330
|
-
// InstancingUtil.markDirty(asset);
|
340
|
+
if (this.webxr) {
|
341
|
+
this.webxr.Rig.add(asset);
|
331
342
|
}
|
332
|
-
else {
|
333
|
-
console.warn("XRImageTracking: missing XRRig");
|
334
|
-
}
|
335
343
|
|
344
|
+
image.applyToObject(asset);
|
345
|
+
if (!asset.activeSelf)
|
346
|
+
GameObject.setActive(asset, true);
|
336
347
|
}
|
337
348
|
});
|
338
349
|
}
|
339
350
|
else {
|
340
351
|
trackedData.frames++;
|
341
|
-
if
|
352
|
+
if(isTracked)
|
342
353
|
trackedData.lastTrackingTime = Date.now();
|
343
354
|
|
344
355
|
// TODO we could do a bit more here: e.g. sample for the first 1s or so of getting pose data
|
@@ -348,16 +359,13 @@
|
|
348
359
|
|
349
360
|
if (!trackedData.object) continue;
|
350
361
|
|
351
|
-
if (
|
362
|
+
if (this.webxr) {
|
363
|
+
this.webxr.Rig.add(trackedData.object);
|
364
|
+
}
|
352
365
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
if (!trackedData.object.activeSelf) {
|
357
|
-
GameObject.setActive(trackedData.object, true);
|
358
|
-
}
|
359
|
-
// InstancingUtil.markDirty(trackedData.object);
|
360
|
-
}
|
366
|
+
image.applyToObject(trackedData.object);
|
367
|
+
if (!trackedData.object.activeSelf)
|
368
|
+
GameObject.setActive(trackedData.object, true);
|
361
369
|
}
|
362
370
|
}
|
363
371
|
}
|
@@ -1,14 +1,13 @@
|
|
1
|
-
import { Box3, BufferAttribute, BufferGeometry, Group, Material,
|
1
|
+
import { Box3, BufferAttribute, BufferGeometry, Group, Material, Mesh, Object3D, Vector3 } from "three";
|
2
2
|
|
3
|
-
import {
|
4
|
-
import {
|
5
|
-
import {
|
3
|
+
import { MeshCollider } from "../Collider.js";
|
4
|
+
import { Behaviour, GameObject } from "../Component.js";
|
5
|
+
import { WebXR, WebXREvent } from "./WebXR.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";
|
8
9
|
import { getParam } from "../../engine/engine_utils.js";
|
9
|
-
import {
|
10
|
-
import { MeshCollider } from "../Collider.js";
|
11
|
-
import { Behaviour, GameObject } from "../Component.js";
|
10
|
+
import { destroy } from "../../engine/engine_gameobject.js";
|
12
11
|
// import { SimplifyModifier } from 'three/examples/jsm/modifiers/SimplifyModifier.js';
|
13
12
|
|
14
13
|
const debug = getParam("debugplanetracking");
|
@@ -42,8 +41,8 @@
|
|
42
41
|
export class WebXRPlaneTracking extends Behaviour {
|
43
42
|
|
44
43
|
/** Optional: if assigned it will be instantiated per tracked plane/tracked mesh */
|
45
|
-
@serializable(
|
46
|
-
dataTemplate?:
|
44
|
+
@serializable(Object3D)
|
45
|
+
dataTemplate?: Object3D;
|
47
46
|
|
48
47
|
@serializable()
|
49
48
|
initiateRoomCaptureIfNoData = true;
|
@@ -54,25 +53,34 @@
|
|
54
53
|
@serializable()
|
55
54
|
useMeshData: boolean = true;
|
56
55
|
|
57
|
-
/** when enabled mesh or plane tracking will also be used in VR */
|
58
|
-
@serializable()
|
59
|
-
runInVR = true;
|
60
|
-
|
61
56
|
get trackedPlanes() { return this._allPlanes.values(); }
|
62
57
|
get trackedMeshes() { return this._allMeshes.values(); }
|
63
58
|
|
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
|
+
}
|
65
70
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
71
|
+
private onModifyAROptions = (event: any) => {
|
72
|
+
const options = event.detail;
|
73
|
+
const features = options.optionalFeatures || [];
|
74
|
+
|
75
|
+
if (this.usePlaneData && !features.includes("plane-detection"))
|
76
|
+
features.push("plane-detection");
|
77
|
+
if (this.useMeshData && !features.includes("mesh-detection"))
|
78
|
+
features.push("mesh-detection");
|
79
|
+
|
80
|
+
options.optionalFeatures = features;
|
73
81
|
}
|
74
82
|
|
75
|
-
|
83
|
+
private onXRStarted = (_evt) => {
|
76
84
|
// remove all previously added data from the scene again
|
77
85
|
for (const data of this._allPlanes.keys()) {
|
78
86
|
this.removeData(data, this._allPlanes);
|
@@ -82,24 +90,18 @@
|
|
82
90
|
}
|
83
91
|
}
|
84
92
|
|
85
|
-
|
86
|
-
|
87
|
-
if (!this.runInVR && args.xr.isVR) return;
|
88
|
-
|
93
|
+
private onXRUpdate = (evt) => {
|
94
|
+
|
89
95
|
// parenting tracked planes to the XR rig ensures that they synced with the real-world user data;
|
90
96
|
// otherwise they would "swim away" when the user rotates / moves / teleports and so on.
|
91
97
|
// There may be cases where we want that! E.g. a user walks around on their own table in castle builder
|
92
|
-
|
93
|
-
if (!rig) {
|
94
|
-
console.warn("No XR rig found, cannot parent tracked planes to it");
|
95
|
-
return;
|
96
|
-
}
|
98
|
+
if (!evt.rig) return;
|
97
99
|
|
98
|
-
const frame =
|
100
|
+
const frame = evt.frame as XRFramePlanes;
|
99
101
|
const renderer = this.context.renderer;
|
100
102
|
const referenceSpace = renderer.xr.getReferenceSpace();
|
101
103
|
if (!referenceSpace) return;
|
102
|
-
|
104
|
+
|
103
105
|
const planes = frame.detectedPlanes;
|
104
106
|
const meshes = frame.detectedMeshes;
|
105
107
|
const hasAnyPlanes = planes !== undefined && planes.size > 0;
|
@@ -124,10 +126,10 @@
|
|
124
126
|
}
|
125
127
|
|
126
128
|
if (planes !== undefined)
|
127
|
-
this.processFrameData(
|
129
|
+
this.processFrameData(evt.rig, evt.frame, planes, this._allPlanes);
|
128
130
|
|
129
131
|
if (meshes !== undefined)
|
130
|
-
this.processFrameData(
|
132
|
+
this.processFrameData(evt.rig, evt.frame, meshes, this._allMeshes);
|
131
133
|
}
|
132
134
|
|
133
135
|
private removeData(data: XRPlane | XRMesh, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
|
@@ -154,11 +156,11 @@
|
|
154
156
|
private readonly _allMeshes = new Map<XRMesh, XRPlaneContext>();
|
155
157
|
private firstTimeNoPlanesDetected = -100;
|
156
158
|
|
157
|
-
private processFrameData(
|
159
|
+
private processFrameData(rig: Object3D, frame: XRFramePlanes, detected: Set<XRPlane | XRMesh>, _all: Map<XRPlane | XRMesh, XRPlaneContext>) {
|
158
160
|
const renderer = this.context.renderer;
|
159
161
|
const referenceSpace = renderer.xr.getReferenceSpace();
|
160
162
|
if (!referenceSpace) return;
|
161
|
-
|
163
|
+
|
162
164
|
for (const data of _all.keys()) {
|
163
165
|
if (!detected.has(data)) {
|
164
166
|
this.removeData(data, _all);
|
@@ -168,7 +170,7 @@
|
|
168
170
|
for (const data of detected) {
|
169
171
|
const space = "planeSpace" in data ? data.planeSpace
|
170
172
|
: ("meshSpace" in data ? data.meshSpace
|
171
|
-
|
173
|
+
: undefined);
|
172
174
|
if (!space) continue;
|
173
175
|
const planePose = frame.getPose(space, referenceSpace);
|
174
176
|
|
@@ -241,18 +243,12 @@
|
|
241
243
|
|
242
244
|
// if we don't have any template assigned we just use a simple mesh object
|
243
245
|
if (!this.dataTemplate) {
|
244
|
-
|
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);
|
246
|
+
this.dataTemplate = new Mesh();
|
248
247
|
}
|
249
248
|
|
250
|
-
if (
|
251
|
-
this.dataTemplate.loadAssetAsync();
|
252
|
-
}
|
253
|
-
else {
|
249
|
+
if (this.dataTemplate) {
|
254
250
|
// Create instance
|
255
|
-
const newPlane = GameObject.instantiate(this.dataTemplate
|
251
|
+
const newPlane = GameObject.instantiate(this.dataTemplate) as GameObject;
|
256
252
|
planeMesh = newPlane;
|
257
253
|
|
258
254
|
if (newPlane instanceof Mesh) {
|
@@ -269,7 +265,7 @@
|
|
269
265
|
}
|
270
266
|
}
|
271
267
|
}
|
272
|
-
|
268
|
+
|
273
269
|
const mc = newPlane.getComponent(MeshCollider) as MeshCollider;
|
274
270
|
if (mc) {
|
275
271
|
const mesh = newPlane as unknown as Mesh;
|
@@ -316,7 +312,6 @@
|
|
316
312
|
if (planePose) {
|
317
313
|
planeMesh.visible = true;
|
318
314
|
planeMesh.matrix.fromArray(planePose.transform.matrix);
|
319
|
-
planeMesh.matrix.premultiply(this._flipForwardMatrix);
|
320
315
|
} else {
|
321
316
|
planeMesh.visible = false;
|
322
317
|
}
|
@@ -324,11 +319,9 @@
|
|
324
319
|
};
|
325
320
|
}
|
326
321
|
|
327
|
-
private _flipForwardMatrix = new Matrix4().makeRotationY(Math.PI);
|
328
|
-
|
329
322
|
// heuristic to determine if a collider should be convex or not -
|
330
323
|
// the "global mesh" should be non-convex, other meshes should be
|
331
|
-
|
324
|
+
checkIfContextShouldBeConvex(mesh: Mesh | Group | undefined, xrData: XRPlane | XRMesh) {
|
332
325
|
if (!mesh) return true;
|
333
326
|
if (mesh) {
|
334
327
|
// get bounding box of the mesh
|
@@ -353,7 +346,7 @@
|
|
353
346
|
return true;
|
354
347
|
}
|
355
348
|
|
356
|
-
|
349
|
+
createGeometry(data: XRPlane | XRMesh) {
|
357
350
|
if ("polygon" in data) {
|
358
351
|
return this.createPlaneGeometry(data.polygon);
|
359
352
|
}
|
@@ -366,7 +359,7 @@
|
|
366
359
|
// we cache vertices-to-geometry, because it looks like when we get an update sometimes the geometry stays the same.
|
367
360
|
// so we don't want to re-create the geometry every time.
|
368
361
|
private _verticesCache = new Map<string, BufferGeometry>();
|
369
|
-
|
362
|
+
createMeshGeometry(vertices: Float32Array, indices: Uint32Array) {
|
370
363
|
const key = vertices.toString() + "_" + indices.toString();
|
371
364
|
if (this._verticesCache.has(key)) {
|
372
365
|
return this._verticesCache.get(key)!;
|
@@ -376,7 +369,7 @@
|
|
376
369
|
geometry.setAttribute('position', new BufferAttribute(vertices, 3));
|
377
370
|
// set UVs in worldspace
|
378
371
|
const uvs = Array<number>();
|
379
|
-
for (let i = 0; i < vertices.length; i
|
372
|
+
for (let i = 0; i < vertices.length; i+=3) {
|
380
373
|
uvs.push(vertices[i], vertices[i + 2]);
|
381
374
|
}
|
382
375
|
geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2));
|
@@ -394,9 +387,9 @@
|
|
394
387
|
|
395
388
|
this._verticesCache.set(key, geometry);
|
396
389
|
return geometry;
|
397
|
-
|
390
|
+
}
|
398
391
|
|
399
|
-
|
392
|
+
createPlaneGeometry(polygon: Vec3[]) {
|
400
393
|
const geometry = new BufferGeometry();
|
401
394
|
|
402
395
|
const vertices: number[] = [];
|
@@ -1,59 +1,22 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
1
|
+
import { Object3D } from "three";
|
4
2
|
import type { IGameObject } from "../../engine/engine_types.js";
|
5
3
|
import { getParam } from "../../engine/engine_utils.js";
|
6
|
-
import { NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
7
|
-
import { IXRRig } from "../../engine/engine_xr.js";
|
8
4
|
import { Behaviour } from "../Component.js";
|
9
5
|
import { BoxGizmo } from "../Gizmos.js";
|
10
6
|
|
11
|
-
const debug = getParam("
|
7
|
+
const debug = getParam("debugrig");
|
12
8
|
|
13
|
-
export class XRRig extends Behaviour
|
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
|
-
|
9
|
+
export class XRRig extends Behaviour {
|
25
10
|
awake(): void {
|
11
|
+
// const helper = new AxesHelper(.1);
|
12
|
+
// this.gameObject.add(helper);
|
26
13
|
if (debug) {
|
27
14
|
const gizmoObj = new Object3D() as IGameObject;
|
28
15
|
gizmoObj.position.y += .5;
|
29
16
|
this.gameObject.add(gizmoObj);
|
30
|
-
const
|
31
|
-
if (
|
32
|
-
|
33
|
-
const axes = new AxesHelper(.5);
|
34
|
-
this.gameObject.add(axes)
|
17
|
+
const gizmo = gizmoObj.addNewComponent(BoxGizmo);
|
18
|
+
if (gizmo)
|
19
|
+
gizmo.isGizmo = false;
|
35
20
|
}
|
36
21
|
}
|
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
|
-
|
59
22
|
}
|
@@ -1,67 +0,0 @@
|
|
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
|
-
}
|
@@ -1,307 +0,0 @@
|
|
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
|
-
}
|
@@ -1,340 +0,0 @@
|
|
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
|
-
|
@@ -1,143 +0,0 @@
|
|
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
|
-
}
|
@@ -1,9 +0,0 @@
|
|
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
|
-
}
|
@@ -0,0 +1,1168 @@
|
|
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
|
+
}
|
@@ -0,0 +1,151 @@
|
|
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
|
+
}
|
@@ -0,0 +1,463 @@
|
|
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
|
+
}
|
@@ -0,0 +1,139 @@
|
|
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
|
+
}
|