diff --git a/interface/resources/serverless/Fonts/LICENSE.txt b/interface/resources/serverless/Fonts/LICENSE.txt
new file mode 100644
index 0000000000..75b52484ea
--- /dev/null
+++ b/interface/resources/serverless/Fonts/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/interface/resources/serverless/Fonts/Roboto-Bold.ttf b/interface/resources/serverless/Fonts/Roboto-Bold.ttf
new file mode 100644
index 0000000000..3742457900
Binary files /dev/null and b/interface/resources/serverless/Fonts/Roboto-Bold.ttf differ
diff --git a/interface/resources/serverless/Fonts/Roboto-BoldItalic.ttf b/interface/resources/serverless/Fonts/Roboto-BoldItalic.ttf
new file mode 100644
index 0000000000..e85e7fb9e3
Binary files /dev/null and b/interface/resources/serverless/Fonts/Roboto-BoldItalic.ttf differ
diff --git a/interface/resources/serverless/Fonts/Roboto-Italic.ttf b/interface/resources/serverless/Fonts/Roboto-Italic.ttf
new file mode 100644
index 0000000000..c9df607a4d
Binary files /dev/null and b/interface/resources/serverless/Fonts/Roboto-Italic.ttf differ
diff --git a/interface/resources/serverless/Fonts/Roboto-Regular.ttf b/interface/resources/serverless/Fonts/Roboto-Regular.ttf
new file mode 100644
index 0000000000..3d6861b423
Binary files /dev/null and b/interface/resources/serverless/Fonts/Roboto-Regular.ttf differ
diff --git a/interface/resources/serverless/Images/applications.png b/interface/resources/serverless/Images/applications.png
new file mode 100644
index 0000000000..7fe7cef292
Binary files /dev/null and b/interface/resources/serverless/Images/applications.png differ
diff --git a/interface/resources/serverless/Images/avatar.png b/interface/resources/serverless/Images/avatar.png
new file mode 100644
index 0000000000..b206e7041a
Binary files /dev/null and b/interface/resources/serverless/Images/avatar.png differ
diff --git a/interface/resources/serverless/Images/controls.png b/interface/resources/serverless/Images/controls.png
new file mode 100644
index 0000000000..f945e77444
Binary files /dev/null and b/interface/resources/serverless/Images/controls.png differ
diff --git a/interface/resources/serverless/Images/tabletAndToolbar.png b/interface/resources/serverless/Images/tabletAndToolbar.png
new file mode 100644
index 0000000000..6529d24f7e
Binary files /dev/null and b/interface/resources/serverless/Images/tabletAndToolbar.png differ
diff --git a/interface/resources/serverless/Models/avatarStand.fbx b/interface/resources/serverless/Models/avatarStand.fbx
new file mode 100644
index 0000000000..e74244132f
Binary files /dev/null and b/interface/resources/serverless/Models/avatarStand.fbx differ
diff --git a/interface/resources/serverless/Models/avatarStand.fst b/interface/resources/serverless/Models/avatarStand.fst
new file mode 100644
index 0000000000..9ecd17d41b
--- /dev/null
+++ b/interface/resources/serverless/Models/avatarStand.fst
@@ -0,0 +1,3 @@
+name = AVATAR_STAND
+filename = qrc:///serverless/Models/avatarStand.fbx
+materialMap = [{"mat::MAIN": {"materials":[{ "name": "MAIN", "albedo": [0.4235294117647059, 1, 0], "roughness": 0.26, "metallic": 1.0, "normalMap": "qrc:///serverless/Textures/concreteNormal512.jpg", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}, {"mat::LIGHT": {"materials":[{ "name": "LIGHT", "albedo": [1, 1, 1], "roughness": 0.5, "metallic": 0.01, "emissive": [1.89, 1.68247, 0.756], "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}]
diff --git a/interface/resources/serverless/Models/dome.fbx b/interface/resources/serverless/Models/dome.fbx
new file mode 100644
index 0000000000..7a0a464012
Binary files /dev/null and b/interface/resources/serverless/Models/dome.fbx differ
diff --git a/interface/resources/serverless/Models/dome.fst b/interface/resources/serverless/Models/dome.fst
new file mode 100644
index 0000000000..94dd350020
--- /dev/null
+++ b/interface/resources/serverless/Models/dome.fst
@@ -0,0 +1,3 @@
+name = DOME
+filename = qrc:///serverless/Models/dome.fbx
+materialMap = [{"mat::FLOOR": {"materials":[{ "name": "FLOOR", "albedo": [0.06 ,0.06 ,0.06], "roughness": 0.32, "metallic": 0.01, "normalMap": "qrc:///serverless/Textures/fundationNormal512.jpg", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}, {"mat::LIGHT": {"materials":[{ "name": "LIGHT", "albedo": [1, 1, 1], "roughness": 0.5, "metallic": 0.01, "emissive": [1.64741, 2.09, 0.713058], "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}, {"mat::STRUCTURE": {"materials":[{ "name": "STRUCTURE", "albedo": [0.7882, 0.9333, 0.6274], "roughness": 0.5, "metallic": 0.1, "albedoMap": "qrc:///serverless/Textures/Concrete15_col-512.jpg", "normalMap": "qrc:///serverless/Textures/concreteNormal512.jpg", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}]
diff --git a/interface/resources/serverless/Models/engery-bowl.fbx b/interface/resources/serverless/Models/engery-bowl.fbx
new file mode 100644
index 0000000000..9d93ea469f
Binary files /dev/null and b/interface/resources/serverless/Models/engery-bowl.fbx differ
diff --git a/interface/resources/serverless/Models/engery-bowl.fst b/interface/resources/serverless/Models/engery-bowl.fst
new file mode 100644
index 0000000000..37ba07d723
--- /dev/null
+++ b/interface/resources/serverless/Models/engery-bowl.fst
@@ -0,0 +1,3 @@
+name = ENERGY_BOWL
+filename = qrc:///serverless/Models/engery-bowl.fbx
+materialMap = [{"mat::MAIN": {"materials":[{ "name": "MAIN", "albedo": [1 ,1 ,1], "albedoMap": "qrc:///serverless/Textures/Metal26_col.jpg","roughness": 0.28, "metallic": 1.0, "normalMap": "qrc:///serverless/Textures/Metal26_nrm.jpg", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}]
diff --git a/interface/resources/serverless/Models/standAngle.fbx b/interface/resources/serverless/Models/standAngle.fbx
new file mode 100644
index 0000000000..99d14ade95
Binary files /dev/null and b/interface/resources/serverless/Models/standAngle.fbx differ
diff --git a/interface/resources/serverless/Models/standAngle_Applications.fst b/interface/resources/serverless/Models/standAngle_Applications.fst
new file mode 100644
index 0000000000..bfa6593aee
--- /dev/null
+++ b/interface/resources/serverless/Models/standAngle_Applications.fst
@@ -0,0 +1,3 @@
+name = STAND-ANGLE_APPLICATIONS
+filename = qrc:///serverless/Models/standAngle.fbx
+materialMap = [{"mat::IMAGE": {"materials":[{ "name": "IMAGE", "albedo": [1.0 ,1.0 ,1.0], "roughness": 0.2, "metallic": 0.01, "unlit": true, "albedoMap": "qrc:///serverless/Images/applications.png", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}]
diff --git a/interface/resources/serverless/Models/standAngle_Avatar.fst b/interface/resources/serverless/Models/standAngle_Avatar.fst
new file mode 100644
index 0000000000..0cb6bd2f05
--- /dev/null
+++ b/interface/resources/serverless/Models/standAngle_Avatar.fst
@@ -0,0 +1,3 @@
+name = STAND-ANGLE_AVATAR
+filename = qrc:///serverless/Models/standAngle.fbx
+materialMap = [{"mat::IMAGE": {"materials":[{ "name": "IMAGE", "albedo": [1.0 ,1.0 ,1.0], "roughness": 0.2, "metallic": 0.01, "unlit": true, "albedoMap": "qrc:///serverless/Images/avatar.png", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}]
diff --git a/interface/resources/serverless/Models/standAngle_ConfigWizard.fst b/interface/resources/serverless/Models/standAngle_ConfigWizard.fst
new file mode 100644
index 0000000000..f956c85086
--- /dev/null
+++ b/interface/resources/serverless/Models/standAngle_ConfigWizard.fst
@@ -0,0 +1,3 @@
+name = STAND-ANGLE_CONFIG-WIZARD
+filename = qrc:///serverless/Models/standAngle.fbx
+materialMap = [{"mat::IMAGE": {"materials":[{ "name": "IMAGE", "albedo": [0 ,0 ,0], "roughness": 1, "metallic": 0.01, "unlit": true, "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}]
diff --git a/interface/resources/serverless/Models/standAngle_Controls.fst b/interface/resources/serverless/Models/standAngle_Controls.fst
new file mode 100644
index 0000000000..c896cd885f
--- /dev/null
+++ b/interface/resources/serverless/Models/standAngle_Controls.fst
@@ -0,0 +1,3 @@
+name = STAND-ANGLE_CONTROLS
+filename = qrc:///serverless/Models/standAngle.fbx
+materialMap = [{"mat::IMAGE": {"materials":[{ "name": "IMAGE", "albedo": [1.0 ,1.0 ,1.0], "roughness": 0.2, "metallic": 0.01, "unlit": true, "albedoMap": "qrc:///serverless/Images/controls.png", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}]
diff --git a/interface/resources/serverless/Models/standAngle_TabletAndToolbar.fst b/interface/resources/serverless/Models/standAngle_TabletAndToolbar.fst
new file mode 100644
index 0000000000..be95bd8412
--- /dev/null
+++ b/interface/resources/serverless/Models/standAngle_TabletAndToolbar.fst
@@ -0,0 +1,3 @@
+name = STAND-ANGLE_TABLET-TOOLBAR
+filename = qrc:///serverless/Models/standAngle.fbx
+materialMap = [{"mat::IMAGE": {"materials":[{ "name": "IMAGE", "albedo": [1.0 ,1.0 ,1.0], "roughness": 0.2, "metallic": 0.01, "unlit": true, "albedoMap": "qrc:///serverless/Images/tabletAndToolbar.png", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}]
diff --git a/interface/resources/serverless/Models/teleporter.fbx b/interface/resources/serverless/Models/teleporter.fbx
new file mode 100644
index 0000000000..773d2ddada
Binary files /dev/null and b/interface/resources/serverless/Models/teleporter.fbx differ
diff --git a/interface/resources/serverless/Models/teleporter.fst b/interface/resources/serverless/Models/teleporter.fst
new file mode 100644
index 0000000000..db5a627717
--- /dev/null
+++ b/interface/resources/serverless/Models/teleporter.fst
@@ -0,0 +1,3 @@
+name = TELEPORTER
+filename = qrc:///serverless/Models/teleporter.fbx
+materialMap = [{"mat::EXO": {"materials":[{ "name": "MAIN", "albedo": [0.06, 0.06, 0.06], "roughness": 0.26, "metallic": 1.0, "normalMap": "qrc:///serverless/Textures/concreteNormal512.jpg", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}, {"mat::LIGHT": {"materials":[{ "name": "LIGHT", "albedo": [1, 1, 1], "roughness": 0.5, "metallic": 0.01, "emissive": [1.51466666, 0.879843137, 2.84], "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}, {"mat::ENDO": {"materials":[{ "name": "ENDO", "albedo": [0.85, 0.85, 0.85], "roughness": 0.26, "metallic": 1.0, "normalMap": "qrc:///serverless/Textures/concreteNormal512.jpg", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}]
diff --git a/interface/resources/serverless/Models/test_area.fbx b/interface/resources/serverless/Models/test_area.fbx
new file mode 100644
index 0000000000..08110410bd
Binary files /dev/null and b/interface/resources/serverless/Models/test_area.fbx differ
diff --git a/interface/resources/serverless/Models/test_area.fst b/interface/resources/serverless/Models/test_area.fst
new file mode 100644
index 0000000000..55189b4987
--- /dev/null
+++ b/interface/resources/serverless/Models/test_area.fst
@@ -0,0 +1,3 @@
+name = QUICK-TEST-AREA
+filename = qrc:///serverless/Models/test_area.fbx
+materialMap = [{"mat::FLOOR": {"materials":[{ "name": "FLOOR", "albedo": [0.06 ,0.06 ,0.06], "roughness": 0.32, "metallic": 0.01, "normalMap": "qrc:///serverless/Textures/fundationNormal512.jpg", "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}, {"mat::LIGHT": {"materials":[{ "name": "LIGHT", "albedo": [1, 1, 1], "roughness": 0.5, "metallic": 0.01, "emissive": [1.64741, 2.09, 0.713058], "cullFaceMode": "CULL_NONE", "model": "hifi_pbr"}]}}]
diff --git a/interface/resources/serverless/Scripts/activator-doppleganger.js b/interface/resources/serverless/Scripts/activator-doppleganger.js
new file mode 100644
index 0000000000..f3f62669bb
--- /dev/null
+++ b/interface/resources/serverless/Scripts/activator-doppleganger.js
@@ -0,0 +1,99 @@
+'use strict';
+//
+// activator-doppleganger.js
+//
+// Created by Alezia Kurdis on February 20th, 2022.
+// Copyright 2022 Overte e.V.
+//
+// This script is display a doppleganger of the user by entering an entity.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+(function() {
+ var isActive = false;
+ var thisEntityID;
+ var versioncall = Math.floor(Math.random()*50000);
+ var DopplegangerClass = Script.require('qrc:///serverless/Scripts/doppleganger.js?version=' + versioncall);
+
+ var doppleganger = new DopplegangerClass({
+ avatar: MyAvatar,
+ mirrored: false,
+ autoUpdate: true
+ });
+
+ this.preload = function(entityID) {
+ thisEntityID = entityID;
+ }
+
+ function onDomainChanged() {
+ if (doppleganger.active) {
+ doppleganger.stop('domain_changed');
+ }
+ }
+
+ Window.domainChanged.connect(onDomainChanged);
+
+ Window.domainConnectionRefused.connect(onDomainChanged);
+
+ Script.scriptEnding.connect(function() {
+ if (isActive) {
+ doppleganger.stop();
+ isActive = false;
+ }
+ Window.domainChanged.disconnect(onDomainChanged);
+ Window.domainConnectionRefused.disconnect(onDomainChanged);
+
+ });
+
+ this.enterEntity = function(entityID) {
+ print("ENTERING");
+ startDopplegangerShow(entityID);
+ isActive = true;
+ }
+
+ this.leaveEntity = function(entityID) {
+ print("LEAVING");
+ doppleganger.stop();
+ isActive = false;
+ }
+
+ function startDopplegangerShow(entityID) {
+ var properties = Entities.getEntityProperties(entityID, ["position", "rotation"]);
+ var avatarPosition = MyAvatar.position;
+ var drawPosition = {
+ "x": properties.position.x,
+ "y": avatarPosition.y,
+ "z": properties.position.z
+ };
+ var param = {
+ "position": drawPosition,
+ "orientation": properties.rotation,
+ "autoUpdate": true
+ };
+ doppleganger.start(param);
+ }
+
+
+ MyAvatar.skeletonModelURLChanged.connect(function () {
+ if (isActive) {
+ print("CHANGED WHILE ACTIVE");
+ doppleganger.stop();
+ isActive = false;
+ var timer = Script.setTimeout(function () {
+ startDopplegangerShow(thisEntityID);
+ isActive = true;
+ }, 4000);
+
+ }
+ });
+
+ // alert the user if there was an error applying their skeletonModelURL
+ doppleganger.addingEntity.connect(function(error, result) {
+ if (doppleganger.active && error) {
+ Window.alert('doppleganger | ' + error + '\n' + doppleganger.skeletonModelURL);
+ }
+ });
+
+})
diff --git a/interface/resources/serverless/Scripts/doppleganger.js b/interface/resources/serverless/Scripts/doppleganger.js
new file mode 100644
index 0000000000..9968b9fd0c
--- /dev/null
+++ b/interface/resources/serverless/Scripts/doppleganger.js
@@ -0,0 +1,529 @@
+"use strict";
+//======================================
+// Version 1.1
+// Addaption to "Local" Entities (since Overlays get deprecated for "Model" type.)
+// by Alezia Kurdis on February 2020
+//======================================
+// Version 1.0
+// doppleganger.js
+//
+// Created by Timothy Dedischew on 04/21/2017.
+// Copyright 2017 High Fidelity, Inc.
+// Copyright 2022 Overte e.V.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+//
+/* global module */
+// @module doppleganger
+//
+// This module contains the `Doppleganger` class implementation for creating an inspectable replica of
+// an Avatar (as a model directly in front of and facing them). Joint positions and rotations are copied
+// over in an update thread, so that the model automatically mirrors the Avatar's joint movements.
+// An Avatar can then for example walk around "themselves" and examine from the back, etc.
+//
+// This should be helpful for inspecting your own look and debugging avatars, etc.
+//
+// The doppleganger is created as an overlay so that others do not see it -- and this also allows for the
+// highest possible update rate when keeping joint data in sync.
+//
+// NOTE: THIS IS A MODIFIED VERSION SPECIFICALLY FOR THE TUTORIAL.
+
+module.exports = Doppleganger;
+
+// @property {bool} - when set true, Script.update will be used instead of setInterval for syncing joint data
+Doppleganger.USE_SCRIPT_UPDATE = false;
+
+// @property {int} - the frame rate to target when using setInterval for joint updates
+Doppleganger.TARGET_FPS = 60;
+
+// @property {int} - the maximum time in seconds to wait for the model overlay to finish loading
+Doppleganger.MAX_WAIT_SECS = 10;
+
+// @function - derive mirrored joint names from a list of regular joint names
+// @param {Array} - list of joint names to mirror
+// @return {Array} - list of mirrored joint names (note: entries for non-mirrored joints will be `undefined`)
+
+var Setarry;
+
+Doppleganger.getMirroredJointNames = function(jointNames) {
+ return jointNames.map(function(name, i) {
+ if (/Left/.test(name)) {
+ return name.replace('Left', 'Right');
+ }
+ if (/Right/.test(name)) {
+ return name.replace('Right', 'Left');
+ }
+ return undefined;
+ });
+};
+
+// @class Doppleganger - Creates a new instance of a Doppleganger.
+// @param {Avatar} [options.avatar=MyAvatar] - Avatar used to retrieve position and joint data.
+// @param {bool} [options.mirrored=true] - Apply "symmetric mirroring" of Left/Right joints.
+// @param {bool} [options.autoUpdate=true] - Automatically sync joint data.
+function Doppleganger(options) {
+ options = options || {};
+ this.avatar = options.avatar || MyAvatar;
+ this.mirrored = 'mirrored' in options ? options.mirrored : false;
+ this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true;
+
+ // @public
+ this.active = false; // whether doppleganger is currently being displayed/updated
+ this.entityID = null; // current doppleganger's Entity id
+ this.frame = 0; // current joint update frame
+
+ // @signal - emitted when .active state changes
+ this.activeChanged = signal(function(active, reason) {});
+ // @signal - emitted once model overlay is either loaded or errors out
+ this.addingEntity = signal(function(error, result){});
+ // @signal - emitted each time the model overlay's joint data has been synchronized
+ this.jointsUpdated = signal(function(entityID){});
+}
+
+Doppleganger.prototype = {
+ // @public @method - toggles doppleganger on/off
+ toggle: function() {
+ if (this.active) {
+ log('toggling off');
+ this.stop();
+ }else{
+ log('toggling on');
+ this.start();
+ }
+ return this.active;
+ },
+
+ // @public @method - synchronize the joint data between Avatar / doppleganger
+ update: function() {
+ this.frame++;
+ try {
+ if (!this.entityID) {
+ throw new Error('!this.entityID');
+ }
+
+ if (this.avatar.skeletonModelURL !== this.skeletonModelURL) {
+ //return this.stop('avatar_changed');
+ }
+
+ var rotations = this.avatar.getJointRotations();
+ var translations = this.avatar.getJointTranslations();
+ var size = rotations.length;
+
+
+ // note: this mismatch can happen when the avatar's model is actively changing
+ if (size !== translations.length ||
+ (this.jointStateCount && size !== this.jointStateCount)) {
+ log('mismatched joint counts (avatar model likely changed)', size, translations.length, this.jointStateCount);
+ this.stop('avatar_changed_joints');
+ return;
+ }
+ this.jointStateCount = size;
+
+
+ if (this.mirrored) {
+ var mirroredIndexes = this.mirroredIndexes;
+ var outRotations = new Array(size);
+ var outTranslations = new Array(size);
+ for (var i=0; i < size; i++) {
+ var index = mirroredIndexes[i];
+ if (index < 0 || index === false) {
+ index = i;
+ }
+ var rot = rotations[index];
+ var trans = translations[index];
+ trans.x *= -1;
+ rot.y *= -1;
+ rot.z *= -1;
+ outRotations[i] = rot;
+ outTranslations[i] = trans;
+ }
+ rotations = outRotations;
+ translations = outTranslations;
+ }
+
+
+ Entities.editEntity(this.entityID, {
+ jointRotations: rotations,
+ jointTranslations: translations,
+ jointRotationsSet: Setarry,
+ jointTranslationsSet: Setarry
+ });
+
+ this.jointsUpdated(this.entityID);
+ } catch (e) {
+ //log('.update error: '+ e, index);
+ this.stop('update_error');
+ }
+ },
+
+ // @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified).
+ // @param {vec3} [options.position=(in front of avatar)] - starting position
+ // @param {quat} [options.orientation=avatar.orientation] - starting orientation
+ start: function(options) {
+
+
+ options = options || {};
+ if (this.entityID) {
+ //log('start() called but entity model already exists', this.entityID);
+ return;
+ }
+ var avatar = this.avatar;
+ if (!avatar.jointNames.length) {
+ return this.stop('joints_unavailable');
+ }
+
+ this.frame = 0;
+ this.position = options.position || Vec3.sum(avatar.position, Quat.getForward(avatar.orientation));
+ this.orientation = options.orientation || avatar.orientation;
+ this.skeletonModelURL = avatar.skeletonModelURL;
+ this.jointStateCount = 0;
+ this.jointNames = avatar.jointNames;
+ this.mirroredNames = Doppleganger.getMirroredJointNames(this.jointNames);
+ //log(this.mirroredNames);
+ this.mirroredIndexes = this.mirroredNames.map(function(name) {
+ return name ? avatar.getJointIndex(name) : false;
+ });
+ //log(this.mirroredIndexes);
+ var prop = {
+ type: "Model",
+ name: 'Doppelganger', //added
+ visible: false, // normally false
+ modelURL: this.skeletonModelURL, //was field: url
+ position: this.position,
+ rotation: this.orientation
+ };
+
+ this.entityID = Entities.addEntity(prop, "local");
+
+ var allJoints = avatar.getJointRotations();
+ var nbrJoints = allJoints.length;
+ Setarry = Array(nbrJoints);
+ for (var i=0; i < nbrJoints; i++) {
+ Setarry[i] = true;
+ }
+
+ this.onAddingEntity = function(error, result) {
+
+ if (error) {
+ return this.stop(error);
+ }
+ log('ModelEntity is ready; # joints == ' + result.jointNames.length);
+ Entities.editEntity(this.entityID, { visible: true });
+
+ this.syncVerticalPosition('LeftFoot');
+
+ if (this.autoUpdate) {
+ this._createUpdateThread();
+ }
+ };
+ this.addingEntity.connect(this, 'onAddingEntity');
+
+ log('doppleganger created; entityID =', this.entityID);
+
+ // trigger clean up (and stop updates) if the overlay gets deleted
+ this.onDeletedEntity = function(uuid) {
+ if (uuid === this.entityID) {
+ log('onDeletedEntity', uuid);
+ this.stop('entity_deleted');
+ }
+ };
+ Entities.deletingEntity.connect(this, 'onDeletedEntity');
+
+ if ('onLoadComplete' in avatar) {
+ // stop the current doppleganger if Avatar loads a different model URL
+ this.onLoadComplete = function() {
+ if (avatar.skeletonModelURL !== this.skeletonModelURL) {
+ //this.stop('avatar_changed_load');
+ }
+ };
+ avatar.onLoadComplete.connect(this, 'onLoadComplete');
+ }
+
+ this.activeChanged(this.active = true, 'start');
+ this._waitForModel(ModelCache.prefetch(this.skeletonModelURL));
+ },
+
+ // @public @method - hide the doppleganger
+ // @param {String} [reason=stop] - the reason stop was called
+ stop: function(reason) {
+ reason = reason || 'stop';
+ if (this.onUpdate) {
+ Script.update.disconnect(this, 'onUpdate');
+ delete this.onUpdate;
+ }
+ if (this._interval) {
+ Script.clearInterval(this._interval);
+ this._interval = undefined;
+ }
+ if (this.onDeletedEntity) {
+ Entities.deletingEntity.disconnect(this, 'onDeletedEntity');
+ delete this.onDeletedEntity;
+ }
+ if (this.onLoadComplete) {
+ this.avatar.onLoadComplete.disconnect(this, 'onLoadComplete');
+ delete this.onLoadComplete;
+ }
+ if (this.onAddingEntity) {
+ this.addingEntity.disconnect(this, 'onAddingEntity');
+ }
+ if (this.entityID) {
+ Entities.deleteEntity(this.entityID);
+ this.entityID = undefined;
+ }
+ if (this.active) {
+ this.activeChanged(this.active = false, reason);
+ } else if (reason) {
+ log('already stopped so not triggering another activeChanged; latest reason was:', reason);
+ }
+ },
+
+ // @public @method - Reposition the doppleganger so it sees "eye to eye" with the Avatar.
+ // @param {String} [byJointName=Hips] - the reference joint used to align the Doppleganger and Avatar
+ syncVerticalPosition: function(byJointName) {
+ byJointName = byJointName || 'Hips';
+
+ var dopplePosition = Entities.getEntityProperties(this.entityID, ["position"]);
+ var doppleJointIndex = Entities.getJointIndex( this.entityID, byJointName );
+ var doppleJointPosition = Vec3.sum(Entities.getAbsoluteJointTranslationInObjectFrame( this.entityID, doppleJointIndex ), dopplePosition);
+
+ //log("Joint Pos = " + JSON.stringify(doppleJointPosition));
+
+ var avatarPosition = this.avatar.position;
+ var avatarJointIndex = this.avatar.getJointIndex(byJointName);
+ var avatarJointPosition = Vec3.sum(this.avatar.getAbsoluteJointTranslationInObjectFrame(avatarJointIndex), avatarPosition);
+
+ //log("AV Joint Pos = " + JSON.stringify(avatarJointPosition));
+
+ dopplePosition.position.y = avatarJointPosition.y - doppleJointPosition.y;
+ this.position = dopplePosition.position;
+ Entities.editEntity(this.entityID, { position: this.position });
+ },
+
+ // @private @method - creates the update thread to synchronize joint data
+ _createUpdateThread: function() {
+ if (Doppleganger.USE_SCRIPT_UPDATE) {
+ log('creating Script.update thread');
+ this.onUpdate = this.update;
+ Script.update.connect(this, 'onUpdate');
+ } else {
+ log('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps');
+ var timeout = 1000 / Doppleganger.TARGET_FPS;
+ this._interval = Script.setInterval(bind(this, 'update'), timeout);
+ }
+ },
+
+ // @private @method - waits for model to load and handles timeouts
+ // @param {ModelResource} resource - a prefetched resource to monitor loading state against
+ _waitForModel: function(resource) {
+ var RECHECK_MS = 50;
+ var id = this.entityID,
+ watchdogTimer = null;
+
+ function waitForJointNames() {
+ var error = null, result = null;
+ if (!watchdogTimer) {
+ error = 'joints_unavailable';
+ } else if (resource.state === Resource.State.FAILED) {
+ error = 'prefetch_failed';
+ } else if (resource.state === Resource.State.FINISHED) {
+ var names = Entities.getJointNames(id);
+ if (Array.isArray(names) && names.length) {
+ result = { entityID: id, jointNames: names };
+ }
+ }
+ if (error || result !== null) {
+ Script.clearInterval(this._interval);
+ this._interval = null;
+ if (watchdogTimer) {
+ Script.clearTimeout(watchdogTimer);
+ }
+ this.addingEntity(error, result);
+ }
+ }
+ watchdogTimer = Script.setTimeout(function() {
+ watchdogTimer = null;
+ }, Doppleganger.MAX_WAIT_SECS * 1000);
+ this._interval = Script.setInterval(bind(this, waitForJointNames), RECHECK_MS);
+ }
+};
+
+// @function - bind a function to a `this` context
+// @param {Object} - the `this` context
+// @param {Function|String} - function or method name
+function bind(thiz, method) {
+ method = thiz[method] || method;
+ return function() {
+ return method.apply(thiz, arguments);
+ };
+}
+
+// @function - Qt signal polyfill
+function signal(template) {
+ var callbacks = [];
+ return Object.defineProperties(function() {
+ var args = [].slice.call(arguments);
+ callbacks.forEach(function(obj) {
+ obj.handler.apply(obj.scope, args);
+ });
+ }, {
+ connect: { value: function(scope, handler) {
+ callbacks.push({scope: scope, handler: scope[handler] || handler || scope});
+ }},
+ disconnect: { value: function(scope, handler) {
+ var match = {scope: scope, handler: scope[handler] || handler || scope};
+ callbacks = callbacks.filter(function(obj) {
+ return !(obj.scope === match.scope && obj.handler === match.handler);
+ });
+ }}
+ });
+}
+
+// @function - debug logging
+function log() {
+ //print('doppleganger | ' + [].slice.call(arguments).join(' '));
+}
+
+// -- ADVANCED DEBUGGING --
+// @function - Add debug joint indicators / extra debugging info.
+// @param {Doppleganger} - existing Doppleganger instance to add controls to
+//
+// @note:
+// * rightclick toggles mirror mode on/off
+// * shift-rightclick toggles the debug indicators on/off
+// * clicking on an indicator displays the joint name and mirrored joint name in the debug log.
+//
+// Example use:
+// var doppleganger = new Doppleganger();
+// Doppleganger.addDebugControls(doppleganger);
+Doppleganger.addDebugControls = function(doppleganger) {
+ DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 };
+ DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 };
+
+ function DebugControls() {
+ this.enableIndicators = true;
+ this.selectedJointName = null;
+ this.debugEntityIDs = undefined;
+ this.jointSelected = signal(function(result) {});
+ }
+ DebugControls.prototype = {
+ start: function() {
+ if (!this.onMousePressEvent) {
+ this.onMousePressEvent = this._onMousePressEvent;
+ Controller.mousePressEvent.connect(this, 'onMousePressEvent');
+ }
+ },
+
+ stop: function() {
+ this.removeIndicators();
+ if (this.onMousePressEvent) {
+ Controller.mousePressEvent.disconnect(this, 'onMousePressEvent');
+ delete this.onMousePressEvent;
+ }
+ },
+
+ createIndicators: function(jointNames) {
+ this.jointNames = jointNames;
+ return jointNames.map(function(name, i) {
+ return Entities.addEntity({
+ type: "Shape",
+ shape: 'Icosahedron',
+ scale: 0.1,
+ solid: false,
+ alpha: 0.5
+ });
+ });
+ },
+
+ removeIndicators: function() {
+ if (this.debugEntityIDs) {
+ this.debugEntityIDs.forEach(Entities.deleteEntity);
+ this.debugEntityIDs = undefined;
+ }
+ },
+
+ onJointsUpdated: function(entityID) {
+ if (!this.enableIndicators) {
+ return;
+ }
+ var jointNames = Entities.getJointNames(entityID),
+ jointOrientations = Entities.getEntityProperties(entityID, ['jointRotations']), //was jointOrientations
+ jointPositions = Entities.getEntityProperties(entityID, ['jointTranslations']), //was jointPositions
+ //selectedIndex = jointNames.indexOf(this.selectedJointName);
+ selectedIndex = Entities.getJointIndex( entityID, this.selectedJointName );
+
+ if (!this.debugEntityIDs) {
+ this.debugEntityIDs = this.createIndicators(jointNames);
+ }
+
+ // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API)
+ var updatedOverlays = this.debugEntityIDs.reduce(function(updates, id, i) {
+ updates[id] = {
+ position: jointPositions.jointTranslations[i],
+ rotation: jointOrientations.jointRotations[i],
+ color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT,
+ solid: i === selectedIndex
+ };
+ return updates;
+ }, {});
+ //Entities.editOverlays(updatedOverlays);
+ },
+
+ _onMousePressEvent: function(evt) {
+ if (!evt.isLeftButton || !this.enableIndicators || !this.debugEntityIDs) {
+ return;
+ }
+ var ray = Camera.computePickRay(evt.x, evt.y),
+ hit = Entities.findRayIntersection(ray, true, this.debugEntityIDs);
+
+ hit.jointIndex = this.debugEntityIDs.indexOf(hit.entityID);
+ hit.jointName = this.jointNames[hit.jointIndex];
+ this.jointSelected(hit);
+ }
+ };
+
+ if ('$debugControls' in doppleganger) {
+ throw new Error('only one set of debug controls can be added per doppleganger');
+ }
+ var debugControls = new DebugControls();
+ doppleganger.$debugControls = debugControls;
+
+ function onMousePressEvent(evt) {
+ if (evt.isRightButton) {
+ if (evt.isShifted) {
+ debugControls.enableIndicators = !debugControls.enableIndicators;
+ if (!debugControls.enableIndicators) {
+ debugControls.removeIndicators();
+ }
+ } else {
+ doppleganger.mirrored = !doppleganger.mirrored;
+ }
+ }
+ }
+
+ doppleganger.activeChanged.connect(function(active) {
+ if (active) {
+ debugControls.start();
+ doppleganger.jointsUpdated.connect(debugControls, 'onJointsUpdated');
+ Controller.mousePressEvent.connect(onMousePressEvent);
+ } else {
+ Controller.mousePressEvent.disconnect(onMousePressEvent);
+ doppleganger.jointsUpdated.disconnect(debugControls, 'onJointsUpdated');
+ debugControls.stop();
+ }
+ });
+
+ debugControls.jointSelected.connect(function(hit) {
+ debugControls.selectedJointName = hit.jointName;
+ if (hit.jointIndex < 0) {
+ return;
+ }
+ hit.mirroredJointName = Doppleganger.getMirroredJointNames([hit.jointName])[0];
+ log('selected joint:', JSON.stringify(hit, 0, 2));
+ });
+
+ Script.scriptEnding.connect(debugControls, 'removeIndicators');
+
+ return doppleganger;
+};
diff --git a/interface/resources/serverless/Scripts/wizard.html b/interface/resources/serverless/Scripts/wizard.html
new file mode 100644
index 0000000000..9011809e98
--- /dev/null
+++ b/interface/resources/serverless/Scripts/wizard.html
@@ -0,0 +1,490 @@
+
+
+
+
+
+ Quick Configuration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Welcome to Overte!
+
+
+
+
+ Let's get you setup to experience the virtual world.
+ First, we need to select some performance and graphics quality options.
+ Press Continue when you are ready.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Quality
+
+
+
+
+ What level of visual quality would you like?
+ Remember! If you do not have a powerful computer, you may want to set this to low or medium at most.
+
+
Very Low Quality Slow Laptop / Very Slow Computer
+
Low Quality Average Laptop / Slow Computer
+
Medium Quality Average Computer - Recommended
+
High Quality Gaming Computer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Performance
+
+
+
+
+ Do you want a smooth experience (high refresh rate) or do you want to conserve power and resources (low refresh rate) on your computer?
+ Note: This does not apply to virtual reality headsets.
+
+
Not Smooth (20 Hz) Conserve Power
+
Smooth (30 Hz) Use Average Resources
+
Very Smooth (60 Hz) Use Maximum Resources - Recommended
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Display Name
+
+
+
+
+ What should people call you?
+ This is simply a nickname, it will be shown in place of your username (if you have one).
+
+ NAME:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
All done!
+
+
+
+
+ Now you're almost ready to go!
+ Press Complete to save your setup.
+ Then take a look at the other information kiosks after completing this wizard.
+